← back to Grassclothwallpaper

migrate-to-supabase-storage.js

244 lines

require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
const https = require('https');
const http = require('http');
const fs = require('fs');
const path = require('path');

const supabaseUrl = process.env.SUPABASE_URL || 'https://fypvpcxyfmmdhtklgbrt.supabase.co';
const supabaseServiceKey = process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImZ5cHZwY3h5Zm1tZGh0a2xnYnJ0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTg4MTAxMjMsImV4cCI6MjA3NDM4NjEyM30.fzDf6VE7jyrqLIbebAwI1NHXi50pI0b_FgQVO-6QcAk';

const supabase = createClient(supabaseUrl, supabaseServiceKey);

const BUCKET_NAME = 'product-images';
const BATCH_SIZE = 10; // Process images in batches

async function downloadImage(url) {
    return new Promise((resolve, reject) => {
        const protocol = url.startsWith('https') ? https : http;

        protocol.get(url, (response) => {
            if (response.statusCode !== 200) {
                reject(new Error(`Failed to download: ${response.statusCode}`));
                return;
            }

            const chunks = [];
            response.on('data', chunk => chunks.push(chunk));
            response.on('end', () => resolve(Buffer.concat(chunks)));
            response.on('error', reject);
        }).on('error', reject);
    });
}

async function createBucketIfNotExists() {
    console.log('📦 Checking/Creating storage bucket...');

    const { data: buckets, error: listError } = await supabase.storage.listBuckets();

    if (listError) {
        console.error('Error listing buckets:', listError);
        return false;
    }

    const bucketExists = buckets?.some(b => b.name === BUCKET_NAME);

    if (!bucketExists) {
        console.log(`Creating bucket: ${BUCKET_NAME}`);
        const { data, error } = await supabase.storage.createBucket(BUCKET_NAME, {
            public: true,
            fileSizeLimit: 5242880 // 5MB
        });

        if (error) {
            console.error('Error creating bucket:', error);
            return false;
        }
        console.log('✅ Bucket created successfully');
    } else {
        console.log('✅ Bucket already exists');
    }

    return true;
}

async function uploadImageToSupabase(imageBuffer, fileName, contentType = 'image/jpeg') {
    try {
        const { data, error } = await supabase.storage
            .from(BUCKET_NAME)
            .upload(fileName, imageBuffer, {
                contentType: contentType,
                upsert: true
            });

        if (error) {
            console.error(`Upload error for ${fileName}:`, error.message);
            return null;
        }

        // Get public URL
        const { data: urlData } = supabase.storage
            .from(BUCKET_NAME)
            .getPublicUrl(fileName);

        return urlData.publicUrl;
    } catch (error) {
        console.error(`Error uploading ${fileName}:`, error);
        return null;
    }
}

async function migrateImagesToSupabase() {
    console.log('🚀 Starting migration to Supabase Storage...\n');

    // 1. Create bucket if needed
    const bucketReady = await createBucketIfNotExists();
    if (!bucketReady) {
        console.error('❌ Failed to create/access bucket');
        return;
    }

    // 2. Get all products with external images
    console.log('\n📊 Fetching products with external images...');
    const { data: products, error } = await supabase
        .from('grasscloth_products')
        .select('*')
        .or('image_url.ilike.%shopify.com%,image_url.ilike.%http%')
        .not('image_url', 'ilike', '%supabase.co%')
        .order('sku');

    if (error) {
        console.error('Error fetching products:', error);
        return;
    }

    console.log(`Found ${products.length} products with external images to migrate\n`);

    // 3. Process products in batches
    let successCount = 0;
    let failCount = 0;
    const failedProducts = [];

    for (let i = 0; i < products.length; i += BATCH_SIZE) {
        const batch = products.slice(i, Math.min(i + BATCH_SIZE, products.length));
        console.log(`\nProcessing batch ${Math.floor(i/BATCH_SIZE) + 1} (${i + 1}-${Math.min(i + BATCH_SIZE, products.length)} of ${products.length})`);

        await Promise.all(batch.map(async (product) => {
            try {
                if (!product.image_url) {
                    console.log(`  ⚠️  ${product.sku}: No image URL`);
                    failedProducts.push(product);
                    failCount++;
                    return;
                }

                // Skip if already Supabase URL
                if (product.image_url.includes('supabase.co')) {
                    console.log(`  ⏭️  ${product.sku}: Already in Supabase`);
                    successCount++;
                    return;
                }

                // Download image
                console.log(`  ⬇️  ${product.sku}: Downloading...`);
                const imageBuffer = await downloadImage(product.image_url);

                // Determine file extension
                const urlParts = product.image_url.split('.');
                const extension = urlParts[urlParts.length - 1].split('?')[0].toLowerCase();
                const validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
                const fileExt = validExtensions.includes(extension) ? extension : 'jpg';

                // Create filename
                const fileName = `${product.sku || `product-${Date.now()}`}.${fileExt}`;

                // Upload to Supabase
                const supabaseUrl = await uploadImageToSupabase(imageBuffer, fileName, `image/${fileExt}`);

                if (supabaseUrl) {
                    // Update product record
                    const { error: updateError } = await supabase
                        .from('grasscloth_products')
                        .update({
                            image_url: supabaseUrl,
                            has_valid_image: true
                        })
                        .eq('sku', product.sku);

                    if (updateError) {
                        console.log(`  ❌ ${product.sku}: Failed to update database`);
                        failedProducts.push(product);
                        failCount++;
                    } else {
                        console.log(`  ✅ ${product.sku}: Migrated successfully`);
                        successCount++;
                    }
                } else {
                    console.log(`  ❌ ${product.sku}: Failed to upload`);
                    failedProducts.push(product);
                    failCount++;
                }
            } catch (error) {
                console.log(`  ❌ ${product.sku}: Error - ${error.message}`);
                failedProducts.push(product);
                failCount++;
            }
        }));

        // Small delay between batches
        if (i + BATCH_SIZE < products.length) {
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    }

    // 4. Handle products without images
    console.log('\n📊 Checking products without any images...');
    const { data: noImageProducts } = await supabase
        .from('grasscloth_products')
        .select('sku, name')
        .is('image_url', null);

    // 5. Final report
    console.log('\n' + '='.repeat(60));
    console.log('📊 MIGRATION COMPLETE:');
    console.log('='.repeat(60));
    console.log(`✅ Successfully migrated: ${successCount} products`);
    console.log(`❌ Failed to migrate: ${failCount} products`);
    console.log(`⚠️  No images: ${noImageProducts?.length || 0} products`);
    console.log('='.repeat(60));

    if (failedProducts.length > 0) {
        console.log('\n❌ Failed products (first 10):');
        failedProducts.slice(0, 10).forEach(p => {
            console.log(`  - ${p.sku}: ${p.name || 'No name'}`);
        });
    }

    // Save failed products list for manual review
    if (failedProducts.length > 0) {
        fs.writeFileSync(
            'failed-migrations.json',
            JSON.stringify(failedProducts, null, 2)
        );
        console.log('\n📝 Failed products saved to failed-migrations.json');
    }

    return {
        success: successCount,
        failed: failCount,
        noImages: noImageProducts?.length || 0
    };
}

// Export for use in other scripts
module.exports = { migrateImagesToSupabase };

// Run if called directly
if (require.main === module) {
    migrateImagesToSupabase().then(result => {
        console.log('\n🎯 Next steps:');
        console.log('1. Review failed-migrations.json for any failed uploads');
        console.log('2. Update frontend to filter by Supabase URLs only');
        console.log('3. Test the website to ensure all images load correctly');
    });
}