@gallop.software/studio 2.3.61 → 2.3.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
12
12
|
}
|
|
13
13
|
</style>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-B_wl32tp.js"></script>
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/dist/server/index.js
CHANGED
|
@@ -544,6 +544,7 @@ async function handleList(request) {
|
|
|
544
544
|
isProtected: true,
|
|
545
545
|
cdnPushed: isPushedToCloud,
|
|
546
546
|
cdnBaseUrl,
|
|
547
|
+
isCloud: isPushedToCloud && !isRemote,
|
|
547
548
|
isRemote,
|
|
548
549
|
dimensions
|
|
549
550
|
});
|
|
@@ -598,6 +599,7 @@ async function handleList(request) {
|
|
|
598
599
|
isProtected: true,
|
|
599
600
|
cdnPushed: isPushedToCloud,
|
|
600
601
|
cdnBaseUrl,
|
|
602
|
+
isCloud: isPushedToCloud && !isRemote,
|
|
601
603
|
isRemote,
|
|
602
604
|
dimensions
|
|
603
605
|
});
|
|
@@ -826,6 +828,7 @@ async function handleList(request) {
|
|
|
826
828
|
hasFull: !!entry.f,
|
|
827
829
|
cdnPushed: isPushedToCloud,
|
|
828
830
|
cdnBaseUrl: fileCdnUrl,
|
|
831
|
+
isCloud: isPushedToCloud && !isRemote,
|
|
829
832
|
isRemote,
|
|
830
833
|
isProtected: isInsideImagesFolder,
|
|
831
834
|
dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0,
|
|
@@ -904,6 +907,7 @@ async function handleSearch(request) {
|
|
|
904
907
|
hasFull: !!entry.f,
|
|
905
908
|
cdnPushed: isPushedToCloud,
|
|
906
909
|
cdnBaseUrl: fileCdnUrl,
|
|
910
|
+
isCloud: isPushedToCloud && !isRemote,
|
|
907
911
|
isRemote,
|
|
908
912
|
dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0,
|
|
909
913
|
hasUpdate: entry.u === 1
|
|
@@ -1224,6 +1228,213 @@ async function handleSync(request) {
|
|
|
1224
1228
|
return jsonResponse({ error: "Failed to push to CDN" }, { status: 500 });
|
|
1225
1229
|
}
|
|
1226
1230
|
}
|
|
1231
|
+
async function handleSyncStream(request) {
|
|
1232
|
+
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
1233
|
+
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
1234
|
+
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
1235
|
+
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
1236
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1237
|
+
const encoder = new TextEncoder();
|
|
1238
|
+
const stream = new ReadableStream({
|
|
1239
|
+
async start(controller) {
|
|
1240
|
+
const sendEvent = (data) => {
|
|
1241
|
+
try {
|
|
1242
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
1243
|
+
|
|
1244
|
+
`));
|
|
1245
|
+
} catch {
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
try {
|
|
1249
|
+
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1250
|
+
sendEvent({ type: "error", message: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." });
|
|
1251
|
+
controller.close();
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const { imageKeys, operationId } = await request.json();
|
|
1255
|
+
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1256
|
+
sendEvent({ type: "error", message: "No image keys provided" });
|
|
1257
|
+
controller.close();
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
const isCancelled = () => operationId ? isOperationCancelled(operationId) : false;
|
|
1261
|
+
const meta = await loadMeta();
|
|
1262
|
+
const cdnUrls = getCdnUrls(meta);
|
|
1263
|
+
const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
|
|
1264
|
+
const r2 = new S3Client2({
|
|
1265
|
+
region: "auto",
|
|
1266
|
+
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
1267
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
1268
|
+
});
|
|
1269
|
+
const pushed = [];
|
|
1270
|
+
const alreadyPushed = [];
|
|
1271
|
+
const errors = [];
|
|
1272
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
1273
|
+
const total = imageKeys.length;
|
|
1274
|
+
sendEvent({ type: "start", total });
|
|
1275
|
+
for (let i = 0; i < imageKeys.length; i++) {
|
|
1276
|
+
if (isCancelled()) {
|
|
1277
|
+
await saveMeta(meta);
|
|
1278
|
+
for (const folder of sourceFolders) {
|
|
1279
|
+
await deleteEmptyFolders(folder);
|
|
1280
|
+
}
|
|
1281
|
+
if (operationId) clearCancelledOperation(operationId);
|
|
1282
|
+
sendEvent({
|
|
1283
|
+
type: "complete",
|
|
1284
|
+
pushed: pushed.length,
|
|
1285
|
+
alreadyPushed: alreadyPushed.length,
|
|
1286
|
+
errors: errors.length,
|
|
1287
|
+
message: `Stopped. ${pushed.length} file${pushed.length !== 1 ? "s" : ""} pushed.`,
|
|
1288
|
+
cancelled: true
|
|
1289
|
+
});
|
|
1290
|
+
controller.close();
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
let imageKey = imageKeys[i];
|
|
1294
|
+
if (!imageKey.startsWith("/")) {
|
|
1295
|
+
imageKey = `/${imageKey}`;
|
|
1296
|
+
}
|
|
1297
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
1298
|
+
if (!entry) {
|
|
1299
|
+
errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
|
|
1300
|
+
sendEvent({
|
|
1301
|
+
type: "progress",
|
|
1302
|
+
current: i + 1,
|
|
1303
|
+
total,
|
|
1304
|
+
pushed: pushed.length,
|
|
1305
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1306
|
+
currentFile: path6.basename(imageKey)
|
|
1307
|
+
});
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const existingCdnUrl = entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
1311
|
+
const isAlreadyInOurR2 = existingCdnUrl === publicUrl;
|
|
1312
|
+
if (isAlreadyInOurR2) {
|
|
1313
|
+
alreadyPushed.push(imageKey);
|
|
1314
|
+
sendEvent({
|
|
1315
|
+
type: "progress",
|
|
1316
|
+
current: i + 1,
|
|
1317
|
+
total,
|
|
1318
|
+
pushed: pushed.length,
|
|
1319
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1320
|
+
currentFile: path6.basename(imageKey)
|
|
1321
|
+
});
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const isRemote = entry.c !== void 0 && existingCdnUrl !== publicUrl;
|
|
1325
|
+
try {
|
|
1326
|
+
let originalBuffer;
|
|
1327
|
+
if (isRemote) {
|
|
1328
|
+
const remoteUrl = `${existingCdnUrl}${imageKey}`;
|
|
1329
|
+
originalBuffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1330
|
+
} else {
|
|
1331
|
+
const originalLocalPath = getPublicPath(imageKey);
|
|
1332
|
+
try {
|
|
1333
|
+
originalBuffer = await fs6.readFile(originalLocalPath);
|
|
1334
|
+
} catch {
|
|
1335
|
+
errors.push(`Original file not found: ${imageKey}`);
|
|
1336
|
+
sendEvent({
|
|
1337
|
+
type: "progress",
|
|
1338
|
+
current: i + 1,
|
|
1339
|
+
total,
|
|
1340
|
+
pushed: pushed.length,
|
|
1341
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1342
|
+
currentFile: path6.basename(imageKey)
|
|
1343
|
+
});
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
await r2.send(
|
|
1348
|
+
new PutObjectCommand2({
|
|
1349
|
+
Bucket: bucketName,
|
|
1350
|
+
Key: imageKey.replace(/^\//, ""),
|
|
1351
|
+
Body: originalBuffer,
|
|
1352
|
+
ContentType: getContentType(imageKey)
|
|
1353
|
+
})
|
|
1354
|
+
);
|
|
1355
|
+
if (!isRemote && isProcessed(entry)) {
|
|
1356
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1357
|
+
const localPath = getPublicPath(thumbPath);
|
|
1358
|
+
try {
|
|
1359
|
+
const fileBuffer = await fs6.readFile(localPath);
|
|
1360
|
+
await r2.send(
|
|
1361
|
+
new PutObjectCommand2({
|
|
1362
|
+
Bucket: bucketName,
|
|
1363
|
+
Key: thumbPath.replace(/^\//, ""),
|
|
1364
|
+
Body: fileBuffer,
|
|
1365
|
+
ContentType: getContentType(thumbPath)
|
|
1366
|
+
})
|
|
1367
|
+
);
|
|
1368
|
+
} catch {
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
entry.c = cdnIndex;
|
|
1373
|
+
if (!isRemote) {
|
|
1374
|
+
const originalLocalPath = getPublicPath(imageKey);
|
|
1375
|
+
sourceFolders.add(path6.dirname(originalLocalPath));
|
|
1376
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1377
|
+
const localPath = getPublicPath(thumbPath);
|
|
1378
|
+
sourceFolders.add(path6.dirname(localPath));
|
|
1379
|
+
try {
|
|
1380
|
+
await fs6.unlink(localPath);
|
|
1381
|
+
} catch {
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
try {
|
|
1385
|
+
await fs6.unlink(originalLocalPath);
|
|
1386
|
+
} catch {
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
await saveMeta(meta);
|
|
1390
|
+
pushed.push(imageKey);
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
console.error(`Failed to push ${imageKey}:`, error);
|
|
1393
|
+
errors.push(`Failed to push: ${imageKey}`);
|
|
1394
|
+
}
|
|
1395
|
+
sendEvent({
|
|
1396
|
+
type: "progress",
|
|
1397
|
+
current: i + 1,
|
|
1398
|
+
total,
|
|
1399
|
+
pushed: pushed.length,
|
|
1400
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1401
|
+
currentFile: path6.basename(imageKey)
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
for (const folder of sourceFolders) {
|
|
1405
|
+
await deleteEmptyFolders(folder);
|
|
1406
|
+
}
|
|
1407
|
+
let message;
|
|
1408
|
+
if (pushed.length === 0 && errors.length === 0) {
|
|
1409
|
+
message = `${alreadyPushed.length} file${alreadyPushed.length !== 1 ? "s" : ""} already on CDN. 0 new files pushed.`;
|
|
1410
|
+
} else if (alreadyPushed.length > 0 && errors.length === 0) {
|
|
1411
|
+
message = `${pushed.length} file${pushed.length !== 1 ? "s" : ""} pushed. ${alreadyPushed.length} already on CDN.`;
|
|
1412
|
+
}
|
|
1413
|
+
if (operationId) clearCancelledOperation(operationId);
|
|
1414
|
+
sendEvent({
|
|
1415
|
+
type: "complete",
|
|
1416
|
+
pushed: pushed.length,
|
|
1417
|
+
alreadyPushed: alreadyPushed.length,
|
|
1418
|
+
errors: errors.length,
|
|
1419
|
+
errorMessages: errors.length > 0 ? errors : void 0,
|
|
1420
|
+
message
|
|
1421
|
+
});
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
console.error("Failed to push:", error);
|
|
1424
|
+
sendEvent({ type: "error", message: "Failed to push to CDN" });
|
|
1425
|
+
} finally {
|
|
1426
|
+
controller.close();
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
});
|
|
1430
|
+
return new Response(stream, {
|
|
1431
|
+
headers: {
|
|
1432
|
+
"Content-Type": "text/event-stream",
|
|
1433
|
+
"Cache-Control": "no-cache",
|
|
1434
|
+
"Connection": "keep-alive"
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1227
1438
|
async function handleUnprocessStream(request) {
|
|
1228
1439
|
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1229
1440
|
const encoder = new TextEncoder();
|
|
@@ -3551,6 +3762,7 @@ async function startServer(options) {
|
|
|
3551
3762
|
app.post("/api/studio/rename-stream", wrapHandler(handleRenameStream, true));
|
|
3552
3763
|
app.post("/api/studio/move", wrapHandler(handleMoveStream, true));
|
|
3553
3764
|
app.post("/api/studio/sync", wrapHandler(handleSync, true));
|
|
3765
|
+
app.post("/api/studio/sync-stream", wrapHandler(handleSyncStream, true));
|
|
3554
3766
|
app.post("/api/studio/reprocess-stream", wrapHandler(handleReprocessStream, true));
|
|
3555
3767
|
app.post("/api/studio/unprocess-stream", wrapHandler(handleUnprocessStream, true));
|
|
3556
3768
|
app.post("/api/studio/download-stream", wrapHandler(handleDownloadStream, true));
|