mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: migration-tool (#269)
* feat: working on migration backup tool * feat: add migration sequences * fix: responsive for migration tool * fix: truncate database name in migration * fix: filter on databases success for migration tool --------- Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
This commit is contained in:
co-authored by
charles-gauthereau
parent
ab55ab349a
commit
ff28a80d7c
@@ -60,6 +60,7 @@ export function backupColumns(
|
||||
cell: ({row}) => {
|
||||
const reference = row.original.id
|
||||
const isImported = row.original.imported
|
||||
const isMigrated = row.original.migrated
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{reference}</span>
|
||||
@@ -68,6 +69,11 @@ export function backupColumns(
|
||||
Imported
|
||||
</BadgeC>
|
||||
)}
|
||||
{isMigrated && (
|
||||
<BadgeC variant="outline" className="bg-blue-400/10 border-blue-600/50 text-blue-600">
|
||||
Migrated
|
||||
</BadgeC>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
"use server"
|
||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../../types';
|
||||
import {
|
||||
StorageCopyInput,
|
||||
StorageDeleteInput,
|
||||
StorageGetInput,
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput
|
||||
} from '../../types';
|
||||
import {GoogleDriveConfig} from "@/features/storages/providers/google-drive/types";
|
||||
import {
|
||||
ensureFolderPath,
|
||||
@@ -144,4 +151,71 @@ export async function pingGoogleDrive(config: GoogleDriveConfig): Promise<Storag
|
||||
} catch (err: any) {
|
||||
return {success: false, provider: "google-drive", response: err.message};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function copyGoogleDrive(
|
||||
config: GoogleDriveConfig,
|
||||
input: {
|
||||
data: StorageCopyInput,
|
||||
metadata?: StorageMetaData;
|
||||
},
|
||||
): Promise<StorageResult> {
|
||||
const client = await getGoogleDriveClient(config);
|
||||
|
||||
const sourceFileId = await resolveFilePath(
|
||||
client,
|
||||
input.data.from,
|
||||
config.folderId,
|
||||
);
|
||||
|
||||
if (!sourceFileId) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "google-drive",
|
||||
error: "Source file not found",
|
||||
};
|
||||
}
|
||||
|
||||
const fullPath = input.data.to;
|
||||
const parts = fullPath.split("/").filter(Boolean);
|
||||
const fileName = parts.pop()!;
|
||||
const folderPath = parts.join("/");
|
||||
|
||||
const folderId = folderPath
|
||||
? await ensureFolderPath(client, folderPath, config.folderId)
|
||||
: config.folderId;
|
||||
|
||||
try {
|
||||
const copied = await client.files.copy({
|
||||
fileId: sourceFileId,
|
||||
requestBody: {
|
||||
name: fileName,
|
||||
parents: [folderId],
|
||||
},
|
||||
fields: "id",
|
||||
supportsAllDrives: true,
|
||||
});
|
||||
|
||||
const newFileId = copied.data.id;
|
||||
|
||||
if (!newFileId) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "google-drive",
|
||||
error: "Copy failed (no file id returned)",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "google-drive",
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "google-drive",
|
||||
error: err.message || "Copy failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import {
|
||||
StorageResult,
|
||||
} from '../types';
|
||||
|
||||
import {uploadLocal, getLocal, deleteLocal, pingLocal} from './local';
|
||||
import {deleteS3, getS3, pingS3, uploadS3} from "@/features/storages/providers/s3";
|
||||
import {uploadLocal, getLocal, deleteLocal, pingLocal, copyLocal} from './local';
|
||||
import {copyS3, deleteS3, getS3, pingS3, uploadS3} from "@/features/storages/providers/s3";
|
||||
import {
|
||||
copyGoogleDrive,
|
||||
deleteGoogleDrive,
|
||||
getGoogleDrive,
|
||||
pingGoogleDrive,
|
||||
@@ -18,6 +19,7 @@ type ProviderHandler = {
|
||||
get: (config: any, input: StorageInput & { action: 'get' }) => Promise<StorageResult>;
|
||||
delete: (config: any, input: StorageInput & { action: 'delete' }) => Promise<StorageResult>;
|
||||
ping: (config: any, input: { action: 'ping' }) => Promise<StorageResult>;
|
||||
copy: (config: any, input: StorageInput & { action: 'copy' }) => Promise<StorageResult>;
|
||||
};
|
||||
|
||||
const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
||||
@@ -26,18 +28,21 @@ const handlers: Record<StorageProviderKind, ProviderHandler> = {
|
||||
get: getLocal,
|
||||
delete: deleteLocal,
|
||||
ping: pingLocal,
|
||||
copy: copyLocal,
|
||||
},
|
||||
s3: {
|
||||
upload: uploadS3,
|
||||
get: getS3,
|
||||
delete: deleteS3,
|
||||
ping: pingS3,
|
||||
copy: copyS3
|
||||
},
|
||||
"google-drive": {
|
||||
upload: uploadGoogleDrive,
|
||||
get: getGoogleDrive,
|
||||
delete: deleteGoogleDrive,
|
||||
ping: pingGoogleDrive,
|
||||
copy: copyGoogleDrive,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { mkdir, unlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import {
|
||||
StorageCopyInput,
|
||||
StorageDeleteInput,
|
||||
StorageGetInput,
|
||||
StorageMetaData,
|
||||
@@ -159,3 +160,58 @@ export async function pingLocal(config: {
|
||||
response: "Local storage OK",
|
||||
};
|
||||
}
|
||||
|
||||
export async function copyLocal(
|
||||
config: { baseDir?: string },
|
||||
input: {
|
||||
data: StorageCopyInput,
|
||||
metadata?: StorageMetaData;
|
||||
},
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir
|
||||
? path.join(process.cwd(), config.baseDir ?? "")
|
||||
: BASE_DIR;
|
||||
|
||||
const sourcePath = path.join(base, input.data.from);
|
||||
const destinationPath = path.join(base, input.data.to);
|
||||
|
||||
const dir = path.dirname(destinationPath);
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: "Source file not found",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const readStream = fs.createReadStream(sourcePath);
|
||||
const writeStream = fs.createWriteStream(destinationPath);
|
||||
|
||||
readStream.on("error", reject);
|
||||
writeStream.on("error", reject);
|
||||
writeStream.on("finish", resolve);
|
||||
|
||||
readStream.pipe(writeStream);
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
};
|
||||
} catch (err: any) {
|
||||
try {
|
||||
await unlink(destinationPath);
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: err.message || "Copy failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
import * as Minio from "minio";
|
||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from "../types";
|
||||
import {
|
||||
StorageCopyInput,
|
||||
StorageDeleteInput,
|
||||
StorageGetInput,
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput
|
||||
} from "../types";
|
||||
import {Readable} from "node:stream";
|
||||
|
||||
type S3Config = {
|
||||
@@ -132,3 +139,36 @@ export async function pingS3(config: S3Config): Promise<StorageResult> {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function copyS3(
|
||||
config: S3Config,
|
||||
input: {
|
||||
data: StorageCopyInput,
|
||||
},
|
||||
): Promise<StorageResult> {
|
||||
const client = await getS3Client(config);
|
||||
await ensureBucket(config);
|
||||
|
||||
const sourceKey = `${BASE_DIR}${input.data.from}`;
|
||||
const destinationKey = `${BASE_DIR}${input.data.to}`;
|
||||
|
||||
try {
|
||||
await client.copyObject(
|
||||
config.bucketName,
|
||||
destinationKey,
|
||||
`/${config.bucketName}/${sourceKey}`
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "s3",
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "s3",
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,18 @@ export interface StorageDeleteInput {
|
||||
path: string;
|
||||
}
|
||||
|
||||
|
||||
export interface StorageCopyInput {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export type StorageInput =
|
||||
| { action: 'upload'; data: StorageUploadInput, metadata?: StorageMetaData }
|
||||
| { action: 'get'; data: StorageGetInput, metadata: StorageMetaData }
|
||||
| { action: 'delete'; data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||
| { action: 'ping'; };
|
||||
| { action: 'ping'; }
|
||||
| { action: 'copy'; data: StorageCopyInput, metadata?: StorageMetaData };
|
||||
|
||||
export interface StorageResult {
|
||||
success: boolean;
|
||||
|
||||
Reference in New Issue
Block a user