@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.
@@ -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-C6_03alH.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-B_wl32tp.js"></script>
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -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));