@gallop.software/studio 0.1.93 → 0.1.95

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
- 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;
259
+ if (isImage && entry.p === 1) {
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;
@@ -246,7 +276,12 @@ async function handleList(request) {
246
276
  }
247
277
  }
248
278
  } else if (isImage) {
249
- thumbnail = key;
279
+ if (isPushedToCloud && entry.c !== void 0) {
280
+ const cdnUrl = cdnUrls[entry.c];
281
+ thumbnail = cdnUrl ? `${cdnUrl}${key}` : key;
282
+ } else {
283
+ thumbnail = key;
284
+ }
250
285
  hasThumbnail = false;
251
286
  }
252
287
  if (!isPushedToCloud) {
@@ -284,19 +319,21 @@ async function handleSearch(request) {
284
319
  }
285
320
  try {
286
321
  const meta = await loadMeta();
322
+ const fileEntries = getFileEntries(meta);
323
+ const cdnUrls = getCdnUrls(meta);
287
324
  const items = [];
288
- for (const [key, entry] of Object.entries(meta)) {
325
+ for (const [key, entry] of fileEntries) {
289
326
  if (!key.toLowerCase().includes(query)) continue;
290
327
  const fileName = _path2.default.basename(key);
291
328
  const relativePath = key.slice(1);
292
329
  const isImage = isImageFile(fileName);
293
- const isPushedToCloud = entry.c === 1;
330
+ const isPushedToCloud = entry.c !== void 0;
294
331
  let thumbnail;
295
332
  let hasThumbnail = false;
296
- 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;
333
+ if (isImage && entry.p === 1) {
334
+ const thumbPath = _chunkMCJNUXQ6js.getThumbnailPath.call(void 0, key, "sm");
335
+ if (isPushedToCloud && entry.c !== void 0) {
336
+ const cdnUrl = cdnUrls[entry.c];
300
337
  if (cdnUrl) {
301
338
  thumbnail = `${cdnUrl}${thumbPath}`;
302
339
  hasThumbnail = true;
@@ -313,7 +350,12 @@ async function handleSearch(request) {
313
350
  }
314
351
  }
315
352
  } else if (isImage) {
316
- thumbnail = key;
353
+ if (isPushedToCloud && entry.c !== void 0) {
354
+ const cdnUrl = cdnUrls[entry.c];
355
+ thumbnail = cdnUrl ? `${cdnUrl}${key}` : key;
356
+ } else {
357
+ thumbnail = key;
358
+ }
317
359
  hasThumbnail = false;
318
360
  }
319
361
  items.push({
@@ -336,8 +378,9 @@ async function handleSearch(request) {
336
378
  async function handleListFolders() {
337
379
  try {
338
380
  const meta = await loadMeta();
381
+ const fileEntries = getFileEntries(meta);
339
382
  const folderSet = /* @__PURE__ */ new Set();
340
- for (const key of Object.keys(meta)) {
383
+ for (const [key] of fileEntries) {
341
384
  const parts = key.split("/");
342
385
  let current = "";
343
386
  for (let i = 1; i < parts.length - 1; i++) {
@@ -366,8 +409,9 @@ async function handleListFolders() {
366
409
  async function handleCountImages() {
367
410
  try {
368
411
  const meta = await loadMeta();
412
+ const fileEntries = getFileEntries(meta);
369
413
  const allImages = [];
370
- for (const key of Object.keys(meta)) {
414
+ for (const [key] of fileEntries) {
371
415
  const fileName = _path2.default.basename(key);
372
416
  if (isImageFile(fileName)) {
373
417
  allImages.push(key.slice(1));
@@ -391,12 +435,13 @@ async function handleFolderImages(request) {
391
435
  }
392
436
  const folders = foldersParam.split(",");
393
437
  const meta = await loadMeta();
438
+ const fileEntries = getFileEntries(meta);
394
439
  const allImages = [];
395
440
  const prefixes = folders.map((f) => {
396
441
  const rel = f.replace(/^public\/?/, "");
397
442
  return rel ? `/${rel}/` : "/";
398
443
  });
399
- for (const key of Object.keys(meta)) {
444
+ for (const [key] of fileEntries) {
400
445
  const fileName = _path2.default.basename(key);
401
446
  if (!isImageFile(fileName)) continue;
402
447
  for (const prefix of prefixes) {
@@ -524,7 +569,7 @@ async function handleDelete(request) {
524
569
  for (const key of Object.keys(meta)) {
525
570
  if (key.startsWith(prefix) || key === imageKey) {
526
571
  if (!meta[key].c) {
527
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, key)) {
572
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, key)) {
528
573
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
529
574
  try {
530
575
  await _fs.promises.unlink(absoluteThumbPath);
@@ -540,7 +585,7 @@ async function handleDelete(request) {
540
585
  const isInImagesFolder = itemPath.startsWith("public/images/");
541
586
  if (!isInImagesFolder && entry) {
542
587
  if (!isPushedToCloud) {
543
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
588
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
544
589
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
545
590
  try {
546
591
  await _fs.promises.unlink(absoluteThumbPath);
@@ -652,8 +697,8 @@ async function handleRename(request) {
652
697
  const newKey = "/" + newRelativePath;
653
698
  if (meta[oldKey]) {
654
699
  const entry = meta[oldKey];
655
- const oldThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, oldKey);
656
- const newThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, newKey);
700
+ const oldThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, oldKey);
701
+ const newThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, newKey);
657
702
  for (let i = 0; i < oldThumbPaths.length; i++) {
658
703
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
659
704
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
@@ -732,8 +777,8 @@ async function handleMove(request) {
732
777
  const newKey = "/" + newRelativePath;
733
778
  if (meta[oldKey]) {
734
779
  const entry = meta[oldKey];
735
- const oldThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, oldKey);
736
- const newThumbPaths = _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, newKey);
780
+ const oldThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, oldKey);
781
+ const newThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, newKey);
737
782
  for (let i = 0; i < oldThumbPaths.length; i++) {
738
783
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
739
784
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
@@ -790,6 +835,7 @@ async function handleSync(request) {
790
835
  return _server.NextResponse.json({ error: "No image keys provided" }, { status: 400 });
791
836
  }
792
837
  const meta = await loadMeta();
838
+ const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
793
839
  const r2 = new (0, _clients3.S3Client)({
794
840
  region: "auto",
795
841
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
@@ -798,12 +844,12 @@ async function handleSync(request) {
798
844
  const pushed = [];
799
845
  const errors = [];
800
846
  for (const imageKey of imageKeys) {
801
- const entry = meta[imageKey];
847
+ const entry = getMetaEntry(meta, imageKey);
802
848
  if (!entry) {
803
849
  errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
804
850
  continue;
805
851
  }
806
- if (entry.c) {
852
+ if (entry.c !== void 0) {
807
853
  pushed.push(imageKey);
808
854
  continue;
809
855
  }
@@ -827,7 +873,7 @@ async function handleSync(request) {
827
873
  errors.push(`Original file not found: ${imageKey}`);
828
874
  continue;
829
875
  }
830
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
876
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
831
877
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
832
878
  try {
833
879
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -842,8 +888,8 @@ async function handleSync(request) {
842
888
  } catch (e20) {
843
889
  }
844
890
  }
845
- entry.c = 1;
846
- for (const thumbPath of _chunkL36EH3PMjs.getAllThumbnailPaths.call(void 0, imageKey)) {
891
+ entry.c = cdnIndex;
892
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
847
893
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
848
894
  try {
849
895
  await _fs.promises.unlink(localPath);
@@ -883,8 +929,9 @@ async function handleReprocess(request) {
883
929
  for (const imageKey of imageKeys) {
884
930
  try {
885
931
  let buffer;
886
- const entry = meta[imageKey];
887
- const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _6 => _6.c]) === 1;
932
+ const entry = getMetaEntry(meta, imageKey);
933
+ const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _6 => _6.c]) !== void 0;
934
+ const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _7 => _7.c]);
888
935
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
889
936
  try {
890
937
  buffer = await _fs.promises.readFile(originalPath);
@@ -900,7 +947,7 @@ async function handleReprocess(request) {
900
947
  }
901
948
  const updatedEntry = await processImage(buffer, imageKey);
902
949
  if (isPushedToCloud) {
903
- updatedEntry.c = 1;
950
+ updatedEntry.c = existingCdnIndex;
904
951
  await uploadToCdn(imageKey);
905
952
  await deleteLocalThumbnails(imageKey);
906
953
  try {
@@ -942,7 +989,7 @@ async function handleProcessAllStream() {
942
989
  const orphansRemoved = [];
943
990
  let alreadyProcessed = 0;
944
991
  const imagesToProcess = [];
945
- for (const [key, entry] of Object.entries(meta)) {
992
+ for (const [key, entry] of getFileEntries(meta)) {
946
993
  const fileName = _path2.default.basename(key);
947
994
  if (!isImageFile(fileName)) continue;
948
995
  if (!entry.p) {
@@ -956,7 +1003,8 @@ async function handleProcessAllStream() {
956
1003
  for (let i = 0; i < imagesToProcess.length; i++) {
957
1004
  const { key, entry } = imagesToProcess[i];
958
1005
  const fullPath = _path2.default.join(process.cwd(), "public", key);
959
- const isInCloud = entry.c === 1;
1006
+ const isInCloud = entry.c !== void 0;
1007
+ const existingCdnIndex = entry.c;
960
1008
  sendEvent({
961
1009
  type: "progress",
962
1010
  current: i + 1,
@@ -996,7 +1044,7 @@ async function handleProcessAllStream() {
996
1044
  meta[key] = {
997
1045
  ...processedEntry,
998
1046
  p: 1,
999
- ...isInCloud ? { c: 1 } : {}
1047
+ ...isInCloud ? { c: existingCdnIndex } : {}
1000
1048
  };
1001
1049
  }
1002
1050
  if (isInCloud) {
@@ -1015,9 +1063,9 @@ async function handleProcessAllStream() {
1015
1063
  }
1016
1064
  sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
1017
1065
  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)) {
1066
+ for (const [imageKey, entry] of getFileEntries(meta)) {
1067
+ if (entry.c === void 0) {
1068
+ for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
1021
1069
  trackedPaths.add(thumbPath);
1022
1070
  }
1023
1071
  }
@@ -1236,6 +1284,132 @@ async function handleScanStream() {
1236
1284
  });
1237
1285
  }
1238
1286
 
1287
+ // src/handlers/import.ts
1288
+
1289
+
1290
+ function parseImageUrl(url) {
1291
+ const parsed = new URL(url);
1292
+ const base = `${parsed.protocol}//${parsed.host}`;
1293
+ const path9 = parsed.pathname;
1294
+ return { base, path: path9 };
1295
+ }
1296
+ async function processRemoteImage(url) {
1297
+ const response = await fetch(url);
1298
+ if (!response.ok) {
1299
+ throw new Error(`Failed to fetch: ${response.status}`);
1300
+ }
1301
+ const buffer = Buffer.from(await response.arrayBuffer());
1302
+ const metadata = await _sharp2.default.call(void 0, buffer).metadata();
1303
+ const { data, info } = await _sharp2.default.call(void 0, buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1304
+ const blurhash = _blurhash.encode.call(void 0, new Uint8ClampedArray(data), info.width, info.height, 4, 4);
1305
+ return {
1306
+ w: metadata.width || 0,
1307
+ h: metadata.height || 0,
1308
+ b: blurhash
1309
+ };
1310
+ }
1311
+ async function handleImportUrls(request) {
1312
+ const encoder = new TextEncoder();
1313
+ const stream = new ReadableStream({
1314
+ async start(controller) {
1315
+ const sendEvent = (data) => {
1316
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
1317
+
1318
+ `));
1319
+ };
1320
+ try {
1321
+ const { urls } = await request.json();
1322
+ if (!urls || !Array.isArray(urls) || urls.length === 0) {
1323
+ sendEvent({ type: "error", message: "No URLs provided" });
1324
+ controller.close();
1325
+ return;
1326
+ }
1327
+ const meta = await loadMeta();
1328
+ const added = [];
1329
+ const skipped = [];
1330
+ const errors = [];
1331
+ const total = urls.length;
1332
+ sendEvent({ type: "start", total });
1333
+ for (let i = 0; i < urls.length; i++) {
1334
+ const url = urls[i].trim();
1335
+ if (!url) continue;
1336
+ sendEvent({
1337
+ type: "progress",
1338
+ current: i + 1,
1339
+ total,
1340
+ percent: Math.round((i + 1) / total * 100),
1341
+ currentFile: url
1342
+ });
1343
+ try {
1344
+ const { base, path: path9 } = parseImageUrl(url);
1345
+ const existingEntry = getMetaEntry(meta, path9);
1346
+ if (existingEntry) {
1347
+ skipped.push(path9);
1348
+ continue;
1349
+ }
1350
+ const cdnIndex = getOrAddCdnIndex(meta, base);
1351
+ const imageData = await processRemoteImage(url);
1352
+ setMetaEntry(meta, path9, {
1353
+ w: imageData.w,
1354
+ h: imageData.h,
1355
+ b: imageData.b,
1356
+ c: cdnIndex
1357
+ });
1358
+ added.push(path9);
1359
+ } catch (error) {
1360
+ console.error(`Failed to import ${url}:`, error);
1361
+ errors.push(url);
1362
+ }
1363
+ }
1364
+ await saveMeta(meta);
1365
+ sendEvent({
1366
+ type: "complete",
1367
+ added: added.length,
1368
+ skipped: skipped.length,
1369
+ errors: errors.length
1370
+ });
1371
+ } catch (error) {
1372
+ console.error("Import failed:", error);
1373
+ sendEvent({ type: "error", message: "Import failed" });
1374
+ } finally {
1375
+ controller.close();
1376
+ }
1377
+ }
1378
+ });
1379
+ return new Response(stream, {
1380
+ headers: {
1381
+ "Content-Type": "text/event-stream",
1382
+ "Cache-Control": "no-cache",
1383
+ "Connection": "keep-alive"
1384
+ }
1385
+ });
1386
+ }
1387
+ async function handleGetCdns() {
1388
+ try {
1389
+ const meta = await loadMeta();
1390
+ const cdns = meta._cdns || [];
1391
+ return Response.json({ cdns });
1392
+ } catch (error) {
1393
+ console.error("Failed to get CDNs:", error);
1394
+ return Response.json({ error: "Failed to get CDNs" }, { status: 500 });
1395
+ }
1396
+ }
1397
+ async function handleUpdateCdns(request) {
1398
+ try {
1399
+ const { cdns } = await request.json();
1400
+ if (!Array.isArray(cdns)) {
1401
+ return Response.json({ error: "Invalid CDN array" }, { status: 400 });
1402
+ }
1403
+ const meta = await loadMeta();
1404
+ meta._cdns = cdns.map((url) => url.replace(/\/$/, ""));
1405
+ await saveMeta(meta);
1406
+ return Response.json({ success: true, cdns: meta._cdns });
1407
+ } catch (error) {
1408
+ console.error("Failed to update CDNs:", error);
1409
+ return Response.json({ error: "Failed to update CDNs" }, { status: 500 });
1410
+ }
1411
+ }
1412
+
1239
1413
  // src/handlers/index.ts
1240
1414
  async function GET(request) {
1241
1415
  if (process.env.NODE_ENV !== "development") {
@@ -1258,6 +1432,9 @@ async function GET(request) {
1258
1432
  if (route === "search") {
1259
1433
  return handleSearch(request);
1260
1434
  }
1435
+ if (route === "cdns") {
1436
+ return handleGetCdns();
1437
+ }
1261
1438
  return _server.NextResponse.json({ error: "Not found" }, { status: 404 });
1262
1439
  }
1263
1440
  async function POST(request) {
@@ -1293,6 +1470,12 @@ async function POST(request) {
1293
1470
  if (route === "scan") {
1294
1471
  return handleScanStream();
1295
1472
  }
1473
+ if (route === "import") {
1474
+ return handleImportUrls(request);
1475
+ }
1476
+ if (route === "cdns") {
1477
+ return handleUpdateCdns(request);
1478
+ }
1296
1479
  return _server.NextResponse.json({ error: "Not found" }, { status: 404 });
1297
1480
  }
1298
1481
  async function DELETE(request) {