Files
moyosapp_beta.0.0.3.3_beta1/scripts/migrate-storage.ts
2026-01-16 19:04:48 +02:00

295 lines
8.4 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env tsx
/**
* Storage Migration Script
*
* Migrates gallery photos from filesystem to Supabase Storage.
*
* Usage:
* tsx scripts/migrate-storage.ts [--dry-run] [--force]
*
* Options:
* --dry-run Show what would be migrated without actually migrating
* --force Force migration even if files already exist in storage
*/
import { promises as fs } from 'fs';
import path from 'path';
import { createSupabaseAdminClient, isSupabaseConfigured } from '../src/lib/supabase';
import { uploadFile } from '../src/lib/storage';
import { logger } from '../src/lib/logger';
const GALLERY_BUCKET = 'gallery';
const DATA_FILE = path.join(process.cwd(), "data", "gallery.json");
const UPLOADS_DIR = path.join(process.cwd(), "public", "uploads", "gallery");
const THUMBNAILS_DIR = path.join(UPLOADS_DIR, "thumbnails");
const FULL_IMAGES_DIR = path.join(UPLOADS_DIR, "full");
interface GalleryItem {
id: string;
guestName: string;
thumbnailUrl: string;
fullImageUrl: string;
caption?: string;
timestamp: string;
orientation?: "portrait" | "landscape" | "square";
}
async function loadGalleryItems(): Promise<GalleryItem[]> {
try {
const raw = await fs.readFile(DATA_FILE, "utf8");
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch (err: any) {
if (err.code === "ENOENT") {
return [];
}
throw err;
}
}
async function saveGalleryItems(items: GalleryItem[]): Promise<void> {
const json = JSON.stringify(items, null, 2);
await fs.writeFile(DATA_FILE, json, "utf8");
}
function isLocalPath(url: string): boolean {
return url.startsWith("/uploads/") || url.startsWith("./");
}
function extractFileName(url: string): string {
return path.basename(url);
}
async function migrateFile(
localPath: string,
storagePath: string,
bucket: string,
dryRun: boolean
): Promise<{ success: boolean; url?: string; error?: string }> {
try {
const fullLocalPath = localPath.startsWith("/")
? path.join(process.cwd(), "public", localPath)
: localPath;
// Check if file exists
try {
await fs.access(fullLocalPath);
} catch {
return { success: false, error: 'File not found' };
}
if (dryRun) {
return { success: true, url: `[DRY RUN] Would upload to ${storagePath}` };
}
// Read file
const fileBuffer = await fs.readFile(fullLocalPath);
const fileName = path.basename(fullLocalPath);
const mimeType = getMimeType(fileName);
// Upload to Supabase Storage
const supabase = createSupabaseAdminClient();
const { data, error } = await supabase.storage
.from(bucket)
.upload(storagePath, fileBuffer, {
cacheControl: '3600',
upsert: false,
contentType: mimeType,
});
if (error) {
// If file already exists, check if we should skip or overwrite
if (error.message.includes('already exists')) {
return { success: false, error: 'File already exists in storage' };
}
throw error;
}
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from(bucket)
.getPublicUrl(storagePath);
return { success: true, url: publicUrl };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function getMimeType(fileName: string): string {
const ext = path.extname(fileName).toLowerCase();
const mimeTypes: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.webp': 'image/webp',
};
return mimeTypes[ext] || 'application/octet-stream';
}
async function migrateGalleryItems(
items: GalleryItem[],
dryRun: boolean,
force: boolean
): Promise<{ migrated: number; failed: number; skipped: number }> {
let migrated = 0;
let failed = 0;
let skipped = 0;
for (const item of items) {
const thumbnailIsLocal = isLocalPath(item.thumbnailUrl);
const fullImageIsLocal = isLocalPath(item.fullImageUrl);
// Skip if both are already remote URLs
if (!thumbnailIsLocal && !fullImageIsLocal) {
skipped++;
continue;
}
console.log(`\nMigrating item ${item.id}...`);
let thumbnailUrl = item.thumbnailUrl;
let fullImageUrl = item.fullImageUrl;
// Migrate thumbnail
if (thumbnailIsLocal) {
const thumbnailFileName = extractFileName(item.thumbnailUrl);
const thumbnailStoragePath = `thumbnails/${thumbnailFileName}`;
console.log(` Thumbnail: ${item.thumbnailUrl} -> ${thumbnailStoragePath}`);
const result = await migrateFile(
item.thumbnailUrl,
thumbnailStoragePath,
GALLERY_BUCKET,
dryRun
);
if (result.success && result.url) {
thumbnailUrl = result.url;
console.log(` ✓ Thumbnail migrated: ${result.url}`);
} else if (result.error?.includes('already exists') && !force) {
console.log(` ⊘ Thumbnail already in storage, skipping`);
skipped++;
continue;
} else {
console.log(` ✗ Thumbnail migration failed: ${result.error}`);
failed++;
continue;
}
}
// Migrate full image
if (fullImageIsLocal) {
const fullImageFileName = extractFileName(item.fullImageUrl);
const fullImageStoragePath = `full/${fullImageFileName}`;
console.log(` Full image: ${item.fullImageUrl} -> ${fullImageStoragePath}`);
const result = await migrateFile(
item.fullImageUrl,
fullImageStoragePath,
GALLERY_BUCKET,
dryRun
);
if (result.success && result.url) {
fullImageUrl = result.url;
console.log(` ✓ Full image migrated: ${result.url}`);
} else if (result.error?.includes('already exists') && !force) {
console.log(` ⊘ Full image already in storage, skipping`);
skipped++;
continue;
} else {
console.log(` ✗ Full image migration failed: ${result.error}`);
failed++;
continue;
}
}
// Update item with new URLs
if (!dryRun && (thumbnailUrl !== item.thumbnailUrl || fullImageUrl !== item.fullImageUrl)) {
item.thumbnailUrl = thumbnailUrl;
item.fullImageUrl = fullImageUrl;
migrated++;
} else if (dryRun) {
migrated++;
}
}
return { migrated, failed, skipped };
}
async function main() {
const args = process.argv.slice(2);
const dryRun = args.includes('--dry-run');
const force = args.includes('--force');
console.log('=== Gallery Storage Migration ===\n');
console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}`);
console.log(`Force: ${force ? 'Yes' : 'No'}\n`);
// Check Supabase configuration
if (!isSupabaseConfigured()) {
console.error('❌ Supabase is not configured!');
console.error('Please set NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY');
process.exit(1);
}
// Check if bucket exists
const supabase = createSupabaseAdminClient();
const { data: buckets, error: bucketsError } = await supabase.storage.listBuckets();
if (bucketsError) {
console.error('❌ Failed to list buckets:', bucketsError.message);
process.exit(1);
}
const galleryBucket = buckets?.find(b => b.name === GALLERY_BUCKET);
if (!galleryBucket) {
console.error(`❌ Bucket "${GALLERY_BUCKET}" does not exist!`);
console.error('Please create it via Supabase dashboard or run the migration SQL first.');
process.exit(1);
}
console.log(`✓ Bucket "${GALLERY_BUCKET}" exists\n`);
// Load gallery items
console.log('Loading gallery items...');
const items = await loadGalleryItems();
console.log(`Found ${items.length} gallery items\n`);
if (items.length === 0) {
console.log('No items to migrate.');
return;
}
// Migrate items
const stats = await migrateGalleryItems(items, dryRun, force);
// Save updated items
if (!dryRun && stats.migrated > 0) {
console.log('\nSaving updated gallery items...');
await saveGalleryItems(items);
console.log('✓ Gallery items saved\n');
}
// Print summary
console.log('\n=== Migration Summary ===');
console.log(`Migrated: ${stats.migrated}`);
console.log(`Failed: ${stats.failed}`);
console.log(`Skipped: ${stats.skipped}`);
console.log(`Total: ${items.length}`);
if (dryRun) {
console.log('\n⚠ This was a DRY RUN. No files were actually migrated.');
console.log('Run without --dry-run to perform the actual migration.');
}
}
main().catch((error) => {
console.error('Migration failed:', error);
process.exit(1);
});