mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: start working on google drive storage
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ export const NotificationChannelFormSchema = ChannelFormSchema.extend({
|
||||
});
|
||||
|
||||
export const StorageChannelFormSchema = ChannelFormSchema.extend({
|
||||
provider: z.enum(["local", "s3"], {required_error: "Provider is required"}),
|
||||
provider: z.enum(["local", "s3", "google-drive"], {required_error: "Provider is required"}),
|
||||
});
|
||||
|
||||
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
|
||||
if (result.success) {
|
||||
toast.success("Successfully connected to storage channel");
|
||||
} else {
|
||||
console.error(result);
|
||||
toast.error("An error occurred while testing the storage channel, check your configuration");
|
||||
}
|
||||
} else {
|
||||
|
||||
+370
@@ -0,0 +1,370 @@
|
||||
// import {UseFormReturn} from "react-hook-form";
|
||||
// import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||
// import {Input} from "@/components/ui/input";
|
||||
// import {Separator} from "@/components/ui/separator";
|
||||
//
|
||||
//
|
||||
// type StorageGoogleDriveFormProps = {
|
||||
// form: UseFormReturn<any, any, any>
|
||||
// }
|
||||
//
|
||||
//
|
||||
// export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
||||
// return (
|
||||
// <>
|
||||
// <Separator className="my-1"/>
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.clientEmail"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Client Email</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="xxx@xxx.iam.gserviceaccount.com"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.privateKey"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Private Key</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="xxxxxxxxxxxx"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.folderId"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Folder Id</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="xxxxxxxxxxxx"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
//
|
||||
// </>
|
||||
// )
|
||||
// }
|
||||
|
||||
// "use client";
|
||||
//
|
||||
// import {UseFormReturn} from "react-hook-form";
|
||||
// import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||
// import {Input} from "@/components/ui/input";
|
||||
// import {Separator} from "@/components/ui/separator";
|
||||
// import {Button} from "@/components/ui/button";
|
||||
// import {getServerUrl} from "@/utils/get-server-url";
|
||||
//
|
||||
// type StorageGoogleDriveFormProps = {
|
||||
// form: UseFormReturn<any, any, any>
|
||||
// };
|
||||
//
|
||||
// export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
||||
// const handleConnect = async () => {
|
||||
// const clientId = form.getValues("config.clientId");
|
||||
// // const baseUrl = getServerUrl();
|
||||
// // const redirectUri = form.getValues("config.redirectUri") || "http://localhost:3000/oauth2callback";
|
||||
// const redirectUri = "http://localhost:8887/dashboard/storages/channels";
|
||||
//
|
||||
// if (!clientId) {
|
||||
// alert("Please fill in Client ID first");
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// // Construct Google OAuth URL
|
||||
// const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file");
|
||||
// const url = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&access_type=offline&prompt=consent`;
|
||||
//
|
||||
// // Open consent screen in a new tab
|
||||
// window.open(url, "_blank");
|
||||
// };
|
||||
//
|
||||
// return (
|
||||
// <>
|
||||
// <Separator className="my-1"/>
|
||||
//
|
||||
// {/* Client ID */}
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.clientId"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Client ID</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="XXXX.apps.googleusercontent.com"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
//
|
||||
// {/* Client Secret */}
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.clientSecret"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Client Secret</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="XXXXXXXXXXXX"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
//
|
||||
// {/* Redirect URI */}
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.redirectUri"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Redirect URI</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="http://localhost:3000/oauth2callback"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
//
|
||||
// {/* Refresh Token */}
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.refreshToken"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Refresh Token</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="Paste refresh token here"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
//
|
||||
// {/* Folder ID */}
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="config.folderId"
|
||||
// render={({field}) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Folder ID</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input {...field} placeholder="XXXXXXXXXXXXXXXXXXXX"/>
|
||||
// </FormControl>
|
||||
// <FormMessage/>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
//
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="oauth.connect"
|
||||
// render={() => (
|
||||
// <FormItem>
|
||||
// <FormLabel>OAuth2 Connect</FormLabel>
|
||||
// <FormControl>
|
||||
// <Button type="button" onClick={handleConnect}>
|
||||
// Connect Google Drive
|
||||
// </Button>
|
||||
// </FormControl>
|
||||
// <FormMessage>
|
||||
// Clicking this will open Google’s consent screen. After granting access, copy the authorization code and exchange it for a refresh token in your backend.
|
||||
// </FormMessage>
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// </>
|
||||
// );
|
||||
// };
|
||||
//
|
||||
"use client";
|
||||
|
||||
import {UseFormReturn} from "react-hook-form";
|
||||
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
|
||||
import {Input} from "@/components/ui/input";
|
||||
import {Separator} from "@/components/ui/separator";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {
|
||||
googleDriveRefreshTokenAction
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers";
|
||||
|
||||
type StorageGoogleDriveFormProps = {
|
||||
form: UseFormReturn<any, any, any>;
|
||||
};
|
||||
|
||||
export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
||||
const handleConnect = () => {
|
||||
const clientId = form.getValues("config.clientId");
|
||||
const redirectUri = form.getValues("config.redirectUri") || `${window.location.origin}/api/google/drive/callback`;
|
||||
|
||||
if (!clientId) {
|
||||
alert("Please fill in Client ID first");
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file");
|
||||
const oauthUrl = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}&response_type=code&scope=${scope}&access_type=offline&prompt=consent`;
|
||||
|
||||
// Open OAuth in a new tab
|
||||
const oauthWindow = window.open(oauthUrl, "_blank", "width=500,height=600");
|
||||
|
||||
// Poll the window for the refresh token (after the redirect)
|
||||
const interval = setInterval(async () => {
|
||||
try {
|
||||
if (!oauthWindow || oauthWindow.closed) {
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if redirected to your server callback
|
||||
if (oauthWindow.location.href.startsWith(`${window.location.origin}/api/google/drive/callback`)) {
|
||||
const params = new URL(oauthWindow.location.href).searchParams;
|
||||
const code = params.get("code");
|
||||
console.log(code)
|
||||
if (code) {
|
||||
|
||||
const clientId = form.getValues("config.clientId");
|
||||
const clientSecret = form.getValues("config.clientSecret");
|
||||
const redirectUri =
|
||||
form.getValues("config.redirectUri") ||
|
||||
`${window.location.origin}/api/google/drive/callback`;
|
||||
|
||||
// Exchange code for refresh token
|
||||
const data = await googleDriveRefreshTokenAction({
|
||||
code: code,
|
||||
clientId: clientId,
|
||||
clientSecret: clientSecret,
|
||||
redirectUri: redirectUri,
|
||||
})
|
||||
|
||||
if (data.data.refreshToken) {
|
||||
console.log(data.data.refreshToken)
|
||||
form.setValue("config.refreshToken", data.data.refreshToken); // auto-update the field
|
||||
}
|
||||
|
||||
oauthWindow.close();
|
||||
clearInterval(interval);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore cross-origin errors until redirect happens
|
||||
}
|
||||
}, 500);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator className="my-1"/>
|
||||
|
||||
{/* Client ID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.clientId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="XXXX.apps.googleusercontent.com"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Client Secret */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.clientSecret"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Client Secret</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="XXXXXXXXXXXX"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Redirect URI */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.redirectUri"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Redirect URI</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="http://localhost:3000/api/google-drive/callback"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Refresh Token */}
|
||||
|
||||
{/*<FormField*/}
|
||||
{/* control={form.control}*/}
|
||||
{/* name="config.refreshToken"*/}
|
||||
{/* render={({field}) => (*/}
|
||||
{/* <FormItem>*/}
|
||||
{/* <FormLabel>Refresh Token</FormLabel>*/}
|
||||
{/* <FormControl>*/}
|
||||
{/* <Input {...field} placeholder="Will be filled after OAuth flow"/>*/}
|
||||
{/* </FormControl>*/}
|
||||
{/* <FormMessage/>*/}
|
||||
{/* </FormItem>*/}
|
||||
{/* )}*/}
|
||||
{/*/>*/}
|
||||
|
||||
{/* Folder ID */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.folderId"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Folder ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="XXXXXXXXXXXXXXXXXXXX"/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="config.refreshToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Refresh Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Will be filled after OAuth flow" readOnly />
|
||||
</FormControl>
|
||||
<FormMessage>
|
||||
The refresh token will automatically appear here after connecting.
|
||||
</FormMessage>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="button" onClick={handleConnect}> Connect Google Drive </Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
"use server"
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
|
||||
import {string, z} from "zod";
|
||||
import {google} from "googleapis";
|
||||
|
||||
|
||||
export const googleDriveRefreshTokenAction = userAction.schema(
|
||||
z.object({
|
||||
code: z.string(),
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
redirectUri: z.string(),
|
||||
})).action(async ({parsedInput}) => {
|
||||
// google.auth.OAuth2
|
||||
// const oauth2Client = new google.auth.OAuth2(parsedInput.clientId,parsedInput.clientSecret, parsedInput.redirectUri);
|
||||
// const { tokens } = await oauth2Client.getToken(parsedInput.code);
|
||||
|
||||
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
client_id: parsedInput.clientId,
|
||||
client_secret: parsedInput.clientSecret,
|
||||
code: parsedInput.code,
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: parsedInput.redirectUri,
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(tokenRes);
|
||||
|
||||
const tokens = await tokenRes.json();
|
||||
console.log(tokens.refresh_token);
|
||||
|
||||
return {
|
||||
refreshToken: tokens.refresh_token,
|
||||
};
|
||||
});
|
||||
@@ -29,6 +29,9 @@ import {LucideProps} from "lucide-react";
|
||||
import {
|
||||
StorageS3Form
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/s3.form";
|
||||
import {
|
||||
StorageGoogleDriveForm
|
||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive.form";
|
||||
|
||||
export type ChannelKind = "notification" | "storage";
|
||||
|
||||
@@ -86,6 +89,8 @@ export const renderChannelForm = (provider: string | undefined, form: UseFormRet
|
||||
return <NotifierWebhookForm form={form}/>;
|
||||
case "s3":
|
||||
return <StorageS3Form form={form}/>
|
||||
case "google-drive":
|
||||
return <StorageGoogleDriveForm form={form}/>
|
||||
case "local":
|
||||
return <></>
|
||||
default:
|
||||
|
||||
@@ -5,6 +5,7 @@ import {ProviderIconTypes} from "@/components/wrappers/dashboard/admin/channels/
|
||||
export const storageProviders: ProviderIconTypes[] = [
|
||||
{value: "local", label: "Local", icon: Server},
|
||||
{value: "s3", label: "S3", icon: S3Icon},
|
||||
{value: "google-drive", label: "Google Drive", icon: GoogleDriveIcon},
|
||||
{value: "blob", label: "Azure Blob Storage", icon: BlobIcon, preview: true},
|
||||
{value: "gcs", label: "Google Cloud Storage", icon: GCSIcon, preview: true},
|
||||
]
|
||||
@@ -18,12 +19,15 @@ export function S3Icon(props: SVGProps<SVGSVGElement>) {
|
||||
);
|
||||
}
|
||||
|
||||
export function GoogleDriveIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} {...props} viewBox="0 0 256 229"><path fill="#0066da" d="m19.354 196.034l11.29 19.5c2.346 4.106 5.718 7.332 9.677 9.678q17.009-21.591 23.68-33.137q6.77-11.717 16.641-36.655q-26.604-3.502-40.32-3.502q-13.165 0-40.322 3.502c0 4.545 1.173 9.09 3.519 13.196z"/><path fill="#ea4335" d="M215.681 225.212c3.96-2.346 7.332-5.572 9.677-9.677l4.692-8.064l22.434-38.855a26.57 26.57 0 0 0 3.518-13.196q-27.315-3.502-40.247-3.502q-13.899 0-40.248 3.502q9.754 25.075 16.422 36.655q6.724 11.683 23.752 33.137"/><path fill="#00832d" d="M128.001 73.311q19.68-23.768 27.125-36.655q5.996-10.377 13.196-33.137C164.363 1.173 159.818 0 155.126 0h-54.25C96.184 0 91.64 1.32 87.68 3.519q9.16 26.103 15.544 37.154q7.056 12.213 24.777 32.638"/><path fill="#2684fc" d="M175.36 155.42H80.642l-40.32 69.792c3.958 2.346 8.503 3.519 13.195 3.519h148.968c4.692 0 9.238-1.32 13.196-3.52z"/><path fill="#00ac47" d="M128.001 73.311L87.681 3.52c-3.96 2.346-7.332 5.571-9.678 9.677L3.519 142.224A26.57 26.57 0 0 0 0 155.42h80.642z"/><path fill="#ffba00" d="m215.242 77.71l-37.243-64.514c-2.345-4.106-5.718-7.331-9.677-9.677l-40.32 69.792l47.358 82.109h80.496c0-4.546-1.173-9.09-3.519-13.196z"/></svg>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export function BlobIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
// <svg xmlns="http://www.w3.org/2000/svg" width={256} height={199} {...props} viewBox="0 0 24 24">
|
||||
// <path fill="currentColor"
|
||||
// d="M13.207.006a2.16 2.16 0 0 0-1.62.582a2.15 2.15 0 0 0-.095 3.035l3.408 3.55a3.042 3.042 0 0 1-.663 4.688l-.463.239V7.285a15.42 15.42 0 0 0-8.018 10.487v.017l6.549-3.328v7.621L13.779 24V13.682l.897-.463a4.443 4.443 0 0 0 1.22-7.03l-3.37-3.525a.75.75 0 0 1 .037-1.055a.75.75 0 0 1 1.056.038l.467.486l-.006.006l4.07 4.244a.057.057 0 0 0 .082 0a.06.06 0 0 0 0-.07l-3.14-5.143l-.149.143l.149-.145C14.494.393 13.829.054 13.207.006m-.902 9.865v2.994l-4.152 2.149a14 14 0 0 1 2.767-3.928a14 14 0 0 1 1.385-1.215"/>
|
||||
// </svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width={256} height={199} {...props} viewBox="0 0 48 48">
|
||||
<path fill="#2979ff"
|
||||
d="M32.66,7H15.34c-0.715,0-1.375,0.381-1.732,1l-8.66,15c-0.357,0.619-0.357,1.381,0,2l8.66,15c0.357,0.619,1.018,1,1.732,1H32.66c0.715,0,1.375-0.381,1.732-1l8.66-15c0.357-0.619,0.357-1.381,0-2l-8.66-15C34.035,7.381,33.375,7,32.66,7z"></path>
|
||||
|
||||
@@ -225,6 +225,13 @@
|
||||
"when": 1768931603846,
|
||||
"tag": "0031_chemical_edwin_jarvis",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 32,
|
||||
"version": "7",
|
||||
"when": 1769203863305,
|
||||
"tag": "0032_sparkling_thunderbolt_ross",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import {z} from "zod";
|
||||
import {database} from "@/db/schema/07_database";
|
||||
|
||||
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3']);
|
||||
export const providerStorageKindEnum = pgEnum('provider_storage_kind', ['local', 's3', 'google-drive']);
|
||||
|
||||
export const storageChannel = pgTable('storage_channel', {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// import {drive_v3, google} from "googleapis";
|
||||
// import Drive = drive_v3.Drive;
|
||||
// import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||
//
|
||||
//
|
||||
// export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<Drive> {
|
||||
// const auth = new google.auth.JWT({
|
||||
// email: config.clientEmail,
|
||||
// key: config.privateKey?.replace(/\\n/g, "\n"),
|
||||
// scopes: ["https://www.googleapis.com/auth/drive"],
|
||||
// });
|
||||
//
|
||||
// return google.drive({
|
||||
// version: "v3",
|
||||
// auth,
|
||||
// });
|
||||
// }
|
||||
//
|
||||
//
|
||||
// export async function findFileByName(
|
||||
// drive: any,
|
||||
// name: string,
|
||||
// folderId: string
|
||||
// ): Promise<string | null> {
|
||||
// const res = await drive.files.list({
|
||||
// q: `name='${name}' and '${folderId}' in parents and trashed=false`,
|
||||
// fields: "files(id)",
|
||||
// pageSize: 1,
|
||||
// });
|
||||
//
|
||||
// return res.data.files?.[0]?.id ?? null;
|
||||
// }
|
||||
|
||||
import {drive_v3, google} from "googleapis";
|
||||
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||
import Drive = drive_v3.Drive;
|
||||
import {getServerUrl} from "@/utils/get-server-url";
|
||||
|
||||
export async function getGoogleDriveClient(config: GoogleDriveConfig): Promise<Drive> {
|
||||
const baseUrl = getServerUrl();
|
||||
|
||||
const oauth2Client = new google.auth.OAuth2(
|
||||
config.clientId,
|
||||
config.clientSecret,
|
||||
// config.redirectUri
|
||||
baseUrl
|
||||
);
|
||||
|
||||
oauth2Client.setCredentials({
|
||||
refresh_token: config.refreshToken
|
||||
});
|
||||
|
||||
return google.drive({
|
||||
version: "v3",
|
||||
auth: oauth2Client
|
||||
});
|
||||
}
|
||||
|
||||
export async function findFileByName(
|
||||
drive: drive_v3.Drive,
|
||||
name: string,
|
||||
folderId: string
|
||||
): Promise<string | null> {
|
||||
const res = await drive.files.list({
|
||||
q: `name='${name}' and '${folderId}' in parents and trashed=false`,
|
||||
fields: "files(id)",
|
||||
pageSize: 1,
|
||||
supportsAllDrives: true,
|
||||
includeItemsFromAllDrives: true,
|
||||
});
|
||||
|
||||
return res.data.files?.[0]?.id ?? null;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
"use server"
|
||||
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from '../../types';
|
||||
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||
import {findFileByName, getGoogleDriveClient} from "@/features/storages/providers/google-drive/helpers";
|
||||
import {Readable} from "node:stream";
|
||||
|
||||
|
||||
// export async function uploadGoogleDrive(
|
||||
// config: GoogleDriveConfig,
|
||||
// input: { data: StorageUploadInput }
|
||||
// ): Promise<StorageResult> {
|
||||
// const client = await getGoogleDriveClient(config);
|
||||
//
|
||||
// const name = input.data.path;
|
||||
//
|
||||
// const existing = await findFileByName(client, name, config.folderId);
|
||||
// if (existing) {
|
||||
// return {
|
||||
// success: false,
|
||||
// provider: "google-drive",
|
||||
// error: "File already exists"
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// await client.files.create({
|
||||
// requestBody: {
|
||||
// name,
|
||||
// parents: [config.folderId],
|
||||
// },
|
||||
// media: {
|
||||
// body: input.data.file as Buffer,
|
||||
// },
|
||||
// });
|
||||
//
|
||||
//
|
||||
// return {
|
||||
// success: true,
|
||||
// provider: 'google-drive',
|
||||
// };
|
||||
// }
|
||||
//
|
||||
// export async function getGoogleDrive(
|
||||
// config: GoogleDriveConfig,
|
||||
// input: { data: StorageGetInput }
|
||||
// ): Promise<StorageResult> {
|
||||
// const client = await getGoogleDriveClient(config);
|
||||
// const name = input.data.path;
|
||||
//
|
||||
// const fileId = await findFileByName(client, name, config.folderId);
|
||||
// if (!fileId) {
|
||||
// return {success: false, provider: "google-drive", error: "File not found"};
|
||||
// }
|
||||
//
|
||||
// const res = await client.files.get(
|
||||
// {fileId, alt: "media"},
|
||||
// {responseType: "arraybuffer"}
|
||||
// );
|
||||
//
|
||||
// return {
|
||||
// success: true,
|
||||
// provider: "google-drive",
|
||||
// file: Buffer.from(res.data as ArrayBuffer),
|
||||
// };
|
||||
// }
|
||||
//
|
||||
//
|
||||
// export async function deleteGoogleDrive(
|
||||
// config: GoogleDriveConfig,
|
||||
// input: { data: StorageDeleteInput }
|
||||
// ): Promise<StorageResult> {
|
||||
// const client = await getGoogleDriveClient(config);
|
||||
// const name = input.data.path;
|
||||
//
|
||||
// const fileId = await findFileByName(client, name, config.folderId);
|
||||
// if (!fileId) {
|
||||
// return {success: false, provider: "google-drive", error: "File not found"};
|
||||
// }
|
||||
//
|
||||
// await client.files.delete({fileId});
|
||||
//
|
||||
// return {
|
||||
// success: true,
|
||||
// provider: "google-drive"
|
||||
// };
|
||||
// }
|
||||
//
|
||||
//
|
||||
// export async function pingGoogleDrive(config: GoogleDriveConfig): Promise<StorageResult> {
|
||||
// try {
|
||||
// const drive = await getGoogleDriveClient(config);
|
||||
// const name = `ping-${Date.now()}.txt`;
|
||||
//
|
||||
// const buffer = Buffer.from("ping");
|
||||
//
|
||||
// const file = await drive.files.create({
|
||||
// requestBody: {
|
||||
// name,
|
||||
// parents: [config.folderId],
|
||||
// },
|
||||
// media: {
|
||||
// mimeType: "text/plain",
|
||||
// body: Readable.from(buffer),
|
||||
// },
|
||||
// fields: "id",
|
||||
// });
|
||||
// console.log(file)
|
||||
//
|
||||
// await drive.files.get({fileId: file.data.id!});
|
||||
// await drive.files.delete({fileId: file.data.id!});
|
||||
//
|
||||
// return {
|
||||
// success: true,
|
||||
// provider: "google-drive",
|
||||
// response: "Google Drive storage OK",
|
||||
// };
|
||||
// } catch (err: any) {
|
||||
// return {
|
||||
// success: false,
|
||||
// provider: "google-drive",
|
||||
// response: err.message,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
export async function uploadGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageUploadInput }
|
||||
): Promise<StorageResult> {
|
||||
const client = await getGoogleDriveClient(config);
|
||||
const name = input.data.path;
|
||||
|
||||
const existing = await findFileByName(client, name, config.folderId);
|
||||
if (existing) return {success: false, provider: "google-drive", error: "File already exists"};
|
||||
|
||||
await client.files.create({
|
||||
requestBody: {name, parents: [config.folderId]},
|
||||
media: {body: Readable.from(input.data.file as Buffer)},
|
||||
fields: "id",
|
||||
supportsAllDrives: true,
|
||||
});
|
||||
|
||||
return {success: true, provider: 'google-drive'};
|
||||
}
|
||||
|
||||
export async function getGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageGetInput }
|
||||
): Promise<StorageResult> {
|
||||
const client = await getGoogleDriveClient(config);
|
||||
const name = input.data.path;
|
||||
|
||||
const fileId = await findFileByName(client, name, config.folderId);
|
||||
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
||||
|
||||
const res = await client.files.get(
|
||||
{fileId, alt: "media", supportsAllDrives: true},
|
||||
{responseType: "arraybuffer"}
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "google-drive",
|
||||
file: Buffer.from(res.data as ArrayBuffer),
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageDeleteInput }
|
||||
): Promise<StorageResult> {
|
||||
const client = await getGoogleDriveClient(config);
|
||||
const name = input.data.path;
|
||||
|
||||
const fileId = await findFileByName(client, name, config.folderId);
|
||||
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
||||
|
||||
await client.files.delete({fileId, supportsAllDrives: true});
|
||||
|
||||
return {success: true, provider: "google-drive"};
|
||||
}
|
||||
|
||||
export async function pingGoogleDrive(config: GoogleDriveConfig): Promise<StorageResult> {
|
||||
try {
|
||||
const drive = await getGoogleDriveClient(config);
|
||||
const name = `ping-${Date.now()}.txt`;
|
||||
const buffer = Buffer.from("ping");
|
||||
|
||||
const file = await drive.files.create({
|
||||
requestBody: {name, parents: [config.folderId]},
|
||||
media: {mimeType: "text/plain", body: Readable.from(buffer)},
|
||||
fields: "id",
|
||||
supportsAllDrives: true,
|
||||
});
|
||||
|
||||
await drive.files.get({fileId: file.data.id!, supportsAllDrives: true});
|
||||
// await drive.files.delete({fileId: file.data.id!, supportsAllDrives: true});
|
||||
|
||||
return {success: true, provider: "google-drive", response: "Google Drive storage OK"};
|
||||
} catch (err: any) {
|
||||
return {success: false, provider: "google-drive", response: err.message};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
// export type GoogleDriveConfig = {
|
||||
// clientEmail: string;
|
||||
// privateKey: string;
|
||||
// folderId: string;
|
||||
// };
|
||||
|
||||
|
||||
export type GoogleDriveConfig = {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
refreshToken: string; // from OAuth flow
|
||||
// redirectUri: string; // e.g., http://localhost:3000/oauth2callback
|
||||
folderId: string; // target folder in your Drive
|
||||
};
|
||||
@@ -6,6 +6,12 @@ import type {
|
||||
|
||||
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
||||
import {deleteS3, getS3, pingS3, uploadS3} from "@/features/storages/providers/s3";
|
||||
import {
|
||||
deleteGoogleDrive,
|
||||
getGoogleDrive,
|
||||
pingGoogleDrive,
|
||||
uploadGoogleDrive
|
||||
} from "@/features/storages/providers/google-drive";
|
||||
|
||||
type ProviderHandler = {
|
||||
upload: (config: any, input: StorageInput & { action: 'upload' }) => Promise<StorageResult>;
|
||||
@@ -27,6 +33,12 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
||||
delete: deleteS3,
|
||||
ping: pingS3,
|
||||
},
|
||||
"google-drive": {
|
||||
upload: uploadGoogleDrive,
|
||||
get: getGoogleDrive,
|
||||
delete: deleteGoogleDrive,
|
||||
ping: pingGoogleDrive,
|
||||
}
|
||||
// gcs: null as any,
|
||||
// azure: null as any,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type StorageProviderKind =
|
||||
| 'local'
|
||||
| 's3'
|
||||
| 'google-drive'
|
||||
;
|
||||
|
||||
export type StorageAction =
|
||||
|
||||
Reference in New Issue
Block a user