@gallop.software/studio 2.1.21 → 2.2.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.
@@ -11,7 +11,7 @@
11
11
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
12
12
  }
13
13
  </style>
14
- <script type="module" crossorigin src="/assets/index-5N7NEJ1M.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-BOpIjx06.js"></script>
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -434,6 +434,7 @@ function countFileTypes(folderPrefix, fileEntries, cdnUrls, r2PublicUrl) {
434
434
  let cloudCount = 0;
435
435
  let remoteCount = 0;
436
436
  let localCount = 0;
437
+ let updateCount = 0;
437
438
  for (const [key, entry] of fileEntries) {
438
439
  if (key.startsWith(folderPrefix)) {
439
440
  if (entry.c !== void 0) {
@@ -446,9 +447,12 @@ function countFileTypes(folderPrefix, fileEntries, cdnUrls, r2PublicUrl) {
446
447
  } else {
447
448
  localCount++;
448
449
  }
450
+ if (entry.u === 1) {
451
+ updateCount++;
452
+ }
449
453
  }
450
454
  }
451
- return { cloudCount, remoteCount, localCount };
455
+ return { cloudCount, remoteCount, localCount, updateCount };
452
456
  }
453
457
  async function handleList(request) {
454
458
  const searchParams = new URL(request.url).searchParams;
@@ -611,6 +615,7 @@ async function handleList(request) {
611
615
  let cloudCount = 0;
612
616
  let remoteCount = 0;
613
617
  let localCount = 0;
618
+ let updateCount = 0;
614
619
  if (isImagesFolder) {
615
620
  for (const [key, metaEntry] of fileEntries) {
616
621
  if (isProcessed(metaEntry)) {
@@ -637,6 +642,7 @@ async function handleList(request) {
637
642
  cloudCount = counts.cloudCount;
638
643
  remoteCount = counts.remoteCount;
639
644
  localCount = counts.localCount;
645
+ updateCount = counts.updateCount;
640
646
  }
641
647
  items.push({
642
648
  name: entry.name,
@@ -646,6 +652,7 @@ async function handleList(request) {
646
652
  cloudCount,
647
653
  remoteCount,
648
654
  localCount,
655
+ updateCount,
649
656
  isProtected: isImagesFolder
650
657
  });
651
658
  }
@@ -714,6 +721,7 @@ async function handleList(request) {
714
721
  cloudCount: counts.cloudCount,
715
722
  remoteCount: counts.remoteCount,
716
723
  localCount: counts.localCount,
724
+ updateCount: counts.updateCount,
717
725
  isProtected: isInsideImagesFolder
718
726
  });
719
727
  }
@@ -776,7 +784,8 @@ async function handleList(request) {
776
784
  cdnBaseUrl: fileCdnUrl,
777
785
  isRemote,
778
786
  isProtected: isInsideImagesFolder,
779
- dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0
787
+ dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0,
788
+ hasUpdate: entry.u === 1
780
789
  });
781
790
  }
782
791
  }
@@ -848,7 +857,8 @@ async function handleSearch(request) {
848
857
  cdnPushed: isPushedToCloud,
849
858
  cdnBaseUrl: fileCdnUrl,
850
859
  isRemote,
851
- dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0
860
+ dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0,
861
+ hasUpdate: entry.u === 1
852
862
  });
853
863
  }
854
864
  return jsonResponse({ items });
@@ -1957,6 +1967,179 @@ async function handleDownloadStream(request) {
1957
1967
  }
1958
1968
  });
1959
1969
  }
