@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.
- package/dist/{StudioUI-LGRI3HCE.mjs → StudioUI-73XFVFV4.mjs} +14 -16
- package/dist/StudioUI-73XFVFV4.mjs.map +1 -0
- package/dist/{StudioUI-HFZJ2YUQ.js → StudioUI-OVL65ONP.js} +14 -16
- package/dist/StudioUI-OVL65ONP.js.map +1 -0
- package/dist/handlers/index.js +151 -67
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/index.mjs +140 -56
- package/dist/handlers/index.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/StudioUI-HFZJ2YUQ.js.map +0 -1
- package/dist/StudioUI-LGRI3HCE.mjs.map +0 -1
package/dist/handlers/index.mjs
CHANGED
|
@@ -160,6 +160,31 @@ async function processImage(buffer, imageKey) {
|
|
|
160
160
|
import { promises as fs3 } from "fs";
|
|
161
161
|
import path4 from "path";
|
|
162
162
|
import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@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 = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1026
1051
|
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1027
1052
|
return NextResponse3.json(
|
|
1028
1053
|
{ error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." },
|
|
@@ -1035,6 +1060,7 @@ async function handleSync(request) {
|
|
|
1035
1060
|
return NextResponse3.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 S3Client2({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
await
|
|
1065
|
-
|
|
1066
|
-
|
|
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 getAllThumbnailPaths(imageKey)) {
|
|
1077
|
-
const localPath = path7.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 = path7.join(process.cwd(), "public", imageKey);
|
|
1078
1097
|
try {
|
|
1079
|
-
|
|
1080
|
-
await r2.send(
|
|
1081
|
-
new PutObjectCommand2({
|
|
1082
|
-
Bucket: bucketName,
|
|
1083
|
-
Key: thumbPath.replace(/^\//, ""),
|
|
1084
|
-
Body: fileBuffer,
|
|
1085
|
-
ContentType: getContentType(thumbPath)
|
|
1086
|
-
})
|
|
1087
|
-
);
|
|
1098
|
+
originalBuffer = await fs6.readFile(originalLocalPath);
|
|
1088
1099
|
} catch {
|
|
1100
|
+
errors.push(`Original file not found: ${imageKey}`);
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
await r2.send(
|
|
1105
|
+
new PutObjectCommand2({
|
|
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 getAllThumbnailPaths(imageKey)) {
|
|
1115
|
+
const localPath = path7.join(process.cwd(), "public", thumbPath);
|
|
1116
|
+
try {
|
|
1117
|
+
const fileBuffer = await fs6.readFile(localPath);
|
|
1118
|
+
await r2.send(
|
|
1119
|
+
new PutObjectCommand2({
|
|
1120
|
+
Bucket: bucketName,
|
|
1121
|
+
Key: thumbPath.replace(/^\//, ""),
|
|
1122
|
+
Body: fileBuffer,
|
|
1123
|
+
ContentType: getContentType(thumbPath)
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
urlsToPurge.push(`${publicUrl}${thumbPath}`);
|
|
1127
|
+
} catch {
|
|
1128
|
+
}
|
|
1089
1129
|
}
|
|
1090
1130
|
}
|
|
1091
1131
|
entry.c = cdnIndex;
|
|
1092
|
-
|
|
1093
|
-
const
|
|
1132
|
+
if (!isRemote) {
|
|
1133
|
+
const originalLocalPath = path7.join(process.cwd(), "public", imageKey);
|
|
1134
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1135
|
+
const localPath = path7.join(process.cwd(), "public", thumbPath);
|
|
1136
|
+
try {
|
|
1137
|
+
await fs6.unlink(localPath);
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1094
1141
|
try {
|
|
1095
|
-
await fs6.unlink(
|
|
1142
|
+
await fs6.unlink(originalLocalPath);
|
|
1096
1143
|
} catch {
|
|
1097
1144
|
}
|
|
1098
1145
|
}
|
|
1099
|
-
try {
|
|
1100
|
-
await fs6.unlink(originalLocalPath);
|
|
1101
|
-
} catch {
|
|
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 NextResponse3.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
|
|
1117
|
-
return NextResponse3.json({ error: "Failed to
|
|
1162
|
+
console.error("Failed to push:", error);
|
|
1163
|
+
return NextResponse3.json({ error: "Failed to push to CDN" }, { status: 500 });
|
|
1118
1164
|
}
|
|
1119
1165
|
}
|
|
1120
1166
|
async function handleReprocess(request) {
|
|
1167
|
+
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1121
1168
|
try {
|
|
1122
1169
|
const { imageKeys } = await request.json();
|
|
1123
1170
|
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1124
1171
|
return NextResponse3.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 = entry?.c !== void 0;
|
|
1134
1182
|
const existingCdnIndex = entry?.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 = path7.join(process.cwd(), "public", imageKey);
|
|
1136
1187
|
try {
|
|
1137
1188
|
buffer = await fs6.readFile(originalPath);
|
|
1138
1189
|
} catch {
|
|
1139
|
-
if (
|
|
1190
|
+
if (isInOurR2) {
|
|
1140
1191
|
buffer = await downloadFromCdn(imageKey);
|
|
1141
1192
|
const dir = path7.dirname(originalPath);
|
|
1142
1193
|
await fs6.mkdir(dir, { recursive: true });
|
|
1143
1194
|
await fs6.writeFile(originalPath, buffer);
|
|
1195
|
+
} else if (isRemote && existingCdnUrl) {
|
|
1196
|
+
const remoteUrl = `${existingCdnUrl}${imageKey}`;
|
|
1197
|
+
buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1198
|
+
const dir = path7.dirname(originalPath);
|
|
1199
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
1200
|
+
await fs6.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 (
|
|
1206
|
+
if (isInOurR2) {
|
|
1150
1207
|
updatedEntry.c = existingCdnIndex;
|
|
1151
1208
|
await uploadToCdn(imageKey);
|
|
1209
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
1210
|
+
urlsToPurge.push(`${publicUrl}${thumbPath}`);
|
|
1211
|
+
}
|
|
1152
1212
|
await deleteLocalThumbnails(imageKey);
|
|
1153
1213
|
try {
|
|
1154
1214
|
await fs6.unlink(originalPath);
|
|
1155
1215
|
} catch {
|
|
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 NextResponse3.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 = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\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 = path7.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 (
|
|
1287
|
+
if (isInOurR2) {
|
|
1219
1288
|
buffer = await downloadFromCdn(key);
|
|
1220
1289
|
const dir = path7.dirname(fullPath);
|
|
1221
1290
|
await fs6.mkdir(dir, { recursive: true });
|
|
1222
1291
|
await fs6.writeFile(fullPath, buffer);
|
|
1292
|
+
} else if (isRemote && existingCdnUrl) {
|
|
1293
|
+
const remoteUrl = `${existingCdnUrl}${key}`;
|
|
1294
|
+
buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
1295
|
+
const dir = path7.dirname(fullPath);
|
|
1296
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
1297
|
+
await fs6.writeFile(fullPath, buffer);
|
|
1223
1298
|
} else {
|
|
1224
1299
|
buffer = await fs6.readFile(fullPath);
|
|
1225
1300
|
}
|
|
@@ -1239,16 +1314,22 @@ 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
|
-
...
|
|
1325
|
+
...isInOurR2 ? { c: existingCdnIndex } : {}
|
|
1248
1326
|
};
|
|
1249
1327
|
}
|
|
1250
|
-
if (
|
|
1328
|
+
if (isInOurR2) {
|
|
1251
1329
|
await uploadToCdn(key);
|
|
1330
|
+
for (const thumbPath of getAllThumbnailPaths(key)) {
|
|
1331
|
+
urlsToPurge.push(`${publicUrl}${thumbPath}`);
|
|
1332
|
+
}
|
|
1252
1333
|
await deleteLocalThumbnails(key);
|
|
1253
1334
|
try {
|
|
1254
1335
|
await fs6.unlink(fullPath);
|
|
@@ -1273,17 +1354,17 @@ async function handleProcessAllStream() {
|
|
|
1273
1354
|
async function findOrphans(dir, relativePath = "") {
|
|
1274
1355
|
try {
|
|
1275
1356
|
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1276
|
-
for (const
|
|
1277
|
-
if (
|
|
1278
|
-
const
|
|
1279
|
-
const relPath = relativePath ? `${relativePath}/${
|
|
1280
|
-
if (
|
|
1281
|
-
await findOrphans(
|
|
1282
|
-
} else if (isImageFile(
|
|
1357
|
+
for (const fsEntry of entries) {
|
|
1358
|
+
if (fsEntry.name.startsWith(".")) continue;
|
|
1359
|
+
const entryFullPath = path7.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 fs6.unlink(
|
|
1367
|
+
await fs6.unlink(entryFullPath);
|
|
1287
1368
|
orphansRemoved.push(publicPath);
|
|
1288
1369
|
} catch (err) {
|
|
1289
1370
|
console.error(`Failed to remove orphan ${publicPath}:`, err);
|
|
@@ -1303,9 +1384,9 @@ async function handleProcessAllStream() {
|
|
|
1303
1384
|
try {
|
|
1304
1385
|
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1305
1386
|
let isEmpty = true;
|
|
1306
|
-
for (const
|
|
1307
|
-
if (
|
|
1308
|
-
const subDirEmpty = await removeEmptyDirs(path7.join(dir,
|
|
1387
|
+
for (const fsEntry of entries) {
|
|
1388
|
+
if (fsEntry.isDirectory()) {
|
|
1389
|
+
const subDirEmpty = await removeEmptyDirs(path7.join(dir, fsEntry.name));
|
|
1309
1390
|
if (!subDirEmpty) isEmpty = false;
|
|
1310
1391
|
} else {
|
|
1311
1392
|
isEmpty = false;
|
|
@@ -1324,6 +1405,9 @@ async function handleProcessAllStream() {
|
|
|
1324
1405
|
} catch {
|
|
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,
|