@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.
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);
|
|
@@ -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(
|
|
2465
|
-
|
|
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",
|
|
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) => {
|