1970
+ async function handlePushUpdatesStream(request) {
1971
+ const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
1972
+ const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
1973
+ const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
1974
+ const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
1975
+ const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "");
1976
+ const encoder = new TextEncoder();
1977
+ const stream = new ReadableStream({
1978
+ async start(controller) {
1979
+ const sendEvent = (data) => {
1980
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
1981
+
1982
+ `));
1983
+ };
1984
+ try {
1985
+ if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
1986
+ sendEvent({ type: "error", message: "R2 not configured" });
1987
+ controller.close();
1988
+ return;
1989
+ }
1990
+ const { paths } = await request.json();
1991
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
1992
+ sendEvent({ type: "error", message: "No paths provided" });
1993
+ controller.close();
1994
+ return;
1995
+ }
1996
+ const s3 = new S3Client2({
1997
+ region: "auto",
1998
+ endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
1999
+ credentials: { accessKeyId, secretAccessKey }
2000
+ });
2001
+ const meta = await loadMeta();
2002
+ const cdnUrls = getCdnUrls(meta);
2003
+ const r2PublicUrl = publicUrl.replace(/\/$/, "");
2004
+ const pushed = [];
2005
+ const skipped = [];
2006
+ const errors = [];
2007
+ const urlsToPurge = [];
2008
+ const total = paths.length;
2009
+ sendEvent({ type: "start", total });
2010
+ for (let i = 0; i < paths.length; i++) {
2011
+ const itemPath = paths[i];
2012
+ const key = itemPath.startsWith("public/") ? "/" + itemPath.slice(7) : itemPath;
2013
+ const entry = meta[key];
2014
+ sendEvent({
2015
+ type: "progress",
2016
+ current: i + 1,
2017
+ total,
2018
+ percent: Math.round((i + 1) / total * 100),
2019
+ currentFile: path7.basename(key)
2020
+ });
2021
+ if (!entry || entry.u !== 1) {
2022
+ skipped.push(key);
2023
+ continue;
2024
+ }
2025
+ const fileCdnUrl = entry.c !== void 0 ? cdnUrls[entry.c]?.replace(/\/$/, "") : void 0;
2026
+ if (!fileCdnUrl || fileCdnUrl !== r2PublicUrl) {
2027
+ skipped.push(key);
2028
+ continue;
2029
+ }
2030
+ try {
2031
+ const localPath = getPublicPath(key);
2032
+ const buffer = await fs7.readFile(localPath);
2033
+ const contentType = getContentType(path7.basename(key));
2034
+ const uploadKey = key.startsWith("/") ? key.slice(1) : key;
2035
+ await s3.send(new PutObjectCommand2({
2036
+ Bucket: bucketName,
2037
+ Key: uploadKey,
2038
+ Body: buffer,
2039
+ ContentType: contentType
2040
+ }));
2041
+ if (isProcessed(entry)) {
2042
+ const processedEntry = await processImage(buffer, key);
2043
+ Object.assign(entry, processedEntry);
2044
+ await uploadToCdn(key);
2045
+ await deleteLocalThumbnails(key);
2046
+ for (const thumbPath of getAllThumbnailPaths(key)) {
2047
+ urlsToPurge.push(`${publicUrl}${thumbPath}`);
2048
+ }
2049
+ }
2050
+ await fs7.unlink(localPath);
2051
+ delete entry.u;
2052
+ urlsToPurge.push(`${publicUrl}${key}`);
2053
+ pushed.push(key);
2054
+ } catch (error) {
2055
+ console.error(`Failed to push update for ${key}:`, error);
2056
+ errors.push(key);
2057
+ }
2058
+ }
2059
+ sendEvent({ type: "cleanup", message: "Cleaning up..." });
2060
+ for (const itemPath of pushed) {
2061
+ const localPath = getPublicPath(itemPath);
2062
+ await deleteEmptyFolders(path7.dirname(localPath));
2063
+ }
2064
+ await saveMeta(meta);
2065
+ if (urlsToPurge.length > 0) {
2066
+ sendEvent({ type: "cleanup", message: "Purging CDN cache..." });
2067
+ await purgeCloudflareCache(urlsToPurge);
2068
+ }
2069
+ let message = `Pushed ${pushed.length} update${pushed.length !== 1 ? "s" : ""} to cloud.`;
2070
+ if (skipped.length > 0) {
2071
+ message += ` ${skipped.length} file${skipped.length !== 1 ? "s" : ""} skipped.`;
2072
+ }
2073
+ if (errors.length > 0) {
2074
+ message += ` ${errors.length} file${errors.length !== 1 ? "s" : ""} failed.`;
2075
+ }
2076
+ sendEvent({
2077
+ type: "complete",
2078
+ pushed: pushed.length,
2079
+ skipped: skipped.length,
2080
+ errors: errors.length,
2081
+ message
2082
+ });
2083
+ } catch (error) {
2084
+ console.error("Push updates error:", error);
2085
+ sendEvent({ type: "error", message: "Failed to push updates" });
2086
+ } finally {
2087
+ controller.close();
2088
+ }
2089
+ }
2090
+ });
2091
+ return new Response(stream, {
2092
+ headers: {
2093
+ "Content-Type": "text/event-stream",
2094
+ "Cache-Control": "no-cache",
2095
+ "Connection": "keep-alive"
2096
+ }
2097
+ });
2098
+ }
2099
+ async function handleCancelUpdates(request) {
2100
+ try {
2101
+ const { paths } = await request.json();
2102
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
2103
+ return jsonResponse({ error: "No paths provided" }, { status: 400 });
2104
+ }
2105
+ const meta = await loadMeta();
2106
+ const cancelled = [];
2107
+ const skipped = [];
2108
+ const errors = [];
2109
+ const foldersToClean = /* @__PURE__ */ new Set();
2110
+ for (const itemPath of paths) {
2111
+ const key = itemPath.startsWith("public/") ? "/" + itemPath.slice(7) : itemPath;
2112
+ const entry = meta[key];
2113
+ if (!entry || entry.u !== 1) {
2114
+ skipped.push(key);
2115
+ continue;
2116
+ }
2117
+ try {
2118
+ const localPath = getPublicPath(key);
2119
+ await fs7.unlink(localPath);
2120
+ foldersToClean.add(path7.dirname(localPath));
2121
+ delete entry.u;
2122
+ cancelled.push(key);
2123
+ } catch (error) {
2124
+ console.error(`Failed to cancel update for ${key}:`, error);
2125
+ errors.push(key);
2126
+ }
2127
+ }
2128
+ for (const folder of foldersToClean) {
2129
+ await deleteEmptyFolders(folder);
2130
+ }
2131
+ await saveMeta(meta);
2132
+ return jsonResponse({
2133
+ success: true,
2134
+ cancelled: cancelled.length,
2135
+ skipped: skipped.length,
2136
+ errors: errors.length
2137
+ });
2138
+ } catch (error) {
2139
+ console.error("Cancel updates error:", error);
2140
+ return jsonResponse({ error: "Failed to cancel updates" }, { status: 500 });
2141
+ }
2142
+ }
1960
2143
 
1961
2144
  // src/handlers/scan.ts
1962
2145
  import { promises as fs8 } from "fs";
@@ -1980,6 +2163,7 @@ async function handleScanStream() {
1980
2163
  const renamed = [];
1981
2164
  const errors = [];
1982
2165
  const orphanedFiles = [];
2166
+ const pendingUpdates = [];
1983
2167
  const allFiles = [];
1984
2168
  async function scanDir(dir, relativePath = "") {
1985
2169
  try {
@@ -2013,6 +2197,11 @@ async function handleScanStream() {
2013
2197
  currentFile: relativePath
2014
2198
  });
2015
2199
  if (existingKeys.has(imageKey)) {
2200
+ const entry = meta[imageKey];
2201
+ if (entry?.c !== void 0 && !entry?.u) {
2202
+ entry.u = 1;
2203
+ pendingUpdates.push(imageKey);
2204
+ }
2016
2205
  continue;
2017
2206
  }
2018
2207
  if (meta[imageKey]) {
@@ -2122,6 +2311,36 @@ async function handleScanStream() {
2122
2311
  await cleanupEmptyFoldersRecursive(imagesDir);
2123
2312
  } catch {
2124
2313
  }
2314
+ sendEvent({ type: "cleanup", message: "Checking for orphaned entries..." });
2315
+ const orphanedEntries = [];
2316
+ const cdnUrls = meta._cdns || [];
2317
+ const r2PublicUrl = (process.env.CLOUDFLARE_R2_PUBLIC_URL || "").replace(/\/$/, "");
2318
+ for (const key of Object.keys(meta)) {
2319
+ if (key.startsWith("_")) continue;
2320
+ const entry = meta[key];
2321
+ if (!entry) continue;
2322
+ if (entry.c !== void 0) {
2323
+ if (entry.u === 1) {
2324
+ const localPath2 = getPublicPath(key);
2325
+ try {
2326
+ await fs8.access(localPath2);
2327
+ } catch {
2328
+ delete entry.u;
2329
+ }
2330
+ }
2331
+ continue;
2332
+ }
2333
+ const localPath = getPublicPath(key);
2334
+ try {
2335
+ await fs8.access(localPath);
2336
+ } catch {
2337
+ orphanedEntries.push(key);
2338
+ delete meta[key];
2339
+ }
2340
+ }
2341
+ if (orphanedEntries.length > 0) {
2342
+ sendEvent({ type: "cleanup", message: `Removed ${orphanedEntries.length} orphaned entries...` });
2343
+ }
2125
2344
  await saveMeta(meta);
2126
2345
  sendEvent({
2127
2346
  type: "complete",
@@ -2130,7 +2349,9 @@ async function handleScanStream() {
2130
2349
  renamed: renamed.length,
2131
2350
  errors: errors.length,
2132
2351
  renamedFiles: renamed,
2133
- orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0
2352
+ orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0,
2353
+ pendingUpdates: pendingUpdates.length,
2354
+ orphanedEntries: orphanedEntries.length
2134
2355
  });
2135
2356
  } catch (error) {
2136
2357
  console.error("Scan failed:", error);
@@ -2489,6 +2710,8 @@ async function startServer(options) {
2489
2710
  app.post("/api/studio/reprocess-stream", wrapHandler(handleReprocessStream, true));
2490
2711
  app.post("/api/studio/unprocess-stream", wrapHandler(handleUnprocessStream, true));
2491
2712
  app.post("/api/studio/download-stream", wrapHandler(handleDownloadStream, true));
2713
+ app.post("/api/studio/push-updates-stream", wrapHandler(handlePushUpdatesStream, true));
2714
+ app.post("/api/studio/cancel-updates", wrapHandler(handleCancelUpdates));
2492
2715
  app.post("/api/studio/scan", wrapHandler(handleScanStream, true));
2493
2716
  app.post("/api/studio/delete-orphans", wrapHandler(handleDeleteOrphans));
2494
2717
  app.post("/api/studio/import", wrapHandler(handleImportUrls, true));