@gallop.software/studio 0.1.105 → 0.1.107

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.
@@ -210,6 +210,56 @@ async function deleteLocalThumbnails(imageKey) {
210
210
  }
211
211
  }
212
212
  }
213
+ async function downloadFromRemoteUrl(url) {
214
+ const response = await fetch(url);
215
+ if (!response.ok) {
216
+ throw new Error(`Failed to download from ${url}: ${response.status}`);
217
+ }
218
+ const arrayBuffer = await response.arrayBuffer();
219
+ return Buffer.from(arrayBuffer);
220
+ }
221
+ async function uploadOriginalToCdn(imageKey) {
222
+ const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
223
+ if (!bucketName) throw new Error("R2 bucket not configured");
224
+ const r2 = getR2Client();
225
+ const localPath = _path2.default.join(process.cwd(), "public", imageKey);
226
+ const fileBuffer = await _fs.promises.readFile(localPath);
227
+ await r2.send(
228
+ new (0, _clients3.PutObjectCommand)({
229
+ Bucket: bucketName,
230
+ Key: imageKey.replace(/^\//, ""),
231
+ Body: fileBuffer,
232
+ ContentType: getContentType(imageKey)
233
+ })
234
+ );
235
+ }
236
+ async function deleteFromCdn(imageKey, hasThumbnails) {
237
+ const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
238
+ if (!bucketName) throw new Error("R2 bucket not configured");
239
+ const r2 = getR2Client();
240
+ try {
241
+ await r2.send(
242
+ new (0, _clients3.DeleteObjectCommand)({
243
+ Bucket: bucketName,
244
+ Key: imageKey.replace(/^\//, "")
245
+ })
246
+ );
247
+ } catch (e4) {
248
+ }
249
+ if (hasThumbnails) {
250
+ for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
251
+ try {
252
+ await r2.send(
253
+ new (0, _clients3.DeleteObjectCommand)({
254
+ Bucket: bucketName,
255
+ Key: thumbPath.replace(/^\//, "")
256
+ })
257
+ );
258
+ } catch (e5) {
259
+ }
260
+ }
261
+ }
262
+ }
213
263
 
214
264
  // src/handlers/list.ts
215
265
  async function handleList(request) {
@@ -273,7 +323,7 @@ async function handleList(request) {
273
323
  await _fs.promises.access(localThumbPath);
274
324
  thumbnail = thumbPath;
275
325
  hasThumbnail = true;
276
- } catch (e4) {
326
+ } catch (e6) {
277
327
  thumbnail = key;
278
328
  hasThumbnail = false;
279
329
  }
@@ -292,7 +342,7 @@ async function handleList(request) {
292
342
  const filePath = _path2.default.join(process.cwd(), "public", key);
293
343
  const stats = await _fs.promises.stat(filePath);
294
344
  fileSize = stats.size;
295
- } catch (e5) {
345
+ } catch (e7) {
296
346
  }
297
347
  }
298
348
  items.push({
@@ -352,7 +402,7 @@ async function handleSearch(request) {
352
402
  await _fs.promises.access(localThumbPath);
353
403
  thumbnail = thumbPath;
354
404
  hasThumbnail = true;
355
- } catch (e6) {
405
+ } catch (e8) {
356
406
  thumbnail = key;
357
407
  hasThumbnail = false;
358
408
  }
@@ -534,7 +584,7 @@ async function handleUpload(request) {
534
584
  w: metadata.width || 0,
535
585
  h: metadata.height || 0
536
586
  };
537
- } catch (e7) {
587
+ } catch (e9) {
538
588
  meta[imageKey] = { w: 0, h: 0 };
539
589
  }
540
590
  } else {
@@ -583,7 +633,7 @@ async function handleDelete(request) {
583
633
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
584
634
  try {
585
635
  await _fs.promises.unlink(absoluteThumbPath);
586
- } catch (e8) {
636
+ } catch (e10) {
587
637
  }
588
638
  }
589
639
  }
@@ -599,14 +649,14 @@ async function handleDelete(request) {
599
649
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
600
650
  try {
601
651
  await _fs.promises.unlink(absoluteThumbPath);
602
- } catch (e9) {
652
+ } catch (e11) {
603
653
  }
604
654
  }
605
655
  }
606
656
  delete meta[imageKey];
607
657
  }
608
658
  }
