@gallop.software/studio 0.1.115 → 1.0.0

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,13 +309,16 @@ 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.isDirectory() && !entry.name.startsWith(".")) {
317
318
  if (!seenFolders.has(entry.name)) {
318
319
  seenFolders.add(entry.name);
320
+ const isImagesFolder = entry.name === "images" && !relativePath;
321
+ const folderPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
319
322
  const folderPrefix = pathPrefix === "/" ? `/${entry.name}/` : `${pathPrefix}${entry.name}/`;
320
323
  let fileCount = 0;
321
324
  for (const k of metaKeys) {
@@ -323,9 +326,10 @@ async function handleList(request) {
323
326
  }
324
327
  items.push({
325
328
  name: entry.name,
326
- path: relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`,
329
+ path: folderPath,
327
330
  type: "folder",
328
- fileCount
331
+ fileCount,
332
+ isProtected: isImagesFolder || isInsideImagesFolder
329
333
  });
330
334
  }
331
335
  }
@@ -354,7 +358,8 @@ async function handleList(request) {
354
358
  name: folderName,
355
359
  path: relativePath ? `public/${relativePath}/${folderName}` : `public/${folderName}`,
356
360
  type: "folder",
357
- fileCount
361
+ fileCount,
362
+ isProtected: isInsideImagesFolder
358
363
  });
359
364
  }
360
365
  } else {
@@ -367,7 +372,7 @@ async function handleList(request) {
367
372
  let hasThumbnail = false;
368
373
  let fileSize;
369
374
  if (isImage && entry.p === 1) {
370
- const thumbPath = _chunkLEOQKJCLjs.getThumbnailPath.call(void 0, key, "sm");
375
+ const thumbPath = _chunkWJJHVPLTjs.getThumbnailPath.call(void 0, key, "sm");
371
376
  if (isPushedToCloud && entry.c !== void 0) {
372
377
  const cdnUrl = cdnUrls[entry.c];
373
378
  if (cdnUrl) {
@@ -413,6 +418,7 @@ async function handleList(request) {
413
418
  cdnPushed: isPushedToCloud,
414
419
  cdnBaseUrl: fileCdnUrl,
415
420
  isRemote,
421
+ isProtected: isInsideImagesFolder,
416
422
  dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
417
423
  });
418
424
  }
@@ -446,7 +452,7 @@ async function handleSearch(request) {
446
452
  let thumbnail;
447
453
  let hasThumbnail = false;
448
454
  if (isImage && entry.p === 1) {
449
- const thumbPath = _chunkLEOQKJCLjs.getThumbnailPath.call(void 0, key, "sm");
455
+ const thumbPath = _chunkWJJHVPLTjs.getThumbnailPath.call(void 0, key, "sm");
450
456
  if (isPushedToCloud && entry.c !== void 0) {
451
457
  const cdnUrl = cdnUrls[entry.c];
452
458
  if (cdnUrl) {
@@ -701,7 +707,7 @@ async function handleDelete(request) {
701
707
  for (const key of Object.keys(meta)) {
702
708
  if (key.startsWith(prefix) || key === imageKey) {
703
709
  if (!meta[key].c) {
704
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, key)) {
710
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, key)) {
705
711
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
706
712
  try {
707
713
  await _fs.promises.unlink(absoluteThumbPath);
@@ -717,7 +723,7 @@ async function handleDelete(request) {
717
723
  const isInImagesFolder = itemPath.startsWith("public/images/");
718
724
  if (!isInImagesFolder && entry) {
719
725
  if (!isPushedToCloud) {
720
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
726
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
721
727
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
722
728
  try {
723
729
  await _fs.promises.unlink(absoluteThumbPath);
@@ -829,8 +835,8 @@ async function handleRename(request) {
829
835
  const newKey = "/" + newRelativePath;
830
836
  if (meta[oldKey]) {
831
837
  const entry = meta[oldKey];
832
- const oldThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, oldKey);
833
- const newThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, newKey);
838
+ const oldThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, oldKey);
839
+ const newThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, newKey);
834
840
  for (let i = 0; i < oldThumbPaths.length; i++) {
835
841
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
836
842
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
@@ -982,8 +988,8 @@ async function handleMoveStream(request) {
982
988
  await _fs.promises.rename(absolutePath, newAbsolutePath);
983
989
  const stats = await _fs.promises.stat(newAbsolutePath);
984
990
  if (stats.isFile() && isImage && entry) {
985
- const oldThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, oldKey);
986
- const newThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, newKey);
991
+ const oldThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, oldKey);
992
+ const newThumbPaths = _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, newKey);
987
993
  for (let j = 0; j < oldThumbPaths.length; j++) {
988
994
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[j]);
989
995
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[j]);
@@ -1083,10 +1089,6 @@ async function handleSync(request) {
1083
1089
  continue;
1084
1090
  }
1085
1091
  const isRemote = entry.c !== void 0 && existingCdnUrl !== publicUrl;
1086
- if (!isRemote && !entry.p) {
1087
- errors.push(`Image not processed: ${imageKey}. Run Process Images first.`);
1088
- continue;
1089
- }
1090
1092
  try {
1091
1093
  let originalBuffer;
1092
1094
  if (isRemote) {
@@ -1111,7 +1113,7 @@ async function handleSync(request) {
1111
1113
  );
1112
1114
  urlsToPurge.push(`${publicUrl}${imageKey}`);
1113
1115
  if (!isRemote && entry.p) {
1114
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1116
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1115
1117
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1116
1118
  try {
1117
1119
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -1131,7 +1133,7 @@ async function handleSync(request) {
1131
1133
  entry.c = cdnIndex;
1132
1134
  if (!isRemote) {
1133
1135
  const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
1134
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1136
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1135
1137
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1136
1138
  try {
1137
1139
  await _fs.promises.unlink(localPath);
@@ -1206,7 +1208,7 @@ async function handleReprocess(request) {
1206
1208
  if (isInOurR2) {
1207
1209
  updatedEntry.c = existingCdnIndex;
1208
1210
  await uploadToCdn(imageKey);
1209
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1211
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1210
1212
  urlsToPurge.push(`${publicUrl}${thumbPath}`);
1211
1213
  }
1212
1214
  await deleteLocalThumbnails(imageKey);
@@ -1327,7 +1329,7 @@ async function handleProcessAllStream() {
1327
1329
  }
1328
1330
  if (isInOurR2) {
1329
1331
  await uploadToCdn(key);
1330
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, key)) {
1332
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, key)) {
1331
1333
  urlsToPurge.push(`${publicUrl}${thumbPath}`);
1332
1334
  }
1333
1335
  await deleteLocalThumbnails(key);
@@ -1346,7 +1348,7 @@ async function handleProcessAllStream() {
1346
1348
  const trackedPaths = /* @__PURE__ */ new Set();
1347
1349
  for (const [imageKey, entry] of getFileEntries(meta)) {
1348
1350
  if (entry.c === void 0) {
1349
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1351
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1350
1352
  trackedPaths.add(thumbPath);
1351
1353
  }
1352
1354
  }
@@ -1437,6 +1439,7 @@ async function handleProcessAllStream() {
1437
1439
 
1438
1440
 
1439
1441
 
1442
+
1440
1443
  async function handleScanStream() {
1441
1444
  const encoder = new TextEncoder();
1442
1445
  const stream = new ReadableStream({
@@ -1448,11 +1451,12 @@ async function handleScanStream() {
1448
1451
  };
1449
1452
  try {
1450
1453
  const meta = await loadMeta();
1451
- const existingCount = Object.keys(meta).length;
1454
+ const existingCount = Object.keys(meta).filter((k) => !k.startsWith("_")).length;
1452
1455
  const existingKeys = new Set(Object.keys(meta));
1453
1456
  const added = [];
1454
1457
  const renamed = [];
1455
1458
  const errors = [];
1459
+ const orphanedFiles = [];
1456
1460
  const allFiles = [];
1457
1461
  async function scanDir(dir, relativePath = "") {
1458
1462
  try {
@@ -1542,6 +1546,40 @@ async function handleScanStream() {
1542
1546
  errors.push(relativePath);
1543
1547
  }
1544
1548
  }
1549
+ sendEvent({ type: "cleanup", message: "Checking for orphaned thumbnails..." });
1550
+ const expectedThumbnails = /* @__PURE__ */ new Set();
1551
+ const fileEntries = getFileEntries(meta);
1552
+ for (const [imageKey, entry] of fileEntries) {
1553
+ if (entry.c === void 0 && entry.p === 1) {
1554
+ for (const thumbPath of _chunkWJJHVPLTjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1555
+ expectedThumbnails.add(thumbPath);
1556
+ }
1557
+ }
1558
+ }
1559
+ async function findOrphans(dir, relativePath = "") {
1560
+ try {
1561
+ const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
1562
+ for (const entry of entries) {
1563
+ if (entry.name.startsWith(".")) continue;
1564
+ const fullPath = _path2.default.join(dir, entry.name);
1565
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1566
+ if (entry.isDirectory()) {
1567
+ await findOrphans(fullPath, relPath);
1568
+ } else if (isImageFile(entry.name)) {
1569
+ const publicPath = `/images/${relPath}`;
1570
+ if (!expectedThumbnails.has(publicPath)) {
1571
+ orphanedFiles.push(publicPath);
1572
+ }
1573
+ }
1574
+ }
1575
+ } catch (e36) {
1576
+ }
1577
+ }
1578
+ const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1579
+ try {
1580
+ await findOrphans(imagesDir);
1581
+ } catch (e37) {
1582
+ }
1545
1583
  await saveMeta(meta);
1546
1584
  sendEvent({
1547
1585
  type: "complete",
@@ -1549,7 +1587,8 @@ async function handleScanStream() {
1549
1587
  added: added.length,
1550
1588
  renamed: renamed.length,
1551
1589
  errors: errors.length,
1552
- renamedFiles: renamed
1590
+ renamedFiles: renamed,
1591
+ orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0
1553
1592
  });
1554
1593
  } catch (error) {
1555
1594
  console.error("Scan failed:", error);
@@ -1567,6 +1606,63 @@ async function handleScanStream() {
1567
1606
  }
1568
1607
  });
1569
1608
  }
1609
+ async function handleDeleteOrphans(request) {
1610
+ try {
1611
+ const { paths } = await request.json();
1612
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
1613
+ return _server.NextResponse.json({ error: "No paths provided" }, { status: 400 });
1614
+ }
1615
+ const deleted = [];
1616
+ const errors = [];
1617
+ for (const orphanPath of paths) {
1618
+ if (!orphanPath.startsWith("/images/")) {
1619
+ errors.push(`Invalid path: ${orphanPath}`);
1620
+ continue;
1621
+ }
1622
+ const fullPath = _path2.default.join(process.cwd(), "public", orphanPath);
1623
+ try {
1624
+ await _fs.promises.unlink(fullPath);
1625
+ deleted.push(orphanPath);
1626
+ } catch (err) {
1627
+ console.error(`Failed to delete ${orphanPath}:`, err);
1628
+ errors.push(orphanPath);
1629
+ }
1630
+ }
1631
+ const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1632
+ async function removeEmptyDirs(dir) {
1633
+ try {
1634
+ const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
1635
+ let isEmpty = true;
1636
+ for (const entry of entries) {
1637
+ if (entry.isDirectory()) {
1638
+ const subDirEmpty = await removeEmptyDirs(_path2.default.join(dir, entry.name));
1639
+ if (!subDirEmpty) isEmpty = false;
1640
+ } else {
1641
+ isEmpty = false;
1642
+ }
1643
+ }
1644
+ if (isEmpty && dir !== imagesDir) {
1645
+ await _fs.promises.rmdir(dir);
1646
+ }
1647
+ return isEmpty;
1648
+ } catch (e38) {
1649
+ return true;
1650
+ }
1651
+ }
1652
+ try {
1653
+ await removeEmptyDirs(imagesDir);
1654
+ } catch (e39) {
1655
+ }
1656
+ return _server.NextResponse.json({
1657
+ success: true,
1658
+ deleted: deleted.length,
1659
+ errors: errors.length
1660
+ });
1661
+ } catch (error) {
1662
+ console.error("Failed to delete orphans:", error);
1663
+ return _server.NextResponse.json({ error: "Failed to delete orphaned files" }, { status: 500 });
1664
+ }
1665
+ }
1570
1666
 
1571
1667
  // src/handlers/import.ts
1572
1668
 
@@ -1754,6 +1850,9 @@ async function POST(request) {
1754
1850
  if (route === "scan") {
1755
1851
  return handleScanStream();
1756
1852
  }
1853
+ if (route === "delete-orphans") {
1854
+ return handleDeleteOrphans(request);
1855
+ }
1757
1856
  if (route === "import") {
1758
1857
  return handleImportUrls(request);
1759
1858
  }