@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.
package/dist/client/index.html
CHANGED
|
@@ -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-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DoxGS88r.js"></script>
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/dist/server/index.js
CHANGED
|
@@ -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));
|