- Reads all image files from uploads/ directory - Uploads each to S3 bucket preserving original filenames as object keys - Handles errors per-file without aborting entire migration - Preserves original files (manual deletion after verification)
92 lines
2.4 KiB
TypeScript
92 lines
2.4 KiB
TypeScript
/**
|
|
* One-time migration script: uploads/ -> MinIO (S3-compatible object storage)
|
|
*
|
|
* Reads all image files from the local uploads/ directory and uploads each
|
|
* to the S3 bucket via the storage service. Preserves original filenames
|
|
* as object keys.
|
|
*
|
|
* Prerequisites:
|
|
* - S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY env vars must be set
|
|
* - The S3 bucket must exist (created by docker-compose or manually)
|
|
*
|
|
* Usage:
|
|
* bun run scripts/migrate-images-to-minio.ts
|
|
*/
|
|
|
|
import { readdir } from "node:fs/promises";
|
|
import { extname, join } from "node:path";
|
|
import { uploadImage } from "../src/server/services/storage.service";
|
|
|
|
const UPLOADS_DIR = "uploads";
|
|
|
|
const CONTENT_TYPE_MAP: Record<string, string> = {
|
|
".jpg": "image/jpeg",
|
|
".jpeg": "image/jpeg",
|
|
".png": "image/png",
|
|
".webp": "image/webp",
|
|
};
|
|
|
|
const IMAGE_EXTENSIONS = new Set(Object.keys(CONTENT_TYPE_MAP));
|
|
|
|
async function main() {
|
|
// Check if uploads/ directory exists
|
|
try {
|
|
await readdir(UPLOADS_DIR);
|
|
} catch {
|
|
console.log("No uploads directory found. Nothing to migrate.");
|
|
process.exit(0);
|
|
}
|
|
|
|
// Read all files
|
|
const allFiles = await readdir(UPLOADS_DIR);
|
|
const imageFiles = allFiles.filter((f) =>
|
|
IMAGE_EXTENSIONS.has(extname(f).toLowerCase()),
|
|
);
|
|
|
|
if (imageFiles.length === 0) {
|
|
console.log("No image files found in uploads/. Nothing to migrate.");
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(`Found ${imageFiles.length} images to migrate\n`);
|
|
|
|
let success = 0;
|
|
let failed = 0;
|
|
|
|
for (const filename of imageFiles) {
|
|
const filePath = join(UPLOADS_DIR, filename);
|
|
const ext = extname(filename).toLowerCase();
|
|
const contentType = CONTENT_TYPE_MAP[ext] ?? "application/octet-stream";
|
|
|
|
try {
|
|
const file = Bun.file(filePath);
|
|
const buffer = await file.arrayBuffer();
|
|
await uploadImage(Buffer.from(buffer), filename, contentType);
|
|
success++;
|
|
console.log(` Migrated: ${filename}`);
|
|
} catch (err) {
|
|
failed++;
|
|
console.error(
|
|
` FAILED: ${filename} - ${err instanceof Error ? err.message : String(err)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log(
|
|
`\nMigration complete: ${success}/${imageFiles.length} files migrated, ${failed} failures`,
|
|
);
|
|
|
|
if (failed > 0) {
|
|
console.log("Re-run this script to retry failed uploads.");
|
|
}
|
|
|
|
console.log(
|
|
"\nOriginal files preserved in uploads/. Delete manually after verifying: rm -rf uploads/",
|
|
);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error("Migration failed:", err);
|
|
process.exit(1);
|
|
});
|