609
- } catch (e10) {
659
+ } catch (e12) {
610
660
  if (entry) {
611
661
  delete meta[imageKey];
612
662
  } else {
@@ -659,7 +709,7 @@ async function handleCreateFolder(request) {
659
709
  try {
660
710
  await _fs.promises.access(folderPath);
661
711
  return _server.NextResponse.json({ error: "A folder with this name already exists" }, { status: 400 });
662
- } catch (e11) {
712
+ } catch (e13) {
663
713
  }
664
714
  await _fs.promises.mkdir(folderPath, { recursive: true });
665
715
  return _server.NextResponse.json({ success: true, path: _path2.default.join(safePath, sanitizedName) });
@@ -687,13 +737,13 @@ async function handleRename(request) {
687
737
  }
688
738
  try {
689
739
  await _fs.promises.access(absoluteOldPath);
690
- } catch (e12) {
740
+ } catch (e14) {
691
741
  return _server.NextResponse.json({ error: "File or folder not found" }, { status: 404 });
692
742
  }
693
743
  try {
694
744
  await _fs.promises.access(absoluteNewPath);
695
745
  return _server.NextResponse.json({ error: "An item with this name already exists" }, { status: 400 });
696
- } catch (e13) {
746
+ } catch (e15) {
697
747
  }
698
748
  const stats = await _fs.promises.stat(absoluteOldPath);
699
749
  const isFile = stats.isFile();
@@ -715,7 +765,7 @@ async function handleRename(request) {
715
765
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
716
766
  try {
717
767
  await _fs.promises.rename(oldThumbPath, newThumbPath);
718
- } catch (e14) {
768
+ } catch (e16) {
719
769
  }
720
770
  }
721
771
  delete meta[oldKey];
@@ -744,49 +794,103 @@ async function handleMove(request) {
744
794
  if (!absoluteDestination.startsWith(_path2.default.join(process.cwd(), "public"))) {
745
795
  return _server.NextResponse.json({ error: "Invalid destination" }, { status: 400 });
746
796
  }
747
- try {
748
- const destStats = await _fs.promises.stat(absoluteDestination);
749
- if (!destStats.isDirectory()) {
750
- return _server.NextResponse.json({ error: "Destination is not a folder" }, { status: 400 });
751
- }
752
- } catch (e15) {
753
- return _server.NextResponse.json({ error: "Destination folder not found" }, { status: 404 });
754
- }
797
+ await _fs.promises.mkdir(absoluteDestination, { recursive: true });
755
798
  const moved = [];
756
799
  const errors = [];
757
800
  const meta = await loadMeta();
801
+ const cdnUrls = getCdnUrls(meta);
802
+ const r2PublicUrl = _optionalChain([process, 'access', _14 => _14.env, 'access', _15 => _15.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _16 => _16.replace, 'call', _17 => _17(/\/$/, "")]) || "";
758
803
  let metaChanged = false;
759
804
  for (const itemPath of paths) {
760
805
  const safePath = itemPath.replace(/\.\./g, "");
761
- const absolutePath = _path2.default.join(process.cwd(), safePath);
762
806
  const itemName = _path2.default.basename(safePath);
763
807
  const newAbsolutePath = _path2.default.join(absoluteDestination, itemName);
764
- if (absoluteDestination.startsWith(absolutePath + _path2.default.sep)) {
765
- errors.push(`Cannot move ${itemName} into itself`);
766
- continue;
767
- }
768
- try {
769
- await _fs.promises.access(absolutePath);
770
- } catch (e16) {
771
- errors.push(`${itemName} not found`);
772
- continue;
773
- }
774
- try {
775
- await _fs.promises.access(newAbsolutePath);
808
+ const oldRelativePath = safePath.replace(/^public\//, "");
809
+ const newRelativePath = _path2.default.join(safeDestination.replace(/^public\//, ""), itemName);
810
+ const oldKey = "/" + oldRelativePath;
811
+ const newKey = "/" + newRelativePath;
812
+ if (meta[newKey]) {
776
813
  errors.push(`${itemName} already exists in destination`);
777
814
  continue;
778
- } catch (e17) {
779
815
  }
816
+ const entry = meta[oldKey];
817
+ const isImage = isImageFile(itemName);
818
+ const isInCloud = _optionalChain([entry, 'optionalAccess', _18 => _18.c]) !== void 0;
819
+ const fileCdnUrl = isInCloud && entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
820
+ const isRemote = isInCloud && (!r2PublicUrl || fileCdnUrl !== r2PublicUrl);
821
+ const isPushedToR2 = isInCloud && r2PublicUrl && fileCdnUrl === r2PublicUrl;
822
+ const hasProcessedThumbnails = _optionalChain([entry, 'optionalAccess', _19 => _19.p]) === 1;
780
823
  try {
781
- await _fs.promises.rename(absolutePath, newAbsolutePath);
782
- const stats = await _fs.promises.stat(newAbsolutePath);
783
- if (stats.isFile() && isImageFile(itemName)) {
784
- const oldRelativePath = safePath.replace(/^public\//, "");
785
- const newRelativePath = _path2.default.join(safeDestination.replace(/^public\//, ""), itemName);
786
- const oldKey = "/" + oldRelativePath;
787
- const newKey = "/" + newRelativePath;
788
- if (meta[oldKey]) {
789
- const entry = meta[oldKey];
824
+ if (isRemote && isImage) {
825
+ const remoteUrl = `${fileCdnUrl}${oldKey}`;
826
+ const buffer = await downloadFromRemoteUrl(remoteUrl);
827
+ await _fs.promises.mkdir(_path2.default.dirname(newAbsolutePath), { recursive: true });
828
+ await _fs.promises.writeFile(newAbsolutePath, buffer);
829
+ const newEntry = {
830
+ w: _optionalChain([entry, 'optionalAccess', _20 => _20.w]),
831
+ h: _optionalChain([entry, 'optionalAccess', _21 => _21.h]),
832
+ b: _optionalChain([entry, 'optionalAccess', _22 => _22.b])
833
+ // Don't copy p since remote images don't have local thumbnails
834
+ // Don't copy c since it's now local
835
+ };
836
+ delete meta[oldKey];
837
+ meta[newKey] = newEntry;
838
+ metaChanged = true;
839
+ moved.push(itemPath);
840
+ } else if (isPushedToR2 && isImage) {
841
+ const buffer = await downloadFromCdn(oldKey);
842
+ await _fs.promises.mkdir(_path2.default.dirname(newAbsolutePath), { recursive: true });
843
+ await _fs.promises.writeFile(newAbsolutePath, buffer);
844
+ const newEntry = {
845
+ w: _optionalChain([entry, 'optionalAccess', _23 => _23.w]),
846
+ h: _optionalChain([entry, 'optionalAccess', _24 => _24.h]),
847
+ b: _optionalChain([entry, 'optionalAccess', _25 => _25.b])
848
+ };
849
+ if (hasProcessedThumbnails) {
850
+ const processedEntry = await processImage(buffer, newKey);
851
+ newEntry.w = processedEntry.w;
852
+ newEntry.h = processedEntry.h;
853
+ newEntry.b = processedEntry.b;
854
+ newEntry.p = 1;
855
+ }
856
+ await uploadOriginalToCdn(newKey);
857
+ if (hasProcessedThumbnails) {
858
+ await uploadToCdn(newKey);
859
+ }
860
+ await deleteFromCdn(oldKey, hasProcessedThumbnails);
861
+ try {
862
+ await _fs.promises.unlink(newAbsolutePath);
863
+ } catch (e17) {
864
+ }
865
+ if (hasProcessedThumbnails) {
866
+ await deleteLocalThumbnails(newKey);
867
+ }
868
+ newEntry.c = _optionalChain([entry, 'optionalAccess', _26 => _26.c]);
869
+ delete meta[oldKey];
870
+ meta[newKey] = newEntry;
871
+ metaChanged = true;
872
+ moved.push(itemPath);
873
+ } else {
874
+ const absolutePath = _path2.default.join(process.cwd(), safePath);
875
+ if (absoluteDestination.startsWith(absolutePath + _path2.default.sep)) {
876
+ errors.push(`Cannot move ${itemName} into itself`);
877
+ continue;
878
+ }
879
+ try {
880
+ await _fs.promises.access(absolutePath);
881
+ } catch (e18) {
882
+ errors.push(`${itemName} not found`);
883
+ continue;
884
+ }
885
+ try {
886
+ await _fs.promises.access(newAbsolutePath);
887
+ errors.push(`${itemName} already exists in destination`);
888
+ continue;
889
+ } catch (e19) {
890
+ }
891
+ await _fs.promises.rename(absolutePath, newAbsolutePath);
892
+ const stats = await _fs.promises.stat(newAbsolutePath);
893
+ if (stats.isFile() && isImage && entry) {
790
894
  const oldThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, oldKey);
791
895
  const newThumbPaths = _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, newKey);
792
896
  for (let i = 0; i < oldThumbPaths.length; i++) {
@@ -795,16 +899,28 @@ async function handleMove(request) {
795
899
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
796
900
  try {
797
901
  await _fs.promises.rename(oldThumbPath, newThumbPath);
798
- } catch (e18) {
902
+ } catch (e20) {
799
903
  }
800
904
  }
801
905
  delete meta[oldKey];
802
906
  meta[newKey] = entry;
803
907
  metaChanged = true;
908
+ } else if (stats.isDirectory()) {
909
+ const oldPrefix = oldKey + "/";
910
+ const newPrefix = newKey + "/";
911
+ for (const key of Object.keys(meta)) {
912
+ if (key.startsWith(oldPrefix)) {
913
+ const newMetaKey = newPrefix + key.slice(oldPrefix.length);
914
+ meta[newMetaKey] = meta[key];
915
+ delete meta[key];
916
+ metaChanged = true;
917
+ }
918
+ }
804
919
  }
920
+ moved.push(itemPath);
805
921
  }
806
- moved.push(itemPath);
807
- } catch (e19) {
922
+ } catch (err) {
923
+ console.error(`Failed to move ${itemName}:`, err);
808
924
  errors.push(`Failed to move ${itemName}`);
809
925
  }
810
926
  }
@@ -895,7 +1011,7 @@ async function handleSync(request) {
895
1011
  ContentType: getContentType(thumbPath)
896
1012
  })
897
1013
  );
898
- } catch (e20) {
1014
+ } catch (e21) {
899
1015
  }
900
1016
  }
901
1017
  entry.c = cdnIndex;
@@ -903,12 +1019,12 @@ async function handleSync(request) {
903
1019
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
904
1020
  try {
905
1021
  await _fs.promises.unlink(localPath);
906
- } catch (e21) {
1022
+ } catch (e22) {
907
1023
  }
908
1024
  }
909
1025
  try {
910
1026
  await _fs.promises.unlink(originalLocalPath);
911
- } catch (e22) {
1027
+ } catch (e23) {
912
1028
  }
913
1029
  pushed.push(imageKey);
914
1030
  } catch (error) {
@@ -940,12 +1056,12 @@ async function handleReprocess(request) {
940
1056
  try {
941
1057
  let buffer;
942
1058
  const entry = getMetaEntry(meta, imageKey);
943
- const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _14 => _14.c]) !== void 0;
944
- const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _15 => _15.c]);
1059
+ const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _27 => _27.c]) !== void 0;
1060
+ const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _28 => _28.c]);
945
1061
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
946
1062
  try {
947
1063
  buffer = await _fs.promises.readFile(originalPath);
948
- } catch (e23) {
1064
+ } catch (e24) {
949
1065
  if (isPushedToCloud) {
950
1066
  buffer = await downloadFromCdn(imageKey);
951
1067
  const dir = _path2.default.dirname(originalPath);
@@ -962,7 +1078,7 @@ async function handleReprocess(request) {
962
1078
  await deleteLocalThumbnails(imageKey);
963
1079
  try {
964
1080
  await _fs.promises.unlink(originalPath);
965
- } catch (e24) {
1081
+ } catch (e25) {
966
1082
  }
967
1083
  }
968
1084
  meta[imageKey] = updatedEntry;
@@ -1062,7 +1178,7 @@ async function handleProcessAllStream() {
1062
1178
  await deleteLocalThumbnails(key);
1063
1179
  try {
1064
1180
  await _fs.promises.unlink(fullPath);
1065
- } catch (e25) {
1181
+ } catch (e26) {
1066
1182
  }
1067
1183
  }
1068
1184
  processed.push(key.slice(1));
@@ -1101,13 +1217,13 @@ async function handleProcessAllStream() {
1101
1217
  }
1102
1218
  }
1103
1219
  }
1104
- } catch (e26) {
1220
+ } catch (e27) {
1105
1221
  }
1106
1222
  }
