@gallop.software/studio 0.1.88 → 0.1.89

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.
@@ -1,6 +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
- var _chunkCN5NRNWBjs = require('../chunk-CN5NRNWB.js');
3
+
4
+ var _chunkHE2DOD2Kjs = require('../chunk-HE2DOD2K.js');
4
5
 
5
6
  // src/handlers/index.ts
6
7
  var _server = require('next/server');
@@ -9,7 +10,6 @@ var _server = require('next/server');
9
10
 
10
11
  var _fs = require('fs');
11
12
  var _path = require('path'); var _path2 = _interopRequireDefault(_path);
12
- var _sharp = require('sharp'); var _sharp2 = _interopRequireDefault(_sharp);
13
13
 
14
14
  // src/handlers/utils/meta.ts
15
15
 
@@ -32,7 +32,6 @@ async function saveMeta(meta) {
32
32
 
33
33
  // src/handlers/utils/files.ts
34
34
 
35
-
36
35
  function isImageFile(filename) {
37
36
  const ext = _path2.default.extname(filename).toLowerCase();
38
37
  return [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"].includes(ext);
@@ -63,34 +62,11 @@ function getContentType(filePath) {
63
62
  return "application/octet-stream";
64
63
  }
65
64
  }
66
- async function getFolderStats(folderPath) {
67
- let fileCount = 0;
68
- let totalSize = 0;
69
- async function scanFolder(dir) {
70
- try {
71
- const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
72
- for (const entry of entries) {
73
- if (entry.name.startsWith(".")) continue;
74
- const fullPath = _path2.default.join(dir, entry.name);
75
- if (entry.isDirectory()) {
76
- await scanFolder(fullPath);
77
- } else if (isMediaFile(entry.name)) {
78
- fileCount++;
79
- const stats = await _fs.promises.stat(fullPath);
80
- totalSize += stats.size;
81
- }
82
- }
83
- } catch (e2) {
84
- }
85
- }
86
- await scanFolder(folderPath);
87
- return { fileCount, totalSize };
88
- }
89
65
 
90
66
  // src/handlers/utils/thumbnails.ts
91
67
 
92
68
 
93
-
69
+ var _sharp = require('sharp'); var _sharp2 = _interopRequireDefault(_sharp);
94
70
  var _blurhash = require('blurhash');
95
71
  var DEFAULT_SIZES = {
96
72
  small: { width: 300, suffix: "-sm" },
@@ -180,7 +156,7 @@ async function uploadToCdn(imageKey) {
180
156
  const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
181
157
  if (!bucketName) throw new Error("R2 bucket not configured");
182
158
  const r2 = getR2Client();
183
- for (const thumbPath of _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, imageKey)) {
159
+ for (const thumbPath of _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, imageKey)) {
184
160
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
185
161
  try {
186
162
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -192,16 +168,16 @@ async function uploadToCdn(imageKey) {
192
168
  ContentType: getContentType(thumbPath)
193
169
  })
194
170
  );
195
- } catch (e3) {
171
+ } catch (e2) {
196
172
  }
197
173
  }
198
174
  }
199
175
  async function deleteLocalThumbnails(imageKey) {
200
- for (const thumbPath of _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, imageKey)) {
176
+ for (const thumbPath of _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, imageKey)) {
201
177
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
202
178
  try {
203
179
  await _fs.promises.unlink(localPath);
204
- } catch (e4) {
180
+ } catch (e3) {
205
181
  }
206
182
  }
207
183
  }
