@gallop.software/studio 1.3.6 → 1.4.1

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.
@@ -309,6 +309,22 @@ async function deleteFromCdn(imageKey, hasThumbnails) {
309
309
  }
310
310
  }
311
311
  }
312
+ async function deleteThumbnailsFromCdn(imageKey) {
313
+ const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
314
+ if (!bucketName) throw new Error("R2 bucket not configured");
315
+ const r2 = getR2Client();
316
+ for (const thumbPath of _chunkWOHZ4LYGjs.getAllThumbnailPaths.call(void 0, imageKey)) {
317
+ try {
318
+ await r2.send(
319
+ new (0, _clients3.DeleteObjectCommand)({
320
+ Bucket: bucketName,
321
+ Key: thumbPath.replace(/^\//, "")
322
+ })
323
+ );
324
+ } catch (e6) {
325
+ }
326
+ }
327
+ }
312
328
 
313
329
  // src/handlers/list.ts
314
330
  function getExistingThumbnails(originalPath, entry) {
@@ -468,7 +484,7 @@ async function handleList(request) {
468
484
  }
469
485
  }
470
486
  }
471
- } catch (e6) {
487
+ } catch (e7) {
472
488
  }
473
489
  if (!relativePath && !seenFolders.has("images")) {
474
490
  let thumbnailCount = 0;
@@ -537,7 +553,7 @@ async function handleList(request) {
537
553
  await _fs.promises.access(localThumbPath);
538
554
  thumbnail = thumbPath;
539
555
  hasThumbnail = true;
540
- } catch (e7) {
556
+ } catch (e8) {
541
557
  thumbnail = key;
542
558
  hasThumbnail = false;
543
559
  }
@@ -556,7 +572,7 @@ async function handleList(request) {
556
572
  const filePath = _path2.default.join(process.cwd(), "public", key);
557
573
  const stats = await _fs.promises.stat(filePath);
558
574
  fileSize = stats.size;
559
- } catch (e8) {
575
+ } catch (e9) {
560
576
  }
561
577
  }
562
578
  items.push({
@@ -618,7 +634,7 @@ async function handleSearch(request) {
618
634
  await _fs.promises.access(localThumbPath);
619
635
  thumbnail = thumbPath;
620
636
  hasThumbnail = true;
621
- } catch (e9) {
637
+ } catch (e10) {
622
638
  thumbnail = key;
623
639
  hasThumbnail = false;
624
640
  }
@@ -674,7 +690,7 @@ async function handleListFolders() {
674
690
  await scanDir(_path2.default.join(dir, entry.name), folderRelPath);
675
691
  }
676
692
  }
677
- } catch (e10) {
693
+ } catch (e11) {
678
694
  }
679
695
  }
680
696
  const publicDir = _path2.default.join(process.cwd(), "public");
@@ -814,7 +830,7 @@ async function handleUpload(request) {
814
830
  meta[imageKey] = {
815
831
  o: { w: metadata.width || 0, h: metadata.height || 0 }
816
832
  };
817
- } catch (e11) {
833
+ } catch (e12) {
818
834
  meta[imageKey] = { o: { w: 0, h: 0 } };
819
835
  }
820
836
  } else {
@@ -864,7 +880,7 @@ async function handleDelete(request) {
864
880
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
865
881
  try {
866
882
  await _fs.promises.unlink(absoluteThumbPath);
867
- } catch (e12) {
883
+ } catch (e13) {
868
884
  }
869
885
  }
870
886
  }
@@ -880,14 +896,14 @@ async function handleDelete(request) {
880
896
  const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
881
897
  try {
882
898
  await _fs.promises.unlink(absoluteThumbPath);
883
- } catch (e13) {
899
+ } catch (e14) {
884
900
  }
885
901
  }
886
902
  }
887
903
  delete meta[imageKey];
888
904
  }
889
905
  }
