@gallop.software/studio 0.1.9 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/handlers.js CHANGED
@@ -450,48 +450,13 @@ async function loadMeta() {
450
450
  const parsed = JSON.parse(content);
451
451
  if (parsed.images && typeof parsed.images === "object") {
452
452
  return parsed;
453
- } else {
454
- const images = {};
455
- for (const [key, value] of Object.entries(parsed)) {
456
- if (key.startsWith("/images/") && typeof value === "object" && value !== null) {
457
- const legacyEntry = value;
458
- const sizes = {
459
- full: { path: "", width: 0, height: 0 },
460
- large: { path: "", width: 0, height: 0 },
461
- medium: { path: "", width: 0, height: 0 },
462
- small: { path: "", width: 0, height: 0 }
463
- };
464
- for (const [sizeName, sizeData] of Object.entries(legacyEntry)) {
465
- if (sizeName === "small" || sizeName === "medium" || sizeName === "large" || sizeName === "full") {
466
- sizes[sizeName] = {
467
- path: sizeData.file,
468
- width: sizeData.width,
469
- height: sizeData.height
470
- };
471
- }
472
- }
473
- const imageKey = key.replace(/^\/images\//, "");
474
- images[imageKey] = {
475
- original: {
476
- path: key.replace("/images/", "/originals/"),
477
- width: sizes.full.width,
478
- height: sizes.full.height,
479
- fileSize: 0
480
- },
481
- sizes,
482
- blurhash: "",
483
- dominantColor: "#888888",
484
- cdn: null
485
- };
486
- }
487
- }
488
- return {
489
- $schema: "https://gallop.software/schemas/studio-meta.json",
490
- version: 1,
491
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
492
- images
493
- };
494
453
  }
454
+ return {
455
+ $schema: "https://gallop.software/schemas/studio-meta.json",
456
+ version: 1,
457
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
458
+ images: {}
459
+ };
495
460
  } catch (e6) {
496
461
  return {
497
462
  $schema: "https://gallop.software/schemas/studio-meta.json",
@@ -1 +1 @@
1
- {"version":3,"sources":["/Users/chrisb/Sites/studio/dist/handlers.js","../src/handlers.ts"],"names":[],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/handlers.js","sourcesContent":[null,"import { NextRequest, NextResponse } from 'next/server'\nimport { promises as fs } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { encode } from 'blurhash'\nimport { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'\nimport type { StudioMeta, ImageEntry, ImageSize, FileItem } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n // Handle legacy flat format (keys are image paths at root level)\n // vs new format with { images: {...} }\n if (parsed.images && typeof parsed.images === 'object') {\n // New format - already has images property\n return parsed\n } else {\n // Legacy format - convert flat structure to new format\n // Filter out metadata keys like $schema, version, generatedAt\n const images: Record<string, ImageEntry> = {}\n for (const [key, value] of Object.entries(parsed)) {\n if (key.startsWith('/images/') && typeof value === 'object' && value !== null) {\n // Convert legacy format to new format\n const legacyEntry = value as Record<string, { width: number; height: number; file: string }>\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n \n for (const [sizeName, sizeData] of Object.entries(legacyEntry)) {\n if (sizeName === 'small' || sizeName === 'medium' || sizeName === 'large' || sizeName === 'full') {\n sizes[sizeName] = {\n path: sizeData.file,\n width: sizeData.width,\n height: sizeData.height,\n }\n }\n }\n \n // Extract image key from path (e.g., \"/images/banner.jpg\" -> \"banner.jpg\")\n const imageKey = key.replace(/^\\/images\\//, '')\n images[imageKey] = {\n original: {\n path: key.replace('/images/', '/originals/'),\n width: sizes.full.width,\n height: sizes.full.height,\n fileSize: 0,\n },\n sizes,\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n }\n }\n \n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images,\n }\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"]}
1
+ {"version":3,"sources":["/Users/chrisb/Sites/studio/dist/handlers.js","../src/handlers.ts"],"names":[],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/handlers.js","sourcesContent":[null,"import { NextRequest, NextResponse } from 'next/server'\nimport { promises as fs } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { encode } from 'blurhash'\nimport { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'\nimport type { StudioMeta, ImageEntry, ImageSize, FileItem } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n if (parsed.images && typeof parsed.images === 'object') {\n return parsed\n }\n \n // Return empty meta if format is invalid\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"]}
package/dist/handlers.mjs CHANGED
@@ -450,48 +450,13 @@ async function loadMeta() {
450
450
  const parsed = JSON.parse(content);
451
451
  if (parsed.images && typeof parsed.images === "object") {
452
452
  return parsed;
453
- } else {
454
- const images = {};
455
- for (const [key, value] of Object.entries(parsed)) {
456
- if (key.startsWith("/images/") && typeof value === "object" && value !== null) {
457
- const legacyEntry = value;
458
- const sizes = {
459
- full: { path: "", width: 0, height: 0 },
460
- large: { path: "", width: 0, height: 0 },
461
- medium: { path: "", width: 0, height: 0 },
462
- small: { path: "", width: 0, height: 0 }
463
- };
464
- for (const [sizeName, sizeData] of Object.entries(legacyEntry)) {
465
- if (sizeName === "small" || sizeName === "medium" || sizeName === "large" || sizeName === "full") {
466
- sizes[sizeName] = {
467
- path: sizeData.file,
468
- width: sizeData.width,
469
- height: sizeData.height
470
- };
471
- }
472
- }
473
- const imageKey = key.replace(/^\/images\//, "");
474
- images[imageKey] = {
475
- original: {
476
- path: key.replace("/images/", "/originals/"),
477
- width: sizes.full.width,
478
- height: sizes.full.height,
479
- fileSize: 0
480
- },
481
- sizes,
482
- blurhash: "",
483
- dominantColor: "#888888",
484
- cdn: null
485
- };
486
- }
487
- }
488
- return {
489
- $schema: "https://gallop.software/schemas/studio-meta.json",
490
- version: 1,
491
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
492
- images
493
- };
494
453
  }
454
+ return {
455
+ $schema: "https://gallop.software/schemas/studio-meta.json",
456
+ version: 1,
457
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
458
+ images: {}
459
+ };
495
460
  } catch {
496
461
  return {
497
462
  $schema: "https://gallop.software/schemas/studio-meta.json",
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/handlers.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server'\nimport { promises as fs } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { encode } from 'blurhash'\nimport { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'\nimport type { StudioMeta, ImageEntry, ImageSize, FileItem } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n // Handle legacy flat format (keys are image paths at root level)\n // vs new format with { images: {...} }\n if (parsed.images && typeof parsed.images === 'object') {\n // New format - already has images property\n return parsed\n } else {\n // Legacy format - convert flat structure to new format\n // Filter out metadata keys like $schema, version, generatedAt\n const images: Record<string, ImageEntry> = {}\n for (const [key, value] of Object.entries(parsed)) {\n if (key.startsWith('/images/') && typeof value === 'object' && value !== null) {\n // Convert legacy format to new format\n const legacyEntry = value as Record<string, { width: number; height: number; file: string }>\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n \n for (const [sizeName, sizeData] of Object.entries(legacyEntry)) {\n if (sizeName === 'small' || sizeName === 'medium' || sizeName === 'large' || sizeName === 'full') {\n sizes[sizeName] = {\n path: sizeData.file,\n width: sizeData.width,\n height: sizeData.height,\n }\n }\n }\n \n // Extract image key from path (e.g., \"/images/banner.jpg\" -> \"banner.jpg\")\n const imageKey = key.replace(/^\\/images\\//, '')\n images[imageKey] = {\n original: {\n path: key.replace('/images/', '/originals/'),\n width: sizes.full.width,\n height: sizes.full.height,\n fileSize: 0,\n },\n sizes,\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n }\n }\n \n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images,\n }\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/handlers.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server'\nimport { promises as fs } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { encode } from 'blurhash'\nimport { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'\nimport type { StudioMeta, ImageEntry, ImageSize, FileItem } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n if (parsed.images && typeof parsed.images === 'object') {\n return parsed\n }\n \n // Return empty meta if format is invalid\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gallop.software/studio",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Media manager for Gallop templates - upload, process, and sync images to CDN",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",