1107
1223
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1108
1224
  try {
1109
1225
  await findOrphans(imagesDir);
1110
- } catch (e27) {
1226
+ } catch (e28) {
1111
1227
  }
1112
1228
  async function removeEmptyDirs(dir) {
1113
1229
  try {
@@ -1125,13 +1241,13 @@ async function handleProcessAllStream() {
1125
1241
  await _fs.promises.rmdir(dir);
1126
1242
  }
1127
1243
  return isEmpty;
1128
- } catch (e28) {
1244
+ } catch (e29) {
1129
1245
  return true;
1130
1246
  }
1131
1247
  }
1132
1248
  try {
1133
1249
  await removeEmptyDirs(imagesDir);
1134
- } catch (e29) {
1250
+ } catch (e30) {
1135
1251
  }
1136
1252
  await saveMeta(meta);
1137
1253
  sendEvent({
@@ -1194,7 +1310,7 @@ async function handleScanStream() {
1194
1310
  allFiles.push({ relativePath: relPath, fullPath });
1195
1311
  }
1196
1312
  }
1197
- } catch (e30) {
1313
+ } catch (e31) {
1198
1314
  }
1199
1315
  }
1200
1316
  const publicDir = _path2.default.join(process.cwd(), "public");
@@ -1254,7 +1370,7 @@ async function handleScanStream() {
1254
1370
  h: metadata.height || 0,
1255
1371
  b: blurhash
1256
1372
  };
1257
- } catch (e31) {
1373
+ } catch (e32) {
1258
1374
  meta[imageKey] = { w: 0, h: 0 };
1259
1375
  }
1260
1376
  }