@gallop.software/studio 0.1.116 → 1.0.1

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-RDNC5ABF.mjs.map
27
+ //# sourceMappingURL=chunk-FDWPNRNZ.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 cdnBaseUrl?: string // CDN base URL when pushed to cloud\n isRemote?: boolean // true if CDN URL doesn't match R2 (external import)\n isProtected?: boolean // true for images folder and its contents (cannot select/modify)\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":";AA0EO,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-LEOQKJCL.js.map
27
+ //# sourceMappingURL=chunk-WJJHVPLT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/Users/chrisb/Sites/studio/dist/chunk-WJJHVPLT.js","../src/types.ts"],"names":["ext","base","outputExt"],"mappings":"AAAA;AC0EO,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;AD5E2B;AACA;AACA;AACA;AACA","file":"/Users/chrisb/Sites/studio/dist/chunk-WJJHVPLT.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 cdnBaseUrl?: string // CDN base URL when pushed to cloud\n isRemote?: boolean // true if CDN URL doesn't match R2 (external import)\n isProtected?: boolean // true for images folder and its contents (cannot select/modify)\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,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 _chunkLEOQKJCLjs = require('../chunk-LEOQKJCL.js');
4
+ var _chunkWJJHVPLTjs = require('../chunk-WJJHVPLT.js');
5
5
 
6
6
  // src/handlers/index.ts
7
7
  var _server = require('next/server');
