@gallop.software/studio 2.1.21 → 2.2.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.
@@ -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-DoxGS88r.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]) {
@@ -2130,7 +2319,8 @@ async function handleScanStream() {
2130
2319
  renamed: renamed.length,
2131
2320
  errors: errors.length,
2132
2321
  renamedFiles: renamed,
2133
- orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0
2322
+ orphanedFiles: orphanedFiles.length > 0 ? orphanedFiles : void 0,
2323
+ pendingUpdates: pendingUpdates.length
2134
2324
  });
2135
2325
  } catch (error) {
2136
2326
  console.error("Scan failed:", error);
@@ -2489,6 +2679,8 @@ async function startServer(options) {
2489
2679
  app.post("/api/studio/reprocess-stream", wrapHandler(handleReprocessStream, true));
2490
2680
  app.post("/api/studio/unprocess-stream", wrapHandler(handleUnprocessStream, true));
2491
2681
  app.post("/api/studio/download-stream", wrapHandler(handleDownloadStream, true));
2682
+ app.post("/api/studio/push-updates-stream", wrapHandler(handlePushUpdatesStream, true));
2683
+ app.post("/api/studio/cancel-updates", wrapHandler(handleCancelUpdates));
2492
2684
  app.post("/api/studio/scan", wrapHandler(handleScanStream, true));
2493
2685
  app.post("/api/studio/delete-orphans", wrapHandler(handleDeleteOrphans));
2494
2686
  app.post("/api/studio/import", wrapHandler(handleImportUrls, true));