@gallop.software/studio 2.3.61 → 2.3.62
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
|
@@ -1224,6 +1224,213 @@ async function handleSync(request) {
|
|
|
1224
1224
|
return jsonResponse({ error: "Failed to push to CDN" }, { status: 500 });
|
|
1225
1225
|
}
|
|
1226
1226
|
}
|
|
1227
|
+
async function handleSyncStream(request) {
|
|
1228
|
+
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
1229
|
+
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
1230
|
+
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
1231
|
+
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
1232
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1233
|
+
const encoder = new TextEncoder();
|
|
1234
|
+
const stream = new ReadableStream({
|
|
1235
|
+
async start(controller) {
|
|
1236
|
+
const sendEvent = (data) => {
|
|
1237
|
+
try {
|
|
1238
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
1239
|
+
|
|
1240
|
+
`));
|
|
1241
|
+
} catch {
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
try {
|
|
1245
|
+
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1246
|
+
sendEvent({ type: "error", message: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." });
|
|
1247
|
+
controller.close();
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const { imageKeys, operationId } = await request.json();
|
|
1251
|
+
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1252
|
+
sendEvent({ type: "error", message: "No image keys provided" });
|
|
1253
|
+
controller.close();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const isCancelled = () => operationId ? isOperationCancelled(operationId) : false;
|
|
1257
|
+
const meta = await loadMeta();
|
|
1258
|
+
const cdnUrls = getCdnUrls(meta);
|
|
1259
|
+
const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
|
|
1260
|
+
const r2 = new S3Client2({
|
|
1261
|
+
region: "auto",
|
|
1262
|
+
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
1263
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
1264
|
+
});
|
|
1265
|
+
const pushed = [];
|
|
1266
|
+
const alreadyPushed = [];
|
|
1267
|
+
const errors = [];
|
|
1268
|
+
const sourceFolders = /* @__PURE__ */ new Set();
|
|
1269
|
+
const total = imageKeys.length;
|
|
1270
|
+
sendEvent({ type: "start", total });
|
|
1271
|
+
for (let i = 0; i < imageKeys.length; i++) {
|
|
1272
|
+
if (isCancelled()) {
|
|
1273
|
+
await saveMeta(meta);
|
|
1274
|
+
for (const folder of sourceFolders) {
|
|
1275
|
+
await deleteEmptyFolders(folder);
|
|
1276
|
+
}
|
|
1277
|
+
if (operationId) clearCancelledOperation(operationId);
|
|
1278
|
+
sendEvent({
|
|
1279
|
+
type: "complete",
|
|
1280
|
+
pushed: pushed.length,
|
|
1281
|
+
alreadyPushed: alreadyPushed.length,
|
|
1282
|
+
errors: errors.length,
|
|
1283
|
+
message: `Stopped. ${pushed.length} file${pushed.length !== 1 ? "s" : ""} pushed.`,
|
|
1284
|
+
cancelled: true
|
|
1285
|
+
});
|
|
1286
|
+
controller.close();
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
let imageKey = imageKeys[i];
|
|
1290
|
+
if (!imageKey.startsWith("/")) {
|
|
1291
|
+
imageKey = `/${imageKey}`;
|
|
1292
|
+
}
|
|
1293
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
1294
|
+
if (!entry) {
|
|
1295
|
+
errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
|
|
1296
|
+
sendEvent({
|
|
1297
|
+
type: "progress",
|
|
1298
|
+
current: i + 1,
|
|
1299
|
+
total,
|
|
1300
|
+
pushed: pushed.length,
|
|
1301
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1302
|
+
currentFile: path6.basename(imageKey)
|
|
1303
|
+
});
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
const existingCdnUrl = entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
1307
|
+
const isAlreadyInOurR2 = existingCdnUrl === publicUrl;
|
|
1308
|
+
if (isAlreadyInOurR2) {
|
|
1309
|
+
alreadyPushed.push(imageKey);
|
|
1310
|
+
sendEvent({
|
|
1311
|
+
type: "progress",
|
|
1312
|
+
current: i + 1,
|
|
1313
|
+
total,
|
|
1314
|
+
pushed: pushed.length,
|
|
1315
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1316
|
+
currentFile: path6.basename(imageKey)
|
|
1317
|
+
});
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const isRemote = entry.c !== void 0 && existingCdnUrl !== publicUrl;
|
|
1321
|
+
try {
|
|
1322
|
+
let originalBuffer;
|
|
1323
|
+
if (isRemote) {
|
|
1324
|
+
const remoteUrl = `${existingCdnUrl}${imageKey}`;
|
|
1325
|
+
originalBuffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1326
|
+
} else {
|
|
1327
|
+
const originalLocalPath = getPublicPath(imageKey);
|
|
1328
|
+
try {
|
|
1329
|
+
originalBuffer = await fs6.readFile(originalLocalPath);
|
|
1330
|
+
} catch {
|
|
1331
|
+
errors.push(`Original file not found: ${imageKey}`);
|
|
1332
|
+
sendEvent({
|
|
1333
|
+
type: "progress",
|
|
1334
|
+
current: i + 1,
|
|
1335
|
+
total,
|
|
1336
|
+
pushed: pushed.length,
|
|
1337
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1338
|
+
currentFile: path6.basename(imageKey)
|
|
1339
|
+
});
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
await r2.send(
|
|
1344
|
+
new PutObjectCommand2({
|
|
1345
|
+
Bucket: bucketName,
|
|
1346
|
+
Key: imageKey.replace(/^\//, ""),
|
|
1347
|
+
Body: originalBuffer,
|
|
1348
|
+
ContentType: getContentType(imageKey)
|
|
1349
|
+
})
|
|
1350
|
+
);
|
|
1351
|
+
if (!isRemote && isProcessed(entry)) {
|
|
1352
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1353
|
+
const localPath = getPublicPath(thumbPath);
|
|
1354
|
+
try {
|
|
1355
|
+
const fileBuffer = await fs6.readFile(localPath);
|
|
1356
|
+
await r2.send(
|
|
1357
|
+
new PutObjectCommand2({
|
|
1358
|
+
Bucket: bucketName,
|
|
1359
|
+
Key: thumbPath.replace(/^\//, ""),
|
|
1360
|
+
Body: fileBuffer,
|
|
1361
|
+
ContentType: getContentType(thumbPath)
|
|
1362
|
+
})
|
|
1363
|
+
);
|
|
1364
|
+
} catch {
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
entry.c = cdnIndex;
|
|
1369
|
+
if (!isRemote) {
|
|
1370
|
+
const originalLocalPath = getPublicPath(imageKey);
|
|
1371
|
+
sourceFolders.add(path6.dirname(originalLocalPath));
|
|
1372
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1373
|
+
const localPath = getPublicPath(thumbPath);
|
|
1374
|
+
sourceFolders.add(path6.dirname(localPath));
|
|
1375
|
+
try {
|
|
1376
|
+
await fs6.unlink(localPath);
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
try {
|
|
1381
|
+
await fs6.unlink(originalLocalPath);
|
|
1382
|
+
} catch {
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
await saveMeta(meta);
|
|
1386
|
+
pushed.push(imageKey);
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
console.error(`Failed to push ${imageKey}:`, error);
|
|
1389
|
+
errors.push(`Failed to push: ${imageKey}`);
|
|
1390
|
+
}
|
|
1391
|
+
sendEvent({
|
|
1392
|
+
type: "progress",
|
|
1393
|
+
current: i + 1,
|
|
1394
|
+
total,
|
|
1395
|
+
pushed: pushed.length,
|
|
1396
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1397
|
+
currentFile: path6.basename(imageKey)
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
for (const folder of sourceFolders) {
|
|
1401
|
+
await deleteEmptyFolders(folder);
|
|
1402
|
+
}
|
|
1403
|
+
let message;
|
|
1404
|
+
if (pushed.length === 0 && errors.length === 0) {
|
|
1405
|
+
message = `${alreadyPushed.length} file${alreadyPushed.length !== 1 ? "s" : ""} already on CDN. 0 new files pushed.`;
|
|
1406
|
+
} else if (alreadyPushed.length > 0 && errors.length === 0) {
|
|
1407
|
+
message = `${pushed.length} file${pushed.length !== 1 ? "s" : ""} pushed. ${alreadyPushed.length} already on CDN.`;
|
|
1408
|
+
}
|
|
1409
|
+
if (operationId) clearCancelledOperation(operationId);
|
|
1410
|
+
sendEvent({
|
|
1411
|
+
type: "complete",
|
|
1412
|
+
pushed: pushed.length,
|
|
1413
|
+
alreadyPushed: alreadyPushed.length,
|
|
1414
|
+
errors: errors.length,
|
|
1415
|
+
errorMessages: errors.length > 0 ? errors : void 0,
|
|
1416
|
+
message
|
|
1417
|
+
});
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
console.error("Failed to push:", error);
|
|
1420
|
+
sendEvent({ type: "error", message: "Failed to push to CDN" });
|
|
1421
|
+
} finally {
|
|
1422
|
+
controller.close();
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
return new Response(stream, {
|
|
1427
|
+
headers: {
|
|
1428
|
+
"Content-Type": "text/event-stream",
|
|
1429
|
+
"Cache-Control": "no-cache",
|
|
1430
|
+
"Connection": "keep-alive"
|
|
1431
|
+
}
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1227
1434
|
async function handleUnprocessStream(request) {
|
|
1228
1435
|
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1229
1436
|
const encoder = new TextEncoder();
|
|
@@ -3551,6 +3758,7 @@ async function startServer(options) {
|
|
|
3551
3758
|
app.post("/api/studio/rename-stream", wrapHandler(handleRenameStream, true));
|
|
3552
3759
|
app.post("/api/studio/move", wrapHandler(handleMoveStream, true));
|
|
3553
3760
|
app.post("/api/studio/sync", wrapHandler(handleSync, true));
|
|
3761
|
+
app.post("/api/studio/sync-stream", wrapHandler(handleSyncStream, true));
|
|
3554
3762
|
app.post("/api/studio/reprocess-stream", wrapHandler(handleReprocessStream, true));
|
|
3555
3763
|
app.post("/api/studio/unprocess-stream", wrapHandler(handleUnprocessStream, true));
|
|
3556
3764
|
app.post("/api/studio/download-stream", wrapHandler(handleDownloadStream, true));
|