890
- } catch (e14) {
906
+ } catch (e15) {
891
907
  if (entry) {
892
908
  delete meta[imageKey];
893
909
  } else {
@@ -940,7 +956,7 @@ async function handleCreateFolder(request) {
940
956
  try {
941
957
  await _fs.promises.access(folderPath);
942
958
  return _server.NextResponse.json({ error: "A folder with this name already exists" }, { status: 400 });
943
- } catch (e15) {
959
+ } catch (e16) {
944
960
  }
945
961
  await _fs.promises.mkdir(folderPath, { recursive: true });
946
962
  return _server.NextResponse.json({ success: true, path: _path2.default.join(safePath, sanitizedName) });
@@ -968,13 +984,13 @@ async function handleRename(request) {
968
984
  }
969
985
  try {
970
986
  await _fs.promises.access(absoluteOldPath);
971
- } catch (e16) {
987
+ } catch (e17) {
972
988
  return _server.NextResponse.json({ error: "File or folder not found" }, { status: 404 });
973
989
  }
974
990
  try {
975
991
  await _fs.promises.access(absoluteNewPath);
976
992
  return _server.NextResponse.json({ error: "An item with this name already exists" }, { status: 400 });
977
- } catch (e17) {
993
+ } catch (e18) {
978
994
  }
979
995
  const stats = await _fs.promises.stat(absoluteOldPath);
980
996
  const isFile = stats.isFile();
@@ -996,7 +1012,7 @@ async function handleRename(request) {
996
1012
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
997
1013
  try {
998
1014
  await _fs.promises.rename(oldThumbPath, newThumbPath);
999
- } catch (e18) {
1015
+ } catch (e19) {
1000
1016
  }
1001
1017
  }
1002
1018
  delete meta[oldKey];
@@ -1106,7 +1122,7 @@ async function handleMoveStream(request) {
1106
1122
  await deleteFromCdn(oldKey, hasProcessedThumbnails);
1107
1123
  try {
1108
1124
  await _fs.promises.unlink(newAbsolutePath);
1109
- } catch (e19) {
1125
+ } catch (e20) {
1110
1126
  }
1111
1127
  if (hasProcessedThumbnails) {
1112
1128
  await deleteLocalThumbnails(newKey);
@@ -1123,7 +1139,7 @@ async function handleMoveStream(request) {
1123
1139
  }
1124
1140
  try {
1125
1141
  await _fs.promises.access(absolutePath);
1126
- } catch (e20) {
1142
+ } catch (e21) {
1127
1143
  errors.push(`${itemName} not found`);
1128
1144
  continue;
1129
1145
  }
@@ -1131,7 +1147,7 @@ async function handleMoveStream(request) {
1131
1147
  await _fs.promises.access(newAbsolutePath);
1132
1148
  errors.push(`${itemName} already exists in destination`);
1133
1149
  continue;
1134
- } catch (e21) {
1150
+ } catch (e22) {
1135
1151
  }
1136
1152
  await _fs.promises.rename(absolutePath, newAbsolutePath);
1137
1153
  const stats = await _fs.promises.stat(newAbsolutePath);
@@ -1144,7 +1160,7 @@ async function handleMoveStream(request) {
1144
1160
  await _fs.promises.mkdir(_path2.default.dirname(newThumbPath), { recursive: true });
1145
1161
  try {
1146
1162
  await _fs.promises.rename(oldThumbPath, newThumbPath);
1147
- } catch (e22) {
1163
+ } catch (e23) {
1148
1164
  }
1149
1165
  }
1150
1166
  delete meta[oldKey];
@@ -1249,7 +1265,7 @@ async function handleSync(request) {
1249
1265
  const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
1250
1266
  try {
1251
1267
  originalBuffer = await _fs.promises.readFile(originalLocalPath);
1252
- } catch (e23) {
1268
+ } catch (e24) {
1253
1269
  errors.push(`Original file not found: ${imageKey}`);
1254
1270
  continue;
1255
1271
  }
@@ -1277,7 +1293,7 @@ async function handleSync(request) {
1277
1293
  })
1278
1294
  );
1279
1295
  urlsToPurge.push(`${publicUrl}${thumbPath}`);
1280
- } catch (e24) {
1296
+ } catch (e25) {
1281
1297
  }
1282
1298
  }
1283
1299
  }
@@ -1288,12 +1304,12 @@ async function handleSync(request) {
1288
1304
  const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1289
1305
  try {
1290
1306
  await _fs.promises.unlink(localPath);
1291
- } catch (e25) {
1307
+ } catch (e26) {
1292
1308
  }
1293
1309
  }
1294
1310
  try {
1295
1311
  await _fs.promises.unlink(originalLocalPath);
1296
- } catch (e26) {
1312
+ } catch (e27) {
1297
1313
  }
1298
1314
  }
