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:
Charles GTE
2026-04-29 22:39:37 +02:00
committed by GitHub
co-authored by charles-gauthereau
parent ab55ab349a
commit ff28a80d7c
20 changed files with 4133 additions and 14 deletions
@@ -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",
};
}
}
+7 -2
View File
@@ -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,
}
};
+56
View File
@@ -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",
};
}
}
+41 -1
View File
@@ -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,
};
}
}
+8 -1
View File
@@ -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;