@gallop.software/studio 0.1.21 → 0.1.23
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-QMBOCTJD.js → StudioUI-2BWLIO4W.js} +654 -779
- package/dist/StudioUI-2BWLIO4W.js.map +1 -0
- package/dist/{StudioUI-WRFD73YR.mjs → StudioUI-QPAHJJ64.mjs} +746 -871
- package/dist/StudioUI-QPAHJJ64.mjs.map +1 -0
- package/dist/chunk-AY2DAS6W.js +64 -0
- package/dist/chunk-AY2DAS6W.js.map +1 -0
- package/dist/chunk-R5WKNVEV.mjs +64 -0
- package/dist/chunk-R5WKNVEV.mjs.map +1 -0
- package/dist/handlers.d.mts +4 -0
- package/dist/handlers.d.ts +4 -0
- package/dist/handlers.js +13 -4
- package/dist/handlers.js.map +1 -1
- package/dist/handlers.mjs +13 -4
- package/dist/handlers.mjs.map +1 -1
- package/dist/index.js +20 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/StudioUI-QMBOCTJD.js.map +0 -1
- package/dist/StudioUI-WRFD73YR.mjs.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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n // For images, provide thumbnail path (the file itself serves as thumbnail)\n const thumbnail = isImageFile(entry.name) ? itemPath.replace('public', '') : undefined\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\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 getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // 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 relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n \n // Image key is the relative path from public/ to the file\n const fullImageKey = relativeDir ? `${relativeDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp and generate thumbnails\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, 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/${relativeDir ? relativeDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: originalPath,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n // If deleting from images/, just delete the file (already done above)\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n 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', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const 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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n // For images, provide thumbnail path (the file itself serves as thumbnail)\n const thumbnail = isImageFile(entry.name) ? itemPath.replace('public', '') : undefined\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\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 getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // Check if this is an image that can be processed\n const isImage = isImageFile(fileName)\n const isSvg = ext === '.svg'\n const isProcessableImage = isImage && !isSvg\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // For non-image media files, just save and return success\n if (!isImage) {\n return NextResponse.json({ \n success: true, \n message: 'File uploaded successfully (non-image, no thumbnails generated)',\n path: `public/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n })\n }\n \n // For images, generate thumbnails and update meta\n const fullImageKey = relativeDir ? `${relativeDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else if (isProcessableImage) {\n // Raster images: process with sharp and generate thumbnails\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, 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/${relativeDir ? relativeDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: originalPath,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n // If deleting from images/, just delete the file (already done above)\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n 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', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const 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
|
@@ -185,7 +185,9 @@ async function handleUpload(request) {
|
|
|
185
185
|
const fileName = file.name;
|
|
186
186
|
const baseName = path.basename(fileName, path.extname(fileName));
|
|
187
187
|
const ext = path.extname(fileName).toLowerCase();
|
|
188
|
+
const isImage = isImageFile(fileName);
|
|
188
189
|
const isSvg = ext === ".svg";
|
|
190
|
+
const isProcessableImage = isImage && !isSvg;
|
|
189
191
|
const meta = await loadMeta();
|
|
190
192
|
if (!meta.images) {
|
|
191
193
|
meta.images = {};
|
|
@@ -202,6 +204,16 @@ async function handleUpload(request) {
|
|
|
202
204
|
{ status: 400 }
|
|
203
205
|
);
|
|
204
206
|
}
|
|
207
|
+
const uploadDir = path.join(process.cwd(), "public", relativeDir);
|
|
208
|
+
await fs.mkdir(uploadDir, { recursive: true });
|
|
209
|
+
await fs.writeFile(path.join(uploadDir, fileName), buffer);
|
|
210
|
+
if (!isImage) {
|
|
211
|
+
return NextResponse.json({
|
|
212
|
+
success: true,
|
|
213
|
+
message: "File uploaded successfully (non-image, no thumbnails generated)",
|
|
214
|
+
path: `public/${relativeDir ? relativeDir + "/" : ""}${fileName}`
|
|
215
|
+
});
|
|
216
|
+
}
|
|
205
217
|
const fullImageKey = relativeDir ? `${relativeDir}/${fileName}` : fileName;
|
|
206
218
|
if (meta.images[fullImageKey]) {
|
|
207
219
|
return NextResponse.json(
|
|
@@ -209,9 +221,6 @@ async function handleUpload(request) {
|
|
|
209
221
|
{ status: 409 }
|
|
210
222
|
);
|
|
211
223
|
}
|
|
212
|
-
const uploadDir = path.join(process.cwd(), "public", relativeDir);
|
|
213
|
-
await fs.mkdir(uploadDir, { recursive: true });
|
|
214
|
-
await fs.writeFile(path.join(uploadDir, fileName), buffer);
|
|
215
224
|
const imagesPath = path.join(process.cwd(), "public", "images", relativeDir);
|
|
216
225
|
await fs.mkdir(imagesPath, { recursive: true });
|
|
217
226
|
let originalWidth = 0;
|
|
@@ -232,7 +241,7 @@ async function handleUpload(request) {
|
|
|
232
241
|
sizes.large = { ...sizes.full };
|
|
233
242
|
sizes.medium = { ...sizes.full };
|
|
234
243
|
sizes.small = { ...sizes.full };
|
|
235
|
-
} else {
|
|
244
|
+
} else if (isProcessableImage) {
|
|
236
245
|
const sharpInstance = sharp(buffer);
|
|
237
246
|
const metadata = await sharpInstance.metadata();
|
|
238
247
|
originalWidth = metadata.width || 0;
|
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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n // For images, provide thumbnail path (the file itself serves as thumbnail)\n const thumbnail = isImageFile(entry.name) ? itemPath.replace('public', '') : undefined\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\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 getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // 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 relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n \n // Image key is the relative path from public/ to the file\n const fullImageKey = relativeDir ? `${relativeDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else {\n // Raster images: process with sharp and generate thumbnails\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, 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/${relativeDir ? relativeDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: originalPath,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n // If deleting from images/, just delete the file (already done above)\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n 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', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const 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 // Calculate folder stats\n const folderStats = await getFolderStats(path.join(absolutePath, entry.name))\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount: folderStats.fileCount,\n totalSize: folderStats.totalSize,\n })\n } else if (isMediaFile(entry.name)) {\n const stats = await fs.stat(path.join(absolutePath, entry.name))\n // For images, provide thumbnail path (the file itself serves as thumbnail)\n const thumbnail = isImageFile(entry.name) ? itemPath.replace('public', '') : undefined\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size: stats.size,\n thumbnail,\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 getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\n}\n\nasync function handleScan() {\n try {\n const meta = await loadMeta()\n\n const untrackedFiles: string[] = []\n const missingFiles: string[] = []\n const validFiles: string[] = []\n\n const imagesDir = path.join(process.cwd(), 'public', 'images')\n const trackedPaths = new Set<string>()\n\n for (const entry of Object.values(meta.images)) {\n for (const sizeData of Object.values(entry.sizes)) {\n trackedPaths.add(sizeData.path)\n }\n }\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n \n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!trackedPaths.has(publicPath)) {\n untrackedFiles.push(publicPath)\n } else {\n validFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await scanDir(imagesDir)\n\n for (const [key, entry] of Object.entries(meta.images)) {\n for (const [size, sizeData] of Object.entries(entry.sizes)) {\n const filePath = path.join(process.cwd(), 'public', sizeData.path)\n try {\n await fs.access(filePath)\n } catch {\n if (!entry.cdn?.synced) {\n missingFiles.push(`${key} (${size}): ${sizeData.path}`)\n }\n }\n }\n }\n\n return NextResponse.json({\n totalInMeta: Object.keys(meta.images).length,\n validFiles: validFiles.length,\n untrackedFiles,\n missingFiles,\n })\n } catch (error) {\n console.error('Failed to scan:', error)\n return NextResponse.json({ error: 'Failed to scan' }, { status: 500 })\n }\n}\n\nasync function handleUpload(request: NextRequest) {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const targetPath = formData.get('path') as string || 'public'\n\n if (!file) {\n return NextResponse.json({ error: 'No file provided' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n const fileName = file.name\n const baseName = path.basename(fileName, path.extname(fileName))\n const ext = path.extname(fileName).toLowerCase()\n\n // Check if this is an image that can be processed\n const isImage = isImageFile(fileName)\n const isSvg = ext === '.svg'\n const isProcessableImage = isImage && !isSvg\n\n const meta = await loadMeta()\n \n // Ensure images object exists\n if (!meta.images) {\n meta.images = {}\n }\n\n // Calculate relative path from public/\n // e.g., \"public/photos\" -> \"photos\", \"public\" -> \"\"\n let relativeDir = ''\n if (targetPath === 'public') {\n relativeDir = ''\n } else if (targetPath.startsWith('public/')) {\n relativeDir = targetPath.replace('public/', '')\n }\n \n // Block uploads to public/images/ - that's for generated thumbnails only\n if (relativeDir === 'images' || relativeDir.startsWith('images/')) {\n return NextResponse.json(\n { error: 'Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically.' },\n { status: 400 }\n )\n }\n\n // Save file to current location\n const uploadDir = path.join(process.cwd(), 'public', relativeDir)\n await fs.mkdir(uploadDir, { recursive: true })\n await fs.writeFile(path.join(uploadDir, fileName), buffer)\n\n // For non-image media files, just save and return success\n if (!isImage) {\n return NextResponse.json({ \n success: true, \n message: 'File uploaded successfully (non-image, no thumbnails generated)',\n path: `public/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n })\n }\n \n // For images, generate thumbnails and update meta\n const fullImageKey = relativeDir ? `${relativeDir}/${fileName}` : fileName\n\n if (meta.images[fullImageKey]) {\n return NextResponse.json(\n { error: `File '${fullImageKey}' already exists in meta` },\n { status: 409 }\n )\n }\n\n // Generate thumbnails in public/images/ with matching subpath\n const imagesPath = path.join(process.cwd(), 'public', 'images', relativeDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n let originalWidth = 0\n let originalHeight = 0\n let blurhash = ''\n let dominantColor = '#888888'\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: 0, height: 0 },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n // Original path is relative to public/\n const originalPath = `/${relativeDir ? relativeDir + '/' : ''}${fileName}`\n\n if (isSvg) {\n // SVG: copy to images folder, no thumbnail processing\n const fullPath = path.join(imagesPath, fileName)\n await fs.writeFile(fullPath, buffer)\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fileName}`, width: 0, height: 0 }\n sizes.large = { ...sizes.full }\n sizes.medium = { ...sizes.full }\n sizes.small = { ...sizes.full }\n } else if (isProcessableImage) {\n // Raster images: process with sharp and generate thumbnails\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n originalWidth = metadata.width || 0\n originalHeight = metadata.height || 0\n\n // Full size\n const outputExt = ext === '.png' ? '.png' : '.jpg'\n const fullFileName = `${baseName}${outputExt}`\n const fullPath = path.join(imagesPath, fullFileName)\n \n if (ext === '.png') {\n await sharp(buffer).png({ quality: 85 }).toFile(fullPath)\n } else {\n await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath)\n }\n sizes.full = { path: `/images/${relativeDir ? relativeDir + '/' : ''}${fullFileName}`, width: originalWidth, height: originalHeight }\n\n // Generate each thumbnail size\n for (const [sizeName, 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/${relativeDir ? relativeDir + '/' : ''}${sizeFileName}`,\n width: maxWidth,\n height: newHeight,\n }\n }\n\n // Blurhash\n const { data, info } = await sharp(buffer)\n .resize(32, 32, { fit: 'inside' })\n .ensureAlpha()\n .raw()\n .toBuffer({ resolveWithObject: true })\n\n blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4)\n\n // Dominant color\n const { dominant } = await sharp(buffer).stats()\n dominantColor = `#${dominant.r.toString(16).padStart(2, '0')}${dominant.g.toString(16).padStart(2, '0')}${dominant.b.toString(16).padStart(2, '0')}`\n }\n\n const entry: ImageEntry = {\n original: {\n path: originalPath,\n width: originalWidth,\n height: originalHeight,\n fileSize: buffer.length,\n },\n sizes,\n blurhash,\n dominantColor,\n cdn: null,\n }\n\n meta.images[fullImageKey] = entry\n await saveMeta(meta)\n\n return NextResponse.json({ success: true, imageKey: fullImageKey, entry })\n } catch (error) {\n console.error('Failed to upload:', error)\n const message = error instanceof Error ? error.message : 'Unknown error'\n return NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 })\n }\n}\n\nasync function handleDelete(request: NextRequest) {\n try {\n const { paths } = await request.json() as { paths: string[] }\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return NextResponse.json({ error: 'No paths provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const itemPath of paths) {\n try {\n if (!itemPath.startsWith('public/')) {\n errors.push(`Invalid path: ${itemPath}`)\n continue\n }\n\n const absolutePath = path.join(process.cwd(), itemPath)\n const stats = await fs.stat(absolutePath)\n\n if (stats.isDirectory()) {\n await fs.rm(absolutePath, { recursive: true })\n \n // Remove prefix to get image key pattern\n const prefix = itemPath\n .replace(/^public\\/images\\/?/, '')\n .replace(/^public\\/?/, '')\n \n for (const key of Object.keys(meta.images)) {\n if (key.startsWith(prefix)) {\n delete meta.images[key]\n }\n }\n } else {\n await fs.unlink(absolutePath)\n\n // Check if this is an original (in public/, not in public/images/)\n const isInImagesFolder = itemPath.startsWith('public/images/')\n \n if (!isInImagesFolder) {\n // Deleting an original from public/ - also delete its thumbnails\n const imageKey = itemPath.replace(/^public\\//, '')\n const entry = meta.images[imageKey]\n if (entry) {\n // Delete all generated thumbnails\n for (const sizeData of Object.values(entry.sizes)) {\n const sizePath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(sizePath) } catch { /* ignore */ }\n }\n delete meta.images[imageKey]\n }\n }\n // If deleting from images/, just delete the file (already done above)\n }\n\n deleted.push(itemPath)\n } catch (error) {\n console.error(`Failed to delete ${itemPath}:`, error)\n errors.push(itemPath)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to delete:', error)\n return NextResponse.json({ error: 'Failed to delete files' }, { status: 500 })\n }\n}\n\nasync function handleSync(request: NextRequest) {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n return NextResponse.json(\n { error: 'R2 not configured. Set CLOUDFLARE_R2_* environment variables.' },\n { status: 400 }\n )\n }\n\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n const synced: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n const entry = meta.images[imageKey]\n if (!entry) {\n errors.push(`Image not found in meta: ${imageKey}`)\n continue\n }\n\n if (entry.cdn?.synced) {\n synced.push(imageKey)\n continue\n }\n\n try {\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n const fileBuffer = await fs.readFile(localPath)\n\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: sizeData.path.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(sizeData.path),\n })\n )\n }\n\n entry.cdn = {\n synced: true,\n baseUrl: publicUrl,\n syncedAt: new Date().toISOString(),\n }\n\n for (const sizeData of Object.values(entry.sizes)) {\n const localPath = path.join(process.cwd(), 'public', sizeData.path)\n try { await fs.unlink(localPath) } catch { /* ignore */ }\n }\n\n synced.push(imageKey)\n } catch (error) {\n console.error(`Failed to sync ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n return NextResponse.json({\n success: true,\n synced,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Failed to sync:', error)\n return NextResponse.json({ error: 'Failed to sync to CDN' }, { status: 500 })\n }\n}\n\nasync function handleReprocess(request: NextRequest) {\n try {\n const { imageKeys } = await request.json() as { imageKeys: string[] }\n\n if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {\n return NextResponse.json({ error: 'No image keys provided' }, { status: 400 })\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n\n for (const imageKey of imageKeys) {\n 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', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nfunction isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents/PDFs\n if (['.pdf'].includes(ext)) return true\n return false\n}\n\nfunction getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nasync function processImage(\n buffer: Buffer,\n entry: ImageEntry,\n imageKey: string\n): Promise<ImageEntry> {\n const sharpInstance = sharp(buffer)\n const metadata = await sharpInstance.metadata()\n const originalWidth = metadata.width || 0\n const originalHeight = metadata.height || 0\n\n const baseName = path.basename(imageKey, path.extname(imageKey))\n const ext = path.extname(imageKey).toLowerCase()\n const imageDir = path.dirname(imageKey)\n\n const imagesPath = path.join(process.cwd(), 'public', 'images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const sizes: Record<ImageSize, { path: string; width: number; height: number }> = {\n full: { path: '', width: originalWidth, height: originalHeight },\n large: { path: '', width: 0, height: 0 },\n medium: { path: '', width: 0, height: 0 },\n small: { path: '', width: 0, height: 0 },\n }\n\n const 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
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client";
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
var _chunkAY2DAS6Wjs = require('./chunk-AY2DAS6W.js');
|
|
8
|
+
|
|
3
9
|
// src/components/StudioButton.tsx
|
|
4
10
|
var _react = require('react');
|
|
5
11
|
var _react3 = require('@emotion/react');
|
|
6
12
|
var _jsxruntime = require('@emotion/react/jsx-runtime');
|
|
7
|
-
var StudioUI = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./StudioUI-
|
|
13
|
+
var StudioUI = _react.lazy.call(void 0, () => Promise.resolve().then(() => _interopRequireWildcard(require("./StudioUI-2BWLIO4W.js"))));
|
|
8
14
|
var spin = _react3.keyframes`
|
|
9
15
|
to {
|
|
10
16
|
transform: rotate(360deg);
|
|
11
17
|
}
|
|
12
18
|
`;
|
|
13
|
-
var colors = {
|
|
14
|
-
primary: "#635bff",
|
|
15
|
-
primaryHover: "#5851e5",
|
|
16
|
-
primaryLight: "#e8e6ff",
|
|
17
|
-
background: "#f6f9fc",
|
|
18
|
-
surface: "#ffffff",
|
|
19
|
-
border: "#e3e8ee",
|
|
20
|
-
text: "#1a1f36",
|
|
21
|
-
textSecondary: "#697386",
|
|
22
|
-
shadow: "rgba(50, 50, 93, 0.1)",
|
|
23
|
-
shadowDark: "rgba(50, 50, 93, 0.2)"
|
|
24
|
-
};
|
|
25
19
|
var styles = {
|
|
26
20
|
button: _react3.css`
|
|
27
21
|
position: fixed;
|
|
@@ -31,20 +25,21 @@ var styles = {
|
|
|
31
25
|
width: 52px;
|
|
32
26
|
height: 52px;
|
|
33
27
|
border-radius: 50%;
|
|
34
|
-
background: ${colors.primary};
|
|
28
|
+
background: ${_chunkAY2DAS6Wjs.colors.primary};
|
|
35
29
|
color: white;
|
|
36
|
-
box-shadow: 0 4px 12px ${colors.shadowDark}, 0 1px 3px ${colors.shadow};
|
|
30
|
+
box-shadow: 0 4px 12px ${_chunkAY2DAS6Wjs.colors.shadowDark}, 0 1px 3px ${_chunkAY2DAS6Wjs.colors.shadow};
|
|
37
31
|
display: flex;
|
|
38
32
|
align-items: center;
|
|
39
33
|
justify-content: center;
|
|
40
34
|
border: none;
|
|
41
35
|
cursor: pointer;
|
|
42
36
|
transition: all 0.15s ease;
|
|
37
|
+
font-family: ${_chunkAY2DAS6Wjs.fontStack};
|
|
43
38
|
|
|
44
39
|
&:hover {
|
|
45
40
|
transform: translateY(-2px);
|
|
46
|
-
box-shadow: 0 8px 20px ${colors.shadowDark}, 0 2px 6px ${colors.shadow};
|
|
47
|
-
background: ${colors.primaryHover};
|
|
41
|
+
box-shadow: 0 8px 20px ${_chunkAY2DAS6Wjs.colors.shadowDark}, 0 2px 6px ${_chunkAY2DAS6Wjs.colors.shadow};
|
|
42
|
+
background: ${_chunkAY2DAS6Wjs.colors.primaryHover};
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
&:active {
|
|
@@ -79,12 +74,13 @@ var styles = {
|
|
|
79
74
|
backdrop-filter: blur(4px);
|
|
80
75
|
`,
|
|
81
76
|
modal: _react3.css`
|
|
77
|
+
${_chunkAY2DAS6Wjs.baseReset}
|
|
82
78
|
position: absolute;
|
|
83
79
|
top: 24px;
|
|
84
80
|
right: 24px;
|
|
85
81
|
bottom: 24px;
|
|
86
82
|
left: 24px;
|
|
87
|
-
background-color: ${colors.surface};
|
|
83
|
+
background-color: ${_chunkAY2DAS6Wjs.colors.surface};
|
|
88
84
|
border-radius: 12px;
|
|
89
85
|
box-shadow: 0 30px 60px -12px rgba(50, 50, 93, 0.25), 0 18px 36px -18px rgba(0, 0, 0, 0.3);
|
|
90
86
|
display: flex;
|
|
@@ -96,7 +92,8 @@ var styles = {
|
|
|
96
92
|
align-items: center;
|
|
97
93
|
justify-content: center;
|
|
98
94
|
height: 100%;
|
|
99
|
-
background: ${colors.background};
|
|
95
|
+
background: ${_chunkAY2DAS6Wjs.colors.background};
|
|
96
|
+
font-family: ${_chunkAY2DAS6Wjs.fontStack};
|
|
100
97
|
`,
|
|
101
98
|
loadingContent: _react3.css`
|
|
102
99
|
display: flex;
|
|
@@ -108,13 +105,13 @@ var styles = {
|
|
|
108
105
|
width: 36px;
|
|
109
106
|
height: 36px;
|
|
110
107
|
border-radius: 50%;
|
|
111
|
-
border: 3px solid ${colors.border};
|
|
112
|
-
border-top-color: ${colors.primary};
|
|
108
|
+
border: 3px solid ${_chunkAY2DAS6Wjs.colors.border};
|
|
109
|
+
border-top-color: ${_chunkAY2DAS6Wjs.colors.primary};
|
|
113
110
|
animation: ${spin} 0.8s linear infinite;
|
|
114
111
|
`,
|
|
115
112
|
loadingText: _react3.css`
|
|
116
|
-
color: ${colors.textSecondary};
|
|
117
|
-
font-size:
|
|
113
|
+
color: ${_chunkAY2DAS6Wjs.colors.textSecondary};
|
|
114
|
+
font-size: ${_chunkAY2DAS6Wjs.fontSize.base};
|
|
118
115
|
font-weight: 500;
|
|
119
116
|
margin: 0;
|
|
120
117
|
letter-spacing: -0.01em;
|