mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: Working version with google drive storage.
This commit is contained in:
@@ -1,16 +1,12 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {Body} from "./route";
|
import {Body} from "./route";
|
||||||
import {isUuidv4} from "@/utils/verify-uuid";
|
import {isUuidv4} from "@/utils/verify-uuid";
|
||||||
import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
|
|
||||||
import {Agent} from "@/db/schema/08_agent";
|
import {Agent} from "@/db/schema/08_agent";
|
||||||
import {Database} from "@/db/schema/07_database";
|
import {Database} from "@/db/schema/07_database";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {db as dbClient} from "@/db";
|
import {db as dbClient} from "@/db";
|
||||||
import {and, eq, inArray} from "drizzle-orm";
|
import {and, eq, inArray} from "drizzle-orm";
|
||||||
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
|
||||||
import {ServerActionResult} from "@/types/action-type";
|
|
||||||
import {SafeActionResult} from "next-safe-action";
|
|
||||||
import {ZodString} from "zod";
|
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import type {StorageInput} from "@/features/storages/types";
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
@@ -83,10 +79,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
||||||
// const backup = await dbClient.query.backup.findFirst({
|
|
||||||
// where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
|
|
||||||
// })
|
|
||||||
|
|
||||||
const activeBackup = await dbClient.query.backup.findFirst({
|
const activeBackup = await dbClient.query.backup.findFirst({
|
||||||
where: and(
|
where: and(
|
||||||
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
|
||||||
@@ -116,25 +108,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
restoreAction = true
|
restoreAction = true
|
||||||
|
|
||||||
|
|
||||||
// const backupToRestore = await dbClient.query.backup.findFirst({
|
|
||||||
// where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
|
|
||||||
// with: {
|
|
||||||
// database: {
|
|
||||||
// with: {
|
|
||||||
// project: true
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
|
||||||
// if (!settings) {
|
|
||||||
// return NextResponse.json(
|
|
||||||
// {error: "Unable to find settings"},
|
|
||||||
// {status: 500}
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
|
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
|
||||||
restoreAction = false
|
restoreAction = false
|
||||||
continue;
|
continue;
|
||||||
@@ -147,6 +120,10 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
|
|||||||
path: restoration.backupStorage.path,
|
path: restoration.backupStorage.path,
|
||||||
signedUrl: true,
|
signedUrl: true,
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: restoration.backupStorage.storageChannelId,
|
||||||
|
fileKind: "backups"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import {db} from "@/db";
|
|
||||||
import {eq} from "drizzle-orm";
|
|
||||||
import * as drizzleDb from "@/db";
|
|
||||||
import type {StorageInput} from "@/features/storages/types";
|
import type {StorageInput} from "@/features/storages/types";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
@@ -15,17 +12,10 @@ export async function GET(
|
|||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
const expires = searchParams.get('expires');
|
const expires = searchParams.get('expires');
|
||||||
const pathFromUrl = searchParams.get('path');
|
const pathFromUrl = searchParams.get('path');
|
||||||
|
const storageId = searchParams.get('storageId');
|
||||||
|
|
||||||
if (!pathFromUrl) {
|
if (!pathFromUrl || !storageId) {
|
||||||
return NextResponse.json({error: "Missing file path in search params"}, {status: 404})
|
return NextResponse.json({error: "Missing search params"}, {status: 404})
|
||||||
}
|
|
||||||
|
|
||||||
const localStorageChannel = await db.query.storageChannel.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.storageChannel.provider, "local"),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!localStorageChannel) {
|
|
||||||
return NextResponse.json({error: "No local storage channel found"})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const input: StorageInput = {
|
const input: StorageInput = {
|
||||||
@@ -34,14 +24,18 @@ export async function GET(
|
|||||||
path: pathFromUrl,
|
path: pathFromUrl,
|
||||||
signedUrl: true,
|
signedUrl: true,
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: storageId,
|
||||||
|
fileKind: "backups",
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.debug(input);
|
console.debug(input);
|
||||||
|
|
||||||
const result = await dispatchStorage(input, undefined, localStorageChannel.id);
|
const result = await dispatchStorage(input, undefined, storageId);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return NextResponse.json({error: "Enable to get file from local storage channel, an error occurred !"})
|
return NextResponse.json({error: "Enable to get file from privided storage channel, an error occurred !"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import {NextResponse} from "next/server";
|
import {NextResponse} from "next/server";
|
||||||
import {auth} from "@/lib/auth/auth";
|
import {auth} from "@/lib/auth/auth";
|
||||||
import {headers} from "next/headers";
|
import {headers} from "next/headers";
|
||||||
import {checkFileExistsInBucket, getObjectFromClient} from "@/utils/s3-file-management";
|
|
||||||
import {env} from "@/env.mjs";
|
|
||||||
import * as stream from "node:stream";
|
|
||||||
import path from "path";
|
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
@@ -16,28 +12,34 @@ export async function GET(
|
|||||||
req: Request,
|
req: Request,
|
||||||
{params}: { params: Promise<{ fileName: string }> }
|
{params}: { params: Promise<{ fileName: string }> }
|
||||||
) {
|
) {
|
||||||
|
const {searchParams} = new URL(req.url);
|
||||||
|
|
||||||
const fileName = (await params).fileName;
|
const fileName = (await params).fileName;
|
||||||
|
const storageId = searchParams.get('storageId');
|
||||||
|
|
||||||
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
|
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
|
||||||
|
|
||||||
const session = await auth.api.getSession({headers: await headers()});
|
const session = await auth.api.getSession({headers: await headers()});
|
||||||
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
|
||||||
|
|
||||||
const settings = await db.query.setting.findFirst({
|
|
||||||
where: eq(drizzleDb.schemas.setting.name, "system"),
|
|
||||||
with: {
|
|
||||||
storageChannel: true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!settings || !settings.storageChannel) {
|
if (!storageId) {
|
||||||
return NextResponse.json({error: "Unable to get settings or no default storage channel"});
|
return NextResponse.json({error: "Missing storageId in search params"}, {status: 404})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const settings = await db.query.setting.findFirst({
|
||||||
|
// where: eq(drizzleDb.schemas.setting.name, "system"),
|
||||||
|
// with: {
|
||||||
|
// storageChannel: true
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// if (!settings || !settings.storageChannel) {
|
||||||
|
// return NextResponse.json({error: "Unable to get settings or no default storage channel"});
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
const ext = fileName.split(".").pop()?.toLowerCase();
|
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||||
const contentType =
|
const contentType =
|
||||||
ext === "png"
|
ext === "png"
|
||||||
@@ -58,10 +60,14 @@ export async function GET(
|
|||||||
action: "get",
|
action: "get",
|
||||||
data: {
|
data: {
|
||||||
path: path,
|
path: path,
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: storageId,
|
||||||
|
fileKind: "images"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
|
const result = await dispatchStorage(input, undefined, storageId);
|
||||||
|
|
||||||
if (!result.file || !Buffer.isBuffer(result.file)) {
|
if (!result.file || !Buffer.isBuffer(result.file)) {
|
||||||
console.error(`An error occurred while getting file :`, result);
|
console.error(`An error occurred while getting file :`, result);
|
||||||
@@ -1,31 +1,23 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { google } from "googleapis";
|
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
const code = url.searchParams.get("code");
|
const code = url.searchParams.get("code");
|
||||||
const clientId = url.searchParams.get("clientId");
|
|
||||||
const clientSecret = url.searchParams.get("clientSecret");
|
|
||||||
const redirectUri = url.searchParams.get("redirectUri");
|
|
||||||
|
|
||||||
if (!code || !clientId || !clientSecret || !redirectUri) {
|
const success = Boolean(code);
|
||||||
return NextResponse.json({ error: "Missing parameters" }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
return new Response(
|
||||||
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
`
|
||||||
const { tokens } = await oauth2Client.getToken(code);
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
// Save tokens.refresh_token securely in your DB here
|
<body>
|
||||||
|
<h1>${success ? "Success" : "Failed"}</h1>
|
||||||
return NextResponse.json(tokens);
|
</body>
|
||||||
} catch (err: any) {
|
</html>
|
||||||
console.error("Error exchanging code:", err);
|
`,
|
||||||
return NextResponse.json({ error: "Failed to exchange code" }, { status: 500 });
|
{
|
||||||
}
|
status: success ? 200 : 400,
|
||||||
} catch (error) {
|
headers: {
|
||||||
console.error("Error in GET handler:", error);
|
"Content-Type": "text/html; charset=utf-8",
|
||||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
},
|
||||||
}
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ function checkRouteExists(pathname: string) {
|
|||||||
/^\/api\/agent\/[^/]+\/status\/?$/,
|
/^\/api\/agent\/[^/]+\/status\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
/^\/api\/agent\/[^/]+\/backup\/?$/,
|
||||||
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
/^\/api\/agent\/[^/]+\/restore\/?$/,
|
||||||
/^\/api\/files\/?$/,
|
/^\/api\/files\/images\/[^/]+\/?$/,
|
||||||
/^\/api\/images\/[^/]+\/?$/,
|
/^\/api\/files\/backups\/?$/,
|
||||||
/^\/api\/events\/?$/,
|
/^\/api\/events\/?$/,
|
||||||
/^\/api\/init\/?$/,
|
/^\/api\/init\/?$/,
|
||||||
/^\/api\/config\/?$/,
|
/^\/api\/config\/?$/,
|
||||||
|
|||||||
@@ -251,3 +251,4 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
|
|||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+60
-279
@@ -1,195 +1,3 @@
|
|||||||
// 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";
|
"use client";
|
||||||
|
|
||||||
import {UseFormReturn} from "react-hook-form";
|
import {UseFormReturn} from "react-hook-form";
|
||||||
@@ -197,33 +5,42 @@ import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/compon
|
|||||||
import {Input} from "@/components/ui/input";
|
import {Input} from "@/components/ui/input";
|
||||||
import {Separator} from "@/components/ui/separator";
|
import {Separator} from "@/components/ui/separator";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button} from "@/components/ui/button";
|
||||||
|
import {PasswordInput} from "@/components/ui/password-input";
|
||||||
import {
|
import {
|
||||||
googleDriveRefreshTokenAction
|
googleDriveRefreshTokenAction
|
||||||
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers";
|
} from "@/components/wrappers/dashboard/admin/channels/channel/channel-form/providers/storages/forms/google-drive/helpers";
|
||||||
|
import {toast} from "sonner";
|
||||||
|
|
||||||
type StorageGoogleDriveFormProps = {
|
type StorageGoogleDriveFormProps = {
|
||||||
form: UseFormReturn<any, any, any>;
|
form: UseFormReturn<any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
||||||
|
const refreshToken = form.watch("config.refreshToken");
|
||||||
|
const isConnected = Boolean(refreshToken);
|
||||||
|
|
||||||
const handleConnect = () => {
|
const handleConnect = () => {
|
||||||
const clientId = form.getValues("config.clientId");
|
const clientId = form.getValues("config.clientId");
|
||||||
const redirectUri = form.getValues("config.redirectUri") || `${window.location.origin}/api/google/drive/callback`;
|
const clientSecret = form.getValues("config.clientSecret");
|
||||||
|
const redirectUri = `${window.location.origin}/api/google/drive/callback`;
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId || !clientSecret) {
|
||||||
alert("Please fill in Client ID first");
|
form.setError("config.clientId", {message: "Client ID and Secret are required"});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = encodeURIComponent("https://www.googleapis.com/auth/drive.file");
|
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(
|
const oauthUrl =
|
||||||
redirectUri
|
`https://accounts.google.com/o/oauth2/v2/auth` +
|
||||||
)}&response_type=code&scope=${scope}&access_type=offline&prompt=consent`;
|
`?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");
|
const oauthWindow = window.open(oauthUrl, "_blank", "width=500,height=600");
|
||||||
|
|
||||||
// Poll the window for the refresh token (after the redirect)
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
if (!oauthWindow || oauthWindow.closed) {
|
if (!oauthWindow || oauthWindow.closed) {
|
||||||
@@ -231,48 +48,49 @@ export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if redirected to your server callback
|
if (oauthWindow.location.href.startsWith(redirectUri)) {
|
||||||
if (oauthWindow.location.href.startsWith(`${window.location.origin}/api/google/drive/callback`)) {
|
const code = new URL(oauthWindow.location.href).searchParams.get("code");
|
||||||
const params = new URL(oauthWindow.location.href).searchParams;
|
if (!code) return;
|
||||||
const code = params.get("code");
|
|
||||||
console.log(code)
|
|
||||||
if (code) {
|
|
||||||
|
|
||||||
const clientId = form.getValues("config.clientId");
|
const result = await googleDriveRefreshTokenAction({
|
||||||
const clientSecret = form.getValues("config.clientSecret");
|
code,
|
||||||
const redirectUri =
|
clientId,
|
||||||
form.getValues("config.redirectUri") ||
|
clientSecret,
|
||||||
`${window.location.origin}/api/google/drive/callback`;
|
redirectUri,
|
||||||
|
});
|
||||||
|
const inner = result?.data;
|
||||||
|
|
||||||
// Exchange code for refresh token
|
if (inner?.success) {
|
||||||
const data = await googleDriveRefreshTokenAction({
|
toast.success(inner.actionSuccess?.message);
|
||||||
code: code,
|
|
||||||
clientId: clientId,
|
|
||||||
clientSecret: clientSecret,
|
|
||||||
redirectUri: redirectUri,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (data.data.refreshToken) {
|
if (!inner.value) {
|
||||||
console.log(data.data.refreshToken)
|
form.setError("config", {message: "OAuth succeeded but no refresh token returned"});
|
||||||
form.setValue("config.refreshToken", data.data.refreshToken); // auto-update the field
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form.setValue("config.refreshToken", inner.value, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
toast.error(inner?.actionError?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
oauthWindow.close();
|
oauthWindow.close();
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
}
|
} catch {
|
||||||
} catch (err) {
|
// Ignore cross-origin errors until redirect
|
||||||
// ignore cross-origin errors until redirect happens
|
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-1"/>
|
<Separator className="my-1"/>
|
||||||
|
|
||||||
{/* Client ID */}
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.clientId"
|
name="config.clientId"
|
||||||
@@ -280,14 +98,13 @@ export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Client ID</FormLabel>
|
<FormLabel>Client ID</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="XXXX.apps.googleusercontent.com"/>
|
<Input {...field} placeholder="xxxx.apps.googleusercontent.com"/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Client Secret */}
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.clientSecret"
|
name="config.clientSecret"
|
||||||
@@ -295,45 +112,13 @@ export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Client Secret</FormLabel>
|
<FormLabel>Client Secret</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="XXXXXXXXXXXX"/>
|
<PasswordInput {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="config.folderId"
|
name="config.folderId"
|
||||||
@@ -341,30 +126,26 @@ export const StorageGoogleDriveForm = ({form}: StorageGoogleDriveFormProps) => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Folder ID</FormLabel>
|
<FormLabel>Folder ID</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} placeholder="XXXXXXXXXXXXXXXXXXXX"/>
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<input type="hidden" {...form.register("config.refreshToken")} />
|
||||||
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>
|
<div className="flex items-center gap-3">
|
||||||
|
<Button type="button" onClick={handleConnect}>
|
||||||
|
{isConnected ? "Reconnect Google Drive" : "Connect Google Drive"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{isConnected && (
|
||||||
|
<span className="text-sm text-green-600 font-medium">
|
||||||
|
Google Drive connected
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+28
-15
@@ -1,8 +1,7 @@
|
|||||||
"use server"
|
"use server"
|
||||||
import {userAction} from "@/lib/safe-actions/actions";
|
import {userAction} from "@/lib/safe-actions/actions";
|
||||||
|
import {z} from "zod";
|
||||||
import {string, z} from "zod";
|
import {ServerActionResult} from "@/types/action-type";
|
||||||
import {google} from "googleapis";
|
|
||||||
|
|
||||||
|
|
||||||
export const googleDriveRefreshTokenAction = userAction.schema(
|
export const googleDriveRefreshTokenAction = userAction.schema(
|
||||||
@@ -11,29 +10,43 @@ export const googleDriveRefreshTokenAction = userAction.schema(
|
|||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string(),
|
clientSecret: z.string(),
|
||||||
redirectUri: z.string(),
|
redirectUri: z.string(),
|
||||||
})).action(async ({parsedInput}) => {
|
})).action(async ({parsedInput}): Promise<ServerActionResult<string>> => {
|
||||||
// google.auth.OAuth2
|
const {code, clientId, clientSecret, redirectUri} = parsedInput;
|
||||||
// const oauth2Client = new google.auth.OAuth2(parsedInput.clientId,parsedInput.clientSecret, parsedInput.redirectUri);
|
|
||||||
// const { tokens } = await oauth2Client.getToken(parsedInput.code);
|
try {
|
||||||
|
|
||||||
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
const tokenRes = await fetch("https://oauth2.googleapis.com/token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
client_id: parsedInput.clientId,
|
client_id: clientId,
|
||||||
client_secret: parsedInput.clientSecret,
|
client_secret: clientSecret,
|
||||||
code: parsedInput.code,
|
code: code,
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
redirect_uri: parsedInput.redirectUri,
|
redirect_uri: redirectUri,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(tokenRes);
|
|
||||||
|
|
||||||
const tokens = await tokenRes.json();
|
const tokens = await tokenRes.json();
|
||||||
console.log(tokens.refresh_token);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
refreshToken: tokens.refresh_token,
|
success: true,
|
||||||
|
value: tokens.refresh_token,
|
||||||
|
actionSuccess: {
|
||||||
|
message: "Refresh token successfully fetched",
|
||||||
|
messageParams: {code: code},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
}catch {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
actionError: {
|
||||||
|
message: "An error occurred",
|
||||||
|
status: 404,
|
||||||
|
messageParams: {code: code},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export const downloadBackupAction = userAction.schema(
|
|||||||
path: backupStorage.path,
|
path: backupStorage.path,
|
||||||
signedUrl: true,
|
signedUrl: true,
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: backupStorage.storageChannelId,
|
||||||
|
fileKind: "backups"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(input)
|
console.log(input)
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ export async function dispatchStorage(
|
|||||||
return await dispatchViaProvider(
|
return await dispatchViaProvider(
|
||||||
channel.provider as StorageProviderKind,
|
channel.provider as StorageProviderKind,
|
||||||
channel.config,
|
channel.config,
|
||||||
input
|
input,
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
||||||
import {dispatchStorage} from "@/features/storages/dispatch";
|
import {dispatchStorage} from "@/features/storages/dispatch";
|
||||||
import type {StorageInput, StorageResult} from "@/features/storages/types";
|
import type {
|
||||||
|
StorageGetInput,
|
||||||
|
StorageInput,
|
||||||
|
StorageMetaData,
|
||||||
|
StorageResult,
|
||||||
|
StorageUploadInput
|
||||||
|
} from "@/features/storages/types";
|
||||||
import * as drizzleDb from "@/db";
|
import * as drizzleDb from "@/db";
|
||||||
import {withUpdatedAt} from "@/db/utils";
|
import {withUpdatedAt} from "@/db/utils";
|
||||||
import {eq} from "drizzle-orm";
|
import {eq} from "drizzle-orm";
|
||||||
import {db} from "@/db";
|
import {db} from "@/db";
|
||||||
import {createHash} from "crypto";
|
import crypto, {createHash} from "crypto";
|
||||||
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
function computeChecksum(buffer: Buffer): string {
|
function computeChecksum(buffer: Buffer): string {
|
||||||
return createHash("sha256").update(buffer).digest("hex");
|
return createHash("sha256").update(buffer).digest("hex");
|
||||||
@@ -114,3 +122,34 @@ export async function storeBackupFiles(
|
|||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export async function generateFileUrl(input: { data: StorageGetInput | StorageUploadInput, metadata?: StorageMetaData }): Promise<string | null> {
|
||||||
|
const fileName = path.basename(input.data.path);
|
||||||
|
const baseUrl = getServerUrl();
|
||||||
|
const metadata = input.metadata;
|
||||||
|
|
||||||
|
if (!metadata){
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let params = new URLSearchParams({
|
||||||
|
storageId: metadata.storageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (metadata.fileKind === "backups") {
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const expiresAt = Date.now() + 60 * 1000;
|
||||||
|
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
|
||||||
|
|
||||||
|
params.set("path", input.data.path);
|
||||||
|
params.set("token", token);
|
||||||
|
params.set("expires", expiresAt.toString());
|
||||||
|
return `${baseUrl}/api/files/${metadata.fileKind}/?${params.toString()}`
|
||||||
|
}else if (metadata.fileKind === "images") {
|
||||||
|
return `${baseUrl}/api/files/${metadata.fileKind}/${fileName}?${params.toString()}`
|
||||||
|
}else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,3 +71,65 @@ export async function findFileByName(
|
|||||||
|
|
||||||
return res.data.files?.[0]?.id ?? null;
|
return res.data.files?.[0]?.id ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function ensureFolderPath(client: any, path: string, rootFolderId: string): Promise<string> {
|
||||||
|
const parts = path.split("/").filter(Boolean); // ["backups", "project-1"]
|
||||||
|
let parentId = rootFolderId;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const res = await client.files.list({
|
||||||
|
q: `'${parentId}' in parents and name='${part}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
|
||||||
|
fields: "files(id, name)",
|
||||||
|
supportsAllDrives: true,
|
||||||
|
includeItemsFromAllDrives: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data.files && res.data.files.length > 0) {
|
||||||
|
parentId = res.data.files[0].id!;
|
||||||
|
} else {
|
||||||
|
const folder = await client.files.create({
|
||||||
|
requestBody: {
|
||||||
|
name: part,
|
||||||
|
mimeType: "application/vnd.google-apps.folder",
|
||||||
|
parents: [parentId],
|
||||||
|
},
|
||||||
|
fields: "id",
|
||||||
|
supportsAllDrives: true,
|
||||||
|
});
|
||||||
|
parentId = folder.data.id!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function resolveFilePath(client: any, fullPath: string, rootFolderId: string): Promise<string | null> {
|
||||||
|
const parts = fullPath.split("/").filter(Boolean);
|
||||||
|
const fileName = parts.pop()!;
|
||||||
|
let parentId = rootFolderId;
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const res = await client.files.list({
|
||||||
|
q: `'${parentId}' in parents and name='${part}' and mimeType='application/vnd.google-apps.folder' and trashed=false`,
|
||||||
|
fields: "files(id, name)",
|
||||||
|
supportsAllDrives: true,
|
||||||
|
includeItemsFromAllDrives: true,
|
||||||
|
});
|
||||||
|
if (res.data.files && res.data.files.length > 0) {
|
||||||
|
parentId = res.data.files[0].id!;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileRes = await client.files.list({
|
||||||
|
q: `'${parentId}' in parents and name='${fileName}' and trashed=false`,
|
||||||
|
fields: "files(id, name)",
|
||||||
|
supportsAllDrives: true,
|
||||||
|
includeItemsFromAllDrives: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return fileRes.data.files?.[0]?.id || null;
|
||||||
|
}
|
||||||
@@ -1,154 +1,71 @@
|
|||||||
"use server"
|
"use server"
|
||||||
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from '../../types';
|
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../../types';
|
||||||
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||||
import {findFileByName, getGoogleDriveClient} from "@/features/storages/providers/google-drive/helpers";
|
import {
|
||||||
|
ensureFolderPath,
|
||||||
|
findFileByName,
|
||||||
|
getGoogleDriveClient, resolveFilePath
|
||||||
|
} from "@/features/storages/providers/google-drive/helpers";
|
||||||
import {Readable} from "node:stream";
|
import {Readable} from "node:stream";
|
||||||
|
import {generateFileUrl} from "@/features/storages/helpers";
|
||||||
|
|
||||||
|
|
||||||
// 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(
|
export async function uploadGoogleDrive(
|
||||||
config: GoogleDriveConfig,
|
config: GoogleDriveConfig,
|
||||||
input: { data: StorageUploadInput }
|
input: { data: StorageUploadInput, metadata?: StorageMetaData },
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const client = await getGoogleDriveClient(config);
|
const client = await getGoogleDriveClient(config);
|
||||||
const name = input.data.path;
|
|
||||||
|
|
||||||
const existing = await findFileByName(client, name, config.folderId);
|
const fullPath = input.data.path;
|
||||||
|
const pathParts = fullPath.split("/").filter(Boolean);
|
||||||
|
const fileName = pathParts.pop()!;
|
||||||
|
const folderPath = pathParts.join("/");
|
||||||
|
|
||||||
|
const folderId = folderPath
|
||||||
|
? await ensureFolderPath(client, folderPath, config.folderId)
|
||||||
|
: config.folderId;
|
||||||
|
|
||||||
|
|
||||||
|
const existing = await findFileByName(client, fileName, folderId);
|
||||||
if (existing) return {success: false, provider: "google-drive", error: "File already exists"};
|
if (existing) return {success: false, provider: "google-drive", error: "File already exists"};
|
||||||
|
|
||||||
|
|
||||||
await client.files.create({
|
await client.files.create({
|
||||||
requestBody: {name, parents: [config.folderId]},
|
requestBody: {name: fileName, parents: [folderId]},
|
||||||
media: {body: Readable.from(input.data.file as Buffer)},
|
media: {body: Readable.from(input.data.file as Buffer)},
|
||||||
fields: "id",
|
fields: "id",
|
||||||
supportsAllDrives: true,
|
supportsAllDrives: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {success: true, provider: 'google-drive'};
|
if (input.data.url) {
|
||||||
|
const url = await generateFileUrl(input);
|
||||||
|
if (!url) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
provider: "google-drive",
|
||||||
|
response: "Unable to get url file"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'google-drive',
|
||||||
|
url: url
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'google-drive',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getGoogleDrive(
|
export async function getGoogleDrive(
|
||||||
config: GoogleDriveConfig,
|
config: GoogleDriveConfig,
|
||||||
input: { data: StorageGetInput }
|
input: { data: StorageGetInput, metadata: StorageMetaData },
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const client = await getGoogleDriveClient(config);
|
const client = await getGoogleDriveClient(config);
|
||||||
const name = input.data.path;
|
|
||||||
|
|
||||||
const fileId = await findFileByName(client, name, config.folderId);
|
const fileId = await resolveFilePath(client, input.data.path, config.folderId);
|
||||||
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
||||||
|
|
||||||
const res = await client.files.get(
|
const res = await client.files.get(
|
||||||
@@ -156,6 +73,26 @@ export async function getGoogleDrive(
|
|||||||
{responseType: "arraybuffer"}
|
{responseType: "arraybuffer"}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
if (input.data.signedUrl) {
|
||||||
|
const url = await generateFileUrl(input);
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
provider: "google-drive",
|
||||||
|
response: "Unable to get url"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: "google-drive",
|
||||||
|
file: Buffer.from(res.data as ArrayBuffer),
|
||||||
|
url: url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: "google-drive",
|
provider: "google-drive",
|
||||||
@@ -165,12 +102,10 @@ export async function getGoogleDrive(
|
|||||||
|
|
||||||
export async function deleteGoogleDrive(
|
export async function deleteGoogleDrive(
|
||||||
config: GoogleDriveConfig,
|
config: GoogleDriveConfig,
|
||||||
input: { data: StorageDeleteInput }
|
input: { data: StorageDeleteInput, metadata?: StorageMetaData },
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const client = await getGoogleDriveClient(config);
|
const client = await getGoogleDriveClient(config);
|
||||||
const name = input.data.path;
|
const fileId = await resolveFilePath(client, input.data.path, config.folderId);
|
||||||
|
|
||||||
const fileId = await findFileByName(client, name, config.folderId);
|
|
||||||
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
if (!fileId) return {success: false, provider: "google-drive", error: "File not found"};
|
||||||
|
|
||||||
await client.files.delete({fileId, supportsAllDrives: true});
|
await client.files.delete({fileId, supportsAllDrives: true});
|
||||||
@@ -192,7 +127,7 @@ export async function pingGoogleDrive(config: GoogleDriveConfig): Promise<Storag
|
|||||||
});
|
});
|
||||||
|
|
||||||
await drive.files.get({fileId: file.data.id!, supportsAllDrives: true});
|
await drive.files.get({fileId: file.data.id!, supportsAllDrives: true});
|
||||||
// await drive.files.delete({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"};
|
return {success: true, provider: "google-drive", response: "Google Drive storage OK"};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
// export type GoogleDriveConfig = {
|
// export type GoogleDriveConfig = {
|
||||||
// clientEmail: string;
|
// clientEmail: string;
|
||||||
// privateKey: string;
|
// privateKey: string;
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
export type GoogleDriveConfig = {
|
export type GoogleDriveConfig = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
refreshToken: string; // from OAuth flow
|
refreshToken: string;
|
||||||
// redirectUri: string; // e.g., http://localhost:3000/oauth2callback
|
folderId: string;
|
||||||
folderId: string; // target folder in your Drive
|
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {
|
import {
|
||||||
StorageProviderKind,
|
StorageProviderKind,
|
||||||
StorageInput,
|
StorageInput,
|
||||||
StorageResult,
|
StorageResult, StorageMetaData,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
||||||
@@ -46,7 +46,7 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
|||||||
export async function dispatchViaProvider(
|
export async function dispatchViaProvider(
|
||||||
kind: StorageProviderKind,
|
kind: StorageProviderKind,
|
||||||
config: any,
|
config: any,
|
||||||
input: StorageInput
|
input: StorageInput,
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const provider = handlers[kind];
|
const provider = handlers[kind];
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
"use server"
|
"use server"
|
||||||
import {mkdir, writeFile, unlink, readFile} from 'fs/promises';
|
import {mkdir, writeFile, unlink, readFile} from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import {StorageDeleteInput, StorageGetInput, StorageResult, StorageUploadInput} from '../types';
|
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../types';
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import {getServerUrl} from "@/utils/get-server-url";
|
import {getServerUrl} from "@/utils/get-server-url";
|
||||||
|
import {generateFileUrl} from "@/features/storages/helpers";
|
||||||
|
|
||||||
const BASE_DIR = "/private/uploads/";
|
const BASE_DIR = "/private/uploads/";
|
||||||
|
|
||||||
export async function uploadLocal(
|
export async function uploadLocal(
|
||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageUploadInput }
|
input: { data: StorageUploadInput, metadata?: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
const base = config.baseDir || BASE_DIR;
|
||||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||||
@@ -18,18 +19,35 @@ export async function uploadLocal(
|
|||||||
|
|
||||||
await mkdir(dir, {recursive: true});
|
await mkdir(dir, {recursive: true});
|
||||||
await writeFile(fullPath, input.data.file);
|
await writeFile(fullPath, input.data.file);
|
||||||
const baseUrl = getServerUrl();
|
|
||||||
|
|
||||||
|
|
||||||
|
if (input.data.url) {
|
||||||
|
const url = await generateFileUrl(input);
|
||||||
|
if (!url) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
provider: "local",
|
||||||
|
response: "Unable to get url file"
|
||||||
|
};
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
provider: 'local',
|
provider: 'local',
|
||||||
url: `${baseUrl}/api/${input.data.path}`,
|
url: url
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: 'local',
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLocal(
|
export async function getLocal(
|
||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageGetInput }
|
input: { data: StorageGetInput, metadata: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
const base = config.baseDir || BASE_DIR;
|
||||||
const filePath = path.join(process.cwd(), base, input.data.path)
|
const filePath = path.join(process.cwd(), base, input.data.path)
|
||||||
@@ -46,37 +64,35 @@ export async function getLocal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (input.data.signedUrl) {
|
if (input.data.signedUrl) {
|
||||||
const crypto = require("crypto");
|
const url = await generateFileUrl(input);
|
||||||
const baseUrl = getServerUrl();
|
|
||||||
|
|
||||||
const expiresAt = Date.now() + 60 * 1000;
|
|
||||||
const token = crypto.createHash("sha256").update(`${fileName}${expiresAt}`).digest("hex");
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
path: input.data.path,
|
|
||||||
token,
|
|
||||||
expires: expiresAt.toString(),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
provider: 'local',
|
provider: "local",
|
||||||
file: file,
|
response: "Unable to get url file"
|
||||||
url: `${baseUrl}/api/files/?${params.toString()}`,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
provider: 'local',
|
|
||||||
file: file,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: "local",
|
||||||
|
file: file,
|
||||||
|
url: url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
provider: "local",
|
||||||
|
file: file,
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteLocal(
|
export async function deleteLocal(
|
||||||
config: { baseDir?: string },
|
config: { baseDir?: string },
|
||||||
input: { data: StorageDeleteInput }
|
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||||
): Promise<StorageResult> {
|
): Promise<StorageResult> {
|
||||||
const base = config.baseDir || BASE_DIR;
|
const base = config.baseDir || BASE_DIR;
|
||||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||||
|
|||||||
@@ -9,9 +9,19 @@ export type StorageAction =
|
|||||||
| 'get'
|
| 'get'
|
||||||
| 'delete';
|
| 'delete';
|
||||||
|
|
||||||
|
export type StorageFileKind =
|
||||||
|
| 'backups'
|
||||||
|
| 'images'
|
||||||
|
|
||||||
|
export type StorageMetaData = {
|
||||||
|
storageId: string,
|
||||||
|
fileKind: StorageFileKind
|
||||||
|
}
|
||||||
|
|
||||||
export interface StorageUploadInput {
|
export interface StorageUploadInput {
|
||||||
path: string;
|
path: string;
|
||||||
file: Buffer | Uint8Array;
|
file: Buffer | Uint8Array;
|
||||||
|
url?: boolean;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +36,9 @@ export interface StorageDeleteInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type StorageInput =
|
export type StorageInput =
|
||||||
| { action: 'upload'; data: StorageUploadInput }
|
| { action: 'upload'; data: StorageUploadInput, metadata?: StorageMetaData }
|
||||||
| { action: 'get'; data: StorageGetInput }
|
| { action: 'get'; data: StorageGetInput, metadata: StorageMetaData}
|
||||||
| { action: 'delete'; data: StorageDeleteInput }
|
| { action: 'delete'; data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||||
| { action: 'ping'; };
|
| { action: 'ping'; };
|
||||||
|
|
||||||
export interface StorageResult {
|
export interface StorageResult {
|
||||||
|
|||||||
@@ -45,11 +45,17 @@ export const uploadUserImageAction = userAction.schema(
|
|||||||
action: "upload",
|
action: "upload",
|
||||||
data: {
|
data: {
|
||||||
path: path,
|
path: path,
|
||||||
file: buffer
|
file: buffer,
|
||||||
|
url: true
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
storageId: settings.storageChannel.id,
|
||||||
|
fileKind: "images"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
|
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
|
||||||
|
console.log(result);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user