@gallop.software/studio 0.1.92 → 0.1.94

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.
@@ -24,4 +24,4 @@ export {
24
24
  getThumbnailPath,
25
25
  getAllThumbnailPaths
26
26
  };
27
- //# sourceMappingURL=chunk-DTVEVFQ2.mjs.map
27
+ //# sourceMappingURL=chunk-IHXG2EE4.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Meta entry - works for images and non-images\n * Images have w, h, b (after processing)\n * c is the index into _cdns array (omit if not on CDN)\n */\nexport interface MetaEntry {\n w?: number // original width (images only)\n h?: number // original height (images only)\n b?: string // blurhash (images only, after processing)\n p?: 1 // processed (has thumbnails and blurhash)\n c?: number // CDN index - index into _cdns array (omit if not on CDN)\n}\n\n/**\n * Full meta schema including special keys\n * _cdns: Array of CDN base URLs\n * Other keys: file paths from public folder\n */\nexport interface FullMeta {\n _cdns?: string[] // Array of CDN base URLs\n [key: string]: MetaEntry | string[] | undefined\n}\n\n/**\n * Meta schema - keyed by path from public folder (legacy type)\n * Example: { \"/portfolio/photo.jpg\": { w: 2400, h: 1600, b: \"...\" } }\n */\nexport type LeanMeta = Record<string, MetaEntry>\n\n// Legacy alias for compatibility\nexport type LeanImageEntry = MetaEntry\n\n/**\n * File/folder item for browser\n */\nexport interface FileItem {\n name: string\n path: string\n type: 'file' | 'folder'\n size?: number\n dimensions?: { width: number; height: number }\n isProcessed?: boolean\n cdnPushed?: boolean\n // Folder-specific properties\n fileCount?: number\n totalSize?: number\n // For showing thumbnails - path to -sm version if exists\n thumbnail?: string\n // Whether a processed thumbnail exists\n hasThumbnail?: boolean\n}\n\n/**\n * Studio configuration\n */\nexport interface StudioConfig {\n r2AccountId?: string\n r2AccessKeyId?: string\n r2SecretAccessKey?: string\n r2BucketName?: string\n r2PublicUrl?: string\n thumbnailSizes?: {\n small: number\n medium: number\n large: number\n }\n}\n\n/**\n * Get thumbnail path from original image path\n */\nexport function getThumbnailPath(originalPath: string, size: 'sm' | 'md' | 'lg' | 'full'): string {\n if (size === 'full') {\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}${outputExt}`\n }\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}-${size}${outputExt}`\n}\n\n/**\n * Get all thumbnail paths for an image\n */\nexport function getAllThumbnailPaths(originalPath: string): string[] {\n return [\n getThumbnailPath(originalPath, 'full'),\n getThumbnailPath(originalPath, 'lg'),\n getThumbnailPath(originalPath, 'md'),\n getThumbnailPath(originalPath, 'sm'),\n ]\n}\n"],"mappings":";AAuEO,SAAS,iBAAiB,cAAsB,MAA2C;AAChG,MAAI,SAAS,QAAQ;AACnB,UAAMA,OAAM,aAAa,MAAM,QAAQ,IAAI,CAAC,KAAK;AACjD,UAAMC,QAAO,aAAa,QAAQ,UAAU,EAAE;AAC9C,UAAMC,aAAYF,KAAI,YAAY,MAAM,SAAS,SAAS;AAC1D,WAAO,UAAUC,KAAI,GAAGC,UAAS;AAAA,EACnC;AACA,QAAM,MAAM,aAAa,MAAM,QAAQ,IAAI,CAAC,KAAK;AACjD,QAAM,OAAO,aAAa,QAAQ,UAAU,EAAE;AAC9C,QAAM,YAAY,IAAI,YAAY,MAAM,SAAS,SAAS;AAC1D,SAAO,UAAU,IAAI,IAAI,IAAI,GAAG,SAAS;AAC3C;AAKO,SAAS,qBAAqB,cAAgC;AACnE,SAAO;AAAA,IACL,iBAAiB,cAAc,MAAM;AAAA,IACrC,iBAAiB,cAAc,IAAI;AAAA,IACnC,iBAAiB,cAAc,IAAI;AAAA,IACnC,iBAAiB,cAAc,IAAI;AAAA,EACrC;AACF;","names":["ext","base","outputExt"]}
@@ -24,4 +24,4 @@ function getAllThumbnailPaths(originalPath) {
24
24
 
25
25
 
26
26
  exports.getThumbnailPath = getThumbnailPath; exports.getAllThumbnailPaths = getAllThumbnailPaths;
27
- //# sourceMappingURL=chunk-L36EH3PM.js.map
27
+ //# sourceMappingURL=chunk-MCJNUXQ6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/chrisb/Sites/studio/dist/chunk-MCJNUXQ6.js","../src/types.ts"],"names":["ext","base","outputExt"],"mappings":"AAAA;ACuEO,SAAS,gBAAA,CAAiB,YAAA,EAAsB,IAAA,EAA2C;AAChG,EAAA,GAAA,CAAI,KAAA,IAAS,MAAA,EAAQ;AACnB,IAAA,MAAMA,KAAAA,kBAAM,YAAA,mBAAa,KAAA,mBAAM,QAAQ,CAAA,4BAAA,CAAI,CAAC,IAAA,GAAK,MAAA;AACjD,IAAA,MAAMC,MAAAA,EAAO,YAAA,CAAa,OAAA,CAAQ,QAAA,EAAU,EAAE,CAAA;AAC9C,IAAA,MAAMC,WAAAA,EAAYF,IAAAA,CAAI,WAAA,CAAY,EAAA,IAAM,OAAA,EAAS,OAAA,EAAS,MAAA;AAC1D,IAAA,OAAO,CAAA,OAAA,EAAUC,KAAI,CAAA,EAAA;AACvB,EAAA;AACyB,EAAA;AACZ,EAAA;AACS,EAAA;AACG,EAAA;AAC3B;AAKgB;AACP,EAAA;AACY,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACnB,EAAA;AACF;ADzE2B;AACA;AACA;AACA;AACA","file":"/Users/chrisb/Sites/studio/dist/chunk-MCJNUXQ6.js","sourcesContent":[null,"/**\n * Meta entry - works for images and non-images\n * Images have w, h, b (after processing)\n * c is the index into _cdns array (omit if not on CDN)\n */\nexport interface MetaEntry {\n w?: number // original width (images only)\n h?: number // original height (images only)\n b?: string // blurhash (images only, after processing)\n p?: 1 // processed (has thumbnails and blurhash)\n c?: number // CDN index - index into _cdns array (omit if not on CDN)\n}\n\n/**\n * Full meta schema including special keys\n * _cdns: Array of CDN base URLs\n * Other keys: file paths from public folder\n */\nexport interface FullMeta {\n _cdns?: string[] // Array of CDN base URLs\n [key: string]: MetaEntry | string[] | undefined\n}\n\n/**\n * Meta schema - keyed by path from public folder (legacy type)\n * Example: { \"/portfolio/photo.jpg\": { w: 2400, h: 1600, b: \"...\" } }\n */\nexport type LeanMeta = Record<string, MetaEntry>\n\n// Legacy alias for compatibility\nexport type LeanImageEntry = MetaEntry\n\n/**\n * File/folder item for browser\n */\nexport interface FileItem {\n name: string\n path: string\n type: 'file' | 'folder'\n size?: number\n dimensions?: { width: number; height: number }\n isProcessed?: boolean\n cdnPushed?: boolean\n // Folder-specific properties\n fileCount?: number\n totalSize?: number\n // For showing thumbnails - path to -sm version if exists\n thumbnail?: string\n // Whether a processed thumbnail exists\n hasThumbnail?: boolean\n}\n\n/**\n * Studio configuration\n */\nexport interface StudioConfig {\n r2AccountId?: string\n r2AccessKeyId?: string\n r2SecretAccessKey?: string\n r2BucketName?: string\n r2PublicUrl?: string\n thumbnailSizes?: {\n small: number\n medium: number\n large: number\n }\n}\n\n/**\n * Get thumbnail path from original image path\n */\nexport function getThumbnailPath(originalPath: string, size: 'sm' | 'md' | 'lg' | 'full'): string {\n if (size === 'full') {\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}${outputExt}`\n }\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}-${size}${outputExt}`\n}\n\n/**\n * Get all thumbnail paths for an image\n */\nexport function getAllThumbnailPaths(originalPath: string): string[] {\n return [\n getThumbnailPath(originalPath, 'full'),\n getThumbnailPath(originalPath, 'lg'),\n getThumbnailPath(originalPath, 'md'),\n getThumbnailPath(originalPath, 'sm'),\n ]\n}\n"]}
@@ -1,23 +1,9 @@
1
- import { F as FileItem } from '../types-C4hCz2w8.mjs';
2
1
  import { NextRequest, NextResponse } from 'next/server';
3
2
 
4
3
  /**
5
4
  * Unified GET handler for all Studio API routes
6
5
  */
7
- declare function GET(request: NextRequest): Promise<NextResponse<{
8
- error: string;
9
- }> | NextResponse<{
10
- folders: {
11
- path: string;
12
- name: string;
13
- depth: number;
14
- }[];
15
- }> | NextResponse<{
16
- items: FileItem[];
17
- }> | NextResponse<{
18
- count: number;
19
- images: string[];
20
- }>>;
6
+ declare function GET(request: NextRequest): Promise<Response>;
21
7
  /**
22
8
  * Unified POST handler for all Studio API routes
23
9
  */
@@ -1,23 +1,9 @@
1
- import { F as FileItem } from '../types-C4hCz2w8.js';
2
1
  import { NextRequest, NextResponse } from 'next/server';
3
2
 
4
3
  /**
5
4
  * Unified GET handler for all Studio API routes
6
5
  */
7
- declare function GET(request: NextRequest): Promise<NextResponse<{
8
- error: string;
9
- }> | NextResponse<{
10
- folders: {
11
- path: string;
12
- name: string;
13
- depth: number;
14
- }[];
15
- }> | NextResponse<{
16
- items: FileItem[];
17
- }> | NextResponse<{
18
- count: number;
19
- images: string[];
20
- }>>;
6
+ declare function GET(request: NextRequest): Promise<Response>;
21
7
  /**
22
8
  * Unified POST handler for all Studio API routes
23
9
  */
@@ -1,7 +1,7 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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; }
2
2
 
