@gallop.software/studio 2.3.85 → 2.3.87
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/server/index.js
CHANGED
|
@@ -148,13 +148,16 @@ var DEFAULT_SIZES = {
|
|
|
148
148
|
large: { width: 1400, suffix: "-lg", key: "lg" }
|
|
149
149
|
};
|
|
150
150
|
async function processImage(buffer, imageKey) {
|
|
151
|
-
const
|
|
152
|
-
const metadata = await
|
|
151
|
+
const rotatedBuffer = await sharp(buffer).rotate().toBuffer();
|
|
152
|
+
const metadata = await sharp(rotatedBuffer).metadata();
|
|
153
153
|
const originalWidth = metadata.width || 0;
|
|
154
154
|
const originalHeight = metadata.height || 0;
|
|
155
155
|
const ratio = originalHeight / originalWidth;
|
|
156
156
|
const keyWithoutSlash = imageKey.startsWith("/") ? imageKey.slice(1) : imageKey;
|
|
157
|
-
const baseName = path3.basename(
|
|
157
|
+
const baseName = path3.basename(
|
|
158
|
+
keyWithoutSlash,
|
|
159
|
+
path3.extname(keyWithoutSlash)
|
|
160
|
+
);
|
|
158
161
|
const ext = path3.extname(keyWithoutSlash).toLowerCase();
|
|
159
162
|
const imageDir = path3.dirname(keyWithoutSlash);
|
|
160
163
|
const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
|
|
@@ -172,15 +175,15 @@ async function processImage(buffer, imageKey) {
|
|
|
172
175
|
fullWidth = FULL_MAX_WIDTH;
|
|
173
176
|
fullHeight = Math.round(FULL_MAX_WIDTH * ratio);
|
|
174
177
|
if (isPng) {
|
|
175
|
-
await sharp(
|
|
178
|
+
await sharp(rotatedBuffer).resize(fullWidth, fullHeight).png({ quality: 85 }).toFile(fullPath);
|
|
176
179
|
} else {
|
|
177
|
-
await sharp(
|
|
180
|
+
await sharp(rotatedBuffer).resize(fullWidth, fullHeight).jpeg({ quality: 85 }).toFile(fullPath);
|
|
178
181
|
}
|
|
179
182
|
} else {
|
|
180
183
|
if (isPng) {
|
|
181
|
-
await sharp(
|
|
184
|
+
await sharp(rotatedBuffer).png({ quality: 85 }).toFile(fullPath);
|
|
182
185
|
} else {
|
|
183
|
-
await sharp(
|
|
186
|
+
await sharp(rotatedBuffer).jpeg({ quality: 85 }).toFile(fullPath);
|
|
184
187
|
}
|
|
185
188
|
}
|
|
186
189
|
entry.f = { w: fullWidth, h: fullHeight };
|
|
@@ -194,9 +197,9 @@ async function processImage(buffer, imageKey) {
|
|
|
194
197
|
const sizeFilePath = imageDir === "." ? sizeFileName : `${imageDir}/${sizeFileName}`;
|
|
195
198
|
const sizePath = getPublicPath("images", sizeFilePath);
|
|
196
199
|
if (isPng) {
|
|
197
|
-
await sharp(
|
|
200
|
+
await sharp(rotatedBuffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
|
|
198
201
|
} else {
|
|
199
|
-
await sharp(
|
|
202
|
+
await sharp(rotatedBuffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
|
|
200
203
|
}
|
|
201
204
|
entry[key] = { w: maxWidth, h: newHeight };
|
|
202
205
|
}
|
|
@@ -1158,7 +1161,11 @@ async function cleanupEmptyFoldersRecursive(dir) {
|
|
|
1158
1161
|
// src/handlers/images.ts
|
|
1159
1162
|
import { promises as fs6 } from "fs";
|
|
1160
1163
|
import path6 from "path";
|
|
1161
|
-
import {
|
|
1164
|
+
import {
|
|
1165
|
+
S3Client as S3Client2,
|
|
1166
|
+
PutObjectCommand as PutObjectCommand2,
|
|
1167
|
+
DeleteObjectCommand as DeleteObjectCommand2
|
|
1168
|
+
} from "@aws-sdk/client-s3";
|
|
1162
1169
|
var cancelledOperations = /* @__PURE__ */ new Set();
|
|
1163
1170
|
function cancelOperation(operationId) {
|
|
1164
1171
|
cancelledOperations.add(operationId);
|
|
@@ -1178,7 +1185,9 @@ async function handleSync(request) {
|
|
|
1178
1185
|
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1179
1186
|
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1180
1187
|
return jsonResponse(
|
|
1181
|
-
{
|
|
1188
|
+
{
|
|
1189
|
+
error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables."
|
|
1190
|
+
},
|
|
1182
1191
|
{ status: 400 }
|
|
1183
1192
|
);
|
|
1184
1193
|
}
|
|
@@ -1303,15 +1312,20 @@ async function handleSyncStream(request) {
|
|
|
1303
1312
|
async start(controller) {
|
|
1304
1313
|
const sendEvent = (data) => {
|
|
1305
1314
|
try {
|
|
1306
|
-
controller.enqueue(
|
|
1315
|
+
controller.enqueue(
|
|
1316
|
+
encoder.encode(`data: ${JSON.stringify(data)}
|
|
1307
1317
|
|
|
1308
|
-
`)
|
|
1318
|
+
`)
|
|
1319
|
+
);
|
|
1309
1320
|
} catch {
|
|
1310
1321
|
}
|
|
1311
1322
|
};
|
|
1312
1323
|
try {
|
|
1313
1324
|
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1314
|
-
sendEvent({
|
|
1325
|
+
sendEvent({
|
|
1326
|
+
type: "error",
|
|
1327
|
+
message: "R2 not configured. Set CLOUDFLARE_R2_* environment variables."
|
|
1328
|
+
});
|
|
1315
1329
|
controller.close();
|
|
1316
1330
|
return;
|
|
1317
1331
|
}
|
|
@@ -1360,7 +1374,9 @@ async function handleSyncStream(request) {
|
|
|
1360
1374
|
}
|
|
1361
1375
|
const entry = getMetaEntry(meta, imageKey);
|
|
1362
1376
|
if (!entry) {
|
|
1363
|
-
errors.push(
|
|
1377
|
+
errors.push(
|
|
1378
|
+
`Image not found in meta: ${imageKey}. Run Scan first.`
|
|
1379
|
+
);
|
|
1364
1380
|
sendEvent({
|
|
1365
1381
|
type: "progress",
|
|
1366
1382
|
current: i + 1,
|
|
@@ -1495,7 +1511,7 @@ async function handleSyncStream(request) {
|
|
|
1495
1511
|
headers: {
|
|
1496
1512
|
"Content-Type": "text/event-stream",
|
|
1497
1513
|
"Cache-Control": "no-cache",
|
|
1498
|
-
|
|
1514
|
+
Connection: "keep-alive"
|
|
1499
1515
|
}
|
|
1500
1516
|
});
|
|
1501
1517
|
}
|
|
@@ -1534,7 +1550,13 @@ async function handleUnprocessStream(request) {
|
|
|
1534
1550
|
if (isCancelled()) {
|
|
1535
1551
|
await saveMeta(meta);
|
|
1536
1552
|
if (operationId) clearCancelledOperation(operationId);
|
|
1537
|
-
sendEvent({
|
|
1553
|
+
sendEvent({
|
|
1554
|
+
type: "complete",
|
|
1555
|
+
processed: removed.length,
|
|
1556
|
+
errors: errors.length,
|
|
1557
|
+
message: `Stopped. Removed thumbnails for ${removed.length} image${removed.length !== 1 ? "s" : ""}.`,
|
|
1558
|
+
cancelled: true
|
|
1559
|
+
});
|
|
1538
1560
|
controller.close();
|
|
1539
1561
|
return;
|
|
1540
1562
|
}
|
|
@@ -1676,7 +1698,13 @@ async function handleReprocessStream(request) {
|
|
|
1676
1698
|
if (isCancelled()) {
|
|
1677
1699
|
await saveMeta(meta);
|
|
1678
1700
|
if (operationId) clearCancelledOperation(operationId);
|
|
1679
|
-
sendEvent({
|
|
1701
|
+
sendEvent({
|
|
1702
|
+
type: "complete",
|
|
1703
|
+
processed: processed.length,
|
|
1704
|
+
errors: errors.length,
|
|
1705
|
+
message: `Stopped. Generated thumbnails for ${processed.length} image${processed.length !== 1 ? "s" : ""}.`,
|
|
1706
|
+
cancelled: true
|
|
1707
|
+
});
|
|
1680
1708
|
controller.close();
|
|
1681
1709
|
return;
|
|
1682
1710
|
}
|
|
@@ -1714,7 +1742,10 @@ async function handleReprocessStream(request) {
|
|
|
1714
1742
|
const isSvg = ext === ".svg";
|
|
1715
1743
|
if (isSvg) {
|
|
1716
1744
|
const imageDir = path6.dirname(imageKey.slice(1));
|
|
1717
|
-
const imagesPath = getPublicPath(
|
|
1745
|
+
const imagesPath = getPublicPath(
|
|
1746
|
+
"images",
|
|
1747
|
+
imageDir === "." ? "" : imageDir
|
|
1748
|
+
);
|
|
1718
1749
|
await fs6.mkdir(imagesPath, { recursive: true });
|
|
1719
1750
|
const fileName = path6.basename(imageKey);
|
|
1720
1751
|
const destPath = path6.join(imagesPath, fileName);
|
|
@@ -1805,9 +1836,11 @@ async function handleDownloadStream(request) {
|
|
|
1805
1836
|
const encoder = new TextEncoder();
|
|
1806
1837
|
const sendEvent = (data) => {
|
|
1807
1838
|
try {
|
|
1808
|
-
controller.enqueue(
|
|
1839
|
+
controller.enqueue(
|
|
1840
|
+
encoder.encode(`data: ${JSON.stringify(data)}
|
|
1809
1841
|
|
|
1810
|
-
`)
|
|
1842
|
+
`)
|
|
1843
|
+
);
|
|
1811
1844
|
} catch {
|
|
1812
1845
|
}
|
|
1813
1846
|
};
|
|
@@ -1920,7 +1953,7 @@ async function handleDownloadStream(request) {
|
|
|
1920
1953
|
headers: {
|
|
1921
1954
|
"Content-Type": "text/event-stream",
|
|
1922
1955
|
"Cache-Control": "no-cache",
|
|
1923
|
-
|
|
1956
|
+
Connection: "keep-alive"
|
|
1924
1957
|
}
|
|
1925
1958
|
});
|
|
1926
1959
|
}
|
|
@@ -1944,8 +1977,8 @@ async function handlePushUpdatesStream(request) {
|
|
|
1944
1977
|
controller.close();
|
|
1945
1978
|
return;
|
|
1946
1979
|
}
|
|
1947
|
-
const { paths, operationId } = await request.json();
|
|
1948
|
-
if (!
|
|
1980
|
+
const { paths: inputPaths, operationId } = await request.json();
|
|
1981
|
+
if (!inputPaths || !Array.isArray(inputPaths) || inputPaths.length === 0) {
|
|
1949
1982
|
sendEvent({ type: "error", message: "No paths provided" });
|
|
1950
1983
|
controller.close();
|
|
1951
1984
|
return;
|
|
@@ -1959,16 +1992,45 @@ async function handlePushUpdatesStream(request) {
|
|
|
1959
1992
|
const meta = await loadMeta();
|
|
1960
1993
|
const cdnUrls = getCdnUrls(meta);
|
|
1961
1994
|
const r2PublicUrl = publicUrl.replace(/\/$/, "");
|
|
1995
|
+
const paths = [];
|
|
1996
|
+
for (const inputPath of inputPaths) {
|
|
1997
|
+
const key = inputPath.startsWith("public/") ? "/" + inputPath.slice(7) : inputPath;
|
|
1998
|
+
const isFolder = !key.match(/\.[a-zA-Z0-9]+$/);
|
|
1999
|
+
if (isFolder) {
|
|
2000
|
+
const folderPrefix = key.endsWith("/") ? key : key + "/";
|
|
2001
|
+
for (const [metaKey, entry] of Object.entries(meta)) {
|
|
2002
|
+
if (metaKey.startsWith(folderPrefix) && entry && typeof entry === "object" && "u" in entry && entry.u === 1) {
|
|
2003
|
+
paths.push(metaKey);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
} else {
|
|
2007
|
+
paths.push(inputPath);
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1962
2010
|
const pushed = [];
|
|
1963
2011
|
const skipped = [];
|
|
1964
2012
|
const errors = [];
|
|
1965
2013
|
const total = paths.length;
|
|
2014
|
+
if (total === 0) {
|
|
2015
|
+
sendEvent({
|
|
2016
|
+
type: "complete",
|
|
2017
|
+
pushed: 0,
|
|
2018
|
+
message: "No files with pending updates found."
|
|
2019
|
+
});
|
|
2020
|
+
controller.close();
|
|
2021
|
+
return;
|
|
2022
|
+
}
|
|
1966
2023
|
sendEvent({ type: "start", total });
|
|
1967
2024
|
for (let i = 0; i < paths.length; i++) {
|
|
1968
2025
|
if (isCancelled()) {
|
|
1969
2026
|
await saveMeta(meta);
|
|
1970
2027
|
if (operationId) clearCancelledOperation(operationId);
|
|
1971
|
-
sendEvent({
|
|
2028
|
+
sendEvent({
|
|
2029
|
+
type: "complete",
|
|
2030
|
+
pushed: pushed.length,
|
|
2031
|
+
message: `Stopped. ${pushed.length} file${pushed.length !== 1 ? "s" : ""} pushed.`,
|
|
2032
|
+
cancelled: true
|
|
2033
|
+
});
|
|
1972
2034
|
controller.close();
|
|
1973
2035
|
return;
|
|
1974
2036
|
}
|
|
@@ -2006,18 +2068,22 @@ async function handlePushUpdatesStream(request) {
|
|
|
2006
2068
|
const contentType = getContentType(path6.basename(key));
|
|
2007
2069
|
const uploadKey = key.startsWith("/") ? key.slice(1) : key;
|
|
2008
2070
|
try {
|
|
2009
|
-
await s3.send(
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2071
|
+
await s3.send(
|
|
2072
|
+
new DeleteObjectCommand2({
|
|
2073
|
+
Bucket: bucketName,
|
|
2074
|
+
Key: uploadKey
|
|
2075
|
+
})
|
|
2076
|
+
);
|
|
2013
2077
|
} catch {
|
|
2014
2078
|
}
|
|
2015
|
-
await s3.send(
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2079
|
+
await s3.send(
|
|
2080
|
+
new PutObjectCommand2({
|
|
2081
|
+
Bucket: bucketName,
|
|
2082
|
+
Key: uploadKey,
|
|
2083
|
+
Body: buffer,
|
|
2084
|
+
ContentType: contentType
|
|
2085
|
+
})
|
|
2086
|
+
);
|
|
2021
2087
|
if (isProcessed(entry)) {
|
|
2022
2088
|
await deleteThumbnailsFromCdn(key);
|
|
2023
2089
|
const processedEntry = await processImage(buffer, key);
|
|
@@ -2083,7 +2149,7 @@ async function handlePushUpdatesStream(request) {
|
|
|
2083
2149
|
headers: {
|
|
2084
2150
|
"Content-Type": "text/event-stream",
|
|
2085
2151
|
"Cache-Control": "no-cache",
|
|
2086
|
-
|
|
2152
|
+
Connection: "keep-alive"
|
|
2087
2153
|
}
|
|
2088
2154
|
});
|
|
2089
2155
|
}
|
|
@@ -2091,22 +2157,43 @@ async function handleCancelStreamOperation(request) {
|
|
|
2091
2157
|
try {
|
|
2092
2158
|
const { operationId } = await request.json();
|
|
2093
2159
|
if (!operationId || typeof operationId !== "string") {
|
|
2094
|
-
return jsonResponse(
|
|
2160
|
+
return jsonResponse(
|
|
2161
|
+
{ error: "No operation ID provided" },
|
|
2162
|
+
{ status: 400 }
|
|
2163
|
+
);
|
|
2095
2164
|
}
|
|
2096
2165
|
cancelOperation(operationId);
|
|
2097
2166
|
return jsonResponse({ success: true, operationId });
|
|
2098
2167
|
} catch (error) {
|
|
2099
2168
|
console.error("Failed to cancel operation:", error);
|
|
2100
|
-
return jsonResponse(
|
|
2169
|
+
return jsonResponse(
|
|
2170
|
+
{ error: "Failed to cancel operation" },
|
|
2171
|
+
{ status: 500 }
|
|
2172
|
+
);
|
|
2101
2173
|
}
|
|
2102
2174
|
}
|
|
2103
2175
|
async function handleCancelUpdates(request) {
|
|
2104
2176
|
try {
|
|
2105
|
-
const { paths } = await request.json();
|
|
2106
|
-
if (!
|
|
2177
|
+
const { paths: inputPaths } = await request.json();
|
|
2178
|
+
if (!inputPaths || !Array.isArray(inputPaths) || inputPaths.length === 0) {
|
|
2107
2179
|
return jsonResponse({ error: "No paths provided" }, { status: 400 });
|
|
2108
2180
|
}
|
|
2109
2181
|
const meta = await loadMeta();
|
|
2182
|
+
const paths = [];
|
|
2183
|
+
for (const inputPath of inputPaths) {
|
|
2184
|
+
const key = inputPath.startsWith("public/") ? "/" + inputPath.slice(7) : inputPath;
|
|
2185
|
+
const isFolder = !key.match(/\.[a-zA-Z0-9]+$/);
|
|
2186
|
+
if (isFolder) {
|
|
2187
|
+
const folderPrefix = key.endsWith("/") ? key : key + "/";
|
|
2188
|
+
for (const [metaKey, entry] of Object.entries(meta)) {
|
|
2189
|
+
if (metaKey.startsWith(folderPrefix) && entry && typeof entry === "object" && "u" in entry && entry.u === 1) {
|
|
2190
|
+
paths.push(metaKey);
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
} else {
|
|
2194
|
+
paths.push(inputPath);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2110
2197
|
const cancelled = [];
|
|
2111
2198
|
const skipped = [];
|
|
2112
2199
|
const errors = [];
|
|
@@ -2169,7 +2256,9 @@ async function handleUpload(request) {
|
|
|
2169
2256
|
}
|
|
2170
2257
|
if (relativeDir === "images" || relativeDir.startsWith("images/")) {
|
|
2171
2258
|
return jsonResponse(
|
|
2172
|
-
{
|
|
2259
|
+
{
|
|
2260
|
+
error: "Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically."
|
|
2261
|
+
},
|
|
2173
2262
|
{ status: 400 }
|
|
2174
2263
|
);
|
|
2175
2264
|
}
|
|
@@ -2199,7 +2288,8 @@ async function handleUpload(request) {
|
|
|
2199
2288
|
}
|
|
2200
2289
|
if (isImage && ext !== ".svg") {
|
|
2201
2290
|
try {
|
|
2202
|
-
const
|
|
2291
|
+
const rotatedBuffer = await sharp2(buffer).rotate().toBuffer();
|
|
2292
|
+
const metadata = await sharp2(rotatedBuffer).metadata();
|
|
2203
2293
|
meta[imageKey] = {
|
|
2204
2294
|
o: { w: metadata.width || 0, h: metadata.height || 0 }
|
|
2205
2295
|
};
|
|
@@ -2218,7 +2308,10 @@ async function handleUpload(request) {
|
|
|
2218
2308
|
} catch (error) {
|
|
2219
2309
|
console.error("Failed to upload:", error);
|
|
2220
2310
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2221
|
-
return jsonResponse(
|
|
2311
|
+
return jsonResponse(
|
|
2312
|
+
{ error: `Failed to upload file: ${message}` },
|
|
2313
|
+
{ status: 500 }
|
|
2314
|
+
);
|
|
2222
2315
|
}
|
|
2223
2316
|
}
|
|
2224
2317
|
async function handleDelete(request) {
|
|
@@ -2348,9 +2441,11 @@ async function handleDeleteStream(request) {
|
|
|
2348
2441
|
async start(controller) {
|
|
2349
2442
|
const sendEvent = (data) => {
|
|
2350
2443
|
try {
|
|
2351
|
-
controller.enqueue(
|
|
2444
|
+
controller.enqueue(
|
|
2445
|
+
encoder.encode(`data: ${JSON.stringify(data)}
|
|
2352
2446
|
|
|
2353
|
-
`)
|
|
2447
|
+
`)
|
|
2448
|
+
);
|
|
2354
2449
|
} catch {
|
|
2355
2450
|
}
|
|
2356
2451
|
};
|
|
@@ -2529,7 +2624,7 @@ async function handleDeleteStream(request) {
|
|
|
2529
2624
|
headers: {
|
|
2530
2625
|
"Content-Type": "text/event-stream",
|
|
2531
2626
|
"Cache-Control": "no-cache",
|
|
2532
|
-
|
|
2627
|
+
Connection: "keep-alive"
|
|
2533
2628
|
}
|
|
2534
2629
|
});
|
|
2535
2630
|
}
|
|
@@ -2537,7 +2632,10 @@ async function handleCreateFolder(request) {
|
|
|
2537
2632
|
try {
|
|
2538
2633
|
const { parentPath, name } = await request.json();
|
|
2539
2634
|
if (!name || typeof name !== "string") {
|
|
2540
|
-
return jsonResponse(
|
|
2635
|
+
return jsonResponse(
|
|
2636
|
+
{ error: "Folder name is required" },
|
|
2637
|
+
{ status: 400 }
|
|
2638
|
+
);
|
|
2541
2639
|
}
|
|
2542
2640
|
const sanitizedName = slugifyFolderName(name);
|
|
2543
2641
|
if (!sanitizedName) {
|
|
@@ -2550,11 +2648,17 @@ async function handleCreateFolder(request) {
|
|
|
2550
2648
|
}
|
|
2551
2649
|
try {
|
|
2552
2650
|
await fs7.access(folderPath);
|
|
2553
|
-
return jsonResponse(
|
|
2651
|
+
return jsonResponse(
|
|
2652
|
+
{ error: "A folder with this name already exists" },
|
|
2653
|
+
{ status: 400 }
|
|
2654
|
+
);
|
|
2554
2655
|
} catch {
|
|
2555
2656
|
}
|
|
2556
2657
|
await fs7.mkdir(folderPath, { recursive: true });
|
|
2557
|
-
return jsonResponse({
|
|
2658
|
+
return jsonResponse({
|
|
2659
|
+
success: true,
|
|
2660
|
+
path: path7.join(safePath, sanitizedName)
|
|
2661
|
+
});
|
|
2558
2662
|
} catch (error) {
|
|
2559
2663
|
console.error("Failed to create folder:", error);
|
|
2560
2664
|
return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
|
|
@@ -2565,7 +2669,10 @@ async function handleRename(request) {
|
|
|
2565
2669
|
try {
|
|
2566
2670
|
const { oldPath, newName } = await request.json();
|
|
2567
2671
|
if (!oldPath || !newName) {
|
|
2568
|
-
return jsonResponse(
|
|
2672
|
+
return jsonResponse(
|
|
2673
|
+
{ error: "Path and new name are required" },
|
|
2674
|
+
{ status: 400 }
|
|
2675
|
+
);
|
|
2569
2676
|
}
|
|
2570
2677
|
const safePath = oldPath.replace(/\.\./g, "");
|
|
2571
2678
|
const absoluteOldPath = getWorkspacePath(safePath);
|
|
@@ -2590,7 +2697,10 @@ async function handleRename(request) {
|
|
|
2590
2697
|
isFile = stats.isFile();
|
|
2591
2698
|
} catch {
|
|
2592
2699
|
if (!isInCloud) {
|
|
2593
|
-
return jsonResponse(
|
|
2700
|
+
return jsonResponse(
|
|
2701
|
+
{ error: "File or folder not found" },
|
|
2702
|
+
{ status: 404 }
|
|
2703
|
+
);
|
|
2594
2704
|
}
|
|
2595
2705
|
}
|
|
2596
2706
|
const sanitizedName = isFile ? slugifyFilename(newName) : slugifyFolderName(newName);
|
|
@@ -2599,14 +2709,23 @@ async function handleRename(request) {
|
|
|
2599
2709
|
}
|
|
2600
2710
|
const parentDir = path7.dirname(absoluteOldPath);
|
|
2601
2711
|
const absoluteNewPath = path7.join(parentDir, sanitizedName);
|
|
2602
|
-
const newRelativePath = path7.join(
|
|
2712
|
+
const newRelativePath = path7.join(
|
|
2713
|
+
path7.dirname(oldRelativePath),
|
|
2714
|
+
sanitizedName
|
|
2715
|
+
);
|
|
2603
2716
|
const newKey = "/" + newRelativePath;
|
|
2604
2717
|
if (meta[newKey]) {
|
|
2605
|
-
return jsonResponse(
|
|
2718
|
+
return jsonResponse(
|
|
2719
|
+
{ error: "An item with this name already exists" },
|
|
2720
|
+
{ status: 400 }
|
|
2721
|
+
);
|
|
2606
2722
|
}
|
|
2607
2723
|
try {
|
|
2608
2724
|
await fs7.access(absoluteNewPath);
|
|
2609
|
-
return jsonResponse(
|
|
2725
|
+
return jsonResponse(
|
|
2726
|
+
{ error: "An item with this name already exists" },
|
|
2727
|
+
{ status: 400 }
|
|
2728
|
+
);
|
|
2610
2729
|
} catch {
|
|
2611
2730
|
}
|
|
2612
2731
|
if (isInOurR2 && !hasLocalFile) {
|
|
@@ -2664,7 +2783,10 @@ async function handleRenameStream(request) {
|
|
|
2664
2783
|
try {
|
|
2665
2784
|
const { oldPath, newName, operationId } = await request.json();
|
|
2666
2785
|
if (!oldPath || !newName) {
|
|
2667
|
-
sendEvent({
|
|
2786
|
+
sendEvent({
|
|
2787
|
+
type: "error",
|
|
2788
|
+
message: "Path and new name are required"
|
|
2789
|
+
});
|
|
2668
2790
|
controller.close();
|
|
2669
2791
|
return;
|
|
2670
2792
|
}
|
|
@@ -2693,7 +2815,9 @@ async function handleRenameStream(request) {
|
|
|
2693
2815
|
isFile = true;
|
|
2694
2816
|
} else {
|
|
2695
2817
|
const folderPrefix = oldKey2 + "/";
|
|
2696
|
-
const hasChildrenInMeta = Object.keys(meta2).some(
|
|
2818
|
+
const hasChildrenInMeta = Object.keys(meta2).some(
|
|
2819
|
+
(key) => key.startsWith(folderPrefix)
|
|
2820
|
+
);
|
|
2697
2821
|
if (hasChildrenInMeta) {
|
|
2698
2822
|
isFile = false;
|
|
2699
2823
|
isVirtualFolder = true;
|
|
@@ -2712,14 +2836,20 @@ async function handleRenameStream(request) {
|
|
|
2712
2836
|
}
|
|
2713
2837
|
const parentDir = path7.dirname(absoluteOldPath);
|
|
2714
2838
|
const absoluteNewPath = path7.join(parentDir, sanitizedName);
|
|
2715
|
-
const newRelativePath = path7.join(
|
|
2839
|
+
const newRelativePath = path7.join(
|
|
2840
|
+
path7.dirname(oldRelativePath),
|
|
2841
|
+
sanitizedName
|
|
2842
|
+
);
|
|
2716
2843
|
const newPath = path7.join(path7.dirname(safePath), sanitizedName);
|
|
2717
2844
|
const meta = await loadMeta();
|
|
2718
2845
|
const cdnUrls = getCdnUrls(meta);
|
|
2719
2846
|
if (isFile) {
|
|
2720
2847
|
const newKey2 = "/" + newRelativePath;
|
|
2721
2848
|
if (meta[newKey2]) {
|
|
2722
|
-
sendEvent({
|
|
2849
|
+
sendEvent({
|
|
2850
|
+
type: "error",
|
|
2851
|
+
message: "An item with this name already exists"
|
|
2852
|
+
});
|
|
2723
2853
|
controller.close();
|
|
2724
2854
|
return;
|
|
2725
2855
|
}
|
|
@@ -2727,7 +2857,10 @@ async function handleRenameStream(request) {
|
|
|
2727
2857
|
if (!isVirtualFolder) {
|
|
2728
2858
|
try {
|
|
2729
2859
|
await fs7.access(absoluteNewPath);
|
|
2730
|
-
sendEvent({
|
|
2860
|
+
sendEvent({
|
|
2861
|
+
type: "error",
|
|
2862
|
+
message: "An item with this name already exists"
|
|
2863
|
+
});
|
|
2731
2864
|
controller.close();
|
|
2732
2865
|
return;
|
|
2733
2866
|
} catch {
|
|
@@ -2735,9 +2868,14 @@ async function handleRenameStream(request) {
|
|
|
2735
2868
|
}
|
|
2736
2869
|
if (isVirtualFolder) {
|
|
2737
2870
|
const newPrefix = "/" + newRelativePath + "/";
|
|
2738
|
-
const hasConflict = Object.keys(meta).some(
|
|
2871
|
+
const hasConflict = Object.keys(meta).some(
|
|
2872
|
+
(key) => key.startsWith(newPrefix)
|
|
2873
|
+
);
|
|
2739
2874
|
if (hasConflict) {
|
|
2740
|
-
sendEvent({
|
|
2875
|
+
sendEvent({
|
|
2876
|
+
type: "error",
|
|
2877
|
+
message: "A folder with this name already exists"
|
|
2878
|
+
});
|
|
2741
2879
|
controller.close();
|
|
2742
2880
|
return;
|
|
2743
2881
|
}
|
|
@@ -2749,11 +2887,19 @@ async function handleRenameStream(request) {
|
|
|
2749
2887
|
for (const [key, entry2] of Object.entries(meta)) {
|
|
2750
2888
|
if (key.startsWith(oldPrefix) && entry2 && typeof entry2 === "object") {
|
|
2751
2889
|
const newKey2 = key.replace(oldPrefix, newPrefix);
|
|
2752
|
-
itemsToUpdate.push({
|
|
2890
|
+
itemsToUpdate.push({
|
|
2891
|
+
oldKey: key,
|
|
2892
|
+
newKey: newKey2,
|
|
2893
|
+
entry: entry2
|
|
2894
|
+
});
|
|
2753
2895
|
}
|
|
2754
2896
|
}
|
|
2755
2897
|
const total = itemsToUpdate.length + 1;
|
|
2756
|
-
sendEvent({
|
|
2898
|
+
sendEvent({
|
|
2899
|
+
type: "start",
|
|
2900
|
+
total,
|
|
2901
|
+
message: `Renaming folder with ${itemsToUpdate.length} item(s)...`
|
|
2902
|
+
});
|
|
2757
2903
|
if (hasLocalItem) {
|
|
2758
2904
|
await fs7.rename(absoluteOldPath, absoluteNewPath);
|
|
2759
2905
|
const imagesDir = getPublicPath("/images");
|
|
@@ -2766,12 +2912,21 @@ async function handleRenameStream(request) {
|
|
|
2766
2912
|
} catch {
|
|
2767
2913
|
}
|
|
2768
2914
|
}
|
|
2769
|
-
sendEvent({
|
|
2915
|
+
sendEvent({
|
|
2916
|
+
type: "progress",
|
|
2917
|
+
current: 1,
|
|
2918
|
+
total,
|
|
2919
|
+
renamed: 1,
|
|
2920
|
+
message: "Renamed folder"
|
|
2921
|
+
});
|
|
2770
2922
|
let renamed = 1;
|
|
2771
2923
|
const handleRenameCancel = async () => {
|
|
2772
2924
|
await saveMeta(meta);
|
|
2773
2925
|
await deleteEmptyFolders(absoluteOldPath);
|
|
2774
|
-
const oldThumbFolder2 = path7.join(
|
|
2926
|
+
const oldThumbFolder2 = path7.join(
|
|
2927
|
+
getPublicPath("/images"),
|
|
2928
|
+
oldRelativePath
|
|
2929
|
+
);
|
|
2775
2930
|
await deleteEmptyFolders(oldThumbFolder2);
|
|
2776
2931
|
sendEvent({ type: "complete", renamed, newPath, cancelled: true });
|
|
2777
2932
|
controller.close();
|
|
@@ -2814,7 +2969,10 @@ async function handleRenameStream(request) {
|
|
|
2814
2969
|
});
|
|
2815
2970
|
}
|
|
2816
2971
|
await deleteEmptyFolders(absoluteOldPath);
|
|
2817
|
-
const oldThumbFolder = path7.join(
|
|
2972
|
+
const oldThumbFolder = path7.join(
|
|
2973
|
+
getPublicPath("/images"),
|
|
2974
|
+
oldRelativePath
|
|
2975
|
+
);
|
|
2818
2976
|
await deleteEmptyFolders(oldThumbFolder);
|
|
2819
2977
|
sendEvent({ type: "complete", renamed, newPath });
|
|
2820
2978
|
controller.close();
|
|
@@ -2937,7 +3095,9 @@ async function handleMoveStream(request) {
|
|
|
2937
3095
|
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2938
3096
|
for (const entry of entries) {
|
|
2939
3097
|
if (entry.isDirectory()) {
|
|
2940
|
-
count += await countFilesRecursive(
|
|
3098
|
+
count += await countFilesRecursive(
|
|
3099
|
+
path7.join(dir, entry.name)
|
|
3100
|
+
);
|
|
2941
3101
|
} else {
|
|
2942
3102
|
count++;
|
|
2943
3103
|
}
|
|
@@ -2959,7 +3119,15 @@ async function handleMoveStream(request) {
|
|
|
2959
3119
|
}
|
|
2960
3120
|
}
|
|
2961
3121
|
totalFiles += localFileCount + cloudOnlyCount;
|
|
2962
|
-
expandedItems.push({
|
|
3122
|
+
expandedItems.push({
|
|
3123
|
+
itemPath,
|
|
3124
|
+
safePath,
|
|
3125
|
+
itemName,
|
|
3126
|
+
oldKey,
|
|
3127
|
+
newKey,
|
|
3128
|
+
newAbsolutePath,
|
|
3129
|
+
isVirtualFolder: false
|
|
3130
|
+
});
|
|
2963
3131
|
} else if (!hasLocalItem) {
|
|
2964
3132
|
const folderPrefix = oldKey + "/";
|
|
2965
3133
|
const virtualItems = [];
|
|
@@ -2967,20 +3135,49 @@ async function handleMoveStream(request) {
|
|
|
2967
3135
|
if (key.startsWith(folderPrefix) && metaEntry && typeof metaEntry === "object") {
|
|
2968
3136
|
const relativePath = key.slice(folderPrefix.length);
|
|
2969
3137
|
const destNewKey = newKey + "/" + relativePath;
|
|
2970
|
-
virtualItems.push({
|
|
3138
|
+
virtualItems.push({
|
|
3139
|
+
oldKey: key,
|
|
3140
|
+
newKey: destNewKey,
|
|
3141
|
+
entry: metaEntry
|
|
3142
|
+
});
|
|
2971
3143
|
}
|
|
2972
3144
|
}
|
|
2973
3145
|
if (virtualItems.length > 0) {
|
|
2974
3146
|
totalFiles += virtualItems.length;
|
|
2975
|
-
expandedItems.push({
|
|
3147
|
+
expandedItems.push({
|
|
3148
|
+
itemPath,
|
|
3149
|
+
safePath,
|
|
3150
|
+
itemName,
|
|
3151
|
+
oldKey,
|
|
3152
|
+
newKey,
|
|
3153
|
+
newAbsolutePath,
|
|
3154
|
+
isVirtualFolder: true,
|
|
3155
|
+
virtualFolderItems: virtualItems
|
|
3156
|
+
});
|
|
2976
3157
|
sourceFolders.add(absolutePath);
|
|
2977
3158
|
} else {
|
|
2978
3159
|
totalFiles++;
|
|
2979
|
-
expandedItems.push({
|
|
3160
|
+
expandedItems.push({
|
|
3161
|
+
itemPath,
|
|
3162
|
+
safePath,
|
|
3163
|
+
itemName,
|
|
3164
|
+
oldKey,
|
|
3165
|
+
newKey,
|
|
3166
|
+
newAbsolutePath,
|
|
3167
|
+
isVirtualFolder: false
|
|
3168
|
+
});
|
|
2980
3169
|
}
|
|
2981
3170
|
} else {
|
|
2982
3171
|
totalFiles++;
|
|
2983
|
-
expandedItems.push({
|
|
3172
|
+
expandedItems.push({
|
|
3173
|
+
itemPath,
|
|
3174
|
+
safePath,
|
|
3175
|
+
itemName,
|
|
3176
|
+
oldKey,
|
|
3177
|
+
newKey,
|
|
3178
|
+
newAbsolutePath,
|
|
3179
|
+
isVirtualFolder: false
|
|
3180
|
+
});
|
|
2984
3181
|
}
|
|
2985
3182
|
}
|
|
2986
3183
|
sendEvent({ type: "start", total: totalFiles });
|
|
@@ -2992,7 +3189,13 @@ async function handleMoveStream(request) {
|
|
|
2992
3189
|
await deleteEmptyFolders(folder);
|
|
2993
3190
|
}
|
|
2994
3191
|
await deleteEmptyFolders(absoluteDestination);
|
|
2995
|
-
sendEvent({
|
|
3192
|
+
sendEvent({
|
|
3193
|
+
type: "complete",
|
|
3194
|
+
moved: filesMoved,
|
|
3195
|
+
errors: errors.length,
|
|
3196
|
+
errorMessages: errors,
|
|
3197
|
+
cancelled: true
|
|
3198
|
+
});
|
|
2996
3199
|
controller.close();
|
|
2997
3200
|
};
|
|
2998
3201
|
for (const expandedItem of expandedItems) {
|
|
@@ -3000,7 +3203,16 @@ async function handleMoveStream(request) {
|
|
|
3000
3203
|
await handleCancel();
|
|
3001
3204
|
return;
|
|
3002
3205
|
}
|
|
3003
|
-
const {
|
|
3206
|
+
const {
|
|
3207
|
+
itemPath,
|
|
3208
|
+
safePath,
|
|
3209
|
+
itemName,
|
|
3210
|
+
oldKey,
|
|
3211
|
+
newKey,
|
|
3212
|
+
newAbsolutePath,
|
|
3213
|
+
isVirtualFolder,
|
|
3214
|
+
virtualFolderItems
|
|
3215
|
+
} = expandedItem;
|
|
3004
3216
|
if (isVirtualFolder && virtualFolderItems) {
|
|
3005
3217
|
for (const vItem of virtualFolderItems) {
|
|
3006
3218
|
if (isCancelled()) {
|
|
@@ -3015,11 +3227,18 @@ async function handleMoveStream(request) {
|
|
|
3015
3227
|
let vItemMoved = false;
|
|
3016
3228
|
if (isItemInR2) {
|
|
3017
3229
|
try {
|
|
3018
|
-
await moveInCdn(
|
|
3230
|
+
await moveInCdn(
|
|
3231
|
+
vItem.oldKey,
|
|
3232
|
+
vItem.newKey,
|
|
3233
|
+
itemHasThumbnails
|
|
3234
|
+
);
|
|
3019
3235
|
vItemMoved = true;
|
|
3020
3236
|
filesMoved++;
|
|
3021
3237
|
} catch (err) {
|
|
3022
|
-
console.error(
|
|
3238
|
+
console.error(
|
|
3239
|
+
`Failed to move cloud item ${vItem.oldKey}:`,
|
|
3240
|
+
err
|
|
3241
|
+
);
|
|
3023
3242
|
delete meta[vItem.oldKey];
|
|
3024
3243
|
await saveMeta(meta);
|
|
3025
3244
|
}
|
|
@@ -3043,7 +3262,10 @@ async function handleMoveStream(request) {
|
|
|
3043
3262
|
}
|
|
3044
3263
|
const newFolderPath = getPublicPath(newKey);
|
|
3045
3264
|
await deleteEmptyFolders(newFolderPath);
|
|
3046
|
-
const newThumbFolder = path7.join(
|
|
3265
|
+
const newThumbFolder = path7.join(
|
|
3266
|
+
getPublicPath("images"),
|
|
3267
|
+
newKey.slice(1)
|
|
3268
|
+
);
|
|
3047
3269
|
await deleteEmptyFolders(newThumbFolder);
|
|
3048
3270
|
const oldFolderPath = getPublicPath(oldKey);
|
|
3049
3271
|
sourceFolders.add(oldFolderPath);
|
|
@@ -3076,7 +3298,9 @@ async function handleMoveStream(request) {
|
|
|
3076
3298
|
if (isRemote) {
|
|
3077
3299
|
const remoteUrl = `${fileCdnUrl}${oldKey}`;
|
|
3078
3300
|
const buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
3079
|
-
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3301
|
+
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3302
|
+
recursive: true
|
|
3303
|
+
});
|
|
3080
3304
|
await fs7.writeFile(newAbsolutePath, buffer);
|
|
3081
3305
|
const newEntry = {
|
|
3082
3306
|
o: entry?.o,
|
|
@@ -3161,7 +3385,9 @@ async function handleMoveStream(request) {
|
|
|
3161
3385
|
}
|
|
3162
3386
|
const stats = await fs7.stat(absolutePath);
|
|
3163
3387
|
if (stats.isFile()) {
|
|
3164
|
-
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3388
|
+
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3389
|
+
recursive: true
|
|
3390
|
+
});
|
|
3165
3391
|
await fs7.rename(absolutePath, newAbsolutePath);
|
|
3166
3392
|
if (isImage && entry) {
|
|
3167
3393
|
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
@@ -3172,7 +3398,9 @@ async function handleMoveStream(request) {
|
|
|
3172
3398
|
try {
|
|
3173
3399
|
await fs7.access(oldThumbPath);
|
|
3174
3400
|
sourceFolders.add(path7.dirname(oldThumbPath));
|
|
3175
|
-
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3401
|
+
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3402
|
+
recursive: true
|
|
3403
|
+
});
|
|
3176
3404
|
await fs7.rename(oldThumbPath, newThumbPath);
|
|
3177
3405
|
} catch {
|
|
3178
3406
|
}
|
|
@@ -3208,13 +3436,21 @@ async function handleMoveStream(request) {
|
|
|
3208
3436
|
const newPrefix = newKey + "/";
|
|
3209
3437
|
const localFiles = [];
|
|
3210
3438
|
const collectLocalFiles = async (dir, relativeDir) => {
|
|
3211
|
-
const entries = await fs7.readdir(dir, {
|
|
3439
|
+
const entries = await fs7.readdir(dir, {
|
|
3440
|
+
withFileTypes: true
|
|
3441
|
+
});
|
|
3212
3442
|
for (const dirEntry of entries) {
|
|
3213
3443
|
const entryRelPath = relativeDir ? `${relativeDir}/${dirEntry.name}` : dirEntry.name;
|
|
3214
3444
|
if (dirEntry.isDirectory()) {
|
|
3215
|
-
await collectLocalFiles(
|
|
3445
|
+
await collectLocalFiles(
|
|
3446
|
+
path7.join(dir, dirEntry.name),
|
|
3447
|
+
entryRelPath
|
|
3448
|
+
);
|
|
3216
3449
|
} else {
|
|
3217
|
-
localFiles.push({
|
|
3450
|
+
localFiles.push({
|
|
3451
|
+
relativePath: entryRelPath,
|
|
3452
|
+
isImage: isImageFile(dirEntry.name)
|
|
3453
|
+
});
|
|
3218
3454
|
}
|
|
3219
3455
|
}
|
|
3220
3456
|
};
|
|
@@ -3240,13 +3476,21 @@ async function handleMoveStream(request) {
|
|
|
3240
3476
|
await handleCancel();
|
|
3241
3477
|
return;
|
|
3242
3478
|
}
|
|
3243
|
-
const fileOldPath = path7.join(
|
|
3244
|
-
|
|
3479
|
+
const fileOldPath = path7.join(
|
|
3480
|
+
absolutePath,
|
|
3481
|
+
localFile.relativePath
|
|
3482
|
+
);
|
|
3483
|
+
const fileNewPath = path7.join(
|
|
3484
|
+
newAbsolutePath,
|
|
3485
|
+
localFile.relativePath
|
|
3486
|
+
);
|
|
3245
3487
|
const fileOldKey = oldPrefix + localFile.relativePath;
|
|
3246
3488
|
const fileNewKey = newPrefix + localFile.relativePath;
|
|
3247
3489
|
const fileEntry = meta[fileOldKey];
|
|
3248
3490
|
sourceFolders.add(path7.dirname(fileOldPath));
|
|
3249
|
-
await fs7.mkdir(path7.dirname(fileNewPath), {
|
|
3491
|
+
await fs7.mkdir(path7.dirname(fileNewPath), {
|
|
3492
|
+
recursive: true
|
|
3493
|
+
});
|
|
3250
3494
|
await fs7.rename(fileOldPath, fileNewPath);
|
|
3251
3495
|
filesMoved++;
|
|
3252
3496
|
if (localFile.isImage && fileEntry) {
|
|
@@ -3258,7 +3502,9 @@ async function handleMoveStream(request) {
|
|
|
3258
3502
|
try {
|
|
3259
3503
|
await fs7.access(oldThumbPath);
|
|
3260
3504
|
sourceFolders.add(path7.dirname(oldThumbPath));
|
|
3261
|
-
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3505
|
+
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3506
|
+
recursive: true
|
|
3507
|
+
});
|
|
3262
3508
|
await fs7.rename(oldThumbPath, newThumbPath);
|
|
3263
3509
|
} catch {
|
|
3264
3510
|
}
|
|
@@ -3297,11 +3543,18 @@ async function handleMoveStream(request) {
|
|
|
3297
3543
|
let cloudFileMoved = false;
|
|
3298
3544
|
if (cloudIsInR2) {
|
|
3299
3545
|
try {
|
|
3300
|
-
await moveInCdn(
|
|
3546
|
+
await moveInCdn(
|
|
3547
|
+
cloudFile.oldKey,
|
|
3548
|
+
cloudFile.newKey,
|
|
3549
|
+
cloudHasThumbs
|
|
3550
|
+
);
|
|
3301
3551
|
cloudFileMoved = true;
|
|
3302
3552
|
filesMoved++;
|
|
3303
3553
|
} catch (err) {
|
|
3304
|
-
console.error(
|
|
3554
|
+
console.error(
|
|
3555
|
+
`Failed to move cloud file ${cloudFile.oldKey}:`,
|
|
3556
|
+
err
|
|
3557
|
+
);
|
|
3305
3558
|
delete meta[cloudFile.oldKey];
|
|
3306
3559
|
await saveMeta(meta);
|
|
3307
3560
|
}
|
|
@@ -3323,7 +3576,10 @@ async function handleMoveStream(request) {
|
|
|
3323
3576
|
}
|
|
3324
3577
|
sourceFolders.add(absolutePath);
|
|
3325
3578
|
const oldThumbRelPath = oldKey.slice(1);
|
|
3326
|
-
const oldThumbFolder = path7.join(
|
|
3579
|
+
const oldThumbFolder = path7.join(
|
|
3580
|
+
getPublicPath("images"),
|
|
3581
|
+
oldThumbRelPath
|
|
3582
|
+
);
|
|
3327
3583
|
sourceFolders.add(oldThumbFolder);
|
|
3328
3584
|
moved.push(itemPath);
|
|
3329
3585
|
}
|
|
@@ -3365,7 +3621,7 @@ async function handleMoveStream(request) {
|
|
|
3365
3621
|
headers: {
|
|
3366
3622
|
"Content-Type": "text/event-stream",
|
|
3367
3623
|
"Cache-Control": "no-cache",
|
|
3368
|
-
|
|
3624
|
+
Connection: "keep-alive"
|
|
3369
3625
|
}
|
|
3370
3626
|
});
|
|
3371
3627
|
}
|
|
@@ -3385,7 +3641,9 @@ async function handleScanStream() {
|
|
|
3385
3641
|
};
|
|
3386
3642
|
try {
|
|
3387
3643
|
const meta = await loadMeta();
|
|
3388
|
-
const existingCount = Object.keys(meta).filter(
|
|
3644
|
+
const existingCount = Object.keys(meta).filter(
|
|
3645
|
+
(k) => !k.startsWith("_")
|
|
3646
|
+
).length;
|
|
3389
3647
|
const existingKeys = new Set(Object.keys(meta));
|
|
3390
3648
|
const added = [];
|
|
3391
3649
|
const renamed = [];
|
|
@@ -3400,7 +3658,8 @@ async function handleScanStream() {
|
|
|
3400
3658
|
if (entry.name.startsWith(".")) continue;
|
|
3401
3659
|
const fullPath = path8.join(dir, entry.name);
|
|
3402
3660
|
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
3403
|
-
if (relPath === "images" || relPath.startsWith("images/"))
|
|
3661
|
+
if (relPath === "images" || relPath.startsWith("images/"))
|
|
3662
|
+
continue;
|
|
3404
3663
|
if (entry.isDirectory()) {
|
|
3405
3664
|
await scanDir(fullPath, relPath);
|
|
3406
3665
|
} else if (isMediaFile(entry.name)) {
|
|
@@ -3484,7 +3743,8 @@ async function handleScanStream() {
|
|
|
3484
3743
|
} else {
|
|
3485
3744
|
try {
|
|
3486
3745
|
const buffer = await fs8.readFile(fullPath);
|
|
3487
|
-
const
|
|
3746
|
+
const rotatedBuffer = await sharp3(buffer).rotate().toBuffer();
|
|
3747
|
+
const metadata = await sharp3(rotatedBuffer).metadata();
|
|
3488
3748
|
meta[imageKey] = {
|
|
3489
3749
|
o: { w: metadata.width || 0, h: metadata.height || 0 }
|
|
3490
3750
|
};
|
|
@@ -3505,7 +3765,10 @@ async function handleScanStream() {
|
|
|
3505
3765
|
errors.push(relativePath);
|
|
3506
3766
|
}
|
|
3507
3767
|
}
|
|
3508
|
-
sendEvent({
|
|
3768
|
+
sendEvent({
|
|
3769
|
+
type: "cleanup",
|
|
3770
|
+
message: "Checking for orphaned thumbnails..."
|
|
3771
|
+
});
|
|
3509
3772
|
const expectedThumbnails = /* @__PURE__ */ new Set();
|
|
3510
3773
|
const fileEntries = getFileEntries(meta);
|
|
3511
3774
|
for (const [imageKey, entry] of fileEntries) {
|
|
@@ -3552,7 +3815,9 @@ async function handleScanStream() {
|
|
|
3552
3815
|
await cleanEmptyFolders(fullPath);
|
|
3553
3816
|
try {
|
|
3554
3817
|
const subEntries = await fs8.readdir(fullPath);
|
|
3555
|
-
const meaningfulEntries = subEntries.filter(
|
|
3818
|
+
const meaningfulEntries = subEntries.filter(
|
|
3819
|
+
(e) => !e.startsWith(".")
|
|
3820
|
+
);
|
|
3556
3821
|
if (meaningfulEntries.length === 0) {
|
|
3557
3822
|
await fs8.rm(fullPath, { recursive: true });
|
|
3558
3823
|
emptyFoldersDeleted++;
|
|
@@ -3570,7 +3835,9 @@ async function handleScanStream() {
|
|
|
3570
3835
|
let isEmpty = true;
|
|
3571
3836
|
for (const entry of entries) {
|
|
3572
3837
|
if (entry.isDirectory()) {
|
|
3573
|
-
const subDirEmpty = await cleanImagesEmptyFolders(
|
|
3838
|
+
const subDirEmpty = await cleanImagesEmptyFolders(
|
|
3839
|
+
path8.join(dir, entry.name)
|
|
3840
|
+
);
|
|
3574
3841
|
if (!subDirEmpty) isEmpty = false;
|
|
3575
3842
|
} else if (!entry.name.startsWith(".")) {
|
|
3576
3843
|
isEmpty = false;
|
|
@@ -3589,7 +3856,10 @@ async function handleScanStream() {
|
|
|
3589
3856
|
await cleanImagesEmptyFolders(imagesDir);
|
|
3590
3857
|
} catch {
|
|
3591
3858
|
}
|
|
3592
|
-
sendEvent({
|
|
3859
|
+
sendEvent({
|
|
3860
|
+
type: "cleanup",
|
|
3861
|
+
message: "Checking for orphaned entries..."
|
|
3862
|
+
});
|
|
3593
3863
|
const orphanedEntries = [];
|
|
3594
3864
|
const cdnUrls = meta._cdns || [];
|
|
3595
3865
|
const r2PublicUrl = (process.env.CLOUDFLARE_R2_PUBLIC_URL || "").replace(/\/$/, "");
|
|
@@ -3617,7 +3887,10 @@ async function handleScanStream() {
|
|
|
3617
3887
|
}
|
|
3618
3888
|
}
|
|
3619
3889
|
if (orphanedEntries.length > 0) {
|
|
3620
|
-
sendEvent({
|
|
3890
|
+
sendEvent({
|
|
3891
|
+
type: "cleanup",
|
|
3892
|
+
message: `Removed ${orphanedEntries.length} orphaned entries...`
|
|
3893
|
+
});
|
|
3621
3894
|
}
|
|
3622
3895
|
await saveMeta(meta);
|
|
3623
3896
|
sendEvent({
|
|
@@ -3644,7 +3917,7 @@ async function handleScanStream() {
|
|
|
3644
3917
|
headers: {
|
|
3645
3918
|
"Content-Type": "text/event-stream",
|
|
3646
3919
|
"Cache-Control": "no-cache",
|
|
3647
|
-
|
|
3920
|
+
Connection: "keep-alive"
|
|
3648
3921
|
}
|
|
3649
3922
|
});
|
|
3650
3923
|
}
|
|
@@ -3682,7 +3955,10 @@ async function handleDeleteOrphans(request) {
|
|
|
3682
3955
|
});
|
|
3683
3956
|
} catch (error) {
|
|
3684
3957
|
console.error("Failed to delete orphans:", error);
|
|
3685
|
-
return jsonResponse(
|
|
3958
|
+
return jsonResponse(
|
|
3959
|
+
{ error: "Failed to delete orphaned files" },
|
|
3960
|
+
{ status: 500 }
|
|
3961
|
+
);
|
|
3686
3962
|
}
|
|
3687
3963
|
}
|
|
3688
3964
|
|
|
@@ -3700,9 +3976,11 @@ async function processRemoteImage(url) {
|
|
|
3700
3976
|
throw new Error(`Failed to fetch: ${response.status}`);
|
|
3701
3977
|
}
|
|
3702
3978
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3703
|
-
const
|
|
3979
|
+
const rotatedBuffer = await sharp4(buffer).rotate().toBuffer();
|
|
3980
|
+
const metadata = await sharp4(rotatedBuffer).metadata();
|
|
3704
3981
|
return {
|
|
3705
3982
|
o: { w: metadata.width || 0, h: metadata.height || 0 }
|
|
3983
|
+
// b: blur hash would be generated here if needed
|
|
3706
3984
|
};
|
|
3707
3985
|
}
|
|
3708
3986
|
async function handleImportUrls(request) {
|
|
@@ -3711,9 +3989,11 @@ async function handleImportUrls(request) {
|
|
|
3711
3989
|
async start(controller) {
|
|
3712
3990
|
const sendEvent = (data) => {
|
|
3713
3991
|
try {
|
|
3714
|
-
controller.enqueue(
|
|
3992
|
+
controller.enqueue(
|
|
3993
|
+
encoder.encode(`data: ${JSON.stringify(data)}
|
|
3715
3994
|
|
|
3716
|
-
`)
|
|
3995
|
+
`)
|
|
3996
|
+
);
|
|
3717
3997
|
} catch {
|
|
3718
3998
|
}
|
|
3719
3999
|
};
|
|
@@ -3813,7 +4093,7 @@ async function handleImportUrls(request) {
|
|
|
3813
4093
|
headers: {
|
|
3814
4094
|
"Content-Type": "text/event-stream",
|
|
3815
4095
|
"Cache-Control": "no-cache",
|
|
3816
|
-
|
|
4096
|
+
Connection: "keep-alive"
|
|
3817
4097
|
}
|
|
3818
4098
|
});
|
|
3819
4099
|
}
|
|
@@ -3866,9 +4146,12 @@ async function handleGenerateFavicon(request) {
|
|
|
3866
4146
|
}
|
|
3867
4147
|
const fileName = path9.basename(imagePath).toLowerCase();
|
|
3868
4148
|
if (fileName !== "favicon.png" && fileName !== "favicon.jpg") {
|
|
3869
|
-
return jsonResponse(
|
|
3870
|
-
|
|
3871
|
-
|
|
4149
|
+
return jsonResponse(
|
|
4150
|
+
{
|
|
4151
|
+
error: "Source file must be named favicon.png or favicon.jpg"
|
|
4152
|
+
},
|
|
4153
|
+
{ status: 400 }
|
|
4154
|
+
);
|
|
3872
4155
|
}
|
|
3873
4156
|
const sourcePath = getPublicPath(imagePath.replace(/^\//, ""));
|
|
3874
4157
|
try {
|
|
@@ -3878,17 +4161,24 @@ async function handleGenerateFavicon(request) {
|
|
|
3878
4161
|
}
|
|
3879
4162
|
let metadata;
|
|
3880
4163
|
try {
|
|
3881
|
-
|
|
4164
|
+
const rotatedBuffer = await sharp5(sourcePath).rotate().toBuffer();
|
|
4165
|
+
metadata = await sharp5(rotatedBuffer).metadata();
|
|
3882
4166
|
} catch {
|
|
3883
|
-
return jsonResponse(
|
|
4167
|
+
return jsonResponse(
|
|
4168
|
+
{ error: "Source file is not a valid image" },
|
|
4169
|
+
{ status: 400 }
|
|
4170
|
+
);
|
|
3884
4171
|
}
|
|
3885
4172
|
const outputDir = getSrcAppPath();
|
|
3886
4173
|
try {
|
|
3887
4174
|
await fs9.access(outputDir);
|
|
3888
4175
|
} catch {
|
|
3889
|
-
return jsonResponse(
|
|
3890
|
-
|
|
3891
|
-
|
|
4176
|
+
return jsonResponse(
|
|
4177
|
+
{
|
|
4178
|
+
error: "Output directory src/app/ not found"
|
|
4179
|
+
},
|
|
4180
|
+
{ status: 500 }
|
|
4181
|
+
);
|
|
3892
4182
|
}
|
|
3893
4183
|
const stream = new ReadableStream({
|
|
3894
4184
|
async start(controller) {
|
|
@@ -3910,7 +4200,7 @@ async function handleGenerateFavicon(request) {
|
|
|
3910
4200
|
const config = FAVICON_CONFIGS[i];
|
|
3911
4201
|
try {
|
|
3912
4202
|
const outputPath = path9.join(outputDir, config.name);
|
|
3913
|
-
await sharp5(sourcePath).resize(config.size, config.size, {
|
|
4203
|
+
await sharp5(sourcePath).rotate().resize(config.size, config.size, {
|
|
3914
4204
|
fit: "cover",
|
|
3915
4205
|
position: "center"
|
|
3916
4206
|
}).png({ quality: 100 }).toFile(outputPath);
|