@gallop.software/studio 2.3.51 → 2.3.52
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-hTSqPphM.js +85 -0
- package/dist/client/index.html +1 -1
- package/dist/server/index.js +1486 -1456
- package/dist/server/index.js.map +1 -1
- package/package.json +1 -1
package/dist/server/index.js
CHANGED
|
@@ -1016,8 +1016,8 @@ async function handleFolderImages(request) {
|
|
|
1016
1016
|
}
|
|
1017
1017
|
|
|
1018
1018
|
// src/handlers/files.ts
|
|
1019
|
-
import { promises as
|
|
1020
|
-
import
|
|
1019
|
+
import { promises as fs7 } from "fs";
|
|
1020
|
+
import path7 from "path";
|
|
1021
1021
|
import sharp2 from "sharp";
|
|
1022
1022
|
|
|
1023
1023
|
// src/handlers/utils/folders.ts
|
|
@@ -1087,324 +1087,288 @@ async function cleanupEmptyFoldersRecursive(dir) {
|
|
|
1087
1087
|
}
|
|
1088
1088
|
}
|
|
1089
1089
|
|
|
1090
|
-
// src/handlers/
|
|
1091
|
-
|
|
1090
|
+
// src/handlers/images.ts
|
|
1091
|
+
import { promises as fs6 } from "fs";
|
|
1092
|
+
import path6 from "path";
|
|
1093
|
+
import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2, DeleteObjectCommand as DeleteObjectCommand2 } from "@aws-sdk/client-s3";
|
|
1094
|
+
var cancelledOperations = /* @__PURE__ */ new Set();
|
|
1095
|
+
function cancelOperation(operationId) {
|
|
1096
|
+
cancelledOperations.add(operationId);
|
|
1097
|
+
setTimeout(() => cancelledOperations.delete(operationId), 6e4);
|
|
1098
|
+
}
|
|
1099
|
+
function isOperationCancelled(operationId) {
|
|
1100
|
+
return cancelledOperations.has(operationId);
|
|
1101
|
+
}
|
|
1102
|
+
function clearCancelledOperation(operationId) {
|
|
1103
|
+
cancelledOperations.delete(operationId);
|
|
1104
|
+
}
|
|
1105
|
+
async function handleSync(request) {
|
|
1106
|
+
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
1107
|
+
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
1108
|
+
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
1109
|
+
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
1110
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1111
|
+
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1112
|
+
return jsonResponse(
|
|
1113
|
+
{ error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." },
|
|
1114
|
+
{ status: 400 }
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1092
1117
|
try {
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
if (!file) {
|
|
1097
|
-
return jsonResponse({ error: "No file provided" }, { status: 400 });
|
|
1118
|
+
const { imageKeys } = await request.json();
|
|
1119
|
+
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1120
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1098
1121
|
}
|
|
1099
|
-
const bytes = await file.arrayBuffer();
|
|
1100
|
-
const buffer = Buffer.from(bytes);
|
|
1101
|
-
const fileName = slugifyFilename(file.name);
|
|
1102
|
-
const ext = path6.extname(fileName).toLowerCase();
|
|
1103
|
-
const isImage = isImageFile(fileName);
|
|
1104
|
-
const isMedia = isMediaFile(fileName);
|
|
1105
1122
|
const meta = await loadMeta();
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const baseName = path6.basename(fileName, ext);
|
|
1121
|
-
let counter = 1;
|
|
1122
|
-
let newFileName = `${baseName}-${counter}${ext}`;
|
|
1123
|
-
let newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
|
|
1124
|
-
while (meta[newKey]) {
|
|
1125
|
-
counter++;
|
|
1126
|
-
newFileName = `${baseName}-${counter}${ext}`;
|
|
1127
|
-
newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
|
|
1123
|
+
const cdnUrls = getCdnUrls(meta);
|
|
1124
|
+
const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
|
|
1125
|
+
const r2 = new S3Client2({
|
|
1126
|
+
region: "auto",
|
|
1127
|
+
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
1128
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
1129
|
+
});
|
|
1130
|
+
const pushed = [];
|
|
1131
|
+
const alreadyPushed = [];
|
|
1132
|
+
const errors = [];
|
|
1133
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
1134
|
+
for (let imageKey of imageKeys) {
|
|
1135
|
+
if (!imageKey.startsWith("/")) {
|
|
1136
|
+
imageKey = `/${imageKey}`;
|
|
1128
1137
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1142
|
-
if (isImage && ext !== ".svg") {
|
|
1138
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
1139
|
+
if (!entry) {
|
|
1140
|
+
errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
const existingCdnUrl = entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
1144
|
+
const isAlreadyInOurR2 = existingCdnUrl === publicUrl;
|
|
1145
|
+
if (isAlreadyInOurR2) {
|
|
1146
|
+
alreadyPushed.push(imageKey);
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
const isRemote = entry.c !== void 0 && existingCdnUrl !== publicUrl;
|
|
1143
1150
|
try {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1151
|
+
let originalBuffer;
|
|
1152
|
+
if (isRemote) {
|
|
1153
|
+
const remoteUrl = `${existingCdnUrl}${imageKey}`;
|
|
1154
|
+
originalBuffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1155
|
+
} else {
|
|
1156
|
+
const originalLocalPath = getPublicPath(imageKey);
|
|
1157
|
+
try {
|
|
1158
|
+
originalBuffer = await fs6.readFile(originalLocalPath);
|
|
1159
|
+
} catch {
|
|
1160
|
+
errors.push(`Original file not found: ${imageKey}`);
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
await r2.send(
|
|
1165
|
+
new PutObjectCommand2({
|
|
1166
|
+
Bucket: bucketName,
|
|
1167
|
+
Key: imageKey.replace(/^\//, ""),
|
|
1168
|
+
Body: originalBuffer,
|
|
1169
|
+
ContentType: getContentType(imageKey)
|
|
1170
|
+
})
|
|
1171
|
+
);
|
|
1172
|
+
if (!isRemote && isProcessed(entry)) {
|
|
1173
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1174
|
+
const localPath = getPublicPath(thumbPath);
|
|
1175
|
+
try {
|
|
1176
|
+
const fileBuffer = await fs6.readFile(localPath);
|
|
1177
|
+
await r2.send(
|
|
1178
|
+
new PutObjectCommand2({
|
|
1179
|
+
Bucket: bucketName,
|
|
1180
|
+
Key: thumbPath.replace(/^\//, ""),
|
|
1181
|
+
Body: fileBuffer,
|
|
1182
|
+
ContentType: getContentType(thumbPath)
|
|
1183
|
+
})
|
|
1184
|
+
);
|
|
1185
|
+
} catch {
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
entry.c = cdnIndex;
|
|
1190
|
+
if (!isRemote) {
|
|
1191
|
+
const originalLocalPath = getPublicPath(imageKey);
|
|
1192
|
+
sourceFolders.add(path6.dirname(originalLocalPath));
|
|
1193
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1194
|
+
const localPath = getPublicPath(thumbPath);
|
|
1195
|
+
sourceFolders.add(path6.dirname(localPath));
|
|
1196
|
+
try {
|
|
1197
|
+
await fs6.unlink(localPath);
|
|
1198
|
+
} catch {
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
await fs6.unlink(originalLocalPath);
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
pushed.push(imageKey);
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
console.error(`Failed to push ${imageKey}:`, error);
|
|
1209
|
+
errors.push(`Failed to push: ${imageKey}`);
|
|
1150
1210
|
}
|
|
1151
|
-
} else {
|
|
1152
|
-
meta[imageKey] = {};
|
|
1153
1211
|
}
|
|
1154
1212
|
await saveMeta(meta);
|
|
1213
|
+
for (const folder of sourceFolders) {
|
|
1214
|
+
await deleteEmptyFolders(folder);
|
|
1215
|
+
}
|
|
1155
1216
|
return jsonResponse({
|
|
1156
1217
|
success: true,
|
|
1157
|
-
|
|
1158
|
-
|
|
1218
|
+
pushed,
|
|
1219
|
+
alreadyPushed: alreadyPushed.length > 0 ? alreadyPushed : void 0,
|
|
1220
|
+
errors: errors.length > 0 ? errors : void 0
|
|
1159
1221
|
});
|
|
1160
1222
|
} catch (error) {
|
|
1161
|
-
console.error("Failed to
|
|
1162
|
-
|
|
1163
|
-
return jsonResponse({ error: `Failed to upload file: ${message}` }, { status: 500 });
|
|
1223
|
+
console.error("Failed to push:", error);
|
|
1224
|
+
return jsonResponse({ error: "Failed to push to CDN" }, { status: 500 });
|
|
1164
1225
|
}
|
|
1165
1226
|
}
|
|
1166
|
-
async function
|
|
1227
|
+
async function handleUnprocessStream(request) {
|
|
1228
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1229
|
+
const encoder = new TextEncoder();
|
|
1230
|
+
let imageKeys;
|
|
1167
1231
|
try {
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1232
|
+
const body = await request.json();
|
|
1233
|
+
imageKeys = body.imageKeys;
|
|
1234
|
+
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1235
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1171
1236
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1237
|
+
} catch {
|
|
1238
|
+
return jsonResponse({ error: "Invalid request body" }, { status: 400 });
|
|
1239
|
+
}
|
|
1240
|
+
const stream = new ReadableStream({
|
|
1241
|
+
async start(controller) {
|
|
1242
|
+
const sendEvent = (data) => {
|
|
1243
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
1244
|
+
|
|
1245
|
+
`));
|
|
1246
|
+
};
|
|
1177
1247
|
try {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
for (const thumbPath of getAllThumbnailPaths(key)) {
|
|
1204
|
-
const absoluteThumbPath = getPublicPath(thumbPath);
|
|
1205
|
-
try {
|
|
1206
|
-
await fs6.unlink(absoluteThumbPath);
|
|
1207
|
-
} catch {
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
delete meta[key];
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
} else {
|
|
1215
|
-
await fs6.unlink(absolutePath);
|
|
1216
|
-
const isInImagesFolder = itemPath.startsWith("public/images/");
|
|
1217
|
-
if (!isInImagesFolder && entry) {
|
|
1218
|
-
if (isPushedToCloud) {
|
|
1219
|
-
try {
|
|
1220
|
-
await deleteFromCdn(imageKey, hasThumbnails);
|
|
1221
|
-
} catch {
|
|
1222
|
-
}
|
|
1223
|
-
} else {
|
|
1224
|
-
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1225
|
-
const absoluteThumbPath = getPublicPath(thumbPath);
|
|
1226
|
-
try {
|
|
1227
|
-
await fs6.unlink(absoluteThumbPath);
|
|
1228
|
-
} catch {
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
delete meta[imageKey];
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
} catch {
|
|
1236
|
-
if (entry) {
|
|
1237
|
-
if (isPushedToCloud) {
|
|
1238
|
-
try {
|
|
1239
|
-
await deleteFromCdn(imageKey, hasThumbnails);
|
|
1240
|
-
} catch {
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
delete meta[imageKey];
|
|
1244
|
-
} else {
|
|
1245
|
-
const prefix = imageKey + "/";
|
|
1246
|
-
let foundAny = false;
|
|
1247
|
-
for (const key of Object.keys(meta)) {
|
|
1248
|
-
if (key.startsWith(prefix)) {
|
|
1249
|
-
const keyEntry = meta[key];
|
|
1250
|
-
const keyHasThumbnails = keyEntry ? isProcessed(keyEntry) : false;
|
|
1251
|
-
if (keyEntry?.c !== void 0) {
|
|
1252
|
-
try {
|
|
1253
|
-
await deleteFromCdn(key, keyHasThumbnails);
|
|
1254
|
-
} catch {
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
delete meta[key];
|
|
1258
|
-
foundAny = true;
|
|
1259
|
-
}
|
|
1248
|
+
const meta = await loadMeta();
|
|
1249
|
+
const cdnUrls = getCdnUrls(meta);
|
|
1250
|
+
const removed = [];
|
|
1251
|
+
const skipped = [];
|
|
1252
|
+
const errors = [];
|
|
1253
|
+
const total = imageKeys.length;
|
|
1254
|
+
sendEvent({ type: "start", total });
|
|
1255
|
+
for (let i = 0; i < imageKeys.length; i++) {
|
|
1256
|
+
let imageKey = imageKeys[i];
|
|
1257
|
+
if (!imageKey.startsWith("/")) {
|
|
1258
|
+
imageKey = `/${imageKey}`;
|
|
1259
|
+
}
|
|
1260
|
+
try {
|
|
1261
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
1262
|
+
if (!entry) {
|
|
1263
|
+
errors.push(imageKey);
|
|
1264
|
+
sendEvent({
|
|
1265
|
+
type: "progress",
|
|
1266
|
+
current: i + 1,
|
|
1267
|
+
total,
|
|
1268
|
+
processed: removed.length,
|
|
1269
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1270
|
+
message: `Error: ${imageKey.slice(1)}`
|
|
1271
|
+
});
|
|
1272
|
+
continue;
|
|
1260
1273
|
}
|
|
1261
|
-
|
|
1262
|
-
|
|
1274
|
+
const hasThumbnails = entry.sm || entry.md || entry.lg || entry.f;
|
|
1275
|
+
if (!hasThumbnails) {
|
|
1276
|
+
skipped.push(imageKey);
|
|
1277
|
+
sendEvent({
|
|
1278
|
+
type: "progress",
|
|
1279
|
+
current: i + 1,
|
|
1280
|
+
total,
|
|
1281
|
+
processed: removed.length,
|
|
1282
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1283
|
+
message: `Skipped ${imageKey.slice(1)} (no thumbnails)`
|
|
1284
|
+
});
|
|
1263
1285
|
continue;
|
|
1264
1286
|
}
|
|
1287
|
+
const existingCdnIndex = entry.c;
|
|
1288
|
+
const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
|
|
1289
|
+
const isInOurR2 = existingCdnUrl === publicUrl;
|
|
1290
|
+
await deleteLocalThumbnails(imageKey);
|
|
1291
|
+
if (isInOurR2) {
|
|
1292
|
+
await deleteThumbnailsFromCdn(imageKey);
|
|
1293
|
+
}
|
|
1294
|
+
meta[imageKey] = {
|
|
1295
|
+
o: entry.o,
|
|
1296
|
+
b: entry.b,
|
|
1297
|
+
...entry.c !== void 0 ? { c: entry.c } : {}
|
|
1298
|
+
};
|
|
1299
|
+
removed.push(imageKey);
|
|
1300
|
+
sendEvent({
|
|
1301
|
+
type: "progress",
|
|
1302
|
+
current: i + 1,
|
|
1303
|
+
total,
|
|
1304
|
+
processed: removed.length,
|
|
1305
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1306
|
+
message: `Removed thumbnails for ${imageKey.slice(1)}`
|
|
1307
|
+
});
|
|
1308
|
+
} catch (error) {
|
|
1309
|
+
console.error(`Failed to unprocess ${imageKey}:`, error);
|
|
1310
|
+
errors.push(imageKey);
|
|
1311
|
+
sendEvent({
|
|
1312
|
+
type: "progress",
|
|
1313
|
+
current: i + 1,
|
|
1314
|
+
total,
|
|
1315
|
+
processed: removed.length,
|
|
1316
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1317
|
+
message: `Failed: ${imageKey.slice(1)}`
|
|
1318
|
+
});
|
|
1265
1319
|
}
|
|
1266
1320
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
await saveMeta(meta);
|
|
1274
|
-
for (const folder of sourceFolders) {
|
|
1275
|
-
await deleteEmptyFolders(folder);
|
|
1276
|
-
}
|
|
1277
|
-
return jsonResponse({
|
|
1278
|
-
success: true,
|
|
1279
|
-
deleted,
|
|
1280
|
-
errors: errors.length > 0 ? errors : void 0
|
|
1281
|
-
});
|
|
1282
|
-
} catch (error) {
|
|
1283
|
-
console.error("Failed to delete:", error);
|
|
1284
|
-
return jsonResponse({ error: "Failed to delete files" }, { status: 500 });
|
|
1285
|
-
}
|
|
1286
|
-
}
|
|
1287
|
-
async function handleCreateFolder(request) {
|
|
1288
|
-
try {
|
|
1289
|
-
const { parentPath, name } = await request.json();
|
|
1290
|
-
if (!name || typeof name !== "string") {
|
|
1291
|
-
return jsonResponse({ error: "Folder name is required" }, { status: 400 });
|
|
1292
|
-
}
|
|
1293
|
-
const sanitizedName = slugifyFolderName(name);
|
|
1294
|
-
if (!sanitizedName) {
|
|
1295
|
-
return jsonResponse({ error: "Invalid folder name" }, { status: 400 });
|
|
1296
|
-
}
|
|
1297
|
-
const safePath = (parentPath || "public").replace(/\.\./g, "");
|
|
1298
|
-
const folderPath = getWorkspacePath(safePath, sanitizedName);
|
|
1299
|
-
if (!folderPath.startsWith(getPublicPath())) {
|
|
1300
|
-
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1301
|
-
}
|
|
1302
|
-
try {
|
|
1303
|
-
await fs6.access(folderPath);
|
|
1304
|
-
return jsonResponse({ error: "A folder with this name already exists" }, { status: 400 });
|
|
1305
|
-
} catch {
|
|
1306
|
-
}
|
|
1307
|
-
await fs6.mkdir(folderPath, { recursive: true });
|
|
1308
|
-
return jsonResponse({ success: true, path: path6.join(safePath, sanitizedName) });
|
|
1309
|
-
} catch (error) {
|
|
1310
|
-
console.error("Failed to create folder:", error);
|
|
1311
|
-
return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
async function handleRename(request) {
|
|
1315
|
-
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "");
|
|
1316
|
-
try {
|
|
1317
|
-
const { oldPath, newName } = await request.json();
|
|
1318
|
-
if (!oldPath || !newName) {
|
|
1319
|
-
return jsonResponse({ error: "Path and new name are required" }, { status: 400 });
|
|
1320
|
-
}
|
|
1321
|
-
const safePath = oldPath.replace(/\.\./g, "");
|
|
1322
|
-
const absoluteOldPath = getWorkspacePath(safePath);
|
|
1323
|
-
if (!absoluteOldPath.startsWith(getPublicPath())) {
|
|
1324
|
-
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1325
|
-
}
|
|
1326
|
-
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
1327
|
-
const oldKey = "/" + oldRelativePath;
|
|
1328
|
-
const isImage = isImageFile(path6.basename(oldPath));
|
|
1329
|
-
const meta = await loadMeta();
|
|
1330
|
-
const cdnUrls = getCdnUrls(meta);
|
|
1331
|
-
const entry = meta[oldKey];
|
|
1332
|
-
const isInCloud = entry?.c !== void 0;
|
|
1333
|
-
const fileCdnUrl = isInCloud && entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
1334
|
-
const isInOurR2 = isInCloud && fileCdnUrl === publicUrl;
|
|
1335
|
-
const hasThumbnails = entry ? isProcessed(entry) : false;
|
|
1336
|
-
let hasLocalFile = false;
|
|
1337
|
-
let isFile = true;
|
|
1338
|
-
try {
|
|
1339
|
-
const stats = await fs6.stat(absoluteOldPath);
|
|
1340
|
-
hasLocalFile = true;
|
|
1341
|
-
isFile = stats.isFile();
|
|
1342
|
-
} catch {
|
|
1343
|
-
if (!isInCloud) {
|
|
1344
|
-
return jsonResponse({ error: "File or folder not found" }, { status: 404 });
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
const sanitizedName = isFile ? slugifyFilename(newName) : slugifyFolderName(newName);
|
|
1348
|
-
if (!sanitizedName) {
|
|
1349
|
-
return jsonResponse({ error: "Invalid name" }, { status: 400 });
|
|
1350
|
-
}
|
|
1351
|
-
const parentDir = path6.dirname(absoluteOldPath);
|
|
1352
|
-
const absoluteNewPath = path6.join(parentDir, sanitizedName);
|
|
1353
|
-
const newRelativePath = path6.join(path6.dirname(oldRelativePath), sanitizedName);
|
|
1354
|
-
const newKey = "/" + newRelativePath;
|
|
1355
|
-
if (meta[newKey]) {
|
|
1356
|
-
return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
|
|
1357
|
-
}
|
|
1358
|
-
try {
|
|
1359
|
-
await fs6.access(absoluteNewPath);
|
|
1360
|
-
return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
|
|
1361
|
-
} catch {
|
|
1362
|
-
}
|
|
1363
|
-
if (isInOurR2 && !hasLocalFile && isImage) {
|
|
1364
|
-
await moveInCdn(oldKey, newKey, hasThumbnails);
|
|
1365
|
-
delete meta[oldKey];
|
|
1366
|
-
meta[newKey] = entry;
|
|
1367
|
-
await saveMeta(meta);
|
|
1368
|
-
const newPath2 = path6.join(path6.dirname(safePath), sanitizedName);
|
|
1369
|
-
return jsonResponse({ success: true, newPath: newPath2 });
|
|
1370
|
-
}
|
|
1371
|
-
if (hasLocalFile) {
|
|
1372
|
-
await fs6.rename(absoluteOldPath, absoluteNewPath);
|
|
1373
|
-
}
|
|
1374
|
-
if (isImage && entry) {
|
|
1375
|
-
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
1376
|
-
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
1377
|
-
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
1378
|
-
const oldThumbPath = getPublicPath(oldThumbPaths[i]);
|
|
1379
|
-
const newThumbPath = getPublicPath(newThumbPaths[i]);
|
|
1380
|
-
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
1321
|
+
sendEvent({ type: "cleanup", message: "Saving metadata..." });
|
|
1322
|
+
await saveMeta(meta);
|
|
1323
|
+
sendEvent({ type: "cleanup", message: "Cleaning up empty folders..." });
|
|
1324
|
+
const imagesDir = getPublicPath("images");
|
|
1381
1325
|
try {
|
|
1382
|
-
await
|
|
1326
|
+
await cleanupEmptyFoldersRecursive(imagesDir);
|
|
1383
1327
|
} catch {
|
|
1384
1328
|
}
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
try {
|
|
1389
|
-
await fs6.unlink(absoluteNewPath);
|
|
1390
|
-
} catch {
|
|
1329
|
+
let message = `Removed thumbnails from ${removed.length} image${removed.length !== 1 ? "s" : ""}.`;
|
|
1330
|
+
if (skipped.length > 0) {
|
|
1331
|
+
message += ` ${skipped.length} image${skipped.length !== 1 ? "s" : ""} had no thumbnails.`;
|
|
1391
1332
|
}
|
|
1392
|
-
|
|
1333
|
+
if (errors.length > 0) {
|
|
1334
|
+
message += ` ${errors.length} image${errors.length !== 1 ? "s" : ""} failed.`;
|
|
1335
|
+
}
|
|
1336
|
+
sendEvent({
|
|
1337
|
+
type: "complete",
|
|
1338
|
+
processed: removed.length,
|
|
1339
|
+
skipped: skipped.length,
|
|
1340
|
+
errors: errors.length,
|
|
1341
|
+
message
|
|
1342
|
+
});
|
|
1343
|
+
controller.close();
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
console.error("Unprocess stream error:", error);
|
|
1346
|
+
sendEvent({ type: "error", message: "Failed to remove thumbnails" });
|
|
1347
|
+
controller.close();
|
|
1393
1348
|
}
|
|
1394
|
-
delete meta[oldKey];
|
|
1395
|
-
meta[newKey] = entry;
|
|
1396
|
-
await saveMeta(meta);
|
|
1397
1349
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1350
|
+
});
|
|
1351
|
+
return new Response(stream, {
|
|
1352
|
+
headers: {
|
|
1353
|
+
"Content-Type": "text/event-stream",
|
|
1354
|
+
"Cache-Control": "no-cache",
|
|
1355
|
+
Connection: "keep-alive"
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1404
1358
|
}
|
|
1405
|
-
async function
|
|
1359
|
+
async function handleReprocessStream(request) {
|
|
1360
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1406
1361
|
const encoder = new TextEncoder();
|
|
1407
|
-
|
|
1362
|
+
let imageKeys;
|
|
1363
|
+
try {
|
|
1364
|
+
const body = await request.json();
|
|
1365
|
+
imageKeys = body.imageKeys;
|
|
1366
|
+
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1367
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1368
|
+
}
|
|
1369
|
+
} catch {
|
|
1370
|
+
return jsonResponse({ error: "Invalid request body" }, { status: 400 });
|
|
1371
|
+
}
|
|
1408
1372
|
const stream = new ReadableStream({
|
|
1409
1373
|
async start(controller) {
|
|
1410
1374
|
const sendEvent = (data) => {
|
|
@@ -1413,642 +1377,239 @@ async function handleRenameStream(request) {
|
|
|
1413
1377
|
`));
|
|
1414
1378
|
};
|
|
1415
1379
|
try {
|
|
1416
|
-
const { oldPath, newName } = await request.json();
|
|
1417
|
-
if (!oldPath || !newName) {
|
|
1418
|
-
sendEvent({ type: "error", message: "Path and new name are required" });
|
|
1419
|
-
controller.close();
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
const safePath = oldPath.replace(/\.\./g, "");
|
|
1423
|
-
const absoluteOldPath = getWorkspacePath(safePath);
|
|
1424
|
-
if (!absoluteOldPath.startsWith(getPublicPath())) {
|
|
1425
|
-
sendEvent({ type: "error", message: "Invalid path" });
|
|
1426
|
-
controller.close();
|
|
1427
|
-
return;
|
|
1428
|
-
}
|
|
1429
|
-
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
1430
|
-
const isImagePath = isImageFile(path6.basename(oldPath));
|
|
1431
|
-
let hasLocalItem = false;
|
|
1432
|
-
let isFile = true;
|
|
1433
|
-
let isVirtualFolder = false;
|
|
1434
|
-
try {
|
|
1435
|
-
const stats = await fs6.stat(absoluteOldPath);
|
|
1436
|
-
hasLocalItem = true;
|
|
1437
|
-
isFile = stats.isFile();
|
|
1438
|
-
} catch {
|
|
1439
|
-
const meta2 = await loadMeta();
|
|
1440
|
-
const oldKey2 = "/" + oldRelativePath;
|
|
1441
|
-
const entry2 = meta2[oldKey2];
|
|
1442
|
-
if (entry2) {
|
|
1443
|
-
isFile = true;
|
|
1444
|
-
} else {
|
|
1445
|
-
const folderPrefix = oldKey2 + "/";
|
|
1446
|
-
const hasChildrenInMeta = Object.keys(meta2).some((key) => key.startsWith(folderPrefix));
|
|
1447
|
-
if (hasChildrenInMeta) {
|
|
1448
|
-
isFile = false;
|
|
1449
|
-
isVirtualFolder = true;
|
|
1450
|
-
} else {
|
|
1451
|
-
sendEvent({ type: "error", message: "File or folder not found" });
|
|
1452
|
-
controller.close();
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
const sanitizedName = isFile ? slugifyFilename(newName) : slugifyFolderName(newName);
|
|
1458
|
-
if (!sanitizedName) {
|
|
1459
|
-
sendEvent({ type: "error", message: "Invalid name" });
|
|
1460
|
-
controller.close();
|
|
1461
|
-
return;
|
|
1462
|
-
}
|
|
1463
|
-
const parentDir = path6.dirname(absoluteOldPath);
|
|
1464
|
-
const absoluteNewPath = path6.join(parentDir, sanitizedName);
|
|
1465
|
-
const newRelativePath = path6.join(path6.dirname(oldRelativePath), sanitizedName);
|
|
1466
|
-
const newPath = path6.join(path6.dirname(safePath), sanitizedName);
|
|
1467
1380
|
const meta = await loadMeta();
|
|
1468
1381
|
const cdnUrls = getCdnUrls(meta);
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1382
|
+
const processed = [];
|
|
1383
|
+
const errors = [];
|
|
1384
|
+
const total = imageKeys.length;
|
|
1385
|
+
sendEvent({ type: "start", total });
|
|
1386
|
+
for (let i = 0; i < imageKeys.length; i++) {
|
|
1387
|
+
let imageKey = imageKeys[i];
|
|
1388
|
+
if (!imageKey.startsWith("/")) {
|
|
1389
|
+
imageKey = `/${imageKey}`;
|
|
1475
1390
|
}
|
|
1476
|
-
}
|
|
1477
|
-
if (!isVirtualFolder) {
|
|
1478
1391
|
try {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
if (isVirtualFolder) {
|
|
1487
|
-
const newPrefix = "/" + newRelativePath + "/";
|
|
1488
|
-
const hasConflict = Object.keys(meta).some((key) => key.startsWith(newPrefix));
|
|
1489
|
-
if (hasConflict) {
|
|
1490
|
-
sendEvent({ type: "error", message: "A folder with this name already exists" });
|
|
1491
|
-
controller.close();
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
if (!isFile) {
|
|
1496
|
-
const oldPrefix = "/" + oldRelativePath + "/";
|
|
1497
|
-
const newPrefix = "/" + newRelativePath + "/";
|
|
1498
|
-
const itemsToUpdate = [];
|
|
1499
|
-
for (const [key, entry2] of Object.entries(meta)) {
|
|
1500
|
-
if (key.startsWith(oldPrefix) && entry2 && typeof entry2 === "object") {
|
|
1501
|
-
const newKey2 = key.replace(oldPrefix, newPrefix);
|
|
1502
|
-
itemsToUpdate.push({ oldKey: key, newKey: newKey2, entry: entry2 });
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
const total = itemsToUpdate.length + 1;
|
|
1506
|
-
sendEvent({ type: "start", total, message: `Renaming folder with ${itemsToUpdate.length} item(s)...` });
|
|
1507
|
-
if (hasLocalItem) {
|
|
1508
|
-
await fs6.rename(absoluteOldPath, absoluteNewPath);
|
|
1509
|
-
const imagesDir = getPublicPath("/images");
|
|
1510
|
-
const oldThumbFolder2 = path6.join(imagesDir, oldRelativePath);
|
|
1511
|
-
const newThumbFolder = path6.join(imagesDir, newRelativePath);
|
|
1392
|
+
let buffer;
|
|
1393
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
1394
|
+
const existingCdnIndex = entry?.c;
|
|
1395
|
+
const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
|
|
1396
|
+
const isInOurR2 = existingCdnUrl === publicUrl;
|
|
1397
|
+
const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
|
|
1398
|
+
const originalPath = getPublicPath(imageKey);
|
|
1512
1399
|
try {
|
|
1513
|
-
await fs6.
|
|
1514
|
-
await fs6.mkdir(path6.dirname(newThumbFolder), { recursive: true });
|
|
1515
|
-
await fs6.rename(oldThumbFolder2, newThumbFolder);
|
|
1400
|
+
buffer = await fs6.readFile(originalPath);
|
|
1516
1401
|
} catch {
|
|
1402
|
+
if (isInOurR2) {
|
|
1403
|
+
buffer = await downloadFromCdn(imageKey);
|
|
1404
|
+
const dir = path6.dirname(originalPath);
|
|
1405
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
1406
|
+
await fs6.writeFile(originalPath, buffer);
|
|
1407
|
+
} else if (isRemote && existingCdnUrl) {
|
|
1408
|
+
const remoteUrl = `${existingCdnUrl}${imageKey}`;
|
|
1409
|
+
buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1410
|
+
const dir = path6.dirname(originalPath);
|
|
1411
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
1412
|
+
await fs6.writeFile(originalPath, buffer);
|
|
1413
|
+
} else {
|
|
1414
|
+
throw new Error(`File not found: ${imageKey}`);
|
|
1415
|
+
}
|
|
1517
1416
|
}
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1417
|
+
const ext = path6.extname(imageKey).toLowerCase();
|
|
1418
|
+
const isSvg = ext === ".svg";
|
|
1419
|
+
if (isSvg) {
|
|
1420
|
+
const imageDir = path6.dirname(imageKey.slice(1));
|
|
1421
|
+
const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
|
|
1422
|
+
await fs6.mkdir(imagesPath, { recursive: true });
|
|
1423
|
+
const fileName = path6.basename(imageKey);
|
|
1424
|
+
const destPath = path6.join(imagesPath, fileName);
|
|
1425
|
+
await fs6.writeFile(destPath, buffer);
|
|
1426
|
+
meta[imageKey] = {
|
|
1427
|
+
...entry,
|
|
1428
|
+
o: { w: 0, h: 0 },
|
|
1429
|
+
b: "",
|
|
1430
|
+
f: { w: 0, h: 0 }
|
|
1431
|
+
};
|
|
1432
|
+
if (isRemote) {
|
|
1433
|
+
delete meta[imageKey].c;
|
|
1434
|
+
}
|
|
1435
|
+
} else {
|
|
1436
|
+
const updatedEntry = await processImage(buffer, imageKey);
|
|
1437
|
+
if (isInOurR2) {
|
|
1438
|
+
updatedEntry.c = existingCdnIndex;
|
|
1439
|
+
await deleteOriginalFromCdn(imageKey);
|
|
1440
|
+
await deleteThumbnailsFromCdn(imageKey);
|
|
1441
|
+
await uploadOriginalToCdn(imageKey);
|
|
1442
|
+
await uploadToCdn(imageKey);
|
|
1443
|
+
await deleteLocalThumbnails(imageKey);
|
|
1531
1444
|
try {
|
|
1532
|
-
await fs6.unlink(
|
|
1445
|
+
await fs6.unlink(originalPath);
|
|
1533
1446
|
} catch {
|
|
1534
1447
|
}
|
|
1535
|
-
if (hasThumbnails2) {
|
|
1536
|
-
await deleteLocalThumbnails(newKey2);
|
|
1537
|
-
}
|
|
1538
|
-
} catch (err) {
|
|
1539
|
-
console.error(`Failed to rename in CDN ${oldKey2}:`, err);
|
|
1540
1448
|
}
|
|
1449
|
+
meta[imageKey] = updatedEntry;
|
|
1541
1450
|
}
|
|
1542
|
-
|
|
1543
|
-
meta[newKey2] = entry2;
|
|
1544
|
-
renamed++;
|
|
1451
|
+
processed.push(imageKey);
|
|
1545
1452
|
sendEvent({
|
|
1546
1453
|
type: "progress",
|
|
1547
|
-
current:
|
|
1454
|
+
current: i + 1,
|
|
1548
1455
|
total,
|
|
1549
|
-
|
|
1550
|
-
|
|
1456
|
+
processed: processed.length,
|
|
1457
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1458
|
+
message: `Processed ${imageKey.slice(1)}`
|
|
1459
|
+
});
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
console.error(`Failed to reprocess ${imageKey}:`, error);
|
|
1462
|
+
errors.push(imageKey);
|
|
1463
|
+
sendEvent({
|
|
1464
|
+
type: "progress",
|
|
1465
|
+
current: i + 1,
|
|
1466
|
+
total,
|
|
1467
|
+
processed: processed.length,
|
|
1468
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1469
|
+
message: `Failed: ${imageKey.slice(1)}`
|
|
1551
1470
|
});
|
|
1552
1471
|
}
|
|
1553
|
-
await saveMeta(meta);
|
|
1554
|
-
await deleteEmptyFolders(absoluteOldPath);
|
|
1555
|
-
const oldThumbFolder = path6.join(getPublicPath("/images"), oldRelativePath);
|
|
1556
|
-
await deleteEmptyFolders(oldThumbFolder);
|
|
1557
|
-
sendEvent({ type: "complete", renamed, newPath });
|
|
1558
|
-
controller.close();
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
const oldKey = "/" + oldRelativePath;
|
|
1562
|
-
const newKey = "/" + newRelativePath;
|
|
1563
|
-
const entry = meta[oldKey];
|
|
1564
|
-
const isInCloud = entry?.c !== void 0;
|
|
1565
|
-
const fileCdnUrl = isInCloud && entry?.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
1566
|
-
const isInOurR2 = isInCloud && fileCdnUrl === publicUrl;
|
|
1567
|
-
const hasThumbnails = entry ? isProcessed(entry) : false;
|
|
1568
|
-
sendEvent({ type: "start", total: 1, message: "Renaming file..." });
|
|
1569
|
-
if (isInOurR2 && !hasLocalItem && isImagePath) {
|
|
1570
|
-
await moveInCdn(oldKey, newKey, hasThumbnails);
|
|
1571
|
-
delete meta[oldKey];
|
|
1572
|
-
if (entry) meta[newKey] = entry;
|
|
1573
|
-
await saveMeta(meta);
|
|
1574
|
-
sendEvent({ type: "complete", renamed: 1, newPath });
|
|
1575
|
-
controller.close();
|
|
1576
|
-
return;
|
|
1577
1472
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
}
|
|
1581
|
-
if (
|
|
1582
|
-
|
|
1583
|
-
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
1584
|
-
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
1585
|
-
const oldThumbPath = getPublicPath(oldThumbPaths[i]);
|
|
1586
|
-
const newThumbPath = getPublicPath(newThumbPaths[i]);
|
|
1587
|
-
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
1588
|
-
try {
|
|
1589
|
-
await fs6.rename(oldThumbPath, newThumbPath);
|
|
1590
|
-
} catch {
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
if (isInOurR2) {
|
|
1594
|
-
await moveInCdn(oldKey, newKey, hasThumbnails);
|
|
1595
|
-
try {
|
|
1596
|
-
await fs6.unlink(absoluteNewPath);
|
|
1597
|
-
} catch {
|
|
1598
|
-
}
|
|
1599
|
-
await deleteLocalThumbnails(newKey);
|
|
1600
|
-
}
|
|
1601
|
-
delete meta[oldKey];
|
|
1602
|
-
meta[newKey] = entry;
|
|
1603
|
-
await saveMeta(meta);
|
|
1473
|
+
sendEvent({ type: "cleanup", message: "Saving metadata..." });
|
|
1474
|
+
await saveMeta(meta);
|
|
1475
|
+
let message = `Generated thumbnails for ${processed.length} image${processed.length !== 1 ? "s" : ""}.`;
|
|
1476
|
+
if (errors.length > 0) {
|
|
1477
|
+
message += ` ${errors.length} image${errors.length !== 1 ? "s" : ""} failed.`;
|
|
1604
1478
|
}
|
|
1605
|
-
sendEvent({
|
|
1479
|
+
sendEvent({
|
|
1480
|
+
type: "complete",
|
|
1481
|
+
processed: processed.length,
|
|
1482
|
+
errors: errors.length,
|
|
1483
|
+
message
|
|
1484
|
+
});
|
|
1606
1485
|
controller.close();
|
|
1607
1486
|
} catch (error) {
|
|
1608
|
-
console.error("
|
|
1609
|
-
sendEvent({ type: "error", message: "Failed to
|
|
1487
|
+
console.error("Reprocess stream error:", error);
|
|
1488
|
+
sendEvent({ type: "error", message: "Failed to generate thumbnails" });
|
|
1610
1489
|
controller.close();
|
|
1611
1490
|
}
|
|
1612
1491
|
}
|
|
1613
1492
|
});
|
|
1614
|
-
return
|
|
1493
|
+
return new Response(stream, {
|
|
1494
|
+
headers: {
|
|
1495
|
+
"Content-Type": "text/event-stream",
|
|
1496
|
+
"Cache-Control": "no-cache",
|
|
1497
|
+
Connection: "keep-alive"
|
|
1498
|
+
}
|
|
1499
|
+
});
|
|
1615
1500
|
}
|
|
1616
|
-
async function
|
|
1617
|
-
const
|
|
1501
|
+
async function handleDownloadStream(request) {
|
|
1502
|
+
const { imageKeys, operationId } = await request.json();
|
|
1503
|
+
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1504
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1505
|
+
}
|
|
1618
1506
|
const stream = new ReadableStream({
|
|
1619
1507
|
async start(controller) {
|
|
1508
|
+
const encoder = new TextEncoder();
|
|
1620
1509
|
const sendEvent = (data) => {
|
|
1621
|
-
|
|
1510
|
+
try {
|
|
1511
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
1622
1512
|
|
|
1623
1513
|
`));
|
|
1514
|
+
} catch {
|
|
1515
|
+
}
|
|
1624
1516
|
};
|
|
1517
|
+
sendEvent({ type: "start", total: imageKeys.length });
|
|
1518
|
+
const downloaded = [];
|
|
1519
|
+
const skipped = [];
|
|
1520
|
+
const errors = [];
|
|
1521
|
+
const isCancelled = () => operationId ? isOperationCancelled(operationId) : false;
|
|
1625
1522
|
try {
|
|
1626
|
-
const { paths, destination } = await request.json();
|
|
1627
|
-
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
1628
|
-
sendEvent({ type: "error", message: "Paths are required" });
|
|
1629
|
-
controller.close();
|
|
1630
|
-
return;
|
|
1631
|
-
}
|
|
1632
|
-
if (!destination || typeof destination !== "string") {
|
|
1633
|
-
sendEvent({ type: "error", message: "Destination is required" });
|
|
1634
|
-
controller.close();
|
|
1635
|
-
return;
|
|
1636
|
-
}
|
|
1637
|
-
const safeDestination = destination.replace(/\.\./g, "");
|
|
1638
|
-
const absoluteDestination = getWorkspacePath(safeDestination);
|
|
1639
|
-
if (!absoluteDestination.startsWith(getPublicPath())) {
|
|
1640
|
-
sendEvent({ type: "error", message: "Invalid destination" });
|
|
1641
|
-
controller.close();
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
await fs6.mkdir(absoluteDestination, { recursive: true });
|
|
1645
1523
|
const meta = await loadMeta();
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
const destWithoutPublic = safeDestination.replace(/^public\/?/, "");
|
|
1658
|
-
const newRelativePath = destWithoutPublic ? path6.join(destWithoutPublic, itemName) : itemName;
|
|
1659
|
-
const oldKey = "/" + oldRelativePath;
|
|
1660
|
-
const newKey = "/" + newRelativePath;
|
|
1661
|
-
const newAbsolutePath = path6.join(absoluteDestination, itemName);
|
|
1662
|
-
const absolutePath = getWorkspacePath(safePath);
|
|
1663
|
-
let hasLocalItem = false;
|
|
1664
|
-
let isDirectory = false;
|
|
1665
|
-
try {
|
|
1666
|
-
const stats = await fs6.stat(absolutePath);
|
|
1667
|
-
hasLocalItem = true;
|
|
1668
|
-
isDirectory = stats.isDirectory();
|
|
1669
|
-
} catch {
|
|
1670
|
-
}
|
|
1671
|
-
if (hasLocalItem && isDirectory) {
|
|
1672
|
-
const countFilesRecursive = async (dir) => {
|
|
1673
|
-
let count = 0;
|
|
1674
|
-
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1675
|
-
for (const entry of entries) {
|
|
1676
|
-
if (entry.isDirectory()) {
|
|
1677
|
-
count += await countFilesRecursive(path6.join(dir, entry.name));
|
|
1678
|
-
} else {
|
|
1679
|
-
count++;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
return count;
|
|
1683
|
-
};
|
|
1684
|
-
const localFileCount = await countFilesRecursive(absolutePath);
|
|
1685
|
-
const folderPrefix = oldKey + "/";
|
|
1686
|
-
let cloudOnlyCount = 0;
|
|
1687
|
-
for (const metaKey of Object.keys(meta)) {
|
|
1688
|
-
if (metaKey.startsWith(folderPrefix)) {
|
|
1689
|
-
const relPath = metaKey.slice(folderPrefix.length);
|
|
1690
|
-
const localPath = path6.join(absolutePath, relPath);
|
|
1691
|
-
try {
|
|
1692
|
-
await fs6.access(localPath);
|
|
1693
|
-
} catch {
|
|
1694
|
-
cloudOnlyCount++;
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
totalFiles += localFileCount + cloudOnlyCount;
|
|
1699
|
-
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: false });
|
|
1700
|
-
} else if (!hasLocalItem) {
|
|
1701
|
-
const folderPrefix = oldKey + "/";
|
|
1702
|
-
const virtualItems = [];
|
|
1703
|
-
for (const [key, metaEntry] of Object.entries(meta)) {
|
|
1704
|
-
if (key.startsWith(folderPrefix) && metaEntry && typeof metaEntry === "object") {
|
|
1705
|
-
const relativePath = key.slice(folderPrefix.length);
|
|
1706
|
-
const destNewKey = newKey + "/" + relativePath;
|
|
1707
|
-
virtualItems.push({ oldKey: key, newKey: destNewKey, entry: metaEntry });
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
if (virtualItems.length > 0) {
|
|
1711
|
-
totalFiles += virtualItems.length;
|
|
1712
|
-
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: true, virtualFolderItems: virtualItems });
|
|
1713
|
-
sourceFolders.add(absolutePath);
|
|
1714
|
-
} else {
|
|
1715
|
-
totalFiles++;
|
|
1716
|
-
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: false });
|
|
1717
|
-
}
|
|
1718
|
-
} else {
|
|
1719
|
-
totalFiles++;
|
|
1720
|
-
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: false });
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
sendEvent({ type: "start", total: totalFiles });
|
|
1724
|
-
let processedFiles = 0;
|
|
1725
|
-
for (const expandedItem of expandedItems) {
|
|
1726
|
-
const { itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder, virtualFolderItems } = expandedItem;
|
|
1727
|
-
if (isVirtualFolder && virtualFolderItems) {
|
|
1728
|
-
for (const vItem of virtualFolderItems) {
|
|
1729
|
-
const itemEntry = vItem.entry;
|
|
1730
|
-
const isItemInCloud = itemEntry.c !== void 0;
|
|
1731
|
-
const itemCdnUrl = isItemInCloud ? cdnUrls[itemEntry.c] : void 0;
|
|
1732
|
-
const isItemInR2 = isItemInCloud && itemCdnUrl === r2PublicUrl;
|
|
1733
|
-
const itemHasThumbnails = isProcessed(itemEntry);
|
|
1734
|
-
let vItemMoved = false;
|
|
1735
|
-
if (isItemInR2) {
|
|
1736
|
-
try {
|
|
1737
|
-
await moveInCdn(vItem.oldKey, vItem.newKey, itemHasThumbnails);
|
|
1738
|
-
vItemMoved = true;
|
|
1739
|
-
} catch (err) {
|
|
1740
|
-
console.error(`Failed to move cloud item ${vItem.oldKey}:`, err);
|
|
1741
|
-
delete meta[vItem.oldKey];
|
|
1742
|
-
}
|
|
1743
|
-
}
|
|
1744
|
-
if (vItemMoved) {
|
|
1745
|
-
delete meta[vItem.oldKey];
|
|
1746
|
-
meta[vItem.newKey] = itemEntry;
|
|
1747
|
-
}
|
|
1748
|
-
processedFiles++;
|
|
1749
|
-
sendEvent({
|
|
1750
|
-
type: "progress",
|
|
1751
|
-
current: processedFiles,
|
|
1752
|
-
total: totalFiles,
|
|
1753
|
-
moved: moved.length,
|
|
1754
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
1755
|
-
currentFile: path6.basename(vItem.newKey)
|
|
1756
|
-
});
|
|
1757
|
-
}
|
|
1758
|
-
const newFolderPath = getPublicPath(newKey);
|
|
1759
|
-
await deleteEmptyFolders(newFolderPath);
|
|
1760
|
-
const newThumbFolder = path6.join(getPublicPath("images"), newKey.slice(1));
|
|
1761
|
-
await deleteEmptyFolders(newThumbFolder);
|
|
1762
|
-
moved.push(itemPath);
|
|
1763
|
-
continue;
|
|
1524
|
+
for (let i = 0; i < imageKeys.length; i++) {
|
|
1525
|
+
if (isCancelled()) {
|
|
1526
|
+
await saveMeta(meta);
|
|
1527
|
+
if (operationId) clearCancelledOperation(operationId);
|
|
1528
|
+
sendEvent({
|
|
1529
|
+
type: "stopped",
|
|
1530
|
+
downloaded: downloaded.length,
|
|
1531
|
+
message: `Stopped. ${downloaded.length} image${downloaded.length !== 1 ? "s" : ""} downloaded.`
|
|
1532
|
+
});
|
|
1533
|
+
controller.close();
|
|
1534
|
+
return;
|
|
1764
1535
|
}
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1536
|
+
const imageKey = imageKeys[i];
|
|
1537
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
1538
|
+
if (!entry || entry.c === void 0) {
|
|
1539
|
+
skipped.push(imageKey);
|
|
1768
1540
|
sendEvent({
|
|
1769
1541
|
type: "progress",
|
|
1770
|
-
current:
|
|
1771
|
-
total:
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
currentFile: itemName
|
|
1542
|
+
current: i + 1,
|
|
1543
|
+
total: imageKeys.length,
|
|
1544
|
+
downloaded: downloaded.length,
|
|
1545
|
+
message: `Skipped ${imageKey} (not on cloud)`
|
|
1775
1546
|
});
|
|
1776
1547
|
continue;
|
|
1777
1548
|
}
|
|
1778
|
-
const entry = meta[oldKey];
|
|
1779
|
-
const isImage = isImageFile(itemName);
|
|
1780
|
-
const isInCloud = entry?.c !== void 0;
|
|
1781
|
-
const fileCdnUrl = isInCloud && entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
1782
|
-
const isRemote = isInCloud && (!r2PublicUrl || fileCdnUrl !== r2PublicUrl);
|
|
1783
|
-
const isPushedToR2 = isInCloud && r2PublicUrl && fileCdnUrl === r2PublicUrl;
|
|
1784
|
-
const hasProcessedThumbnails = isProcessed(entry);
|
|
1785
1549
|
try {
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
const buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1791
|
-
await fs6.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
|
|
1792
|
-
await fs6.writeFile(newAbsolutePath, buffer);
|
|
1793
|
-
const newEntry = {
|
|
1794
|
-
o: entry?.o,
|
|
1795
|
-
b: entry?.b
|
|
1796
|
-
};
|
|
1797
|
-
delete meta[oldKey];
|
|
1798
|
-
meta[newKey] = newEntry;
|
|
1799
|
-
moved.push(itemPath);
|
|
1800
|
-
processedFiles++;
|
|
1801
|
-
sendEvent({
|
|
1802
|
-
type: "progress",
|
|
1803
|
-
current: processedFiles,
|
|
1804
|
-
total: totalFiles,
|
|
1805
|
-
moved: moved.length,
|
|
1806
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
1807
|
-
currentFile: itemName
|
|
1808
|
-
});
|
|
1809
|
-
} else if (isPushedToR2 && isImage) {
|
|
1810
|
-
await moveInCdn(oldKey, newKey, hasProcessedThumbnails);
|
|
1811
|
-
delete meta[oldKey];
|
|
1812
|
-
if (entry) {
|
|
1813
|
-
meta[newKey] = entry;
|
|
1814
|
-
}
|
|
1815
|
-
moved.push(itemPath);
|
|
1816
|
-
processedFiles++;
|
|
1550
|
+
const imageBuffer = await downloadFromCdn(imageKey);
|
|
1551
|
+
if (isCancelled()) {
|
|
1552
|
+
await saveMeta(meta);
|
|
1553
|
+
if (operationId) clearCancelledOperation(operationId);
|
|
1817
1554
|
sendEvent({
|
|
1818
|
-
type: "
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
moved: moved.length,
|
|
1822
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
1823
|
-
currentFile: itemName
|
|
1555
|
+
type: "stopped",
|
|
1556
|
+
downloaded: downloaded.length,
|
|
1557
|
+
message: `Stopped. ${downloaded.length} image${downloaded.length !== 1 ? "s" : ""} downloaded.`
|
|
1824
1558
|
});
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
try {
|
|
1841
|
-
await fs6.access(absolutePath);
|
|
1842
|
-
} catch {
|
|
1843
|
-
errors.push(`${itemName} not found`);
|
|
1844
|
-
processedFiles++;
|
|
1845
|
-
sendEvent({
|
|
1846
|
-
type: "progress",
|
|
1847
|
-
current: processedFiles,
|
|
1848
|
-
total: totalFiles,
|
|
1849
|
-
moved: moved.length,
|
|
1850
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
1851
|
-
currentFile: itemName
|
|
1852
|
-
});
|
|
1853
|
-
continue;
|
|
1854
|
-
}
|
|
1855
|
-
try {
|
|
1856
|
-
await fs6.access(newAbsolutePath);
|
|
1857
|
-
errors.push(`${itemName} already exists in destination`);
|
|
1858
|
-
processedFiles++;
|
|
1859
|
-
sendEvent({
|
|
1860
|
-
type: "progress",
|
|
1861
|
-
current: processedFiles,
|
|
1862
|
-
total: totalFiles,
|
|
1863
|
-
moved: moved.length,
|
|
1864
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
1865
|
-
currentFile: itemName
|
|
1866
|
-
});
|
|
1867
|
-
continue;
|
|
1868
|
-
} catch {
|
|
1869
|
-
}
|
|
1870
|
-
const stats = await fs6.stat(absolutePath);
|
|
1871
|
-
if (stats.isFile()) {
|
|
1872
|
-
await fs6.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
|
|
1873
|
-
await fs6.rename(absolutePath, newAbsolutePath);
|
|
1874
|
-
if (isImage && entry) {
|
|
1875
|
-
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
1876
|
-
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
1877
|
-
for (let j = 0; j < oldThumbPaths.length; j++) {
|
|
1878
|
-
const oldThumbPath = getPublicPath(oldThumbPaths[j]);
|
|
1879
|
-
const newThumbPath = getPublicPath(newThumbPaths[j]);
|
|
1880
|
-
try {
|
|
1881
|
-
await fs6.access(oldThumbPath);
|
|
1882
|
-
sourceFolders.add(path6.dirname(oldThumbPath));
|
|
1883
|
-
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
1884
|
-
await fs6.rename(oldThumbPath, newThumbPath);
|
|
1885
|
-
} catch {
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1888
|
-
const fileIsInCloud = entry.c !== void 0;
|
|
1889
|
-
const fileCdnUrl2 = fileIsInCloud ? cdnUrls[entry.c] : void 0;
|
|
1890
|
-
const fileIsInR2 = fileIsInCloud && fileCdnUrl2 === r2PublicUrl;
|
|
1891
|
-
const fileHasThumbs = isProcessed(entry);
|
|
1892
|
-
if (fileIsInR2) {
|
|
1893
|
-
await deleteFromCdn(oldKey, fileHasThumbs);
|
|
1894
|
-
await uploadOriginalToCdn(newKey);
|
|
1895
|
-
if (fileHasThumbs) {
|
|
1896
|
-
await uploadToCdn(newKey);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
delete meta[oldKey];
|
|
1900
|
-
meta[newKey] = entry;
|
|
1901
|
-
}
|
|
1902
|
-
processedFiles++;
|
|
1903
|
-
sendEvent({
|
|
1904
|
-
type: "progress",
|
|
1905
|
-
current: processedFiles,
|
|
1906
|
-
total: totalFiles,
|
|
1907
|
-
moved: moved.length,
|
|
1908
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
1909
|
-
currentFile: itemName
|
|
1910
|
-
});
|
|
1911
|
-
moved.push(itemPath);
|
|
1912
|
-
} else if (stats.isDirectory()) {
|
|
1913
|
-
const oldPrefix = oldKey + "/";
|
|
1914
|
-
const newPrefix = newKey + "/";
|
|
1915
|
-
const localFiles = [];
|
|
1916
|
-
const collectLocalFiles = async (dir, relativeDir) => {
|
|
1917
|
-
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1918
|
-
for (const dirEntry of entries) {
|
|
1919
|
-
const entryRelPath = relativeDir ? `${relativeDir}/${dirEntry.name}` : dirEntry.name;
|
|
1920
|
-
if (dirEntry.isDirectory()) {
|
|
1921
|
-
await collectLocalFiles(path6.join(dir, dirEntry.name), entryRelPath);
|
|
1922
|
-
} else {
|
|
1923
|
-
localFiles.push({ relativePath: entryRelPath, isImage: isImageFile(dirEntry.name) });
|
|
1924
|
-
}
|
|
1925
|
-
}
|
|
1926
|
-
};
|
|
1927
|
-
await collectLocalFiles(absolutePath, "");
|
|
1928
|
-
const cloudOnlyFiles = [];
|
|
1929
|
-
for (const [metaKey, metaEntry] of Object.entries(meta)) {
|
|
1930
|
-
if (metaKey.startsWith(oldPrefix) && metaEntry && typeof metaEntry === "object") {
|
|
1931
|
-
const relPath = metaKey.slice(oldPrefix.length);
|
|
1932
|
-
const localPath = path6.join(absolutePath, relPath);
|
|
1933
|
-
try {
|
|
1934
|
-
await fs6.access(localPath);
|
|
1935
|
-
} catch {
|
|
1936
|
-
cloudOnlyFiles.push({
|
|
1937
|
-
oldKey: metaKey,
|
|
1938
|
-
newKey: newPrefix + relPath,
|
|
1939
|
-
entry: metaEntry
|
|
1940
|
-
});
|
|
1941
|
-
}
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
for (const localFile of localFiles) {
|
|
1945
|
-
const fileOldPath = path6.join(absolutePath, localFile.relativePath);
|
|
1946
|
-
const fileNewPath = path6.join(newAbsolutePath, localFile.relativePath);
|
|
1947
|
-
const fileOldKey = oldPrefix + localFile.relativePath;
|
|
1948
|
-
const fileNewKey = newPrefix + localFile.relativePath;
|
|
1949
|
-
const fileEntry = meta[fileOldKey];
|
|
1950
|
-
sourceFolders.add(path6.dirname(fileOldPath));
|
|
1951
|
-
await fs6.mkdir(path6.dirname(fileNewPath), { recursive: true });
|
|
1952
|
-
await fs6.rename(fileOldPath, fileNewPath);
|
|
1953
|
-
if (localFile.isImage && fileEntry) {
|
|
1954
|
-
const oldThumbPaths = getAllThumbnailPaths(fileOldKey);
|
|
1955
|
-
const newThumbPaths = getAllThumbnailPaths(fileNewKey);
|
|
1956
|
-
for (let t = 0; t < oldThumbPaths.length; t++) {
|
|
1957
|
-
const oldThumbPath = getPublicPath(oldThumbPaths[t]);
|
|
1958
|
-
const newThumbPath = getPublicPath(newThumbPaths[t]);
|
|
1959
|
-
try {
|
|
1960
|
-
await fs6.access(oldThumbPath);
|
|
1961
|
-
sourceFolders.add(path6.dirname(oldThumbPath));
|
|
1962
|
-
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
1963
|
-
await fs6.rename(oldThumbPath, newThumbPath);
|
|
1964
|
-
} catch {
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
const fileIsInCloud = fileEntry.c !== void 0;
|
|
1968
|
-
const fileCdnUrl2 = fileIsInCloud ? cdnUrls[fileEntry.c] : void 0;
|
|
1969
|
-
const fileIsInR2 = fileIsInCloud && fileCdnUrl2 === r2PublicUrl;
|
|
1970
|
-
const fileHasThumbs = isProcessed(fileEntry);
|
|
1971
|
-
if (fileIsInR2) {
|
|
1972
|
-
await moveInCdn(fileOldKey, fileNewKey, fileHasThumbs);
|
|
1973
|
-
}
|
|
1974
|
-
delete meta[fileOldKey];
|
|
1975
|
-
meta[fileNewKey] = fileEntry;
|
|
1976
|
-
}
|
|
1977
|
-
processedFiles++;
|
|
1978
|
-
sendEvent({
|
|
1979
|
-
type: "progress",
|
|
1980
|
-
current: processedFiles,
|
|
1981
|
-
total: totalFiles,
|
|
1982
|
-
moved: moved.length,
|
|
1983
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
1984
|
-
currentFile: path6.basename(localFile.relativePath)
|
|
1985
|
-
});
|
|
1986
|
-
}
|
|
1987
|
-
for (const cloudFile of cloudOnlyFiles) {
|
|
1988
|
-
const cloudEntry = cloudFile.entry;
|
|
1989
|
-
const cloudIsInCloud = cloudEntry.c !== void 0;
|
|
1990
|
-
const cloudCdnUrl = cloudIsInCloud ? cdnUrls[cloudEntry.c] : void 0;
|
|
1991
|
-
const cloudIsInR2 = cloudIsInCloud && cloudCdnUrl === r2PublicUrl;
|
|
1992
|
-
const cloudHasThumbs = isProcessed(cloudEntry);
|
|
1993
|
-
let cloudFileMoved = false;
|
|
1994
|
-
if (cloudIsInR2) {
|
|
1995
|
-
try {
|
|
1996
|
-
await moveInCdn(cloudFile.oldKey, cloudFile.newKey, cloudHasThumbs);
|
|
1997
|
-
cloudFileMoved = true;
|
|
1998
|
-
} catch (err) {
|
|
1999
|
-
console.error(`Failed to move cloud file ${cloudFile.oldKey}:`, err);
|
|
2000
|
-
delete meta[cloudFile.oldKey];
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
if (cloudFileMoved) {
|
|
2004
|
-
delete meta[cloudFile.oldKey];
|
|
2005
|
-
meta[cloudFile.newKey] = cloudEntry;
|
|
2006
|
-
}
|
|
2007
|
-
processedFiles++;
|
|
2008
|
-
sendEvent({
|
|
2009
|
-
type: "progress",
|
|
2010
|
-
current: processedFiles,
|
|
2011
|
-
total: totalFiles,
|
|
2012
|
-
moved: moved.length,
|
|
2013
|
-
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2014
|
-
currentFile: path6.basename(cloudFile.newKey)
|
|
2015
|
-
});
|
|
2016
|
-
}
|
|
2017
|
-
sourceFolders.add(absolutePath);
|
|
2018
|
-
const oldThumbRelPath = oldKey.slice(1);
|
|
2019
|
-
const oldThumbFolder = path6.join(getPublicPath("images"), oldThumbRelPath);
|
|
2020
|
-
sourceFolders.add(oldThumbFolder);
|
|
2021
|
-
moved.push(itemPath);
|
|
2022
|
-
}
|
|
1559
|
+
controller.close();
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const localPath = getPublicPath(imageKey.replace(/^\//, ""));
|
|
1563
|
+
await fs6.mkdir(path6.dirname(localPath), { recursive: true });
|
|
1564
|
+
await fs6.writeFile(localPath, imageBuffer);
|
|
1565
|
+
await deleteThumbnailsFromCdn(imageKey);
|
|
1566
|
+
const wasProcessed = isProcessed(entry);
|
|
1567
|
+
delete entry.c;
|
|
1568
|
+
if (wasProcessed) {
|
|
1569
|
+
const processedEntry = await processImage(imageBuffer, imageKey);
|
|
1570
|
+
entry.sm = processedEntry.sm;
|
|
1571
|
+
entry.md = processedEntry.md;
|
|
1572
|
+
entry.lg = processedEntry.lg;
|
|
1573
|
+
entry.f = processedEntry.f;
|
|
2023
1574
|
}
|
|
2024
|
-
|
|
2025
|
-
console.error(`Failed to move ${itemName}:`, err);
|
|
2026
|
-
errors.push(`Failed to move ${itemName}`);
|
|
2027
|
-
processedFiles++;
|
|
1575
|
+
downloaded.push(imageKey);
|
|
2028
1576
|
sendEvent({
|
|
2029
1577
|
type: "progress",
|
|
2030
|
-
current:
|
|
2031
|
-
total:
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
1578
|
+
current: i + 1,
|
|
1579
|
+
total: imageKeys.length,
|
|
1580
|
+
downloaded: downloaded.length,
|
|
1581
|
+
message: `Downloaded ${imageKey}`
|
|
1582
|
+
});
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
console.error(`Failed to download ${imageKey}:`, error);
|
|
1585
|
+
errors.push(imageKey);
|
|
1586
|
+
sendEvent({
|
|
1587
|
+
type: "progress",
|
|
1588
|
+
current: i + 1,
|
|
1589
|
+
total: imageKeys.length,
|
|
1590
|
+
downloaded: downloaded.length,
|
|
1591
|
+
message: `Failed to download ${imageKey}`
|
|
2035
1592
|
});
|
|
2036
1593
|
}
|
|
2037
1594
|
}
|
|
2038
1595
|
await saveMeta(meta);
|
|
2039
|
-
|
|
2040
|
-
|
|
1596
|
+
let message = `Downloaded ${downloaded.length} image${downloaded.length !== 1 ? "s" : ""}.`;
|
|
1597
|
+
if (skipped.length > 0) {
|
|
1598
|
+
message += ` ${skipped.length} image${skipped.length !== 1 ? "s were" : " was"} not on cloud.`;
|
|
1599
|
+
}
|
|
1600
|
+
if (errors.length > 0) {
|
|
1601
|
+
message += ` ${errors.length} image${errors.length !== 1 ? "s" : ""} failed.`;
|
|
2041
1602
|
}
|
|
2042
|
-
await deleteEmptyFolders(absoluteDestination);
|
|
2043
1603
|
sendEvent({
|
|
2044
1604
|
type: "complete",
|
|
2045
|
-
|
|
1605
|
+
downloaded: downloaded.length,
|
|
1606
|
+
skipped: skipped.length,
|
|
2046
1607
|
errors: errors.length,
|
|
2047
|
-
|
|
1608
|
+
message
|
|
2048
1609
|
});
|
|
2049
1610
|
} catch (error) {
|
|
2050
|
-
console.error("
|
|
2051
|
-
sendEvent({ type: "error", message: "Failed to
|
|
1611
|
+
console.error("Download stream error:", error);
|
|
1612
|
+
sendEvent({ type: "error", message: "Failed to download images" });
|
|
2052
1613
|
} finally {
|
|
2053
1614
|
controller.close();
|
|
2054
1615
|
}
|
|
@@ -2062,157 +1623,13 @@ async function handleMoveStream(request) {
|
|
|
2062
1623
|
}
|
|
2063
1624
|
});
|
|
2064
1625
|
}
|
|
2065
|
-
|
|
2066
|
-
// src/handlers/images.ts
|
|
2067
|
-
import { promises as fs7 } from "fs";
|
|
2068
|
-
import path7 from "path";
|
|
2069
|
-
import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2, DeleteObjectCommand as DeleteObjectCommand2 } from "@aws-sdk/client-s3";
|
|
2070
|
-
var cancelledOperations = /* @__PURE__ */ new Set();
|
|
2071
|
-
function cancelOperation(operationId) {
|
|
2072
|
-
cancelledOperations.add(operationId);
|
|
2073
|
-
setTimeout(() => cancelledOperations.delete(operationId), 6e4);
|
|
2074
|
-
}
|
|
2075
|
-
function isOperationCancelled(operationId) {
|
|
2076
|
-
return cancelledOperations.has(operationId);
|
|
2077
|
-
}
|
|
2078
|
-
function clearCancelledOperation(operationId) {
|
|
2079
|
-
cancelledOperations.delete(operationId);
|
|
2080
|
-
}
|
|
2081
|
-
async function handleSync(request) {
|
|
1626
|
+
async function handlePushUpdatesStream(request) {
|
|
2082
1627
|
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
2083
1628
|
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
2084
1629
|
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
2085
1630
|
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
2086
|
-
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(
|
|
2087
|
-
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
2088
|
-
return jsonResponse(
|
|
2089
|
-
{ error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." },
|
|
2090
|
-
{ status: 400 }
|
|
2091
|
-
);
|
|
2092
|
-
}
|
|
2093
|
-
try {
|
|
2094
|
-
const { imageKeys } = await request.json();
|
|
2095
|
-
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
2096
|
-
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
2097
|
-
}
|
|
2098
|
-
const meta = await loadMeta();
|
|
2099
|
-
const cdnUrls = getCdnUrls(meta);
|
|
2100
|
-
const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
|
|
2101
|
-
const r2 = new S3Client2({
|
|
2102
|
-
region: "auto",
|
|
2103
|
-
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
2104
|
-
credentials: { accessKeyId, secretAccessKey }
|
|
2105
|
-
});
|
|
2106
|
-
const pushed = [];
|
|
2107
|
-
const alreadyPushed = [];
|
|
2108
|
-
const errors = [];
|
|
2109
|
-
const sourceFolders = /* @__PURE__ */ new Set();
|
|
2110
|
-
for (let imageKey of imageKeys) {
|
|
2111
|
-
if (!imageKey.startsWith("/")) {
|
|
2112
|
-
imageKey = `/${imageKey}`;
|
|
2113
|
-
}
|
|
2114
|
-
const entry = getMetaEntry(meta, imageKey);
|
|
2115
|
-
if (!entry) {
|
|
2116
|
-
errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
|
|
2117
|
-
continue;
|
|
2118
|
-
}
|
|
2119
|
-
const existingCdnUrl = entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
2120
|
-
const isAlreadyInOurR2 = existingCdnUrl === publicUrl;
|
|
2121
|
-
if (isAlreadyInOurR2) {
|
|
2122
|
-
alreadyPushed.push(imageKey);
|
|
2123
|
-
continue;
|
|
2124
|
-
}
|
|
2125
|
-
const isRemote = entry.c !== void 0 && existingCdnUrl !== publicUrl;
|
|
2126
|
-
try {
|
|
2127
|
-
let originalBuffer;
|
|
2128
|
-
if (isRemote) {
|
|
2129
|
-
const remoteUrl = `${existingCdnUrl}${imageKey}`;
|
|
2130
|
-
originalBuffer = await downloadFromRemoteUrl(remoteUrl);
|
|
2131
|
-
} else {
|
|
2132
|
-
const originalLocalPath = getPublicPath(imageKey);
|
|
2133
|
-
try {
|
|
2134
|
-
originalBuffer = await fs7.readFile(originalLocalPath);
|
|
2135
|
-
} catch {
|
|
2136
|
-
errors.push(`Original file not found: ${imageKey}`);
|
|
2137
|
-
continue;
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
await r2.send(
|
|
2141
|
-
new PutObjectCommand2({
|
|
2142
|
-
Bucket: bucketName,
|
|
2143
|
-
Key: imageKey.replace(/^\//, ""),
|
|
2144
|
-
Body: originalBuffer,
|
|
2145
|
-
ContentType: getContentType(imageKey)
|
|
2146
|
-
})
|
|
2147
|
-
);
|
|
2148
|
-
if (!isRemote && isProcessed(entry)) {
|
|
2149
|
-
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
2150
|
-
const localPath = getPublicPath(thumbPath);
|
|
2151
|
-
try {
|
|
2152
|
-
const fileBuffer = await fs7.readFile(localPath);
|
|
2153
|
-
await r2.send(
|
|
2154
|
-
new PutObjectCommand2({
|
|
2155
|
-
Bucket: bucketName,
|
|
2156
|
-
Key: thumbPath.replace(/^\//, ""),
|
|
2157
|
-
Body: fileBuffer,
|
|
2158
|
-
ContentType: getContentType(thumbPath)
|
|
2159
|
-
})
|
|
2160
|
-
);
|
|
2161
|
-
} catch {
|
|
2162
|
-
}
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
entry.c = cdnIndex;
|
|
2166
|
-
if (!isRemote) {
|
|
2167
|
-
const originalLocalPath = getPublicPath(imageKey);
|
|
2168
|
-
sourceFolders.add(path7.dirname(originalLocalPath));
|
|
2169
|
-
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
2170
|
-
const localPath = getPublicPath(thumbPath);
|
|
2171
|
-
sourceFolders.add(path7.dirname(localPath));
|
|
2172
|
-
try {
|
|
2173
|
-
await fs7.unlink(localPath);
|
|
2174
|
-
} catch {
|
|
2175
|
-
}
|
|
2176
|
-
}
|
|
2177
|
-
try {
|
|
2178
|
-
await fs7.unlink(originalLocalPath);
|
|
2179
|
-
} catch {
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
pushed.push(imageKey);
|
|
2183
|
-
} catch (error) {
|
|
2184
|
-
console.error(`Failed to push ${imageKey}:`, error);
|
|
2185
|
-
errors.push(`Failed to push: ${imageKey}`);
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
await saveMeta(meta);
|
|
2189
|
-
for (const folder of sourceFolders) {
|
|
2190
|
-
await deleteEmptyFolders(folder);
|
|
2191
|
-
}
|
|
2192
|
-
return jsonResponse({
|
|
2193
|
-
success: true,
|
|
2194
|
-
pushed,
|
|
2195
|
-
alreadyPushed: alreadyPushed.length > 0 ? alreadyPushed : void 0,
|
|
2196
|
-
errors: errors.length > 0 ? errors : void 0
|
|
2197
|
-
});
|
|
2198
|
-
} catch (error) {
|
|
2199
|
-
console.error("Failed to push:", error);
|
|
2200
|
-
return jsonResponse({ error: "Failed to push to CDN" }, { status: 500 });
|
|
2201
|
-
}
|
|
2202
|
-
}
|
|
2203
|
-
async function handleUnprocessStream(request) {
|
|
2204
|
-
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1631
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "");
|
|
2205
1632
|
const encoder = new TextEncoder();
|
|
2206
|
-
let imageKeys;
|
|
2207
|
-
try {
|
|
2208
|
-
const body = await request.json();
|
|
2209
|
-
imageKeys = body.imageKeys;
|
|
2210
|
-
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
2211
|
-
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
2212
|
-
}
|
|
2213
|
-
} catch {
|
|
2214
|
-
return jsonResponse({ error: "Invalid request body" }, { status: 400 });
|
|
2215
|
-
}
|
|
2216
1633
|
const stream = new ReadableStream({
|
|
2217
1634
|
async start(controller) {
|
|
2218
1635
|
const sendEvent = (data) => {
|
|
@@ -2221,105 +1638,133 @@ async function handleUnprocessStream(request) {
|
|
|
2221
1638
|
`));
|
|
2222
1639
|
};
|
|
2223
1640
|
try {
|
|
1641
|
+
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1642
|
+
sendEvent({ type: "error", message: "R2 not configured" });
|
|
1643
|
+
controller.close();
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const { paths } = await request.json();
|
|
1647
|
+
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
1648
|
+
sendEvent({ type: "error", message: "No paths provided" });
|
|
1649
|
+
controller.close();
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const s3 = new S3Client2({
|
|
1653
|
+
region: "auto",
|
|
1654
|
+
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
1655
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
1656
|
+
});
|
|
2224
1657
|
const meta = await loadMeta();
|
|
2225
1658
|
const cdnUrls = getCdnUrls(meta);
|
|
2226
|
-
const
|
|
1659
|
+
const r2PublicUrl = publicUrl.replace(/\/$/, "");
|
|
1660
|
+
const pushed = [];
|
|
2227
1661
|
const skipped = [];
|
|
2228
1662
|
const errors = [];
|
|
2229
|
-
const total =
|
|
1663
|
+
const total = paths.length;
|
|
2230
1664
|
sendEvent({ type: "start", total });
|
|
2231
|
-
for (let i = 0; i <
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
1665
|
+
for (let i = 0; i < paths.length; i++) {
|
|
1666
|
+
const itemPath = paths[i];
|
|
1667
|
+
const key = itemPath.startsWith("public/") ? "/" + itemPath.slice(7) : itemPath;
|
|
1668
|
+
const entry = meta[key];
|
|
1669
|
+
if (!entry || entry.u !== 1) {
|
|
1670
|
+
skipped.push(key);
|
|
1671
|
+
sendEvent({
|
|
1672
|
+
type: "progress",
|
|
1673
|
+
current: i + 1,
|
|
1674
|
+
total,
|
|
1675
|
+
pushed: pushed.length,
|
|
1676
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1677
|
+
currentFile: path6.basename(key)
|
|
1678
|
+
});
|
|
1679
|
+
continue;
|
|
2235
1680
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
const
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
});
|
|
2261
|
-
continue;
|
|
1681
|
+
const fileCdnUrl = entry.c !== void 0 ? cdnUrls[entry.c]?.replace(/\/$/, "") : void 0;
|
|
1682
|
+
if (!fileCdnUrl || fileCdnUrl !== r2PublicUrl) {
|
|
1683
|
+
skipped.push(key);
|
|
1684
|
+
sendEvent({
|
|
1685
|
+
type: "progress",
|
|
1686
|
+
current: i + 1,
|
|
1687
|
+
total,
|
|
1688
|
+
pushed: pushed.length,
|
|
1689
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1690
|
+
currentFile: path6.basename(key)
|
|
1691
|
+
});
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
try {
|
|
1695
|
+
const localPath = getPublicPath(key);
|
|
1696
|
+
const buffer = await fs6.readFile(localPath);
|
|
1697
|
+
const contentType = getContentType(path6.basename(key));
|
|
1698
|
+
const uploadKey = key.startsWith("/") ? key.slice(1) : key;
|
|
1699
|
+
try {
|
|
1700
|
+
await s3.send(new DeleteObjectCommand2({
|
|
1701
|
+
Bucket: bucketName,
|
|
1702
|
+
Key: uploadKey
|
|
1703
|
+
}));
|
|
1704
|
+
} catch {
|
|
2262
1705
|
}
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
1706
|
+
await s3.send(new PutObjectCommand2({
|
|
1707
|
+
Bucket: bucketName,
|
|
1708
|
+
Key: uploadKey,
|
|
1709
|
+
Body: buffer,
|
|
1710
|
+
ContentType: contentType
|
|
1711
|
+
}));
|
|
1712
|
+
if (isProcessed(entry)) {
|
|
1713
|
+
await deleteThumbnailsFromCdn(key);
|
|
1714
|
+
const processedEntry = await processImage(buffer, key);
|
|
1715
|
+
Object.assign(entry, processedEntry);
|
|
1716
|
+
await uploadToCdn(key);
|
|
1717
|
+
await deleteLocalThumbnails(key);
|
|
2269
1718
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
...entry.c !== void 0 ? { c: entry.c } : {}
|
|
2274
|
-
};
|
|
2275
|
-
removed.push(imageKey);
|
|
1719
|
+
await fs6.unlink(localPath);
|
|
1720
|
+
delete entry.u;
|
|
1721
|
+
pushed.push(key);
|
|
2276
1722
|
sendEvent({
|
|
2277
1723
|
type: "progress",
|
|
2278
1724
|
current: i + 1,
|
|
2279
1725
|
total,
|
|
2280
|
-
|
|
1726
|
+
pushed: pushed.length,
|
|
2281
1727
|
percent: Math.round((i + 1) / total * 100),
|
|
2282
|
-
|
|
1728
|
+
currentFile: path6.basename(key)
|
|
2283
1729
|
});
|
|
2284
1730
|
} catch (error) {
|
|
2285
|
-
console.error(`Failed to
|
|
2286
|
-
errors.push(
|
|
1731
|
+
console.error(`Failed to push update for ${key}:`, error);
|
|
1732
|
+
errors.push(key);
|
|
2287
1733
|
sendEvent({
|
|
2288
1734
|
type: "progress",
|
|
2289
1735
|
current: i + 1,
|
|
2290
1736
|
total,
|
|
2291
|
-
|
|
1737
|
+
pushed: pushed.length,
|
|
2292
1738
|
percent: Math.round((i + 1) / total * 100),
|
|
2293
|
-
|
|
1739
|
+
currentFile: path6.basename(key),
|
|
1740
|
+
message: `Failed: ${path6.basename(key)}`
|
|
2294
1741
|
});
|
|
2295
1742
|
}
|
|
2296
1743
|
}
|
|
2297
|
-
sendEvent({ type: "cleanup", message: "
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
try {
|
|
2302
|
-
await cleanupEmptyFoldersRecursive(imagesDir);
|
|
2303
|
-
} catch {
|
|
1744
|
+
sendEvent({ type: "cleanup", message: "Cleaning up..." });
|
|
1745
|
+
for (const itemPath of pushed) {
|
|
1746
|
+
const localPath = getPublicPath(itemPath);
|
|
1747
|
+
await deleteEmptyFolders(path6.dirname(localPath));
|
|
2304
1748
|
}
|
|
2305
|
-
|
|
1749
|
+
await saveMeta(meta);
|
|
1750
|
+
let message = `Pushed ${pushed.length} update${pushed.length !== 1 ? "s" : ""} to cloud.`;
|
|
2306
1751
|
if (skipped.length > 0) {
|
|
2307
|
-
message += ` ${skipped.length}
|
|
1752
|
+
message += ` ${skipped.length} file${skipped.length !== 1 ? "s" : ""} skipped.`;
|
|
2308
1753
|
}
|
|
2309
1754
|
if (errors.length > 0) {
|
|
2310
|
-
message += ` ${errors.length}
|
|
1755
|
+
message += ` ${errors.length} file${errors.length !== 1 ? "s" : ""} failed.`;
|
|
2311
1756
|
}
|
|
2312
1757
|
sendEvent({
|
|
2313
1758
|
type: "complete",
|
|
2314
|
-
|
|
1759
|
+
pushed: pushed.length,
|
|
2315
1760
|
skipped: skipped.length,
|
|
2316
1761
|
errors: errors.length,
|
|
2317
1762
|
message
|
|
2318
1763
|
});
|
|
2319
|
-
controller.close();
|
|
2320
1764
|
} catch (error) {
|
|
2321
|
-
console.error("
|
|
2322
|
-
sendEvent({ type: "error", message: "Failed to
|
|
1765
|
+
console.error("Push updates error:", error);
|
|
1766
|
+
sendEvent({ type: "error", message: "Failed to push updates" });
|
|
1767
|
+
} finally {
|
|
2323
1768
|
controller.close();
|
|
2324
1769
|
}
|
|
2325
1770
|
}
|
|
@@ -2328,283 +1773,602 @@ async function handleUnprocessStream(request) {
|
|
|
2328
1773
|
headers: {
|
|
2329
1774
|
"Content-Type": "text/event-stream",
|
|
2330
1775
|
"Cache-Control": "no-cache",
|
|
2331
|
-
Connection: "keep-alive"
|
|
1776
|
+
"Connection": "keep-alive"
|
|
2332
1777
|
}
|
|
2333
1778
|
});
|
|
2334
1779
|
}
|
|
2335
|
-
async function
|
|
2336
|
-
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
2337
|
-
const encoder = new TextEncoder();
|
|
2338
|
-
let imageKeys;
|
|
1780
|
+
async function handleCancelStreamOperation(request) {
|
|
2339
1781
|
try {
|
|
2340
|
-
const
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1782
|
+
const { operationId } = await request.json();
|
|
1783
|
+
if (!operationId || typeof operationId !== "string") {
|
|
1784
|
+
return jsonResponse({ error: "No operation ID provided" }, { status: 400 });
|
|
2344
1785
|
}
|
|
2345
|
-
|
|
2346
|
-
return jsonResponse({
|
|
1786
|
+
cancelOperation(operationId);
|
|
1787
|
+
return jsonResponse({ success: true, operationId });
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
console.error("Failed to cancel operation:", error);
|
|
1790
|
+
return jsonResponse({ error: "Failed to cancel operation" }, { status: 500 });
|
|
2347
1791
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
1792
|
+
}
|
|
1793
|
+
async function handleCancelUpdates(request) {
|
|
1794
|
+
try {
|
|
1795
|
+
const { paths } = await request.json();
|
|
1796
|
+
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
1797
|
+
return jsonResponse({ error: "No paths provided" }, { status: 400 });
|
|
1798
|
+
}
|
|
1799
|
+
const meta = await loadMeta();
|
|
1800
|
+
const cancelled = [];
|
|
1801
|
+
const skipped = [];
|
|
1802
|
+
const errors = [];
|
|
1803
|
+
const foldersToClean = /* @__PURE__ */ new Set();
|
|
1804
|
+
for (const itemPath of paths) {
|
|
1805
|
+
const key = itemPath.startsWith("public/") ? "/" + itemPath.slice(7) : itemPath;
|
|
1806
|
+
const entry = meta[key];
|
|
1807
|
+
if (!entry || entry.u !== 1) {
|
|
1808
|
+
skipped.push(key);
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
try {
|
|
1812
|
+
const localPath = getPublicPath(key);
|
|
1813
|
+
await fs6.unlink(localPath);
|
|
1814
|
+
foldersToClean.add(path6.dirname(localPath));
|
|
1815
|
+
delete entry.u;
|
|
1816
|
+
cancelled.push(key);
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
console.error(`Failed to cancel update for ${key}:`, error);
|
|
1819
|
+
errors.push(key);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
for (const folder of foldersToClean) {
|
|
1823
|
+
await deleteEmptyFolders(folder);
|
|
1824
|
+
}
|
|
1825
|
+
await saveMeta(meta);
|
|
1826
|
+
return jsonResponse({
|
|
1827
|
+
success: true,
|
|
1828
|
+
cancelled: cancelled.length,
|
|
1829
|
+
skipped: skipped.length,
|
|
1830
|
+
errors: errors.length
|
|
1831
|
+
});
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
console.error("Cancel updates error:", error);
|
|
1834
|
+
return jsonResponse({ error: "Failed to cancel updates" }, { status: 500 });
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
2352
1837
|
|
|
2353
|
-
|
|
2354
|
-
|
|
1838
|
+
// src/handlers/files.ts
|
|
1839
|
+
async function handleUpload(request) {
|
|
1840
|
+
try {
|
|
1841
|
+
const formData = await request.formData();
|
|
1842
|
+
const file = formData.get("file");
|
|
1843
|
+
const targetPath = formData.get("path") || "public";
|
|
1844
|
+
if (!file) {
|
|
1845
|
+
return jsonResponse({ error: "No file provided" }, { status: 400 });
|
|
1846
|
+
}
|
|
1847
|
+
const bytes = await file.arrayBuffer();
|
|
1848
|
+
const buffer = Buffer.from(bytes);
|
|
1849
|
+
const fileName = slugifyFilename(file.name);
|
|
1850
|
+
const ext = path7.extname(fileName).toLowerCase();
|
|
1851
|
+
const isImage = isImageFile(fileName);
|
|
1852
|
+
const isMedia = isMediaFile(fileName);
|
|
1853
|
+
const meta = await loadMeta();
|
|
1854
|
+
let relativeDir = "";
|
|
1855
|
+
if (targetPath === "public") {
|
|
1856
|
+
relativeDir = "";
|
|
1857
|
+
} else if (targetPath.startsWith("public/")) {
|
|
1858
|
+
relativeDir = targetPath.replace("public/", "");
|
|
1859
|
+
}
|
|
1860
|
+
if (relativeDir === "images" || relativeDir.startsWith("images/")) {
|
|
1861
|
+
return jsonResponse(
|
|
1862
|
+
{ error: "Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically." },
|
|
1863
|
+
{ status: 400 }
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
let imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
|
|
1867
|
+
if (meta[imageKey]) {
|
|
1868
|
+
const baseName = path7.basename(fileName, ext);
|
|
1869
|
+
let counter = 1;
|
|
1870
|
+
let newFileName = `${baseName}-${counter}${ext}`;
|
|
1871
|
+
let newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
|
|
1872
|
+
while (meta[newKey]) {
|
|
1873
|
+
counter++;
|
|
1874
|
+
newFileName = `${baseName}-${counter}${ext}`;
|
|
1875
|
+
newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
|
|
1876
|
+
}
|
|
1877
|
+
imageKey = newKey;
|
|
1878
|
+
}
|
|
1879
|
+
const actualFileName = path7.basename(imageKey);
|
|
1880
|
+
const uploadDir = getPublicPath(relativeDir);
|
|
1881
|
+
await fs7.mkdir(uploadDir, { recursive: true });
|
|
1882
|
+
await fs7.writeFile(path7.join(uploadDir, actualFileName), buffer);
|
|
1883
|
+
if (!isMedia) {
|
|
1884
|
+
return jsonResponse({
|
|
1885
|
+
success: true,
|
|
1886
|
+
message: "File uploaded (not a media file)",
|
|
1887
|
+
path: `public/${relativeDir ? relativeDir + "/" : ""}${actualFileName}`
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
if (isImage && ext !== ".svg") {
|
|
2355
1891
|
try {
|
|
2356
|
-
const
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
1892
|
+
const metadata = await sharp2(buffer).metadata();
|
|
1893
|
+
meta[imageKey] = {
|
|
1894
|
+
o: { w: metadata.width || 0, h: metadata.height || 0 }
|
|
1895
|
+
};
|
|
1896
|
+
} catch {
|
|
1897
|
+
meta[imageKey] = { o: { w: 0, h: 0 } };
|
|
1898
|
+
}
|
|
1899
|
+
} else {
|
|
1900
|
+
meta[imageKey] = {};
|
|
1901
|
+
}
|
|
1902
|
+
await saveMeta(meta);
|
|
1903
|
+
return jsonResponse({
|
|
1904
|
+
success: true,
|
|
1905
|
+
imageKey,
|
|
1906
|
+
message: 'File uploaded. Run "Process Images" to generate thumbnails.'
|
|
1907
|
+
});
|
|
1908
|
+
} catch (error) {
|
|
1909
|
+
console.error("Failed to upload:", error);
|
|
1910
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1911
|
+
return jsonResponse({ error: `Failed to upload file: ${message}` }, { status: 500 });
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
async function handleDelete(request) {
|
|
1915
|
+
try {
|
|
1916
|
+
const { paths } = await request.json();
|
|
1917
|
+
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
1918
|
+
return jsonResponse({ error: "No paths provided" }, { status: 400 });
|
|
1919
|
+
}
|
|
1920
|
+
const meta = await loadMeta();
|
|
1921
|
+
const deleted = [];
|
|
1922
|
+
const errors = [];
|
|
1923
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
1924
|
+
for (const itemPath of paths) {
|
|
1925
|
+
try {
|
|
1926
|
+
if (!itemPath.startsWith("public/")) {
|
|
1927
|
+
errors.push(`Invalid path: ${itemPath}`);
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
const absolutePath = getWorkspacePath(itemPath);
|
|
1931
|
+
const imageKey = "/" + itemPath.replace(/^public\//, "");
|
|
1932
|
+
sourceFolders.add(path7.dirname(absolutePath));
|
|
1933
|
+
const entry = meta[imageKey];
|
|
1934
|
+
const isPushedToCloud = entry?.c !== void 0;
|
|
1935
|
+
const hasThumbnails = entry ? isProcessed(entry) : false;
|
|
1936
|
+
try {
|
|
1937
|
+
const stats = await fs7.stat(absolutePath);
|
|
1938
|
+
if (stats.isDirectory()) {
|
|
1939
|
+
await fs7.rm(absolutePath, { recursive: true });
|
|
1940
|
+
const prefix = imageKey + "/";
|
|
1941
|
+
for (const key of Object.keys(meta)) {
|
|
1942
|
+
if (key.startsWith(prefix) || key === imageKey) {
|
|
1943
|
+
const keyEntry = meta[key];
|
|
1944
|
+
const keyHasThumbnails = keyEntry ? isProcessed(keyEntry) : false;
|
|
1945
|
+
if (keyEntry?.c !== void 0) {
|
|
1946
|
+
try {
|
|
1947
|
+
await deleteFromCdn(key, keyHasThumbnails);
|
|
1948
|
+
} catch {
|
|
1949
|
+
}
|
|
1950
|
+
} else {
|
|
1951
|
+
for (const thumbPath of getAllThumbnailPaths(key)) {
|
|
1952
|
+
const absoluteThumbPath = getPublicPath(thumbPath);
|
|
1953
|
+
try {
|
|
1954
|
+
await fs7.unlink(absoluteThumbPath);
|
|
1955
|
+
} catch {
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
delete meta[key];
|
|
2391
1960
|
}
|
|
2392
1961
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
await fs7.mkdir(imagesPath, { recursive: true });
|
|
2399
|
-
const fileName = path7.basename(imageKey);
|
|
2400
|
-
const destPath = path7.join(imagesPath, fileName);
|
|
2401
|
-
await fs7.writeFile(destPath, buffer);
|
|
2402
|
-
meta[imageKey] = {
|
|
2403
|
-
...entry,
|
|
2404
|
-
o: { w: 0, h: 0 },
|
|
2405
|
-
b: "",
|
|
2406
|
-
f: { w: 0, h: 0 }
|
|
2407
|
-
};
|
|
2408
|
-
if (isRemote) {
|
|
2409
|
-
delete meta[imageKey].c;
|
|
2410
|
-
}
|
|
2411
|
-
} else {
|
|
2412
|
-
const updatedEntry = await processImage(buffer, imageKey);
|
|
2413
|
-
if (isInOurR2) {
|
|
2414
|
-
updatedEntry.c = existingCdnIndex;
|
|
2415
|
-
await deleteOriginalFromCdn(imageKey);
|
|
2416
|
-
await deleteThumbnailsFromCdn(imageKey);
|
|
2417
|
-
await uploadOriginalToCdn(imageKey);
|
|
2418
|
-
await uploadToCdn(imageKey);
|
|
2419
|
-
await deleteLocalThumbnails(imageKey);
|
|
1962
|
+
} else {
|
|
1963
|
+
await fs7.unlink(absolutePath);
|
|
1964
|
+
const isInImagesFolder = itemPath.startsWith("public/images/");
|
|
1965
|
+
if (!isInImagesFolder && entry) {
|
|
1966
|
+
if (isPushedToCloud) {
|
|
2420
1967
|
try {
|
|
2421
|
-
await
|
|
1968
|
+
await deleteFromCdn(imageKey, hasThumbnails);
|
|
2422
1969
|
} catch {
|
|
2423
1970
|
}
|
|
1971
|
+
} else {
|
|
1972
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1973
|
+
const absoluteThumbPath = getPublicPath(thumbPath);
|
|
1974
|
+
try {
|
|
1975
|
+
await fs7.unlink(absoluteThumbPath);
|
|
1976
|
+
} catch {
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
2424
1979
|
}
|
|
2425
|
-
meta[imageKey]
|
|
1980
|
+
delete meta[imageKey];
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
} catch {
|
|
1984
|
+
if (entry) {
|
|
1985
|
+
if (isPushedToCloud) {
|
|
1986
|
+
try {
|
|
1987
|
+
await deleteFromCdn(imageKey, hasThumbnails);
|
|
1988
|
+
} catch {
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
delete meta[imageKey];
|
|
1992
|
+
} else {
|
|
1993
|
+
const prefix = imageKey + "/";
|
|
1994
|
+
let foundAny = false;
|
|
1995
|
+
for (const key of Object.keys(meta)) {
|
|
1996
|
+
if (key.startsWith(prefix)) {
|
|
1997
|
+
const keyEntry = meta[key];
|
|
1998
|
+
const keyHasThumbnails = keyEntry ? isProcessed(keyEntry) : false;
|
|
1999
|
+
if (keyEntry?.c !== void 0) {
|
|
2000
|
+
try {
|
|
2001
|
+
await deleteFromCdn(key, keyHasThumbnails);
|
|
2002
|
+
} catch {
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
delete meta[key];
|
|
2006
|
+
foundAny = true;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
if (!foundAny) {
|
|
2010
|
+
errors.push(`Not found: ${itemPath}`);
|
|
2011
|
+
continue;
|
|
2426
2012
|
}
|
|
2427
|
-
processed.push(imageKey);
|
|
2428
|
-
sendEvent({
|
|
2429
|
-
type: "progress",
|
|
2430
|
-
current: i + 1,
|
|
2431
|
-
total,
|
|
2432
|
-
processed: processed.length,
|
|
2433
|
-
percent: Math.round((i + 1) / total * 100),
|
|
2434
|
-
message: `Processed ${imageKey.slice(1)}`
|
|
2435
|
-
});
|
|
2436
|
-
} catch (error) {
|
|
2437
|
-
console.error(`Failed to reprocess ${imageKey}:`, error);
|
|
2438
|
-
errors.push(imageKey);
|
|
2439
|
-
sendEvent({
|
|
2440
|
-
type: "progress",
|
|
2441
|
-
current: i + 1,
|
|
2442
|
-
total,
|
|
2443
|
-
processed: processed.length,
|
|
2444
|
-
percent: Math.round((i + 1) / total * 100),
|
|
2445
|
-
message: `Failed: ${imageKey.slice(1)}`
|
|
2446
|
-
});
|
|
2447
2013
|
}
|
|
2448
2014
|
}
|
|
2449
|
-
|
|
2450
|
-
await saveMeta(meta);
|
|
2451
|
-
let message = `Generated thumbnails for ${processed.length} image${processed.length !== 1 ? "s" : ""}.`;
|
|
2452
|
-
if (errors.length > 0) {
|
|
2453
|
-
message += ` ${errors.length} image${errors.length !== 1 ? "s" : ""} failed.`;
|
|
2454
|
-
}
|
|
2455
|
-
sendEvent({
|
|
2456
|
-
type: "complete",
|
|
2457
|
-
processed: processed.length,
|
|
2458
|
-
errors: errors.length,
|
|
2459
|
-
message
|
|
2460
|
-
});
|
|
2461
|
-
controller.close();
|
|
2015
|
+
deleted.push(itemPath);
|
|
2462
2016
|
} catch (error) {
|
|
2463
|
-
console.error(
|
|
2464
|
-
|
|
2465
|
-
controller.close();
|
|
2017
|
+
console.error(`Failed to delete ${itemPath}:`, error);
|
|
2018
|
+
errors.push(itemPath);
|
|
2466
2019
|
}
|
|
2467
2020
|
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
"Content-Type": "text/event-stream",
|
|
2472
|
-
"Cache-Control": "no-cache",
|
|
2473
|
-
Connection: "keep-alive"
|
|
2021
|
+
await saveMeta(meta);
|
|
2022
|
+
for (const folder of sourceFolders) {
|
|
2023
|
+
await deleteEmptyFolders(folder);
|
|
2474
2024
|
}
|
|
2475
|
-
|
|
2025
|
+
return jsonResponse({
|
|
2026
|
+
success: true,
|
|
2027
|
+
deleted,
|
|
2028
|
+
errors: errors.length > 0 ? errors : void 0
|
|
2029
|
+
});
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
console.error("Failed to delete:", error);
|
|
2032
|
+
return jsonResponse({ error: "Failed to delete files" }, { status: 500 });
|
|
2033
|
+
}
|
|
2476
2034
|
}
|
|
2477
|
-
async function
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2035
|
+
async function handleCreateFolder(request) {
|
|
2036
|
+
try {
|
|
2037
|
+
const { parentPath, name } = await request.json();
|
|
2038
|
+
if (!name || typeof name !== "string") {
|
|
2039
|
+
return jsonResponse({ error: "Folder name is required" }, { status: 400 });
|
|
2040
|
+
}
|
|
2041
|
+
const sanitizedName = slugifyFolderName(name);
|
|
2042
|
+
if (!sanitizedName) {
|
|
2043
|
+
return jsonResponse({ error: "Invalid folder name" }, { status: 400 });
|
|
2044
|
+
}
|
|
2045
|
+
const safePath = (parentPath || "public").replace(/\.\./g, "");
|
|
2046
|
+
const folderPath = getWorkspacePath(safePath, sanitizedName);
|
|
2047
|
+
if (!folderPath.startsWith(getPublicPath())) {
|
|
2048
|
+
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
2049
|
+
}
|
|
2050
|
+
try {
|
|
2051
|
+
await fs7.access(folderPath);
|
|
2052
|
+
return jsonResponse({ error: "A folder with this name already exists" }, { status: 400 });
|
|
2053
|
+
} catch {
|
|
2054
|
+
}
|
|
2055
|
+
await fs7.mkdir(folderPath, { recursive: true });
|
|
2056
|
+
return jsonResponse({ success: true, path: path7.join(safePath, sanitizedName) });
|
|
2057
|
+
} catch (error) {
|
|
2058
|
+
console.error("Failed to create folder:", error);
|
|
2059
|
+
return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
async function handleRename(request) {
|
|
2063
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "");
|
|
2064
|
+
try {
|
|
2065
|
+
const { oldPath, newName } = await request.json();
|
|
2066
|
+
if (!oldPath || !newName) {
|
|
2067
|
+
return jsonResponse({ error: "Path and new name are required" }, { status: 400 });
|
|
2068
|
+
}
|
|
2069
|
+
const safePath = oldPath.replace(/\.\./g, "");
|
|
2070
|
+
const absoluteOldPath = getWorkspacePath(safePath);
|
|
2071
|
+
if (!absoluteOldPath.startsWith(getPublicPath())) {
|
|
2072
|
+
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
2073
|
+
}
|
|
2074
|
+
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
2075
|
+
const oldKey = "/" + oldRelativePath;
|
|
2076
|
+
const isImage = isImageFile(path7.basename(oldPath));
|
|
2077
|
+
const meta = await loadMeta();
|
|
2078
|
+
const cdnUrls = getCdnUrls(meta);
|
|
2079
|
+
const entry = meta[oldKey];
|
|
2080
|
+
const isInCloud = entry?.c !== void 0;
|
|
2081
|
+
const fileCdnUrl = isInCloud && entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
2082
|
+
const isInOurR2 = isInCloud && fileCdnUrl === publicUrl;
|
|
2083
|
+
const hasThumbnails = entry ? isProcessed(entry) : false;
|
|
2084
|
+
let hasLocalFile = false;
|
|
2085
|
+
let isFile = true;
|
|
2086
|
+
try {
|
|
2087
|
+
const stats = await fs7.stat(absoluteOldPath);
|
|
2088
|
+
hasLocalFile = true;
|
|
2089
|
+
isFile = stats.isFile();
|
|
2090
|
+
} catch {
|
|
2091
|
+
if (!isInCloud) {
|
|
2092
|
+
return jsonResponse({ error: "File or folder not found" }, { status: 404 });
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
const sanitizedName = isFile ? slugifyFilename(newName) : slugifyFolderName(newName);
|
|
2096
|
+
if (!sanitizedName) {
|
|
2097
|
+
return jsonResponse({ error: "Invalid name" }, { status: 400 });
|
|
2098
|
+
}
|
|
2099
|
+
const parentDir = path7.dirname(absoluteOldPath);
|
|
2100
|
+
const absoluteNewPath = path7.join(parentDir, sanitizedName);
|
|
2101
|
+
const newRelativePath = path7.join(path7.dirname(oldRelativePath), sanitizedName);
|
|
2102
|
+
const newKey = "/" + newRelativePath;
|
|
2103
|
+
if (meta[newKey]) {
|
|
2104
|
+
return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
|
|
2105
|
+
}
|
|
2106
|
+
try {
|
|
2107
|
+
await fs7.access(absoluteNewPath);
|
|
2108
|
+
return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
|
|
2109
|
+
} catch {
|
|
2110
|
+
}
|
|
2111
|
+
if (isInOurR2 && !hasLocalFile && isImage) {
|
|
2112
|
+
await moveInCdn(oldKey, newKey, hasThumbnails);
|
|
2113
|
+
delete meta[oldKey];
|
|
2114
|
+
meta[newKey] = entry;
|
|
2115
|
+
await saveMeta(meta);
|
|
2116
|
+
const newPath2 = path7.join(path7.dirname(safePath), sanitizedName);
|
|
2117
|
+
return jsonResponse({ success: true, newPath: newPath2 });
|
|
2118
|
+
}
|
|
2119
|
+
if (hasLocalFile) {
|
|
2120
|
+
await fs7.rename(absoluteOldPath, absoluteNewPath);
|
|
2121
|
+
}
|
|
2122
|
+
if (isImage && entry) {
|
|
2123
|
+
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
2124
|
+
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
2125
|
+
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
2126
|
+
const oldThumbPath = getPublicPath(oldThumbPaths[i]);
|
|
2127
|
+
const newThumbPath = getPublicPath(newThumbPaths[i]);
|
|
2128
|
+
await fs7.mkdir(path7.dirname(newThumbPath), { recursive: true });
|
|
2129
|
+
try {
|
|
2130
|
+
await fs7.rename(oldThumbPath, newThumbPath);
|
|
2131
|
+
} catch {
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
if (isInOurR2) {
|
|
2135
|
+
await moveInCdn(oldKey, newKey, hasThumbnails);
|
|
2136
|
+
try {
|
|
2137
|
+
await fs7.unlink(absoluteNewPath);
|
|
2138
|
+
} catch {
|
|
2139
|
+
}
|
|
2140
|
+
await deleteLocalThumbnails(newKey);
|
|
2141
|
+
}
|
|
2142
|
+
delete meta[oldKey];
|
|
2143
|
+
meta[newKey] = entry;
|
|
2144
|
+
await saveMeta(meta);
|
|
2145
|
+
}
|
|
2146
|
+
const newPath = path7.join(path7.dirname(safePath), sanitizedName);
|
|
2147
|
+
return jsonResponse({ success: true, newPath });
|
|
2148
|
+
} catch (error) {
|
|
2149
|
+
console.error("Failed to rename:", error);
|
|
2150
|
+
return jsonResponse({ error: "Failed to rename" }, { status: 500 });
|
|
2481
2151
|
}
|
|
2152
|
+
}
|
|
2153
|
+
async function handleRenameStream(request) {
|
|
2154
|
+
const encoder = new TextEncoder();
|
|
2155
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "");
|
|
2482
2156
|
const stream = new ReadableStream({
|
|
2483
2157
|
async start(controller) {
|
|
2484
|
-
const encoder = new TextEncoder();
|
|
2485
2158
|
const sendEvent = (data) => {
|
|
2486
|
-
|
|
2487
|
-
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
2159
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
2488
2160
|
|
|
2489
2161
|
`));
|
|
2490
|
-
} catch {
|
|
2491
|
-
}
|
|
2492
2162
|
};
|
|
2493
|
-
sendEvent({ type: "start", total: imageKeys.length });
|
|
2494
|
-
const downloaded = [];
|
|
2495
|
-
const skipped = [];
|
|
2496
|
-
const errors = [];
|
|
2497
|
-
const isCancelled = () => operationId ? isOperationCancelled(operationId) : false;
|
|
2498
2163
|
try {
|
|
2164
|
+
const { oldPath, newName, operationId } = await request.json();
|
|
2165
|
+
if (!oldPath || !newName) {
|
|
2166
|
+
sendEvent({ type: "error", message: "Path and new name are required" });
|
|
2167
|
+
controller.close();
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
const isCancelled = () => operationId ? isOperationCancelled(operationId) : false;
|
|
2171
|
+
const safePath = oldPath.replace(/\.\./g, "");
|
|
2172
|
+
const absoluteOldPath = getWorkspacePath(safePath);
|
|
2173
|
+
if (!absoluteOldPath.startsWith(getPublicPath())) {
|
|
2174
|
+
sendEvent({ type: "error", message: "Invalid path" });
|
|
2175
|
+
controller.close();
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
2179
|
+
const isImagePath = isImageFile(path7.basename(oldPath));
|
|
2180
|
+
let hasLocalItem = false;
|
|
2181
|
+
let isFile = true;
|
|
2182
|
+
let isVirtualFolder = false;
|
|
2183
|
+
try {
|
|
2184
|
+
const stats = await fs7.stat(absoluteOldPath);
|
|
2185
|
+
hasLocalItem = true;
|
|
2186
|
+
isFile = stats.isFile();
|
|
2187
|
+
} catch {
|
|
2188
|
+
const meta2 = await loadMeta();
|
|
2189
|
+
const oldKey2 = "/" + oldRelativePath;
|
|
2190
|
+
const entry2 = meta2[oldKey2];
|
|
2191
|
+
if (entry2) {
|
|
2192
|
+
isFile = true;
|
|
2193
|
+
} else {
|
|
2194
|
+
const folderPrefix = oldKey2 + "/";
|
|
2195
|
+
const hasChildrenInMeta = Object.keys(meta2).some((key) => key.startsWith(folderPrefix));
|
|
2196
|
+
if (hasChildrenInMeta) {
|
|
2197
|
+
isFile = false;
|
|
2198
|
+
isVirtualFolder = true;
|
|
2199
|
+
} else {
|
|
2200
|
+
sendEvent({ type: "error", message: "File or folder not found" });
|
|
2201
|
+
controller.close();
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
const sanitizedName = isFile ? slugifyFilename(newName) : slugifyFolderName(newName);
|
|
2207
|
+
if (!sanitizedName) {
|
|
2208
|
+
sendEvent({ type: "error", message: "Invalid name" });
|
|
2209
|
+
controller.close();
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
const parentDir = path7.dirname(absoluteOldPath);
|
|
2213
|
+
const absoluteNewPath = path7.join(parentDir, sanitizedName);
|
|
2214
|
+
const newRelativePath = path7.join(path7.dirname(oldRelativePath), sanitizedName);
|
|
2215
|
+
const newPath = path7.join(path7.dirname(safePath), sanitizedName);
|
|
2499
2216
|
const meta = await loadMeta();
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
sendEvent({
|
|
2505
|
-
type: "stopped",
|
|
2506
|
-
downloaded: downloaded.length,
|
|
2507
|
-
message: `Stopped. ${downloaded.length} image${downloaded.length !== 1 ? "s" : ""} downloaded.`
|
|
2508
|
-
});
|
|
2217
|
+
const cdnUrls = getCdnUrls(meta);
|
|
2218
|
+
if (isFile) {
|
|
2219
|
+
const newKey2 = "/" + newRelativePath;
|
|
2220
|
+
if (meta[newKey2]) {
|
|
2221
|
+
sendEvent({ type: "error", message: "An item with this name already exists" });
|
|
2509
2222
|
controller.close();
|
|
2510
2223
|
return;
|
|
2511
2224
|
}
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
if (!entry || entry.c === void 0) {
|
|
2515
|
-
skipped.push(imageKey);
|
|
2516
|
-
sendEvent({
|
|
2517
|
-
type: "progress",
|
|
2518
|
-
current: i + 1,
|
|
2519
|
-
total: imageKeys.length,
|
|
2520
|
-
downloaded: downloaded.length,
|
|
2521
|
-
message: `Skipped ${imageKey} (not on cloud)`
|
|
2522
|
-
});
|
|
2523
|
-
continue;
|
|
2524
|
-
}
|
|
2225
|
+
}
|
|
2226
|
+
if (!isVirtualFolder) {
|
|
2525
2227
|
try {
|
|
2526
|
-
|
|
2228
|
+
await fs7.access(absoluteNewPath);
|
|
2229
|
+
sendEvent({ type: "error", message: "An item with this name already exists" });
|
|
2230
|
+
controller.close();
|
|
2231
|
+
return;
|
|
2232
|
+
} catch {
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
if (isVirtualFolder) {
|
|
2236
|
+
const newPrefix = "/" + newRelativePath + "/";
|
|
2237
|
+
const hasConflict = Object.keys(meta).some((key) => key.startsWith(newPrefix));
|
|
2238
|
+
if (hasConflict) {
|
|
2239
|
+
sendEvent({ type: "error", message: "A folder with this name already exists" });
|
|
2240
|
+
controller.close();
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (!isFile) {
|
|
2245
|
+
const oldPrefix = "/" + oldRelativePath + "/";
|
|
2246
|
+
const newPrefix = "/" + newRelativePath + "/";
|
|
2247
|
+
const itemsToUpdate = [];
|
|
2248
|
+
for (const [key, entry2] of Object.entries(meta)) {
|
|
2249
|
+
if (key.startsWith(oldPrefix) && entry2 && typeof entry2 === "object") {
|
|
2250
|
+
const newKey2 = key.replace(oldPrefix, newPrefix);
|
|
2251
|
+
itemsToUpdate.push({ oldKey: key, newKey: newKey2, entry: entry2 });
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
const total = itemsToUpdate.length + 1;
|
|
2255
|
+
sendEvent({ type: "start", total, message: `Renaming folder with ${itemsToUpdate.length} item(s)...` });
|
|
2256
|
+
if (hasLocalItem) {
|
|
2257
|
+
await fs7.rename(absoluteOldPath, absoluteNewPath);
|
|
2258
|
+
const imagesDir = getPublicPath("/images");
|
|
2259
|
+
const oldThumbFolder2 = path7.join(imagesDir, oldRelativePath);
|
|
2260
|
+
const newThumbFolder = path7.join(imagesDir, newRelativePath);
|
|
2261
|
+
try {
|
|
2262
|
+
await fs7.access(oldThumbFolder2);
|
|
2263
|
+
await fs7.mkdir(path7.dirname(newThumbFolder), { recursive: true });
|
|
2264
|
+
await fs7.rename(oldThumbFolder2, newThumbFolder);
|
|
2265
|
+
} catch {
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
sendEvent({ type: "progress", current: 1, total, renamed: 1, message: "Renamed folder" });
|
|
2269
|
+
let renamed = 1;
|
|
2270
|
+
for (const item of itemsToUpdate) {
|
|
2527
2271
|
if (isCancelled()) {
|
|
2528
2272
|
await saveMeta(meta);
|
|
2529
|
-
|
|
2530
|
-
sendEvent({
|
|
2531
|
-
type: "stopped",
|
|
2532
|
-
downloaded: downloaded.length,
|
|
2533
|
-
message: `Stopped. ${downloaded.length} image${downloaded.length !== 1 ? "s" : ""} downloaded.`
|
|
2534
|
-
});
|
|
2273
|
+
sendEvent({ type: "complete", renamed, newPath, cancelled: true });
|
|
2535
2274
|
controller.close();
|
|
2536
2275
|
return;
|
|
2537
2276
|
}
|
|
2538
|
-
const
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
const
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2277
|
+
const { oldKey: oldKey2, newKey: newKey2, entry: entry2 } = item;
|
|
2278
|
+
const isInCloud2 = entry2.c !== void 0;
|
|
2279
|
+
const fileCdnUrl2 = isInCloud2 ? cdnUrls[entry2.c] : void 0;
|
|
2280
|
+
const isInOurR22 = isInCloud2 && fileCdnUrl2 === publicUrl;
|
|
2281
|
+
const hasThumbnails2 = isProcessed(entry2);
|
|
2282
|
+
if (isInOurR22) {
|
|
2283
|
+
try {
|
|
2284
|
+
await moveInCdn(oldKey2, newKey2, hasThumbnails2);
|
|
2285
|
+
const localFilePath = getPublicPath(newKey2);
|
|
2286
|
+
try {
|
|
2287
|
+
await fs7.unlink(localFilePath);
|
|
2288
|
+
} catch {
|
|
2289
|
+
}
|
|
2290
|
+
if (hasThumbnails2) {
|
|
2291
|
+
await deleteLocalThumbnails(newKey2);
|
|
2292
|
+
}
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
console.error(`Failed to rename in CDN ${oldKey2}:`, err);
|
|
2295
|
+
}
|
|
2550
2296
|
}
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
current: i + 1,
|
|
2555
|
-
total: imageKeys.length,
|
|
2556
|
-
downloaded: downloaded.length,
|
|
2557
|
-
message: `Downloaded ${imageKey}`
|
|
2558
|
-
});
|
|
2559
|
-
} catch (error) {
|
|
2560
|
-
console.error(`Failed to download ${imageKey}:`, error);
|
|
2561
|
-
errors.push(imageKey);
|
|
2297
|
+
delete meta[oldKey2];
|
|
2298
|
+
meta[newKey2] = entry2;
|
|
2299
|
+
renamed++;
|
|
2562
2300
|
sendEvent({
|
|
2563
2301
|
type: "progress",
|
|
2564
|
-
current:
|
|
2565
|
-
total
|
|
2566
|
-
|
|
2567
|
-
message: `
|
|
2302
|
+
current: renamed,
|
|
2303
|
+
total,
|
|
2304
|
+
renamed,
|
|
2305
|
+
message: `Renamed ${path7.basename(newKey2)}`
|
|
2568
2306
|
});
|
|
2569
2307
|
}
|
|
2308
|
+
await saveMeta(meta);
|
|
2309
|
+
await deleteEmptyFolders(absoluteOldPath);
|
|
2310
|
+
const oldThumbFolder = path7.join(getPublicPath("/images"), oldRelativePath);
|
|
2311
|
+
await deleteEmptyFolders(oldThumbFolder);
|
|
2312
|
+
sendEvent({ type: "complete", renamed, newPath });
|
|
2313
|
+
controller.close();
|
|
2314
|
+
return;
|
|
2570
2315
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2316
|
+
const oldKey = "/" + oldRelativePath;
|
|
2317
|
+
const newKey = "/" + newRelativePath;
|
|
2318
|
+
const entry = meta[oldKey];
|
|
2319
|
+
const isInCloud = entry?.c !== void 0;
|
|
2320
|
+
const fileCdnUrl = isInCloud && entry?.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
2321
|
+
const isInOurR2 = isInCloud && fileCdnUrl === publicUrl;
|
|
2322
|
+
const hasThumbnails = entry ? isProcessed(entry) : false;
|
|
2323
|
+
sendEvent({ type: "start", total: 1, message: "Renaming file..." });
|
|
2324
|
+
if (isInOurR2 && !hasLocalItem && isImagePath) {
|
|
2325
|
+
await moveInCdn(oldKey, newKey, hasThumbnails);
|
|
2326
|
+
delete meta[oldKey];
|
|
2327
|
+
if (entry) meta[newKey] = entry;
|
|
2328
|
+
await saveMeta(meta);
|
|
2329
|
+
sendEvent({ type: "complete", renamed: 1, newPath });
|
|
2330
|
+
controller.close();
|
|
2331
|
+
return;
|
|
2575
2332
|
}
|
|
2576
|
-
if (
|
|
2577
|
-
|
|
2333
|
+
if (hasLocalItem) {
|
|
2334
|
+
await fs7.rename(absoluteOldPath, absoluteNewPath);
|
|
2578
2335
|
}
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2336
|
+
if (isImagePath && entry) {
|
|
2337
|
+
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
2338
|
+
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
2339
|
+
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
2340
|
+
const oldThumbPath = getPublicPath(oldThumbPaths[i]);
|
|
2341
|
+
const newThumbPath = getPublicPath(newThumbPaths[i]);
|
|
2342
|
+
await fs7.mkdir(path7.dirname(newThumbPath), { recursive: true });
|
|
2343
|
+
try {
|
|
2344
|
+
await fs7.rename(oldThumbPath, newThumbPath);
|
|
2345
|
+
} catch {
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (isInOurR2) {
|
|
2349
|
+
await moveInCdn(oldKey, newKey, hasThumbnails);
|
|
2350
|
+
try {
|
|
2351
|
+
await fs7.unlink(absoluteNewPath);
|
|
2352
|
+
} catch {
|
|
2353
|
+
}
|
|
2354
|
+
await deleteLocalThumbnails(newKey);
|
|
2355
|
+
}
|
|
2356
|
+
delete meta[oldKey];
|
|
2357
|
+
meta[newKey] = entry;
|
|
2358
|
+
await saveMeta(meta);
|
|
2359
|
+
}
|
|
2360
|
+
sendEvent({ type: "complete", renamed: 1, newPath });
|
|
2361
|
+
controller.close();
|
|
2586
2362
|
} catch (error) {
|
|
2587
|
-
console.error("
|
|
2588
|
-
sendEvent({ type: "error", message: "Failed to
|
|
2589
|
-
} finally {
|
|
2363
|
+
console.error("Rename stream error:", error);
|
|
2364
|
+
sendEvent({ type: "error", message: "Failed to rename" });
|
|
2590
2365
|
controller.close();
|
|
2591
2366
|
}
|
|
2592
2367
|
}
|
|
2593
2368
|
});
|
|
2594
|
-
return
|
|
2595
|
-
headers: {
|
|
2596
|
-
"Content-Type": "text/event-stream",
|
|
2597
|
-
"Cache-Control": "no-cache",
|
|
2598
|
-
"Connection": "keep-alive"
|
|
2599
|
-
}
|
|
2600
|
-
});
|
|
2369
|
+
return streamResponse(stream);
|
|
2601
2370
|
}
|
|
2602
|
-
async function
|
|
2603
|
-
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
2604
|
-
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
2605
|
-
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
2606
|
-
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
2607
|
-
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "");
|
|
2371
|
+
async function handleMoveStream(request) {
|
|
2608
2372
|
const encoder = new TextEncoder();
|
|
2609
2373
|
const stream = new ReadableStream({
|
|
2610
2374
|
async start(controller) {
|
|
@@ -2614,132 +2378,455 @@ async function handlePushUpdatesStream(request) {
|
|
|
2614
2378
|
`));
|
|
2615
2379
|
};
|
|
2616
2380
|
try {
|
|
2617
|
-
|
|
2618
|
-
|
|
2381
|
+
const { paths, destination, operationId } = await request.json();
|
|
2382
|
+
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
2383
|
+
sendEvent({ type: "error", message: "Paths are required" });
|
|
2619
2384
|
controller.close();
|
|
2620
2385
|
return;
|
|
2621
2386
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
sendEvent({ type: "error", message: "No paths provided" });
|
|
2387
|
+
if (!destination || typeof destination !== "string") {
|
|
2388
|
+
sendEvent({ type: "error", message: "Destination is required" });
|
|
2625
2389
|
controller.close();
|
|
2626
2390
|
return;
|
|
2627
2391
|
}
|
|
2628
|
-
const
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2392
|
+
const isCancelled = () => operationId ? isOperationCancelled(operationId) : false;
|
|
2393
|
+
const safeDestination = destination.replace(/\.\./g, "");
|
|
2394
|
+
const absoluteDestination = getWorkspacePath(safeDestination);
|
|
2395
|
+
if (!absoluteDestination.startsWith(getPublicPath())) {
|
|
2396
|
+
sendEvent({ type: "error", message: "Invalid destination" });
|
|
2397
|
+
controller.close();
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
await fs7.mkdir(absoluteDestination, { recursive: true });
|
|
2633
2401
|
const meta = await loadMeta();
|
|
2634
2402
|
const cdnUrls = getCdnUrls(meta);
|
|
2635
|
-
const r2PublicUrl =
|
|
2636
|
-
const
|
|
2637
|
-
const skipped = [];
|
|
2403
|
+
const r2PublicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "") || "";
|
|
2404
|
+
const moved = [];
|
|
2638
2405
|
const errors = [];
|
|
2639
|
-
const
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
const
|
|
2644
|
-
const
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2406
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
2407
|
+
let totalFiles = 0;
|
|
2408
|
+
const expandedItems = [];
|
|
2409
|
+
for (const itemPath of paths) {
|
|
2410
|
+
const safePath = itemPath.replace(/\.\./g, "");
|
|
2411
|
+
const itemName = path7.basename(safePath);
|
|
2412
|
+
const oldRelativePath = safePath.replace(/^public\/?/, "");
|
|
2413
|
+
const destWithoutPublic = safeDestination.replace(/^public\/?/, "");
|
|
2414
|
+
const newRelativePath = destWithoutPublic ? path7.join(destWithoutPublic, itemName) : itemName;
|
|
2415
|
+
const oldKey = "/" + oldRelativePath;
|
|
2416
|
+
const newKey = "/" + newRelativePath;
|
|
2417
|
+
const newAbsolutePath = path7.join(absoluteDestination, itemName);
|
|
2418
|
+
const absolutePath = getWorkspacePath(safePath);
|
|
2419
|
+
let hasLocalItem = false;
|
|
2420
|
+
let isDirectory = false;
|
|
2421
|
+
try {
|
|
2422
|
+
const stats = await fs7.stat(absolutePath);
|
|
2423
|
+
hasLocalItem = true;
|
|
2424
|
+
isDirectory = stats.isDirectory();
|
|
2425
|
+
} catch {
|
|
2426
|
+
}
|
|
2427
|
+
if (hasLocalItem && isDirectory) {
|
|
2428
|
+
const countFilesRecursive = async (dir) => {
|
|
2429
|
+
let count = 0;
|
|
2430
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2431
|
+
for (const entry of entries) {
|
|
2432
|
+
if (entry.isDirectory()) {
|
|
2433
|
+
count += await countFilesRecursive(path7.join(dir, entry.name));
|
|
2434
|
+
} else {
|
|
2435
|
+
count++;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
return count;
|
|
2439
|
+
};
|
|
2440
|
+
const localFileCount = await countFilesRecursive(absolutePath);
|
|
2441
|
+
const folderPrefix = oldKey + "/";
|
|
2442
|
+
let cloudOnlyCount = 0;
|
|
2443
|
+
for (const metaKey of Object.keys(meta)) {
|
|
2444
|
+
if (metaKey.startsWith(folderPrefix)) {
|
|
2445
|
+
const relPath = metaKey.slice(folderPrefix.length);
|
|
2446
|
+
const localPath = path7.join(absolutePath, relPath);
|
|
2447
|
+
try {
|
|
2448
|
+
await fs7.access(localPath);
|
|
2449
|
+
} catch {
|
|
2450
|
+
cloudOnlyCount++;
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
totalFiles += localFileCount + cloudOnlyCount;
|
|
2455
|
+
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: false });
|
|
2456
|
+
} else if (!hasLocalItem) {
|
|
2457
|
+
const folderPrefix = oldKey + "/";
|
|
2458
|
+
const virtualItems = [];
|
|
2459
|
+
for (const [key, metaEntry] of Object.entries(meta)) {
|
|
2460
|
+
if (key.startsWith(folderPrefix) && metaEntry && typeof metaEntry === "object") {
|
|
2461
|
+
const relativePath = key.slice(folderPrefix.length);
|
|
2462
|
+
const destNewKey = newKey + "/" + relativePath;
|
|
2463
|
+
virtualItems.push({ oldKey: key, newKey: destNewKey, entry: metaEntry });
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (virtualItems.length > 0) {
|
|
2467
|
+
totalFiles += virtualItems.length;
|
|
2468
|
+
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: true, virtualFolderItems: virtualItems });
|
|
2469
|
+
sourceFolders.add(absolutePath);
|
|
2470
|
+
} else {
|
|
2471
|
+
totalFiles++;
|
|
2472
|
+
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: false });
|
|
2473
|
+
}
|
|
2474
|
+
} else {
|
|
2475
|
+
totalFiles++;
|
|
2476
|
+
expandedItems.push({ itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder: false });
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
sendEvent({ type: "start", total: totalFiles });
|
|
2480
|
+
let processedFiles = 0;
|
|
2481
|
+
for (const expandedItem of expandedItems) {
|
|
2482
|
+
if (isCancelled()) {
|
|
2483
|
+
sendEvent({ type: "complete", moved: moved.length, errors: errors.length, errorMessages: errors, cancelled: true });
|
|
2484
|
+
controller.close();
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const { itemPath, safePath, itemName, oldKey, newKey, newAbsolutePath, isVirtualFolder, virtualFolderItems } = expandedItem;
|
|
2488
|
+
if (isVirtualFolder && virtualFolderItems) {
|
|
2489
|
+
for (const vItem of virtualFolderItems) {
|
|
2490
|
+
if (isCancelled()) {
|
|
2491
|
+
sendEvent({ type: "complete", moved: moved.length, errors: errors.length, errorMessages: errors, cancelled: true });
|
|
2492
|
+
controller.close();
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
const itemEntry = vItem.entry;
|
|
2496
|
+
const isItemInCloud = itemEntry.c !== void 0;
|
|
2497
|
+
const itemCdnUrl = isItemInCloud ? cdnUrls[itemEntry.c] : void 0;
|
|
2498
|
+
const isItemInR2 = isItemInCloud && itemCdnUrl === r2PublicUrl;
|
|
2499
|
+
const itemHasThumbnails = isProcessed(itemEntry);
|
|
2500
|
+
let vItemMoved = false;
|
|
2501
|
+
if (isItemInR2) {
|
|
2502
|
+
try {
|
|
2503
|
+
await moveInCdn(vItem.oldKey, vItem.newKey, itemHasThumbnails);
|
|
2504
|
+
vItemMoved = true;
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
console.error(`Failed to move cloud item ${vItem.oldKey}:`, err);
|
|
2507
|
+
delete meta[vItem.oldKey];
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
if (vItemMoved) {
|
|
2511
|
+
delete meta[vItem.oldKey];
|
|
2512
|
+
meta[vItem.newKey] = itemEntry;
|
|
2513
|
+
}
|
|
2514
|
+
processedFiles++;
|
|
2515
|
+
sendEvent({
|
|
2516
|
+
type: "progress",
|
|
2517
|
+
current: processedFiles,
|
|
2518
|
+
total: totalFiles,
|
|
2519
|
+
moved: moved.length,
|
|
2520
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2521
|
+
currentFile: path7.basename(vItem.newKey)
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
const newFolderPath = getPublicPath(newKey);
|
|
2525
|
+
await deleteEmptyFolders(newFolderPath);
|
|
2526
|
+
const newThumbFolder = path7.join(getPublicPath("images"), newKey.slice(1));
|
|
2527
|
+
await deleteEmptyFolders(newThumbFolder);
|
|
2528
|
+
moved.push(itemPath);
|
|
2655
2529
|
continue;
|
|
2656
2530
|
}
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2531
|
+
if (meta[newKey]) {
|
|
2532
|
+
errors.push(`${itemName} already exists in destination`);
|
|
2533
|
+
processedFiles++;
|
|
2660
2534
|
sendEvent({
|
|
2661
2535
|
type: "progress",
|
|
2662
|
-
current:
|
|
2663
|
-
total,
|
|
2664
|
-
|
|
2665
|
-
percent: Math.round(
|
|
2666
|
-
currentFile:
|
|
2536
|
+
current: processedFiles,
|
|
2537
|
+
total: totalFiles,
|
|
2538
|
+
moved: moved.length,
|
|
2539
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2540
|
+
currentFile: itemName
|
|
2667
2541
|
});
|
|
2668
2542
|
continue;
|
|
2669
2543
|
}
|
|
2544
|
+
const entry = meta[oldKey];
|
|
2545
|
+
const isImage = isImageFile(itemName);
|
|
2546
|
+
const isInCloud = entry?.c !== void 0;
|
|
2547
|
+
const fileCdnUrl = isInCloud && entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
2548
|
+
const isRemote = isInCloud && (!r2PublicUrl || fileCdnUrl !== r2PublicUrl);
|
|
2549
|
+
const isPushedToR2 = isInCloud && r2PublicUrl && fileCdnUrl === r2PublicUrl;
|
|
2550
|
+
const hasProcessedThumbnails = isProcessed(entry);
|
|
2670
2551
|
try {
|
|
2671
|
-
const
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
await
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2552
|
+
const sourceFolder = path7.dirname(getWorkspacePath(safePath));
|
|
2553
|
+
sourceFolders.add(sourceFolder);
|
|
2554
|
+
if (isRemote && isImage) {
|
|
2555
|
+
const remoteUrl = `${fileCdnUrl}${oldKey}`;
|
|
2556
|
+
const buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
2557
|
+
await fs7.mkdir(path7.dirname(newAbsolutePath), { recursive: true });
|
|
2558
|
+
await fs7.writeFile(newAbsolutePath, buffer);
|
|
2559
|
+
const newEntry = {
|
|
2560
|
+
o: entry?.o,
|
|
2561
|
+
b: entry?.b
|
|
2562
|
+
};
|
|
2563
|
+
delete meta[oldKey];
|
|
2564
|
+
meta[newKey] = newEntry;
|
|
2565
|
+
moved.push(itemPath);
|
|
2566
|
+
processedFiles++;
|
|
2567
|
+
sendEvent({
|
|
2568
|
+
type: "progress",
|
|
2569
|
+
current: processedFiles,
|
|
2570
|
+
total: totalFiles,
|
|
2571
|
+
moved: moved.length,
|
|
2572
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2573
|
+
currentFile: itemName
|
|
2574
|
+
});
|
|
2575
|
+
} else if (isPushedToR2 && isImage) {
|
|
2576
|
+
await moveInCdn(oldKey, newKey, hasProcessedThumbnails);
|
|
2577
|
+
delete meta[oldKey];
|
|
2578
|
+
if (entry) {
|
|
2579
|
+
meta[newKey] = entry;
|
|
2580
|
+
}
|
|
2581
|
+
moved.push(itemPath);
|
|
2582
|
+
processedFiles++;
|
|
2583
|
+
sendEvent({
|
|
2584
|
+
type: "progress",
|
|
2585
|
+
current: processedFiles,
|
|
2586
|
+
total: totalFiles,
|
|
2587
|
+
moved: moved.length,
|
|
2588
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2589
|
+
currentFile: itemName
|
|
2590
|
+
});
|
|
2591
|
+
} else {
|
|
2592
|
+
const absolutePath = getWorkspacePath(safePath);
|
|
2593
|
+
if (absoluteDestination.startsWith(absolutePath + path7.sep)) {
|
|
2594
|
+
errors.push(`Cannot move ${itemName} into itself`);
|
|
2595
|
+
processedFiles++;
|
|
2596
|
+
sendEvent({
|
|
2597
|
+
type: "progress",
|
|
2598
|
+
current: processedFiles,
|
|
2599
|
+
total: totalFiles,
|
|
2600
|
+
moved: moved.length,
|
|
2601
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2602
|
+
currentFile: itemName
|
|
2603
|
+
});
|
|
2604
|
+
continue;
|
|
2605
|
+
}
|
|
2606
|
+
try {
|
|
2607
|
+
await fs7.access(absolutePath);
|
|
2608
|
+
} catch {
|
|
2609
|
+
errors.push(`${itemName} not found`);
|
|
2610
|
+
processedFiles++;
|
|
2611
|
+
sendEvent({
|
|
2612
|
+
type: "progress",
|
|
2613
|
+
current: processedFiles,
|
|
2614
|
+
total: totalFiles,
|
|
2615
|
+
moved: moved.length,
|
|
2616
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2617
|
+
currentFile: itemName
|
|
2618
|
+
});
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
try {
|
|
2622
|
+
await fs7.access(newAbsolutePath);
|
|
2623
|
+
errors.push(`${itemName} already exists in destination`);
|
|
2624
|
+
processedFiles++;
|
|
2625
|
+
sendEvent({
|
|
2626
|
+
type: "progress",
|
|
2627
|
+
current: processedFiles,
|
|
2628
|
+
total: totalFiles,
|
|
2629
|
+
moved: moved.length,
|
|
2630
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2631
|
+
currentFile: itemName
|
|
2632
|
+
});
|
|
2633
|
+
continue;
|
|
2634
|
+
} catch {
|
|
2635
|
+
}
|
|
2636
|
+
const stats = await fs7.stat(absolutePath);
|
|
2637
|
+
if (stats.isFile()) {
|
|
2638
|
+
await fs7.mkdir(path7.dirname(newAbsolutePath), { recursive: true });
|
|
2639
|
+
await fs7.rename(absolutePath, newAbsolutePath);
|
|
2640
|
+
if (isImage && entry) {
|
|
2641
|
+
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
2642
|
+
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
2643
|
+
for (let j = 0; j < oldThumbPaths.length; j++) {
|
|
2644
|
+
const oldThumbPath = getPublicPath(oldThumbPaths[j]);
|
|
2645
|
+
const newThumbPath = getPublicPath(newThumbPaths[j]);
|
|
2646
|
+
try {
|
|
2647
|
+
await fs7.access(oldThumbPath);
|
|
2648
|
+
sourceFolders.add(path7.dirname(oldThumbPath));
|
|
2649
|
+
await fs7.mkdir(path7.dirname(newThumbPath), { recursive: true });
|
|
2650
|
+
await fs7.rename(oldThumbPath, newThumbPath);
|
|
2651
|
+
} catch {
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
const fileIsInCloud = entry.c !== void 0;
|
|
2655
|
+
const fileCdnUrl2 = fileIsInCloud ? cdnUrls[entry.c] : void 0;
|
|
2656
|
+
const fileIsInR2 = fileIsInCloud && fileCdnUrl2 === r2PublicUrl;
|
|
2657
|
+
const fileHasThumbs = isProcessed(entry);
|
|
2658
|
+
if (fileIsInR2) {
|
|
2659
|
+
await deleteFromCdn(oldKey, fileHasThumbs);
|
|
2660
|
+
await uploadOriginalToCdn(newKey);
|
|
2661
|
+
if (fileHasThumbs) {
|
|
2662
|
+
await uploadToCdn(newKey);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
delete meta[oldKey];
|
|
2666
|
+
meta[newKey] = entry;
|
|
2667
|
+
}
|
|
2668
|
+
processedFiles++;
|
|
2669
|
+
sendEvent({
|
|
2670
|
+
type: "progress",
|
|
2671
|
+
current: processedFiles,
|
|
2672
|
+
total: totalFiles,
|
|
2673
|
+
moved: moved.length,
|
|
2674
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2675
|
+
currentFile: itemName
|
|
2676
|
+
});
|
|
2677
|
+
moved.push(itemPath);
|
|
2678
|
+
} else if (stats.isDirectory()) {
|
|
2679
|
+
const oldPrefix = oldKey + "/";
|
|
2680
|
+
const newPrefix = newKey + "/";
|
|
2681
|
+
const localFiles = [];
|
|
2682
|
+
const collectLocalFiles = async (dir, relativeDir) => {
|
|
2683
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
2684
|
+
for (const dirEntry of entries) {
|
|
2685
|
+
const entryRelPath = relativeDir ? `${relativeDir}/${dirEntry.name}` : dirEntry.name;
|
|
2686
|
+
if (dirEntry.isDirectory()) {
|
|
2687
|
+
await collectLocalFiles(path7.join(dir, dirEntry.name), entryRelPath);
|
|
2688
|
+
} else {
|
|
2689
|
+
localFiles.push({ relativePath: entryRelPath, isImage: isImageFile(dirEntry.name) });
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
await collectLocalFiles(absolutePath, "");
|
|
2694
|
+
const cloudOnlyFiles = [];
|
|
2695
|
+
for (const [metaKey, metaEntry] of Object.entries(meta)) {
|
|
2696
|
+
if (metaKey.startsWith(oldPrefix) && metaEntry && typeof metaEntry === "object") {
|
|
2697
|
+
const relPath = metaKey.slice(oldPrefix.length);
|
|
2698
|
+
const localPath = path7.join(absolutePath, relPath);
|
|
2699
|
+
try {
|
|
2700
|
+
await fs7.access(localPath);
|
|
2701
|
+
} catch {
|
|
2702
|
+
cloudOnlyFiles.push({
|
|
2703
|
+
oldKey: metaKey,
|
|
2704
|
+
newKey: newPrefix + relPath,
|
|
2705
|
+
entry: metaEntry
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
for (const localFile of localFiles) {
|
|
2711
|
+
if (isCancelled()) {
|
|
2712
|
+
await saveMeta(meta);
|
|
2713
|
+
sendEvent({ type: "complete", moved: moved.length, errors: errors.length, errorMessages: errors, cancelled: true });
|
|
2714
|
+
controller.close();
|
|
2715
|
+
return;
|
|
2716
|
+
}
|
|
2717
|
+
const fileOldPath = path7.join(absolutePath, localFile.relativePath);
|
|
2718
|
+
const fileNewPath = path7.join(newAbsolutePath, localFile.relativePath);
|
|
2719
|
+
const fileOldKey = oldPrefix + localFile.relativePath;
|
|
2720
|
+
const fileNewKey = newPrefix + localFile.relativePath;
|
|
2721
|
+
const fileEntry = meta[fileOldKey];
|
|
2722
|
+
sourceFolders.add(path7.dirname(fileOldPath));
|
|
2723
|
+
await fs7.mkdir(path7.dirname(fileNewPath), { recursive: true });
|
|
2724
|
+
await fs7.rename(fileOldPath, fileNewPath);
|
|
2725
|
+
if (localFile.isImage && fileEntry) {
|
|
2726
|
+
const oldThumbPaths = getAllThumbnailPaths(fileOldKey);
|
|
2727
|
+
const newThumbPaths = getAllThumbnailPaths(fileNewKey);
|
|
2728
|
+
for (let t = 0; t < oldThumbPaths.length; t++) {
|
|
2729
|
+
const oldThumbPath = getPublicPath(oldThumbPaths[t]);
|
|
2730
|
+
const newThumbPath = getPublicPath(newThumbPaths[t]);
|
|
2731
|
+
try {
|
|
2732
|
+
await fs7.access(oldThumbPath);
|
|
2733
|
+
sourceFolders.add(path7.dirname(oldThumbPath));
|
|
2734
|
+
await fs7.mkdir(path7.dirname(newThumbPath), { recursive: true });
|
|
2735
|
+
await fs7.rename(oldThumbPath, newThumbPath);
|
|
2736
|
+
} catch {
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
const fileIsInCloud = fileEntry.c !== void 0;
|
|
2740
|
+
const fileCdnUrl2 = fileIsInCloud ? cdnUrls[fileEntry.c] : void 0;
|
|
2741
|
+
const fileIsInR2 = fileIsInCloud && fileCdnUrl2 === r2PublicUrl;
|
|
2742
|
+
const fileHasThumbs = isProcessed(fileEntry);
|
|
2743
|
+
if (fileIsInR2) {
|
|
2744
|
+
await moveInCdn(fileOldKey, fileNewKey, fileHasThumbs);
|
|
2745
|
+
}
|
|
2746
|
+
delete meta[fileOldKey];
|
|
2747
|
+
meta[fileNewKey] = fileEntry;
|
|
2748
|
+
}
|
|
2749
|
+
processedFiles++;
|
|
2750
|
+
sendEvent({
|
|
2751
|
+
type: "progress",
|
|
2752
|
+
current: processedFiles,
|
|
2753
|
+
total: totalFiles,
|
|
2754
|
+
moved: moved.length,
|
|
2755
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2756
|
+
currentFile: path7.basename(localFile.relativePath)
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
for (const cloudFile of cloudOnlyFiles) {
|
|
2760
|
+
if (isCancelled()) {
|
|
2761
|
+
await saveMeta(meta);
|
|
2762
|
+
sendEvent({ type: "complete", moved: moved.length, errors: errors.length, errorMessages: errors, cancelled: true });
|
|
2763
|
+
controller.close();
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
const cloudEntry = cloudFile.entry;
|
|
2767
|
+
const cloudIsInCloud = cloudEntry.c !== void 0;
|
|
2768
|
+
const cloudCdnUrl = cloudIsInCloud ? cdnUrls[cloudEntry.c] : void 0;
|
|
2769
|
+
const cloudIsInR2 = cloudIsInCloud && cloudCdnUrl === r2PublicUrl;
|
|
2770
|
+
const cloudHasThumbs = isProcessed(cloudEntry);
|
|
2771
|
+
let cloudFileMoved = false;
|
|
2772
|
+
if (cloudIsInR2) {
|
|
2773
|
+
try {
|
|
2774
|
+
await moveInCdn(cloudFile.oldKey, cloudFile.newKey, cloudHasThumbs);
|
|
2775
|
+
cloudFileMoved = true;
|
|
2776
|
+
} catch (err) {
|
|
2777
|
+
console.error(`Failed to move cloud file ${cloudFile.oldKey}:`, err);
|
|
2778
|
+
delete meta[cloudFile.oldKey];
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
if (cloudFileMoved) {
|
|
2782
|
+
delete meta[cloudFile.oldKey];
|
|
2783
|
+
meta[cloudFile.newKey] = cloudEntry;
|
|
2784
|
+
}
|
|
2785
|
+
processedFiles++;
|
|
2786
|
+
sendEvent({
|
|
2787
|
+
type: "progress",
|
|
2788
|
+
current: processedFiles,
|
|
2789
|
+
total: totalFiles,
|
|
2790
|
+
moved: moved.length,
|
|
2791
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2792
|
+
currentFile: path7.basename(cloudFile.newKey)
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
sourceFolders.add(absolutePath);
|
|
2796
|
+
const oldThumbRelPath = oldKey.slice(1);
|
|
2797
|
+
const oldThumbFolder = path7.join(getPublicPath("images"), oldThumbRelPath);
|
|
2798
|
+
sourceFolders.add(oldThumbFolder);
|
|
2799
|
+
moved.push(itemPath);
|
|
2800
|
+
}
|
|
2694
2801
|
}
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
type: "progress",
|
|
2700
|
-
current: i + 1,
|
|
2701
|
-
total,
|
|
2702
|
-
pushed: pushed.length,
|
|
2703
|
-
percent: Math.round((i + 1) / total * 100),
|
|
2704
|
-
currentFile: path7.basename(key)
|
|
2705
|
-
});
|
|
2706
|
-
} catch (error) {
|
|
2707
|
-
console.error(`Failed to push update for ${key}:`, error);
|
|
2708
|
-
errors.push(key);
|
|
2802
|
+
} catch (err) {
|
|
2803
|
+
console.error(`Failed to move ${itemName}:`, err);
|
|
2804
|
+
errors.push(`Failed to move ${itemName}`);
|
|
2805
|
+
processedFiles++;
|
|
2709
2806
|
sendEvent({
|
|
2710
2807
|
type: "progress",
|
|
2711
|
-
current:
|
|
2712
|
-
total,
|
|
2713
|
-
|
|
2714
|
-
percent: Math.round(
|
|
2715
|
-
currentFile:
|
|
2716
|
-
message: `Failed: ${path7.basename(key)}`
|
|
2808
|
+
current: processedFiles,
|
|
2809
|
+
total: totalFiles,
|
|
2810
|
+
moved: moved.length,
|
|
2811
|
+
percent: Math.round(processedFiles / totalFiles * 100),
|
|
2812
|
+
currentFile: itemName
|
|
2717
2813
|
});
|
|
2718
2814
|
}
|
|
2719
2815
|
}
|
|
2720
|
-
sendEvent({ type: "cleanup", message: "Cleaning up..." });
|
|
2721
|
-
for (const itemPath of pushed) {
|
|
2722
|
-
const localPath = getPublicPath(itemPath);
|
|
2723
|
-
await deleteEmptyFolders(path7.dirname(localPath));
|
|
2724
|
-
}
|
|
2725
2816
|
await saveMeta(meta);
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
message += ` ${skipped.length} file${skipped.length !== 1 ? "s" : ""} skipped.`;
|
|
2729
|
-
}
|
|
2730
|
-
if (errors.length > 0) {
|
|
2731
|
-
message += ` ${errors.length} file${errors.length !== 1 ? "s" : ""} failed.`;
|
|
2817
|
+
for (const folder of sourceFolders) {
|
|
2818
|
+
await deleteEmptyFolders(folder);
|
|
2732
2819
|
}
|
|
2820
|
+
await deleteEmptyFolders(absoluteDestination);
|
|
2733
2821
|
sendEvent({
|
|
2734
2822
|
type: "complete",
|
|
2735
|
-
|
|
2736
|
-
skipped: skipped.length,
|
|
2823
|
+
moved: moved.length,
|
|
2737
2824
|
errors: errors.length,
|
|
2738
|
-
|
|
2825
|
+
errorMessages: errors
|
|
2739
2826
|
});
|
|
2740
2827
|
} catch (error) {
|
|
2741
|
-
console.error("
|
|
2742
|
-
sendEvent({ type: "error", message: "Failed to
|
|
2828
|
+
console.error("Failed to move:", error);
|
|
2829
|
+
sendEvent({ type: "error", message: "Failed to move items" });
|
|
2743
2830
|
} finally {
|
|
2744
2831
|
controller.close();
|
|
2745
2832
|
}
|
|
@@ -2753,63 +2840,6 @@ async function handlePushUpdatesStream(request) {
|
|
|
2753
2840
|
}
|
|
2754
2841
|
});
|
|
2755
2842
|
}
|
|
2756
|
-
async function handleCancelStreamOperation(request) {
|
|
2757
|
-
try {
|
|
2758
|
-
const { operationId } = await request.json();
|
|
2759
|
-
if (!operationId || typeof operationId !== "string") {
|
|
2760
|
-
return jsonResponse({ error: "No operation ID provided" }, { status: 400 });
|
|
2761
|
-
}
|
|
2762
|
-
cancelOperation(operationId);
|
|
2763
|
-
return jsonResponse({ success: true, operationId });
|
|
2764
|
-
} catch (error) {
|
|
2765
|
-
console.error("Failed to cancel operation:", error);
|
|
2766
|
-
return jsonResponse({ error: "Failed to cancel operation" }, { status: 500 });
|
|
2767
|
-
}
|
|
2768
|
-
}
|
|
2769
|
-
async function handleCancelUpdates(request) {
|
|
2770
|
-
try {
|
|
2771
|
-
const { paths } = await request.json();
|
|
2772
|
-
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
2773
|
-
return jsonResponse({ error: "No paths provided" }, { status: 400 });
|
|
2774
|
-
}
|
|
2775
|
-
const meta = await loadMeta();
|
|
2776
|
-
const cancelled = [];
|
|
2777
|
-
const skipped = [];
|
|
2778
|
-
const errors = [];
|
|
2779
|
-
const foldersToClean = /* @__PURE__ */ new Set();
|
|
2780
|
-
for (const itemPath of paths) {
|
|
2781
|
-
const key = itemPath.startsWith("public/") ? "/" + itemPath.slice(7) : itemPath;
|
|
2782
|
-
const entry = meta[key];
|
|
2783
|
-
if (!entry || entry.u !== 1) {
|
|
2784
|
-
skipped.push(key);
|
|
2785
|
-
continue;
|
|
2786
|
-
}
|
|
2787
|
-
try {
|
|
2788
|
-
const localPath = getPublicPath(key);
|
|
2789
|
-
await fs7.unlink(localPath);
|
|
2790
|
-
foldersToClean.add(path7.dirname(localPath));
|
|
2791
|
-
delete entry.u;
|
|
2792
|
-
cancelled.push(key);
|
|
2793
|
-
} catch (error) {
|
|
2794
|
-
console.error(`Failed to cancel update for ${key}:`, error);
|
|
2795
|
-
errors.push(key);
|
|
2796
|
-
}
|
|
2797
|
-
}
|
|
2798
|
-
for (const folder of foldersToClean) {
|
|
2799
|
-
await deleteEmptyFolders(folder);
|
|
2800
|
-
}
|
|
2801
|
-
await saveMeta(meta);
|
|
2802
|
-
return jsonResponse({
|
|
2803
|
-
success: true,
|
|
2804
|
-
cancelled: cancelled.length,
|
|
2805
|
-
skipped: skipped.length,
|
|
2806
|
-
errors: errors.length
|
|
2807
|
-
});
|
|
2808
|
-
} catch (error) {
|
|
2809
|
-
console.error("Cancel updates error:", error);
|
|
2810
|
-
return jsonResponse({ error: "Failed to cancel updates" }, { status: 500 });
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
2843
|
|
|
2814
2844
|
// src/handlers/scan.ts
|
|
2815
2845
|
import { promises as fs8 } from "fs";
|