3
3
 
4
- var _chunkL36EH3PMjs = require('../chunk-L36EH3PM.js');
4
+ var _chunkMCJNUXQ6js = require('../chunk-MCJNUXQ6.js');
5
5
 
6
6
  // src/handlers/index.ts
7
7
  var _server = require('next/server');
@@ -29,6 +29,35 @@ async function saveMeta(meta) {
29
29
  const metaPath = _path2.default.join(dataDir, "_meta.json");
30
30
  await _fs.promises.writeFile(metaPath, JSON.stringify(meta, null, 2));
31
31
  }
32
+ function getCdnUrls(meta) {
33
+ return meta._cdns || [];
34
+ }
35
+ function getOrAddCdnIndex(meta, cdnUrl) {
36
+ if (!meta._cdns) {
37
+ meta._cdns = [];
38
+ }
39
+ const normalizedUrl = cdnUrl.replace(/\/$/, "");
40
+ const existingIndex = meta._cdns.indexOf(normalizedUrl);
41
+ if (existingIndex >= 0) {
42
+ return existingIndex;
43
+ }
44
+ meta._cdns.push(normalizedUrl);
45
+ return meta._cdns.length - 1;
46
+ }
47
+ function getMetaEntry(meta, key) {
48
+ if (key.startsWith("_")) return void 0;
49
+ const value = meta[key];
50
+ if (Array.isArray(value)) return void 0;
51
+ return value;
52
+ }
53
+ function setMetaEntry(meta, key, entry) {
54
+ meta[key] = entry;
55
+ }
56
+ function getFileEntries(meta) {
57
+ return Object.entries(meta).filter(
58
+ ([key, value]) => !key.startsWith("_") && !Array.isArray(value)
59
+ );
60
+ }
32
61
 
