diff --git a/scripts/migrate-images-to-minio.ts b/scripts/migrate-images-to-minio.ts new file mode 100644 index 0000000..3061c23 --- /dev/null +++ b/scripts/migrate-images-to-minio.ts @@ -0,0 +1,91 @@ +/** + * 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 = { + ".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); +});