@gallop.software/studio 0.1.71 → 0.1.72
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 +25 -12
- package/dist/handlers.js.map +1 -1
- package/dist/handlers.mjs +25 -12
- package/dist/handlers.mjs.map +1 -1
- package/package.json +1 -1
package/dist/handlers.js
CHANGED
|
@@ -898,25 +898,38 @@ async function handleProcessAllStream() {
|
|
|
898
898
|
}
|
|
899
899
|
async function loadMeta() {
|
|
900
900
|
const metaPath = _path2.default.join(process.cwd(), "_data", "_meta.json");
|
|
901
|
+
const emptyMeta = {
|
|
902
|
+
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
903
|
+
version: 1,
|
|
904
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
905
|
+
images: {}
|
|
906
|
+
};
|
|
901
907
|
try {
|
|
902
908
|
const content = await _fs.promises.readFile(metaPath, "utf-8");
|
|
903
909
|
const parsed = JSON.parse(content);
|
|
904
910
|
if (parsed.images && typeof parsed.images === "object") {
|
|
905
911
|
return parsed;
|
|
906
912
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
images
|
|
912
|
-
|
|
913
|
+
const meta = { ...emptyMeta, images: {} };
|
|
914
|
+
for (const [imagePath, entry] of Object.entries(parsed)) {
|
|
915
|
+
const leanEntry = entry;
|
|
916
|
+
const key = imagePath.startsWith("/") ? imagePath.slice(1) : imagePath;
|
|
917
|
+
meta.images[key] = {
|
|
918
|
+
original: {
|
|
919
|
+
path: imagePath,
|
|
920
|
+
width: leanEntry.w,
|
|
921
|
+
height: leanEntry.h,
|
|
922
|
+
fileSize: 0
|
|
923
|
+
},
|
|
924
|
+
sizes: {},
|
|
925
|
+
blurhash: leanEntry.blur,
|
|
926
|
+
dominantColor: "",
|
|
927
|
+
cdn: leanEntry.s ? { synced: true, baseUrl: "", syncedAt: "" } : null
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
return meta;
|
|
913
931
|
} catch (e18) {
|
|
914
|
-
return
|
|
915
|
-
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
916
|
-
version: 1,
|
|
917
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
918
|
-
images: {}
|
|
919
|
-
};
|
|
932
|
+
return emptyMeta;
|
|
920
933
|
}
|
|
921
934
|
}
|
|
922
935
|
async function saveMeta(meta) {
|
package/dist/handlers.js.map
CHANGED
|
@@ -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, LeanMeta, LeanImageEntry } from './types'\n\n// Default thumbnail sizes with their suffixes\nconst DEFAULT_SIZES: Record<string, { width: number; suffix: string }> = {\n small: { width: 300, suffix: '-sm' },\n medium: { width: 700, suffix: '-md' },\n large: { width: 1400, suffix: '-lg' },\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-folders (must come before 'list' check)\n if (route === 'list-folders') {\n return handleListFolders()\n }\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 // Route: /api/studio/count-images\n if (route === 'count-images') {\n return handleCountImages()\n }\n\n // Route: /api/studio/folder-images\n if (route === 'folder-images') {\n return handleFolderImages(request)\n }\n\n // Route: /api/studio/search\n if (route === 'search') {\n return handleSearch(request)\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 // Route: /api/studio/process-all (streaming)\n if (route === 'process-all') {\n return handleProcessAllStream()\n }\n\n // Route: /api/studio/create-folder\n if (route === 'create-folder') {\n return handleCreateFolder(request)\n }\n\n // Route: /api/studio/rename\n if (route === 'rename') {\n return handleRename(request)\n }\n\n // Route: /api/studio/move\n if (route === 'move') {\n return handleMove(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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const filePath = path.join(absolutePath, entry.name)\n const stats = await fs.stat(filePath)\n const isImage = isImageFile(entry.name)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n \n if (isImage) {\n const relativePath = safePath.replace(/^public\\/?/, '')\n \n // If we're already inside the images folder, these ARE the thumbnails\n if (relativePath === 'images' || relativePath.startsWith('images/')) {\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = true // They are thumbnails themselves\n } else {\n // Check for -sm thumbnail in images folder\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n \n try {\n await fs.access(thumbnailPath)\n // Thumbnail exists\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n // No thumbnail, fall back to original\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = false\n }\n }\n \n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(filePath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n }\n \n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\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 handleSearch(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const query = searchParams.get('q')?.toLowerCase() || ''\n \n if (query.length < 2) {\n return NextResponse.json({ items: [] })\n }\n\n try {\n const items: FileItem[] = []\n const publicDir = path.join(process.cwd(), 'public')\n\n async function searchDir(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 itemPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`\n const itemRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n // Skip public/images folder (generated thumbnails)\n if (itemRelPath === 'images') continue\n await searchDir(fullPath, itemRelPath)\n } else if (isImageFile(entry.name)) {\n // Check if path matches query\n if (itemPath.toLowerCase().includes(query)) {\n const stats = await fs.stat(fullPath)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n\n // Check for -sm thumbnail\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n\n try {\n await fs.access(thumbnailPath)\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n thumbnail = `/${itemRelPath}`\n hasThumbnail = false\n }\n\n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(fullPath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\n })\n }\n }\n }\n } catch {\n // Ignore directory access errors\n }\n }\n\n await searchDir(publicDir, '')\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to search:', error)\n return NextResponse.json({ error: 'Failed to search' }, { status: 500 })\n }\n}\n\nasync function getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\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 // Check if this is an image that can be processed\n const isImage = isImageFile(fileName)\n const isSvg = ext === '.svg'\n const isProcessableImage = isImage && !isSvg\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // For non-image media files, just save and return success\n if (!isImage) {\n return NextResponse.json({ \n success: true, \n message: 'File uploaded successfully (non-image, no thumbnails generated)',\n path: `public/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n })\n }\n \n // For images, generate thumbnails and update meta\n const fullImageKey = relativeDir ? `${relativeDir}/${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 // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\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 // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else if (isProcessableImage) {\n // Raster images: process with sharp and generate thumbnails\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/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${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 as ImageSize] = {\n path: `/images/${relativeDir ? relativeDir + '/' : ''}${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: originalPath,\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 // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\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 // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\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 // If deleting from images/, just delete the file (already done above)\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 try {\n let buffer: Buffer\n let entry = meta.images[imageKey]\n \n // Try to read the original file from public folder\n const originalPath = path.join(process.cwd(), 'public', imageKey)\n \n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n // File not in public folder, try from entry's original path or CDN\n if (entry) {\n const entryOriginalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(entryOriginalPath)\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 } else {\n throw new Error(`File not found: ${imageKey}`)\n }\n }\n\n // If no existing entry, create a minimal one for new images\n if (!entry) {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const stats = await fs.stat(originalPath)\n \n entry = {\n original: {\n path: imageKey,\n width: metadata.width || 0,\n height: metadata.height || 0,\n fileSize: stats.size,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#000000',\n cdn: null,\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\nasync function handleCountImages() {\n try {\n const allImages: string[] = []\n\n // Scan public folder recursively for ALL images, excluding public/images/\n async function scanPublicFolder(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 // Skip the images folder - that's for generated thumbnails\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to count images:', error)\n return NextResponse.json({ error: 'Failed to count images' }, { status: 500 })\n }\n}\n\nasync function handleFolderImages(request: NextRequest) {\n try {\n const searchParams = request.nextUrl.searchParams\n const foldersParam = searchParams.get('folders')\n \n if (!foldersParam) {\n return NextResponse.json({ error: 'No folders provided' }, { status: 400 })\n }\n\n const folders = foldersParam.split(',')\n const allImages: string[] = []\n\n async function scanFolder(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 scanFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n for (const folder of folders) {\n // Folder paths come as \"public/photos\" - we need relative path from public\n const relativePath = folder.replace(/^public\\/?/, '')\n \n // Skip the images folder\n if (relativePath === 'images' || relativePath.startsWith('images/')) continue\n \n const folderPath = path.join(process.cwd(), folder)\n await scanFolder(folderPath, relativePath)\n }\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to get folder images:', error)\n return NextResponse.json({ error: 'Failed to get folder images' }, { status: 500 })\n }\n}\n\nasync function handleProcessAllStream() {\n const encoder = new TextEncoder()\n \n const stream = new ReadableStream({\n async start(controller) {\n const sendEvent = (data: object) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`))\n }\n\n try {\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n const orphansRemoved: string[] = []\n\n // Step 1: Scan public folder for ALL images (excluding public/images/)\n const allImages: Array<{ key: string; fullPath: string }> = []\n\n async function scanPublicFolder(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 // Skip the images folder\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push({ key: relPath, fullPath })\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n const total = allImages.length\n sendEvent({ type: 'start', total })\n\n // Step 2: Process each image (reprocess all, not just unprocessed)\n for (let i = 0; i < allImages.length; i++) {\n const { key, fullPath } = allImages[i]\n \n sendEvent({ \n type: 'progress', \n current: i + 1, \n total, \n percent: Math.round(((i + 1) / total) * 100),\n currentFile: key \n })\n\n try {\n const buffer = await fs.readFile(fullPath)\n const ext = path.extname(key).toLowerCase()\n const isSvg = ext === '.svg'\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const imageDir = path.dirname(key)\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n \n const fileName = path.basename(key)\n const destPath = path.join(imagesPath, fileName)\n await fs.writeFile(destPath, buffer)\n\n const sizePath = `/images/${imageDir === '.' ? '' : imageDir + '/'}${fileName}`\n meta.images[key] = {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\n full: { path: sizePath, width: 0, height: 0 },\n large: { path: sizePath, width: 0, height: 0 },\n medium: { path: sizePath, width: 0, height: 0 },\n small: { path: sizePath, width: 0, height: 0 },\n },\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n } else {\n // Raster image: full processing\n const existingEntry = meta.images[key]\n const baseEntry: ImageEntry = existingEntry || {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n\n const processedEntry = await processImage(buffer, baseEntry, key)\n meta.images[key] = processedEntry\n }\n\n processed.push(key)\n } catch (error) {\n console.error(`Failed to process ${key}:`, error)\n errors.push(key)\n }\n }\n\n // Step 3: Remove orphaned thumbnails\n sendEvent({ type: 'cleanup', message: 'Removing orphaned thumbnails...' })\n \n const trackedPaths = new Set<string>()\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 findOrphans(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 findOrphans(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n try {\n await fs.unlink(fullPath)\n orphansRemoved.push(publicPath)\n } catch (err) {\n console.error(`Failed to remove orphan ${publicPath}:`, err)\n }\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n await findOrphans(imagesDir)\n\n // Step 4: Clean up empty directories\n async function removeEmptyDirs(dir: string): Promise<boolean> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n let isEmpty = true\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n const subDirEmpty = await removeEmptyDirs(path.join(dir, entry.name))\n if (!subDirEmpty) isEmpty = false\n } else {\n isEmpty = false\n }\n }\n\n if (isEmpty && dir !== imagesDir) {\n await fs.rmdir(dir)\n }\n\n return isEmpty\n } catch {\n return true\n }\n }\n\n await removeEmptyDirs(imagesDir)\n await saveMeta(meta)\n\n sendEvent({ \n type: 'complete', \n processed: processed.length, \n orphansRemoved: orphansRemoved.length,\n errors: errors.length,\n })\n } catch (error) {\n console.error('Failed to process all:', error)\n sendEvent({ type: 'error', message: 'Failed to process images' })\n } finally {\n controller.close()\n }\n }\n })\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\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 dataDir = path.join(process.cwd(), '_data')\n await fs.mkdir(dataDir, { recursive: true })\n \n // Convert to lean format and write to _meta.json\n const lean: LeanMeta = {}\n for (const [key, entry] of Object.entries(meta.images)) {\n const imagePath = entry.original?.path || `/${key}`\n lean[imagePath] = {\n w: entry.original?.width || 0,\n h: entry.original?.height || 0,\n blur: entry.blurhash || '',\n }\n if (entry.cdn?.synced) {\n lean[imagePath].s = 1\n }\n }\n const metaPath = path.join(dataDir, '_meta.json')\n await fs.writeFile(metaPath, JSON.stringify(lean, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\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 isPng = ext === '.png'\n const outputExt = isPng ? '.png' : '.jpg'\n const fullFileName = imageDir === '.' ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n \n if (isPng) {\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/${fullFileName}`\n\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${outputExt}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n if (isPng) {\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 as ImageSize] = {\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\n// ============================================================================\n// FOLDER MANAGEMENT HANDLERS\n// ============================================================================\n\nasync function handleCreateFolder(request: NextRequest) {\n try {\n const { parentPath, name } = await request.json()\n\n if (!name || typeof name !== 'string') {\n return NextResponse.json({ error: 'Folder name is required' }, { status: 400 })\n }\n\n // Sanitize folder name\n const sanitizedName = name.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid folder name' }, { status: 400 })\n }\n\n const safePath = (parentPath || 'public').replace(/\\.\\./g, '')\n const folderPath = path.join(process.cwd(), safePath, sanitizedName)\n\n // Check if we're within public folder\n if (!folderPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if folder already exists\n try {\n await fs.access(folderPath)\n return NextResponse.json({ error: 'A folder with this name already exists' }, { status: 400 })\n } catch {\n // Good - folder doesn't exist\n }\n\n await fs.mkdir(folderPath, { recursive: true })\n\n return NextResponse.json({ success: true, path: path.join(safePath, sanitizedName) })\n } catch (error) {\n console.error('Failed to create folder:', error)\n return NextResponse.json({ error: 'Failed to create folder' }, { status: 500 })\n }\n}\n\nasync function handleRename(request: NextRequest) {\n try {\n const { oldPath, newName } = await request.json()\n\n if (!oldPath || !newName) {\n return NextResponse.json({ error: 'Path and new name are required' }, { status: 400 })\n }\n\n // Sanitize new name\n const sanitizedName = newName.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid name' }, { status: 400 })\n }\n\n const safePath = oldPath.replace(/\\.\\./g, '')\n const absoluteOldPath = path.join(process.cwd(), safePath)\n const parentDir = path.dirname(absoluteOldPath)\n const absoluteNewPath = path.join(parentDir, sanitizedName)\n\n // Check path is within public folder\n if (!absoluteOldPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if old path exists\n try {\n await fs.access(absoluteOldPath)\n } catch {\n return NextResponse.json({ error: 'File or folder not found' }, { status: 404 })\n }\n\n // Check if new path already exists\n try {\n await fs.access(absoluteNewPath)\n return NextResponse.json({ error: 'An item with this name already exists' }, { status: 400 })\n } catch {\n // Good - new path doesn't exist\n }\n\n // Check if it's a file (for meta updates)\n const stats = await fs.stat(absoluteOldPath)\n const isFile = stats.isFile()\n const isImage = isFile && isImageFile(path.basename(oldPath))\n\n // Rename the file/folder\n await fs.rename(absoluteOldPath, absoluteNewPath)\n\n // Update meta if it's an image\n if (isImage) {\n const meta = await loadMeta()\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(path.dirname(oldRelativePath), sanitizedName)\n\n // Find and update meta entry\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n // Update original path\n entry.original.path = `/${newRelativePath}`\n\n // Rename thumbnails in public/images\n const oldExt = path.extname(path.basename(oldPath))\n const oldBaseName = path.basename(oldPath, oldExt)\n const newExt = path.extname(sanitizedName)\n const newBaseName = path.basename(sanitizedName, newExt)\n const oldDirRelative = path.dirname(oldRelativePath)\n const thumbnailDir = path.join(process.cwd(), 'public', 'images', oldDirRelative)\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const oldThumbName = `${oldBaseName}${suffix}${oldExt === '.png' ? '.png' : '.jpg'}`\n const newThumbName = `${newBaseName}${suffix}${newExt === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(thumbnailDir, oldThumbName)\n const newThumbPath = path.join(thumbnailDir, newThumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${oldDirRelative}/${newThumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update the key in meta\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n break\n }\n }\n\n await saveMeta(meta)\n }\n\n const newPath = path.join(path.dirname(safePath), sanitizedName)\n return NextResponse.json({ success: true, newPath })\n } catch (error) {\n console.error('Failed to rename:', error)\n return NextResponse.json({ error: 'Failed to rename' }, { status: 500 })\n }\n}\n\nasync function handleMove(request: NextRequest) {\n try {\n const { paths, destination } = await request.json()\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'Paths are required' }, { status: 400 })\n }\n\n if (!destination || typeof destination !== 'string') {\n return NextResponse.json({ error: 'Destination is required' }, { status: 400 })\n }\n\n const safeDestination = destination.replace(/\\.\\./g, '')\n const absoluteDestination = path.join(process.cwd(), safeDestination)\n\n // Check destination is within public folder\n if (!absoluteDestination.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid destination' }, { status: 400 })\n }\n\n // Cannot move to protected images folder\n if (safeDestination === 'public/images' || safeDestination.startsWith('public/images/')) {\n return NextResponse.json({ error: 'Cannot move items to the protected images folder' }, { status: 400 })\n }\n\n // Check destination exists and is a directory\n try {\n const destStats = await fs.stat(absoluteDestination)\n if (!destStats.isDirectory()) {\n return NextResponse.json({ error: 'Destination is not a folder' }, { status: 400 })\n }\n } catch {\n return NextResponse.json({ error: 'Destination folder not found' }, { status: 404 })\n }\n\n const moved: string[] = []\n const errors: string[] = []\n const meta = await loadMeta()\n let metaChanged = false\n\n for (const itemPath of paths) {\n const safePath = itemPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n const itemName = path.basename(safePath)\n const newAbsolutePath = path.join(absoluteDestination, itemName)\n\n // Cannot move a folder into itself\n if (absoluteDestination.startsWith(absolutePath + path.sep)) {\n errors.push(`Cannot move ${itemName} into itself`)\n continue\n }\n\n // Check source exists\n try {\n await fs.access(absolutePath)\n } catch {\n errors.push(`${itemName} not found`)\n continue\n }\n\n // Check if destination already has item with same name\n try {\n await fs.access(newAbsolutePath)\n errors.push(`${itemName} already exists in destination`)\n continue\n } catch {\n // Good - doesn't exist\n }\n\n try {\n await fs.rename(absolutePath, newAbsolutePath)\n\n // Update meta for images\n const stats = await fs.stat(newAbsolutePath)\n if (stats.isFile() && isImageFile(itemName)) {\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(safeDestination.replace(/^public\\//, ''), itemName)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n entry.original.path = `/${newRelativePath}`\n\n // Move thumbnails too\n const oldDir = path.dirname(oldRelativePath)\n const newDir = path.dirname(newRelativePath)\n const ext = path.extname(itemName)\n const baseName = path.basename(itemName, ext)\n const oldThumbDir = path.join(process.cwd(), 'public', 'images', oldDir)\n const newThumbDir = path.join(process.cwd(), 'public', 'images', newDir)\n\n // Ensure new thumb directory exists\n await fs.mkdir(newThumbDir, { recursive: true })\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const thumbName = `${baseName}${suffix}${ext === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(oldThumbDir, thumbName)\n const newThumbPath = path.join(newThumbDir, thumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${newDir}/${thumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update key\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n metaChanged = true\n break\n }\n }\n }\n\n moved.push(itemPath)\n } catch (error) {\n errors.push(`Failed to move ${itemName}`)\n }\n }\n\n if (metaChanged) {\n await saveMeta(meta)\n }\n\n return NextResponse.json({\n success: errors.length === 0,\n moved,\n errors: errors.length > 0 ? errors : undefined\n })\n } catch (error) {\n console.error('Failed to move:', error)\n return NextResponse.json({ error: 'Failed to move items' }, { status: 500 })\n }\n}\n\nasync function handleListFolders() {\n try {\n const publicDir = path.join(process.cwd(), 'public')\n const folders: { path: string; name: string; depth: number }[] = []\n\n async function scanDir(dir: string, relativePath: string, depth: number): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n if (entry.name.startsWith('.')) continue\n // Skip protected images folder\n if (relativePath === '' && entry.name === 'images') continue\n\n const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n folders.push({\n path: `public/${folderRelativePath}`,\n name: entry.name,\n depth\n })\n\n // Recursively scan subdirectories\n await scanDir(path.join(dir, entry.name), folderRelativePath, depth + 1)\n }\n } catch {\n // Ignore errors\n }\n }\n\n // Add root public folder\n folders.push({ path: 'public', name: 'public', depth: 0 })\n\n await scanDir(publicDir, '', 1)\n\n return NextResponse.json({ folders })\n } catch (error) {\n console.error('Failed to list folders:', error)\n return NextResponse.json({ error: 'Failed to list folders' }, { status: 500 })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/chrisb/Sites/studio/dist/handlers.js","../src/handlers.ts"],"names":[],"mappings":"AAAA;ACAA,qCAA0C;AAC1C,wBAA+B;AAC/B,wEAAiB;AACjB,4EAAkB;AAClB,oCAAuB;AACvB,8CAA6D;AAI7D,IAAM,cAAA,EAAmE;AAAA,EACvE,KAAA,EAAO,EAAE,KAAA,EAAO,GAAA,EAAK,MAAA,EAAQ,MAAM,CAAA;AAAA,EACnC,MAAA,EAAQ,EAAE,KAAA,EAAO,GAAA,EAAK,MAAA,EAAQ,MAAM,CAAA;AAAA,EACpC,KAAA,EAAO,EAAE,KAAA,EAAO,IAAA,EAAM,MAAA,EAAQ,MAAM;AACtC,CAAA;AAKA,MAAA,SAAsB,GAAA,CAAI,OAAA,EAAsB;AAC9C,EAAA,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,SAAA,IAAa,aAAA,EAAe;AAC1C,IAAA,OAAO,oBAAA,CAAa,IAAA,CAAK,EAAE,KAAA,EAAO,8BAA8B,CAAA,EAAG,EAAE,MAAA,EAAQ,IAAI,CAAC,CAAA;AAAA,EACpF;AAEA,EAAA,MAAM,SAAA,EAAW,OAAA,CAAQ,OAAA,CAAQ,QAAA;AACjC,EAAA,MAAM,MAAA,EAAQ,QAAA,CAAS,OAAA,CAAQ,mBAAA,EAAqB,EAAE,CAAA;AAGtD,EAAA,GAAA,CAAI,MAAA,IAAU,cAAA,EAAgB;AAC5B,IAAA,OAAO,iBAAA,CAAkB,CAAA;AAAA,EAC3B;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,OAAA,GAAU,KAAA,CAAM,UAAA,CAAW,MAAM,CAAA,EAAG;AAChD,IAAA,OAAO,UAAA,CAAW,OAAO,CAAA;AAAA,EAC3B;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,MAAA,EAAQ;AACpB,IAAA,OAAO,UAAA,CAAW,CAAA;AAAA,EACpB;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,cAAA,EAAgB;AAC5B,IAAA,OAAO,iBAAA,CAAkB,CAAA;AAAA,EAC3B;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,eAAA,EAAiB;AAC7B,IAAA,OAAO,kBAAA,CAAmB,OAAO,CAAA;AAAA,EACnC;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,QAAA,EAAU;AACtB,IAAA,OAAO,YAAA,CAAa,OAAO,CAAA;AAAA,EAC7B;AAEA,EAAA,OAAO,oBAAA,CAAa,IAAA,CAAK,EAAE,KAAA,EAAO,YAAY,CAAA,EAAG,EAAE,MAAA,EAAQ,IAAI,CAAC,CAAA;AAClE;AAKA,MAAA,SAAsB,IAAA,CAAK,OAAA,EAAsB;AAC/C,EAAA,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,SAAA,IAAa,aAAA,EAAe;AAC1C,IAAA,OAAO,oBAAA,CAAa,IAAA,CAAK,EAAE,KAAA,EAAO,8BAA8B,CAAA,EAAG,EAAE,MAAA,EAAQ,IAAI,CAAC,CAAA;AAAA,EACpF;AAEA,EAAA,MAAM,SAAA,EAAW,OAAA,CAAQ,OAAA,CAAQ,QAAA;AACjC,EAAA,MAAM,MAAA,EAAQ,QAAA,CAAS,OAAA,CAAQ,mBAAA,EAAqB,EAAE,CAAA;AAGtD,EAAA,GAAA,CAAI,MAAA,IAAU,QAAA,EAAU;AACtB,IAAA,OAAO,YAAA,CAAa,OAAO,CAAA;AAAA,EAC7B;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,QAAA,EAAU;AACtB,IAAA,OAAO,YAAA,CAAa,OAAO,CAAA;AAAA,EAC7B;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,MAAA,EAAQ;AACpB,IAAA,OAAO,UAAA,CAAW,OAAO,CAAA;AAAA,EAC3B;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,WAAA,EAAa;AACzB,IAAA,OAAO,eAAA,CAAgB,OAAO,CAAA;AAAA,EAChC;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,aAAA,EAAe;AAC3B,IAAA,OAAO,sBAAA,CAAuB,CAAA;AAAA,EAChC;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,eAAA,EAAiB;AAC7B,IAAA,OAAO,kBAAA,CAAmB,OAAO,CAAA;AAAA,EACnC;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,QAAA,EAAU;AACtB,IAAA,OAAO,YAAA,CAAa,OAAO,CAAA;AAAA,EAC7B;AAGA,EAAA,GAAA,CAAI,MAAA,IAAU,MAAA,EAAQ;AACpB,IAAA,OAAO,UAAA,CAAW,OAAO,CAAA;AAAA,EAC3B;AAEA,EAAA,OAAO,oBAAA,CAAa,IAAA,CAAK,EAAE,KAAA,EAAO,YAAY,CAAA,EAAG,EAAE,MAAA,EAAQ,IAAI,CAAC,CAAA;AAClE;AAKA,MAAA,SAAsB,MAAA,CAAO,OAAA,EAAsB;AACjD,EAAA,GAAA,CAAI,OAAA,CAAQ,GAAA,CAAI,SAAA,IAAa,aAAA,EAAe;AAC1C,IAAA,OAAO,oBAAA,CAAa,IAAA,CAAK,EAAE,KAAA,EAAO,8BAA8B,CAAA,EAAG,EAAE,MAAA,EAAQ,IAAI,CAAC,CAAA;AAAA,EACpF;AAEA,EAAA,OAAO,YAAA,CAAa,OAAO,CAAA;AAC7B;AAMA,MAAA,SAAe,UAAA,CAAW,OAAA,EAAsB;AAC9C,EAAA,MAAM,aAAA,EAAe,OAAA,CAAQ,OAAA,CAAQ,YAAA;AACrC,EAAA,MAAM,cAAA,EAAgB,YAAA,CAAa,GAAA,CAAI,MAAM,EAAA,GAAK,QAAA;AAElD,EAAA,IAAI;AACF,IAAA,MAAM,SAAA,EAAW,aAAA,CAAc,OAAA,CAAQ,OAAA,EAAS,EAAE,CAAA;AAClD,IAAA,MAAM,aAAA,EAAe,cAAA,CAAK,IAAA,CAAK,OAAA,CAAQ,GAAA,CAAI,CAAA,EAAG,QAAQ,CAAA;AAEtD,IAAA,GAAA,CAAI,CAAC,YAAA,CAAa,UAAA,CAAW,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,EAAG;AAC3C,MAAA,OAAO,oBAAA,CAAa,IAAA,CAAK,EAAE,KAAA,EAAO,eAAe,CAAA,EAAG,EAAE,MAAA,EAAQ,IAAI,CAAC,CAAA;AAAA,IACrE;AAEA,IAAA,MAAM,MAAA,EAAoB,CAAC,CAAA;AAC3B,IAAA,MAAM,QAAA,EAAU,MAAM,YAAA,CAAG,OAAA,CAAQ,YAAA,EAAc,EAAE,aAAA,EAAe,KAAK,CAAC,CAAA;AAEtE,IAAA,IAAA,CAAA,MAAW,MAAA,GAAS,OAAA,EAAS;AAC3B,MAAA,GAAA,CAAI,KAAA,CAAM,IAAA,CAAK,UAAA,CAAW,GAAG,CAAA,EAAG,QAAA;AAEhC,MAAA,MAAM,SAAA,EAAW,cAAA,CAAK,IAAA,CAAK,QAAA,EAAU,KAAA,CAAM,IAAI,CAAA;AAE/C,MAAA,GAAA,CAAI,KAAA,CAAM,WAAA,CAAY,CAAA,EAAG;AAEvB,QAAA,MAAM,YAAA,EAAc,MAAM,cAAA,CAAe,cAAA,CAAK,IAAA,CAAK,YAAA,EAAc,KAAA,CAAM,IAAI,CAAC,CAAA;AAC5E,QAAA,KAAA,CAAM,IAAA,CAAK;AAAA,UACT,IAAA,EAAM,KAAA,CAAM,IAAA;AAAA,UACZ,IAAA,EAAM,QAAA;AAAA,UACN,IAAA,EAAM,QAAA;AAAA,UACN,SAAA,EAAW,WAAA,CAAY,SAAA;AAAA,UACvB,SAAA,EAAW,WAAA,CAAY;AAAA,QACzB,CAAC,CAAA;AAAA,MACH,EAAA,KAAA,GAAA,CAAW,WAAA,CAAY,KAAA,CAAM,IAAI,CAAA,EAAG;AAClC,QAAA,MAAM,SAAA,EAAW,cAAA,CAAK,IAAA,CAAK,YAAA,EAAc,KAAA,CAAM,IAAI,CAAA;AACnD,QAAA,MAAM,MAAA,EAAQ,MAAM,YAAA,CAAG,IAAA,CAAK,QAAQ,CAAA;AACpC,QAAA,MAAM,QAAA,EAAU,WAAA,CAAY,KAAA,CAAM,IAAI,CAAA;AAEtC,QAAA,IAAI,SAAA;AACJ,QAAA,IAAI,aAAA,EAAe,KAAA;AACnB,QAAA,IAAI,UAAA;AAEJ,QAAA,GAAA,CAAI,OAAA,EAAS;AACX,UAAA,MAAM,aAAA,EAAe,QAAA,CAAS,OAAA,CAAQ,YAAA,EAAc,EAAE,CAAA;AAGtD,UAAA,GAAA,CAAI,aAAA,IAAiB,SAAA,GAAY,YAAA,CAAa,UAAA,CAAW,SAAS,CAAA,EAAG;AACnE,YAAA,UAAA,EAAY,QAAA,CAAS,OAAA,CAAQ,QAAA,EAAU,EAAE,CAAA;AACzC,YAAA,aAAA,EAAe,IAAA;AAAA,UACjB,EAAA,KAAO;AAEL,YAAA,MAAM,IAAA,EAAM,cAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA,CAAE,WAAA,CAAY,CAAA;AACjD,YAAA,MAAM,SAAA,EAAW,cAAA,CAAK,QAAA,CAAS,KAAA,CAAM,IAAA,EAAM,GAAG,CAAA;AAC9C,YAAA,MAAM,aAAA,EAAe,aAAA,EAAe,CAAA,OAAA,EAAU,YAAY,CAAA,EAAA;AACF,YAAA;AACC,YAAA;AAErD,YAAA;AAC2B,cAAA;AAEgB,cAAA;AAC9B,cAAA;AACT,YAAA;AAEmC,cAAA;AAC1B,cAAA;AACjB,YAAA;AACF,UAAA;AAGgD,UAAA;AAC1C,YAAA;AAC8C,cAAA;AACT,cAAA;AACkB,gBAAA;AACzD,cAAA;AACM,YAAA;AAER,YAAA;AACF,UAAA;AACF,QAAA;AAEW,QAAA;AACG,UAAA;AACN,UAAA;AACA,UAAA;AACM,UAAA;AACZ,UAAA;AACA,UAAA;AACA,UAAA;AACD,QAAA;AACH,MAAA;AACF,IAAA;AAEkC,IAAA;AACpB,EAAA;AACkC,IAAA;AACkB,IAAA;AACpE,EAAA;AACF;AAEkD;AACX,EAAA;AACiB,EAAA;AAEhC,EAAA;AACkB,IAAA;AACxC,EAAA;AAEI,EAAA;AACyB,IAAA;AACwB,IAAA;AAEwB,IAAA;AACrE,MAAA;AAC2D,QAAA;AAEhC,QAAA;AACK,UAAA;AAEU,UAAA;AACgB,UAAA;AACE,UAAA;AAEnC,UAAA;AAEO,YAAA;AACO,YAAA;AACH,UAAA;AAEU,YAAA;AACN,cAAA;AAEhC,cAAA;AACe,cAAA;AACf,cAAA;AAG6C,cAAA;AACH,cAAA;AACA,cAAA;AACU,cAAA;AACC,cAAA;AAErD,cAAA;AAC2B,gBAAA;AACgB,gBAAA;AAC9B,gBAAA;AACT,cAAA;AACqB,gBAAA;AACZ,gBAAA;AACjB,cAAA;AAGgD,cAAA;AAC1C,gBAAA;AAC8C,kBAAA;AACT,kBAAA;AACS,oBAAA;AAChD,kBAAA;AACM,gBAAA;AAER,gBAAA;AACF,cAAA;AAEW,cAAA;AACG,gBAAA;AACN,gBAAA;AACA,gBAAA;AACM,gBAAA;AACZ,gBAAA;AACA,gBAAA;AACA,gBAAA;AACD,cAAA;AACH,YAAA;AACF,UAAA;AACF,QAAA;AACM,MAAA;AAER,MAAA;AACF,IAAA;AAE6B,IAAA;AAEK,IAAA;AACpB,EAAA;AAC0B,IAAA;AAC0B,IAAA;AACpE,EAAA;AACF;AAEqG;AACnF,EAAA;AACA,EAAA;AAEsC,EAAA;AAChD,IAAA;AAC2D,MAAA;AAChC,MAAA;AACK,QAAA;AACU,QAAA;AACjB,QAAA;AACE,UAAA;AACS,QAAA;AAClC,UAAA;AACoC,UAAA;AACjB,UAAA;AACrB,QAAA;AACF,MAAA;AACM,IAAA;AAAsB,IAAA;AAChC,EAAA;AAE2B,EAAA;AACG,EAAA;AAChC;AAE4B;AACtB,EAAA;AAC0B,IAAA;AAEM,IAAA;AACF,IAAA;AACF,IAAA;AAE+B,IAAA;AACxB,IAAA;AAEW,IAAA;AACK,MAAA;AACnB,QAAA;AAChC,MAAA;AACF,IAAA;AAE8E,IAAA;AACxE,MAAA;AAC2D,QAAA;AAEhC,QAAA;AACK,UAAA;AAEU,UAAA;AACkB,UAAA;AAEnC,UAAA;AACQ,YAAA;AACG,UAAA;AACG,YAAA;AACF,YAAA;AACH,cAAA;AACzB,YAAA;AACqB,cAAA;AAC5B,YAAA;AACF,UAAA;AACF,QAAA;AACM,MAAA;AAER,MAAA;AACF,IAAA;AAEuB,IAAA;AAEiC,IAAA;AACM,MAAA;AACG,QAAA;AACzD,QAAA;AACsB,UAAA;AAClB,QAAA;AACkB,UAAA;AACgC,YAAA;AACxD,UAAA;AACF,QAAA;AACF,MAAA;AACF,IAAA;AAEyB,IAAA;AACe,MAAA;AACf,MAAA;AACvB,MAAA;AACA,MAAA;AACD,IAAA;AACa,EAAA;AACwB,IAAA;AAC0B,IAAA;AAClE,EAAA;AACF;AAEkD;AAC5C,EAAA;AACsC,IAAA;AACR,IAAA;AACqB,IAAA;AAE1C,IAAA;AACiD,MAAA;AAC5D,IAAA;AAEqC,IAAA;AACL,IAAA;AAEV,IAAA;AACyC,IAAA;AAChB,IAAA;AAGX,IAAA;AACd,IAAA;AACiB,IAAA;AAEX,IAAA;AAGV,IAAA;AACD,MAAA;AACjB,IAAA;AAIkB,IAAA;AACW,IAAA;AACb,MAAA;AAC6B,IAAA;AACG,MAAA;AAChD,IAAA;AAGmE,IAAA;AAC7C,MAAA;AACT,QAAA;AACK,QAAA;AAChB,MAAA;AACF,IAAA;AAGgE,IAAA;AACnB,IAAA;AACY,IAAA;AAG3C,IAAA;AACa,MAAA;AACd,QAAA;AACA,QAAA;AACsD,QAAA;AAChE,MAAA;AACH,IAAA;AAGkE,IAAA;AAEnC,IAAA;AACT,MAAA;AACuC,QAAA;AAC3C,QAAA;AAChB,MAAA;AACF,IAAA;AAGgE,IAAA;AAClB,IAAA;AAE1B,IAAA;AACC,IAAA;AACN,IAAA;AACK,IAAA;AAC8D,IAAA;AAC1C,MAAA;AACC,MAAA;AACC,MAAA;AACD,MAAA;AACzC,IAAA;AAGgE,IAAA;AAErD,IAAA;AAEsC,MAAA;AACZ,MAAA;AACyB,MAAA;AAC9B,MAAA;AACC,MAAA;AACD,MAAA;AACD,IAAA;AAEK,MAAA;AACY,MAAA;AACZ,MAAA;AACE,MAAA;AAGQ,MAAA;AACA,MAAA;AACO,MAAA;AAE/B,MAAA;AACsC,QAAA;AACnD,MAAA;AACoD,QAAA;AAC3D,MAAA;AAC4D,MAAA;AAGK,MAAA;AAC3B,QAAA;AACL,QAAA;AACkB,UAAA;AAC/C,UAAA;AACF,QAAA;AAE+B,QAAA;AACc,QAAA;AACQ,QAAA;AACF,QAAA;AAE/B,QAAA;AACoC,UAAA;AACjD,QAAA;AACkD,UAAA;AACzD,QAAA;AAE+B,QAAA;AAC2B,UAAA;AACjD,UAAA;AACC,UAAA;AACV,QAAA;AACF,MAAA;AAIyB,MAAA;AAKuC,MAAA;AAGjB,MAAA;AACgB,MAAA;AACjE,IAAA;AAE0B,IAAA;AACd,MAAA;AACF,QAAA;AACC,QAAA;AACC,QAAA;AACS,QAAA;AACnB,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACK,MAAA;AACP,IAAA;AAE4B,IAAA;AACT,IAAA;AAE+C,IAAA;AACpD,EAAA;AAC0B,IAAA;AACiB,IAAA;AACU,IAAA;AACrE,EAAA;AACF;AAEkD;AAC5C,EAAA;AACmC,IAAA;AAEsB,IAAA;AACE,MAAA;AAC7D,IAAA;AAE4B,IAAA;AACD,IAAA;AACD,IAAA;AAEI,IAAA;AACxB,MAAA;AACmC,QAAA;AACI,UAAA;AACvC,UAAA;AACF,QAAA;AAEsD,QAAA;AACd,QAAA;AAEf,QAAA;AACsB,UAAA;AAK1C,UAAA;AAEyC,UAAA;AACd,YAAA;AACJ,cAAA;AACxB,YAAA;AACF,UAAA;AACK,QAAA;AACuB,UAAA;AAGiC,UAAA;AAEtC,UAAA;AAE4B,YAAA;AACf,YAAA;AACvB,YAAA;AAE0C,cAAA;AACG,gBAAA;AAChD,gBAAA;AAA0B,kBAAA;AAAU,gBAAA;AAAe,gBAAA;AACzD,cAAA;AAC2B,cAAA;AAC7B,YAAA;AACF,UAAA;AAEF,QAAA;AAEqB,QAAA;AACP,MAAA;AACsC,QAAA;AAChC,QAAA;AACtB,MAAA;AACF,IAAA;AAEmB,IAAA;AAEM,IAAA;AACd,MAAA;AACT,MAAA;AACqC,MAAA;AACtC,IAAA;AACa,EAAA;AAC0B,IAAA;AACwB,IAAA;AAClE,EAAA;AACF;AAEgD;AAChB,EAAA;AACE,EAAA;AACI,EAAA;AACL,EAAA;AACD,EAAA;AAEuC,EAAA;AAC/C,IAAA;AACT,MAAA;AACK,MAAA;AAChB,IAAA;AACF,EAAA;AAEI,EAAA;AACuC,IAAA;AAEgB,IAAA;AACS,MAAA;AAClE,IAAA;AAE4B,IAAA;AAEJ,IAAA;AACd,MAAA;AACsB,MAAA;AACc,MAAA;AAC7C,IAAA;AAEyB,IAAA;AACA,IAAA;AAEQ,IAAA;AACE,MAAA;AACtB,MAAA;AACwC,QAAA;AAClD,QAAA;AACF,MAAA;AAEuB,MAAA;AACD,QAAA;AACpB,QAAA;AACF,MAAA;AAEI,MAAA;AACiD,QAAA;AACI,UAAA;AACP,UAAA;AAErC,UAAA;AACc,YAAA;AACX,cAAA;AAC4B,cAAA;AAC9B,cAAA;AACmC,cAAA;AAC1C,YAAA;AACH,UAAA;AACF,QAAA;AAEY,QAAA;AACF,UAAA;AACC,UAAA;AACwB,UAAA;AACnC,QAAA;AAEmD,QAAA;AACI,UAAA;AACjD,UAAA;AAA2B,YAAA;AAAU,UAAA;AAAe,UAAA;AAC1D,QAAA;AAEoB,QAAA;AACN,MAAA;AACoC,QAAA;AAC9B,QAAA;AACtB,MAAA;AACF,IAAA;AAEmB,IAAA;AAEM,IAAA;AACd,MAAA;AACT,MAAA;AACqC,MAAA;AACtC,IAAA;AACa,EAAA;AACwB,IAAA;AACyB,IAAA;AACjE,EAAA;AACF;AAEqD;AAC/C,EAAA;AACuC,IAAA;AAEgB,IAAA;AACS,MAAA;AAClE,IAAA;AAE4B,IAAA;AACC,IAAA;AACH,IAAA;AAEQ,IAAA;AAC5B,MAAA;AACE,QAAA;AAC4B,QAAA;AAGwB,QAAA;AAEpD,QAAA;AACqC,UAAA;AACjC,QAAA;AAEK,UAAA;AAC0C,YAAA;AAC/C,YAAA;AAC0C,cAAA;AACtC,YAAA;AACiB,cAAA;AAC6B,gBAAA;AAC7C,cAAA;AACW,gBAAA;AAClB,cAAA;AACF,YAAA;AACK,UAAA;AACwC,YAAA;AAC/C,UAAA;AACF,QAAA;AAGY,QAAA;AACwB,UAAA;AACY,UAAA;AACN,UAAA;AAEhC,UAAA;AACI,YAAA;AACF,cAAA;AACmB,cAAA;AACE,cAAA;AACX,cAAA;AAClB,YAAA;AACO,YAAA;AACiC,cAAA;AACC,cAAA;AACC,cAAA;AACD,cAAA;AACzC,YAAA;AACU,YAAA;AACK,YAAA;AACV,YAAA;AACP,UAAA;AACF,QAAA;AAE+D,QAAA;AACvC,QAAA;AAED,QAAA;AACS,UAAA;AACK,UAAA;AACrC,QAAA;AAEuB,QAAA;AACT,MAAA;AACyC,QAAA;AACnC,QAAA;AACtB,MAAA;AACF,IAAA;AAEmB,IAAA;AAEM,IAAA;AACd,MAAA;AACT,MAAA;AACqC,MAAA;AACtC,IAAA;AACa,EAAA;AAC6B,IAAA;AACuB,IAAA;AACpE,EAAA;AACF;AAEmC;AAC7B,EAAA;AAC2B,IAAA;AAG0D,IAAA;AACjF,MAAA;AAC2D,QAAA;AAEhC,QAAA;AACK,UAAA;AAEU,UAAA;AACkB,UAAA;AAGD,UAAA;AAElC,UAAA;AACiB,YAAA;AACN,UAAA;AACZ,YAAA;AACxB,UAAA;AACF,QAAA;AACM,MAAA;AAER,MAAA;AACF,IAAA;AAEmD,IAAA;AACnB,IAAA;AAEP,IAAA;AACN,MAAA;AACT,MAAA;AACT,IAAA;AACa,EAAA;AACgC,IAAA;AACkB,IAAA;AAClE,EAAA;AACF;AAEwD;AAClD,EAAA;AACmC,IAAA;AACU,IAAA;AAE5B,IAAA;AAC4C,MAAA;AAC/D,IAAA;AAEsC,IAAA;AACT,IAAA;AAEoD,IAAA;AAC3E,MAAA;AAC2D,QAAA;AAEhC,QAAA;AACK,UAAA;AAEU,UAAA;AACkB,UAAA;AAEnC,UAAA;AACW,YAAA;AACA,UAAA;AACZ,YAAA;AACxB,UAAA;AACF,QAAA;AACM,MAAA;AAER,MAAA;AACF,IAAA;AAE8B,IAAA;AAEwB,MAAA;AAGK,MAAA;AAEP,MAAA;AACT,MAAA;AAC3C,IAAA;AAEyB,IAAA;AACN,MAAA;AACT,MAAA;AACT,IAAA;AACa,EAAA;AACqC,IAAA;AACgB,IAAA;AACrE,EAAA;AACF;AAEwC;AACN,EAAA;AAEE,EAAA;AACR,IAAA;AACc,MAAA;AAC6B,QAAA;AAAA;AAAO;AACxE,MAAA;AAEI,MAAA;AAC0B,QAAA;AACC,QAAA;AACH,QAAA;AACQ,QAAA;AAG2B,QAAA;AAE0B,QAAA;AACjF,UAAA;AACqD,YAAA;AAE1B,YAAA;AACK,cAAA;AAEU,cAAA;AACc,cAAA;AAGA,cAAA;AAE/B,cAAA;AACiB,gBAAA;AACN,cAAA;AACO,gBAAA;AAC3C,cAAA;AACF,YAAA;AACM,UAAA;AAER,UAAA;AACF,QAAA;AAEmD,QAAA;AACnB,QAAA;AAER,QAAA;AACU,QAAA;AAGS,QAAA;AACJ,UAAA;AAE3B,UAAA;AACF,YAAA;AACO,YAAA;AACb,YAAA;AAC2C,YAAA;AAC9B,YAAA;AACd,UAAA;AAEG,UAAA;AACuC,YAAA;AACC,YAAA;AACpB,YAAA;AAEX,YAAA;AAEwB,cAAA;AACqB,cAAA;AACR,cAAA;AAEZ,cAAA;AACa,cAAA;AACZ,cAAA;AAEiB,cAAA;AACjC,cAAA;AACP,gBAAA;AACK,kBAAA;AACN,kBAAA;AACC,kBAAA;AACS,kBAAA;AACnB,gBAAA;AACO,gBAAA;AACuC,kBAAA;AACC,kBAAA;AACC,kBAAA;AACD,kBAAA;AAC/C,gBAAA;AACU,gBAAA;AACK,gBAAA;AACV,gBAAA;AACP,cAAA;AACK,YAAA;AAEgC,cAAA;AACU,cAAA;AACnC,gBAAA;AACK,kBAAA;AACN,kBAAA;AACC,kBAAA;AACS,kBAAA;AACnB,gBAAA;AACO,gBAAA;AACiC,kBAAA;AACC,kBAAA;AACC,kBAAA;AACD,kBAAA;AACzC,gBAAA;AACU,gBAAA;AACK,gBAAA;AACV,gBAAA;AACP,cAAA;AAEkD,cAAA;AAC/B,cAAA;AACrB,YAAA;AAEkB,YAAA;AACJ,UAAA;AACkC,YAAA;AACjC,YAAA;AACjB,UAAA;AACF,QAAA;AAGsC,QAAA;AAED,QAAA;AACW,QAAA;AACK,UAAA;AACnB,YAAA;AAChC,UAAA;AACF,QAAA;AAEkF,QAAA;AAC5E,UAAA;AACqD,YAAA;AAE1B,YAAA;AACK,cAAA;AAEU,cAAA;AACc,cAAA;AAE/B,cAAA;AACY,gBAAA;AACD,cAAA;AACG,gBAAA;AACF,gBAAA;AAC7B,kBAAA;AACsB,oBAAA;AACM,oBAAA;AAClB,kBAAA;AACuC,oBAAA;AACrD,kBAAA;AACF,gBAAA;AACF,cAAA;AACF,YAAA;AACM,UAAA;AAER,UAAA;AACF,QAAA;AAE6D,QAAA;AAClC,QAAA;AAGmC,QAAA;AACxD,UAAA;AACqD,YAAA;AACzC,YAAA;AAEe,YAAA;AACF,cAAA;AAC6B,gBAAA;AACxB,gBAAA;AACvB,cAAA;AACK,gBAAA;AACZ,cAAA;AACF,YAAA;AAEkC,YAAA;AACd,cAAA;AACpB,YAAA;AAEO,YAAA;AACD,UAAA;AACC,YAAA;AACT,UAAA;AACF,QAAA;AAE+B,QAAA;AACZ,QAAA;AAET,QAAA;AACF,UAAA;AACe,UAAA;AACU,UAAA;AAChB,UAAA;AAChB,QAAA;AACa,MAAA;AAC+B,QAAA;AACkB,QAAA;AAC/D,MAAA;AACiB,QAAA;AACnB,MAAA;AACF,IAAA;AACD,EAAA;AAE2B,EAAA;AACjB,IAAA;AACS,MAAA;AACC,MAAA;AACH,MAAA;AAChB,IAAA;AACD,EAAA;AACH;AAM+C;AACkB,EAAA;AACjC,EAAA;AACnB,IAAA;AACA,IAAA;AAC2B,IAAA;AAC3B,IAAA;AACX,EAAA;AAEI,EAAA;AACiD,IAAA;AAClB,IAAA;AAGuB,IAAA;AAC/C,MAAA;AACT,IAAA;AAGoD,IAAA;AACK,IAAA;AACrC,MAAA;AAE2C,MAAA;AAC1C,MAAA;AACP,QAAA;AACF,UAAA;AACW,UAAA;AACC,UAAA;AACR,UAAA;AACZ,QAAA;AACQ,QAAA;AACY,QAAA;AACL,QAAA;AAC8C,QAAA;AAC/D,MAAA;AACF,IAAA;AACO,IAAA;AACD,EAAA;AACC,IAAA;AACT,EAAA;AACF;AAEyD;AACP,EAAA;AACL,EAAA;AAGnB,EAAA;AACgC,EAAA;AACL,IAAA;AAC/B,IAAA;AACY,MAAA;AACC,MAAA;AACL,MAAA;AAC1B,IAAA;AACuB,IAAA;AACD,MAAA;AACtB,IAAA;AACF,EAAA;AACgD,EAAA;AACU,EAAA;AAC5D;AAEgD;AACC,EAAA;AACmB,EAAA;AACpE;AAEgD;AACC,EAAA;AAEgB,EAAA;AAEM,EAAA;AAEA,EAAA;AAElC,EAAA;AAC5B,EAAA;AACT;AAEkD;AACD,EAAA;AAClC,EAAA;AACN,IAAA;AACA,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACJ,IAAA;AACI,MAAA;AACT,IAAA;AACS,MAAA;AACX,EAAA;AACF;AAMuB;AACa,EAAA;AACY,EAAA;AACN,EAAA;AACE,EAAA;AAEqB,EAAA;AAChB,EAAA;AACT,EAAA;AAE0B,EAAA;AAClB,EAAA;AAEoC,EAAA;AACjB,IAAA;AACxB,IAAA;AACC,IAAA;AACD,IAAA;AACzC,EAAA;AAEsB,EAAA;AACa,EAAA;AACiC,EAAA;AACN,EAAA;AAEnD,EAAA;AAC+C,IAAA;AACnD,EAAA;AACoD,IAAA;AAC3D,EAAA;AACyC,EAAA;AAE2B,EAAA;AAC9B,IAAA;AACL,IAAA;AACkB,MAAA;AAC/C,MAAA;AACF,IAAA;AAE+B,IAAA;AACc,IAAA;AACQ,IAAA;AACa,IAAA;AACJ,IAAA;AAEnD,IAAA;AACsD,MAAA;AAC1D,IAAA;AAC2D,MAAA;AAClE,IAAA;AAE+B,IAAA;AACA,MAAA;AACtB,MAAA;AACC,MAAA;AACV,IAAA;AACF,EAAA;AAGyB,EAAA;AAKwC,EAAA;AAElB,EAAA;AACsB,EAAA;AAE9D,EAAA;AACF,IAAA;AACO,IAAA;AACC,MAAA;AACF,MAAA;AACC,MAAA;AACS,MAAA;AACnB,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEsE;AACtC,EAAA;AACE,EAAA;AACI,EAAA;AACL,EAAA;AAEoC,EAAA;AAC9B,IAAA;AACrC,EAAA;AAEwB,EAAA;AACd,IAAA;AACsB,IAAA;AACc,IAAA;AAC7C,EAAA;AAEyB,EAAA;AACH,IAAA;AACX,MAAA;AAC2B,MAAA;AACpC,IAAA;AACH,EAAA;AAEwB,EAAA;AACE,EAAA;AACQ,EAAA;AACF,IAAA;AAChC,EAAA;AAC2B,EAAA;AAC7B;AAE6D;AAC7B,EAAA;AACE,EAAA;AACI,EAAA;AACL,EAAA;AAEoC,EAAA;AAC9B,IAAA;AACrC,EAAA;AAEwB,EAAA;AACd,IAAA;AACsB,IAAA;AACc,IAAA;AAC7C,EAAA;AAEkD,EAAA;AACiB,IAAA;AACpB,IAAA;AAErC,IAAA;AACc,MAAA;AACX,QAAA;AAC4B,QAAA;AAC9B,QAAA;AACmC,QAAA;AAC1C,MAAA;AACH,IAAA;AACF,EAAA;AACF;AAEkE;AACb,EAAA;AACiB,IAAA;AAC9D,IAAA;AACuB,MAAA;AACnB,IAAA;AAER,IAAA;AACF,EAAA;AACF;AAMwD;AAClD,EAAA;AAC8C,IAAA;AAET,IAAA;AAC4B,MAAA;AACnE,IAAA;AAG6D,IAAA;AACzC,IAAA;AAC2C,MAAA;AAC/D,IAAA;AAE6D,IAAA;AACM,IAAA;AAGH,IAAA;AACA,MAAA;AAChE,IAAA;AAGI,IAAA;AACwB,MAAA;AACQ,MAAA;AAC5B,IAAA;AAER,IAAA;AAE8C,IAAA;AAEY,IAAA;AAC5C,EAAA;AACiC,IAAA;AACkB,IAAA;AACnE,EAAA;AACF;AAEkD;AAC5C,EAAA;AAC8C,IAAA;AAEtB,IAAA;AACU,MAAA;AACpC,IAAA;AAGgE,IAAA;AAC5C,IAAA;AAC4C,MAAA;AAChE,IAAA;AAE4C,IAAA;AACa,IAAA;AACX,IAAA;AACY,IAAA;AAGQ,IAAA;AACF,MAAA;AAChE,IAAA;AAGI,IAAA;AAC6B,MAAA;AACzB,IAAA;AAC0D,MAAA;AAClE,IAAA;AAGI,IAAA;AAC6B,MAAA;AACG,MAAA;AAC5B,IAAA;AAER,IAAA;AAG2C,IAAA;AACf,IAAA;AACgC,IAAA;AAGZ,IAAA;AAGnC,IAAA;AACiB,MAAA;AAC4B,MAAA;AACS,MAAA;AAGT,MAAA;AACH,QAAA;AAER,UAAA;AAGS,UAAA;AACD,UAAA;AACR,UAAA;AACc,UAAA;AACJ,UAAA;AACK,UAAA;AAEK,UAAA;AACL,YAAA;AACI,YAAA;AACA,YAAA;AACD,YAAA;AACA,YAAA;AAErD,YAAA;AACwC,cAAA;AACe,cAAA;AACnD,YAAA;AAER,YAAA;AACF,UAAA;AAGkC,UAAA;AACZ,UAAA;AACA,UAAA;AACtB,UAAA;AACF,QAAA;AACF,MAAA;AAEmB,MAAA;AACrB,IAAA;AAE+D,IAAA;AACZ,IAAA;AACrC,EAAA;AAC0B,IAAA;AAC0B,IAAA;AACpE,EAAA;AACF;AAEgD;AAC1C,EAAA;AACgD,IAAA;AAES,IAAA;AACG,MAAA;AAC9D,IAAA;AAEqD,IAAA;AACc,MAAA;AACnE,IAAA;AAEuD,IAAA;AACF,IAAA;AAGQ,IAAA;AACE,MAAA;AAC/D,IAAA;AAG2D,IAAA;AACvB,MAAA;AACpC,IAAA;AAGI,IAAA;AACiD,MAAA;AACrB,MAAA;AACM,QAAA;AACpC,MAAA;AACM,IAAA;AAC2D,MAAA;AACnE,IAAA;AAEyB,IAAA;AACC,IAAA;AACE,IAAA;AACV,IAAA;AAEY,IAAA;AACiB,MAAA;AACS,MAAA;AACf,MAAA;AACwB,MAAA;AAGF,MAAA;AACV,QAAA;AACjD,QAAA;AACF,MAAA;AAGI,MAAA;AAC0B,QAAA;AACtB,MAAA;AAC6B,QAAA;AACnC,QAAA;AACF,MAAA;AAGI,MAAA;AAC6B,QAAA;AACwB,QAAA;AACvD,QAAA;AACM,MAAA;AAER,MAAA;AAEI,MAAA;AAC2C,QAAA;AAGF,QAAA;AACE,QAAA;AACa,UAAA;AACE,UAAA;AAEF,UAAA;AACH,YAAA;AACR,cAAA;AAGE,cAAA;AACA,cAAA;AACV,cAAA;AACW,cAAA;AACW,cAAA;AACA,cAAA;AAGR,cAAA;AAES,cAAA;AACA,gBAAA;AACL,gBAAA;AACI,gBAAA;AACA,gBAAA;AAEjD,gBAAA;AACwC,kBAAA;AACO,kBAAA;AAC3C,gBAAA;AAER,gBAAA;AACF,cAAA;AAGkC,cAAA;AACZ,cAAA;AACA,cAAA;AACR,cAAA;AACd,cAAA;AACF,YAAA;AACF,UAAA;AACF,QAAA;AAEmB,QAAA;AACL,MAAA;AAC0B,QAAA;AAC1C,MAAA;AACF,IAAA;AAEiB,IAAA;AACI,MAAA;AACrB,IAAA;AAEyB,IAAA;AACI,MAAA;AAC3B,MAAA;AACqC,MAAA;AACtC,IAAA;AACa,EAAA;AACwB,IAAA;AACwB,IAAA;AAChE,EAAA;AACF;AAEmC;AAC7B,EAAA;AACiD,IAAA;AACe,IAAA;AAEsB,IAAA;AAClF,MAAA;AAC2D,QAAA;AAEhC,QAAA;AACD,UAAA;AACM,UAAA;AAEoB,UAAA;AAES,UAAA;AAChD,UAAA;AACuB,YAAA;AACtB,YAAA;AACZ,YAAA;AACD,UAAA;AAGyC,UAAA;AAC5C,QAAA;AACM,MAAA;AAER,MAAA;AACF,IAAA;AAGyD,IAAA;AAE3B,IAAA;AAEM,IAAA;AACtB,EAAA;AACgC,IAAA;AACkB,IAAA;AAClE,EAAA;AACF;AD7ZuE;AACA;AACA;AACA;AACA","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, LeanMeta, LeanImageEntry } from './types'\n\n// Default thumbnail sizes with their suffixes\nconst DEFAULT_SIZES: Record<string, { width: number; suffix: string }> = {\n small: { width: 300, suffix: '-sm' },\n medium: { width: 700, suffix: '-md' },\n large: { width: 1400, suffix: '-lg' },\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-folders (must come before 'list' check)\n if (route === 'list-folders') {\n return handleListFolders()\n }\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 // Route: /api/studio/count-images\n if (route === 'count-images') {\n return handleCountImages()\n }\n\n // Route: /api/studio/folder-images\n if (route === 'folder-images') {\n return handleFolderImages(request)\n }\n\n // Route: /api/studio/search\n if (route === 'search') {\n return handleSearch(request)\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 // Route: /api/studio/process-all (streaming)\n if (route === 'process-all') {\n return handleProcessAllStream()\n }\n\n // Route: /api/studio/create-folder\n if (route === 'create-folder') {\n return handleCreateFolder(request)\n }\n\n // Route: /api/studio/rename\n if (route === 'rename') {\n return handleRename(request)\n }\n\n // Route: /api/studio/move\n if (route === 'move') {\n return handleMove(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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const filePath = path.join(absolutePath, entry.name)\n const stats = await fs.stat(filePath)\n const isImage = isImageFile(entry.name)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n \n if (isImage) {\n const relativePath = safePath.replace(/^public\\/?/, '')\n \n // If we're already inside the images folder, these ARE the thumbnails\n if (relativePath === 'images' || relativePath.startsWith('images/')) {\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = true // They are thumbnails themselves\n } else {\n // Check for -sm thumbnail in images folder\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n \n try {\n await fs.access(thumbnailPath)\n // Thumbnail exists\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n // No thumbnail, fall back to original\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = false\n }\n }\n \n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(filePath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n }\n \n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\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 handleSearch(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const query = searchParams.get('q')?.toLowerCase() || ''\n \n if (query.length < 2) {\n return NextResponse.json({ items: [] })\n }\n\n try {\n const items: FileItem[] = []\n const publicDir = path.join(process.cwd(), 'public')\n\n async function searchDir(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 itemPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`\n const itemRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n // Skip public/images folder (generated thumbnails)\n if (itemRelPath === 'images') continue\n await searchDir(fullPath, itemRelPath)\n } else if (isImageFile(entry.name)) {\n // Check if path matches query\n if (itemPath.toLowerCase().includes(query)) {\n const stats = await fs.stat(fullPath)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n\n // Check for -sm thumbnail\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n\n try {\n await fs.access(thumbnailPath)\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n thumbnail = `/${itemRelPath}`\n hasThumbnail = false\n }\n\n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(fullPath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\n })\n }\n }\n }\n } catch {\n // Ignore directory access errors\n }\n }\n\n await searchDir(publicDir, '')\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to search:', error)\n return NextResponse.json({ error: 'Failed to search' }, { status: 500 })\n }\n}\n\nasync function getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\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 // Check if this is an image that can be processed\n const isImage = isImageFile(fileName)\n const isSvg = ext === '.svg'\n const isProcessableImage = isImage && !isSvg\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // For non-image media files, just save and return success\n if (!isImage) {\n return NextResponse.json({ \n success: true, \n message: 'File uploaded successfully (non-image, no thumbnails generated)',\n path: `public/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n })\n }\n \n // For images, generate thumbnails and update meta\n const fullImageKey = relativeDir ? `${relativeDir}/${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 // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\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 // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else if (isProcessableImage) {\n // Raster images: process with sharp and generate thumbnails\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/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${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 as ImageSize] = {\n path: `/images/${relativeDir ? relativeDir + '/' : ''}${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: originalPath,\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 // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\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 // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\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 // If deleting from images/, just delete the file (already done above)\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 try {\n let buffer: Buffer\n let entry = meta.images[imageKey]\n \n // Try to read the original file from public folder\n const originalPath = path.join(process.cwd(), 'public', imageKey)\n \n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n // File not in public folder, try from entry's original path or CDN\n if (entry) {\n const entryOriginalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(entryOriginalPath)\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 } else {\n throw new Error(`File not found: ${imageKey}`)\n }\n }\n\n // If no existing entry, create a minimal one for new images\n if (!entry) {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const stats = await fs.stat(originalPath)\n \n entry = {\n original: {\n path: imageKey,\n width: metadata.width || 0,\n height: metadata.height || 0,\n fileSize: stats.size,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#000000',\n cdn: null,\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\nasync function handleCountImages() {\n try {\n const allImages: string[] = []\n\n // Scan public folder recursively for ALL images, excluding public/images/\n async function scanPublicFolder(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 // Skip the images folder - that's for generated thumbnails\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to count images:', error)\n return NextResponse.json({ error: 'Failed to count images' }, { status: 500 })\n }\n}\n\nasync function handleFolderImages(request: NextRequest) {\n try {\n const searchParams = request.nextUrl.searchParams\n const foldersParam = searchParams.get('folders')\n \n if (!foldersParam) {\n return NextResponse.json({ error: 'No folders provided' }, { status: 400 })\n }\n\n const folders = foldersParam.split(',')\n const allImages: string[] = []\n\n async function scanFolder(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 scanFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n for (const folder of folders) {\n // Folder paths come as \"public/photos\" - we need relative path from public\n const relativePath = folder.replace(/^public\\/?/, '')\n \n // Skip the images folder\n if (relativePath === 'images' || relativePath.startsWith('images/')) continue\n \n const folderPath = path.join(process.cwd(), folder)\n await scanFolder(folderPath, relativePath)\n }\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to get folder images:', error)\n return NextResponse.json({ error: 'Failed to get folder images' }, { status: 500 })\n }\n}\n\nasync function handleProcessAllStream() {\n const encoder = new TextEncoder()\n \n const stream = new ReadableStream({\n async start(controller) {\n const sendEvent = (data: object) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`))\n }\n\n try {\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n const orphansRemoved: string[] = []\n\n // Step 1: Scan public folder for ALL images (excluding public/images/)\n const allImages: Array<{ key: string; fullPath: string }> = []\n\n async function scanPublicFolder(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 // Skip the images folder\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push({ key: relPath, fullPath })\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n const total = allImages.length\n sendEvent({ type: 'start', total })\n\n // Step 2: Process each image (reprocess all, not just unprocessed)\n for (let i = 0; i < allImages.length; i++) {\n const { key, fullPath } = allImages[i]\n \n sendEvent({ \n type: 'progress', \n current: i + 1, \n total, \n percent: Math.round(((i + 1) / total) * 100),\n currentFile: key \n })\n\n try {\n const buffer = await fs.readFile(fullPath)\n const ext = path.extname(key).toLowerCase()\n const isSvg = ext === '.svg'\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const imageDir = path.dirname(key)\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n \n const fileName = path.basename(key)\n const destPath = path.join(imagesPath, fileName)\n await fs.writeFile(destPath, buffer)\n\n const sizePath = `/images/${imageDir === '.' ? '' : imageDir + '/'}${fileName}`\n meta.images[key] = {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\n full: { path: sizePath, width: 0, height: 0 },\n large: { path: sizePath, width: 0, height: 0 },\n medium: { path: sizePath, width: 0, height: 0 },\n small: { path: sizePath, width: 0, height: 0 },\n },\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n } else {\n // Raster image: full processing\n const existingEntry = meta.images[key]\n const baseEntry: ImageEntry = existingEntry || {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n\n const processedEntry = await processImage(buffer, baseEntry, key)\n meta.images[key] = processedEntry\n }\n\n processed.push(key)\n } catch (error) {\n console.error(`Failed to process ${key}:`, error)\n errors.push(key)\n }\n }\n\n // Step 3: Remove orphaned thumbnails\n sendEvent({ type: 'cleanup', message: 'Removing orphaned thumbnails...' })\n \n const trackedPaths = new Set<string>()\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 findOrphans(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 findOrphans(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n try {\n await fs.unlink(fullPath)\n orphansRemoved.push(publicPath)\n } catch (err) {\n console.error(`Failed to remove orphan ${publicPath}:`, err)\n }\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n await findOrphans(imagesDir)\n\n // Step 4: Clean up empty directories\n async function removeEmptyDirs(dir: string): Promise<boolean> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n let isEmpty = true\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n const subDirEmpty = await removeEmptyDirs(path.join(dir, entry.name))\n if (!subDirEmpty) isEmpty = false\n } else {\n isEmpty = false\n }\n }\n\n if (isEmpty && dir !== imagesDir) {\n await fs.rmdir(dir)\n }\n\n return isEmpty\n } catch {\n return true\n }\n }\n\n await removeEmptyDirs(imagesDir)\n await saveMeta(meta)\n\n sendEvent({ \n type: 'complete', \n processed: processed.length, \n orphansRemoved: orphansRemoved.length,\n errors: errors.length,\n })\n } catch (error) {\n console.error('Failed to process all:', error)\n sendEvent({ type: 'error', message: 'Failed to process images' })\n } finally {\n controller.close()\n }\n }\n })\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\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 const emptyMeta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n \n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n // Check if it's the old verbose format\n if (parsed.images && typeof parsed.images === 'object') {\n return parsed\n }\n \n // Convert lean format to internal verbose format\n const meta: StudioMeta = { ...emptyMeta, images: {} }\n for (const [imagePath, entry] of Object.entries(parsed)) {\n const leanEntry = entry as LeanImageEntry\n // Use the path as the key (without leading slash)\n const key = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath\n meta.images[key] = {\n original: {\n path: imagePath,\n width: leanEntry.w,\n height: leanEntry.h,\n fileSize: 0,\n },\n sizes: {} as Record<ImageSize, { path: string; width: number; height: number }>,\n blurhash: leanEntry.blur,\n dominantColor: '',\n cdn: leanEntry.s ? { synced: true, baseUrl: '', syncedAt: '' } : null,\n }\n }\n return meta\n } catch {\n return emptyMeta\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const dataDir = path.join(process.cwd(), '_data')\n await fs.mkdir(dataDir, { recursive: true })\n \n // Convert to lean format and write to _meta.json\n const lean: LeanMeta = {}\n for (const [key, entry] of Object.entries(meta.images)) {\n const imagePath = entry.original?.path || `/${key}`\n lean[imagePath] = {\n w: entry.original?.width || 0,\n h: entry.original?.height || 0,\n blur: entry.blurhash || '',\n }\n if (entry.cdn?.synced) {\n lean[imagePath].s = 1\n }\n }\n const metaPath = path.join(dataDir, '_meta.json')\n await fs.writeFile(metaPath, JSON.stringify(lean, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\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 isPng = ext === '.png'\n const outputExt = isPng ? '.png' : '.jpg'\n const fullFileName = imageDir === '.' ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n \n if (isPng) {\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/${fullFileName}`\n\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${outputExt}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n if (isPng) {\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 as ImageSize] = {\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\n// ============================================================================\n// FOLDER MANAGEMENT HANDLERS\n// ============================================================================\n\nasync function handleCreateFolder(request: NextRequest) {\n try {\n const { parentPath, name } = await request.json()\n\n if (!name || typeof name !== 'string') {\n return NextResponse.json({ error: 'Folder name is required' }, { status: 400 })\n }\n\n // Sanitize folder name\n const sanitizedName = name.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid folder name' }, { status: 400 })\n }\n\n const safePath = (parentPath || 'public').replace(/\\.\\./g, '')\n const folderPath = path.join(process.cwd(), safePath, sanitizedName)\n\n // Check if we're within public folder\n if (!folderPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if folder already exists\n try {\n await fs.access(folderPath)\n return NextResponse.json({ error: 'A folder with this name already exists' }, { status: 400 })\n } catch {\n // Good - folder doesn't exist\n }\n\n await fs.mkdir(folderPath, { recursive: true })\n\n return NextResponse.json({ success: true, path: path.join(safePath, sanitizedName) })\n } catch (error) {\n console.error('Failed to create folder:', error)\n return NextResponse.json({ error: 'Failed to create folder' }, { status: 500 })\n }\n}\n\nasync function handleRename(request: NextRequest) {\n try {\n const { oldPath, newName } = await request.json()\n\n if (!oldPath || !newName) {\n return NextResponse.json({ error: 'Path and new name are required' }, { status: 400 })\n }\n\n // Sanitize new name\n const sanitizedName = newName.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid name' }, { status: 400 })\n }\n\n const safePath = oldPath.replace(/\\.\\./g, '')\n const absoluteOldPath = path.join(process.cwd(), safePath)\n const parentDir = path.dirname(absoluteOldPath)\n const absoluteNewPath = path.join(parentDir, sanitizedName)\n\n // Check path is within public folder\n if (!absoluteOldPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if old path exists\n try {\n await fs.access(absoluteOldPath)\n } catch {\n return NextResponse.json({ error: 'File or folder not found' }, { status: 404 })\n }\n\n // Check if new path already exists\n try {\n await fs.access(absoluteNewPath)\n return NextResponse.json({ error: 'An item with this name already exists' }, { status: 400 })\n } catch {\n // Good - new path doesn't exist\n }\n\n // Check if it's a file (for meta updates)\n const stats = await fs.stat(absoluteOldPath)\n const isFile = stats.isFile()\n const isImage = isFile && isImageFile(path.basename(oldPath))\n\n // Rename the file/folder\n await fs.rename(absoluteOldPath, absoluteNewPath)\n\n // Update meta if it's an image\n if (isImage) {\n const meta = await loadMeta()\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(path.dirname(oldRelativePath), sanitizedName)\n\n // Find and update meta entry\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n // Update original path\n entry.original.path = `/${newRelativePath}`\n\n // Rename thumbnails in public/images\n const oldExt = path.extname(path.basename(oldPath))\n const oldBaseName = path.basename(oldPath, oldExt)\n const newExt = path.extname(sanitizedName)\n const newBaseName = path.basename(sanitizedName, newExt)\n const oldDirRelative = path.dirname(oldRelativePath)\n const thumbnailDir = path.join(process.cwd(), 'public', 'images', oldDirRelative)\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const oldThumbName = `${oldBaseName}${suffix}${oldExt === '.png' ? '.png' : '.jpg'}`\n const newThumbName = `${newBaseName}${suffix}${newExt === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(thumbnailDir, oldThumbName)\n const newThumbPath = path.join(thumbnailDir, newThumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${oldDirRelative}/${newThumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update the key in meta\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n break\n }\n }\n\n await saveMeta(meta)\n }\n\n const newPath = path.join(path.dirname(safePath), sanitizedName)\n return NextResponse.json({ success: true, newPath })\n } catch (error) {\n console.error('Failed to rename:', error)\n return NextResponse.json({ error: 'Failed to rename' }, { status: 500 })\n }\n}\n\nasync function handleMove(request: NextRequest) {\n try {\n const { paths, destination } = await request.json()\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'Paths are required' }, { status: 400 })\n }\n\n if (!destination || typeof destination !== 'string') {\n return NextResponse.json({ error: 'Destination is required' }, { status: 400 })\n }\n\n const safeDestination = destination.replace(/\\.\\./g, '')\n const absoluteDestination = path.join(process.cwd(), safeDestination)\n\n // Check destination is within public folder\n if (!absoluteDestination.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid destination' }, { status: 400 })\n }\n\n // Cannot move to protected images folder\n if (safeDestination === 'public/images' || safeDestination.startsWith('public/images/')) {\n return NextResponse.json({ error: 'Cannot move items to the protected images folder' }, { status: 400 })\n }\n\n // Check destination exists and is a directory\n try {\n const destStats = await fs.stat(absoluteDestination)\n if (!destStats.isDirectory()) {\n return NextResponse.json({ error: 'Destination is not a folder' }, { status: 400 })\n }\n } catch {\n return NextResponse.json({ error: 'Destination folder not found' }, { status: 404 })\n }\n\n const moved: string[] = []\n const errors: string[] = []\n const meta = await loadMeta()\n let metaChanged = false\n\n for (const itemPath of paths) {\n const safePath = itemPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n const itemName = path.basename(safePath)\n const newAbsolutePath = path.join(absoluteDestination, itemName)\n\n // Cannot move a folder into itself\n if (absoluteDestination.startsWith(absolutePath + path.sep)) {\n errors.push(`Cannot move ${itemName} into itself`)\n continue\n }\n\n // Check source exists\n try {\n await fs.access(absolutePath)\n } catch {\n errors.push(`${itemName} not found`)\n continue\n }\n\n // Check if destination already has item with same name\n try {\n await fs.access(newAbsolutePath)\n errors.push(`${itemName} already exists in destination`)\n continue\n } catch {\n // Good - doesn't exist\n }\n\n try {\n await fs.rename(absolutePath, newAbsolutePath)\n\n // Update meta for images\n const stats = await fs.stat(newAbsolutePath)\n if (stats.isFile() && isImageFile(itemName)) {\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(safeDestination.replace(/^public\\//, ''), itemName)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n entry.original.path = `/${newRelativePath}`\n\n // Move thumbnails too\n const oldDir = path.dirname(oldRelativePath)\n const newDir = path.dirname(newRelativePath)\n const ext = path.extname(itemName)\n const baseName = path.basename(itemName, ext)\n const oldThumbDir = path.join(process.cwd(), 'public', 'images', oldDir)\n const newThumbDir = path.join(process.cwd(), 'public', 'images', newDir)\n\n // Ensure new thumb directory exists\n await fs.mkdir(newThumbDir, { recursive: true })\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const thumbName = `${baseName}${suffix}${ext === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(oldThumbDir, thumbName)\n const newThumbPath = path.join(newThumbDir, thumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${newDir}/${thumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update key\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n metaChanged = true\n break\n }\n }\n }\n\n moved.push(itemPath)\n } catch (error) {\n errors.push(`Failed to move ${itemName}`)\n }\n }\n\n if (metaChanged) {\n await saveMeta(meta)\n }\n\n return NextResponse.json({\n success: errors.length === 0,\n moved,\n errors: errors.length > 0 ? errors : undefined\n })\n } catch (error) {\n console.error('Failed to move:', error)\n return NextResponse.json({ error: 'Failed to move items' }, { status: 500 })\n }\n}\n\nasync function handleListFolders() {\n try {\n const publicDir = path.join(process.cwd(), 'public')\n const folders: { path: string; name: string; depth: number }[] = []\n\n async function scanDir(dir: string, relativePath: string, depth: number): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n if (entry.name.startsWith('.')) continue\n // Skip protected images folder\n if (relativePath === '' && entry.name === 'images') continue\n\n const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n folders.push({\n path: `public/${folderRelativePath}`,\n name: entry.name,\n depth\n })\n\n // Recursively scan subdirectories\n await scanDir(path.join(dir, entry.name), folderRelativePath, depth + 1)\n }\n } catch {\n // Ignore errors\n }\n }\n\n // Add root public folder\n folders.push({ path: 'public', name: 'public', depth: 0 })\n\n await scanDir(publicDir, '', 1)\n\n return NextResponse.json({ folders })\n } catch (error) {\n console.error('Failed to list folders:', error)\n return NextResponse.json({ error: 'Failed to list folders' }, { status: 500 })\n }\n}\n"]}
|
package/dist/handlers.mjs
CHANGED
|
@@ -898,25 +898,38 @@ async function handleProcessAllStream() {
|
|
|
898
898
|
}
|
|
899
899
|
async function loadMeta() {
|
|
900
900
|
const metaPath = path.join(process.cwd(), "_data", "_meta.json");
|
|
901
|
+
const emptyMeta = {
|
|
902
|
+
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
903
|
+
version: 1,
|
|
904
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
905
|
+
images: {}
|
|
906
|
+
};
|
|
901
907
|
try {
|
|
902
908
|
const content = await fs.readFile(metaPath, "utf-8");
|
|
903
909
|
const parsed = JSON.parse(content);
|
|
904
910
|
if (parsed.images && typeof parsed.images === "object") {
|
|
905
911
|
return parsed;
|
|
906
912
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
images
|
|
912
|
-
|
|
913
|
+
const meta = { ...emptyMeta, images: {} };
|
|
914
|
+
for (const [imagePath, entry] of Object.entries(parsed)) {
|
|
915
|
+
const leanEntry = entry;
|
|
916
|
+
const key = imagePath.startsWith("/") ? imagePath.slice(1) : imagePath;
|
|
917
|
+
meta.images[key] = {
|
|
918
|
+
original: {
|
|
919
|
+
path: imagePath,
|
|
920
|
+
width: leanEntry.w,
|
|
921
|
+
height: leanEntry.h,
|
|
922
|
+
fileSize: 0
|
|
923
|
+
},
|
|
924
|
+
sizes: {},
|
|
925
|
+
blurhash: leanEntry.blur,
|
|
926
|
+
dominantColor: "",
|
|
927
|
+
cdn: leanEntry.s ? { synced: true, baseUrl: "", syncedAt: "" } : null
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
return meta;
|
|
913
931
|
} catch {
|
|
914
|
-
return
|
|
915
|
-
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
916
|
-
version: 1,
|
|
917
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
918
|
-
images: {}
|
|
919
|
-
};
|
|
932
|
+
return emptyMeta;
|
|
920
933
|
}
|
|
921
934
|
}
|
|
922
935
|
async function saveMeta(meta) {
|
package/dist/handlers.mjs.map
CHANGED
|
@@ -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, LeanMeta, LeanImageEntry } from './types'\n\n// Default thumbnail sizes with their suffixes\nconst DEFAULT_SIZES: Record<string, { width: number; suffix: string }> = {\n small: { width: 300, suffix: '-sm' },\n medium: { width: 700, suffix: '-md' },\n large: { width: 1400, suffix: '-lg' },\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-folders (must come before 'list' check)\n if (route === 'list-folders') {\n return handleListFolders()\n }\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 // Route: /api/studio/count-images\n if (route === 'count-images') {\n return handleCountImages()\n }\n\n // Route: /api/studio/folder-images\n if (route === 'folder-images') {\n return handleFolderImages(request)\n }\n\n // Route: /api/studio/search\n if (route === 'search') {\n return handleSearch(request)\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 // Route: /api/studio/process-all (streaming)\n if (route === 'process-all') {\n return handleProcessAllStream()\n }\n\n // Route: /api/studio/create-folder\n if (route === 'create-folder') {\n return handleCreateFolder(request)\n }\n\n // Route: /api/studio/rename\n if (route === 'rename') {\n return handleRename(request)\n }\n\n // Route: /api/studio/move\n if (route === 'move') {\n return handleMove(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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const filePath = path.join(absolutePath, entry.name)\n const stats = await fs.stat(filePath)\n const isImage = isImageFile(entry.name)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n \n if (isImage) {\n const relativePath = safePath.replace(/^public\\/?/, '')\n \n // If we're already inside the images folder, these ARE the thumbnails\n if (relativePath === 'images' || relativePath.startsWith('images/')) {\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = true // They are thumbnails themselves\n } else {\n // Check for -sm thumbnail in images folder\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n \n try {\n await fs.access(thumbnailPath)\n // Thumbnail exists\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n // No thumbnail, fall back to original\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = false\n }\n }\n \n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(filePath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n }\n \n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\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 handleSearch(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const query = searchParams.get('q')?.toLowerCase() || ''\n \n if (query.length < 2) {\n return NextResponse.json({ items: [] })\n }\n\n try {\n const items: FileItem[] = []\n const publicDir = path.join(process.cwd(), 'public')\n\n async function searchDir(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 itemPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`\n const itemRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n // Skip public/images folder (generated thumbnails)\n if (itemRelPath === 'images') continue\n await searchDir(fullPath, itemRelPath)\n } else if (isImageFile(entry.name)) {\n // Check if path matches query\n if (itemPath.toLowerCase().includes(query)) {\n const stats = await fs.stat(fullPath)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n\n // Check for -sm thumbnail\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n\n try {\n await fs.access(thumbnailPath)\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n thumbnail = `/${itemRelPath}`\n hasThumbnail = false\n }\n\n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(fullPath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\n })\n }\n }\n }\n } catch {\n // Ignore directory access errors\n }\n }\n\n await searchDir(publicDir, '')\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to search:', error)\n return NextResponse.json({ error: 'Failed to search' }, { status: 500 })\n }\n}\n\nasync function getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\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 // Check if this is an image that can be processed\n const isImage = isImageFile(fileName)\n const isSvg = ext === '.svg'\n const isProcessableImage = isImage && !isSvg\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // For non-image media files, just save and return success\n if (!isImage) {\n return NextResponse.json({ \n success: true, \n message: 'File uploaded successfully (non-image, no thumbnails generated)',\n path: `public/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n })\n }\n \n // For images, generate thumbnails and update meta\n const fullImageKey = relativeDir ? `${relativeDir}/${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 // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\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 // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else if (isProcessableImage) {\n // Raster images: process with sharp and generate thumbnails\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/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${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 as ImageSize] = {\n path: `/images/${relativeDir ? relativeDir + '/' : ''}${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: originalPath,\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 // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\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 // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\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 // If deleting from images/, just delete the file (already done above)\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 try {\n let buffer: Buffer\n let entry = meta.images[imageKey]\n \n // Try to read the original file from public folder\n const originalPath = path.join(process.cwd(), 'public', imageKey)\n \n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n // File not in public folder, try from entry's original path or CDN\n if (entry) {\n const entryOriginalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(entryOriginalPath)\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 } else {\n throw new Error(`File not found: ${imageKey}`)\n }\n }\n\n // If no existing entry, create a minimal one for new images\n if (!entry) {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const stats = await fs.stat(originalPath)\n \n entry = {\n original: {\n path: imageKey,\n width: metadata.width || 0,\n height: metadata.height || 0,\n fileSize: stats.size,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#000000',\n cdn: null,\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\nasync function handleCountImages() {\n try {\n const allImages: string[] = []\n\n // Scan public folder recursively for ALL images, excluding public/images/\n async function scanPublicFolder(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 // Skip the images folder - that's for generated thumbnails\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to count images:', error)\n return NextResponse.json({ error: 'Failed to count images' }, { status: 500 })\n }\n}\n\nasync function handleFolderImages(request: NextRequest) {\n try {\n const searchParams = request.nextUrl.searchParams\n const foldersParam = searchParams.get('folders')\n \n if (!foldersParam) {\n return NextResponse.json({ error: 'No folders provided' }, { status: 400 })\n }\n\n const folders = foldersParam.split(',')\n const allImages: string[] = []\n\n async function scanFolder(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 scanFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n for (const folder of folders) {\n // Folder paths come as \"public/photos\" - we need relative path from public\n const relativePath = folder.replace(/^public\\/?/, '')\n \n // Skip the images folder\n if (relativePath === 'images' || relativePath.startsWith('images/')) continue\n \n const folderPath = path.join(process.cwd(), folder)\n await scanFolder(folderPath, relativePath)\n }\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to get folder images:', error)\n return NextResponse.json({ error: 'Failed to get folder images' }, { status: 500 })\n }\n}\n\nasync function handleProcessAllStream() {\n const encoder = new TextEncoder()\n \n const stream = new ReadableStream({\n async start(controller) {\n const sendEvent = (data: object) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`))\n }\n\n try {\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n const orphansRemoved: string[] = []\n\n // Step 1: Scan public folder for ALL images (excluding public/images/)\n const allImages: Array<{ key: string; fullPath: string }> = []\n\n async function scanPublicFolder(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 // Skip the images folder\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push({ key: relPath, fullPath })\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n const total = allImages.length\n sendEvent({ type: 'start', total })\n\n // Step 2: Process each image (reprocess all, not just unprocessed)\n for (let i = 0; i < allImages.length; i++) {\n const { key, fullPath } = allImages[i]\n \n sendEvent({ \n type: 'progress', \n current: i + 1, \n total, \n percent: Math.round(((i + 1) / total) * 100),\n currentFile: key \n })\n\n try {\n const buffer = await fs.readFile(fullPath)\n const ext = path.extname(key).toLowerCase()\n const isSvg = ext === '.svg'\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const imageDir = path.dirname(key)\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n \n const fileName = path.basename(key)\n const destPath = path.join(imagesPath, fileName)\n await fs.writeFile(destPath, buffer)\n\n const sizePath = `/images/${imageDir === '.' ? '' : imageDir + '/'}${fileName}`\n meta.images[key] = {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\n full: { path: sizePath, width: 0, height: 0 },\n large: { path: sizePath, width: 0, height: 0 },\n medium: { path: sizePath, width: 0, height: 0 },\n small: { path: sizePath, width: 0, height: 0 },\n },\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n } else {\n // Raster image: full processing\n const existingEntry = meta.images[key]\n const baseEntry: ImageEntry = existingEntry || {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n\n const processedEntry = await processImage(buffer, baseEntry, key)\n meta.images[key] = processedEntry\n }\n\n processed.push(key)\n } catch (error) {\n console.error(`Failed to process ${key}:`, error)\n errors.push(key)\n }\n }\n\n // Step 3: Remove orphaned thumbnails\n sendEvent({ type: 'cleanup', message: 'Removing orphaned thumbnails...' })\n \n const trackedPaths = new Set<string>()\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 findOrphans(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 findOrphans(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n try {\n await fs.unlink(fullPath)\n orphansRemoved.push(publicPath)\n } catch (err) {\n console.error(`Failed to remove orphan ${publicPath}:`, err)\n }\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n await findOrphans(imagesDir)\n\n // Step 4: Clean up empty directories\n async function removeEmptyDirs(dir: string): Promise<boolean> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n let isEmpty = true\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n const subDirEmpty = await removeEmptyDirs(path.join(dir, entry.name))\n if (!subDirEmpty) isEmpty = false\n } else {\n isEmpty = false\n }\n }\n\n if (isEmpty && dir !== imagesDir) {\n await fs.rmdir(dir)\n }\n\n return isEmpty\n } catch {\n return true\n }\n }\n\n await removeEmptyDirs(imagesDir)\n await saveMeta(meta)\n\n sendEvent({ \n type: 'complete', \n processed: processed.length, \n orphansRemoved: orphansRemoved.length,\n errors: errors.length,\n })\n } catch (error) {\n console.error('Failed to process all:', error)\n sendEvent({ type: 'error', message: 'Failed to process images' })\n } finally {\n controller.close()\n }\n }\n })\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\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 dataDir = path.join(process.cwd(), '_data')\n await fs.mkdir(dataDir, { recursive: true })\n \n // Convert to lean format and write to _meta.json\n const lean: LeanMeta = {}\n for (const [key, entry] of Object.entries(meta.images)) {\n const imagePath = entry.original?.path || `/${key}`\n lean[imagePath] = {\n w: entry.original?.width || 0,\n h: entry.original?.height || 0,\n blur: entry.blurhash || '',\n }\n if (entry.cdn?.synced) {\n lean[imagePath].s = 1\n }\n }\n const metaPath = path.join(dataDir, '_meta.json')\n await fs.writeFile(metaPath, JSON.stringify(lean, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\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 isPng = ext === '.png'\n const outputExt = isPng ? '.png' : '.jpg'\n const fullFileName = imageDir === '.' ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n \n if (isPng) {\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/${fullFileName}`\n\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${outputExt}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n if (isPng) {\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 as ImageSize] = {\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\n// ============================================================================\n// FOLDER MANAGEMENT HANDLERS\n// ============================================================================\n\nasync function handleCreateFolder(request: NextRequest) {\n try {\n const { parentPath, name } = await request.json()\n\n if (!name || typeof name !== 'string') {\n return NextResponse.json({ error: 'Folder name is required' }, { status: 400 })\n }\n\n // Sanitize folder name\n const sanitizedName = name.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid folder name' }, { status: 400 })\n }\n\n const safePath = (parentPath || 'public').replace(/\\.\\./g, '')\n const folderPath = path.join(process.cwd(), safePath, sanitizedName)\n\n // Check if we're within public folder\n if (!folderPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if folder already exists\n try {\n await fs.access(folderPath)\n return NextResponse.json({ error: 'A folder with this name already exists' }, { status: 400 })\n } catch {\n // Good - folder doesn't exist\n }\n\n await fs.mkdir(folderPath, { recursive: true })\n\n return NextResponse.json({ success: true, path: path.join(safePath, sanitizedName) })\n } catch (error) {\n console.error('Failed to create folder:', error)\n return NextResponse.json({ error: 'Failed to create folder' }, { status: 500 })\n }\n}\n\nasync function handleRename(request: NextRequest) {\n try {\n const { oldPath, newName } = await request.json()\n\n if (!oldPath || !newName) {\n return NextResponse.json({ error: 'Path and new name are required' }, { status: 400 })\n }\n\n // Sanitize new name\n const sanitizedName = newName.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid name' }, { status: 400 })\n }\n\n const safePath = oldPath.replace(/\\.\\./g, '')\n const absoluteOldPath = path.join(process.cwd(), safePath)\n const parentDir = path.dirname(absoluteOldPath)\n const absoluteNewPath = path.join(parentDir, sanitizedName)\n\n // Check path is within public folder\n if (!absoluteOldPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if old path exists\n try {\n await fs.access(absoluteOldPath)\n } catch {\n return NextResponse.json({ error: 'File or folder not found' }, { status: 404 })\n }\n\n // Check if new path already exists\n try {\n await fs.access(absoluteNewPath)\n return NextResponse.json({ error: 'An item with this name already exists' }, { status: 400 })\n } catch {\n // Good - new path doesn't exist\n }\n\n // Check if it's a file (for meta updates)\n const stats = await fs.stat(absoluteOldPath)\n const isFile = stats.isFile()\n const isImage = isFile && isImageFile(path.basename(oldPath))\n\n // Rename the file/folder\n await fs.rename(absoluteOldPath, absoluteNewPath)\n\n // Update meta if it's an image\n if (isImage) {\n const meta = await loadMeta()\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(path.dirname(oldRelativePath), sanitizedName)\n\n // Find and update meta entry\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n // Update original path\n entry.original.path = `/${newRelativePath}`\n\n // Rename thumbnails in public/images\n const oldExt = path.extname(path.basename(oldPath))\n const oldBaseName = path.basename(oldPath, oldExt)\n const newExt = path.extname(sanitizedName)\n const newBaseName = path.basename(sanitizedName, newExt)\n const oldDirRelative = path.dirname(oldRelativePath)\n const thumbnailDir = path.join(process.cwd(), 'public', 'images', oldDirRelative)\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const oldThumbName = `${oldBaseName}${suffix}${oldExt === '.png' ? '.png' : '.jpg'}`\n const newThumbName = `${newBaseName}${suffix}${newExt === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(thumbnailDir, oldThumbName)\n const newThumbPath = path.join(thumbnailDir, newThumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${oldDirRelative}/${newThumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update the key in meta\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n break\n }\n }\n\n await saveMeta(meta)\n }\n\n const newPath = path.join(path.dirname(safePath), sanitizedName)\n return NextResponse.json({ success: true, newPath })\n } catch (error) {\n console.error('Failed to rename:', error)\n return NextResponse.json({ error: 'Failed to rename' }, { status: 500 })\n }\n}\n\nasync function handleMove(request: NextRequest) {\n try {\n const { paths, destination } = await request.json()\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'Paths are required' }, { status: 400 })\n }\n\n if (!destination || typeof destination !== 'string') {\n return NextResponse.json({ error: 'Destination is required' }, { status: 400 })\n }\n\n const safeDestination = destination.replace(/\\.\\./g, '')\n const absoluteDestination = path.join(process.cwd(), safeDestination)\n\n // Check destination is within public folder\n if (!absoluteDestination.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid destination' }, { status: 400 })\n }\n\n // Cannot move to protected images folder\n if (safeDestination === 'public/images' || safeDestination.startsWith('public/images/')) {\n return NextResponse.json({ error: 'Cannot move items to the protected images folder' }, { status: 400 })\n }\n\n // Check destination exists and is a directory\n try {\n const destStats = await fs.stat(absoluteDestination)\n if (!destStats.isDirectory()) {\n return NextResponse.json({ error: 'Destination is not a folder' }, { status: 400 })\n }\n } catch {\n return NextResponse.json({ error: 'Destination folder not found' }, { status: 404 })\n }\n\n const moved: string[] = []\n const errors: string[] = []\n const meta = await loadMeta()\n let metaChanged = false\n\n for (const itemPath of paths) {\n const safePath = itemPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n const itemName = path.basename(safePath)\n const newAbsolutePath = path.join(absoluteDestination, itemName)\n\n // Cannot move a folder into itself\n if (absoluteDestination.startsWith(absolutePath + path.sep)) {\n errors.push(`Cannot move ${itemName} into itself`)\n continue\n }\n\n // Check source exists\n try {\n await fs.access(absolutePath)\n } catch {\n errors.push(`${itemName} not found`)\n continue\n }\n\n // Check if destination already has item with same name\n try {\n await fs.access(newAbsolutePath)\n errors.push(`${itemName} already exists in destination`)\n continue\n } catch {\n // Good - doesn't exist\n }\n\n try {\n await fs.rename(absolutePath, newAbsolutePath)\n\n // Update meta for images\n const stats = await fs.stat(newAbsolutePath)\n if (stats.isFile() && isImageFile(itemName)) {\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(safeDestination.replace(/^public\\//, ''), itemName)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n entry.original.path = `/${newRelativePath}`\n\n // Move thumbnails too\n const oldDir = path.dirname(oldRelativePath)\n const newDir = path.dirname(newRelativePath)\n const ext = path.extname(itemName)\n const baseName = path.basename(itemName, ext)\n const oldThumbDir = path.join(process.cwd(), 'public', 'images', oldDir)\n const newThumbDir = path.join(process.cwd(), 'public', 'images', newDir)\n\n // Ensure new thumb directory exists\n await fs.mkdir(newThumbDir, { recursive: true })\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const thumbName = `${baseName}${suffix}${ext === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(oldThumbDir, thumbName)\n const newThumbPath = path.join(newThumbDir, thumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${newDir}/${thumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update key\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n metaChanged = true\n break\n }\n }\n }\n\n moved.push(itemPath)\n } catch (error) {\n errors.push(`Failed to move ${itemName}`)\n }\n }\n\n if (metaChanged) {\n await saveMeta(meta)\n }\n\n return NextResponse.json({\n success: errors.length === 0,\n moved,\n errors: errors.length > 0 ? errors : undefined\n })\n } catch (error) {\n console.error('Failed to move:', error)\n return NextResponse.json({ error: 'Failed to move items' }, { status: 500 })\n }\n}\n\nasync function handleListFolders() {\n try {\n const publicDir = path.join(process.cwd(), 'public')\n const folders: { path: string; name: string; depth: number }[] = []\n\n async function scanDir(dir: string, relativePath: string, depth: number): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n if (entry.name.startsWith('.')) continue\n // Skip protected images folder\n if (relativePath === '' && entry.name === 'images') continue\n\n const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n folders.push({\n path: `public/${folderRelativePath}`,\n name: entry.name,\n depth\n })\n\n // Recursively scan subdirectories\n await scanDir(path.join(dir, entry.name), folderRelativePath, depth + 1)\n }\n } catch {\n // Ignore errors\n }\n }\n\n // Add root public folder\n folders.push({ path: 'public', name: 'public', depth: 0 })\n\n await scanDir(publicDir, '', 1)\n\n return NextResponse.json({ folders })\n } catch (error) {\n console.error('Failed to list folders:', error)\n return NextResponse.json({ error: 'Failed to list folders' }, { status: 500 })\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, LeanMeta, LeanImageEntry } from './types'\n\n// Default thumbnail sizes with their suffixes\nconst DEFAULT_SIZES: Record<string, { width: number; suffix: string }> = {\n small: { width: 300, suffix: '-sm' },\n medium: { width: 700, suffix: '-md' },\n large: { width: 1400, suffix: '-lg' },\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-folders (must come before 'list' check)\n if (route === 'list-folders') {\n return handleListFolders()\n }\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 // Route: /api/studio/count-images\n if (route === 'count-images') {\n return handleCountImages()\n }\n\n // Route: /api/studio/folder-images\n if (route === 'folder-images') {\n return handleFolderImages(request)\n }\n\n // Route: /api/studio/search\n if (route === 'search') {\n return handleSearch(request)\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 // Route: /api/studio/process-all (streaming)\n if (route === 'process-all') {\n return handleProcessAllStream()\n }\n\n // Route: /api/studio/create-folder\n if (route === 'create-folder') {\n return handleCreateFolder(request)\n }\n\n // Route: /api/studio/rename\n if (route === 'rename') {\n return handleRename(request)\n }\n\n // Route: /api/studio/move\n if (route === 'move') {\n return handleMove(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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const filePath = path.join(absolutePath, entry.name)\n const stats = await fs.stat(filePath)\n const isImage = isImageFile(entry.name)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n \n if (isImage) {\n const relativePath = safePath.replace(/^public\\/?/, '')\n \n // If we're already inside the images folder, these ARE the thumbnails\n if (relativePath === 'images' || relativePath.startsWith('images/')) {\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = true // They are thumbnails themselves\n } else {\n // Check for -sm thumbnail in images folder\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n \n try {\n await fs.access(thumbnailPath)\n // Thumbnail exists\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n // No thumbnail, fall back to original\n thumbnail = itemPath.replace('public', '')\n hasThumbnail = false\n }\n }\n \n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(filePath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n }\n \n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\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 handleSearch(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const query = searchParams.get('q')?.toLowerCase() || ''\n \n if (query.length < 2) {\n return NextResponse.json({ items: [] })\n }\n\n try {\n const items: FileItem[] = []\n const publicDir = path.join(process.cwd(), 'public')\n\n async function searchDir(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 itemPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`\n const itemRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n // Skip public/images folder (generated thumbnails)\n if (itemRelPath === 'images') continue\n await searchDir(fullPath, itemRelPath)\n } else if (isImageFile(entry.name)) {\n // Check if path matches query\n if (itemPath.toLowerCase().includes(query)) {\n const stats = await fs.stat(fullPath)\n \n let thumbnail: string | undefined\n let hasThumbnail = false\n let dimensions: { width: number; height: number } | undefined\n\n // Check for -sm thumbnail\n const ext = path.extname(entry.name).toLowerCase()\n const baseName = path.basename(entry.name, ext)\n const thumbnailDir = relativePath ? `images/${relativePath}` : 'images'\n const thumbnailName = `${baseName}-sm${ext === '.png' ? '.png' : '.jpg'}`\n const thumbnailPath = path.join(process.cwd(), 'public', thumbnailDir, thumbnailName)\n\n try {\n await fs.access(thumbnailPath)\n thumbnail = `/${thumbnailDir}/${thumbnailName}`\n hasThumbnail = true\n } catch {\n thumbnail = `/${itemRelPath}`\n hasThumbnail = false\n }\n\n // Get dimensions\n if (!entry.name.toLowerCase().endsWith('.svg')) {\n try {\n const metadata = await sharp(fullPath).metadata()\n if (metadata.width && metadata.height) {\n dimensions = { width: metadata.width, height: metadata.height }\n }\n } catch {\n // Ignore dimension errors\n }\n }\n\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\n hasThumbnail,\n dimensions,\n })\n }\n }\n }\n } catch {\n // Ignore directory access errors\n }\n }\n\n await searchDir(publicDir, '')\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to search:', error)\n return NextResponse.json({ error: 'Failed to search' }, { status: 500 })\n }\n}\n\nasync function getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\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 // Check if this is an image that can be processed\n const isImage = isImageFile(fileName)\n const isSvg = ext === '.svg'\n const isProcessableImage = isImage && !isSvg\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // For non-image media files, just save and return success\n if (!isImage) {\n return NextResponse.json({ \n success: true, \n message: 'File uploaded successfully (non-image, no thumbnails generated)',\n path: `public/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n })\n }\n \n // For images, generate thumbnails and update meta\n const fullImageKey = relativeDir ? `${relativeDir}/${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 // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\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 // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else if (isProcessableImage) {\n // Raster images: process with sharp and generate thumbnails\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/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${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 as ImageSize] = {\n path: `/images/${relativeDir ? relativeDir + '/' : ''}${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: originalPath,\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 // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\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 // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\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 // If deleting from images/, just delete the file (already done above)\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 try {\n let buffer: Buffer\n let entry = meta.images[imageKey]\n \n // Try to read the original file from public folder\n const originalPath = path.join(process.cwd(), 'public', imageKey)\n \n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n // File not in public folder, try from entry's original path or CDN\n if (entry) {\n const entryOriginalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(entryOriginalPath)\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 } else {\n throw new Error(`File not found: ${imageKey}`)\n }\n }\n\n // If no existing entry, create a minimal one for new images\n if (!entry) {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const stats = await fs.stat(originalPath)\n \n entry = {\n original: {\n path: imageKey,\n width: metadata.width || 0,\n height: metadata.height || 0,\n fileSize: stats.size,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#000000',\n cdn: null,\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\nasync function handleCountImages() {\n try {\n const allImages: string[] = []\n\n // Scan public folder recursively for ALL images, excluding public/images/\n async function scanPublicFolder(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 // Skip the images folder - that's for generated thumbnails\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to count images:', error)\n return NextResponse.json({ error: 'Failed to count images' }, { status: 500 })\n }\n}\n\nasync function handleFolderImages(request: NextRequest) {\n try {\n const searchParams = request.nextUrl.searchParams\n const foldersParam = searchParams.get('folders')\n \n if (!foldersParam) {\n return NextResponse.json({ error: 'No folders provided' }, { status: 400 })\n }\n\n const folders = foldersParam.split(',')\n const allImages: string[] = []\n\n async function scanFolder(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 scanFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push(relPath)\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n for (const folder of folders) {\n // Folder paths come as \"public/photos\" - we need relative path from public\n const relativePath = folder.replace(/^public\\/?/, '')\n \n // Skip the images folder\n if (relativePath === 'images' || relativePath.startsWith('images/')) continue\n \n const folderPath = path.join(process.cwd(), folder)\n await scanFolder(folderPath, relativePath)\n }\n\n return NextResponse.json({\n count: allImages.length,\n images: allImages,\n })\n } catch (error) {\n console.error('Failed to get folder images:', error)\n return NextResponse.json({ error: 'Failed to get folder images' }, { status: 500 })\n }\n}\n\nasync function handleProcessAllStream() {\n const encoder = new TextEncoder()\n \n const stream = new ReadableStream({\n async start(controller) {\n const sendEvent = (data: object) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`))\n }\n\n try {\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n const orphansRemoved: string[] = []\n\n // Step 1: Scan public folder for ALL images (excluding public/images/)\n const allImages: Array<{ key: string; fullPath: string }> = []\n\n async function scanPublicFolder(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 // Skip the images folder\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanPublicFolder(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n allImages.push({ key: relPath, fullPath })\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = path.join(process.cwd(), 'public')\n await scanPublicFolder(publicDir)\n\n const total = allImages.length\n sendEvent({ type: 'start', total })\n\n // Step 2: Process each image (reprocess all, not just unprocessed)\n for (let i = 0; i < allImages.length; i++) {\n const { key, fullPath } = allImages[i]\n \n sendEvent({ \n type: 'progress', \n current: i + 1, \n total, \n percent: Math.round(((i + 1) / total) * 100),\n currentFile: key \n })\n\n try {\n const buffer = await fs.readFile(fullPath)\n const ext = path.extname(key).toLowerCase()\n const isSvg = ext === '.svg'\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const imageDir = path.dirname(key)\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n \n const fileName = path.basename(key)\n const destPath = path.join(imagesPath, fileName)\n await fs.writeFile(destPath, buffer)\n\n const sizePath = `/images/${imageDir === '.' ? '' : imageDir + '/'}${fileName}`\n meta.images[key] = {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\n full: { path: sizePath, width: 0, height: 0 },\n large: { path: sizePath, width: 0, height: 0 },\n medium: { path: sizePath, width: 0, height: 0 },\n small: { path: sizePath, width: 0, height: 0 },\n },\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n } else {\n // Raster image: full processing\n const existingEntry = meta.images[key]\n const baseEntry: ImageEntry = existingEntry || {\n original: {\n path: `/${key}`,\n width: 0,\n height: 0,\n fileSize: buffer.length,\n },\n sizes: {\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 blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n\n const processedEntry = await processImage(buffer, baseEntry, key)\n meta.images[key] = processedEntry\n }\n\n processed.push(key)\n } catch (error) {\n console.error(`Failed to process ${key}:`, error)\n errors.push(key)\n }\n }\n\n // Step 3: Remove orphaned thumbnails\n sendEvent({ type: 'cleanup', message: 'Removing orphaned thumbnails...' })\n \n const trackedPaths = new Set<string>()\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 findOrphans(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 findOrphans(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n try {\n await fs.unlink(fullPath)\n orphansRemoved.push(publicPath)\n } catch (err) {\n console.error(`Failed to remove orphan ${publicPath}:`, err)\n }\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n await findOrphans(imagesDir)\n\n // Step 4: Clean up empty directories\n async function removeEmptyDirs(dir: string): Promise<boolean> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n let isEmpty = true\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n const subDirEmpty = await removeEmptyDirs(path.join(dir, entry.name))\n if (!subDirEmpty) isEmpty = false\n } else {\n isEmpty = false\n }\n }\n\n if (isEmpty && dir !== imagesDir) {\n await fs.rmdir(dir)\n }\n\n return isEmpty\n } catch {\n return true\n }\n }\n\n await removeEmptyDirs(imagesDir)\n await saveMeta(meta)\n\n sendEvent({ \n type: 'complete', \n processed: processed.length, \n orphansRemoved: orphansRemoved.length,\n errors: errors.length,\n })\n } catch (error) {\n console.error('Failed to process all:', error)\n sendEvent({ type: 'error', message: 'Failed to process images' })\n } finally {\n controller.close()\n }\n }\n })\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\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 const emptyMeta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n \n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n // Check if it's the old verbose format\n if (parsed.images && typeof parsed.images === 'object') {\n return parsed\n }\n \n // Convert lean format to internal verbose format\n const meta: StudioMeta = { ...emptyMeta, images: {} }\n for (const [imagePath, entry] of Object.entries(parsed)) {\n const leanEntry = entry as LeanImageEntry\n // Use the path as the key (without leading slash)\n const key = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath\n meta.images[key] = {\n original: {\n path: imagePath,\n width: leanEntry.w,\n height: leanEntry.h,\n fileSize: 0,\n },\n sizes: {} as Record<ImageSize, { path: string; width: number; height: number }>,\n blurhash: leanEntry.blur,\n dominantColor: '',\n cdn: leanEntry.s ? { synced: true, baseUrl: '', syncedAt: '' } : null,\n }\n }\n return meta\n } catch {\n return emptyMeta\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const dataDir = path.join(process.cwd(), '_data')\n await fs.mkdir(dataDir, { recursive: true })\n \n // Convert to lean format and write to _meta.json\n const lean: LeanMeta = {}\n for (const [key, entry] of Object.entries(meta.images)) {\n const imagePath = entry.original?.path || `/${key}`\n lean[imagePath] = {\n w: entry.original?.width || 0,\n h: entry.original?.height || 0,\n blur: entry.blurhash || '',\n }\n if (entry.cdn?.synced) {\n lean[imagePath].s = 1\n }\n }\n const metaPath = path.join(dataDir, '_meta.json')\n await fs.writeFile(metaPath, JSON.stringify(lean, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\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 isPng = ext === '.png'\n const outputExt = isPng ? '.png' : '.jpg'\n const fullFileName = imageDir === '.' ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n \n if (isPng) {\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/${fullFileName}`\n\n for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix } = sizeConfig\n if (originalWidth <= maxWidth) {\n sizes[sizeName as ImageSize] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}${suffix}${outputExt}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n if (isPng) {\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 as ImageSize] = {\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\n// ============================================================================\n// FOLDER MANAGEMENT HANDLERS\n// ============================================================================\n\nasync function handleCreateFolder(request: NextRequest) {\n try {\n const { parentPath, name } = await request.json()\n\n if (!name || typeof name !== 'string') {\n return NextResponse.json({ error: 'Folder name is required' }, { status: 400 })\n }\n\n // Sanitize folder name\n const sanitizedName = name.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid folder name' }, { status: 400 })\n }\n\n const safePath = (parentPath || 'public').replace(/\\.\\./g, '')\n const folderPath = path.join(process.cwd(), safePath, sanitizedName)\n\n // Check if we're within public folder\n if (!folderPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if folder already exists\n try {\n await fs.access(folderPath)\n return NextResponse.json({ error: 'A folder with this name already exists' }, { status: 400 })\n } catch {\n // Good - folder doesn't exist\n }\n\n await fs.mkdir(folderPath, { recursive: true })\n\n return NextResponse.json({ success: true, path: path.join(safePath, sanitizedName) })\n } catch (error) {\n console.error('Failed to create folder:', error)\n return NextResponse.json({ error: 'Failed to create folder' }, { status: 500 })\n }\n}\n\nasync function handleRename(request: NextRequest) {\n try {\n const { oldPath, newName } = await request.json()\n\n if (!oldPath || !newName) {\n return NextResponse.json({ error: 'Path and new name are required' }, { status: 400 })\n }\n\n // Sanitize new name\n const sanitizedName = newName.replace(/[<>:\"/\\\\|?*]/g, '').trim()\n if (!sanitizedName) {\n return NextResponse.json({ error: 'Invalid name' }, { status: 400 })\n }\n\n const safePath = oldPath.replace(/\\.\\./g, '')\n const absoluteOldPath = path.join(process.cwd(), safePath)\n const parentDir = path.dirname(absoluteOldPath)\n const absoluteNewPath = path.join(parentDir, sanitizedName)\n\n // Check path is within public folder\n if (!absoluteOldPath.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n // Check if old path exists\n try {\n await fs.access(absoluteOldPath)\n } catch {\n return NextResponse.json({ error: 'File or folder not found' }, { status: 404 })\n }\n\n // Check if new path already exists\n try {\n await fs.access(absoluteNewPath)\n return NextResponse.json({ error: 'An item with this name already exists' }, { status: 400 })\n } catch {\n // Good - new path doesn't exist\n }\n\n // Check if it's a file (for meta updates)\n const stats = await fs.stat(absoluteOldPath)\n const isFile = stats.isFile()\n const isImage = isFile && isImageFile(path.basename(oldPath))\n\n // Rename the file/folder\n await fs.rename(absoluteOldPath, absoluteNewPath)\n\n // Update meta if it's an image\n if (isImage) {\n const meta = await loadMeta()\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(path.dirname(oldRelativePath), sanitizedName)\n\n // Find and update meta entry\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n // Update original path\n entry.original.path = `/${newRelativePath}`\n\n // Rename thumbnails in public/images\n const oldExt = path.extname(path.basename(oldPath))\n const oldBaseName = path.basename(oldPath, oldExt)\n const newExt = path.extname(sanitizedName)\n const newBaseName = path.basename(sanitizedName, newExt)\n const oldDirRelative = path.dirname(oldRelativePath)\n const thumbnailDir = path.join(process.cwd(), 'public', 'images', oldDirRelative)\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const oldThumbName = `${oldBaseName}${suffix}${oldExt === '.png' ? '.png' : '.jpg'}`\n const newThumbName = `${newBaseName}${suffix}${newExt === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(thumbnailDir, oldThumbName)\n const newThumbPath = path.join(thumbnailDir, newThumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${oldDirRelative}/${newThumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update the key in meta\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n break\n }\n }\n\n await saveMeta(meta)\n }\n\n const newPath = path.join(path.dirname(safePath), sanitizedName)\n return NextResponse.json({ success: true, newPath })\n } catch (error) {\n console.error('Failed to rename:', error)\n return NextResponse.json({ error: 'Failed to rename' }, { status: 500 })\n }\n}\n\nasync function handleMove(request: NextRequest) {\n try {\n const { paths, destination } = await request.json()\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'Paths are required' }, { status: 400 })\n }\n\n if (!destination || typeof destination !== 'string') {\n return NextResponse.json({ error: 'Destination is required' }, { status: 400 })\n }\n\n const safeDestination = destination.replace(/\\.\\./g, '')\n const absoluteDestination = path.join(process.cwd(), safeDestination)\n\n // Check destination is within public folder\n if (!absoluteDestination.startsWith(path.join(process.cwd(), 'public'))) {\n return NextResponse.json({ error: 'Invalid destination' }, { status: 400 })\n }\n\n // Cannot move to protected images folder\n if (safeDestination === 'public/images' || safeDestination.startsWith('public/images/')) {\n return NextResponse.json({ error: 'Cannot move items to the protected images folder' }, { status: 400 })\n }\n\n // Check destination exists and is a directory\n try {\n const destStats = await fs.stat(absoluteDestination)\n if (!destStats.isDirectory()) {\n return NextResponse.json({ error: 'Destination is not a folder' }, { status: 400 })\n }\n } catch {\n return NextResponse.json({ error: 'Destination folder not found' }, { status: 404 })\n }\n\n const moved: string[] = []\n const errors: string[] = []\n const meta = await loadMeta()\n let metaChanged = false\n\n for (const itemPath of paths) {\n const safePath = itemPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n const itemName = path.basename(safePath)\n const newAbsolutePath = path.join(absoluteDestination, itemName)\n\n // Cannot move a folder into itself\n if (absoluteDestination.startsWith(absolutePath + path.sep)) {\n errors.push(`Cannot move ${itemName} into itself`)\n continue\n }\n\n // Check source exists\n try {\n await fs.access(absolutePath)\n } catch {\n errors.push(`${itemName} not found`)\n continue\n }\n\n // Check if destination already has item with same name\n try {\n await fs.access(newAbsolutePath)\n errors.push(`${itemName} already exists in destination`)\n continue\n } catch {\n // Good - doesn't exist\n }\n\n try {\n await fs.rename(absolutePath, newAbsolutePath)\n\n // Update meta for images\n const stats = await fs.stat(newAbsolutePath)\n if (stats.isFile() && isImageFile(itemName)) {\n const oldRelativePath = safePath.replace(/^public\\//, '')\n const newRelativePath = path.join(safeDestination.replace(/^public\\//, ''), itemName)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n if (entry.original.path === `/${oldRelativePath}`) {\n entry.original.path = `/${newRelativePath}`\n\n // Move thumbnails too\n const oldDir = path.dirname(oldRelativePath)\n const newDir = path.dirname(newRelativePath)\n const ext = path.extname(itemName)\n const baseName = path.basename(itemName, ext)\n const oldThumbDir = path.join(process.cwd(), 'public', 'images', oldDir)\n const newThumbDir = path.join(process.cwd(), 'public', 'images', newDir)\n\n // Ensure new thumb directory exists\n await fs.mkdir(newThumbDir, { recursive: true })\n\n for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {\n const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`\n const thumbName = `${baseName}${suffix}${ext === '.png' ? '.png' : '.jpg'}`\n const oldThumbPath = path.join(oldThumbDir, thumbName)\n const newThumbPath = path.join(newThumbDir, thumbName)\n\n try {\n await fs.rename(oldThumbPath, newThumbPath)\n sizeData.path = `/images/${newDir}/${thumbName}`.replace(/\\/+/g, '/')\n } catch {\n // Thumbnail might not exist\n }\n }\n\n // Update key\n const newKey = `/${newRelativePath}`\n delete meta.images[key]\n meta.images[newKey] = entry\n metaChanged = true\n break\n }\n }\n }\n\n moved.push(itemPath)\n } catch (error) {\n errors.push(`Failed to move ${itemName}`)\n }\n }\n\n if (metaChanged) {\n await saveMeta(meta)\n }\n\n return NextResponse.json({\n success: errors.length === 0,\n moved,\n errors: errors.length > 0 ? errors : undefined\n })\n } catch (error) {\n console.error('Failed to move:', error)\n return NextResponse.json({ error: 'Failed to move items' }, { status: 500 })\n }\n}\n\nasync function handleListFolders() {\n try {\n const publicDir = path.join(process.cwd(), 'public')\n const folders: { path: string; name: string; depth: number }[] = []\n\n async function scanDir(dir: string, relativePath: string, depth: number): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n if (entry.name.startsWith('.')) continue\n // Skip protected images folder\n if (relativePath === '' && entry.name === 'images') continue\n\n const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n folders.push({\n path: `public/${folderRelativePath}`,\n name: entry.name,\n depth\n })\n\n // Recursively scan subdirectories\n await scanDir(path.join(dir, entry.name), folderRelativePath, depth + 1)\n }\n } catch {\n // Ignore errors\n }\n }\n\n // Add root public folder\n folders.push({ path: 'public', name: 'public', depth: 0 })\n\n await scanDir(publicDir, '', 1)\n\n return NextResponse.json({ folders })\n } catch (error) {\n console.error('Failed to list folders:', error)\n return NextResponse.json({ error: 'Failed to list folders' }, { status: 500 })\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED