@gallop.software/studio 0.1.113 → 0.1.115

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.
@@ -160,6 +160,31 @@ async function processImage(buffer, imageKey) {
160
160
 
161
161
 
162
162
  var _clients3 = require('@aws-sdk/client-s3');
163
+ async function purgeCloudflareCache(urls) {
164
+ const zoneId = process.env.CLOUDFLARE_ZONE_ID;
165
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
166
+ if (!zoneId || !apiToken || urls.length === 0) {
167
+ return;
168
+ }
169
+ try {
170
+ const response = await fetch(
171
+ `https://api.cloudflare.com/client/v4/zones/${zoneId}/purge_cache`,
172
+ {
173
+ method: "POST",
174
+ headers: {
175
+ "Authorization": `Bearer ${apiToken}`,
176
+ "Content-Type": "application/json"
177
+ },
178
+ body: JSON.stringify({ files: urls })
179
+ }
180
+ );
181
+ if (!response.ok) {
182
+ console.error("Cache purge failed:", await response.text());
183
+ }
184
+ } catch (error) {
185
+ console.error("Cache purge error:", error);
186
+ }
187
+ }
163
188
  function getR2Client() {
164
189
  const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
165
190
  const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
@@ -1022,7 +1047,7 @@ async function handleSync(request) {
1022
1047
  const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
1023
1048
  const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
1024
1049
  const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
1025
- const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL;
1050
+ const publicUrl = _optionalChain([process, 'access', _27 => _27.env, 'access', _28 => _28.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _29 => _29.replace, 'call', _30 => _30(/\/\s*$/, "")]);
1026
1051
  if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
1027
1052
  return _server.NextResponse.json(
1028
1053
  { error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." },
@@ -1035,6 +1060,7 @@ async function handleSync(request) {
1035
1060
  return _server.NextResponse.json({ error: "No image keys provided" }, { status: 400 });
1036
1061
  }
1037
1062
  const meta = await loadMeta();
1063
+ const cdnUrls = getCdnUrls(meta);
1038
1064
  const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
1039
1065
  const r2 = new (0, _clients3.S3Client)({
1040
1066
  region: "auto",
@@ -1043,63 +1069,80 @@ async function handleSync(request) {
1043
1069
  });
1044
1070
  const pushed = [];
1045
1071
  const errors = [];
1072
+ const urlsToPurge = [];
1046
1073
  for (const imageKey of imageKeys) {
1047
1074
  const entry = getMetaEntry(meta, imageKey);
1048
1075
  if (!entry) {
1049
1076
  errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
1050
1077
  continue;
1051
1078
  }
1052
- if (entry.c !== void 0) {
1079
+ const existingCdnUrl = entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
1080
+ const isAlreadyInOurR2 = existingCdnUrl === publicUrl;
1081
+ if (isAlreadyInOurR2) {
1053
1082
  pushed.push(imageKey);
1054
1083
  continue;
1055
1084
  }
1056
- if (!entry.p) {
1085
+ const isRemote = entry.c !== void 0 && existingCdnUrl !== publicUrl;
1086
+ if (!isRemote && !entry.p) {
1057
1087
  errors.push(`Image not processed: ${imageKey}. Run Process Images first.`);
1058
1088
  continue;
1059
1089
  }
1060
1090
  try {
1061
- const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
1062
- try {
1063
- const originalBuffer = await _fs.promises.readFile(originalLocalPath);
1064
- await r2.send(
1065
- new (0, _clients3.PutObjectCommand)({
1066
- Bucket: bucketName,
1067
- Key: imageKey.replace(/^\//, ""),
1068
- Body: originalBuffer,
1069
- ContentType: getContentType(imageKey)
1070
- })
1071
- );
1072
- } catch (err) {
1073
- errors.push(`Original file not found: ${imageKey}`);
1074
- continue;
1075
- }
1076
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1077
- const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1091
+ let originalBuffer;
1092
+ if (isRemote) {
1093
+ const remoteUrl = `${existingCdnUrl}${imageKey}`;
1094
+ originalBuffer = await downloadFromRemoteUrl(remoteUrl);
1095
+ } else {
1096
+ const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
1078
1097
  try {
1079
- const fileBuffer = await _fs.promises.readFile(localPath);
1080
- await r2.send(
1081
- new (0, _clients3.PutObjectCommand)({
1082
- Bucket: bucketName,
1083
- Key: thumbPath.replace(/^\//, ""),
1084
- Body: fileBuffer,
1085
- ContentType: getContentType(thumbPath)
1086
- })
1087
- );
1098
+ originalBuffer = await _fs.promises.readFile(originalLocalPath);
1088
1099
  } catch (e23) {
1100
+ errors.push(`Original file not found: ${imageKey}`);
1101
+ continue;
1102
+ }
1103
+ }
1104
+ await r2.send(
1105
+ new (0, _clients3.PutObjectCommand)({
1106
+ Bucket: bucketName,
1107
+ Key: imageKey.replace(/^\//, ""),
1108
+ Body: originalBuffer,
1109
+ ContentType: getContentType(imageKey)
1110
+ })
1111
+ );
1112
+ urlsToPurge.push(`${publicUrl}${imageKey}`);
1113
+ if (!isRemote && entry.p) {
1114
+ for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1115
+ const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1116
+ try {
1117
+ const fileBuffer = await _fs.promises.readFile(localPath);
1118
+ await r2.send(
1119
+ new (0, _clients3.PutObjectCommand)({
1120
+ Bucket: bucketName,
1121
+ Key: thumbPath.replace(/^\//, ""),
1122
+ Body: fileBuffer,
1123
+ ContentType: getContentType(thumbPath)
1124
+ })
1125
+ );
1126
+ urlsToPurge.push(`${publicUrl}${thumbPath}`);
1127
+ } catch (e24) {
1128
+ }
1089
1129
  }
1090
1130
  }
1091
1131
  entry.c = cdnIndex;
1092
- for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1093
- const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1132
+ if (!isRemote) {
1133
+ const originalLocalPath = _path2.default.join(process.cwd(), "public", imageKey);
1134
+ for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1135
+ const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
1136
+ try {
1137
+ await _fs.promises.unlink(localPath);
1138
+ } catch (e25) {
1139
+ }
1140
+ }
1094
1141
  try {
1095
- await _fs.promises.unlink(localPath);
1096
- } catch (e24) {
1142
+ await _fs.promises.unlink(originalLocalPath);
1143
+ } catch (e26) {
1097
1144
  }
1098
1145
  }
1099
- try {
1100
- await _fs.promises.unlink(originalLocalPath);
1101
- } catch (e25) {
1102
- }
1103
1146
  pushed.push(imageKey);
1104
1147
  } catch (error) {
1105
1148
  console.error(`Failed to push ${imageKey}:`, error);
@@ -1107,53 +1150,71 @@ async function handleSync(request) {
1107
1150
  }
1108
1151
  }
1109
1152
  await saveMeta(meta);
1153
+ if (urlsToPurge.length > 0) {
1154
+ await purgeCloudflareCache(urlsToPurge);
1155
+ }
1110
1156
  return _server.NextResponse.json({
1111
1157
  success: true,
1112
1158
  pushed,
1113
1159
  errors: errors.length > 0 ? errors : void 0
1114
1160
  });
1115
1161
  } catch (error) {
1116
- console.error("Failed to sync:", error);
1117
- return _server.NextResponse.json({ error: "Failed to sync to CDN" }, { status: 500 });
1162
+ console.error("Failed to push:", error);
1163
+ return _server.NextResponse.json({ error: "Failed to push to CDN" }, { status: 500 });
1118
1164
  }
1119
1165
  }
1120
1166
  async function handleReprocess(request) {
1167
+ const publicUrl = _optionalChain([process, 'access', _31 => _31.env, 'access', _32 => _32.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _33 => _33.replace, 'call', _34 => _34(/\/\s*$/, "")]);
1121
1168
  try {
1122
1169
  const { imageKeys } = await request.json();
1123
1170
  if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1124
1171
  return _server.NextResponse.json({ error: "No image keys provided" }, { status: 400 });
1125
1172
  }
1126
1173
  const meta = await loadMeta();
1174
+ const cdnUrls = getCdnUrls(meta);
1127
1175
  const processed = [];
1128
1176
  const errors = [];
1177
+ const urlsToPurge = [];
1129
1178
  for (const imageKey of imageKeys) {
1130
1179
  try {
1131
1180
  let buffer;
1132
1181
  const entry = getMetaEntry(meta, imageKey);
1133
- const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _27 => _27.c]) !== void 0;
1134
- const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _28 => _28.c]);
1182
+ const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _35 => _35.c]);
1183
+ const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1184
+ const isInOurR2 = existingCdnUrl === publicUrl;
1185
+ const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1135
1186
  const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
1136
1187
  try {
1137
1188
  buffer = await _fs.promises.readFile(originalPath);
1138
- } catch (e26) {
1139
- if (isPushedToCloud) {
1189
+ } catch (e27) {
1190
+ if (isInOurR2) {
1140
1191
  buffer = await downloadFromCdn(imageKey);
1141
1192
  const dir = _path2.default.dirname(originalPath);
1142
1193
  await _fs.promises.mkdir(dir, { recursive: true });
1143
1194
  await _fs.promises.writeFile(originalPath, buffer);
1195
+ } else if (isRemote && existingCdnUrl) {
1196
+ const remoteUrl = `${existingCdnUrl}${imageKey}`;
1197
+ buffer = await downloadFromRemoteUrl(remoteUrl);
1198
+ const dir = _path2.default.dirname(originalPath);
1199
+ await _fs.promises.mkdir(dir, { recursive: true });
1200
+ await _fs.promises.writeFile(originalPath, buffer);
1144
1201
  } else {
1145
1202
  throw new Error(`File not found: ${imageKey}`);
1146
1203
  }
1147
1204
  }
1148
1205
  const updatedEntry = await processImage(buffer, imageKey);
1149
- if (isPushedToCloud) {
1206
+ if (isInOurR2) {
1150
1207
  updatedEntry.c = existingCdnIndex;
1151
1208
  await uploadToCdn(imageKey);
1209
+ for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, imageKey)) {
1210
+ urlsToPurge.push(`${publicUrl}${thumbPath}`);
1211
+ }
1152
1212
  await deleteLocalThumbnails(imageKey);
1153
1213
  try {
1154
1214
  await _fs.promises.unlink(originalPath);
1155
- } catch (e27) {
1215
+ } catch (e28) {
1156
1216
  }
1217
+ } else if (isRemote) {
1157
1218
  }
1158
1219
  meta[imageKey] = updatedEntry;
1159
1220
  processed.push(imageKey);
@@ -1163,6 +1224,9 @@ async function handleReprocess(request) {
1163
1224
  }
1164
1225
  }
1165
1226
  await saveMeta(meta);
1227
+ if (urlsToPurge.length > 0) {
1228
+ await purgeCloudflareCache(urlsToPurge);
1229
+ }
1166
1230
  return _server.NextResponse.json({
1167
1231
  success: true,
1168
1232
  processed,
@@ -1174,6 +1238,7 @@ async function handleReprocess(request) {
1174
1238
  }
1175
1239
  }
1176
1240
  async function handleProcessAllStream() {
1241
+ const publicUrl = _optionalChain([process, 'access', _36 => _36.env, 'access', _37 => _37.CLOUDFLARE_R2_PUBLIC_URL, 'optionalAccess', _38 => _38.replace, 'call', _39 => _39(/\/\s*$/, "")]);
1177
1242
  const encoder = new TextEncoder();
1178
1243
  const stream = new ReadableStream({
1179
1244
  async start(controller) {
@@ -1184,9 +1249,11 @@ async function handleProcessAllStream() {
1184
1249
  };
1185
1250
  try {
1186
1251
  const meta = await loadMeta();
1252
+ const cdnUrls = getCdnUrls(meta);
1187
1253
  const processed = [];
1188
1254
  const errors = [];
1189
1255
  const orphansRemoved = [];
1256
+ const urlsToPurge = [];
1190
1257
  let alreadyProcessed = 0;
1191
1258
  const imagesToProcess = [];
1192
1259
  for (const [key, entry] of getFileEntries(meta)) {
@@ -1203,8 +1270,10 @@ async function handleProcessAllStream() {
1203
1270
  for (let i = 0; i < imagesToProcess.length; i++) {
1204
1271
  const { key, entry } = imagesToProcess[i];
1205
1272
  const fullPath = _path2.default.join(process.cwd(), "public", key);
1206
- const isInCloud = entry.c !== void 0;
1207
1273
  const existingCdnIndex = entry.c;
1274
+ const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1275
+ const isInOurR2 = existingCdnUrl === publicUrl;
1276
+ const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1208
1277
  sendEvent({
1209
1278
  type: "progress",
1210
1279
  current: i + 1,
@@ -1215,11 +1284,17 @@ async function handleProcessAllStream() {
1215
1284
  });
1216
1285
  try {
1217
1286
  let buffer;
1218
- if (isInCloud) {
1287
+ if (isInOurR2) {
1219
1288
  buffer = await downloadFromCdn(key);
1220
1289
  const dir = _path2.default.dirname(fullPath);
1221
1290
  await _fs.promises.mkdir(dir, { recursive: true });
1222
1291
  await _fs.promises.writeFile(fullPath, buffer);
1292
+ } else if (isRemote && existingCdnUrl) {
1293
+ const remoteUrl = `${existingCdnUrl}${key}`;
1294
+ buffer = await downloadFromRemoteUrl(remoteUrl);
1295
+ const dir = _path2.default.dirname(fullPath);
1296
+ await _fs.promises.mkdir(dir, { recursive: true });
1297
+ await _fs.promises.writeFile(fullPath, buffer);
1223
1298
  } else {
1224
1299
  buffer = await _fs.promises.readFile(fullPath);
1225
1300
  }
@@ -1239,20 +1314,26 @@ async function handleProcessAllStream() {
1239
1314
  b: "",
1240
1315
  p: 1
1241
1316
  };
1317
+ if (isRemote) {
1318
+ delete meta[key].c;
1319
+ }
1242
1320
  } else {
1243
1321
  const processedEntry = await processImage(buffer, key);
1244
1322
  meta[key] = {
1245
1323
  ...processedEntry,
1246
1324
  p: 1,
1247
- ...isInCloud ? { c: existingCdnIndex } : {}
1325
+ ...isInOurR2 ? { c: existingCdnIndex } : {}
1248
1326
  };
1249
1327
  }
1250
- if (isInCloud) {
1328
+ if (isInOurR2) {
1251
1329
  await uploadToCdn(key);
1330
+ for (const thumbPath of _chunkLEOQKJCLjs.getAllThumbnailPaths.call(void 0, key)) {
1331
+ urlsToPurge.push(`${publicUrl}${thumbPath}`);
1332
+ }
1252
1333
  await deleteLocalThumbnails(key);
1253
1334
  try {
1254
1335
  await _fs.promises.unlink(fullPath);
1255
- } catch (e28) {
1336
+ } catch (e29) {
1256
1337
  }
1257
1338
  }
1258
1339
  processed.push(key.slice(1));
@@ -1273,17 +1354,17 @@ async function handleProcessAllStream() {
1273
1354
  async function findOrphans(dir, relativePath = "") {
1274
1355
  try {
1275
1356
  const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
1276
- for (const entry of entries) {
1277
- if (entry.name.startsWith(".")) continue;
1278
- const fullPath = _path2.default.join(dir, entry.name);
1279
- const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1280
- if (entry.isDirectory()) {
1281
- await findOrphans(fullPath, relPath);
1282
- } else if (isImageFile(entry.name)) {
1357
+ for (const fsEntry of entries) {
1358
+ if (fsEntry.name.startsWith(".")) continue;
1359
+ const entryFullPath = _path2.default.join(dir, fsEntry.name);
1360
+ const relPath = relativePath ? `${relativePath}/${fsEntry.name}` : fsEntry.name;
1361
+ if (fsEntry.isDirectory()) {
1362
+ await findOrphans(entryFullPath, relPath);
1363
+ } else if (isImageFile(fsEntry.name)) {
1283
1364
  const publicPath = `/images/${relPath}`;
1284
1365
  if (!trackedPaths.has(publicPath)) {
1285
1366
  try {
1286
- await _fs.promises.unlink(fullPath);
1367
+ await _fs.promises.unlink(entryFullPath);
1287
1368
  orphansRemoved.push(publicPath);
1288
1369
  } catch (err) {
1289
1370
  console.error(`Failed to remove orphan ${publicPath}:`, err);
@@ -1291,21 +1372,21 @@ async function handleProcessAllStream() {
1291
1372
  }
1292
1373
  }
1293
1374
  }
1294
- } catch (e29) {
1375
+ } catch (e30) {
1295
1376
  }
1296
1377
  }
1297
1378
  const imagesDir = _path2.default.join(process.cwd(), "public", "images");
1298
1379
  try {
1299
1380
  await findOrphans(imagesDir);
1300
- } catch (e30) {
1381
+ } catch (e31) {
1301
1382
  }
1302
1383
  async function removeEmptyDirs(dir) {
1303
1384
  try {
1304
1385
  const entries = await _fs.promises.readdir(dir, { withFileTypes: true });
1305
1386
  let isEmpty = true;
1306
- for (const entry of entries) {
1307
- if (entry.isDirectory()) {
1308
- const subDirEmpty = await removeEmptyDirs(_path2.default.join(dir, entry.name));
1387
+ for (const fsEntry of entries) {
1388
+ if (fsEntry.isDirectory()) {
1389
+ const subDirEmpty = await removeEmptyDirs(_path2.default.join(dir, fsEntry.name));
1309
1390
  if (!subDirEmpty) isEmpty = false;
1310
1391
  } else {
1311
1392
  isEmpty = false;
@@ -1315,15 +1396,18 @@ async function handleProcessAllStream() {
1315
1396
  await _fs.promises.rmdir(dir);
1316
1397
  }
1317
1398
  return isEmpty;
1318
- } catch (e31) {
1399
+ } catch (e32) {
1319
1400
  return true;
1320
1401
  }
1321
1402
  }
1322
1403
  try {
1323
1404
  await removeEmptyDirs(imagesDir);
1324
- } catch (e32) {
1405
+ } catch (e33) {
1325
1406
  }
1326
1407
  await saveMeta(meta);
1408
+ if (urlsToPurge.length > 0) {
1409
+ await purgeCloudflareCache(urlsToPurge);
1410
+ }
1327
1411
  sendEvent({
1328
1412
  type: "complete",
1329
1413
  processed: processed.length,
@@ -1384,7 +1468,7 @@ async function handleScanStream() {
1384
1468
  allFiles.push({ relativePath: relPath, fullPath });
1385
1469
  }
1386
1470
  }
1387
- } catch (e33) {
1471
+ } catch (e34) {
1388
1472
  }
1389
1473
  }
1390
1474
  const publicDir = _path2.default.join(process.cwd(), "public");
@@ -1444,7 +1528,7 @@ async function handleScanStream() {
1444
1528
  h: metadata.height || 0,
1445
1529
  b: blurhash
1446
1530
  };
1447
- } catch (e34) {
1531
+ } catch (e35) {
1448
1532
  meta[imageKey] = { w: 0, h: 0 };
1449
1533
  }
1450
1534
  }