33
62
  // src/handlers/utils/files.ts
34
63
 
@@ -156,7 +185,7 @@ async function uploadToCdn(imageKey) {
156
185
  const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
157
186
  if (!bucketName) throw new Error("R2 bucket not configured");
158
187
  const r2 = getR2Client();
159
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
188
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
160
189
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
161
190
  try {
162
191
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -173,7 +202,7 @@ async function uploadToCdn(imageKey) {
173
202
  }
174
203
  }
175
204
  async function deleteLocalThumbnails(imageKey) {
176
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
205
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
177
206
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
178
207
  try {
179
208
  await _fs.promises.unlink(localPath);
@@ -188,16 +217,17 @@ async function handleList(request) {
188
217
  const requestedPath = searchParams.get("path") || "public";
189
218
  try {
190
219
  const meta = await loadMeta();
191
- const metaKeys = Object.keys(meta);
192
- if (metaKeys.length === 0) {
220
+ const fileEntries = getFileEntries(meta);
221
+ const cdnUrls = getCdnUrls(meta);
222
+ if (fileEntries.length === 0) {
193
223
  return _server.NextResponse.json({ items: [], isEmpty: true });
194
224
  }
195
225
  const relativePath = requestedPath.replace(/^public\/?/, "");
196
226
  const pathPrefix = relativePath ? `/${relativePath}/` : "/";
197
227
  const items = [];
198
228
  const seenFolders = /* @__PURE__ */ new Set();
199
- for (const key of metaKeys) {
200
- const entry = meta[key];
229
+ const metaKeys = fileEntries.map(([key]) => key);
230
+ for (const [key, entry] of fileEntries) {
201
231
  if (!key.startsWith(pathPrefix) && pathPrefix !== "/") continue;
202
232
  if (pathPrefix === "/" && !key.startsWith("/")) continue;
203
233
  const remaining = pathPrefix === "/" ? key.slice(1) : key.slice(pathPrefix.length);
@@ -222,14 +252,14 @@ async function handleList(request) {
222
252
  } else {
223
253
  const fileName = remaining;
224
254
  const isImage = isImageFile(fileName);
225
- const isPushedToCloud = entry.c === 1;
255
+ const isPushedToCloud = entry.c !== void 0;
226
256
  let thumbnail;
227
257
  let hasThumbnail = false;
228
258
  let fileSize;
229
259
  if (isImage && (entry.w || entry.b)) {
230
- const thumbPath = _chunkL36EH3PMjs.getThumbnailPath.call(void 0, key, "sm");
231
- if (isPushedToCloud) {
232
- const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL;
260
+ const thumbPath = _chunkMCJNUXQ6js.getThumbnailPath.call(void 0, key, "sm");
261
+ if (isPushedToCloud && entry.c !== void 0) {
262
+ const cdnUrl = cdnUrls[entry.c];
233
263
  if (cdnUrl) {
234
264
  thumbnail = `${cdnUrl}${thumbPath}`;
235
265
  hasThumbnail = true;
@@ -284,19 +314,21 @@ async function handleSearch(request) {
284
314
  }
285
315
  try {
286
316
  const meta = await loadMeta();
317
+ const fileEntries = getFileEntries(meta);
318
+ const cdnUrls = getCdnUrls(meta);
287
319
  const items = [];
288
- for (const [key, entry] of Object.entries(meta)) {
320
+ for (const [key, entry] of fileEntries) {
289
321
  if (!key.toLowerCase().includes(query)) continue;
290
322
  const fileName = _path2.default.basename(key);
291
323
  const relativePath = key.slice(1);
292
324
  const isImage = isImageFile(fileName);
293
- const isPushedToCloud = entry.c === 1;
325
+ const isPushedToCloud = entry.c !== void 0;
294
326
  let thumbnail;
295
327
  let hasThumbnail = false;
296
328
  if (isImage && (entry.w || entry.b)) {
297
- const thumbPath = _chunkL36EH3PMjs.getThumbnailPath.call(void 0, key, "sm");
298
- if (isPushedToCloud) {
299
- const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL;
329
+ const thumbPath = _chunkMCJNUXQ6js.getThumbnailPath.call(void 0, key, "sm");
330
+ if (isPushedToCloud && entry.c !== void 0) {
331
+ const cdnUrl = cdnUrls[entry.c];
300
332
  if (cdnUrl) {
301
333
  thumbnail = `${cdnUrl}${thumbPath}`;
302
334
  hasThumbnail = true;
@@ -336,8 +368,9 @@ async function handleSearch(request) {
336
368
  async function handleListFolders() {
337
369
  try {
338
370
  const meta = await loadMeta();
371
+ const fileEntries = getFileEntries(meta);
339
372
  const folderSet = /* @__PURE__ */ new Set();
340
- for (const key of Object.keys(meta)) {
373
+ for (const [key] of fileEntries) {
341
374
  const parts = key.split("/");
342
375
  let current = "";
343
376
  for (let i = 1; i < parts.length - 1; i++) {
@@ -366,8 +399,9 @@ async function handleListFolders() {
366
399
  async function handleCountImages() {
367
400
  try {
368
401
  const meta = await loadMeta();
402
+ const fileEntries = getFileEntries(meta);
369
403
  const allImages = [];
370
- for (const key of Object.keys(meta)) {
404
+ for (const [key] of fileEntries) {
371
405
  const fileName = _path2.default.basename(key);
372
406
  if (isImageFile(fileName)) {
373
407
  allImages.push(key.slice(1));
@@ -391,12 +425,13 @@ async function handleFolderImages(request) {
391
425
  }
392
426
  const folders = foldersParam.split(",");
393
427
  const meta = await loadMeta();
428
+ const fileEntries = getFileEntries(meta);
394
429
  const allImages = [];
395
430
  const prefixes = folders.map((f) => {
396
431
  const rel = f.replace(/^public\/?/, "");
397
432
  return rel ? `/${rel}/` : "/";
398
433
  });
399
- for (const key of Object.keys(meta)) {
434
+ for (const [key] of fileEntries) {
400
435
  const fileName = _path2.default.basename(key);
401
436
  if (!isImageFile(fileName)) continue;
402
437
  for (const prefix of prefixes) {
@@ -524,7 +559,7 @@ async function handleDelete(request) {
524
559
  for (const key of Object.keys(meta)) {
525
560
  if (key.startsWith(prefix) || key === imageKey) {
526
561
  if (!meta[key].c) {
527
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, key)) {
562
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, key)) {
528
563
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
529
564
  try {
530
565
  await _fs.promises.unlink(absoluteThumbPath);
@@ -540,7 +575,7 @@ async function handleDelete(request) {
540
575
  const isInImagesFolder = itemPath.startsWith("public/images/");
541
576
  if (!isInImagesFolder && entry) {
542
577
  if (!isPushedToCloud) {
543
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
578
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
544
579
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
545
580
  try {
546
581
  await _fs.promises.unlink(absoluteThumbPath);
@@ -652,8 +687,8 @@ async function handleRename(request) {
652
687
  const newKey = "/" + newRelativePath;
653
688
  if (meta[oldKey]) {
654
689
  const entry = meta[oldKey];
655
- const oldThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, oldKey);
656
- const newThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, newKey);
690
+ const oldThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, oldKey);
691
+ const newThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, newKey);
657
692
  for (let i = 0; i < oldThumbPaths.length; i++) {
658
693
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
659
694
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
@@ -732,8 +767,8 @@ async function handleMove(request) {
732
767
  const newKey = "/" + newRelativePath;
733
768
  if (meta[oldKey]) {
734
769
  const entry = meta[oldKey];
735
- const oldThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, oldKey);
736
- const newThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, newKey);
770
+ const oldThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, oldKey);
771
+ const newThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, newKey);
737
772
  for (let i = 0; i < oldThumbPaths.length; i++) {
738
773
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
739
774
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
@@ -790,6 +825,7 @@ async function handleSync(request) {
790
825
  return _server.NextResponse.json({ error: "No image keys provided" }, { status: 400 });
791
826
  }
792
827
  const meta = await loadMeta();
828
+ const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
793
829
  const r2 = new (0, _clients3.S3Client)({
794
830
  region: "auto",
795
831
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
@@ -798,12 +834,12 @@ async function handleSync(request) {
798
834
  const pushed = [];
799
835
  const errors = [];
800
836
  for (const imageKey of imageKeys) {
801
- const entry = meta[imageKey];
837
+ const entry = getMetaEntry(meta, imageKey);
802
838
  if (!entry) {
803
839
  errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
804
840
  continue;
805
841
  }
806
- if (entry.c) {
842
+ if (entry.c !== void 0) {
807
843
  pushed.push(imageKey);
808
844
  continue;
809
845
  }
@@ -827,7 +863,7 @@ async function handleSync(request) {
827
863
  errors.push(`Original file not found: ${imageKey}`);
828
864
  continue;
829
865
  }
830
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
866
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
831
867
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
832
868
  try {
833
869
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -842,8 +878,8 @@ async function handleSync(request) {
842
878
  } catch (e20) {
843
879
  }
844
880
  }
845
- entry.c = 1;
846
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
881
+ entry.c = cdnIndex;
882
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
847
883
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
848
884
  try {
849
885
  await _fs.promises.unlink(localPath);
@@ -883,8 +919,9 @@ async function handleReprocess(request) {
883
919
  for (const imageKey of imageKeys) {
884
920
  try {
885
921
  let buffer;
886
- const entry = meta[imageKey];
887
- const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _6 => _6.c]) === 1;
922
+ const entry = getMetaEntry(meta, imageKey);
923
+ const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _6 => _6.c]) !== void 0;
924
+ const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _7 => _7.c]);
888
925
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
889
926
  try {
890
927
  buffer = await _fs.promises.readFile(originalPath);
@@ -900,7 +937,7 @@ async function handleReprocess(request) {
900
937
  }
901
938
  const updatedEntry = await processImage(buffer, imageKey);
902
939
  if (isPushedToCloud) {
903
- updatedEntry.c = 1;
940
+ updatedEntry.c = existingCdnIndex;
904
941
  await uploadToCdn(imageKey);
905
942
  await deleteLocalThumbnails(imageKey);
906
943
  try {
@@ -942,7 +979,7 @@ async function handleProcessAllStream() {
942
979
  const orphansRemoved = [];
943
980
  let alreadyProcessed = 0;
944
981
  const imagesToProcess = [];
945
- for (const [key, entry] of Object.entries(meta)) {
982
+ for (const [key, entry] of getFileEntries(meta)) {
946
983
  const fileName = _path2.default.basename(key);
947
984
  if (!isImageFile(fileName)) continue;
948
985
  if (!entry.p) {
@@ -956,7 +993,8 @@ async function handleProcessAllStream() {
956
993
  for (let i = 0; i < imagesToProcess.length; i++) {
957
994
  const { key, entry } = imagesToProcess[i];
958
995
  const fullPath = _path2.default.join(process.cwd(), "public", key);
959
- const isInCloud = entry.c === 1;
996
+ const isInCloud = entry.c !== void 0;
997
+ const existingCdnIndex = entry.c;
960
998
  sendEvent({
961
999
  type: "progress",
962
1000
  current: i + 1,
@@ -996,7 +1034,7 @@ async function handleProcessAllStream() {
996
1034
  meta[key] = {
997
1035
  ...processedEntry,
998
1036
  p: 1,
999
- ...isInCloud ? { c: 1 } : {}
1037
+ ...isInCloud ? { c: existingCdnIndex } : {}
1000
1038
  };
1001
1039
  }
1002
1040
  if (isInCloud) {
@@ -1015,9 +1053,9 @@ async function handleProcessAllStream() {
1015
1053
  }
1016
1054
  sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
1017
1055
  const trackedPaths = /* @__PURE__ */ new Set();
1018
- for (const imageKey of Object.keys(meta)) {
1019
- if (!meta[imageKey].c) {
1020
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1056
+ for (const [imageKey, entry] of getFileEntries(meta)) {
1057
+ if (entry.c === void 0) {
1058
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
1021
1059
  trackedPaths.add(thumbPath);
1022
1060
  }
1023
1061
  }
@@ -1104,6 +1142,7 @@ async function handleProcessAllStream() {
1104
1142
 
1105
1143
 
1106
1144
 
1145
+
1107
1146
  async function handleScanStream() {
1108
1147
  const encoder = new TextEncoder();
1109
1148
  const stream = new ReadableStream({
@@ -1115,6 +1154,7 @@ async function handleScanStream() {
1115
1154
  };
1116
1155
  try {
1117
1156
  const meta = await loadMeta();
1157
+ const existingCount = Object.keys(meta).length;
1118
1158
  const existingKeys = new Set(Object.keys(meta));
1119
1159
  const added = [];
1120
1160
  const renamed = [];
@@ -1182,13 +1222,17 @@ async function handleScanStream() {
1182
1222
  if (isImage) {
1183
1223
  const ext = _path2.default.extname(relativePath).toLowerCase();
1184
1224
  if (ext === ".svg") {
1185
- meta[imageKey] = { w: 0, h: 0 };
1225
+ meta[imageKey] = { w: 0, h: 0, b: "" };
1186
1226
  } else {
1187
1227
  try {
1188
- const metadata = await _sharp2.default.call(void 0, fullPath).metadata();
1228
+ const buffer = await _fs.promises.readFile(fullPath);
1229
+ const metadata = await _sharp2.default.call(void 0, buffer).metadata();
1230
+ const { data, info } = await _sharp2.default.call(void 0, buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1231
+ const blurhash = _blurhash.encode.call(void 0, new Uint8ClampedArray(data), info.width, info.height, 4, 4);
1189
1232
  meta[imageKey] = {
1190
1233
  w: metadata.width || 0,
1191
- h: metadata.height || 0
1234
+ h: metadata.height || 0,
1235
+ b: blurhash
1192
1236
  };
1193
1237
  } catch (e31) {
1194
1238
  meta[imageKey] = { w: 0, h: 0 };
@@ -1207,6 +1251,7 @@ async function handleScanStream() {
1207
1251
  await saveMeta(meta);
1208
1252
  sendEvent({
1209
1253
  type: "complete",
1254
+ existingCount,
1210
1255
  added: added.length,
1211
1256
  renamed: renamed.length,
1212
1257
  errors: errors.length,
@@ -1229,6 +1274,132 @@ async function handleScanStream() {
1229
1274
  });
1230
1275
  }
1231
1276
 
1277
+ // src/handlers/import.ts
1278
+
1279
+
1280
+ function parseImageUrl(url) {
1281
+ const parsed = new URL(url);
1282
+ const base = `${parsed.protocol}//${parsed.host}`;
1283
+ const path9 = parsed.pathname;
1284
+ return { base, path: path9 };
1285
+ }
1286
+ async function processRemoteImage(url) {
1287
+ const response = await fetch(url);
1288
+ if (!response.ok) {
1289
+ throw new Error(`Failed to fetch: ${response.status}`);
1290
+ }
1291
+ const buffer = Buffer.from(await response.arrayBuffer());
1292
+ const metadata = await _sharp2.default.call(void 0, buffer).metadata();
1293
+ const { data, info } = await _sharp2.default.call(void 0, buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1294
+ const blurhash = _blurhash.encode.call(void 0, new Uint8ClampedArray(data), info.width, info.height, 4, 4);
1295
+ return {
1296
+ w: metadata.width || 0,
1297
+ h: metadata.height || 0,
1298
+ b: blurhash
1299
+ };
1300
+ }
1301
+ async function handleImportUrls(request) {
1302
+ const encoder = new TextEncoder();
1303
+ const stream = new ReadableStream({
1304
+ async start(controller) {
1305
+ const sendEvent = (data) => {
1306
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
1307
+
1308
+ `));
1309
+ };
1310
+ try {
1311
+ const { urls } = await request.json();
1312
+ if (!urls || !Array.isArray(urls) || urls.length === 0) {
1313
+ sendEvent({ type: "error", message: "No URLs provided" });
1314
+ controller.close();
1315
+ return;
1316
+ }
1317
+ const meta = await loadMeta();
1318
+ const added = [];
1319
+ const skipped = [];
1320
+ const errors = [];
1321
+ const total = urls.length;
1322
+ sendEvent({ type: "start", total });
1323
+ for (let i = 0; i < urls.length; i++) {
1324
+ const url = urls[i].trim();
1325
+ if (!url) continue;
1326
+ sendEvent({
1327
+ type: "progress",
1328
+ current: i + 1,
1329
+ total,
1330
+ percent: Math.round((i + 1) / total * 100),
1331
+ currentFile: url
1332
+ });
1333
+ try {
1334
+ const { base, path: path9 } = parseImageUrl(url);
1335
+ const existingEntry = getMetaEntry(meta, path9);
1336
+ if (existingEntry) {
1337
+ skipped.push(path9);
1338
+ continue;
1339
+ }
1340
+ const cdnIndex = getOrAddCdnIndex(meta, base);
1341
+ const imageData = await processRemoteImage(url);
1342
+ setMetaEntry(meta, path9, {
1343
+ w: imageData.w,
1344
+ h: imageData.h,
1345
+ b: imageData.b,
1346
+ c: cdnIndex
1347
+ });
1348
+ added.push(path9);
1349
+ } catch (error) {
1350
+ console.error(`Failed to import ${url}:`, error);
1351
+ errors.push(url);
1352
+ }
1353
+ }
1354
+ await saveMeta(meta);
1355
+ sendEvent({
1356
+ type: "complete",
1357
+ added: added.length,
1358
+ skipped: skipped.length,
1359
+ errors: errors.length
1360
+ });
1361
+ } catch (error) {
1362
+ console.error("Import failed:", error);
1363
+ sendEvent({ type: "error", message: "Import failed" });
1364
+ } finally {
1365
+ controller.close();
1366
+ }
1367
+ }
1368
+ });
1369
+ return new Response(stream, {
1370
+ headers: {
1371
+ "Content-Type": "text/event-stream",
1372
+ "Cache-Control": "no-cache",
1373
+ "Connection": "keep-alive"
1374
+ }
1375
+ });
1376
+ }
1377
+ async function handleGetCdns() {
1378
+ try {
1379
+ const meta = await loadMeta();
1380
+ const cdns = meta._cdns || [];
1381
+ return Response.json({ cdns });
1382
+ } catch (error) {
1383
+ console.error("Failed to get CDNs:", error);
1384
+ return Response.json({ error: "Failed to get CDNs" }, { status: 500 });
1385
+ }
1386
+ }
1387
+ async function handleUpdateCdns(request) {
1388
+ try {
1389
+ const { cdns } = await request.json();
1390
+ if (!Array.isArray(cdns)) {
1391
+ return Response.json({ error: "Invalid CDN array" }, { status: 400 });
1392
+ }
1393
+ const meta = await loadMeta();
1394
+ meta._cdns = cdns.map((url) => url.replace(/\/$/, ""));
1395
+ await saveMeta(meta);
1396
+ return Response.json({ success: true, cdns: meta._cdns });
1397
+ } catch (error) {
1398
+ console.error("Failed to update CDNs:", error);
1399
+ return Response.json({ error: "Failed to update CDNs" }, { status: 500 });
1400
+ }
1401
+ }
1402
+
1232
1403
  // src/handlers/index.ts
1233
1404
  async function GET(request) {
1234
1405
  if (process.env.NODE_ENV !== "development") {
@@ -1251,6 +1422,9 @@ async function GET(request) {
1251
1422
  if (route === "search") {
1252
1423
  return handleSearch(request);
1253
1424
  }
1425
+ if (route === "cdns") {
1426
+ return handleGetCdns();
1427
+ }
1254
1428
  return _server.NextResponse.json({ error: "Not found" }, { status: 404 });
1255
1429
  }
1256
1430
  async function POST(request) {
@@ -1286,6 +1460,12 @@ async function POST(request) {
1286
1460
  if (route === "scan") {
1287
1461
  return handleScanStream();
1288
1462
  }
1463
+ if (route === "import") {
1464
+ return handleImportUrls(request);
1465
+ }
1466
+ if (route === "cdns") {
1467
+ return handleUpdateCdns(request);
1468
+ }
1289
1469
  return _server.NextResponse.json({ error: "Not found" }, { status: 404 });
1290
1470
  }
1291
1471
  async function DELETE(request) {