@gallop.software/studio 2.1.5 → 2.1.6
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 +135 -91
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -891,9 +891,35 @@ async function handleFolderImages(request) {
|
|
|
891
891
|
}
|
|
892
892
|
|
|
893
893
|
// src/handlers/files.ts
|
|
894
|
+
import { promises as fs6 } from "fs";
|
|
895
|
+
import path6 from "path";
|
|
896
|
+
import sharp2 from "sharp";
|
|
897
|
+
|
|
898
|
+
// src/handlers/utils/folders.ts
|
|
894
899
|
import { promises as fs5 } from "fs";
|
|
895
900
|
import path5 from "path";
|
|
896
|
-
|
|
901
|
+
async function deleteEmptyFolders(folderPath) {
|
|
902
|
+
const publicPath = getPublicPath();
|
|
903
|
+
const normalizedFolder = path5.resolve(folderPath);
|
|
904
|
+
const normalizedPublic = path5.resolve(publicPath);
|
|
905
|
+
if (normalizedFolder === normalizedPublic) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (!normalizedFolder.startsWith(normalizedPublic)) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
try {
|
|
912
|
+
const entries = await fs5.readdir(folderPath);
|
|
913
|
+
if (entries.length === 0) {
|
|
914
|
+
await fs5.rmdir(folderPath);
|
|
915
|
+
const parentFolder = path5.dirname(folderPath);
|
|
916
|
+
await deleteEmptyFolders(parentFolder);
|
|
917
|
+
}
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// src/handlers/files.ts
|
|
897
923
|
async function handleUpload(request) {
|
|
898
924
|
try {
|
|
899
925
|
const formData = await request.formData();
|
|
@@ -905,7 +931,7 @@ async function handleUpload(request) {
|
|
|
905
931
|
const bytes = await file.arrayBuffer();
|
|
906
932
|
const buffer = Buffer.from(bytes);
|
|
907
933
|
const fileName = file.name;
|
|
908
|
-
const ext =
|
|
934
|
+
const ext = path6.extname(fileName).toLowerCase();
|
|
909
935
|
const isImage = isImageFile(fileName);
|
|
910
936
|
const isMedia = isMediaFile(fileName);
|
|
911
937
|
const meta = await loadMeta();
|
|
@@ -923,7 +949,7 @@ async function handleUpload(request) {
|
|
|
923
949
|
}
|
|
924
950
|
let imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
|
|
925
951
|
if (meta[imageKey]) {
|
|
926
|
-
const baseName =
|
|
952
|
+
const baseName = path6.basename(fileName, ext);
|
|
927
953
|
let counter = 1;
|
|
928
954
|
let newFileName = `${baseName}-${counter}${ext}`;
|
|
929
955
|
let newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
|
|
@@ -934,10 +960,10 @@ async function handleUpload(request) {
|
|
|
934
960
|
}
|
|
935
961
|
imageKey = newKey;
|
|
936
962
|
}
|
|
937
|
-
const actualFileName =
|
|
963
|
+
const actualFileName = path6.basename(imageKey);
|
|
938
964
|
const uploadDir = getPublicPath(relativeDir);
|
|
939
|
-
await
|
|
940
|
-
await
|
|
965
|
+
await fs6.mkdir(uploadDir, { recursive: true });
|
|
966
|
+
await fs6.writeFile(path6.join(uploadDir, actualFileName), buffer);
|
|
941
967
|
if (!isMedia) {
|
|
942
968
|
return jsonResponse({
|
|
943
969
|
success: true,
|
|
@@ -978,6 +1004,7 @@ async function handleDelete(request) {
|
|
|
978
1004
|
const meta = await loadMeta();
|
|
979
1005
|
const deleted = [];
|
|
980
1006
|
const errors = [];
|
|
1007
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
981
1008
|
for (const itemPath of paths) {
|
|
982
1009
|
try {
|
|
983
1010
|
if (!itemPath.startsWith("public/")) {
|
|
@@ -986,12 +1013,13 @@ async function handleDelete(request) {
|
|
|
986
1013
|
}
|
|
987
1014
|
const absolutePath = getWorkspacePath(itemPath);
|
|
988
1015
|
const imageKey = "/" + itemPath.replace(/^public\//, "");
|
|
1016
|
+
sourceFolders.add(path6.dirname(absolutePath));
|
|
989
1017
|
const entry = meta[imageKey];
|
|
990
1018
|
const isPushedToCloud = entry?.c !== void 0;
|
|
991
1019
|
try {
|
|
992
|
-
const stats = await
|
|
1020
|
+
const stats = await fs6.stat(absolutePath);
|
|
993
1021
|
if (stats.isDirectory()) {
|
|
994
|
-
await
|
|
1022
|
+
await fs6.rm(absolutePath, { recursive: true });
|
|
995
1023
|
const prefix = imageKey + "/";
|
|
996
1024
|
for (const key of Object.keys(meta)) {
|
|
997
1025
|
if (key.startsWith(prefix) || key === imageKey) {
|
|
@@ -1000,7 +1028,7 @@ async function handleDelete(request) {
|
|
|
1000
1028
|
for (const thumbPath of getAllThumbnailPaths(key)) {
|
|
1001
1029
|
const absoluteThumbPath = getPublicPath(thumbPath);
|
|
1002
1030
|
try {
|
|
1003
|
-
await
|
|
1031
|
+
await fs6.unlink(absoluteThumbPath);
|
|
1004
1032
|
} catch {
|
|
1005
1033
|
}
|
|
1006
1034
|
}
|
|
@@ -1009,14 +1037,14 @@ async function handleDelete(request) {
|
|
|
1009
1037
|
}
|
|
1010
1038
|
}
|
|
1011
1039
|
} else {
|
|
1012
|
-
await
|
|
1040
|
+
await fs6.unlink(absolutePath);
|
|
1013
1041
|
const isInImagesFolder = itemPath.startsWith("public/images/");
|
|
1014
1042
|
if (!isInImagesFolder && entry) {
|
|
1015
1043
|
if (!isPushedToCloud) {
|
|
1016
1044
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1017
1045
|
const absoluteThumbPath = getPublicPath(thumbPath);
|
|
1018
1046
|
try {
|
|
1019
|
-
await
|
|
1047
|
+
await fs6.unlink(absoluteThumbPath);
|
|
1020
1048
|
} catch {
|
|
1021
1049
|
}
|
|
1022
1050
|
}
|
|
@@ -1049,6 +1077,9 @@ async function handleDelete(request) {
|
|
|
1049
1077
|
}
|
|
1050
1078
|
}
|
|
1051
1079
|
await saveMeta(meta);
|
|
1080
|
+
for (const folder of sourceFolders) {
|
|
1081
|
+
await deleteEmptyFolders(folder);
|
|
1082
|
+
}
|
|
1052
1083
|
return jsonResponse({
|
|
1053
1084
|
success: true,
|
|
1054
1085
|
deleted,
|
|
@@ -1075,12 +1106,12 @@ async function handleCreateFolder(request) {
|
|
|
1075
1106
|
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1076
1107
|
}
|
|
1077
1108
|
try {
|
|
1078
|
-
await
|
|
1109
|
+
await fs6.access(folderPath);
|
|
1079
1110
|
return jsonResponse({ error: "A folder with this name already exists" }, { status: 400 });
|
|
1080
1111
|
} catch {
|
|
1081
1112
|
}
|
|
1082
|
-
await
|
|
1083
|
-
return jsonResponse({ success: true, path:
|
|
1113
|
+
await fs6.mkdir(folderPath, { recursive: true });
|
|
1114
|
+
return jsonResponse({ success: true, path: path6.join(safePath, sanitizedName) });
|
|
1084
1115
|
} catch (error) {
|
|
1085
1116
|
console.error("Failed to create folder:", error);
|
|
1086
1117
|
return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
|
|
@@ -1098,29 +1129,29 @@ async function handleRename(request) {
|
|
|
1098
1129
|
}
|
|
1099
1130
|
const safePath = oldPath.replace(/\.\./g, "");
|
|
1100
1131
|
const absoluteOldPath = getWorkspacePath(safePath);
|
|
1101
|
-
const parentDir =
|
|
1102
|
-
const absoluteNewPath =
|
|
1132
|
+
const parentDir = path6.dirname(absoluteOldPath);
|
|
1133
|
+
const absoluteNewPath = path6.join(parentDir, sanitizedName);
|
|
1103
1134
|
if (!absoluteOldPath.startsWith(getPublicPath())) {
|
|
1104
1135
|
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1105
1136
|
}
|
|
1106
1137
|
try {
|
|
1107
|
-
await
|
|
1138
|
+
await fs6.access(absoluteOldPath);
|
|
1108
1139
|
} catch {
|
|
1109
1140
|
return jsonResponse({ error: "File or folder not found" }, { status: 404 });
|
|
1110
1141
|
}
|
|
1111
1142
|
try {
|
|
1112
|
-
await
|
|
1143
|
+
await fs6.access(absoluteNewPath);
|
|
1113
1144
|
return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
|
|
1114
1145
|
} catch {
|
|
1115
1146
|
}
|
|
1116
|
-
const stats = await
|
|
1147
|
+
const stats = await fs6.stat(absoluteOldPath);
|
|
1117
1148
|
const isFile = stats.isFile();
|
|
1118
|
-
const isImage = isFile && isImageFile(
|
|
1119
|
-
await
|
|
1149
|
+
const isImage = isFile && isImageFile(path6.basename(oldPath));
|
|
1150
|
+
await fs6.rename(absoluteOldPath, absoluteNewPath);
|
|
1120
1151
|
if (isImage) {
|
|
1121
1152
|
const meta = await loadMeta();
|
|
1122
1153
|
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
1123
|
-
const newRelativePath =
|
|
1154
|
+
const newRelativePath = path6.join(path6.dirname(oldRelativePath), sanitizedName);
|
|
1124
1155
|
const oldKey = "/" + oldRelativePath;
|
|
1125
1156
|
const newKey = "/" + newRelativePath;
|
|
1126
1157
|
if (meta[oldKey]) {
|
|
@@ -1130,9 +1161,9 @@ async function handleRename(request) {
|
|
|
1130
1161
|
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
1131
1162
|
const oldThumbPath = getPublicPath(oldThumbPaths[i]);
|
|
1132
1163
|
const newThumbPath = getPublicPath(newThumbPaths[i]);
|
|
1133
|
-
await
|
|
1164
|
+
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
1134
1165
|
try {
|
|
1135
|
-
await
|
|
1166
|
+
await fs6.rename(oldThumbPath, newThumbPath);
|
|
1136
1167
|
} catch {
|
|
1137
1168
|
}
|
|
1138
1169
|
}
|
|
@@ -1141,7 +1172,7 @@ async function handleRename(request) {
|
|
|
1141
1172
|
}
|
|
1142
1173
|
await saveMeta(meta);
|
|
1143
1174
|
}
|
|
1144
|
-
const newPath =
|
|
1175
|
+
const newPath = path6.join(path6.dirname(safePath), sanitizedName);
|
|
1145
1176
|
return jsonResponse({ success: true, newPath });
|
|
1146
1177
|
} catch (error) {
|
|
1147
1178
|
console.error("Failed to rename:", error);
|
|
@@ -1176,21 +1207,22 @@ async function handleMoveStream(request) {
|
|
|
1176
1207
|
controller.close();
|
|
1177
1208
|
return;
|
|
1178
1209
|
}
|
|
1179
|
-
await
|
|
1210
|
+
await fs6.mkdir(absoluteDestination, { recursive: true });
|
|
1180
1211
|
const meta = await loadMeta();
|
|
1181
1212
|
const cdnUrls = getCdnUrls(meta);
|
|
1182
1213
|
const r2PublicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "") || "";
|
|
1183
1214
|
const moved = [];
|
|
1184
1215
|
const errors = [];
|
|
1216
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
1185
1217
|
const total = paths.length;
|
|
1186
1218
|
sendEvent({ type: "start", total });
|
|
1187
1219
|
for (let i = 0; i < paths.length; i++) {
|
|
1188
1220
|
const itemPath = paths[i];
|
|
1189
1221
|
const safePath = itemPath.replace(/\.\./g, "");
|
|
1190
|
-
const itemName =
|
|
1191
|
-
const newAbsolutePath =
|
|
1222
|
+
const itemName = path6.basename(safePath);
|
|
1223
|
+
const newAbsolutePath = path6.join(absoluteDestination, itemName);
|
|
1192
1224
|
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
1193
|
-
const newRelativePath =
|
|
1225
|
+
const newRelativePath = path6.join(safeDestination.replace(/^public\//, ""), itemName);
|
|
1194
1226
|
const oldKey = "/" + oldRelativePath;
|
|
1195
1227
|
const newKey = "/" + newRelativePath;
|
|
1196
1228
|
sendEvent({
|
|
@@ -1212,11 +1244,13 @@ async function handleMoveStream(request) {
|
|
|
1212
1244
|
const isPushedToR2 = isInCloud && r2PublicUrl && fileCdnUrl === r2PublicUrl;
|
|
1213
1245
|
const hasProcessedThumbnails = isProcessed(entry);
|
|
1214
1246
|
try {
|
|
1247
|
+
const sourceFolder = path6.dirname(getWorkspacePath(safePath));
|
|
1248
|
+
sourceFolders.add(sourceFolder);
|
|
1215
1249
|
if (isRemote && isImage) {
|
|
1216
1250
|
const remoteUrl = `${fileCdnUrl}${oldKey}`;
|
|
1217
1251
|
const buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1218
|
-
await
|
|
1219
|
-
await
|
|
1252
|
+
await fs6.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
|
|
1253
|
+
await fs6.writeFile(newAbsolutePath, buffer);
|
|
1220
1254
|
const newEntry = {
|
|
1221
1255
|
o: entry?.o,
|
|
1222
1256
|
b: entry?.b
|
|
@@ -1226,8 +1260,8 @@ async function handleMoveStream(request) {
|
|
|
1226
1260
|
moved.push(itemPath);
|
|
1227
1261
|
} else if (isPushedToR2 && isImage) {
|
|
1228
1262
|
const buffer = await downloadFromCdn(oldKey);
|
|
1229
|
-
await
|
|
1230
|
-
await
|
|
1263
|
+
await fs6.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
|
|
1264
|
+
await fs6.writeFile(newAbsolutePath, buffer);
|
|
1231
1265
|
let newEntry = {
|
|
1232
1266
|
o: entry?.o,
|
|
1233
1267
|
b: entry?.b
|
|
@@ -1242,7 +1276,7 @@ async function handleMoveStream(request) {
|
|
|
1242
1276
|
}
|
|
1243
1277
|
await deleteFromCdn(oldKey, hasProcessedThumbnails);
|
|
1244
1278
|
try {
|
|
1245
|
-
await
|
|
1279
|
+
await fs6.unlink(newAbsolutePath);
|
|
1246
1280
|
} catch {
|
|
1247
1281
|
}
|
|
1248
1282
|
if (hasProcessedThumbnails) {
|
|
@@ -1254,33 +1288,34 @@ async function handleMoveStream(request) {
|
|
|
1254
1288
|
moved.push(itemPath);
|
|
1255
1289
|
} else {
|
|
1256
1290
|
const absolutePath = getWorkspacePath(safePath);
|
|
1257
|
-
if (absoluteDestination.startsWith(absolutePath +
|
|
1291
|
+
if (absoluteDestination.startsWith(absolutePath + path6.sep)) {
|
|
1258
1292
|
errors.push(`Cannot move ${itemName} into itself`);
|
|
1259
1293
|
continue;
|
|
1260
1294
|
}
|
|
1261
1295
|
try {
|
|
1262
|
-
await
|
|
1296
|
+
await fs6.access(absolutePath);
|
|
1263
1297
|
} catch {
|
|
1264
1298
|
errors.push(`${itemName} not found`);
|
|
1265
1299
|
continue;
|
|
1266
1300
|
}
|
|
1267
1301
|
try {
|
|
1268
|
-
await
|
|
1302
|
+
await fs6.access(newAbsolutePath);
|
|
1269
1303
|
errors.push(`${itemName} already exists in destination`);
|
|
1270
1304
|
continue;
|
|
1271
1305
|
} catch {
|
|
1272
1306
|
}
|
|
1273
|
-
await
|
|
1274
|
-
const stats = await
|
|
1307
|
+
await fs6.rename(absolutePath, newAbsolutePath);
|
|
1308
|
+
const stats = await fs6.stat(newAbsolutePath);
|
|
1275
1309
|
if (stats.isFile() && isImage && entry) {
|
|
1276
1310
|
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
1277
1311
|
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
1278
1312
|
for (let j = 0; j < oldThumbPaths.length; j++) {
|
|
1279
1313
|
const oldThumbPath = getPublicPath(oldThumbPaths[j]);
|
|
1280
1314
|
const newThumbPath = getPublicPath(newThumbPaths[j]);
|
|
1281
|
-
|
|
1315
|
+
sourceFolders.add(path6.dirname(oldThumbPath));
|
|
1316
|
+
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
1282
1317
|
try {
|
|
1283
|
-
await
|
|
1318
|
+
await fs6.rename(oldThumbPath, newThumbPath);
|
|
1284
1319
|
} catch {
|
|
1285
1320
|
}
|
|
1286
1321
|
}
|
|
@@ -1305,6 +1340,9 @@ async function handleMoveStream(request) {
|
|
|
1305
1340
|
}
|
|
1306
1341
|
}
|
|
1307
1342
|
await saveMeta(meta);
|
|
1343
|
+
for (const folder of sourceFolders) {
|
|
1344
|
+
await deleteEmptyFolders(folder);
|
|
1345
|
+
}
|
|
1308
1346
|
sendEvent({
|
|
1309
1347
|
type: "complete",
|
|
1310
1348
|
moved: moved.length,
|
|
@@ -1329,8 +1367,8 @@ async function handleMoveStream(request) {
|
|
|
1329
1367
|
}
|
|
1330
1368
|
|
|
1331
1369
|
// src/handlers/images.ts
|
|
1332
|
-
import { promises as
|
|
1333
|
-
import
|
|
1370
|
+
import { promises as fs7 } from "fs";
|
|
1371
|
+
import path7 from "path";
|
|
1334
1372
|
import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
|
|
1335
1373
|
async function handleSync(request) {
|
|
1336
1374
|
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
@@ -1360,6 +1398,7 @@ async function handleSync(request) {
|
|
|
1360
1398
|
const pushed = [];
|
|
1361
1399
|
const errors = [];
|
|
1362
1400
|
const urlsToPurge = [];
|
|
1401
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
1363
1402
|
for (let imageKey of imageKeys) {
|
|
1364
1403
|
if (!imageKey.startsWith("/")) {
|
|
1365
1404
|
imageKey = `/${imageKey}`;
|
|
@@ -1384,7 +1423,7 @@ async function handleSync(request) {
|
|
|
1384
1423
|
} else {
|
|
1385
1424
|
const originalLocalPath = getPublicPath(imageKey);
|
|
1386
1425
|
try {
|
|
1387
|
-
originalBuffer = await
|
|
1426
|
+
originalBuffer = await fs7.readFile(originalLocalPath);
|
|
1388
1427
|
} catch {
|
|
1389
1428
|
errors.push(`Original file not found: ${imageKey}`);
|
|
1390
1429
|
continue;
|
|
@@ -1403,7 +1442,7 @@ async function handleSync(request) {
|
|
|
1403
1442
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1404
1443
|
const localPath = getPublicPath(thumbPath);
|
|
1405
1444
|
try {
|
|
1406
|
-
const fileBuffer = await
|
|
1445
|
+
const fileBuffer = await fs7.readFile(localPath);
|
|
1407
1446
|
await r2.send(
|
|
1408
1447
|
new PutObjectCommand2({
|
|
1409
1448
|
Bucket: bucketName,
|
|
@@ -1420,15 +1459,17 @@ async function handleSync(request) {
|
|
|
1420
1459
|
entry.c = cdnIndex;
|
|
1421
1460
|
if (!isRemote) {
|
|
1422
1461
|
const originalLocalPath = getPublicPath(imageKey);
|
|
1462
|
+
sourceFolders.add(path7.dirname(originalLocalPath));
|
|
1423
1463
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1424
1464
|
const localPath = getPublicPath(thumbPath);
|
|
1465
|
+
sourceFolders.add(path7.dirname(localPath));
|
|
1425
1466
|
try {
|
|
1426
|
-
await
|
|
1467
|
+
await fs7.unlink(localPath);
|
|
1427
1468
|
} catch {
|
|
1428
1469
|
}
|
|
1429
1470
|
}
|
|
1430
1471
|
try {
|
|
1431
|
-
await
|
|
1472
|
+
await fs7.unlink(originalLocalPath);
|
|
1432
1473
|
} catch {
|
|
1433
1474
|
}
|
|
1434
1475
|
}
|
|
@@ -1439,6 +1480,9 @@ async function handleSync(request) {
|
|
|
1439
1480
|
}
|
|
1440
1481
|
}
|
|
1441
1482
|
await saveMeta(meta);
|
|
1483
|
+
for (const folder of sourceFolders) {
|
|
1484
|
+
await deleteEmptyFolders(folder);
|
|
1485
|
+
}
|
|
1442
1486
|
if (urlsToPurge.length > 0) {
|
|
1443
1487
|
await purgeCloudflareCache(urlsToPurge);
|
|
1444
1488
|
}
|
|
@@ -1610,32 +1654,32 @@ async function handleReprocessStream(request) {
|
|
|
1610
1654
|
const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
|
|
1611
1655
|
const originalPath = getPublicPath(imageKey);
|
|
1612
1656
|
try {
|
|
1613
|
-
buffer = await
|
|
1657
|
+
buffer = await fs7.readFile(originalPath);
|
|
1614
1658
|
} catch {
|
|
1615
1659
|
if (isInOurR2) {
|
|
1616
1660
|
buffer = await downloadFromCdn(imageKey);
|
|
1617
|
-
const dir =
|
|
1618
|
-
await
|
|
1619
|
-
await
|
|
1661
|
+
const dir = path7.dirname(originalPath);
|
|
1662
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
1663
|
+
await fs7.writeFile(originalPath, buffer);
|
|
1620
1664
|
} else if (isRemote && existingCdnUrl) {
|
|
1621
1665
|
const remoteUrl = `${existingCdnUrl}${imageKey}`;
|
|
1622
1666
|
buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1623
|
-
const dir =
|
|
1624
|
-
await
|
|
1625
|
-
await
|
|
1667
|
+
const dir = path7.dirname(originalPath);
|
|
1668
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
1669
|
+
await fs7.writeFile(originalPath, buffer);
|
|
1626
1670
|
} else {
|
|
1627
1671
|
throw new Error(`File not found: ${imageKey}`);
|
|
1628
1672
|
}
|
|
1629
1673
|
}
|
|
1630
|
-
const ext =
|
|
1674
|
+
const ext = path7.extname(imageKey).toLowerCase();
|
|
1631
1675
|
const isSvg = ext === ".svg";
|
|
1632
1676
|
if (isSvg) {
|
|
1633
|
-
const imageDir =
|
|
1677
|
+
const imageDir = path7.dirname(imageKey.slice(1));
|
|
1634
1678
|
const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
|
|
1635
|
-
await
|
|
1636
|
-
const fileName =
|
|
1637
|
-
const destPath =
|
|
1638
|
-
await
|
|
1679
|
+
await fs7.mkdir(imagesPath, { recursive: true });
|
|
1680
|
+
const fileName = path7.basename(imageKey);
|
|
1681
|
+
const destPath = path7.join(imagesPath, fileName);
|
|
1682
|
+
await fs7.writeFile(destPath, buffer);
|
|
1639
1683
|
meta[imageKey] = {
|
|
1640
1684
|
...entry,
|
|
1641
1685
|
o: { w: 0, h: 0 },
|
|
@@ -1655,7 +1699,7 @@ async function handleReprocessStream(request) {
|
|
|
1655
1699
|
}
|
|
1656
1700
|
await deleteLocalThumbnails(imageKey);
|
|
1657
1701
|
try {
|
|
1658
|
-
await
|
|
1702
|
+
await fs7.unlink(originalPath);
|
|
1659
1703
|
} catch {
|
|
1660
1704
|
}
|
|
1661
1705
|
}
|
|
@@ -1734,8 +1778,8 @@ async function handleDownloadStream(request) {
|
|
|
1734
1778
|
try {
|
|
1735
1779
|
const imageBuffer = await downloadFromCdn(imageKey);
|
|
1736
1780
|
const localPath = getPublicPath(imageKey.replace(/^\//, ""));
|
|
1737
|
-
await
|
|
1738
|
-
await
|
|
1781
|
+
await fs7.mkdir(path7.dirname(localPath), { recursive: true });
|
|
1782
|
+
await fs7.writeFile(localPath, imageBuffer);
|
|
1739
1783
|
await deleteThumbnailsFromCdn(imageKey);
|
|
1740
1784
|
const wasProcessed = isProcessed(entry);
|
|
1741
1785
|
delete entry.c;
|
|
@@ -1797,8 +1841,8 @@ async function handleDownloadStream(request) {
|
|
|
1797
1841
|
}
|
|
1798
1842
|
|
|
1799
1843
|
// src/handlers/scan.ts
|
|
1800
|
-
import { promises as
|
|
1801
|
-
import
|
|
1844
|
+
import { promises as fs8 } from "fs";
|
|
1845
|
+
import path8 from "path";
|
|
1802
1846
|
import sharp3 from "sharp";
|
|
1803
1847
|
import { encode as encode2 } from "blurhash";
|
|
1804
1848
|
async function handleScanStream() {
|
|
@@ -1821,10 +1865,10 @@ async function handleScanStream() {
|
|
|
1821
1865
|
const allFiles = [];
|
|
1822
1866
|
async function scanDir(dir, relativePath = "") {
|
|
1823
1867
|
try {
|
|
1824
|
-
const entries = await
|
|
1868
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1825
1869
|
for (const entry of entries) {
|
|
1826
1870
|
if (entry.name.startsWith(".")) continue;
|
|
1827
|
-
const fullPath =
|
|
1871
|
+
const fullPath = path8.join(dir, entry.name);
|
|
1828
1872
|
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1829
1873
|
if (relPath === "images" || relPath.startsWith("images/")) continue;
|
|
1830
1874
|
if (entry.isDirectory()) {
|
|
@@ -1854,7 +1898,7 @@ async function handleScanStream() {
|
|
|
1854
1898
|
continue;
|
|
1855
1899
|
}
|
|
1856
1900
|
if (meta[imageKey]) {
|
|
1857
|
-
const ext =
|
|
1901
|
+
const ext = path8.extname(relativePath);
|
|
1858
1902
|
const baseName = relativePath.slice(0, -ext.length);
|
|
1859
1903
|
let counter = 1;
|
|
1860
1904
|
let newKey = `/${baseName}-${counter}${ext}`;
|
|
@@ -1865,7 +1909,7 @@ async function handleScanStream() {
|
|
|
1865
1909
|
const newRelativePath = `${baseName}-${counter}${ext}`;
|
|
1866
1910
|
const newFullPath = getPublicPath(newRelativePath);
|
|
1867
1911
|
try {
|
|
1868
|
-
await
|
|
1912
|
+
await fs8.rename(fullPath, newFullPath);
|
|
1869
1913
|
renamed.push({ from: relativePath, to: newRelativePath });
|
|
1870
1914
|
relativePath = newRelativePath;
|
|
1871
1915
|
fullPath = newFullPath;
|
|
@@ -1879,12 +1923,12 @@ async function handleScanStream() {
|
|
|
1879
1923
|
try {
|
|
1880
1924
|
const isImage = isImageFile(relativePath);
|
|
1881
1925
|
if (isImage) {
|
|
1882
|
-
const ext =
|
|
1926
|
+
const ext = path8.extname(relativePath).toLowerCase();
|
|
1883
1927
|
if (ext === ".svg") {
|
|
1884
1928
|
meta[imageKey] = { o: { w: 0, h: 0 }, b: "" };
|
|
1885
1929
|
} else {
|
|
1886
1930
|
try {
|
|
1887
|
-
const buffer = await
|
|
1931
|
+
const buffer = await fs8.readFile(fullPath);
|
|
1888
1932
|
const metadata = await sharp3(buffer).metadata();
|
|
1889
1933
|
const { data, info } = await sharp3(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
1890
1934
|
const blurhash = encode2(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
|
|
@@ -1918,10 +1962,10 @@ async function handleScanStream() {
|
|
|
1918
1962
|
}
|
|
1919
1963
|
async function findOrphans(dir, relativePath = "") {
|
|
1920
1964
|
try {
|
|
1921
|
-
const entries = await
|
|
1965
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1922
1966
|
for (const entry of entries) {
|
|
1923
1967
|
if (entry.name.startsWith(".")) continue;
|
|
1924
|
-
const fullPath =
|
|
1968
|
+
const fullPath = path8.join(dir, entry.name);
|
|
1925
1969
|
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1926
1970
|
if (entry.isDirectory()) {
|
|
1927
1971
|
await findOrphans(fullPath, relPath);
|
|
@@ -1981,7 +2025,7 @@ async function handleDeleteOrphans(request) {
|
|
|
1981
2025
|
}
|
|
1982
2026
|
const fullPath = getPublicPath(orphanPath);
|
|
1983
2027
|
try {
|
|
1984
|
-
await
|
|
2028
|
+
await fs8.unlink(fullPath);
|
|
1985
2029
|
deleted.push(orphanPath);
|
|
1986
2030
|
} catch (err) {
|
|
1987
2031
|
console.error(`Failed to delete ${orphanPath}:`, err);
|
|
@@ -1991,18 +2035,18 @@ async function handleDeleteOrphans(request) {
|
|
|
1991
2035
|
const imagesDir = getPublicPath("images");
|
|
1992
2036
|
async function removeEmptyDirs(dir) {
|
|
1993
2037
|
try {
|
|
1994
|
-
const entries = await
|
|
2038
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
1995
2039
|
let isEmpty = true;
|
|
1996
2040
|
for (const entry of entries) {
|
|
1997
2041
|
if (entry.isDirectory()) {
|
|
1998
|
-
const subDirEmpty = await removeEmptyDirs(
|
|
2042
|
+
const subDirEmpty = await removeEmptyDirs(path8.join(dir, entry.name));
|
|
1999
2043
|
if (!subDirEmpty) isEmpty = false;
|
|
2000
2044
|
} else {
|
|
2001
2045
|
isEmpty = false;
|
|
2002
2046
|
}
|
|
2003
2047
|
}
|
|
2004
|
-
if (isEmpty
|
|
2005
|
-
await
|
|
2048
|
+
if (isEmpty) {
|
|
2049
|
+
await fs8.rmdir(dir);
|
|
2006
2050
|
}
|
|
2007
2051
|
return isEmpty;
|
|
2008
2052
|
} catch {
|
|
@@ -2030,8 +2074,8 @@ import { encode as encode3 } from "blurhash";
|
|
|
2030
2074
|
function parseImageUrl(url) {
|
|
2031
2075
|
const parsed = new URL(url);
|
|
2032
2076
|
const base = `${parsed.protocol}//${parsed.host}`;
|
|
2033
|
-
const
|
|
2034
|
-
return { base, path:
|
|
2077
|
+
const path10 = parsed.pathname;
|
|
2078
|
+
return { base, path: path10 };
|
|
2035
2079
|
}
|
|
2036
2080
|
async function processRemoteImage(url) {
|
|
2037
2081
|
const response = await fetch(url);
|
|
@@ -2080,20 +2124,20 @@ async function handleImportUrls(request) {
|
|
|
2080
2124
|
currentFile: url
|
|
2081
2125
|
});
|
|
2082
2126
|
try {
|
|
2083
|
-
const { base, path:
|
|
2084
|
-
const existingEntry = getMetaEntry(meta,
|
|
2127
|
+
const { base, path: path10 } = parseImageUrl(url);
|
|
2128
|
+
const existingEntry = getMetaEntry(meta, path10);
|
|
2085
2129
|
if (existingEntry) {
|
|
2086
|
-
skipped.push(
|
|
2130
|
+
skipped.push(path10);
|
|
2087
2131
|
continue;
|
|
2088
2132
|
}
|
|
2089
2133
|
const cdnIndex = getOrAddCdnIndex(meta, base);
|
|
2090
2134
|
const imageData = await processRemoteImage(url);
|
|
2091
|
-
setMetaEntry(meta,
|
|
2135
|
+
setMetaEntry(meta, path10, {
|
|
2092
2136
|
o: imageData.o,
|
|
2093
2137
|
b: imageData.b,
|
|
2094
2138
|
c: cdnIndex
|
|
2095
2139
|
});
|
|
2096
|
-
added.push(
|
|
2140
|
+
added.push(path10);
|
|
2097
2141
|
} catch (error) {
|
|
2098
2142
|
console.error(`Failed to import ${url}:`, error);
|
|
2099
2143
|
errors.push(url);
|
|
@@ -2150,8 +2194,8 @@ async function handleUpdateCdns(request) {
|
|
|
2150
2194
|
|
|
2151
2195
|
// src/handlers/favicon.ts
|
|
2152
2196
|
import sharp5 from "sharp";
|
|
2153
|
-
import
|
|
2154
|
-
import
|
|
2197
|
+
import path9 from "path";
|
|
2198
|
+
import fs9 from "fs/promises";
|
|
2155
2199
|
var FAVICON_CONFIGS = [
|
|
2156
2200
|
{ name: "favicon.ico", size: 48 },
|
|
2157
2201
|
{ name: "icon.png", size: 32 },
|
|
@@ -2169,7 +2213,7 @@ async function handleGenerateFavicon(request) {
|
|
|
2169
2213
|
} catch {
|
|
2170
2214
|
return jsonResponse({ error: "Invalid request body" }, { status: 400 });
|
|
2171
2215
|
}
|
|
2172
|
-
const fileName =
|
|
2216
|
+
const fileName = path9.basename(imagePath).toLowerCase();
|
|
2173
2217
|
if (fileName !== "favicon.png" && fileName !== "favicon.jpg") {
|
|
2174
2218
|
return jsonResponse({
|
|
2175
2219
|
error: "Source file must be named favicon.png or favicon.jpg"
|
|
@@ -2177,7 +2221,7 @@ async function handleGenerateFavicon(request) {
|
|
|
2177
2221
|
}
|
|
2178
2222
|
const sourcePath = getPublicPath(imagePath.replace(/^\//, ""));
|
|
2179
2223
|
try {
|
|
2180
|
-
await
|
|
2224
|
+
await fs9.access(sourcePath);
|
|
2181
2225
|
} catch {
|
|
2182
2226
|
return jsonResponse({ error: "Source file not found" }, { status: 404 });
|
|
2183
2227
|
}
|
|
@@ -2189,7 +2233,7 @@ async function handleGenerateFavicon(request) {
|
|
|
2189
2233
|
}
|
|
2190
2234
|
const outputDir = getSrcAppPath();
|
|
2191
2235
|
try {
|
|
2192
|
-
await
|
|
2236
|
+
await fs9.access(outputDir);
|
|
2193
2237
|
} catch {
|
|
2194
2238
|
return jsonResponse({
|
|
2195
2239
|
error: "Output directory src/app/ not found"
|
|
@@ -2221,7 +2265,7 @@ async function handleGenerateFavicon(request) {
|
|
|
2221
2265
|
message: `Generating ${config.name} (${config.size}x${config.size})...`
|
|
2222
2266
|
});
|
|
2223
2267
|
try {
|
|
2224
|
-
const outputPath =
|
|
2268
|
+
const outputPath = path9.join(outputDir, config.name);
|
|
2225
2269
|
await sharp5(sourcePath).resize(config.size, config.size, {
|
|
2226
2270
|
fit: "cover",
|
|
2227
2271
|
position: "center"
|