@gallop.software/studio 2.3.86 → 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/client/assets/index-BPVxJmuY.js +89 -0
- package/dist/client/index.html +1 -1
- package/dist/server/index.js +366 -111
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
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
|
}
|
|
@@ -1979,7 +2012,11 @@ async function handlePushUpdatesStream(request) {
|
|
|
1979
2012
|
const errors = [];
|
|
1980
2013
|
const total = paths.length;
|
|
1981
2014
|
if (total === 0) {
|
|
1982
|
-
sendEvent({
|
|
2015
|
+
sendEvent({
|
|
2016
|
+
type: "complete",
|
|
2017
|
+
pushed: 0,
|
|
2018
|
+
message: "No files with pending updates found."
|
|
2019
|
+
});
|
|
1983
2020
|
controller.close();
|
|
1984
2021
|
return;
|
|
1985
2022
|
}
|
|
@@ -1988,7 +2025,12 @@ async function handlePushUpdatesStream(request) {
|
|
|
1988
2025
|
if (isCancelled()) {
|
|
1989
2026
|
await saveMeta(meta);
|
|
1990
2027
|
if (operationId) clearCancelledOperation(operationId);
|
|
1991
|
-
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
|
+
});
|
|
1992
2034
|
controller.close();
|
|
1993
2035
|
return;
|
|
1994
2036
|
}
|
|
@@ -2026,18 +2068,22 @@ async function handlePushUpdatesStream(request) {
|
|
|
2026
2068
|
const contentType = getContentType(path6.basename(key));
|
|
2027
2069
|
const uploadKey = key.startsWith("/") ? key.slice(1) : key;
|
|
2028
2070
|
try {
|
|
2029
|
-
await s3.send(
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2071
|
+
await s3.send(
|
|
2072
|
+
new DeleteObjectCommand2({
|
|
2073
|
+
Bucket: bucketName,
|
|
2074
|
+
Key: uploadKey
|
|
2075
|
+
})
|
|
2076
|
+
);
|
|
2033
2077
|
} catch {
|
|
2034
2078
|
}
|
|
2035
|
-
await s3.send(
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2079
|
+
await s3.send(
|
|
2080
|
+
new PutObjectCommand2({
|
|
2081
|
+
Bucket: bucketName,
|
|
2082
|
+
Key: uploadKey,
|
|
2083
|
+
Body: buffer,
|
|
2084
|
+
ContentType: contentType
|
|
2085
|
+
})
|
|
2086
|
+
);
|
|
2041
2087
|
if (isProcessed(entry)) {
|
|
2042
2088
|
await deleteThumbnailsFromCdn(key);
|
|
2043
2089
|
const processedEntry = await processImage(buffer, key);
|
|
@@ -2103,7 +2149,7 @@ async function handlePushUpdatesStream(request) {
|
|
|
2103
2149
|
headers: {
|
|
2104
2150
|
"Content-Type": "text/event-stream",
|
|
2105
2151
|
"Cache-Control": "no-cache",
|
|
2106
|
-
|
|
2152
|
+
Connection: "keep-alive"
|
|
2107
2153
|
}
|
|
2108
2154
|
});
|
|
2109
2155
|
}
|
|
@@ -2111,13 +2157,19 @@ async function handleCancelStreamOperation(request) {
|
|
|
2111
2157
|
try {
|
|
2112
2158
|
const { operationId } = await request.json();
|
|
2113
2159
|
if (!operationId || typeof operationId !== "string") {
|
|
2114
|
-
return jsonResponse(
|
|
2160
|
+
return jsonResponse(
|
|
2161
|
+
{ error: "No operation ID provided" },
|
|
2162
|
+
{ status: 400 }
|
|
2163
|
+
);
|
|
2115
2164
|
}
|
|
2116
2165
|
cancelOperation(operationId);
|
|
2117
2166
|
return jsonResponse({ success: true, operationId });
|
|
2118
2167
|
} catch (error) {
|
|
2119
2168
|
console.error("Failed to cancel operation:", error);
|
|
2120
|
-
return jsonResponse(
|
|
2169
|
+
return jsonResponse(
|
|
2170
|
+
{ error: "Failed to cancel operation" },
|
|
2171
|
+
{ status: 500 }
|
|
2172
|
+
);
|
|
2121
2173
|
}
|
|
2122
2174
|
}
|
|
2123
2175
|
async function handleCancelUpdates(request) {
|
|
@@ -2204,7 +2256,9 @@ async function handleUpload(request) {
|
|
|
2204
2256
|
}
|
|
2205
2257
|
if (relativeDir === "images" || relativeDir.startsWith("images/")) {
|
|
2206
2258
|
return jsonResponse(
|
|
2207
|
-
{
|
|
2259
|
+
{
|
|
2260
|
+
error: "Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically."
|
|
2261
|
+
},
|
|
2208
2262
|
{ status: 400 }
|
|
2209
2263
|
);
|
|
2210
2264
|
}
|
|
@@ -2234,7 +2288,8 @@ async function handleUpload(request) {
|
|
|
2234
2288
|
}
|
|
2235
2289
|
if (isImage && ext !== ".svg") {
|
|
2236
2290
|
try {
|
|
2237
|
-
const
|
|
2291
|
+
const rotatedBuffer = await sharp2(buffer).rotate().toBuffer();
|
|
2292
|
+
const metadata = await sharp2(rotatedBuffer).metadata();
|
|
2238
2293
|
meta[imageKey] = {
|
|
2239
2294
|
o: { w: metadata.width || 0, h: metadata.height || 0 }
|
|
2240
2295
|
};
|
|
@@ -2253,7 +2308,10 @@ async function handleUpload(request) {
|
|
|
2253
2308
|
} catch (error) {
|
|
2254
2309
|
console.error("Failed to upload:", error);
|
|
2255
2310
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2256
|
-
return jsonResponse(
|
|
2311
|
+
return jsonResponse(
|
|
2312
|
+
{ error: `Failed to upload file: ${message}` },
|
|
2313
|
+
{ status: 500 }
|
|
2314
|
+
);
|
|
2257
2315
|
}
|
|
2258
2316
|
}
|
|
2259
2317
|
async function handleDelete(request) {
|
|
@@ -2383,9 +2441,11 @@ async function handleDeleteStream(request) {
|
|
|
2383
2441
|
async start(controller) {
|
|
2384
2442
|
const sendEvent = (data) => {
|
|
2385
2443
|
try {
|
|
2386
|
-
controller.enqueue(
|
|
2444
|
+
controller.enqueue(
|
|
2445
|
+
encoder.encode(`data: ${JSON.stringify(data)}
|
|
2387
2446
|
|
|
2388
|
-
`)
|
|
2447
|
+
`)
|
|
2448
|
+
);
|
|
2389
2449
|
} catch {
|
|
2390
2450
|
}
|
|
2391
2451
|
};
|
|
@@ -2564,7 +2624,7 @@ async function handleDeleteStream(request) {
|
|
|
2564
2624
|
headers: {
|
|
2565
2625
|
"Content-Type": "text/event-stream",
|
|
2566
2626
|
"Cache-Control": "no-cache",
|
|
2567
|
-
|
|
2627
|
+
Connection: "keep-alive"
|
|
2568
2628
|
}
|
|
2569
2629
|
});
|
|
2570
2630
|
}
|
|
@@ -2572,7 +2632,10 @@ async function handleCreateFolder(request) {
|
|
|
2572
2632
|
try {
|
|
2573
2633
|
const { parentPath, name } = await request.json();
|
|
2574
2634
|
if (!name || typeof name !== "string") {
|
|
2575
|
-
return jsonResponse(
|
|
2635
|
+
return jsonResponse(
|
|
2636
|
+
{ error: "Folder name is required" },
|
|
2637
|
+
{ status: 400 }
|
|
2638
|
+
);
|
|
2576
2639
|
}
|
|
2577
2640
|
const sanitizedName = slugifyFolderName(name);
|
|
2578
2641
|
if (!sanitizedName) {
|
|
@@ -2585,11 +2648,17 @@ async function handleCreateFolder(request) {
|
|
|
2585
2648
|
}
|
|
2586
2649
|
try {
|
|
2587
2650
|
await fs7.access(folderPath);
|
|
2588
|
-
return jsonResponse(
|
|
2651
|
+
return jsonResponse(
|
|
2652
|
+
{ error: "A folder with this name already exists" },
|
|
2653
|
+
{ status: 400 }
|
|
2654
|
+
);
|
|
2589
2655
|
} catch {
|
|
2590
2656
|
}
|
|
2591
2657
|
await fs7.mkdir(folderPath, { recursive: true });
|
|
2592
|
-
return jsonResponse({
|
|
2658
|
+
return jsonResponse({
|
|
2659
|
+
success: true,
|
|
2660
|
+
path: path7.join(safePath, sanitizedName)
|
|
2661
|
+
});
|
|
2593
2662
|
} catch (error) {
|
|
2594
2663
|
console.error("Failed to create folder:", error);
|
|
2595
2664
|
return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
|
|
@@ -2600,7 +2669,10 @@ async function handleRename(request) {
|
|
|
2600
2669
|
try {
|
|
2601
2670
|
const { oldPath, newName } = await request.json();
|
|
2602
2671
|
if (!oldPath || !newName) {
|
|
2603
|
-
return jsonResponse(
|
|
2672
|
+
return jsonResponse(
|
|
2673
|
+
{ error: "Path and new name are required" },
|
|
2674
|
+
{ status: 400 }
|
|
2675
|
+
);
|
|
2604
2676
|
}
|
|
2605
2677
|
const safePath = oldPath.replace(/\.\./g, "");
|
|
2606
2678
|
const absoluteOldPath = getWorkspacePath(safePath);
|
|
@@ -2625,7 +2697,10 @@ async function handleRename(request) {
|
|
|
2625
2697
|
isFile = stats.isFile();
|
|
2626
2698
|
} catch {
|
|
2627
2699
|
if (!isInCloud) {
|
|
2628
|
-
return jsonResponse(
|
|
2700
|
+
return jsonResponse(
|
|
2701
|
+
{ error: "File or folder not found" },
|
|
2702
|
+
{ status: 404 }
|
|
2703
|
+
);
|
|
2629
2704
|
}
|
|
2630
2705
|
}
|
|
2631
2706
|
const sanitizedName = isFile ? slugifyFilename(newName) : slugifyFolderName(newName);
|
|
@@ -2634,14 +2709,23 @@ async function handleRename(request) {
|
|
|
2634
2709
|
}
|
|
2635
2710
|
const parentDir = path7.dirname(absoluteOldPath);
|
|
2636
2711
|
const absoluteNewPath = path7.join(parentDir, sanitizedName);
|
|
2637
|
-
const newRelativePath = path7.join(
|
|
2712
|
+
const newRelativePath = path7.join(
|
|
2713
|
+
path7.dirname(oldRelativePath),
|
|
2714
|
+
sanitizedName
|
|
2715
|
+
);
|
|
2638
2716
|
const newKey = "/" + newRelativePath;
|
|
2639
2717
|
if (meta[newKey]) {
|
|
2640
|
-
return jsonResponse(
|
|
2718
|
+
return jsonResponse(
|
|
2719
|
+
{ error: "An item with this name already exists" },
|
|
2720
|
+
{ status: 400 }
|
|
2721
|
+
);
|
|
2641
2722
|
}
|
|
2642
2723
|
try {
|
|
2643
2724
|
await fs7.access(absoluteNewPath);
|
|
2644
|
-
return jsonResponse(
|
|
2725
|
+
return jsonResponse(
|
|
2726
|
+
{ error: "An item with this name already exists" },
|
|
2727
|
+
{ status: 400 }
|
|
2728
|
+
);
|
|
2645
2729
|
} catch {
|
|
2646
2730
|
}
|
|
2647
2731
|
if (isInOurR2 && !hasLocalFile) {
|
|
@@ -2699,7 +2783,10 @@ async function handleRenameStream(request) {
|
|
|
2699
2783
|
try {
|
|
2700
2784
|
const { oldPath, newName, operationId } = await request.json();
|
|
2701
2785
|
if (!oldPath || !newName) {
|
|
2702
|
-
sendEvent({
|
|
2786
|
+
sendEvent({
|
|
2787
|
+
type: "error",
|
|
2788
|
+
message: "Path and new name are required"
|
|
2789
|
+
});
|
|
2703
2790
|
controller.close();
|
|
2704
2791
|
return;
|
|
2705
2792
|
}
|
|
@@ -2728,7 +2815,9 @@ async function handleRenameStream(request) {
|
|
|
2728
2815
|
isFile = true;
|
|
2729
2816
|
} else {
|
|
2730
2817
|
const folderPrefix = oldKey2 + "/";
|
|
2731
|
-
const hasChildrenInMeta = Object.keys(meta2).some(
|
|
2818
|
+
const hasChildrenInMeta = Object.keys(meta2).some(
|
|
2819
|
+
(key) => key.startsWith(folderPrefix)
|
|
2820
|
+
);
|
|
2732
2821
|
if (hasChildrenInMeta) {
|
|
2733
2822
|
isFile = false;
|
|
2734
2823
|
isVirtualFolder = true;
|
|
@@ -2747,14 +2836,20 @@ async function handleRenameStream(request) {
|
|
|
2747
2836
|
}
|
|
2748
2837
|
const parentDir = path7.dirname(absoluteOldPath);
|
|
2749
2838
|
const absoluteNewPath = path7.join(parentDir, sanitizedName);
|
|
2750
|
-
const newRelativePath = path7.join(
|
|
2839
|
+
const newRelativePath = path7.join(
|
|
2840
|
+
path7.dirname(oldRelativePath),
|
|
2841
|
+
sanitizedName
|
|
2842
|
+
);
|
|
2751
2843
|
const newPath = path7.join(path7.dirname(safePath), sanitizedName);
|
|
2752
2844
|
const meta = await loadMeta();
|
|
2753
2845
|
const cdnUrls = getCdnUrls(meta);
|
|
2754
2846
|
if (isFile) {
|
|
2755
2847
|
const newKey2 = "/" + newRelativePath;
|
|
2756
2848
|
if (meta[newKey2]) {
|
|
2757
|
-
sendEvent({
|
|
2849
|
+
sendEvent({
|
|
2850
|
+
type: "error",
|
|
2851
|
+
message: "An item with this name already exists"
|
|
2852
|
+
});
|
|
2758
2853
|
controller.close();
|
|
2759
2854
|
return;
|
|
2760
2855
|
}
|
|
@@ -2762,7 +2857,10 @@ async function handleRenameStream(request) {
|
|
|
2762
2857
|
if (!isVirtualFolder) {
|
|
2763
2858
|
try {
|
|
2764
2859
|
await fs7.access(absoluteNewPath);
|
|
2765
|
-
sendEvent({
|
|
2860
|
+
sendEvent({
|
|
2861
|
+
type: "error",
|
|
2862
|
+
message: "An item with this name already exists"
|
|
2863
|
+
});
|
|
2766
2864
|
controller.close();
|
|
2767
2865
|
return;
|
|
2768
2866
|
} catch {
|
|
@@ -2770,9 +2868,14 @@ async function handleRenameStream(request) {
|
|
|
2770
2868
|
}
|
|
2771
2869
|
if (isVirtualFolder) {
|
|
2772
2870
|
const newPrefix = "/" + newRelativePath + "/";
|
|
2773
|
-
const hasConflict = Object.keys(meta).some(
|
|
2871
|
+
const hasConflict = Object.keys(meta).some(
|
|
2872
|
+
(key) => key.startsWith(newPrefix)
|
|
2873
|
+
);
|
|
2774
2874
|
if (hasConflict) {
|
|
2775
|
-
sendEvent({
|
|
2875
|
+
sendEvent({
|
|
2876
|
+
type: "error",
|
|
2877
|
+
message: "A folder with this name already exists"
|
|
2878
|
+
});
|
|
2776
2879
|
controller.close();
|
|
2777
2880
|
return;
|
|
2778
2881
|
}
|
|
@@ -2784,11 +2887,19 @@ async function handleRenameStream(request) {
|
|
|
2784
2887
|
for (const [key, entry2] of Object.entries(meta)) {
|
|
2785
2888
|
if (key.startsWith(oldPrefix) && entry2 && typeof entry2 === "object") {
|
|
2786
2889
|
const newKey2 = key.replace(oldPrefix, newPrefix);
|
|
2787
|
-
itemsToUpdate.push({
|
|
2890
|
+
itemsToUpdate.push({
|
|
2891
|
+
oldKey: key,
|
|
2892
|
+
newKey: newKey2,
|
|
2893
|
+
entry: entry2
|
|
2894
|
+
});
|
|
2788
2895
|
}
|
|
2789
2896
|
}
|
|
2790
2897
|
const total = itemsToUpdate.length + 1;
|
|
2791
|
-
sendEvent({
|
|
2898
|
+
sendEvent({
|
|
2899
|
+
type: "start",
|
|
2900
|
+
total,
|
|
2901
|
+
message: `Renaming folder with ${itemsToUpdate.length} item(s)...`
|
|
2902
|
+
});
|
|
2792
2903
|
if (hasLocalItem) {
|
|
2793
2904
|
await fs7.rename(absoluteOldPath, absoluteNewPath);
|
|
2794
2905
|
const imagesDir = getPublicPath("/images");
|
|
@@ -2801,12 +2912,21 @@ async function handleRenameStream(request) {
|
|
|
2801
2912
|
} catch {
|
|
2802
2913
|
}
|
|
2803
2914
|
}
|
|
2804
|
-
sendEvent({
|
|
2915
|
+
sendEvent({
|
|
2916
|
+
type: "progress",
|
|
2917
|
+
current: 1,
|
|
2918
|
+
total,
|
|
2919
|
+
renamed: 1,
|
|
2920
|
+
message: "Renamed folder"
|
|
2921
|
+
});
|
|
2805
2922
|
let renamed = 1;
|
|
2806
2923
|
const handleRenameCancel = async () => {
|
|
2807
2924
|
await saveMeta(meta);
|
|
2808
2925
|
await deleteEmptyFolders(absoluteOldPath);
|
|
2809
|
-
const oldThumbFolder2 = path7.join(
|
|
2926
|
+
const oldThumbFolder2 = path7.join(
|
|
2927
|
+
getPublicPath("/images"),
|
|
2928
|
+
oldRelativePath
|
|
2929
|
+
);
|
|
2810
2930
|
await deleteEmptyFolders(oldThumbFolder2);
|
|
2811
2931
|
sendEvent({ type: "complete", renamed, newPath, cancelled: true });
|
|
2812
2932
|
controller.close();
|
|
@@ -2849,7 +2969,10 @@ async function handleRenameStream(request) {
|
|
|
2849
2969
|
});
|
|
2850
2970
|
}
|
|
2851
2971
|
await deleteEmptyFolders(absoluteOldPath);
|
|
2852
|
-
const oldThumbFolder = path7.join(
|
|
2972
|
+
const oldThumbFolder = path7.join(
|
|
2973
|
+
getPublicPath("/images"),
|
|
2974
|
+
oldRelativePath
|
|
2975
|
+
);
|
|
2853
2976
|
await deleteEmptyFolders(oldThumbFolder);
|
|
2854
2977
|
sendEvent({ type: "complete", renamed, newPath });
|
|
2855
2978
|
controller.close();
|
|
@@ -2972,7 +3095,9 @@ async function handleMoveStream(request) {
|
|
|
2972
3095
|
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2973
3096
|
for (const entry of entries) {
|
|
2974
3097
|
if (entry.isDirectory()) {
|
|
2975
|
-
count += await countFilesRecursive(
|
|
3098
|
+
count += await countFilesRecursive(
|
|
3099
|
+
path7.join(dir, entry.name)
|
|
3100
|
+
);
|
|
2976
3101
|
} else {
|
|
2977
3102
|
count++;
|
|
2978
3103
|
}
|
|
@@ -2994,7 +3119,15 @@ async function handleMoveStream(request) {
|
|
|
2994
3119
|
}
|
|
2995
3120
|
}
|
|
2996
3121
|
totalFiles += localFileCount + cloudOnlyCount;
|
|
2997
|
-
expandedItems.push({
|
|
3122
|
+
expandedItems.push({
|
|
3123
|
+
itemPath,
|
|
3124
|
+
safePath,
|
|
3125
|
+
itemName,
|
|
3126
|
+
oldKey,
|
|
3127
|
+
newKey,
|
|
3128
|
+
newAbsolutePath,
|
|
3129
|
+
isVirtualFolder: false
|
|
3130
|
+
});
|
|
2998
3131
|
} else if (!hasLocalItem) {
|
|
2999
3132
|
const folderPrefix = oldKey + "/";
|
|
3000
3133
|
const virtualItems = [];
|
|
@@ -3002,20 +3135,49 @@ async function handleMoveStream(request) {
|
|
|
3002
3135
|
if (key.startsWith(folderPrefix) && metaEntry && typeof metaEntry === "object") {
|
|
3003
3136
|
const relativePath = key.slice(folderPrefix.length);
|
|
3004
3137
|
const destNewKey = newKey + "/" + relativePath;
|
|
3005
|
-
virtualItems.push({
|
|
3138
|
+
virtualItems.push({
|
|
3139
|
+
oldKey: key,
|
|
3140
|
+
newKey: destNewKey,
|
|
3141
|
+
entry: metaEntry
|
|
3142
|
+
});
|
|
3006
3143
|
}
|
|
3007
3144
|
}
|
|
3008
3145
|
if (virtualItems.length > 0) {
|
|
3009
3146
|
totalFiles += virtualItems.length;
|
|
3010
|
-
expandedItems.push({
|
|
3147
|
+
expandedItems.push({
|
|
3148
|
+
itemPath,
|
|
3149
|
+
safePath,
|
|
3150
|
+
itemName,
|
|
3151
|
+
oldKey,
|
|
3152
|
+
newKey,
|
|
3153
|
+
newAbsolutePath,
|
|
3154
|
+
isVirtualFolder: true,
|
|
3155
|
+
virtualFolderItems: virtualItems
|
|
3156
|
+
});
|
|
3011
3157
|
sourceFolders.add(absolutePath);
|
|
3012
3158
|
} else {
|
|
3013
3159
|
totalFiles++;
|
|
3014
|
-
expandedItems.push({
|
|
3160
|
+
expandedItems.push({
|
|
3161
|
+
itemPath,
|
|
3162
|
+
safePath,
|
|
3163
|
+
itemName,
|
|
3164
|
+
oldKey,
|
|
3165
|
+
newKey,
|
|
3166
|
+
newAbsolutePath,
|
|
3167
|
+
isVirtualFolder: false
|
|
3168
|
+
});
|
|
3015
3169
|
}
|
|
3016
3170
|
} else {
|
|
3017
3171
|
totalFiles++;
|
|
3018
|
-
expandedItems.push({
|
|
3172
|
+
expandedItems.push({
|
|
3173
|
+
itemPath,
|
|
3174
|
+
safePath,
|
|
3175
|
+
itemName,
|
|
3176
|
+
oldKey,
|
|
3177
|
+
newKey,
|
|
3178
|
+
newAbsolutePath,
|
|
3179
|
+
isVirtualFolder: false
|
|
3180
|
+
});
|
|
3019
3181
|
}
|
|
3020
3182
|
}
|
|
3021
3183
|
sendEvent({ type: "start", total: totalFiles });
|
|
@@ -3027,7 +3189,13 @@ async function handleMoveStream(request) {
|
|
|
3027
3189
|
await deleteEmptyFolders(folder);
|
|
3028
3190
|
}
|
|
3029
3191
|
await deleteEmptyFolders(absoluteDestination);
|
|
3030
|
-
sendEvent({
|
|
3192
|
+
sendEvent({
|
|
3193
|
+
type: "complete",
|
|
3194
|
+
moved: filesMoved,
|
|
3195
|
+
errors: errors.length,
|
|
3196
|
+
errorMessages: errors,
|
|
3197
|
+
cancelled: true
|
|
3198
|
+
});
|
|
3031
3199
|
controller.close();
|
|
3032
3200
|
};
|
|
3033
3201
|
for (const expandedItem of expandedItems) {
|
|
@@ -3035,7 +3203,16 @@ async function handleMoveStream(request) {
|
|
|
3035
3203
|
await handleCancel();
|
|
3036
3204
|
return;
|
|
3037
3205
|
}
|
|
3038
|
-
const {
|
|
3206
|
+
const {
|
|
3207
|
+
itemPath,
|
|
3208
|
+
safePath,
|
|
3209
|
+
itemName,
|
|
3210
|
+
oldKey,
|
|
3211
|
+
newKey,
|
|
3212
|
+
newAbsolutePath,
|
|
3213
|
+
isVirtualFolder,
|
|
3214
|
+
virtualFolderItems
|
|
3215
|
+
} = expandedItem;
|
|
3039
3216
|
if (isVirtualFolder && virtualFolderItems) {
|
|
3040
3217
|
for (const vItem of virtualFolderItems) {
|
|
3041
3218
|
if (isCancelled()) {
|
|
@@ -3050,11 +3227,18 @@ async function handleMoveStream(request) {
|
|
|
3050
3227
|
let vItemMoved = false;
|
|
3051
3228
|
if (isItemInR2) {
|
|
3052
3229
|
try {
|
|
3053
|
-
await moveInCdn(
|
|
3230
|
+
await moveInCdn(
|
|
3231
|
+
vItem.oldKey,
|
|
3232
|
+
vItem.newKey,
|
|
3233
|
+
itemHasThumbnails
|
|
3234
|
+
);
|
|
3054
3235
|
vItemMoved = true;
|
|
3055
3236
|
filesMoved++;
|
|
3056
3237
|
} catch (err) {
|
|
3057
|
-
console.error(
|
|
3238
|
+
console.error(
|
|
3239
|
+
`Failed to move cloud item ${vItem.oldKey}:`,
|
|
3240
|
+
err
|
|
3241
|
+
);
|
|
3058
3242
|
delete meta[vItem.oldKey];
|
|
3059
3243
|
await saveMeta(meta);
|
|
3060
3244
|
}
|
|
@@ -3078,7 +3262,10 @@ async function handleMoveStream(request) {
|
|
|
3078
3262
|
}
|
|
3079
3263
|
const newFolderPath = getPublicPath(newKey);
|
|
3080
3264
|
await deleteEmptyFolders(newFolderPath);
|
|
3081
|
-
const newThumbFolder = path7.join(
|
|
3265
|
+
const newThumbFolder = path7.join(
|
|
3266
|
+
getPublicPath("images"),
|
|
3267
|
+
newKey.slice(1)
|
|
3268
|
+
);
|
|
3082
3269
|
await deleteEmptyFolders(newThumbFolder);
|
|
3083
3270
|
const oldFolderPath = getPublicPath(oldKey);
|
|
3084
3271
|
sourceFolders.add(oldFolderPath);
|
|
@@ -3111,7 +3298,9 @@ async function handleMoveStream(request) {
|
|
|
3111
3298
|
if (isRemote) {
|
|
3112
3299
|
const remoteUrl = `${fileCdnUrl}${oldKey}`;
|
|
3113
3300
|
const buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
3114
|
-
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3301
|
+
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3302
|
+
recursive: true
|
|
3303
|
+
});
|
|
3115
3304
|
await fs7.writeFile(newAbsolutePath, buffer);
|
|
3116
3305
|
const newEntry = {
|
|
3117
3306
|
o: entry?.o,
|
|
@@ -3196,7 +3385,9 @@ async function handleMoveStream(request) {
|
|
|
3196
3385
|
}
|
|
3197
3386
|
const stats = await fs7.stat(absolutePath);
|
|
3198
3387
|
if (stats.isFile()) {
|
|
3199
|
-
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3388
|
+
await fs7.mkdir(path7.dirname(newAbsolutePath), {
|
|
3389
|
+
recursive: true
|
|
3390
|
+
});
|
|
3200
3391
|
await fs7.rename(absolutePath, newAbsolutePath);
|
|
3201
3392
|
if (isImage && entry) {
|
|
3202
3393
|
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
@@ -3207,7 +3398,9 @@ async function handleMoveStream(request) {
|
|
|
3207
3398
|
try {
|
|
3208
3399
|
await fs7.access(oldThumbPath);
|
|
3209
3400
|
sourceFolders.add(path7.dirname(oldThumbPath));
|
|
3210
|
-
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3401
|
+
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3402
|
+
recursive: true
|
|
3403
|
+
});
|
|
3211
3404
|
await fs7.rename(oldThumbPath, newThumbPath);
|
|
3212
3405
|
} catch {
|
|
3213
3406
|
}
|
|
@@ -3243,13 +3436,21 @@ async function handleMoveStream(request) {
|
|
|
3243
3436
|
const newPrefix = newKey + "/";
|
|
3244
3437
|
const localFiles = [];
|
|
3245
3438
|
const collectLocalFiles = async (dir, relativeDir) => {
|
|
3246
|
-
const entries = await fs7.readdir(dir, {
|
|
3439
|
+
const entries = await fs7.readdir(dir, {
|
|
3440
|
+
withFileTypes: true
|
|
3441
|
+
});
|
|
3247
3442
|
for (const dirEntry of entries) {
|
|
3248
3443
|
const entryRelPath = relativeDir ? `${relativeDir}/${dirEntry.name}` : dirEntry.name;
|
|
3249
3444
|
if (dirEntry.isDirectory()) {
|
|
3250
|
-
await collectLocalFiles(
|
|
3445
|
+
await collectLocalFiles(
|
|
3446
|
+
path7.join(dir, dirEntry.name),
|
|
3447
|
+
entryRelPath
|
|
3448
|
+
);
|
|
3251
3449
|
} else {
|
|
3252
|
-
localFiles.push({
|
|
3450
|
+
localFiles.push({
|
|
3451
|
+
relativePath: entryRelPath,
|
|
3452
|
+
isImage: isImageFile(dirEntry.name)
|
|
3453
|
+
});
|
|
3253
3454
|
}
|
|
3254
3455
|
}
|
|
3255
3456
|
};
|
|
@@ -3275,13 +3476,21 @@ async function handleMoveStream(request) {
|
|
|
3275
3476
|
await handleCancel();
|
|
3276
3477
|
return;
|
|
3277
3478
|
}
|
|
3278
|
-
const fileOldPath = path7.join(
|
|
3279
|
-
|
|
3479
|
+
const fileOldPath = path7.join(
|
|
3480
|
+
absolutePath,
|
|
3481
|
+
localFile.relativePath
|
|
3482
|
+
);
|
|
3483
|
+
const fileNewPath = path7.join(
|
|
3484
|
+
newAbsolutePath,
|
|
3485
|
+
localFile.relativePath
|
|
3486
|
+
);
|
|
3280
3487
|
const fileOldKey = oldPrefix + localFile.relativePath;
|
|
3281
3488
|
const fileNewKey = newPrefix + localFile.relativePath;
|
|
3282
3489
|
const fileEntry = meta[fileOldKey];
|
|
3283
3490
|
sourceFolders.add(path7.dirname(fileOldPath));
|
|
3284
|
-
await fs7.mkdir(path7.dirname(fileNewPath), {
|
|
3491
|
+
await fs7.mkdir(path7.dirname(fileNewPath), {
|
|
3492
|
+
recursive: true
|
|
3493
|
+
});
|
|
3285
3494
|
await fs7.rename(fileOldPath, fileNewPath);
|
|
3286
3495
|
filesMoved++;
|
|
3287
3496
|
if (localFile.isImage && fileEntry) {
|
|
@@ -3293,7 +3502,9 @@ async function handleMoveStream(request) {
|
|
|
3293
3502
|
try {
|
|
3294
3503
|
await fs7.access(oldThumbPath);
|
|
3295
3504
|
sourceFolders.add(path7.dirname(oldThumbPath));
|
|
3296
|
-
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3505
|
+
await fs7.mkdir(path7.dirname(newThumbPath), {
|
|
3506
|
+
recursive: true
|
|
3507
|
+
});
|
|
3297
3508
|
await fs7.rename(oldThumbPath, newThumbPath);
|
|
3298
3509
|
} catch {
|
|
3299
3510
|
}
|
|
@@ -3332,11 +3543,18 @@ async function handleMoveStream(request) {
|
|
|
3332
3543
|
let cloudFileMoved = false;
|
|
3333
3544
|
if (cloudIsInR2) {
|
|
3334
3545
|
try {
|
|
3335
|
-
await moveInCdn(
|
|
3546
|
+
await moveInCdn(
|
|
3547
|
+
cloudFile.oldKey,
|
|
3548
|
+
cloudFile.newKey,
|
|
3549
|
+
cloudHasThumbs
|
|
3550
|
+
);
|
|
3336
3551
|
cloudFileMoved = true;
|
|
3337
3552
|
filesMoved++;
|
|
3338
3553
|
} catch (err) {
|
|
3339
|
-
console.error(
|
|
3554
|
+
console.error(
|
|
3555
|
+
`Failed to move cloud file ${cloudFile.oldKey}:`,
|
|
3556
|
+
err
|
|
3557
|
+
);
|
|
3340
3558
|
delete meta[cloudFile.oldKey];
|
|
3341
3559
|
await saveMeta(meta);
|
|
3342
3560
|
}
|
|
@@ -3358,7 +3576,10 @@ async function handleMoveStream(request) {
|
|
|
3358
3576
|
}
|
|
3359
3577
|
sourceFolders.add(absolutePath);
|
|
3360
3578
|
const oldThumbRelPath = oldKey.slice(1);
|
|
3361
|
-
const oldThumbFolder = path7.join(
|
|
3579
|
+
const oldThumbFolder = path7.join(
|
|
3580
|
+
getPublicPath("images"),
|
|
3581
|
+
oldThumbRelPath
|
|
3582
|
+
);
|
|
3362
3583
|
sourceFolders.add(oldThumbFolder);
|
|
3363
3584
|
moved.push(itemPath);
|
|
3364
3585
|
}
|
|
@@ -3400,7 +3621,7 @@ async function handleMoveStream(request) {
|
|
|
3400
3621
|
headers: {
|
|
3401
3622
|
"Content-Type": "text/event-stream",
|
|
3402
3623
|
"Cache-Control": "no-cache",
|
|
3403
|
-
|
|
3624
|
+
Connection: "keep-alive"
|
|
3404
3625
|
}
|
|
3405
3626
|
});
|
|
3406
3627
|
}
|
|
@@ -3420,7 +3641,9 @@ async function handleScanStream() {
|
|
|
3420
3641
|
};
|
|
3421
3642
|
try {
|
|
3422
3643
|
const meta = await loadMeta();
|
|
3423
|
-
const existingCount = Object.keys(meta).filter(
|
|
3644
|
+
const existingCount = Object.keys(meta).filter(
|
|
3645
|
+
(k) => !k.startsWith("_")
|
|
3646
|
+
).length;
|
|
3424
3647
|
const existingKeys = new Set(Object.keys(meta));
|
|
3425
3648
|
const added = [];
|
|
3426
3649
|
const renamed = [];
|
|
@@ -3435,7 +3658,8 @@ async function handleScanStream() {
|
|
|
3435
3658
|
if (entry.name.startsWith(".")) continue;
|
|
3436
3659
|
const fullPath = path8.join(dir, entry.name);
|
|
3437
3660
|
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
3438
|
-
if (relPath === "images" || relPath.startsWith("images/"))
|
|
3661
|
+
if (relPath === "images" || relPath.startsWith("images/"))
|
|
3662
|
+
continue;
|
|
3439
3663
|
if (entry.isDirectory()) {
|
|
3440
3664
|
await scanDir(fullPath, relPath);
|
|
3441
3665
|
} else if (isMediaFile(entry.name)) {
|
|
@@ -3519,7 +3743,8 @@ async function handleScanStream() {
|
|
|
3519
3743
|
} else {
|
|
3520
3744
|
try {
|
|
3521
3745
|
const buffer = await fs8.readFile(fullPath);
|
|
3522
|
-
const
|
|
3746
|
+
const rotatedBuffer = await sharp3(buffer).rotate().toBuffer();
|
|
3747
|
+
const metadata = await sharp3(rotatedBuffer).metadata();
|
|
3523
3748
|
meta[imageKey] = {
|
|
3524
3749
|
o: { w: metadata.width || 0, h: metadata.height || 0 }
|
|
3525
3750
|
};
|
|
@@ -3540,7 +3765,10 @@ async function handleScanStream() {
|
|
|
3540
3765
|
errors.push(relativePath);
|
|
3541
3766
|
}
|
|
3542
3767
|
}
|
|
3543
|
-
sendEvent({
|
|
3768
|
+
sendEvent({
|
|
3769
|
+
type: "cleanup",
|
|
3770
|
+
message: "Checking for orphaned thumbnails..."
|
|
3771
|
+
});
|
|
3544
3772
|
const expectedThumbnails = /* @__PURE__ */ new Set();
|
|
3545
3773
|
const fileEntries = getFileEntries(meta);
|
|
3546
3774
|
for (const [imageKey, entry] of fileEntries) {
|
|
@@ -3587,7 +3815,9 @@ async function handleScanStream() {
|
|
|
3587
3815
|
await cleanEmptyFolders(fullPath);
|
|
3588
3816
|
try {
|
|
3589
3817
|
const subEntries = await fs8.readdir(fullPath);
|
|
3590
|
-
const meaningfulEntries = subEntries.filter(
|
|
3818
|
+
const meaningfulEntries = subEntries.filter(
|
|
3819
|
+
(e) => !e.startsWith(".")
|
|
3820
|
+
);
|
|
3591
3821
|
if (meaningfulEntries.length === 0) {
|
|
3592
3822
|
await fs8.rm(fullPath, { recursive: true });
|
|
3593
3823
|
emptyFoldersDeleted++;
|
|
@@ -3605,7 +3835,9 @@ async function handleScanStream() {
|
|
|
3605
3835
|
let isEmpty = true;
|
|
3606
3836
|
for (const entry of entries) {
|
|
3607
3837
|
if (entry.isDirectory()) {
|
|
3608
|
-
const subDirEmpty = await cleanImagesEmptyFolders(
|
|
3838
|
+
const subDirEmpty = await cleanImagesEmptyFolders(
|
|
3839
|
+
path8.join(dir, entry.name)
|
|
3840
|
+
);
|
|
3609
3841
|
if (!subDirEmpty) isEmpty = false;
|
|
3610
3842
|
} else if (!entry.name.startsWith(".")) {
|
|
3611
3843
|
isEmpty = false;
|
|
@@ -3624,7 +3856,10 @@ async function handleScanStream() {
|
|
|
3624
3856
|
await cleanImagesEmptyFolders(imagesDir);
|
|
3625
3857
|
} catch {
|
|
3626
3858
|
}
|
|
3627
|
-
sendEvent({
|
|
3859
|
+
sendEvent({
|
|
3860
|
+
type: "cleanup",
|
|
3861
|
+
message: "Checking for orphaned entries..."
|
|
3862
|
+
});
|
|
3628
3863
|
const orphanedEntries = [];
|
|
3629
3864
|
const cdnUrls = meta._cdns || [];
|
|
3630
3865
|
const r2PublicUrl = (process.env.CLOUDFLARE_R2_PUBLIC_URL || "").replace(/\/$/, "");
|
|
@@ -3652,7 +3887,10 @@ async function handleScanStream() {
|
|
|
3652
3887
|
}
|
|
3653
3888
|
}
|
|
3654
3889
|
if (orphanedEntries.length > 0) {
|
|
3655
|
-
sendEvent({
|
|
3890
|
+
sendEvent({
|
|
3891
|
+
type: "cleanup",
|
|
3892
|
+
message: `Removed ${orphanedEntries.length} orphaned entries...`
|
|
3893
|
+
});
|
|
3656
3894
|
}
|
|
3657
3895
|
await saveMeta(meta);
|
|
3658
3896
|
sendEvent({
|
|
@@ -3679,7 +3917,7 @@ async function handleScanStream() {
|
|
|
3679
3917
|
headers: {
|
|
3680
3918
|
"Content-Type": "text/event-stream",
|
|
3681
3919
|
"Cache-Control": "no-cache",
|
|
3682
|
-
|
|
3920
|
+
Connection: "keep-alive"
|
|
3683
3921
|
}
|
|
3684
3922
|
});
|
|
3685
3923
|
}
|
|
@@ -3717,7 +3955,10 @@ async function handleDeleteOrphans(request) {
|
|
|
3717
3955
|
});
|
|
3718
3956
|
} catch (error) {
|
|
3719
3957
|
console.error("Failed to delete orphans:", error);
|
|
3720
|
-
return jsonResponse(
|
|
3958
|
+
return jsonResponse(
|
|
3959
|
+
{ error: "Failed to delete orphaned files" },
|
|
3960
|
+
{ status: 500 }
|
|
3961
|
+
);
|
|
3721
3962
|
}
|
|
3722
3963
|
}
|
|
3723
3964
|
|
|
@@ -3735,9 +3976,11 @@ async function processRemoteImage(url) {
|
|
|
3735
3976
|
throw new Error(`Failed to fetch: ${response.status}`);
|
|
3736
3977
|
}
|
|
3737
3978
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3738
|
-
const
|
|
3979
|
+
const rotatedBuffer = await sharp4(buffer).rotate().toBuffer();
|
|
3980
|
+
const metadata = await sharp4(rotatedBuffer).metadata();
|
|
3739
3981
|
return {
|
|
3740
3982
|
o: { w: metadata.width || 0, h: metadata.height || 0 }
|
|
3983
|
+
// b: blur hash would be generated here if needed
|
|
3741
3984
|
};
|
|
3742
3985
|
}
|
|
3743
3986
|
async function handleImportUrls(request) {
|
|
@@ -3746,9 +3989,11 @@ async function handleImportUrls(request) {
|
|
|
3746
3989
|
async start(controller) {
|
|
3747
3990
|
const sendEvent = (data) => {
|
|
3748
3991
|
try {
|
|
3749
|
-
controller.enqueue(
|
|
3992
|
+
controller.enqueue(
|
|
3993
|
+
encoder.encode(`data: ${JSON.stringify(data)}
|
|
3750
3994
|
|
|
3751
|
-
`)
|
|
3995
|
+
`)
|
|
3996
|
+
);
|
|
3752
3997
|
} catch {
|
|
3753
3998
|
}
|
|
3754
3999
|
};
|
|
@@ -3848,7 +4093,7 @@ async function handleImportUrls(request) {
|
|
|
3848
4093
|
headers: {
|
|
3849
4094
|
"Content-Type": "text/event-stream",
|
|
3850
4095
|
"Cache-Control": "no-cache",
|
|
3851
|
-
|
|
4096
|
+
Connection: "keep-alive"
|
|
3852
4097
|
}
|
|
3853
4098
|
});
|
|
3854
4099
|
}
|
|
@@ -3901,9 +4146,12 @@ async function handleGenerateFavicon(request) {
|
|
|
3901
4146
|
}
|
|
3902
4147
|
const fileName = path9.basename(imagePath).toLowerCase();
|
|
3903
4148
|
if (fileName !== "favicon.png" && fileName !== "favicon.jpg") {
|
|
3904
|
-
return jsonResponse(
|
|
3905
|
-
|
|
3906
|
-
|
|
4149
|
+
return jsonResponse(
|
|
4150
|
+
{
|
|
4151
|
+
error: "Source file must be named favicon.png or favicon.jpg"
|
|
4152
|
+
},
|
|
4153
|
+
{ status: 400 }
|
|
4154
|
+
);
|
|
3907
4155
|
}
|
|
3908
4156
|
const sourcePath = getPublicPath(imagePath.replace(/^\//, ""));
|
|
3909
4157
|
try {
|
|
@@ -3913,17 +4161,24 @@ async function handleGenerateFavicon(request) {
|
|
|
3913
4161
|
}
|
|
3914
4162
|
let metadata;
|
|
3915
4163
|
try {
|
|
3916
|
-
|
|
4164
|
+
const rotatedBuffer = await sharp5(sourcePath).rotate().toBuffer();
|
|
4165
|
+
metadata = await sharp5(rotatedBuffer).metadata();
|
|
3917
4166
|
} catch {
|
|
3918
|
-
return jsonResponse(
|
|
4167
|
+
return jsonResponse(
|
|
4168
|
+
{ error: "Source file is not a valid image" },
|
|
4169
|
+
{ status: 400 }
|
|
4170
|
+
);
|
|
3919
4171
|
}
|
|
3920
4172
|
const outputDir = getSrcAppPath();
|
|
3921
4173
|
try {
|
|
3922
4174
|
await fs9.access(outputDir);
|
|
3923
4175
|
} catch {
|
|
3924
|
-
return jsonResponse(
|
|
3925
|
-
|
|
3926
|
-
|
|
4176
|
+
return jsonResponse(
|
|
4177
|
+
{
|
|
4178
|
+
error: "Output directory src/app/ not found"
|
|
4179
|
+
},
|
|
4180
|
+
{ status: 500 }
|
|
4181
|
+
);
|
|
3927
4182
|
}
|
|
3928
4183
|
const stream = new ReadableStream({
|
|
3929
4184
|
async start(controller) {
|
|
@@ -3945,7 +4200,7 @@ async function handleGenerateFavicon(request) {
|
|
|
3945
4200
|
const config = FAVICON_CONFIGS[i];
|
|
3946
4201
|
try {
|
|
3947
4202
|
const outputPath = path9.join(outputDir, config.name);
|
|
3948
|
-
await sharp5(sourcePath).resize(config.size, config.size, {
|
|
4203
|
+
await sharp5(sourcePath).rotate().resize(config.size, config.size, {
|
|
3949
4204
|
fit: "cover",
|
|
3950
4205
|
position: "center"
|
|
3951
4206
|
}).png({ quality: 100 }).toFile(outputPath);
|