@gallop.software/studio 1.3.5 → 1.4.0

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({
@@ -1433,18 +1544,17 @@ async function handleReprocessStream(request) {
1433
1544
  percent: Math.round((i + 1) / total * 100),
1434
1545
  message: `Processing ${imageKey.slice(1)}...`
1435
1546
  });
1436
- await new Promise((resolve) => setTimeout(resolve, 1e3));
1437
1547
  try {
1438
1548
  let buffer;
1439
1549
  const entry = getMetaEntry(meta, imageKey);
1440
- const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _42 => _42.c]);
1550
+ const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _46 => _46.c]);
1441
1551
  const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1442
1552
  const isInOurR2 = existingCdnUrl === publicUrl;
1443
1553
  const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1444
1554
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
1445
1555
  try {
1446
1556
  buffer = await _fs.promises.readFile(originalPath);
1447
- } catch (e30) {
1557
+ } catch (e32) {
1448
1558
  if (isInOurR2) {
1449
1559
  buffer = await downloadFromCdn(imageKey);
1450
1560
  const dir = _path2.default.dirname(originalPath);
@@ -1489,7 +1599,7 @@ async function handleReprocessStream(request) {
1489
1599
  await deleteLocalThumbnails(imageKey);
1490
1600
  try {
1491
1601
  await _fs.promises.unlink(originalPath);
1492
- } catch (e31) {
1602
+ } catch (e33) {
1493
1603
  }
1494
1604
  }
1495
1605
  meta[imageKey] = updatedEntry;
@@ -1529,7 +1639,7 @@ async function handleReprocessStream(request) {
1529
1639
  });
1530
1640
  }
1531
1641
  async function handleProcessAllStream() {
1532
- 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*$/, "")]);
1533
1643
  const encoder = new TextEncoder();
1534
1644
  const stream = new ReadableStream({
1535
1645
  async start(controller) {
@@ -1623,7 +1733,7 @@ async function handleProcessAllStream() {
1623
1733
  await deleteLocalThumbnails(key);
1624
1734
  try {
1625
1735
  await _fs.promises.unlink(fullPath);
1626
- } catch (e32) {
1736
+ } catch (e34) {
1627
1737
  }
1628
1738
  }
1629
1739
  processed.push(key.slice(1));
@@ -1662,13 +1772,13 @@ async function handleProcessAllStream() {
1662
1772
  }
1663
1773
  }
1664
1774
  }
1665
- } catch (e33) {
1775
+ } catch (e35) {
1666
1776
  }
1667
1777
  }
1668
1778
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1669
1779
  try {
1670
1780
  await findOrphans(imagesDir);
1671
- } catch (e34) {
1781
+ } catch (e36) {
1672
1782
  }
1673
1783
  async function removeEmptyDirs(dir) {
1674
1784
  try {
@@ -1686,13 +1796,13 @@ async function handleProcessAllStream() {
1686
1796
  await _fs.promises.rmdir(dir);
1687
1797
  }
1688
1798
  return isEmpty;
1689
- } catch (e35) {
1799
+ } catch (e37) {
1690
1800
  return true;
1691
1801
  }
1692
1802
  }
1693
1803
  try {
1694
1804
  await removeEmptyDirs(imagesDir);
1695
- } catch (e36) {
1805
+ } catch (e38) {
1696
1806
  }
1697
1807
  await saveMeta(meta);
1698
1808
  if (urlsToPurge.length > 0) {
@@ -1760,7 +1870,7 @@ async function handleScanStream() {
1760
1870
  allFiles.push({ relativePath: relPath, fullPath });
1761
1871
  }
1762
1872
  }
1763
- } catch (e37) {
1873
+ } catch (e39) {
1764
1874
  }
1765
1875
  }
1766
1876
  const publicDir = _path2.default.join(process.cwd(), "public");
@@ -1819,7 +1929,7 @@ async function handleScanStream() {
1819
1929
  o: { w: metadata.width || 0, h: metadata.height || 0 },
1820
1930
  b: blurhash
1821
1931
  };
1822
- } catch (e38) {
1932
+ } catch (e40) {
1823
1933
  meta[imageKey] = { o: { w: 0, h: 0 } };
1824
1934
  }
1825
1935
  }
@@ -1859,13 +1969,13 @@ async function handleScanStream() {
1859
1969
  }
1860
1970
  }
1861
1971
  }
1862
- } catch (e39) {
1972
+ } catch (e41) {
1863
1973
  }
1864
1974
  }
1865
1975
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1866
1976
  try {
1867
1977
  await findOrphans(imagesDir);
1868
- } catch (e40) {
1978
+ } catch (e42) {
1869
1979
  }
1870
1980
  await saveMeta(meta);
1871
1981
  sendEvent({
@@ -1932,13 +2042,13 @@ async function handleDeleteOrphans(request) {
1932
2042
  await _fs.promises.rmdir(dir);
1933
2043
  }
1934
2044
  return isEmpty;
1935
- } catch (e41) {
2045
+ } catch (e43) {
1936
2046
  return true;
1937
2047
  }
1938
2048
  }
1939
2049
  try {
1940
2050
  await removeEmptyDirs(imagesDir);
1941
- } catch (e42) {
2051
+ } catch (e44) {
1942
2052
  }
1943
2053
  return _server.NextResponse.json({
1944
2054
  success: true,
@@ -2123,6 +2233,9 @@ async function POST(request) {
2123
2233
  if (route === "reprocess-stream") {
2124
2234
  return handleReprocessStream(request);
2125
2235
  }
2236
+ if (route === "unprocess-stream") {
2237
+ return handleUnprocessStream(request);
2238
+ }
2126
2239
  if (route === "process-all") {
2127
2240
  return handleProcessAllStream();
2128
2241
  }