ee68dc2897
- 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
131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { requireRole } from '@/lib/auth';
|
|
import { db } from '@/lib/db';
|
|
import sharp from 'sharp';
|
|
|
|
export const maxDuration = 300; // 5 minutes for large migrations
|
|
|
|
async function convertToWebP(base64Data: string): Promise<string> {
|
|
const matches = base64Data.match(/^data:image\/(\w+);base64,(.+)$/);
|
|
if (!matches) {
|
|
return base64Data;
|
|
}
|
|
|
|
const buffer = Buffer.from(matches[2], 'base64');
|
|
const webpBuffer = await sharp(buffer)
|
|
.webp({ quality: 90 })
|
|
.toBuffer();
|
|
|
|
return `data:image/webp;base64,${webpBuffer.toString('base64')}`;
|
|
}
|
|
|
|
export async function POST() {
|
|
try {
|
|
await requireRole('admin');
|
|
|
|
console.log('Starting WebP migration...');
|
|
|
|
const { rows: images } = await db.query(
|
|
'SELECT id, filename, data, thumbnail, medium FROM images'
|
|
);
|
|
|
|
console.log(`Found ${images.length} images to process`);
|
|
|
|
let converted = 0;
|
|
let skipped = 0;
|
|
let errors: number[] = [];
|
|
let totalSavedKB = 0;
|
|
|
|
for (const img of images) {
|
|
try {
|
|
let needsUpdate = false;
|
|
const updates: { data?: string; thumbnail?: string; medium?: string } = {};
|
|
|
|
if (img.data && !img.data.includes('image/webp')) {
|
|
console.log(`Converting image ${img.id}: ${img.filename} (full)`);
|
|
updates.data = await convertToWebP(img.data);
|
|
needsUpdate = true;
|
|
}
|
|
|
|
if (img.thumbnail && !img.thumbnail.includes('image/webp')) {
|
|
console.log(`Converting image ${img.id}: ${img.filename} (thumbnail)`);
|
|
updates.thumbnail = await convertToWebP(img.thumbnail);
|
|
needsUpdate = true;
|
|
}
|
|
|
|
if (img.medium && !img.medium.includes('image/webp')) {
|
|
console.log(`Converting image ${img.id}: ${img.filename} (medium)`);
|
|
updates.medium = await convertToWebP(img.medium);
|
|
needsUpdate = true;
|
|
}
|
|
|
|
if (needsUpdate) {
|
|
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 savedKB = Math.round((oldSize - newSize) / 1024);
|
|
totalSavedKB += savedKB;
|
|
|
|
await db.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 {
|
|
skipped++;
|
|
}
|
|
} catch (err) {
|
|
console.error(`Error processing image ${img.id}:`, err);
|
|
errors.push(img.id);
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
total: images.length,
|
|
converted,
|
|
skipped,
|
|
errors,
|
|
totalSavedMB: Math.round(totalSavedKB / 1024 * 10) / 10,
|
|
});
|
|
} catch (error) {
|
|
console.error('Migration error:', error);
|
|
if ((error as Error).message === 'Unauthorized') {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
return NextResponse.json({ error: 'Migration failed', details: (error as Error).message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
export async function GET() {
|
|
try {
|
|
await requireRole('admin');
|
|
|
|
const { rows: images } = await db.query(
|
|
'SELECT id, filename, LENGTH(data) as data_size, LENGTH(thumbnail) as thumb_size, LENGTH(medium) as medium_size FROM images'
|
|
);
|
|
|
|
let totalSize = 0;
|
|
let webpCount = 0;
|
|
let jpegCount = 0;
|
|
|
|
for (const img of images) {
|
|
totalSize += (img.data_size || 0) + (img.thumb_size || 0) + (img.medium_size || 0);
|
|
if (img.data_size) {
|
|
// We'd need to fetch the actual data to check format, so estimate based on size
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
totalImages: images.length,
|
|
totalSizeKB: Math.round(totalSize / 1024),
|
|
totalSizeMB: Math.round(totalSize / 1024 / 1024 * 10) / 10,
|
|
});
|
|
} catch (error) {
|
|
if ((error as Error).message === 'Unauthorized') {
|
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
}
|
|
return NextResponse.json({ error: 'Failed to get stats' }, { status: 500 });
|
|
}
|
|
} |