@@ -219,7 +219,7 @@ async function uploadToCdn(imageKey) {
219
219
  const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
220
220
  if (!bucketName) throw new Error("R2 bucket not configured");
221
221
  const r2 = getR2Client();
222
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
222
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
223
223
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
224
224
  try {
225
225
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -236,7 +236,7 @@ async function uploadToCdn(imageKey) {
236
236
  }
237
237
  }
238
238
  async function deleteLocalThumbnails(imageKey) {
239
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
239
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
240
240
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
241
241
  try {
242
242
  await _fs.promises.unlink(localPath);
@@ -281,7 +281,7 @@ async function deleteFromCdn(imageKey, hasThumbnails) {
281
281
  } catch (e4) {
282
282
  }
283
283
  if (hasThumbnails) {
284
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
284
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
285
285
  try {
286
286
  await r2.send(
287
287
  new (0, _clients3.DeleteObjectCommand)({
@@ -309,28 +309,61 @@ async function handleList(request) {
309
309
  const items = [];
310
310
  const seenFolders = /* @__PURE__ */ new Set();
311
311
  const metaKeys = fileEntries.map(([key]) => key);
312
+ const isInsideImagesFolder = relativePath === "images" || relativePath.startsWith("images/");
312
313
  const absoluteDir = _path2.default.join(process.cwd(), requestedPath);
313
314
  try {
314
315
  const dirEntries = await _fs.promises.readdir(absoluteDir, { withFileTypes: true });
315
316
  for (const entry of dirEntries) {
316
- if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "images") {
317
+ if (entry.name.startsWith(".")) continue;
318
+ if (entry.isDirectory()) {
317
319
  if (!seenFolders.has(entry.name)) {
318
320
  seenFolders.add(entry.name);
319
- const folderPrefix = pathPrefix === "/" ? `/${entry.name}/` : `${pathPrefix}${entry.name}/`;
321
+ const isImagesFolder = entry.name === "images" && !relativePath;
322
+ const folderPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
320
323
  let fileCount = 0;
321
- for (const k of metaKeys) {
322
- if (k.startsWith(folderPrefix)) fileCount++;
324
+ if (isInsideImagesFolder || isImagesFolder) {
325
+ const subDir = _path2.default.join(absoluteDir, entry.name);
326
+ try {
327
+ const subEntries = await _fs.promises.readdir(subDir);
328
+ fileCount = subEntries.filter((f) => !f.startsWith(".")).length;
329
+ } catch (e6) {
330
+ }
331
+ } else {
332
+ const folderPrefix = pathPrefix === "/" ? `/${entry.name}/` : `${pathPrefix}${entry.name}/`;
333
+ for (const k of metaKeys) {
334
+ if (k.startsWith(folderPrefix)) fileCount++;
335
+ }
323
336
  }
324
337
  items.push({
325
338
  name: entry.name,
326
- path: relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`,
339
+ path: folderPath,
327
340
  type: "folder",
328
- fileCount
341
+ fileCount,
342
+ isProtected: isImagesFolder || isInsideImagesFolder
329
343
  });
330
344
  }
345
+ } else if (isInsideImagesFolder) {
346
+ const filePath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
347
+ const fullPath = _path2.default.join(absoluteDir, entry.name);
348
+ let fileSize;
349
+ try {
350
+ const stats = await _fs.promises.stat(fullPath);
351
+ fileSize = stats.size;
352
+ } catch (e7) {
353
+ }
354
+ const isImage = isImageFile(entry.name);
355
+ items.push({
356
+ name: entry.name,
357
+ path: filePath,
358
+ type: "file",
359
+ size: fileSize,
360
+ thumbnail: isImage ? `/${relativePath}/${entry.name}` : void 0,
361
+ hasThumbnail: false,
362
+ isProtected: true
363
+ });
331
364
  }
332
365
  }
333
- } catch (e6) {
366
+ } catch (e8) {
334
367
  }
335
368
  if (fileEntries.length === 0 && items.length === 0) {
336
369
  return _server.NextResponse.json({ items: [], isEmpty: true });
@@ -354,7 +387,8 @@ async function handleList(request) {
354
387
  name: folderName,
355
388
  path: relativePath ? `public/${relativePath}/${folderName}` : `public/${folderName}`,
356
389
  type: "folder",
357
- fileCount
390
+ fileCount,
391
+ isProtected: isInsideImagesFolder
358
392
  });
359
393
  }
360
394
  } else {
@@ -367,7 +401,7 @@ async function handleList(request) {
367
401
  let hasThumbnail = false;
368
402
  let fileSize;
369
403
  if (isImage && entry.p === 1) {
370
- const thumbPath = _chunkLEOQKJCLjs.getThumbnailPath.call(void 0, key, "sm");
404
+ const thumbPath = _chunkWJJHVPLTjs.getThumbnailPath.call(void 0, key, "sm");
371
405
  if (isPushedToCloud && entry.c !== void 0) {
372
406
  const cdnUrl = cdnUrls[entry.c];
373
407
  if (cdnUrl) {
@@ -380,7 +414,7 @@ async function handleList(request) {
380
414
  await _fs.promises.access(localThumbPath);
381
415
  thumbnail = thumbPath;
382
416
  hasThumbnail = true;
383
- } catch (e7) {
417
+ } catch (e9) {
384
418
  thumbnail = key;
385
419
  hasThumbnail = false;
386
420
  }
@@ -399,7 +433,7 @@ async function handleList(request) {
399
433
  const filePath = _path2.default.join(process.cwd(), "public", key);
400
434
  const stats = await _fs.promises.stat(filePath);
401
435
  fileSize = stats.size;
402
- } catch (e8) {
436
+ } catch (e10) {
403
437
  }
404
438
  }
405
439
  items.push({
@@ -413,6 +447,7 @@ async function handleList(request) {
413
447
  cdnPushed: isPushedToCloud,
414
448
  cdnBaseUrl: fileCdnUrl,
415
449
  isRemote,
450
+ isProtected: isInsideImagesFolder,
416
451
  dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
417
452
  });
418
453
  }
@@ -446,7 +481,7 @@ async function handleSearch(request) {
446
481
  let thumbnail;
447
482
  let hasThumbnail = false;
448
483
  if (isImage && entry.p === 1) {
449
- const thumbPath = _chunkLEOQKJCLjs.getThumbnailPath.call(void 0, key, "sm");
484
+ const thumbPath = _chunkWJJHVPLTjs.getThumbnailPath.call(void 0, key, "sm");
450
485
  if (isPushedToCloud && entry.c !== void 0) {
451
486
  const cdnUrl = cdnUrls[entry.c];
452
487
  if (cdnUrl) {
@@ -459,7 +494,7 @@ async function handleSearch(request) {
459
494
  await _fs.promises.access(localThumbPath);
460
495
  thumbnail = thumbPath;
461
496
  hasThumbnail = true;
462
- } catch (e9) {
497
+ } catch (e11) {
463
498
  thumbnail = key;
464
499
  hasThumbnail = false;
465
500
  }
@@ -515,7 +550,7 @@ async function handleListFolders() {
515
550
  await scanDir(_path2.default.join(dir, entry.name), folderRelPath);
516
551
  }
517
552
  }
518
- } catch (e10) {
553
+ } catch (e12) {
519
554
  }
520
555
  }
521
556
  const publicDir = _path2.default.join(process.cwd(), "public");
@@ -656,7 +691,7 @@ async function handleUpload(request) {
656
691
  w: metadata.width || 0,
657
692
  h: metadata.height || 0
658
693
  };
659
- } catch (e11) {
694
+ } catch (e13) {
660
695
  meta[imageKey] = { w: 0, h: 0 };
661
696
  }
662
697
  } else {
@@ -701,11 +736,11 @@ async function handleDelete(request) {
701
736
  for (const key of Object.keys(meta)) {
702
737
  if (key.startsWith(prefix) || key === imageKey) {
703
738
  if (!meta[key].c) {
704
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, key)) {
739
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, key)) {
705
740
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
706
741
  try {
707
742
  await _fs.promises.unlink(absoluteThumbPath);
708
- } catch (e12) {
743
+ } catch (e14) {
709
744
  }
710
745
  }
711
746
  }
@@ -717,18 +752,18 @@ async function handleDelete(request) {
717
752
  const isInImagesFolder = itemPath.startsWith("public/images/");
718
753
  if (!isInImagesFolder && entry) {
719
754
  if (!isPushedToCloud) {
720
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
755
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
721
756
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
722
757
  try {
723
758
  await _fs.promises.unlink(absoluteThumbPath);
724
- } catch (e13) {
759
+ } catch (e15) {
725
760
  }
726
761
  }
727
762
  }
728
763
  delete meta[imageKey];
729
764
  }
730
765
  }
731
- } catch (e14) {
766
+ } catch (e16) {
732
767
  if (entry) {
733
768
  delete meta[imageKey];
734
769
  } else {
@@ -781,7 +816,7 @@ async function handleCreateFolder(request) {
781
816
  try {
782
817
  await _fs.promises.access(folderPath);
783
818
  return _server.NextResponse.json({ error: "A folder with this name already exists" }, { status: 400 });
784
- } catch (e15) {
819
+ } catch (e17) {
785
820
  }
786
821
  await _fs.promises.mkdir(folderPath, { recursive: true });
787
822
  return _server.NextResponse.json({ success: true, path: _path2.default.join(safePath, sanitizedName) });
@@ -809,13 +844,13 @@ async function handleRename(request) {
809
844
  }
810
845
  try {
811
846
  await _fs.promises.access(absoluteOldPath);
812
- } catch (e16) {
847
+ } catch (e18) {
813
848
  return _server.NextResponse.json({ error: "File or folder not found" }, { status: 404 });
814
849
  }
815
850
  try {
816
851
  await _fs.promises.access(absoluteNewPath);
817
852
  return _server.NextResponse.json({ error: "An item with this name already exists" }, { status: 400 });
818
- } catch (e17) {
853
+ } catch (e19) {
819
854
  }
820
855
  const stats = await _fs.promises.stat(absoluteOldPath);
821
856
  const isFile = stats.isFile();
@@ -829,15 +864,15 @@ async function handleRename(request) {
829
864
  const newKey = "/" + newRelativePath;
830
865
  if (meta[oldKey]) {
831
866
  const entry = meta[oldKey];
832
- const oldThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, oldKey);
833
- const newThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, newKey);
867
+ const oldThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, oldKey);
868
+ const newThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, newKey);
834
869
  for (let i = 0; i < oldThumbPaths.length; i++) {
835
870
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
836
871
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
837
872
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
838
873
  try {
839
874
  await _fs.promises.rename(oldThumbPath, newThumbPath);
840
- } catch (e18) {
875
+ } catch (e20) {
841
876
  }
842
877
  }
843
878
  delete meta[oldKey];
@@ -952,7 +987,7 @@ async function handleMoveStream(request) {
952
987
  await deleteFromCdn(oldKey, hasProcessedThumbnails);
953
988
  try {
954
989
  await _fs.promises.unlink(newAbsolutePath);
955
- } catch (e19) {
990
+ } catch (e21) {
956
991
  }
957
992
  if (hasProcessedThumbnails) {
958
993
  await deleteLocalThumbnails(newKey);
@@ -969,7 +1004,7 @@ async function handleMoveStream(request) {
969
1004
  }
970
1005
  try {
971
1006
  await _fs.promises.access(absolutePath);
972
- } catch (e20) {
1007
+ } catch (e22) {
973
1008
  errors.push(`${itemName} not found`);
974
1009
  continue;
975
1010
  }
@@ -977,20 +1012,20 @@ async function handleMoveStream(request) {
977
1012
  await _fs.promises.access(newAbsolutePath);
978
1013
  errors.push(`${itemName} already exists in destination`);
979
1014
  continue;
980
- } catch (e21) {
1015
+ } catch (e23) {
981
1016
  }
982
1017
  await _fs.promises.rename(absolutePath, newAbsolutePath);
983
1018
  const stats = await _fs.promises.stat(newAbsolutePath);
984
1019
  if (stats.isFile() && isImage && entry) {
985
- const oldThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, oldKey);
986
- const newThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, newKey);
1020
+ const oldThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, oldKey);
1021
+ const newThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, newKey);
987
1022
  for (let j = 0; j < oldThumbPaths.length; j++) {
988
1023
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[j]);
989
1024
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[j]);
990
1025
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
991
1026
  try {
992
1027
  await _fs.promises.rename(oldThumbPath, newThumbPath);
993
- } catch (e22) {
1028
+ } catch (e24) {
994
1029
  }
995
1030
  }
996
1031
  delete meta[oldKey];
@@ -1092,7 +1127,7 @@ async function handleSync(request) {
1092
1127
  const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
1093
1128
  try {
1094
1129
  originalBuffer = await _fs.promises.readFile(originalLocalPath);
1095
- } catch (e23) {
1130
+ } catch (e25) {
1096
1131
  errors.push(`Original file not found: ${imageKey}`);
1097
1132
  continue;
1098
1133
  }
@@ -1107,7 +1142,7 @@ async function handleSync(request) {
1107
1142
  );
1108
1143
  urlsToPurge.push(`${publicUrl}${imageKey}`);
1109
1144
  if (!isRemote && entry.p) {
1110
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1145
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1111
1146
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1112
1147
  try {
1113
1148
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -1120,23 +1155,23 @@ async function handleSync(request) {
1120
1155
  })
1121
1156
  );
1122
1157
  urlsToPurge.push(`${publicUrl}${thumbPath}`);
1123
- } catch (e24) {
1158
+ } catch (e26) {
1124
1159
  }
1125
1160
  }
1126
1161
  }
1127
1162
  entry.c = cdnIndex;
1128
1163
  if (!isRemote) {
1129
1164
  const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
1130
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1165
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1131
1166
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1132
1167
  try {
1133
1168
  await _fs.promises.unlink(localPath);
1134
- } catch (e25) {
1169
+ } catch (e27) {
1135
1170
  }
1136
1171
  }
1137
1172
  try {
1138
1173
  await _fs.promises.unlink(originalLocalPath);
1139
- } catch (e26) {
1174
+ } catch (e28) {
1140
1175
  }
1141
1176
  }
1142
1177
  pushed.push(imageKey);
@@ -1182,7 +1217,7 @@ async function handleReprocess(request) {
1182
1217
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
1183
1218
  try {
1184
1219
  buffer = await _fs.promises.readFile(originalPath);
1185
- } catch (e27) {
1220
+ } catch (e29) {
1186
1221
  if (isInOurR2) {
1187
1222
  buffer = await downloadFromCdn(imageKey);
1188
1223
  const dir = _path2.default.dirname(originalPath);
@@ -1202,13 +1237,13 @@ async function handleReprocess(request) {
1202
1237
  if (isInOurR2) {
1203
1238
  updatedEntry.c = existingCdnIndex;
1204
1239
  await uploadToCdn(imageKey);
1205
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1240
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1206
1241
  urlsToPurge.push(`${publicUrl}${thumbPath}`);
1207
1242
  }
1208
1243
  await deleteLocalThumbnails(imageKey);
1209
1244
  try {
1210
1245
  await _fs.promises.unlink(originalPath);
1211
- } catch (e28) {
1246
+ } catch (e30) {
1212
1247
  }
1213
1248
  } else if (isRemote) {
1214
1249
  }
@@ -1323,13 +1358,13 @@ async function handleProcessAllStream() {
1323
1358
  }
1324
1359
  if (isInOurR2) {
1325
1360
  await uploadToCdn(key);
1326
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, key)) {
1361
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, key)) {
1327
1362
  urlsToPurge.push(`${publicUrl}${thumbPath}`);
1328
1363
  }
1329
1364
  await deleteLocalThumbnails(key);
1330
1365
  try {
1331
1366
  await _fs.promises.unlink(fullPath);
1332
- } catch (e29) {
1367
+ } catch (e31) {
1333
1368
  }
1334
1369
  }
1335
1370
  processed.push(key.slice(1));
@@ -1342,7 +1377,7 @@ async function handleProcessAllStream() {
1342
1377
  const trackedPaths = /* @__PURE__ */ new Set();
1343
1378
  for (const [imageKey, entry] of getFileEntries(meta)) {
1344
1379
  if (entry.c === void 0) {
1345
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1380
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1346
1381
  trackedPaths.add(thumbPath);
1347
1382
  }
1348
1383
  }
@@ -1368,13 +1403,13 @@ async function handleProcessAllStream() {
1368
1403
  }
1369
1404
  }
1370
1405
  }
1371
- } catch (e30) {
1406
+ } catch (e32) {
1372
1407
  }
1373
1408
  }
1374
1409
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1375
1410
  try {
1376
1411
  await findOrphans(imagesDir);
1377
- } catch (e31) {
1412
+ } catch (e33) {
1378
1413
  }
1379
1414
  async function removeEmptyDirs(dir) {
1380
1415
  try {
@@ -1392,13 +1427,13 @@ async function handleProcessAllStream() {
1392
1427
  await _fs.promises.rmdir(dir);
1393
1428
  }
1394
1429
  return isEmpty;
1395
- } catch (e32) {
1430
+ } catch (e34) {
1396
1431
  return true;
1397
1432
  }
1398
1433
  }
1399
1434
  try {
1400
1435
  await removeEmptyDirs(imagesDir);
1401
- } catch (e33) {
1436
+ } catch (e35) {
1402
1437
  }
1403
1438
  await saveMeta(meta);
1404
1439
  if (urlsToPurge.length > 0) {
@@ -1433,6 +1468,7 @@ async function handleProcessAllStream() {
1433
1468
 
1434
1469
 
1435
1470
 
1471
+
1436
1472
  async function handleScanStream() {
1437
1473
  const encoder = new TextEncoder();
1438
1474
  const stream = new ReadableStream({
@@ -1444,11 +1480,12 @@ async function handleScanStream() {
1444
1480
  };
1445
1481
  try {
1446
1482
  const meta = await loadMeta();
1447
- const existingCount = Object.keys(meta).length;
1483
+ const existingCount = Object.keys(meta).filter((k) => !k.startsWith("_")).length;
1448
1484
  const existingKeys = new Set(Object.keys(meta));
1449
1485
  const added = [];
1450
1486
  const renamed = [];
1451
1487
  const errors = [];
1488
+ const orphanedFiles = [];
1452
1489
  const allFiles = [];
1453
1490
  async function scanDir(dir, relativePath = "") {
1454
1491
  try {
@@ -1464,7 +1501,7 @@ async function handleScanStream() {
1464
1501
  allFiles.push({ relativePath: relPath, fullPath });
1465
1502
  }
1466
1503
  }
1467
- } catch (e34) {
1504
+ } catch (e36) {
1468
1505
  }
1469
1506
  }
1470
1507
  const publicDir = _path2.default.join(process.cwd(), "public");
@@ -1524,7 +1561,7 @@ async function handleScanStream() {
1524
1561
  h: metadata.height || 0,
1525
1562
  b: blurhash
1526
1563
  };
1527
- } catch (e35) {
1564
+ } catch (e37) {
1528
1565
  meta[imageKey] = { w: 0, h: 0 };
1529
1566
  }
1530
1567
  }
@@ -1538,6 +1575,40 @@ async function handleScanStream() {
1538
1575
  errors.push(relativePath);
1539
1576
  }
1540
1577
  }
1578
+ sendEvent({ type: "cleanup", message: "Checking for orphaned thumbnails..." });
1579
+ const expectedThumbnails = /* @__PURE__ */ new Set();
1580
+ const fileEntries = getFileEntries(meta);
1581
+ for (const [imageKey, entry] of fileEntries) {
1582
+ if (entry.c === void 0 && entry.p === 1) {
1583
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1584
+ expectedThumbnails.add(thumbPath);
1585
+ }
1586
+ }
1587
+ }
1588
+ async function findOrphans(dir, relativePath = "") {
1589
+ try {
1590
+ const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
1591
+ for (const entry of entries) {
1592
+ if (entry.name.startsWith(".")) continue;
1593
+ const fullPath = _path2.default.join(dir, entry.name);
1594
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1595
+ if (entry.isDirectory()) {
1596
+ await findOrphans(fullPath, relPath);
1597
+ } else if (isImageFile(entry.name)) {
1598
+ const publicPath = `/images/${relPath}`;
1599
+ if (!expectedThumbnails.has(publicPath)) {
1600
+ orphanedFiles.push(publicPath);
1601
+ }
1602
+ }
1603
+ }
1604
+ } catch (e38) {
1605
+ }
1606
+ }
1607
+ const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1608
+ try {
1609
+ await findOrphans(imagesDir);
1610
+ } catch (e39) {
1611
+ }
1541
1612
  await saveMeta(meta);
1542
1613
  sendEvent({
1543
1614
  type: "complete",
@@ -1545,7 +1616,8 @@ async function handleScanStream() {
1545
1616
  added: added.length,
1546
1617
  renamed: renamed.length,
1547
1618
  errors: errors.length,
1548
- renamedFiles: renamed
1619
+ renamedFiles: renamed,
1620
+ orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0
1549
1621
  });
1550
1622
  } catch (error) {
1551
1623
  console.error("Scan failed:", error);
@@ -1563,6 +1635,63 @@ async function handleScanStream() {
1563
1635
  }
1564
1636
  });
1565
1637
  }
1638
+ async function handleDeleteOrphans(request) {
1639
+ try {
1640
+ const { paths } = await request.json();
1641
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
1642
+ return _server.NextResponse.json({ error: "No paths provided" }, { status: 400 });
1643
+ }
1644
+ const deleted = [];
1645
+ const errors = [];
1646
+ for (const orphanPath of paths) {
1647
+ if (!orphanPath.startsWith("/images/")) {
1648
+ errors.push(`Invalid path: ${orphanPath}`);
1649
+ continue;
1650
+ }
1651
+ const fullPath = _path2.default.join(process.cwd(), "public", orphanPath);
1652
+ try {
1653
+ await _fs.promises.unlink(fullPath);
1654
+ deleted.push(orphanPath);
1655
+ } catch (err) {
1656
+ console.error(`Failed to delete ${orphanPath}:`, err);
1657
+ errors.push(orphanPath);
1658
+ }
1659
+ }
1660
+ const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1661
+ async function removeEmptyDirs(dir) {
1662
+ try {
1663
+ const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
1664
+ let isEmpty = true;
1665
+ for (const entry of entries) {
1666
+ if (entry.isDirectory()) {
1667
+ const subDirEmpty = await removeEmptyDirs(_path2.default.join(dir, entry.name));
1668
+ if (!subDirEmpty) isEmpty = false;
1669
+ } else {
1670
+ isEmpty = false;
1671
+ }
1672
+ }
1673
+ if (isEmpty && dir !== imagesDir) {
1674
+ await _fs.promises.rmdir(dir);
1675
+ }
1676
+ return isEmpty;
1677
+ } catch (e40) {
1678
+ return true;
1679
+ }
1680
+ }
1681
+ try {
1682
+ await removeEmptyDirs(imagesDir);
1683
+ } catch (e41) {
1684
+ }
1685
+ return _server.NextResponse.json({
1686
+ success: true,
1687
+ deleted: deleted.length,
1688
+ errors: errors.length
1689
+ });
1690
+ } catch (error) {
1691
+ console.error("Failed to delete orphans:", error);
1692
+ return _server.NextResponse.json({ error: "Failed to delete orphaned files" }, { status: 500 });
1693
+ }
1694
+ }
1566
1695
 
1567
1696
  // src/handlers/import.ts
1568
1697
 
@@ -1750,6 +1879,9 @@ async function POST(request) {
1750
1879
  if (route === "scan") {
1751
1880
  return handleScanStream();
1752
1881
  }
1882
+ if (route === "delete-orphans") {
1883
+ return handleDeleteOrphans(request);
1884
+ }
1753
1885
  if (route === "import") {
1754
1886
  return handleImportUrls(request);
1755
1887
  }