@@ -211,70 +187,85 @@ async function handleList(request) {
211
187
  const searchParams = request.nextUrl.searchParams;
212
188
  const requestedPath = searchParams.get("path") || "public";
213
189
  try {
214
- const safePath = requestedPath.replace(/\.\./g, "");
215
- const absolutePath = _path2.default.join(process.cwd(), safePath);
216
- if (!absolutePath.startsWith(process.cwd())) {
217
- return _server.NextResponse.json({ error: "Invalid path" }, { status: 400 });
190
+ const meta = await loadMeta();
191
+ const metaKeys = Object.keys(meta);
192
+ if (metaKeys.length === 0) {
193
+ return _server.NextResponse.json({ items: [], isEmpty: true });
218
194
  }
195
+ const relativePath = requestedPath.replace(/^public\/?/, "");
196
+ const pathPrefix = relativePath ? `/${relativePath}/` : "/";
219
197
  const items = [];
220
- const entries = await _fs.promises.readdir(absolutePath, { withFileTypes: true });
221
- for (const entry of entries) {
222
- if (entry.name.startsWith(".")) continue;
223
- const itemPath = _path2.default.join(safePath, entry.name);
224
- if (entry.isDirectory()) {
225
- const folderStats = await getFolderStats(_path2.default.join(absolutePath, entry.name));
226
- items.push({
227
- name: entry.name,
228
- path: itemPath,
229
- type: "folder",
230
- fileCount: folderStats.fileCount,
231
- totalSize: folderStats.totalSize
232
- });
233
- } else if (isMediaFile(entry.name)) {
234
- const filePath = _path2.default.join(absolutePath, entry.name);
235
- const stats = await _fs.promises.stat(filePath);
236
- const isImage = isImageFile(entry.name);
198
+ const seenFolders = /* @__PURE__ */ new Set();
199
+ for (const key of metaKeys) {
200
+ const entry = meta[key];
201
+ if (!key.startsWith(pathPrefix) && pathPrefix !== "/") continue;
202
+ if (pathPrefix === "/" && !key.startsWith("/")) continue;
203
+ const remaining = pathPrefix === "/" ? key.slice(1) : key.slice(pathPrefix.length);
204
+ if (!remaining) continue;
205
+ const slashIndex = remaining.indexOf("/");
206
+ if (slashIndex !== -1) {
207
+ const folderName = remaining.slice(0, slashIndex);
208
+ if (!seenFolders.has(folderName)) {
209
+ seenFolders.add(folderName);
210
+ const folderPrefix = pathPrefix === "/" ? `/${folderName}/` : `${pathPrefix}${folderName}/`;
211
+ let fileCount = 0;
212
+ for (const k of metaKeys) {
213
+ if (k.startsWith(folderPrefix)) fileCount++;
214
+ }
215
+ items.push({
216
+ name: folderName,
217
+ path: relativePath ? `public/${relativePath}/${folderName}` : `public/${folderName}`,
218
+ type: "folder",
219
+ fileCount
220
+ });
221
+ }
222
+ } else {
223
+ const fileName = remaining;
224
+ const isImage = isImageFile(fileName);
225
+ const isSynced = entry.s === 1;
237
226
  let thumbnail;
238
227
  let hasThumbnail = false;
239
- let dimensions;
240
- if (isImage) {
241
- const relativePath = safePath.replace(/^public\/?/, "");
242
- if (relativePath === "images" || relativePath.startsWith("images/")) {
243
- thumbnail = itemPath.replace("public", "");
244
- hasThumbnail = true;
228
+ let fileSize;
229
+ if (isImage && (entry.w || entry.blur)) {
230
+ const thumbPath = _chunkHE2DOD2Kjs.getThumbnailPath.call(void 0, key, "sm");
231
+ if (isSynced) {
232
+ const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL;
233
+ if (cdnUrl) {
234
+ thumbnail = `${cdnUrl}${thumbPath}`;
235
+ hasThumbnail = true;
236
+ }
245
237
  } else {
246
- const ext = _path2.default.extname(entry.name).toLowerCase();
247
- const baseName = _path2.default.basename(entry.name, ext);
248
- const thumbnailDir = relativePath ? `images/${relativePath}` : "images";
249
- const thumbnailName = `${baseName}-sm${ext === ".png" ? ".png" : ".jpg"}`;
250
- const thumbnailPath = _path2.default.join(process.cwd(), "public", thumbnailDir, thumbnailName);
238
+ const localThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
251
239
  try {
252
- await _fs.promises.access(thumbnailPath);
253
- thumbnail = `/${thumbnailDir}/${thumbnailName}`;
240
+ await _fs.promises.access(localThumbPath);
241
+ thumbnail = thumbPath;
254
242
  hasThumbnail = true;
255
- } catch (e5) {
256
- thumbnail = itemPath.replace("public", "");
243
+ } catch (e4) {
244
+ thumbnail = key;
257
245
  hasThumbnail = false;
258
246
  }
259
247
  }
260
- if (!entry.name.toLowerCase().endsWith(".svg")) {
261
- try {
262
- const metadata = await _sharp2.default.call(void 0, filePath).metadata();
263
- if (metadata.width && metadata.height) {
264
- dimensions = { width: metadata.width, height: metadata.height };
265
- }
266
- } catch (e6) {
267
- }
248
+ } else if (isImage) {
249
+ thumbnail = key;
250
+ hasThumbnail = false;
251
+ }
252
+ if (!isSynced) {
253
+ try {
254
+ const filePath = _path2.default.join(process.cwd(), "public", key);
255
+ const stats = await _fs.promises.stat(filePath);
256
+ fileSize = stats.size;
257
+ } catch (e5) {
268
258
  }
269
259
  }
270
260
  items.push({
271
- name: entry.name,
272
- path: itemPath,
261
+ name: fileName,
262
+ path: relativePath ? `public/${relativePath}/${fileName}` : `public/${fileName}`,
273
263
  type: "file",
274
- size: stats.size,
264
+ size: fileSize,
275
265
  thumbnail,
276
266
  hasThumbnail,
277
- dimensions
267
+ cdnSynced: isSynced,
268
+ dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
278
269
  });
279
270
  }
280
271
  }
@@ -291,62 +282,49 @@ async function handleSearch(request) {
291
282
  return _server.NextResponse.json({ items: [] });
292
283
  }
293
284
  try {
285
+ const meta = await loadMeta();
294
286
  const items = [];
295
- const publicDir = _path2.default.join(process.cwd(), "public");
296
- async function searchDir(dir, relativePath) {
297
- try {
298
- const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
299
- for (const entry of entries) {
300
- if (entry.name.startsWith(".")) continue;
301
- const fullPath = _path2.default.join(dir, entry.name);
302
- const itemPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
303
- const itemRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
304
- if (entry.isDirectory()) {
305
- await searchDir(fullPath, itemRelPath);
306
- } else if (isImageFile(entry.name)) {
307
- if (itemPath.toLowerCase().includes(query)) {
308
- const stats = await _fs.promises.stat(fullPath);
309
- let thumbnail;
310
- let hasThumbnail = false;
311
- let dimensions;
312
- const ext = _path2.default.extname(entry.name).toLowerCase();
313
- const baseName = _path2.default.basename(entry.name, ext);
314
- const thumbnailDir = relativePath ? `images/${relativePath}` : "images";
315
- const thumbnailName = `${baseName}-sm${ext === ".png" ? ".png" : ".jpg"}`;
316
- const thumbnailPath = _path2.default.join(process.cwd(), "public", thumbnailDir, thumbnailName);
317
- try {
318
- await _fs.promises.access(thumbnailPath);
319
- thumbnail = `/${thumbnailDir}/${thumbnailName}`;
320
- hasThumbnail = true;
321
- } catch (e7) {
322
- thumbnail = `/${itemRelPath}`;
323
- hasThumbnail = false;
324
- }
325
- if (!entry.name.toLowerCase().endsWith(".svg")) {
326
- try {
327
- const metadata = await _sharp2.default.call(void 0, fullPath).metadata();
328
- if (metadata.width && metadata.height) {
329
- dimensions = { width: metadata.width, height: metadata.height };
330
- }
331
- } catch (e8) {
332
- }
333
- }
334
- items.push({
335
- name: entry.name,
336
- path: itemPath,
337
- type: "file",
338
- size: stats.size,
339
- thumbnail,
340
- hasThumbnail,
341
- dimensions
342
- });
343
- }
287
+ for (const [key, entry] of Object.entries(meta)) {
288
+ if (!key.toLowerCase().includes(query)) continue;
289
+ const fileName = _path2.default.basename(key);
290
+ const relativePath = key.slice(1);
291
+ const isImage = isImageFile(fileName);
292
+ const isSynced = entry.s === 1;
293
+ let thumbnail;
294
+ let hasThumbnail = false;
295
+ if (isImage && (entry.w || entry.blur)) {
296
+ const thumbPath = _chunkHE2DOD2Kjs.getThumbnailPath.call(void 0, key, "sm");
297
+ if (isSynced) {
298
+ const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL;
299
+ if (cdnUrl) {
300
+ thumbnail = `${cdnUrl}${thumbPath}`;
301
+ hasThumbnail = true;
302
+ }
303
+ } else {
304
+ const localThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
305
+ try {
306
+ await _fs.promises.access(localThumbPath);
307
+ thumbnail = thumbPath;
308
+ hasThumbnail = true;
309
+ } catch (e6) {
310
+ thumbnail = key;
311
+ hasThumbnail = false;
344
312
  }
345
313
  }
346
- } catch (e9) {
314
+ } else if (isImage) {
315
+ thumbnail = key;
316
+ hasThumbnail = false;
347
317
  }
318
+ items.push({
319
+ name: fileName,
320
+ path: `public/${relativePath}`,
321
+ type: "file",
322
+ thumbnail,
323
+ hasThumbnail,
324
+ cdnSynced: isSynced,
325
+ dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
326
+ });
348
327
  }
349
- await searchDir(publicDir, "");
350
328
  return _server.NextResponse.json({ items });
351
329
  } catch (error) {
352
330
  console.error("Failed to search:", error);
@@ -355,27 +333,28 @@ async function handleSearch(request) {
355
333
  }
356
334
  async function handleListFolders() {
357
335
  try {
358
- const publicDir = _path2.default.join(process.cwd(), "public");
359
- const folders = [];
360
- async function scanDir(dir, relativePath, depth) {
361
- try {
362
- const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
363
- for (const entry of entries) {
364
- if (!entry.isDirectory()) continue;
365
- if (entry.name.startsWith(".")) continue;
366
- const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
367
- folders.push({
368
- path: `public/${folderRelativePath}`,
369
- name: entry.name,
370
- depth
371
- });
372
- await scanDir(_path2.default.join(dir, entry.name), folderRelativePath, depth + 1);
373
- }
374
- } catch (e10) {
336
+ const meta = await loadMeta();
337
+ const folderSet = /* @__PURE__ */ new Set();
338
+ for (const key of Object.keys(meta)) {
339
+ const parts = key.split("/");
340
+ let current = "";
341
+ for (let i = 1; i < parts.length - 1; i++) {
342
+ current = current ? `${current}/${parts[i]}` : parts[i];
343
+ folderSet.add(current);
375
344
  }
376
345
  }
346
+ const folders = [];
377
347
  folders.push({ path: "public", name: "public", depth: 0 });
378
- await scanDir(publicDir, "", 1);
348
+ const sortedFolders = Array.from(folderSet).sort();
349
+ for (const folderPath of sortedFolders) {
350
+ const depth = folderPath.split("/").length;
351
+ const name = folderPath.split("/").pop() || folderPath;
352
+ folders.push({
353
+ path: `public/${folderPath}`,
354
+ name,
355
+ depth
356
+ });
357
+ }
379
358
  return _server.NextResponse.json({ folders });
380
359
  } catch (error) {
381
360
  console.error("Failed to list folders:", error);
@@ -384,26 +363,14 @@ async function handleListFolders() {
384
363
  }
385
364
  async function handleCountImages() {
386
365
  try {
366
+ const meta = await loadMeta();
387
367
  const allImages = [];
388
- async function scanPublicFolder(dir, relativePath = "") {
389
- try {
390
- const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
391
- for (const entry of entries) {
392
- if (entry.name.startsWith(".")) continue;
393
- const fullPath = _path2.default.join(dir, entry.name);
394
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
395
- if (relPath === "images" || relPath.startsWith("images/")) continue;
396
- if (entry.isDirectory()) {
397
- await scanPublicFolder(fullPath, relPath);
398
- } else if (isImageFile(entry.name)) {
399
- allImages.push(relPath);
400
- }
401
- }
402
- } catch (e11) {
368
+ for (const key of Object.keys(meta)) {
369
+ const fileName = _path2.default.basename(key);
370
+ if (isImageFile(fileName)) {
371
+ allImages.push(key.slice(1));
403
372
  }
404
373
  }
405
- const publicDir = _path2.default.join(process.cwd(), "public");
406
- await scanPublicFolder(publicDir);
407
374
  return _server.NextResponse.json({
408
375
  count: allImages.length,
409
376
  images: allImages
@@ -421,29 +388,22 @@ async function handleFolderImages(request) {
421
388
  return _server.NextResponse.json({ error: "No folders provided" }, { status: 400 });
422
389
  }
423
390
  const folders = foldersParam.split(",");
391
+ const meta = await loadMeta();
424
392
  const allImages = [];
425
- async function scanFolder(dir, relativePath = "") {
426
- try {
427
- const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
428
- for (const entry of entries) {
429
- if (entry.name.startsWith(".")) continue;
430
- const fullPath = _path2.default.join(dir, entry.name);
431
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
432
- if (entry.isDirectory()) {
433
- await scanFolder(fullPath, relPath);
434
- } else if (isImageFile(entry.name)) {
435
- allImages.push(relPath);
436
- }
393
+ const prefixes = folders.map((f) => {
394
+ const rel = f.replace(/^public\/?/, "");
395
+ return rel ? `/${rel}/` : "/";
396
+ });
397
+ for (const key of Object.keys(meta)) {
398
+ const fileName = _path2.default.basename(key);
399
+ if (!isImageFile(fileName)) continue;
400
+ for (const prefix of prefixes) {
401
+ if (key.startsWith(prefix) || prefix === "/" && key.startsWith("/")) {
402
+ allImages.push(key.slice(1));
403
+ break;
437
404
  }
438
- } catch (e12) {
439
405
  }
440
406
  }
441
- for (const folder of folders) {
442
- const relativePath = folder.replace(/^public\/?/, "");
443
- if (relativePath === "images" || relativePath.startsWith("images/")) continue;
444
- const folderPath = _path2.default.join(process.cwd(), folder);
445
- await scanFolder(folderPath, relativePath);
446
- }
447
407
  return _server.NextResponse.json({
448
408
  count: allImages.length,
449
409
  images: allImages
@@ -459,7 +419,6 @@ async function handleFolderImages(request) {
459
419
 
460
420
 
461
421
 
462
-
463
422
  async function handleUpload(request) {
464
423
  try {
465
424
  const formData = await request.formData();
@@ -471,11 +430,9 @@ async function handleUpload(request) {
471
430
  const bytes = await file.arrayBuffer();
472
431
  const buffer = Buffer.from(bytes);
473
432
  const fileName = file.name;
474
- const baseName = _path2.default.basename(fileName, _path2.default.extname(fileName));
475
433
  const ext = _path2.default.extname(fileName).toLowerCase();
476
434
  const isImage = isImageFile(fileName);
477
- const isSvg = ext === ".svg";
478
- const isProcessableImage = isImage && !isSvg;
435
+ const isMedia = isMediaFile(fileName);
479
436
  const meta = await loadMeta();
480
437
  let relativeDir = "";
481
438
  if (targetPath === "public") {
@@ -489,71 +446,49 @@ async function handleUpload(request) {
489
446
  { status: 400 }
490
447
  );
491
448
  }
449
+ let imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
450
+ if (meta[imageKey]) {
451
+ const baseName = _path2.default.basename(fileName, ext);
452
+ let counter = 1;
453
+ let newFileName = `${baseName}-${counter}${ext}`;
454
+ let newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
455
+ while (meta[newKey]) {
456
+ counter++;
457
+ newFileName = `${baseName}-${counter}${ext}`;
458
+ newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
459
+ }
460
+ imageKey = newKey;
461
+ }
462
+ const actualFileName = _path2.default.basename(imageKey);
492
463
  const uploadDir = _path2.default.join(process.cwd(), "public", relativeDir);
493
464
  await _fs.promises.mkdir(uploadDir, { recursive: true });
494
- await _fs.promises.writeFile(_path2.default.join(uploadDir, fileName), buffer);
495
- if (!isImage) {
465
+ await _fs.promises.writeFile(_path2.default.join(uploadDir, actualFileName), buffer);
466
+ if (!isMedia) {
496
467
  return _server.NextResponse.json({
497
468
  success: true,
498
- message: "File uploaded successfully (non-image, no thumbnails generated)",
499
- path: `public/${relativeDir ? relativeDir + "/" : ""}${fileName}`
469
+ message: "File uploaded (not a media file)",
470
+ path: `public/${relativeDir ? relativeDir + "/" : ""}${actualFileName}`
500
471
  });
501
472
  }
502
- const imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
503
- if (meta[imageKey]) {
504
- return _server.NextResponse.json(
505
- { error: `File '${imageKey}' already exists in meta` },
506
- { status: 409 }
507
- );
508
- }
509
- const imagesPath = _path2.default.join(process.cwd(), "public", "images", relativeDir);
510
- await _fs.promises.mkdir(imagesPath, { recursive: true });
511
- let originalWidth = 0;
512
- let originalHeight = 0;
513
- let blurhash = "";
514
- const originalPath = `/${relativeDir ? relativeDir + "/" : ""}${fileName}`;
515
- if (isSvg) {
516
- const fullPath = _path2.default.join(imagesPath, fileName);
517
- await _fs.promises.writeFile(fullPath, buffer);
518
- } else if (isProcessableImage) {
519
- const sharpInstance = _sharp2.default.call(void 0, buffer);
520
- const metadata = await sharpInstance.metadata();
521
- originalWidth = metadata.width || 0;
522
- originalHeight = metadata.height || 0;
523
- const outputExt = ext === ".png" ? ".png" : ".jpg";
524
- const fullFileName = `${baseName}${outputExt}`;
525
- const fullPath = _path2.default.join(imagesPath, fullFileName);
526
- if (ext === ".png") {
527
- await _sharp2.default.call(void 0, buffer).png({ quality: 85 }).toFile(fullPath);
528
- } else {
529
- await _sharp2.default.call(void 0, buffer).jpeg({ quality: 85 }).toFile(fullPath);
530
- }
531
- for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
532
- const { width: maxWidth, suffix } = sizeConfig;
533
- if (originalWidth <= maxWidth) {
534
- continue;
535
- }
536
- const ratio = originalHeight / originalWidth;
537
- const newHeight = Math.round(maxWidth * ratio);
538
- const sizeFileName = `${baseName}${suffix}${outputExt}`;
539
- const sizePath = _path2.default.join(imagesPath, sizeFileName);
540
- if (ext === ".png") {
541
- await _sharp2.default.call(void 0, buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
542
- } else {
543
- await _sharp2.default.call(void 0, buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
544
- }
473
+ if (isImage && ext !== ".svg") {
474
+ try {
475
+ const metadata = await _sharp2.default.call(void 0, buffer).metadata();
476
+ meta[imageKey] = {
477
+ w: metadata.width || 0,
478
+ h: metadata.height || 0
479
+ };
480
+ } catch (e7) {
481
+ meta[imageKey] = { w: 0, h: 0 };
545
482
  }
546
- const { data, info } = await _sharp2.default.call(void 0, buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
547
- blurhash = _blurhash.encode.call(void 0, new Uint8ClampedArray(data), info.width, info.height, 4, 4);
483
+ } else {
484
+ meta[imageKey] = {};
548
485
  }
549
- const entry = {
550
- w: originalWidth,
551
- h: originalHeight,
552
- blur: blurhash
553
- };
554
- meta[originalPath] = entry;
555
486
  await saveMeta(meta);
556
- return _server.NextResponse.json({ success: true, imageKey: originalPath, entry });
487
+ return _server.NextResponse.json({
488
+ success: true,
489
+ imageKey,
490
+ message: 'File uploaded. Run "Process Images" to generate thumbnails.'
491
+ });
557
492
  } catch (error) {
558
493
  console.error("Failed to upload:", error);
559
494
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -576,31 +511,61 @@ async function handleDelete(request) {
576
511
  continue;
577
512
  }
578
513
  const absolutePath = _path2.default.join(process.cwd(), itemPath);
579
- const stats = await _fs.promises.stat(absolutePath);
580
- if (stats.isDirectory()) {
581
- await _fs.promises.rm(absolutePath, { recursive: true });
582
- const prefix = "/" + itemPath.replace(/^public\/images\/?/, "").replace(/^public\/?/, "");
583
- for (const key of Object.keys(meta)) {
584
- if (key.startsWith(prefix)) {
585
- delete meta[key];
514
+ const imageKey = "/" + itemPath.replace(/^public\//, "");
515
+ const entry = meta[imageKey];
516
+ const isSynced = _optionalChain([entry, 'optionalAccess', _5 => _5.s]) === 1;
517
+ try {
518
+ const stats = await _fs.promises.stat(absolutePath);
519
+ if (stats.isDirectory()) {
520
+ await _fs.promises.rm(absolutePath, { recursive: true });
521
+ const prefix = imageKey + "/";
522
+ for (const key of Object.keys(meta)) {
523
+ if (key.startsWith(prefix) || key === imageKey) {
524
+ if (!meta[key].s) {
525
+ for (const thumbPath of _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, key)) {
526
+ const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
527
+ try {
528
+ await _fs.promises.unlink(absoluteThumbPath);
529
+ } catch (e8) {
530
+ }
531
+ }
532
+ }
533
+ delete meta[key];
534
+ }
586
535
  }
587
- }
588
- } else {
589
- await _fs.promises.unlink(absolutePath);
590
- const isInImagesFolder = itemPath.startsWith("public/images/");
591
- if (!isInImagesFolder) {
592
- const imageKey = "/" + itemPath.replace(/^public\//, "");
593
- if (meta[imageKey]) {
594
- for (const thumbPath of _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, imageKey)) {
595
- const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
596
- try {
597
- await _fs.promises.unlink(absoluteThumbPath);
598
- } catch (e13) {
536
+ } else {
537
+ await _fs.promises.unlink(absolutePath);
538
+ const isInImagesFolder = itemPath.startsWith("public/images/");
539
+ if (!isInImagesFolder && entry) {
540
+ if (!isSynced) {
541
+ for (const thumbPath of _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, imageKey)) {
542
+ const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
543
+ try {
544
+ await _fs.promises.unlink(absoluteThumbPath);
545
+ } catch (e9) {
546
+ }
599
547
  }
600
548
  }
601
549
  delete meta[imageKey];
602
550
  }
603
551
  }
552
+ } catch (e10) {
553
+ if (entry) {
554
+ delete meta[imageKey];
555
+ } else {
556
+ const prefix = imageKey + "/";
557
+ let foundAny = false;
558
+ for (const key of Object.keys(meta)) {
559
+ if (key.startsWith(prefix)) {
560
+ delete meta[key];
561
+ foundAny = true;
562
+ }
563
+ }
564
+ if (!foundAny) {
565
+ errors.push(`Not found: ${itemPath}`);
566
+ continue;
567
+ }
568
+ }
604
569
  }
605
570
  deleted.push(itemPath);
606
571
  } catch (error) {
@@ -637,7 +602,7 @@ async function handleCreateFolder(request) {
637
602
  try {
638
603
  await _fs.promises.access(folderPath);
639
604
  return _server.NextResponse.json({ error: "A folder with this name already exists" }, { status: 400 });
640
- } catch (e14) {
605
+ } catch (e11) {
641
606
  }
642
607
  await _fs.promises.mkdir(folderPath, { recursive: true });
643
608
  return _server.NextResponse.json({ success: true, path: _path2.default.join(safePath, sanitizedName) });
@@ -665,13 +630,13 @@ async function handleRename(request) {
665
630
  }
666
631
  try {
667
632
  await _fs.promises.access(absoluteOldPath);
668
- } catch (e15) {
633
+ } catch (e12) {
669
634
  return _server.NextResponse.json({ error: "File or folder not found" }, { status: 404 });
670
635
  }
671
636
  try {
672
637
  await _fs.promises.access(absoluteNewPath);
673
638
  return _server.NextResponse.json({ error: "An item with this name already exists" }, { status: 400 });
674
- } catch (e16) {
639
+ } catch (e13) {
675
640
  }
676
641
  const stats = await _fs.promises.stat(absoluteOldPath);
677
642
  const isFile = stats.isFile();
@@ -685,15 +650,15 @@ async function handleRename(request) {
685
650
  const newKey = "/" + newRelativePath;
686
651
  if (meta[oldKey]) {
687
652
  const entry = meta[oldKey];
688
- const oldThumbPaths = _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, oldKey);
689
- const newThumbPaths = _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, newKey);
653
+ const oldThumbPaths = _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, oldKey);
654
+ const newThumbPaths = _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, newKey);
690
655
  for (let i = 0; i < oldThumbPaths.length; i++) {
691
656
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
692
657
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
693
658
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
694
659
  try {
695
660
  await _fs.promises.rename(oldThumbPath, newThumbPath);
696
- } catch (e17) {
661
+ } catch (e14) {
697
662
  }
698
663
  }
699
664
  delete meta[oldKey];
@@ -727,7 +692,7 @@ async function handleMove(request) {
727
692
  if (!destStats.isDirectory()) {
728
693
  return _server.NextResponse.json({ error: "Destination is not a folder" }, { status: 400 });
729
694
  }
730
- } catch (e18) {
695
+ } catch (e15) {
731
696
  return _server.NextResponse.json({ error: "Destination folder not found" }, { status: 404 });
732
697
  }
733
698
  const moved = [];
@@ -745,7 +710,7 @@ async function handleMove(request) {
745
710
  }
746
711
  try {
747
712
  await _fs.promises.access(absolutePath);
748
- } catch (e19) {
713
+ } catch (e16) {
749
714
  errors.push(`${itemName} not found`);
750
715
  continue;
751
716
  }
@@ -753,7 +718,7 @@ async function handleMove(request) {
753
718
  await _fs.promises.access(newAbsolutePath);
754
719
  errors.push(`${itemName} already exists in destination`);
755
720
  continue;
756
- } catch (e20) {
721
+ } catch (e17) {
757
722
  }
758
723
  try {
759
724
  await _fs.promises.rename(absolutePath, newAbsolutePath);
@@ -765,15 +730,15 @@ async function handleMove(request) {
765
730
  const newKey = "/" + newRelativePath;
766
731
  if (meta[oldKey]) {
767
732
  const entry = meta[oldKey];
768
- const oldThumbPaths = _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, oldKey);
769
- const newThumbPaths = _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, newKey);
733
+ const oldThumbPaths = _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, oldKey);
734
+ const newThumbPaths = _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, newKey);
770
735
  for (let i = 0; i < oldThumbPaths.length; i++) {
771
736
  const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
772
737
  const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
773
738
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
774
739
  try {
775
740
  await _fs.promises.rename(oldThumbPath, newThumbPath);
776
- } catch (e21) {
741
+ } catch (e18) {
777
742
  }
778
743
  }
779
744
  delete meta[oldKey];
@@ -782,7 +747,7 @@ async function handleMove(request) {
782
747
  }
783
748
  }
784
749
  moved.push(itemPath);
785
- } catch (e22) {
750
+ } catch (e19) {
786
751
  errors.push(`Failed to move ${itemName}`);
787
752
  }
788
753
  }
@@ -833,7 +798,7 @@ async function handleSync(request) {
833
798
  for (const imageKey of imageKeys) {
834
799
  const entry = meta[imageKey];
835
800
  if (!entry) {
836
- errors.push(`Image not found in meta: ${imageKey}`);
801
+ errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
837
802
  continue;
838
803
  }
839
804
  if (entry.s) {
@@ -841,7 +806,22 @@ async function handleSync(request) {
841
806
  continue;
842
807
  }
843
808
  try {
844
- for (const thumbPath of _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, imageKey)) {
809
+ const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
810
+ try {
811
+ const originalBuffer = await _fs.promises.readFile(originalLocalPath);
812
+ await r2.send(
813
+ new (0, _clients3.PutObjectCommand)({
814
+ Bucket: bucketName,
815
+ Key: imageKey.replace(/^\//, ""),
816
+ Body: originalBuffer,
817
+ ContentType: getContentType(imageKey)
818
+ })
819
+ );
820
+ } catch (err) {
821
+ errors.push(`Original file not found: ${imageKey}`);
822
+ continue;
823
+ }
824
+ for (const thumbPath of _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, imageKey)) {
845
825
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
846
826
  try {
847
827
  const fileBuffer = await _fs.promises.readFile(localPath);
@@ -853,21 +833,25 @@ async function handleSync(request) {
853
833
  ContentType: getContentType(thumbPath)
854
834
  })
855
835
  );
856
- } catch (e23) {
836
+ } catch (e20) {
857
837
  }
858
838
  }
859
839
  entry.s = 1;
860
- for (const thumbPath of _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, imageKey)) {
840
+ for (const thumbPath of _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, imageKey)) {
861
841
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
862
842
  try {
863
843
  await _fs.promises.unlink(localPath);
864
- } catch (e24) {
844
+ } catch (e21) {
865
845
  }
866
846
  }
847
+ try {
848
+ await _fs.promises.unlink(originalLocalPath);
849
+ } catch (e22) {
850
+ }
867
851
  synced.push(imageKey);
868
852
  } catch (error) {
869
853
  console.error(`Failed to sync ${imageKey}:`, error);
870
- errors.push(imageKey);
854
+ errors.push(`Failed to sync: ${imageKey}`);
871
855
  }
872
856
  }
873
857
  await saveMeta(meta);
@@ -894,21 +878,29 @@ async function handleReprocess(request) {
894
878
  try {
895
879
  let buffer;
896
880
  const entry = meta[imageKey];
881
+ const isSynced = _optionalChain([entry, 'optionalAccess', _6 => _6.s]) === 1;
897
882
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
898
883
  try {
899
884
  buffer = await _fs.promises.readFile(originalPath);
900
- } catch (e25) {
901
- if (_optionalChain([entry, 'optionalAccess', _5 => _5.s])) {
885
+ } catch (e23) {
886
+ if (isSynced) {
902
887
  buffer = await downloadFromCdn(imageKey);
888
+ const dir = _path2.default.dirname(originalPath);
889
+ await _fs.promises.mkdir(dir, { recursive: true });
890
+ await _fs.promises.writeFile(originalPath, buffer);
903
891
  } else {
904
892
  throw new Error(`File not found: ${imageKey}`);
905
893
  }
906
894
  }
907
895
  const updatedEntry = await processImage(buffer, imageKey);
908
- if (_optionalChain([entry, 'optionalAccess', _6 => _6.s])) {
896
+ if (isSynced) {
909
897
  updatedEntry.s = 1;
910
898
  await uploadToCdn(imageKey);
911
899
  await deleteLocalThumbnails(imageKey);
900
+ try {
901
+ await _fs.promises.unlink(originalPath);
902
+ } catch (e24) {
903
+ }
912
904
  }
913
905
  meta[imageKey] = updatedEntry;
914
906
  processed.push(imageKey);
@@ -942,73 +934,61 @@ async function handleProcessAllStream() {
942
934
  const processed = [];
943
935
  const errors = [];
944
936
  const orphansRemoved = [];
945
- const allImages = [];
946
- async function scanPublicFolder(dir, relativePath = "") {
947
- try {
948
- const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
949
- for (const entry of entries) {
950
- if (entry.name.startsWith(".")) continue;
951
- const fullPath = _path2.default.join(dir, entry.name);
952
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
953
- if (relPath === "images" || relPath.startsWith("images/")) continue;
954
- if (entry.isDirectory()) {
955
- await scanPublicFolder(fullPath, relPath);
956
- } else if (isImageFile(entry.name)) {
957
- allImages.push({ key: relPath, fullPath });
958
- }
959
- }
960
- } catch (e26) {
937
+ const imagesToProcess = [];
938
+ for (const [key, entry] of Object.entries(meta)) {
939
+ if (entry.s) continue;
940
+ const fileName = _path2.default.basename(key);
941
+ if (!isImageFile(fileName)) continue;
942
+ if (!entry.blur) {
943
+ imagesToProcess.push({ key, entry });
961
944
  }
962
945
  }
963
- const publicDir = _path2.default.join(process.cwd(), "public");
964
- await scanPublicFolder(publicDir);
965
- const total = allImages.length;
946
+ const total = imagesToProcess.length;
966
947
  sendEvent({ type: "start", total });
967
- for (let i = 0; i < allImages.length; i++) {
968
- const { key, fullPath } = allImages[i];
969
- const imageKey = "/" + key;
948
+ for (let i = 0; i < imagesToProcess.length; i++) {
949
+ const { key } = imagesToProcess[i];
950
+ const fullPath = _path2.default.join(process.cwd(), "public", key);
970
951
  sendEvent({
971
952
  type: "progress",
972
953
  current: i + 1,
973
954
  total,
974
955
  percent: Math.round((i + 1) / total * 100),
975
- currentFile: key
956
+ currentFile: key.slice(1)
957
+ // Remove leading /
976
958
  });
977
959
  try {
978
960
  const buffer = await _fs.promises.readFile(fullPath);
979
961
  const ext = _path2.default.extname(key).toLowerCase();
980
962
  const isSvg = ext === ".svg";
981
963
  if (isSvg) {
982
- const imageDir = _path2.default.dirname(key);
964
+ const imageDir = _path2.default.dirname(key.slice(1));
983
965
  const imagesPath = _path2.default.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
984
966
  await _fs.promises.mkdir(imagesPath, { recursive: true });
985
967
  const fileName = _path2.default.basename(key);
986
968
  const destPath = _path2.default.join(imagesPath, fileName);
987
969
  await _fs.promises.writeFile(destPath, buffer);
988
- meta[imageKey] = {
970
+ meta[key] = {
989
971
  w: 0,
990
972
  h: 0,
991
973
  blur: ""
992
974
  };
993
975
  } else {
994
- const existingEntry = meta[imageKey];
995
- const processedEntry = await processImage(buffer, imageKey);
996
- if (_optionalChain([existingEntry, 'optionalAccess', _7 => _7.s])) {
997
- processedEntry.s = 1;
998
- }
999
- meta[imageKey] = processedEntry;
976
+ const processedEntry = await processImage(buffer, key);
977
+ meta[key] = processedEntry;
1000
978
  }
1001
- processed.push(key);
979
+ processed.push(key.slice(1));
1002
980
  } catch (error) {
1003
981
  console.error(`Failed to process ${key}:`, error);
1004
- errors.push(key);
982
+ errors.push(key.slice(1));
1005
983
  }
1006
984
  }
1007
985
  sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
1008
986
  const trackedPaths = /* @__PURE__ */ new Set();
1009
987
  for (const imageKey of Object.keys(meta)) {
1010
- for (const thumbPath of _chunkCN5NRNWBjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1011
- trackedPaths.add(thumbPath);
988
+ if (!meta[imageKey].s) {
989
+ for (const thumbPath of _chunkHE2DOD2Kjs.getAllThumbnailPaths.call(void 0, imageKey)) {
990
+ trackedPaths.add(thumbPath);
991
+ }
1012
992
  }
1013
993
  }
1014
994
  async function findOrphans(dir, relativePath = "") {
@@ -1032,11 +1012,14 @@ async function handleProcessAllStream() {
1032
1012
  }
1033
1013
  }
1034
1014
  }
1035
- } catch (e27) {
1015
+ } catch (e25) {
1036
1016
  }
1037
1017
  }
1038
1018
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1039
- await findOrphans(imagesDir);
1019
+ try {
1020
+ await findOrphans(imagesDir);
1021
+ } catch (e26) {
1022
+ }
1040
1023
  async function removeEmptyDirs(dir) {
1041
1024
  try {
1042
1025
  const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
@@ -1053,11 +1036,14 @@ async function handleProcessAllStream() {
1053
1036
  await _fs.promises.rmdir(dir);
1054
1037
  }
1055
1038
  return isEmpty;
1056
- } catch (e28) {
1039
+ } catch (e27) {
1057
1040
  return true;
1058
1041
  }
1059
1042
  }
1060
- await removeEmptyDirs(imagesDir);
1043
+ try {
1044
+ await removeEmptyDirs(imagesDir);
1045
+ } catch (e28) {
1046
+ }
1061
1047
  await saveMeta(meta);
1062
1048
  sendEvent({
1063
1049
  type: "complete",
@@ -1082,6 +1068,135 @@ async function handleProcessAllStream() {
1082
1068
  });
1083
1069
  }
1084
1070
 
1071
+ // src/handlers/scan.ts
1072
+
1073
+
1074
+
1075
+ async function handleScanStream() {
1076
+ const encoder = new TextEncoder();
1077
+ const stream = new ReadableStream({
1078
+ async start(controller) {
1079
+ const sendEvent = (data) => {
1080
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
1081
+
1082
+ `));
1083
+ };
1084
+ try {
1085
+ const meta = await loadMeta();
1086
+ const existingKeys = new Set(Object.keys(meta));
1087
+ const added = [];
1088
+ const renamed = [];
1089
+ const errors = [];
1090
+ const allFiles = [];
1091
+ async function scanDir(dir, relativePath = "") {
1092
+ try {
1093
+ const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
1094
+ for (const entry of entries) {
1095
+ if (entry.name.startsWith(".")) continue;
1096
+ const fullPath = _path2.default.join(dir, entry.name);
1097
+ const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1098
+ if (relPath === "images" || relPath.startsWith("images/")) continue;
1099
+ if (entry.isDirectory()) {
1100
+ await scanDir(fullPath, relPath);
1101
+ } else if (isMediaFile(entry.name)) {
1102
+ allFiles.push({ relativePath: relPath, fullPath });
1103
+ }
1104
+ }
1105
+ } catch (e29) {
1106
+ }
1107
+ }
1108
+ const publicDir = _path2.default.join(process.cwd(), "public");
1109
+ await scanDir(publicDir);
1110
+ const total = allFiles.length;
1111
+ sendEvent({ type: "start", total });
1112
+ for (let i = 0; i < allFiles.length; i++) {
1113
+ let { relativePath, fullPath } = allFiles[i];
1114
+ let imageKey = "/" + relativePath;
1115
+ sendEvent({
1116
+ type: "progress",
1117
+ current: i + 1,
1118
+ total,
1119
+ percent: Math.round((i + 1) / total * 100),
1120
+ currentFile: relativePath
1121
+ });
1122
+ if (existingKeys.has(imageKey)) {
1123
+ continue;
1124
+ }
1125
+ if (meta[imageKey]) {
1126
+ const ext = _path2.default.extname(relativePath);
1127
+ const baseName = relativePath.slice(0, -ext.length);
1128
+ let counter = 1;
1129
+ let newKey = `/${baseName}-${counter}${ext}`;
1130
+ while (meta[newKey]) {
1131
+ counter++;
1132
+ newKey = `/${baseName}-${counter}${ext}`;
1133
+ }
1134
+ const newRelativePath = `${baseName}-${counter}${ext}`;
1135
+ const newFullPath = _path2.default.join(process.cwd(), "public", newRelativePath);
1136
+ try {
1137
+ await _fs.promises.rename(fullPath, newFullPath);
1138
+ renamed.push({ from: relativePath, to: newRelativePath });
1139
+ relativePath = newRelativePath;
1140
+ fullPath = newFullPath;
1141
+ imageKey = newKey;
1142
+ } catch (err) {
1143
+ console.error(`Failed to rename ${relativePath}:`, err);
1144
+ errors.push(`Failed to rename ${relativePath}`);
1145
+ continue;
1146
+ }
1147
+ }
1148
+ try {
1149
+ const isImage = isImageFile(relativePath);
1150
+ if (isImage) {
1151
+ const ext = _path2.default.extname(relativePath).toLowerCase();
1152
+ if (ext === ".svg") {
1153
+ meta[imageKey] = { w: 0, h: 0 };
1154
+ } else {
1155
+ try {
1156
+ const metadata = await _sharp2.default.call(void 0, fullPath).metadata();
1157
+ meta[imageKey] = {
1158
+ w: metadata.width || 0,
1159
+ h: metadata.height || 0
1160
+ };
1161
+ } catch (e30) {
1162
+ meta[imageKey] = { w: 0, h: 0 };
1163
+ }
1164
+ }
1165
+ } else {
1166
+ meta[imageKey] = {};
1167
+ }
1168
+ existingKeys.add(imageKey);
1169
+ added.push(imageKey);
1170
+ } catch (error) {
1171
+ console.error(`Failed to process ${relativePath}:`, error);
1172
+ errors.push(relativePath);
1173
+ }
1174
+ }
1175
+ await saveMeta(meta);
1176
+ sendEvent({
1177
+ type: "complete",
1178
+ added: added.length,
1179
+ renamed: renamed.length,
1180
+ errors: errors.length,
1181
+ renamedFiles: renamed
1182
+ });
1183
+ } catch (error) {
1184
+ console.error("Scan failed:", error);
1185
+ sendEvent({ type: "error", message: "Scan failed" });
1186
+ } finally {
1187
+ controller.close();
1188
+ }
1189
+ }
1190
+ });
1191
+ return new Response(stream, {
1192
+ headers: {
1193
+ "Content-Type": "text/event-stream",
1194
+ "Cache-Control": "no-cache",
1195
+ "Connection": "keep-alive"
1196
+ }
1197
+ });
1198
+ }
1199
+
1085
1200
  // src/handlers/index.ts
1086
1201
  async function GET(request) {
1087
1202
  if (process.env.NODE_ENV !== "development") {
@@ -1136,6 +1251,9 @@ async function POST(request) {
1136
1251
  if (route === "move") {
1137
1252
  return handleMove(request);
1138
1253
  }
1254
+ if (route === "scan") {
1255
+ return handleScanStream();
1256
+ }
1139
1257
  return _server.NextResponse.json({ error: "Not found" }, { status: 404 });
1140
1258
  }
1141
1259
  async function DELETE(request) {