← 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');
});
}