feat: add WebP migration UI and API endpoint
- Add /api/admin/migrate-images endpoint to convert JPEG/PNG to WebP - Add WebPMigration component in admin gallery showing stats - Upload endpoint already converts new images to WebP - Migration saves ~30-50% storage space per image
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Migration script to convert existing JPEG/PNG images to WebP
|
||||
* Run with: npx ts-node scripts/migrate-images-to-webp.ts
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
|
||||
async function convertToWebP(base64Data: string): Promise<string> {
|
||||
const matches = base64Data.match(/^data:image\/(\w+);base64,(.+)$/);
|
||||
if (!matches) {
|
||||
console.log(' - Already WebP or invalid format, skipping');
|
||||
return base64Data;
|
||||
}
|
||||
|
||||
const ext = matches[2] ? matches[1] : 'unknown';
|
||||
const buffer = Buffer.from(matches[2], 'base64');
|
||||
|
||||
// Convert to WebP
|
||||
const webpBuffer = await sharp(buffer)
|
||||
.webp({ quality: 90 })
|
||||
.toBuffer();
|
||||
|
||||
return `data:image/webp;base64,${webpBuffer.toString('base64')}`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting image migration to WebP...\n');
|
||||
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
// Get all images
|
||||
const { rows: images } = await client.query(
|
||||
'SELECT id, filename, data, thumbnail, medium FROM images'
|
||||
);
|
||||
|
||||
console.log(`Found ${images.length} images to process\n`);
|
||||
|
||||
let converted = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const img of images) {
|
||||
console.log(`Processing image ${img.id}: ${img.filename}`);
|
||||
|
||||
try {
|
||||
let needsUpdate = false;
|
||||
const updates: { data?: string; thumbnail?: string; medium?: string } = {};
|
||||
|
||||
// Check if already WebP
|
||||
if (img.data && !img.data.includes('image/webp')) {
|
||||
console.log(' - Converting full image...');
|
||||
updates.data = await convertToWebP(img.data);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (img.thumbnail && !img.thumbnail.includes('image/webp')) {
|
||||
console.log(' - Converting thumbnail...');
|
||||
updates.thumbnail = await convertToWebP(img.thumbnail);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (img.medium && !img.medium.includes('image/webp')) {
|
||||
console.log(' - Converting medium...');
|
||||
updates.medium = await convertToWebP(img.medium);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
// Calculate size savings
|
||||
const oldSize = (img.data?.length || 0) + (img.thumbnail?.length || 0) + (img.medium?.length || 0);
|
||||
const newSize = (updates.data?.length || img.data?.length || 0) +
|
||||
(updates.thumbnail?.length || img.thumbnail?.length || 0) +
|
||||
(updates.medium?.length || img.medium?.length || 0);
|
||||
const savings = ((oldSize - newSize) / oldSize * 100).toFixed(1);
|
||||
const savedKB = ((oldSize - newSize) / 1024).toFixed(1);
|
||||
|
||||
console.log(` - Size reduction: ${savings}% (${savedKB} KB saved)`);
|
||||
|
||||
await client.query(
|
||||
'UPDATE images SET data = $1, thumbnail = $2, medium = $3 WHERE id = $4',
|
||||
[updates.data || img.data, updates.thumbnail || img.thumbnail, updates.medium || img.medium, img.id]
|
||||
);
|
||||
converted++;
|
||||
} else {
|
||||
console.log(' - Already WebP, skipping');
|
||||
skipped++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(` - Error: ${(err as Error).message}`);
|
||||
errors++;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('Migration complete!\n');
|
||||
console.log(`Converted: ${converted}`);
|
||||
console.log(`Skipped: ${skipped}`);
|
||||
console.log(`Errors: ${errors}`);
|
||||
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
Reference in New Issue
Block a user