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:
@@ -89,7 +89,8 @@ export async function dispatchStorage(
|
||||
return await dispatchViaProvider(
|
||||
channel.provider as StorageProviderKind,
|
||||
channel.config,
|
||||
input
|
||||
input,
|
||||
|
||||
);
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import {Backup, DatabaseWith} from "@/db/schema/07_database";
|
||||
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 {withUpdatedAt} from "@/db/utils";
|
||||
import {eq} from "drizzle-orm";
|
||||
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 {
|
||||
return createHash("sha256").update(buffer).digest("hex");
|
||||
@@ -114,3 +122,34 @@ export async function storeBackupFiles(
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -70,4 +70,66 @@ export async function findFileByName(
|
||||
});
|
||||
|
||||
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"
|
||||
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 {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 {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(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageUploadInput }
|
||||
input: { data: StorageUploadInput, metadata?: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
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"};
|
||||
|
||||
|
||||
await client.files.create({
|
||||
requestBody: {name, parents: [config.folderId]},
|
||||
requestBody: {name: fileName, parents: [folderId]},
|
||||
media: {body: Readable.from(input.data.file as Buffer)},
|
||||
fields: "id",
|
||||
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(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageGetInput }
|
||||
input: { data: StorageGetInput, metadata: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
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"};
|
||||
|
||||
const res = await client.files.get(
|
||||
@@ -156,6 +73,26 @@ export async function getGoogleDrive(
|
||||
{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 {
|
||||
success: true,
|
||||
provider: "google-drive",
|
||||
@@ -165,12 +102,10 @@ export async function getGoogleDrive(
|
||||
|
||||
export async function deleteGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: { data: StorageDeleteInput }
|
||||
input: { data: StorageDeleteInput, metadata?: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
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"};
|
||||
|
||||
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.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"};
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// export type GoogleDriveConfig = {
|
||||
// clientEmail: string;
|
||||
// privateKey: string;
|
||||
@@ -9,7 +8,6 @@
|
||||
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
|
||||
refreshToken: string;
|
||||
folderId: string;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {
|
||||
import {
|
||||
StorageProviderKind,
|
||||
StorageInput,
|
||||
StorageResult,
|
||||
StorageResult, StorageMetaData,
|
||||
} from '../types';
|
||||
|
||||
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
||||
@@ -46,7 +46,7 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
||||
export async function dispatchViaProvider(
|
||||
kind: StorageProviderKind,
|
||||
config: any,
|
||||
input: StorageInput
|
||||
input: StorageInput,
|
||||
): Promise<StorageResult> {
|
||||
const provider = handlers[kind];
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"use server"
|
||||
import {mkdir, writeFile, unlink, readFile} from 'fs/promises';
|
||||
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 {getServerUrl} from "@/utils/get-server-url";
|
||||
import {generateFileUrl} from "@/features/storages/helpers";
|
||||
|
||||
const BASE_DIR = "/private/uploads/";
|
||||
|
||||
export async function uploadLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageUploadInput }
|
||||
input: { data: StorageUploadInput, metadata?: StorageMetaData }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||
@@ -18,18 +19,35 @@ export async function uploadLocal(
|
||||
|
||||
await mkdir(dir, {recursive: true});
|
||||
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 {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
url: url
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
url: `${baseUrl}/api/${input.data.path}`,
|
||||
};
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
export async function getLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageGetInput }
|
||||
input: { data: StorageGetInput, metadata: StorageMetaData }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const filePath = path.join(process.cwd(), base, input.data.path)
|
||||
@@ -46,37 +64,35 @@ export async function getLocal(
|
||||
}
|
||||
|
||||
if (input.data.signedUrl) {
|
||||
const crypto = require("crypto");
|
||||
const baseUrl = getServerUrl();
|
||||
const url = await generateFileUrl(input);
|
||||
|
||||
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 {
|
||||
success: false,
|
||||
provider: "local",
|
||||
response: "Unable to get url file"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
file: file,
|
||||
url: `${baseUrl}/api/files/?${params.toString()}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
provider: "local",
|
||||
file: file,
|
||||
url: url,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
file: file,
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export async function deleteLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageDeleteInput }
|
||||
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir || BASE_DIR;
|
||||
const fullPath = path.join(process.cwd(), base, input.data.path);
|
||||
|
||||
@@ -9,9 +9,19 @@ export type StorageAction =
|
||||
| 'get'
|
||||
| 'delete';
|
||||
|
||||
export type StorageFileKind =
|
||||
| 'backups'
|
||||
| 'images'
|
||||
|
||||
export type StorageMetaData = {
|
||||
storageId: string,
|
||||
fileKind: StorageFileKind
|
||||
}
|
||||
|
||||
export interface StorageUploadInput {
|
||||
path: string;
|
||||
file: Buffer | Uint8Array;
|
||||
url?: boolean;
|
||||
contentType?: string;
|
||||
}
|
||||
|
||||
@@ -26,9 +36,9 @@ export interface StorageDeleteInput {
|
||||
}
|
||||
|
||||
export type StorageInput =
|
||||
| { action: 'upload'; data: StorageUploadInput }
|
||||
| { action: 'get'; data: StorageGetInput }
|
||||
| { action: 'delete'; data: StorageDeleteInput }
|
||||
| { action: 'upload'; data: StorageUploadInput, metadata?: StorageMetaData }
|
||||
| { action: 'get'; data: StorageGetInput, metadata: StorageMetaData}
|
||||
| { action: 'delete'; data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||
| { action: 'ping'; };
|
||||
|
||||
export interface StorageResult {
|
||||
|
||||
@@ -45,11 +45,17 @@ export const uploadUserImageAction = userAction.schema(
|
||||
action: "upload",
|
||||
data: {
|
||||
path: path,
|
||||
file: buffer
|
||||
file: buffer,
|
||||
url: true
|
||||
},
|
||||
metadata: {
|
||||
storageId: settings.storageChannel.id,
|
||||
fileKind: "images"
|
||||
}
|
||||
}
|
||||
|
||||
const result = await dispatchStorage(input, undefined, settings.storageChannel.id);
|
||||
console.log(result);
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
|
||||
Reference in New Issue
Block a user