@gallop.software/studio 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{StudioUI-ELH3QOUT.js → StudioUI-QG2NJQTM.js} +9 -1
- package/dist/StudioUI-QG2NJQTM.js.map +1 -0
- package/dist/{StudioUI-5I7VRE4F.mjs → StudioUI-RS5UCUIN.mjs} +9 -1
- package/dist/StudioUI-RS5UCUIN.mjs.map +1 -0
- package/dist/handlers.js +6 -41
- package/dist/handlers.js.map +1 -1
- package/dist/handlers.mjs +6 -41
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/StudioUI-5I7VRE4F.mjs.map +0 -1
- package/dist/StudioUI-ELH3QOUT.js.map +0 -1
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 } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n // Handle legacy flat format (keys are image paths at root level)\n // vs new format with { images: {...} }\n if (parsed.images && typeof parsed.images === 'object') {\n // New format - already has images property\n return parsed\n } else {\n // Legacy format - convert flat structure to new format\n // Filter out metadata keys like $schema, version, generatedAt\n const images: Record<string, ImageEntry> = {}\n for (const [key, value] of Object.entries(parsed)) {\n if (key.startsWith('/images/') && typeof value === 'object' && value !== null) {\n // Convert legacy format to new format\n const legacyEntry = value as Record<string, { width: number; height: number; file: string }>\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n \n for (const [sizeName, sizeData] of Object.entries(legacyEntry)) {\n if (sizeName === 'small' || sizeName === 'medium' || sizeName === 'large' || sizeName === 'full') {\n sizes[sizeName] = {\n path: sizeData.file,\n width: sizeData.width,\n height: sizeData.height,\n }\n }\n }\n \n // Extract image key from path (e.g., \"/images/banner.jpg\" -> \"banner.jpg\")\n const imageKey = key.replace(/^\\/images\\//, '')\n images[imageKey] = {\n original: {\n path: key.replace('/images/', '/originals/'),\n width: sizes.full.width,\n height: sizes.full.height,\n fileSize: 0,\n },\n sizes,\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n }\n }\n \n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images,\n }\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/chrisb/Sites/studio/dist/handlers.js","../src/handlers.ts"],"names":[],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/handlers.js","sourcesContent":[null,"import { NextRequest, NextResponse } from 'next/server'\nimport { promises as fs } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { encode } from 'blurhash'\nimport { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'\nimport type { StudioMeta, ImageEntry, ImageSize, FileItem } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n if (parsed.images && typeof parsed.images === 'object') {\n return parsed\n }\n \n // Return empty meta if format is invalid\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"]}
|
package/dist/handlers.mjs
CHANGED
|
@@ -450,48 +450,13 @@ async function loadMeta() {
|
|
|
450
450
|
const parsed = JSON.parse(content);
|
|
451
451
|
if (parsed.images && typeof parsed.images === "object") {
|
|
452
452
|
return parsed;
|
|
453
|
-
} else {
|
|
454
|
-
const images = {};
|
|
455
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
456
|
-
if (key.startsWith("/images/") && typeof value === "object" && value !== null) {
|
|
457
|
-
const legacyEntry = value;
|
|
458
|
-
const sizes = {
|
|
459
|
-
full: { path: "", width: 0, height: 0 },
|
|
460
|
-
large: { path: "", width: 0, height: 0 },
|
|
461
|
-
medium: { path: "", width: 0, height: 0 },
|
|
462
|
-
small: { path: "", width: 0, height: 0 }
|
|
463
|
-
};
|
|
464
|
-
for (const [sizeName, sizeData] of Object.entries(legacyEntry)) {
|
|
465
|
-
if (sizeName === "small" || sizeName === "medium" || sizeName === "large" || sizeName === "full") {
|
|
466
|
-
sizes[sizeName] = {
|
|
467
|
-
path: sizeData.file,
|
|
468
|
-
width: sizeData.width,
|
|
469
|
-
height: sizeData.height
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
const imageKey = key.replace(/^\/images\//, "");
|
|
474
|
-
images[imageKey] = {
|
|
475
|
-
original: {
|
|
476
|
-
path: key.replace("/images/", "/originals/"),
|
|
477
|
-
width: sizes.full.width,
|
|
478
|
-
height: sizes.full.height,
|
|
479
|
-
fileSize: 0
|
|
480
|
-
},
|
|
481
|
-
sizes,
|
|
482
|
-
blurhash: "",
|
|
483
|
-
dominantColor: "#888888",
|
|
484
|
-
cdn: null
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
490
|
-
version: 1,
|
|
491
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
492
|
-
images
|
|
493
|
-
};
|
|
494
453
|
}
|
|
454
|
+
return {
|
|
455
|
+
$schema: "https://gallop.software/schemas/studio-meta.json",
|
|
456
|
+
version: 1,
|
|
457
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
458
|
+
images: {}
|
|
459
|
+
};
|
|
495
460
|
} catch {
|
|
496
461
|
return {
|
|
497
462
|
$schema: "https://gallop.software/schemas/studio-meta.json",
|
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 } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n // Handle legacy flat format (keys are image paths at root level)\n // vs new format with { images: {...} }\n if (parsed.images && typeof parsed.images === 'object') {\n // New format - already has images property\n return parsed\n } else {\n // Legacy format - convert flat structure to new format\n // Filter out metadata keys like $schema, version, generatedAt\n const images: Record<string, ImageEntry> = {}\n for (const [key, value] of Object.entries(parsed)) {\n if (key.startsWith('/images/') && typeof value === 'object' && value !== null) {\n // Convert legacy format to new format\n const legacyEntry = value as Record<string, { width: number; height: number; file: string }>\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n \n for (const [sizeName, sizeData] of Object.entries(legacyEntry)) {\n if (sizeName === 'small' || sizeName === 'medium' || sizeName === 'large' || sizeName === 'full') {\n sizes[sizeName] = {\n path: sizeData.file,\n width: sizeData.width,\n height: sizeData.height,\n }\n }\n }\n \n // Extract image key from path (e.g., \"/images/banner.jpg\" -> \"banner.jpg\")\n const imageKey = key.replace(/^\\/images\\//, '')\n images[imageKey] = {\n original: {\n path: key.replace('/images/', '/originals/'),\n width: sizes.full.width,\n height: sizes.full.height,\n fileSize: 0,\n },\n sizes,\n blurhash: '',\n dominantColor: '#888888',\n cdn: null,\n }\n }\n }\n \n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images,\n }\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/handlers.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server'\nimport { promises as fs } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { encode } from 'blurhash'\nimport { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'\nimport type { StudioMeta, ImageEntry, ImageSize, FileItem } from './types'\n\n// Default thumbnail sizes\nconst DEFAULT_SIZES = {\n small: 300,\n medium: 700,\n large: 1400,\n}\n\n/**\n * Unified GET handler for all Studio API routes\n */\nexport async function GET(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/list\n if (route === 'list' || route.startsWith('list')) {\n return handleList(request)\n }\n\n // Route: /api/studio/scan\n if (route === 'scan') {\n return handleScan()\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified POST handler for all Studio API routes\n */\nexport async function POST(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n const pathname = request.nextUrl.pathname\n const route = pathname.replace(/^\\/api\\/studio\\/?/, '')\n\n // Route: /api/studio/upload\n if (route === 'upload') {\n return handleUpload(request)\n }\n\n // Route: /api/studio/delete\n if (route === 'delete') {\n return handleDelete(request)\n }\n\n // Route: /api/studio/sync\n if (route === 'sync') {\n return handleSync(request)\n }\n\n // Route: /api/studio/reprocess\n if (route === 'reprocess') {\n return handleReprocess(request)\n }\n\n return NextResponse.json({ error: 'Not found' }, { status: 404 })\n}\n\n/**\n * Unified DELETE handler\n */\nexport async function DELETE(request: NextRequest) {\n if (process.env.NODE_ENV !== 'development') {\n return NextResponse.json({ error: 'Not available in production' }, { status: 403 })\n }\n\n return handleDelete(request)\n}\n\n// ============================================================================\n// Handler implementations\n// ============================================================================\n\nasync function handleList(request: NextRequest) {\n const searchParams = request.nextUrl.searchParams\n const requestedPath = searchParams.get('path') || 'public'\n\n try {\n const safePath = requestedPath.replace(/\\.\\./g, '')\n const absolutePath = path.join(process.cwd(), safePath)\n\n if (!absolutePath.startsWith(process.cwd())) {\n return NextResponse.json({ error: 'Invalid path' }, { status: 400 })\n }\n\n const items: FileItem[] = []\n const entries = await fs.readdir(absolutePath, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const itemPath = path.join(safePath, entry.name)\n\n if (entry.isDirectory()) {\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n })\n } else if (isImageFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n })\n }\n }\n\n return NextResponse.json({ items })\n } catch (error) {\n console.error('Failed to list directory:', error)\n return NextResponse.json({ error: 'Failed to list directory' }, { status: 500 })\n }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // SVG files can't be processed by sharp for thumbnails\n const isSvg = ext === '.svg'\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate the subdirectory within images/originals\n // If viewing public/images/photos, the subdir is \"photos\"\n let subDir = ''\n if (targetPath.startsWith('public/images/')) {\n subDir = targetPath.replace('public/images/', '')\n } else if (targetPath.startsWith('public/originals/')) {\n subDir = targetPath.replace('public/originals/', '')\n }\n \n const fullImageKey = subDir ? `${subDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save original\n const originalsPath = path.join(process.cwd(), 'public', 'originals', subDir)\n await fs.mkdir(originalsPath, { recursive: true })\n await fs.writeFile(path.join(originalsPath, fileName), buffer)\n\n // Generate thumbnails directory\n const imagesPath = path.join(process.cwd(), 'public', 'images', subDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n if (isSvg) {\n // SVG: just copy to images folder, no processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${subDir ? subDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${outputExt}`\n const sizePath = path.join(imagesPath, sizeFileName)\n\n if (ext === '.png') {\n await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath)\n } else {\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n }\n\n sizes[sizeName] = {\n path: `/images/${subDir ? subDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: `/originals/${subDir ? subDir + '/' : ''}${fileName}`,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n const prefix = itemPath\n .replace(/^public\\/originals\\/?/, '')\n .replace(/^public\\/images\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n const imageKey = itemPath\n .replace(/^public\\/originals\\//, '')\n .replace(/^public\\/images\\//, '')\n\n if (itemPath.includes('/originals/')) {\n const entry = meta.images[imageKey]\n if (entry) {\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n try {\n let buffer: Buffer\n\n const originalPath = path.join(process.cwd(), 'public', entry.original.path)\n try {\n buffer = await fs.readFile(originalPath)\n } catch {\n if (entry.cdn?.synced) {\n buffer = await downloadFromCdn(entry.original.path)\n } else {\n throw new Error('Original not found locally and not on CDN')\n }\n }\n\n const updatedEntry = await processImage(buffer, entry, imageKey)\n meta.images[imageKey] = updatedEntry\n\n if (entry.cdn?.synced) {\n await uploadToCdn(updatedEntry)\n await deleteLocalFiles(updatedEntry)\n }\n\n processed.push(imageKey)\n } catch (error) {\n console.error(`Failed to reprocess ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n processed,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to reprocess:', error)\n return NextResponse.json({ error: 'Failed to reprocess images' }, { status: 500 })\n }\n}\n\n// ============================================================================\n// Helper functions\n// ============================================================================\n\nasync function loadMeta(): Promise<StudioMeta> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n const parsed = JSON.parse(content)\n \n if (parsed.images && typeof parsed.images === 'object') {\n return parsed\n }\n \n // Return empty meta if format is invalid\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n } catch {\n return {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n }\n }\n}\n\nasync function saveMeta(meta: StudioMeta): Promise<void> {\n const metaPath = path.join(process.cwd(), '_data', '_meta.json')\n await fs.mkdir(path.join(process.cwd(), '_data'), { recursive: true })\n meta.generatedAt = new Date().toISOString()\n await fs.writeFile(metaPath, JSON.stringify(meta, null, 2))\n}\n\nfunction isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'].includes(ext)\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const fullFileName = imageDir === '.' ? `${baseName}${ext}` : `${imageDir}/${baseName}${ext}`\n const fullPath = path.join(process.cwd(), 'public', 'images', fullFileName)\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n sizes.full.path = `/images/${fullFileName}`\n\n for (const [sizeName, maxWidth] of Object.entries(DEFAULT_SIZES) as [ImageSize, number][]) {\n if (originalWidth <= maxWidth) {\n sizes[sizeName] = { ...sizes.full }\n continue\n }\n\n const ratio = originalHeight / originalWidth\n const newHeight = Math.round(maxWidth * ratio)\n const sizeFileName = `${baseName}-${maxWidth}${ext === '.png' ? '.png' : '.jpg'}`\n const sizeFilePath = imageDir === '.' ? sizeFileName : `${imageDir}/${sizeFileName}`\n const sizePath = path.join(process.cwd(), 'public', 'images', sizeFilePath)\n\n await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath)\n\n sizes[sizeName] = {\n path: `/images/${sizeFilePath}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n const { dominant } = await sharp(buffer).stats()\n const dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n\n return {\n ...entry,\n original: {\n ...entry.original,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n }\n}\n\nasync function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n}\n\nasync function uploadToCdn(entry: ImageEntry): Promise<void> {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {\n throw new Error('R2 not configured')\n }\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n}\n\nasync function deleteLocalFiles(entry: ImageEntry): Promise<void> {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
var _react = require('react');
|
|
5
5
|
var _react3 = require('@emotion/react');
|
|
6
6
|
var _jsxruntime = require('@emotion/react/jsx-runtime');
|
|
7
|
-
var StudioUI = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./StudioUI-
|
|
7
|
+
var StudioUI = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./StudioUI-QG2NJQTM.js"))));
|
|
8
8
|
var spin = _react3.keyframes`
|
|
9
9
|
to {
|
|
10
10
|
transform: rotate(360deg);
|
|
@@ -45,6 +45,12 @@ var styles = {
|
|
|
45
45
|
bottom: 0;
|
|
46
46
|
left: 0;
|
|
47
47
|
z-index: 9999;
|
|
48
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
49
|
+
`,
|
|
50
|
+
overlayHidden: _react3.css`
|
|
51
|
+
opacity: 0;
|
|
52
|
+
visibility: hidden;
|
|
53
|
+
pointer-events: none;
|
|
48
54
|
`,
|
|
49
55
|
backdrop: _react3.css`
|
|
50
56
|
position: absolute;
|
|
@@ -97,9 +103,14 @@ var styles = {
|
|
|
97
103
|
function StudioButton() {
|
|
98
104
|
const [mounted, setMounted] = _react.useState.call(void 0, false);
|
|
99
105
|
const [isOpen, setIsOpen] = _react.useState.call(void 0, false);
|
|
106
|
+
const [hasBeenOpened, setHasBeenOpened] = _react.useState.call(void 0, false);
|
|
100
107
|
_react.useEffect.call(void 0, () => {
|
|
101
108
|
setMounted(true);
|
|
102
109
|
}, []);
|
|
110
|
+
const handleOpen = () => {
|
|
111
|
+
setIsOpen(true);
|
|
112
|
+
setHasBeenOpened(true);
|
|
113
|
+
};
|
|
103
114
|
if (!mounted || process.env.NODE_ENV !== "development") {
|
|
104
115
|
return null;
|
|
105
116
|
}
|
|
@@ -108,13 +119,13 @@ function StudioButton() {
|
|
|
108
119
|
"button",
|
|
109
120
|
{
|
|
110
121
|
css: styles.button,
|
|
111
|
-
onClick:
|
|
122
|
+
onClick: handleOpen,
|
|
112
123
|
title: "Open Studio",
|
|
113
124
|
"aria-label": "Open Studio media manager",
|
|
114
125
|
children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, ImageIcon, {})
|
|
115
126
|
}
|
|
116
127
|
),
|
|
117
|
-
|
|
128
|
+
hasBeenOpened && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { css: [styles.overlay, !isOpen && styles.overlayHidden], children: [
|
|
118
129
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles.backdrop, onClick: () => setIsOpen(false) }),
|
|
119
130
|
/* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { css: styles.modal, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _react.Suspense, { fallback: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, LoadingState, {}), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, StudioUI, { onClose: () => setIsOpen(false) }) }) })
|
|
120
131
|
] })
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/chrisb/Sites/studio/dist/index.js","../src/components/StudioButton.tsx","../src/lib/meta.ts"],"names":[],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/index.js","sourcesContent":[null,"/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState, useEffect, lazy, Suspense } from 'react'\nimport { css, keyframes } from '@emotion/react'\n\n// Lazy load the full Studio UI to avoid bundling in production\nconst StudioUI = lazy(() => import('./StudioUI'))\n\nconst spin = keyframes`\n to {\n transform: rotate(360deg);\n }\n`\n\nconst styles = {\n button: css`\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9998;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n background: linear-gradient(to bottom right, #a855f7, #ec4899);\n color: white;\n box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n cursor: pointer;\n transition: all 0.2s;\n \n &:hover {\n transform: scale(1.05);\n box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n }\n `,\n buttonIcon: css`\n width: 24px;\n height: 24px;\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 9999;\n `,\n backdrop: css`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(4px);\n `,\n modal: css`\n position: absolute;\n top: 32px;\n right: 32px;\n bottom: 32px;\n left: 32px;\n background-color: white;\n border-radius: 16px;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n `,\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n `,\n loadingContent: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n `,\n spinner: css`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 2px solid transparent;\n border-bottom-color: #9333ea;\n animation: ${spin} 1s linear infinite;\n `,\n loadingText: css`\n color: #6b7280;\n font-size: 14px;\n margin: 0;\n `,\n}\n\n/**\n * Floating button that opens the Studio modal.\n * Fixed position in bottom-right corner.\n * Only renders in development mode.\n */\nexport function StudioButton() {\n const [mounted, setMounted] = useState(false)\n const [isOpen, setIsOpen] = useState(false)\n\n // Only render on client to avoid hydration mismatch\n useEffect(() => {\n setMounted(true)\n }, [])\n\n // Only render in development and on client\n if (!mounted || process.env.NODE_ENV !== 'development') {\n return null\n }\n\n return (\n <>\n {!isOpen && (\n <button\n css={styles.button}\n onClick={() => setIsOpen(true)}\n title=\"Open Studio\"\n aria-label=\"Open Studio media manager\"\n >\n <ImageIcon />\n </button>\n )}\n\n {isOpen && (\n <div css={styles.overlay}>\n <div css={styles.backdrop} onClick={() => setIsOpen(false)} />\n <div css={styles.modal}>\n <Suspense fallback={<LoadingState />}>\n <StudioUI onClose={() => setIsOpen(false)} />\n </Suspense>\n </div>\n </div>\n )}\n </>\n )\n}\n\nfunction ImageIcon() {\n return (\n <svg\n css={styles.buttonIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n )\n}\n\nfunction LoadingState() {\n return (\n <div css={styles.loading}>\n <div css={styles.loadingContent}>\n <div css={styles.spinner} />\n <p css={styles.loadingText}>Loading Studio...</p>\n </div>\n </div>\n )\n}\n","import type { StudioMeta, ImageEntry, ImageSize, SizeEntry } from '../types'\n\n// Default empty meta - will be populated when reading from project\nlet _meta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n}\n\n/**\n * The meta object containing all image metadata.\n * This is read from _data/_meta.json in the consuming project.\n */\nexport const meta: StudioMeta = _meta\n\n/**\n * Initialize meta from a JSON object (called during build/runtime)\n */\nexport function initializeMeta(data: StudioMeta): void {\n _meta = data\n Object.assign(meta, data)\n}\n\n/**\n * Get the resolved URL for an image, handling CDN vs local paths\n */\nexport function getImageUrl(\n imageKey: string,\n size: ImageSize = 'medium'\n): string | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n\n const sizeData = image.sizes[size] || image.sizes.full\n if (!sizeData) return undefined\n\n // If synced to CDN, use CDN URL\n if (image.cdn?.synced && image.cdn.baseUrl) {\n return `${image.cdn.baseUrl}${sizeData.path}`\n }\n\n // Otherwise use local path\n return sizeData.path\n}\n\n/**\n * Get the full image entry for a key\n */\nexport function getStudioMeta(imageKey: string): ImageEntry | undefined {\n return meta.images[imageKey]\n}\n\n/**\n * Get size data for an image\n */\nexport function getImageSize(\n imageKey: string,\n size: ImageSize = 'medium'\n): SizeEntry | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n return image.sizes[size] || image.sizes.full\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["/Users/chrisb/Sites/studio/dist/index.js","../src/components/StudioButton.tsx","../src/lib/meta.ts"],"names":[],"mappings":"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","file":"/Users/chrisb/Sites/studio/dist/index.js","sourcesContent":[null,"/** @jsxImportSource @emotion/react */\n'use client'\n\nimport { useState, useEffect, lazy, Suspense } from 'react'\nimport { css, keyframes } from '@emotion/react'\n\n// Lazy load the full Studio UI to avoid bundling in production\nconst StudioUI = lazy(() => import('./StudioUI'))\n\nconst spin = keyframes`\n to {\n transform: rotate(360deg);\n }\n`\n\nconst styles = {\n button: css`\n position: fixed;\n bottom: 24px;\n right: 24px;\n z-index: 9998;\n width: 48px;\n height: 48px;\n border-radius: 50%;\n background: linear-gradient(to bottom right, #a855f7, #ec4899);\n color: white;\n box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n display: flex;\n align-items: center;\n justify-content: center;\n border: none;\n cursor: pointer;\n transition: all 0.2s;\n \n &:hover {\n transform: scale(1.05);\n box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);\n }\n `,\n buttonIcon: css`\n width: 24px;\n height: 24px;\n `,\n overlay: css`\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 9999;\n transition: opacity 0.2s, visibility 0.2s;\n `,\n overlayHidden: css`\n opacity: 0;\n visibility: hidden;\n pointer-events: none;\n `,\n backdrop: css`\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n background-color: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(4px);\n `,\n modal: css`\n position: absolute;\n top: 32px;\n right: 32px;\n bottom: 32px;\n left: 32px;\n background-color: white;\n border-radius: 16px;\n box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n `,\n loading: css`\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n `,\n loadingContent: css`\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 16px;\n `,\n spinner: css`\n width: 32px;\n height: 32px;\n border-radius: 50%;\n border: 2px solid transparent;\n border-bottom-color: #9333ea;\n animation: ${spin} 1s linear infinite;\n `,\n loadingText: css`\n color: #6b7280;\n font-size: 14px;\n margin: 0;\n `,\n}\n\n/**\n * Floating button that opens the Studio modal.\n * Fixed position in bottom-right corner.\n * Only renders in development mode.\n */\nexport function StudioButton() {\n const [mounted, setMounted] = useState(false)\n const [isOpen, setIsOpen] = useState(false)\n const [hasBeenOpened, setHasBeenOpened] = useState(false)\n\n // Only render on client to avoid hydration mismatch\n useEffect(() => {\n setMounted(true)\n }, [])\n\n const handleOpen = () => {\n setIsOpen(true)\n setHasBeenOpened(true)\n }\n\n // Only render in development and on client\n if (!mounted || process.env.NODE_ENV !== 'development') {\n return null\n }\n\n return (\n <>\n {!isOpen && (\n <button\n css={styles.button}\n onClick={handleOpen}\n title=\"Open Studio\"\n aria-label=\"Open Studio media manager\"\n >\n <ImageIcon />\n </button>\n )}\n\n {/* Keep mounted once opened to preserve state */}\n {hasBeenOpened && (\n <div css={[styles.overlay, !isOpen && styles.overlayHidden]}>\n <div css={styles.backdrop} onClick={() => setIsOpen(false)} />\n <div css={styles.modal}>\n <Suspense fallback={<LoadingState />}>\n <StudioUI onClose={() => setIsOpen(false)} />\n </Suspense>\n </div>\n </div>\n )}\n </>\n )\n}\n\nfunction ImageIcon() {\n return (\n <svg\n css={styles.buttonIcon}\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\" ry=\"2\" />\n <circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\" />\n <polyline points=\"21 15 16 10 5 21\" />\n </svg>\n )\n}\n\nfunction LoadingState() {\n return (\n <div css={styles.loading}>\n <div css={styles.loadingContent}>\n <div css={styles.spinner} />\n <p css={styles.loadingText}>Loading Studio...</p>\n </div>\n </div>\n )\n}\n","import type { StudioMeta, ImageEntry, ImageSize, SizeEntry } from '../types'\n\n// Default empty meta - will be populated when reading from project\nlet _meta: StudioMeta = {\n $schema: 'https://gallop.software/schemas/studio-meta.json',\n version: 1,\n generatedAt: new Date().toISOString(),\n images: {},\n}\n\n/**\n * The meta object containing all image metadata.\n * This is read from _data/_meta.json in the consuming project.\n */\nexport const meta: StudioMeta = _meta\n\n/**\n * Initialize meta from a JSON object (called during build/runtime)\n */\nexport function initializeMeta(data: StudioMeta): void {\n _meta = data\n Object.assign(meta, data)\n}\n\n/**\n * Get the resolved URL for an image, handling CDN vs local paths\n */\nexport function getImageUrl(\n imageKey: string,\n size: ImageSize = 'medium'\n): string | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n\n const sizeData = image.sizes[size] || image.sizes.full\n if (!sizeData) return undefined\n\n // If synced to CDN, use CDN URL\n if (image.cdn?.synced && image.cdn.baseUrl) {\n return `${image.cdn.baseUrl}${sizeData.path}`\n }\n\n // Otherwise use local path\n return sizeData.path\n}\n\n/**\n * Get the full image entry for a key\n */\nexport function getStudioMeta(imageKey: string): ImageEntry | undefined {\n return meta.images[imageKey]\n}\n\n/**\n * Get size data for an image\n */\nexport function getImageSize(\n imageKey: string,\n size: ImageSize = 'medium'\n): SizeEntry | undefined {\n const image = meta.images[imageKey]\n if (!image) return undefined\n return image.sizes[size] || image.sizes.full\n}\n"]}
|
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { useState, useEffect, lazy, Suspense } from "react";
|
|
5
5
|
import { css, keyframes } from "@emotion/react";
|
|
6
6
|
import { Fragment, jsx, jsxs } from "@emotion/react/jsx-runtime";
|
|
7
|
-
var StudioUI = lazy(() => import("./StudioUI-
|
|
7
|
+
var StudioUI = lazy(() => import("./StudioUI-RS5UCUIN.mjs"));
|
|
8
8
|
var spin = keyframes`
|
|
9
9
|
to {
|
|
10
10
|
transform: rotate(360deg);
|
|
@@ -45,6 +45,12 @@ var styles = {
|
|
|
45
45
|
bottom: 0;
|
|
46
46
|
left: 0;
|
|
47
47
|
z-index: 9999;
|
|
48
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
49
|
+
`,
|
|
50
|
+
overlayHidden: css`
|
|
51
|
+
opacity: 0;
|
|
52
|
+
visibility: hidden;
|
|
53
|
+
pointer-events: none;
|
|
48
54
|
`,
|
|
49
55
|
backdrop: css`
|
|
50
56
|
position: absolute;
|
|
@@ -97,9 +103,14 @@ var styles = {
|
|
|
97
103
|
function StudioButton() {
|
|
98
104
|
const [mounted, setMounted] = useState(false);
|
|
99
105
|
const [isOpen, setIsOpen] = useState(false);
|
|
106
|
+
const [hasBeenOpened, setHasBeenOpened] = useState(false);
|
|
100
107
|
useEffect(() => {
|
|
101
108
|
setMounted(true);
|
|
102
109
|
}, []);
|
|
110
|
+
const handleOpen = () => {
|
|
111
|
+
setIsOpen(true);
|
|
112
|
+
setHasBeenOpened(true);
|
|
113
|
+
};
|
|
103
114
|
if (!mounted || process.env.NODE_ENV !== "development") {
|
|
104
115
|
return null;
|
|
105
116
|
}
|
|
@@ -108,13 +119,13 @@ function StudioButton() {
|
|
|
108
119
|
"button",
|
|
109
120
|
{
|
|
110
121
|
css: styles.button,
|
|
111
|
-
onClick:
|
|
122
|
+
onClick: handleOpen,
|
|
112
123
|
title: "Open Studio",
|
|
113
124
|
"aria-label": "Open Studio media manager",
|
|
114
125
|
children: /* @__PURE__ */ jsx(ImageIcon, {})
|
|
115
126
|
}
|
|
116
127
|
),
|
|
117
|
-
|
|
128
|
+
hasBeenOpened && /* @__PURE__ */ jsxs("div", { css: [styles.overlay, !isOpen && styles.overlayHidden], children: [
|
|
118
129
|
/* @__PURE__ */ jsx("div", { css: styles.backdrop, onClick: () => setIsOpen(false) }),
|
|
119
130
|
/* @__PURE__ */ jsx("div", { css: styles.modal, children: /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(LoadingState, {}), children: /* @__PURE__ */ jsx(StudioUI, { onClose: () => setIsOpen(false) }) }) })
|
|
120
131
|
] })
|