1299
1315
  pushed.push(imageKey);
@@ -1342,7 +1358,7 @@ async function handleReprocess(request) {
1342
1358
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
1343
1359
  try {
1344
1360
  buffer = await _fs.promises.readFile(originalPath);
1345
- } catch (e27) {
1361
+ } catch (e28) {
1346
1362
  if (isInOurR2) {
1347
1363
  buffer = await downloadFromCdn(imageKey);
1348
1364
  const dir = _path2.default.dirname(originalPath);
@@ -1368,7 +1384,7 @@ async function handleReprocess(request) {
1368
1384
  await deleteLocalThumbnails(imageKey);
1369
1385
  try {
1370
1386
  await _fs.promises.unlink(originalPath);
1371
- } catch (e28) {
1387
+ } catch (e29) {
1372
1388
  }
1373
1389
  } else if (isRemote) {
1374
1390
  }
@@ -1393,7 +1409,7 @@ async function handleReprocess(request) {
1393
1409
  return _server.NextResponse.json({ error: "Failed to reprocess images" }, { status: 500 });
1394
1410
  }
1395
1411
  }
1396
- async function handleReprocessStream(request) {
1412
+ async function handleUnprocessStream(request) {
1397
1413
  const publicUrl = _optionalChain([process, 'access', _38 => _38.env, 'access', _39 => _39.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _40 => _40.replace, 'call', _41 => _41(/\/\s*$/, "")]);
1398
1414
  const encoder = new TextEncoder();
1399
1415
  let imageKeys;
@@ -1403,7 +1419,102 @@ async function handleReprocessStream(request) {
1403
1419
  if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1404
1420
  return _server.NextResponse.json({ error: "No image keys provided" }, { status: 400 });
1405
1421
  }
1406
- } catch (e29) {
1422
+ } catch (e30) {
1423
+ return _server.NextResponse.json({ error: "Invalid request body" }, { status: 400 });
1424
+ }
1425
+ const stream = new ReadableStream({
1426
+ async start(controller) {
1427
+ const sendEvent = (data) => {
1428
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
1429
+
1430
+ `));
1431
+ };
1432
+ try {
1433
+ const meta = await loadMeta();
1434
+ const cdnUrls = getCdnUrls(meta);
1435
+ const unprocessed = [];
1436
+ const errors = [];
1437
+ const urlsToPurge = [];
1438
+ const total = imageKeys.length;
1439
+ sendEvent({ type: "start", total });
1440
+ for (let i = 0; i < imageKeys.length; i++) {
1441
+ let imageKey = imageKeys[i];
1442
+ if (!imageKey.startsWith("/")) {
1443
+ imageKey = `/${imageKey}`;
1444
+ }
1445
+ sendEvent({
1446
+ type: "progress",
1447
+ current: i + 1,
1448
+ total,
1449
+ percent: Math.round((i + 1) / total * 100),
1450
+ message: `Removing thumbnails for ${imageKey.slice(1)}...`
1451
+ });
1452
+ try {
1453
+ const entry = getMetaEntry(meta, imageKey);
1454
+ if (!entry) {
1455
+ errors.push(imageKey);
1456
+ continue;
1457
+ }
1458
+ const existingCdnIndex = entry.c;
1459
+ const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1460
+ const isInOurR2 = existingCdnUrl === publicUrl;
1461
+ await deleteLocalThumbnails(imageKey);
1462
+ if (isInOurR2) {
1463
+ await deleteThumbnailsFromCdn(imageKey);
1464
+ for (const thumbPath of _chunkWOHZ4LYGjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1465
+ urlsToPurge.push(`${publicUrl}${thumbPath}`);
1466
+ }
1467
+ }
1468
+ meta[imageKey] = {
1469
+ o: entry.o,
1470
+ b: entry.b,
1471
+ ...entry.c !== void 0 ? { c: entry.c } : {}
1472
+ };
1473
+ unprocessed.push(imageKey);
1474
+ } catch (error) {
1475
+ console.error(`Failed to unprocess ${imageKey}:`, error);
1476
+ errors.push(imageKey);
1477
+ }
1478
+ }
1479
+ sendEvent({ type: "cleanup", message: "Saving metadata..." });
1480
+ await saveMeta(meta);
1481
+ if (urlsToPurge.length > 0) {
1482
+ sendEvent({ type: "cleanup", message: "Purging CDN cache..." });
1483
+ await purgeCloudflareCache(urlsToPurge);
1484
+ }
1485
+ sendEvent({
1486
+ type: "complete",
1487
+ processed: unprocessed.length,
1488
+ errors: errors.length,
1489
+ message: `Removed thumbnails from ${unprocessed.length} image${unprocessed.length !== 1 ? "s" : ""}${errors.length > 0 ? `, ${errors.length} error${errors.length !== 1 ? "s" : ""}` : ""}`
1490
+ });
1491
+ controller.close();
1492
+ } catch (error) {
1493
+ console.error("Unprocess stream error:", error);
1494
+ sendEvent({ type: "error", message: "Failed to remove thumbnails" });
1495
+ controller.close();
1496
+ }
1497
+ }
1498
+ });
1499
+ return new Response(stream, {
1500
+ headers: {
1501
+ "Content-Type": "text/event-stream",
1502
+ "Cache-Control": "no-cache",
1503
+ Connection: "keep-alive"
1504
+ }
1505
+ });
1506
+ }
1507
+ async function handleReprocessStream(request) {
1508
+ const publicUrl = _optionalChain([process, 'access', _42 => _42.env, 'access', _43 => _43.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _44 => _44.replace, 'call', _45 => _45(/\/\s*$/, "")]);
1509
+ const encoder = new TextEncoder();
1510
+ let imageKeys;
1511
+ try {
1512
+ const body = await request.json();
1513
+ imageKeys = body.imageKeys;
1514
+ if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1515
+ return _server.NextResponse.json({ error: "No image keys provided" }, { status: 400 });
1516
+ }
1517
+ } catch (e31) {
1407
1518
  return _server.NextResponse.json({ error: "Invalid request body" }, { status: 400 });
1408
1519
  }
1409
1520
  const stream = new ReadableStream({
@@ -1436,14 +1547,14 @@ async function handleReprocessStream(request) {
1436
1547
  try {
1437
1548
  let buffer;
1438
1549
  const entry = getMetaEntry(meta, imageKey);
1439
- const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _42 => _42.c]);
1550
+ const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _46 => _46.c]);
1440
1551
  const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1441
1552
  const isInOurR2 = existingCdnUrl === publicUrl;
1442
1553
  const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1443
1554
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
1444
1555
  try {
1445
1556
  buffer = await _fs.promises.readFile(originalPath);
1446
- } catch (e30) {
1557
+ } catch (e32) {
1447
1558
  if (isInOurR2) {
1448
1559
  buffer = await downloadFromCdn(imageKey);
1449
1560
  const dir = _path2.default.dirname(originalPath);
@@ -1488,7 +1599,7 @@ async function handleReprocessStream(request) {
1488
1599
  await deleteLocalThumbnails(imageKey);
1489
1600
  try {
1490
1601
  await _fs.promises.unlink(originalPath);
1491
- } catch (e31) {
1602
+ } catch (e33) {
1492
1603
  }
1493
1604
  }
1494
1605
  meta[imageKey] = updatedEntry;
@@ -1528,7 +1639,7 @@ async function handleReprocessStream(request) {
1528
1639
  });
1529
1640
  }
1530
1641
  async function handleProcessAllStream() {
1531
- const publicUrl = _optionalChain([process, 'access', _43 => _43.env, 'access', _44 => _44.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _45 => _45.replace, 'call', _46 => _46(/\/\s*$/, "")]);
1642
+ const publicUrl = _optionalChain([process, 'access', _47 => _47.env, 'access', _48 => _48.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _49 => _49.replace, 'call', _50 => _50(/\/\s*$/, "")]);
1532
1643
  const encoder = new TextEncoder();
1533
1644
  const stream = new ReadableStream({
1534
1645
  async start(controller) {
@@ -1622,7 +1733,7 @@ async function handleProcessAllStream() {
1622
1733
  await deleteLocalThumbnails(key);
1623
1734
  try {
1624
1735
  await _fs.promises.unlink(fullPath);
1625
- } catch (e32) {
1736
+ } catch (e34) {
1626
1737
  }
1627
1738
  }
1628
1739
  processed.push(key.slice(1));
@@ -1661,13 +1772,13 @@ async function handleProcessAllStream() {
1661
1772
  }
1662
1773
  }
1663
1774
  }
1664
- } catch (e33) {
1775
+ } catch (e35) {
1665
1776
  }
1666
1777
  }
1667
1778
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1668
1779
  try {
1669
1780
  await findOrphans(imagesDir);
1670
- } catch (e34) {
1781
+ } catch (e36) {
1671
1782
  }
1672
1783
  async function removeEmptyDirs(dir) {
1673
1784
  try {
@@ -1685,13 +1796,13 @@ async function handleProcessAllStream() {
1685
1796
  await _fs.promises.rmdir(dir);
1686
1797
  }
1687
1798
  return isEmpty;
1688
- } catch (e35) {
1799
+ } catch (e37) {
1689
1800
  return true;
1690
1801
  }
1691
1802
  }
1692
1803
  try {
1693
1804
  await removeEmptyDirs(imagesDir);
1694
- } catch (e36) {
1805
+ } catch (e38) {
1695
1806
  }
1696
1807
  await saveMeta(meta);
1697
1808
  if (urlsToPurge.length > 0) {
@@ -1759,7 +1870,7 @@ async function handleScanStream() {
1759
1870
  allFiles.push({ relativePath: relPath, fullPath });
1760
1871
  }
1761
1872
  }
1762
- } catch (e37) {
1873
+ } catch (e39) {
1763
1874
  }
1764
1875
  }
1765
1876
  const publicDir = _path2.default.join(process.cwd(), "public");
@@ -1818,7 +1929,7 @@ async function handleScanStream() {
1818
1929
  o: { w: metadata.width || 0, h: metadata.height || 0 },
1819
1930
  b: blurhash
1820
1931
  };
1821
- } catch (e38) {
1932
+ } catch (e40) {
1822
1933
  meta[imageKey] = { o: { w: 0, h: 0 } };
1823
1934
  }
1824
1935
  }
@@ -1858,13 +1969,13 @@ async function handleScanStream() {
1858
1969
  }
1859
1970
  }
1860
1971
  }
1861
- } catch (e39) {
1972
+ } catch (e41) {
1862
1973
  }
1863
1974
  }
1864
1975
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1865
1976
  try {
1866
1977
  await findOrphans(imagesDir);
1867
- } catch (e40) {
1978
+ } catch (e42) {
1868
1979
  }
1869
1980
  await saveMeta(meta);
1870
1981
  sendEvent({
@@ -1931,13 +2042,13 @@ async function handleDeleteOrphans(request) {
1931
2042
  await _fs.promises.rmdir(dir);
1932
2043
  }
1933
2044
  return isEmpty;
1934
- } catch (e41) {
2045
+ } catch (e43) {
1935
2046
  return true;
1936
2047
  }
1937
2048
  }
1938
2049
  try {
1939
2050
  await removeEmptyDirs(imagesDir);
1940
- } catch (e42) {
2051
+ } catch (e44) {
1941
2052
  }
1942
2053
  return _server.NextResponse.json({
1943
2054
  success: true,
@@ -2122,6 +2233,9 @@ async function POST(request) {
2122
2233
  if (route === "reprocess-stream") {
2123
2234
  return handleReprocessStream(request);
2124
2235
  }
2236
+ if (route === "unprocess-stream") {
2237
+ return handleUnprocessStream(request);
2238
+ }
2125
2239
  if (route === "process-all") {
2126
2240
  return handleProcessAllStream();
2127
2241
  }