@gallop.software/studio 0.1.72 → 0.1.74

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.
package/dist/handlers.mjs CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ getAllThumbnailPaths
3
+ } from "./chunk-3RI33B7A.mjs";
4
+
1
5
  // src/handlers.ts
2
6
  import { NextResponse } from "next/server";
3
7
  import { promises as fs } from "fs";
@@ -252,9 +256,9 @@ async function handleScan() {
252
256
  const validFiles = [];
253
257
  const imagesDir = path.join(process.cwd(), "public", "images");
254
258
  const trackedPaths = /* @__PURE__ */ new Set();
255
- for (const entry of Object.values(meta.images)) {
256
- for (const sizeData of Object.values(entry.sizes)) {
257
- trackedPaths.add(sizeData.path);
259
+ for (const imageKey of Object.keys(meta)) {
260
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
261
+ trackedPaths.add(thumbPath);
258
262
  }
259
263
  }
260
264
  async function scanDir(dir, relativePath = "") {
@@ -279,20 +283,20 @@ async function handleScan() {
279
283
  }
280
284
  }
281
285
  await scanDir(imagesDir);
282
- for (const [key, entry] of Object.entries(meta.images)) {
283
- for (const [size, sizeData] of Object.entries(entry.sizes)) {
284
- const filePath = path.join(process.cwd(), "public", sizeData.path);
286
+ for (const [imageKey, entry] of Object.entries(meta)) {
287
+ if (entry.s) continue;
288
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
289
+ const filePath = path.join(process.cwd(), "public", thumbPath);
285
290
  try {
286
291
  await fs.access(filePath);
287
292
  } catch {
288
- if (!entry.cdn?.synced) {
289
- missingFiles.push(`${key} (${size}): ${sizeData.path}`);
290
- }
293
+ missingFiles.push(`${imageKey}: ${thumbPath}`);
294
+ break;
291
295
  }
292
296
  }
293
297
  }
294
298
  return NextResponse.json({
295
- totalInMeta: Object.keys(meta.images).length,
299
+ totalInMeta: Object.keys(meta).length,
296
300
  validFiles: validFiles.length,
297
301
  untrackedFiles,
298
302
  missingFiles
@@ -319,9 +323,6 @@ async function handleUpload(request) {
319
323
  const isSvg = ext === ".svg";
320
324
  const isProcessableImage = isImage && !isSvg;
321
325
  const meta = await loadMeta();
322
- if (!meta.images) {
323
- meta.images = {};
324
- }
325
326
  let relativeDir = "";
326
327
  if (targetPath === "public") {
327
328
  relativeDir = "";
@@ -344,10 +345,10 @@ async function handleUpload(request) {
344
345
  path: `public/${relativeDir ? relativeDir + "/" : ""}${fileName}`
345
346
  });
346
347
  }
347
- const fullImageKey = relativeDir ? `${relativeDir}/${fileName}` : fileName;
348
- if (meta.images[fullImageKey]) {
348
+ const imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
349
+ if (meta[imageKey]) {
349
350
  return NextResponse.json(
350
- { error: `File '${fullImageKey}' already exists in meta` },
351
+ { error: `File '${imageKey}' already exists in meta` },
351
352
  { status: 409 }
352
353
  );
353
354
  }
@@ -356,21 +357,10 @@ async function handleUpload(request) {
356
357
  let originalWidth = 0;
357
358
  let originalHeight = 0;
358
359
  let blurhash = "";
359
- let dominantColor = "#888888";
360
- const sizes = {
361
- full: { path: "", width: 0, height: 0 },
362
- large: { path: "", width: 0, height: 0 },
363
- medium: { path: "", width: 0, height: 0 },
364
- small: { path: "", width: 0, height: 0 }
365
- };
366
360
  const originalPath = `/${relativeDir ? relativeDir + "/" : ""}${fileName}`;
367
361
  if (isSvg) {
368
362
  const fullPath = path.join(imagesPath, fileName);
369
363
  await fs.writeFile(fullPath, buffer);
370
- sizes.full = { path: `/images/${relativeDir ? relativeDir + "/" : ""}${fileName}`, width: 0, height: 0 };
371
- sizes.large = { ...sizes.full };
372
- sizes.medium = { ...sizes.full };
373
- sizes.small = { ...sizes.full };
374
364
  } else if (isProcessableImage) {
375
365
  const sharpInstance = sharp(buffer);
376
366
  const metadata = await sharpInstance.metadata();
@@ -384,11 +374,9 @@ async function handleUpload(request) {
384
374
  } else {
385
375
  await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath);
386
376
  }
387
- sizes.full = { path: `/images/${relativeDir ? relativeDir + "/" : ""}${fullFileName}`, width: originalWidth, height: originalHeight };
388
- for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
377
+ for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
389
378
  const { width: maxWidth, suffix } = sizeConfig;
390
379
  if (originalWidth <= maxWidth) {
391
- sizes[sizeName] = { ...sizes.full };
392
380
  continue;
393
381
  }
394
382
  const ratio = originalHeight / originalWidth;
@@ -400,32 +388,18 @@ async function handleUpload(request) {
400
388
  } else {
401
389
  await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
402
390
  }
403
- sizes[sizeName] = {
404
- path: `/images/${relativeDir ? relativeDir + "/" : ""}${sizeFileName}`,
405
- width: maxWidth,
406
- height: newHeight
407
- };
408
391
  }
409
392
  const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
410
393
  blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
411
- const { dominant } = await sharp(buffer).stats();
412
- dominantColor = `#${dominant.r.toString(16).padStart(2, "0")}${dominant.g.toString(16).padStart(2, "0")}${dominant.b.toString(16).padStart(2, "0")}`;
413
394
  }
414
395
  const entry = {
415
- original: {
416
- path: originalPath,
417
- width: originalWidth,
418
- height: originalHeight,
419
- fileSize: buffer.length
420
- },
421
- sizes,
422
- blurhash,
423
- dominantColor,
424
- cdn: null
396
+ w: originalWidth,
397
+ h: originalHeight,
398
+ blur: blurhash
425
399
  };
426
- meta.images[fullImageKey] = entry;
400
+ meta[originalPath] = entry;
427
401
  await saveMeta(meta);
428
- return NextResponse.json({ success: true, imageKey: fullImageKey, entry });
402
+ return NextResponse.json({ success: true, imageKey: originalPath, entry });
429
403
  } catch (error) {
430
404
  console.error("Failed to upload:", error);
431
405
  const message = error instanceof Error ? error.message : "Unknown error";
@@ -451,27 +425,26 @@ async function handleDelete(request) {
451
425
  const stats = await fs.stat(absolutePath);
452
426
  if (stats.isDirectory()) {
453
427
  await fs.rm(absolutePath, { recursive: true });
454
- const prefix = itemPath.replace(/^public\/images\/?/, "").replace(/^public\/?/, "");
455
- for (const key of Object.keys(meta.images)) {
428
+ const prefix = "/" + itemPath.replace(/^public\/images\/?/, "").replace(/^public\/?/, "");
429
+ for (const key of Object.keys(meta)) {
456
430
  if (key.startsWith(prefix)) {
457
- delete meta.images[key];
431
+ delete meta[key];
458
432
  }
459
433
  }
460
434
  } else {
461
435
  await fs.unlink(absolutePath);
462
436
  const isInImagesFolder = itemPath.startsWith("public/images/");
463
437
  if (!isInImagesFolder) {
464
- const imageKey = itemPath.replace(/^public\//, "");
465
- const entry = meta.images[imageKey];
466
- if (entry) {
467
- for (const sizeData of Object.values(entry.sizes)) {
468
- const sizePath = path.join(process.cwd(), "public", sizeData.path);
438
+ const imageKey = "/" + itemPath.replace(/^public\//, "");
439
+ if (meta[imageKey]) {
440
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
441
+ const absoluteThumbPath = path.join(process.cwd(), "public", thumbPath);
469
442
  try {
470
- await fs.unlink(sizePath);
443
+ await fs.unlink(absoluteThumbPath);
471
444
  } catch {
472
445
  }
473
446
  }
474
- delete meta.images[imageKey];
447
+ delete meta[imageKey];
475
448
  }
476
449
  }
477
450
  }
@@ -518,35 +491,34 @@ async function handleSync(request) {
518
491
  const synced = [];
519
492
  const errors = [];
520
493
  for (const imageKey of imageKeys) {
521
- const entry = meta.images[imageKey];
494
+ const entry = meta[imageKey];
522
495
  if (!entry) {
523
496
  errors.push(`Image not found in meta: ${imageKey}`);
524
497
  continue;
525
498
  }
526
- if (entry.cdn?.synced) {
499
+ if (entry.s) {
527
500
  synced.push(imageKey);
528
501
  continue;
529
502
  }
530
503
  try {
531
- for (const sizeData of Object.values(entry.sizes)) {
532
- const localPath = path.join(process.cwd(), "public", sizeData.path);
533
- const fileBuffer = await fs.readFile(localPath);
534
- await r2.send(
535
- new PutObjectCommand({
536
- Bucket: bucketName,
537
- Key: sizeData.path.replace(/^\//, ""),
538
- Body: fileBuffer,
539
- ContentType: getContentType(sizeData.path)
540
- })
541
- );
504
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
505
+ const localPath = path.join(process.cwd(), "public", thumbPath);
506
+ try {
507
+ const fileBuffer = await fs.readFile(localPath);
508
+ await r2.send(
509
+ new PutObjectCommand({
510
+ Bucket: bucketName,
511
+ Key: thumbPath.replace(/^\//, ""),
512
+ Body: fileBuffer,
513
+ ContentType: getContentType(thumbPath)
514
+ })
515
+ );
516
+ } catch {
517
+ }
542
518
  }
543
- entry.cdn = {
544
- synced: true,
545
- baseUrl: publicUrl,
546
- syncedAt: (/* @__PURE__ */ new Date()).toISOString()
547
- };
548
- for (const sizeData of Object.values(entry.sizes)) {
549
- const localPath = path.join(process.cwd(), "public", sizeData.path);
519
+ entry.s = 1;
520
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
521
+ const localPath = path.join(process.cwd(), "public", thumbPath);
550
522
  try {
551
523
  await fs.unlink(localPath);
552
524
  } catch {
@@ -581,54 +553,24 @@ async function handleReprocess(request) {
581
553
  for (const imageKey of imageKeys) {
582
554
  try {
583
555
  let buffer;
584
- let entry = meta.images[imageKey];
556
+ const entry = meta[imageKey];
585
557
  const originalPath = path.join(process.cwd(), "public", imageKey);
586
558
  try {
587
559
  buffer = await fs.readFile(originalPath);
588
560
  } catch {
589
- if (entry) {
590
- const entryOriginalPath = path.join(process.cwd(), "public", entry.original.path);
591
- try {
592
- buffer = await fs.readFile(entryOriginalPath);
593
- } catch {
594
- if (entry.cdn?.synced) {
595
- buffer = await downloadFromCdn(entry.original.path);
596
- } else {
597
- throw new Error("Original not found locally and not on CDN");
598
- }
599
- }
561
+ if (entry?.s) {
562
+ buffer = await downloadFromCdn(imageKey);
600
563
  } else {
601
564
  throw new Error(`File not found: ${imageKey}`);
602
565
  }
603
566
  }
604
- if (!entry) {
605
- const sharpInstance = sharp(buffer);
606
- const metadata = await sharpInstance.metadata();
607
- const stats = await fs.stat(originalPath);
608
- entry = {
609
- original: {
610
- path: imageKey,
611
- width: metadata.width || 0,
612
- height: metadata.height || 0,
613
- fileSize: stats.size
614
- },
615
- sizes: {
616
- full: { path: "", width: 0, height: 0 },
617
- large: { path: "", width: 0, height: 0 },
618
- medium: { path: "", width: 0, height: 0 },
619
- small: { path: "", width: 0, height: 0 }
620
- },
621
- blurhash: "",
622
- dominantColor: "#000000",
623
- cdn: null
624
- };
625
- }
626
- const updatedEntry = await processImage(buffer, entry, imageKey);
627
- meta.images[imageKey] = updatedEntry;
628
- if (entry.cdn?.synced) {
629
- await uploadToCdn(updatedEntry);
630
- await deleteLocalFiles(updatedEntry);
567
+ const updatedEntry = await processImage(buffer, imageKey);
568
+ if (entry?.s) {
569
+ updatedEntry.s = 1;
570
+ await uploadToCdn(imageKey);
571
+ await deleteLocalThumbnails(imageKey);
631
572
  }
573
+ meta[imageKey] = updatedEntry;
632
574
  processed.push(imageKey);
633
575
  } catch (error) {
634
576
  console.error(`Failed to reprocess ${imageKey}:`, error);
@@ -755,6 +697,7 @@ async function handleProcessAllStream() {
755
697
  sendEvent({ type: "start", total });
756
698
  for (let i = 0; i < allImages.length; i++) {
757
699
  const { key, fullPath } = allImages[i];
700
+ const imageKey = "/" + key;
758
701
  sendEvent({
759
702
  type: "progress",
760
703
  current: i + 1,
@@ -773,45 +716,18 @@ async function handleProcessAllStream() {
773
716
  const fileName = path.basename(key);
774
717
  const destPath = path.join(imagesPath, fileName);
775
718
  await fs.writeFile(destPath, buffer);
776
- const sizePath = `/images/${imageDir === "." ? "" : imageDir + "/"}${fileName}`;
777
- meta.images[key] = {
778
- original: {
779
- path: `/${key}`,
780
- width: 0,
781
- height: 0,
782
- fileSize: buffer.length
783
- },
784
- sizes: {
785
- full: { path: sizePath, width: 0, height: 0 },
786
- large: { path: sizePath, width: 0, height: 0 },
787
- medium: { path: sizePath, width: 0, height: 0 },
788
- small: { path: sizePath, width: 0, height: 0 }
789
- },
790
- blurhash: "",
791
- dominantColor: "#888888",
792
- cdn: null
719
+ meta[imageKey] = {
720
+ w: 0,
721
+ h: 0,
722
+ blur: ""
793
723
  };
794
724
  } else {
795
- const existingEntry = meta.images[key];
796
- const baseEntry = existingEntry || {
797
- original: {
798
- path: `/${key}`,
799
- width: 0,
800
- height: 0,
801
- fileSize: buffer.length
802
- },
803
- sizes: {
804
- full: { path: "", width: 0, height: 0 },
805
- large: { path: "", width: 0, height: 0 },
806
- medium: { path: "", width: 0, height: 0 },
807
- small: { path: "", width: 0, height: 0 }
808
- },
809
- blurhash: "",
810
- dominantColor: "#888888",
811
- cdn: null
812
- };
813
- const processedEntry = await processImage(buffer, baseEntry, key);
814
- meta.images[key] = processedEntry;
725
+ const existingEntry = meta[imageKey];
726
+ const processedEntry = await processImage(buffer, imageKey);
727
+ if (existingEntry?.s) {
728
+ processedEntry.s = 1;
729
+ }
730
+ meta[imageKey] = processedEntry;
815
731
  }
816
732
  processed.push(key);
817
733
  } catch (error) {
@@ -821,9 +737,9 @@ async function handleProcessAllStream() {
821
737
  }
822
738
  sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
823
739
  const trackedPaths = /* @__PURE__ */ new Set();
824
- for (const entry of Object.values(meta.images)) {
825
- for (const sizeData of Object.values(entry.sizes)) {
826
- trackedPaths.add(sizeData.path);
740
+ for (const imageKey of Object.keys(meta)) {
741
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
742
+ trackedPaths.add(thumbPath);
827
743
  }
828
744
  }
829
745
  async function findOrphans(dir, relativePath = "") {
@@ -898,57 +814,18 @@ async function handleProcessAllStream() {
898
814
  }
899
815
  async function loadMeta() {
900
816
  const metaPath = path.join(process.cwd(), "_data", "_meta.json");
901
- const emptyMeta = {
902
- $schema: "https://gallop.software/schemas/studio-meta.json",
903
- version: 1,
904
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
905
- images: {}
906
- };
907
817
  try {
908
818
  const content = await fs.readFile(metaPath, "utf-8");
909
- const parsed = JSON.parse(content);
910
- if (parsed.images && typeof parsed.images === "object") {
911
- return parsed;
912
- }
913
- const meta = { ...emptyMeta, images: {} };
914
- for (const [imagePath, entry] of Object.entries(parsed)) {
915
- const leanEntry = entry;
916
- const key = imagePath.startsWith("/") ? imagePath.slice(1) : imagePath;
917
- meta.images[key] = {
918
- original: {
919
- path: imagePath,
920
- width: leanEntry.w,
921
- height: leanEntry.h,
922
- fileSize: 0
923
- },
924
- sizes: {},
925
- blurhash: leanEntry.blur,
926
- dominantColor: "",
927
- cdn: leanEntry.s ? { synced: true, baseUrl: "", syncedAt: "" } : null
928
- };
929
- }
930
- return meta;
819
+ return JSON.parse(content);
931
820
  } catch {
932
- return emptyMeta;
821
+ return {};
933
822
  }
934
823
  }
935
824
  async function saveMeta(meta) {
936
825
  const dataDir = path.join(process.cwd(), "_data");
937
826
  await fs.mkdir(dataDir, { recursive: true });
938
- const lean = {};
939
- for (const [key, entry] of Object.entries(meta.images)) {
940
- const imagePath = entry.original?.path || `/${key}`;
941
- lean[imagePath] = {
942
- w: entry.original?.width || 0,
943
- h: entry.original?.height || 0,
944
- blur: entry.blurhash || ""
945
- };
946
- if (entry.cdn?.synced) {
947
- lean[imagePath].s = 1;
948
- }
949
- }
950
827
  const metaPath = path.join(dataDir, "_meta.json");
951
- await fs.writeFile(metaPath, JSON.stringify(lean, null, 2));
828
+ await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
952
829
  }
953
830
  function isImageFile(filename) {
954
831
  const ext = path.extname(filename).toLowerCase();
@@ -980,22 +857,17 @@ function getContentType(filePath) {
980
857
  return "application/octet-stream";
981
858
  }
982
859
  }
983
- async function processImage(buffer, entry, imageKey) {
860
+ async function processImage(buffer, imageKey) {
984
861
  const sharpInstance = sharp(buffer);
985
862
  const metadata = await sharpInstance.metadata();
986
863
  const originalWidth = metadata.width || 0;
987
864
  const originalHeight = metadata.height || 0;
988
- const baseName = path.basename(imageKey, path.extname(imageKey));
989
- const ext = path.extname(imageKey).toLowerCase();
990
- const imageDir = path.dirname(imageKey);
865
+ const keyWithoutSlash = imageKey.startsWith("/") ? imageKey.slice(1) : imageKey;
866
+ const baseName = path.basename(keyWithoutSlash, path.extname(keyWithoutSlash));
867
+ const ext = path.extname(keyWithoutSlash).toLowerCase();
868
+ const imageDir = path.dirname(keyWithoutSlash);
991
869
  const imagesPath = path.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
992
870
  await fs.mkdir(imagesPath, { recursive: true });
993
- const sizes = {
994
- full: { path: "", width: originalWidth, height: originalHeight },
995
- large: { path: "", width: 0, height: 0 },
996
- medium: { path: "", width: 0, height: 0 },
997
- small: { path: "", width: 0, height: 0 }
998
- };
999
871
  const isPng = ext === ".png";
1000
872
  const outputExt = isPng ? ".png" : ".jpg";
1001
873
  const fullFileName = imageDir === "." ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`;
@@ -1005,11 +877,9 @@ async function processImage(buffer, entry, imageKey) {
1005
877
  } else {
1006
878
  await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath);
1007
879
  }
1008
- sizes.full.path = `/images/${fullFileName}`;
1009
- for (const [sizeName, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
880
+ for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
1010
881
  const { width: maxWidth, suffix } = sizeConfig;
1011
882
  if (originalWidth <= maxWidth) {
1012
- sizes[sizeName] = { ...sizes.full };
1013
883
  continue;
1014
884
  }
1015
885
  const ratio = originalHeight / originalWidth;
@@ -1022,27 +892,13 @@ async function processImage(buffer, entry, imageKey) {
1022
892
  } else {
1023
893
  await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
1024
894
  }
1025
- sizes[sizeName] = {
1026
- path: `/images/${sizeFilePath}`,
1027
- width: maxWidth,
1028
- height: newHeight
1029
- };
1030
895
  }
1031
896
  const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
1032
897
  const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
1033
- const { dominant } = await sharp(buffer).stats();
1034
- const dominantColor = `#${dominant.r.toString(16).padStart(2, "0")}${dominant.g.toString(16).padStart(2, "0")}${dominant.b.toString(16).padStart(2, "0")}`;
1035
898
  return {
1036
- ...entry,
1037
- original: {
1038
- ...entry.original,
1039
- width: originalWidth,
1040
- height: originalHeight,
1041
- fileSize: buffer.length
1042
- },
1043
- sizes,
1044
- blurhash,
1045
- dominantColor
899
+ w: originalWidth,
900
+ h: originalHeight,
901
+ blur: blurhash
1046
902
  };
1047
903
  }
1048
904
  async function downloadFromCdn(originalPath) {
@@ -1071,7 +927,7 @@ async function downloadFromCdn(originalPath) {
1071
927
  }
1072
928
  return Buffer.concat(chunks);
1073
929
  }
1074
- async function uploadToCdn(entry) {
930
+ async function uploadToCdn(imageKey) {
1075
931
  const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
1076
932
  const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
1077
933
  const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
@@ -1084,22 +940,25 @@ async function uploadToCdn(entry) {
1084
940
  endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
1085
941
  credentials: { accessKeyId, secretAccessKey }
1086
942
  });
1087
- for (const sizeData of Object.values(entry.sizes)) {
1088
- const localPath = path.join(process.cwd(), "public", sizeData.path);
1089
- const fileBuffer = await fs.readFile(localPath);
1090
- await r2.send(
1091
- new PutObjectCommand({
1092
- Bucket: bucketName,
1093
- Key: sizeData.path.replace(/^\//, ""),
1094
- Body: fileBuffer,
1095
- ContentType: getContentType(sizeData.path)
1096
- })
1097
- );
943
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
944
+ const localPath = path.join(process.cwd(), "public", thumbPath);
945
+ try {
946
+ const fileBuffer = await fs.readFile(localPath);
947
+ await r2.send(
948
+ new PutObjectCommand({
949
+ Bucket: bucketName,
950
+ Key: thumbPath.replace(/^\//, ""),
951
+ Body: fileBuffer,
952
+ ContentType: getContentType(thumbPath)
953
+ })
954
+ );
955
+ } catch {
956
+ }
1098
957
  }
1099
958
  }
1100
- async function deleteLocalFiles(entry) {
1101
- for (const sizeData of Object.values(entry.sizes)) {
1102
- const localPath = path.join(process.cwd(), "public", sizeData.path);
959
+ async function deleteLocalThumbnails(imageKey) {
960
+ for (const thumbPath of getAllThumbnailPaths(imageKey)) {
961
+ const localPath = path.join(process.cwd(), "public", thumbPath);
1103
962
  try {
1104
963
  await fs.unlink(localPath);
1105
964
  } catch {
@@ -1168,32 +1027,23 @@ async function handleRename(request) {
1168
1027
  const meta = await loadMeta();
1169
1028
  const oldRelativePath = safePath.replace(/^public\//, "");
1170
1029
  const newRelativePath = path.join(path.dirname(oldRelativePath), sanitizedName);
1171
- for (const [key, entry] of Object.entries(meta.images)) {
1172
- if (entry.original.path === `/${oldRelativePath}`) {
1173
- entry.original.path = `/${newRelativePath}`;
1174
- const oldExt = path.extname(path.basename(oldPath));
1175
- const oldBaseName = path.basename(oldPath, oldExt);
1176
- const newExt = path.extname(sanitizedName);
1177
- const newBaseName = path.basename(sanitizedName, newExt);
1178
- const oldDirRelative = path.dirname(oldRelativePath);
1179
- const thumbnailDir = path.join(process.cwd(), "public", "images", oldDirRelative);
1180
- for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {
1181
- const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`;
1182
- const oldThumbName = `${oldBaseName}${suffix}${oldExt === ".png" ? ".png" : ".jpg"}`;
1183
- const newThumbName = `${newBaseName}${suffix}${newExt === ".png" ? ".png" : ".jpg"}`;
1184
- const oldThumbPath = path.join(thumbnailDir, oldThumbName);
1185
- const newThumbPath = path.join(thumbnailDir, newThumbName);
1186
- try {
1187
- await fs.rename(oldThumbPath, newThumbPath);
1188
- sizeData.path = `/images/${oldDirRelative}/${newThumbName}`.replace(/\/+/g, "/");
1189
- } catch {
1190
- }
1030
+ const oldKey = "/" + oldRelativePath;
1031
+ const newKey = "/" + newRelativePath;
1032
+ if (meta[oldKey]) {
1033
+ const entry = meta[oldKey];
1034
+ const oldThumbPaths = getAllThumbnailPaths(oldKey);
1035
+ const newThumbPaths = getAllThumbnailPaths(newKey);
1036
+ for (let i = 0; i < oldThumbPaths.length; i++) {
1037
+ const oldThumbPath = path.join(process.cwd(), "public", oldThumbPaths[i]);
1038
+ const newThumbPath = path.join(process.cwd(), "public", newThumbPaths[i]);
1039
+ await fs.mkdir(path.dirname(newThumbPath), { recursive: true });
1040
+ try {
1041
+ await fs.rename(oldThumbPath, newThumbPath);
1042
+ } catch {
1191
1043
  }
1192
- const newKey = `/${newRelativePath}`;
1193
- delete meta.images[key];
1194
- meta.images[newKey] = entry;
1195
- break;
1196
1044
  }
1045
+ delete meta[oldKey];
1046
+ meta[newKey] = entry;
1197
1047
  }
1198
1048
  await saveMeta(meta);
1199
1049
  }
@@ -1260,33 +1110,24 @@ async function handleMove(request) {
1260
1110
  if (stats.isFile() && isImageFile(itemName)) {
1261
1111
  const oldRelativePath = safePath.replace(/^public\//, "");
1262
1112
  const newRelativePath = path.join(safeDestination.replace(/^public\//, ""), itemName);
1263
- for (const [key, entry] of Object.entries(meta.images)) {
1264
- if (entry.original.path === `/${oldRelativePath}`) {
1265
- entry.original.path = `/${newRelativePath}`;
1266
- const oldDir = path.dirname(oldRelativePath);
1267
- const newDir = path.dirname(newRelativePath);
1268
- const ext = path.extname(itemName);
1269
- const baseName = path.basename(itemName, ext);
1270
- const oldThumbDir = path.join(process.cwd(), "public", "images", oldDir);
1271
- const newThumbDir = path.join(process.cwd(), "public", "images", newDir);
1272
- await fs.mkdir(newThumbDir, { recursive: true });
1273
- for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {
1274
- const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`;
1275
- const thumbName = `${baseName}${suffix}${ext === ".png" ? ".png" : ".jpg"}`;
1276
- const oldThumbPath = path.join(oldThumbDir, thumbName);
1277
- const newThumbPath = path.join(newThumbDir, thumbName);
1278
- try {
1279
- await fs.rename(oldThumbPath, newThumbPath);
1280
- sizeData.path = `/images/${newDir}/${thumbName}`.replace(/\/+/g, "/");
1281
- } catch {
1282
- }
1113
+ const oldKey = "/" + oldRelativePath;
1114
+ const newKey = "/" + newRelativePath;
1115
+ if (meta[oldKey]) {
1116
+ const entry = meta[oldKey];
1117
+ const oldThumbPaths = getAllThumbnailPaths(oldKey);
1118
+ const newThumbPaths = getAllThumbnailPaths(newKey);
1119
+ for (let i = 0; i < oldThumbPaths.length; i++) {
1120
+ const oldThumbPath = path.join(process.cwd(), "public", oldThumbPaths[i]);
1121
+ const newThumbPath = path.join(process.cwd(), "public", newThumbPaths[i]);
1122
+ await fs.mkdir(path.dirname(newThumbPath), { recursive: true });
1123
+ try {
1124
+ await fs.rename(oldThumbPath, newThumbPath);
1125
+ } catch {
1283
1126
  }
1284
- const newKey = `/${newRelativePath}`;
1285
- delete meta.images[key];
1286
- meta.images[newKey] = entry;
1287
- metaChanged = true;
1288
- break;
1289
1127
  }
1128
+ delete meta[oldKey];
1129
+ meta[newKey] = entry;
1130
+ metaChanged = true;
1290
1131
  }
1291
1132
  }
1292
1133
  moved.push(itemPath);