@gallop.software/studio 2.1.20 → 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);
@@ -2461,15 +2651,27 @@ async function startServer(options) {
2461
2651
  } else if (existsSync(envPath)) {
2462
2652
  loadEnv({ path: envPath, quiet: true });
2463
2653
  }
2464
- app.use(express.json({ limit: "50mb" }));
2465
- app.use(express.urlencoded({ extended: true, limit: "50mb" }));
2654
+ app.use((req, res, next) => {
2655
+ if (req.path === "/api/studio/upload") {
2656
+ next();
2657
+ } else {
2658
+ express.json({ limit: "50mb" })(req, res, next);
2659
+ }
2660
+ });
2661
+ app.use((req, res, next) => {
2662
+ if (req.path === "/api/studio/upload") {
2663
+ next();
2664
+ } else {
2665
+ express.urlencoded({ extended: true, limit: "50mb" })(req, res, next);
2666
+ }
2667
+ });
2466
2668
  app.get("/api/studio/list", wrapHandler(handleList));
2467
2669
  app.get("/api/studio/list-folders", wrapHandler(handleListFolders));
2468
2670
  app.get("/api/studio/search", wrapHandler(handleSearch));
2469
2671
  app.get("/api/studio/count-images", wrapHandler(handleCountImages));
2470
2672
  app.get("/api/studio/folder-images", wrapHandler(handleFolderImages));
2471
2673
  app.get("/api/studio/cdns", wrapHandler(handleGetCdns));
2472
- app.post("/api/studio/upload", wrapHandler(handleUpload));
2674
+ app.post("/api/studio/upload", wrapRawHandler(handleUpload));
2473
2675
  app.post("/api/studio/create-folder", wrapHandler(handleCreateFolder));
2474
2676
  app.post("/api/studio/rename", wrapHandler(handleRename));
2475
2677
  app.post("/api/studio/move", wrapHandler(handleMoveStream, true));
@@ -2477,6 +2679,8 @@ async function startServer(options) {
2477
2679
  app.post("/api/studio/reprocess-stream", wrapHandler(handleReprocessStream, true));
2478
2680
  app.post("/api/studio/unprocess-stream", wrapHandler(handleUnprocessStream, true));
2479
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));
2480
2684
  app.post("/api/studio/scan", wrapHandler(handleScanStream, true));
2481
2685
  app.post("/api/studio/delete-orphans", wrapHandler(handleDeleteOrphans));
2482
2686
  app.post("/api/studio/import", wrapHandler(handleImportUrls, true));
@@ -2534,6 +2738,18 @@ function wrapHandler(handler, streaming = false) {
2534
2738
  }
2535
2739
  };
2536
2740
  }
2741
+ function wrapRawHandler(handler) {
2742
+ return async (req, res) => {
2743
+ try {
2744
+ const request = await createRawFetchRequest(req);
2745
+ const response = await handler(request);
2746
+ await sendResponse(res, response);
2747
+ } catch (error) {
2748
+ console.error("Handler error:", error);
2749
+ res.status(500).json({ error: "Internal server error" });
2750
+ }
2751
+ };
2752
+ }
2537
2753
  function createFetchRequest(req) {
2538
2754
  const url = new URL(req.url, `http://${req.headers.host}`);
2539
2755
  const headers = new Headers();
@@ -2557,6 +2773,30 @@ function createFetchRequest(req) {
2557
2773
  }
2558
2774
  return new globalThis.Request(url.toString(), init);
2559
2775
  }
2776
+ async function createRawFetchRequest(req) {
2777
+ const url = new URL(req.url, `http://${req.headers.host}`);
2778
+ const headers = new Headers();
2779
+ for (const [key, value] of Object.entries(req.headers)) {
2780
+ if (value) {
2781
+ if (Array.isArray(value)) {
2782
+ value.forEach((v) => headers.append(key, v));
2783
+ } else {
2784
+ headers.set(key, value);
2785
+ }
2786
+ }
2787
+ }
2788
+ const chunks = [];
2789
+ for await (const chunk of req) {
2790
+ chunks.push(Buffer.from(chunk));
2791
+ }
2792
+ const body = Buffer.concat(chunks);
2793
+ const init = {
2794
+ method: req.method,
2795
+ headers,
2796
+ body
2797
+ };
2798
+ return new globalThis.Request(url.toString(), init);
2799
+ }
2560
2800
  async function sendResponse(res, response) {
2561
2801
  res.status(response.status);
2562
2802
  response.headers.forEach((value, key) => {