@gallop.software/studio 2.3.25 → 2.3.27

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.
@@ -95,6 +95,17 @@ function getFileEntries(meta) {
95
95
 
96
96
  // src/handlers/utils/files.ts
97
97
  import path2 from "path";
98
+ function slugifyFilename(filename) {
99
+ const ext = path2.extname(filename).toLowerCase();
100
+ const baseName = path2.basename(filename, path2.extname(filename));
101
+ const slugged = baseName.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
102
+ const finalSlug = slugged || "file";
103
+ return finalSlug + ext;
104
+ }
105
+ function slugifyFolderName(name) {
106
+ const slugged = name.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
107
+ return slugged || "folder";
108
+ }
98
109
  function isImageFile(filename) {
99
110
  const ext = path2.extname(filename).toLowerCase();
100
111
  return [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"].includes(ext);
@@ -375,6 +386,20 @@ async function deleteThumbnailsFromCdn(imageKey) {
375
386
  }
376
387
  }
377
388
  }
389
+ async function deleteOriginalFromCdn(imageKey) {
390
+ const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
391
+ if (!bucketName) throw new Error("R2 bucket not configured");
392
+ const r2 = getR2Client();
393
+ try {
394
+ await r2.send(
395
+ new DeleteObjectCommand({
396
+ Bucket: bucketName,
397
+ Key: imageKey.replace(/^\//, "")
398
+ })
399
+ );
400
+ } catch {
401
+ }
402
+ }
378
403
 
379
404
  // src/handlers/utils/response.ts
380
405
  function jsonResponse(data, init) {
@@ -1033,7 +1058,7 @@ async function handleUpload(request) {
1033
1058
  }
1034
1059
  const bytes = await file.arrayBuffer();
1035
1060
  const buffer = Buffer.from(bytes);
1036
- const fileName = file.name;
1061
+ const fileName = slugifyFilename(file.name);
1037
1062
  const ext = path6.extname(fileName).toLowerCase();
1038
1063
  const isImage = isImageFile(fileName);
1039
1064
  const isMedia = isMediaFile(fileName);
@@ -1225,7 +1250,7 @@ async function handleCreateFolder(request) {
1225
1250
  if (!name || typeof name !== "string") {
1226
1251
  return jsonResponse({ error: "Folder name is required" }, { status: 400 });
1227
1252
  }
1228
- const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "").trim();
1253
+ const sanitizedName = slugifyFolderName(name);
1229
1254
  if (!sanitizedName) {
1230
1255
  return jsonResponse({ error: "Invalid folder name" }, { status: 400 });
1231
1256
  }
@@ -1252,14 +1277,8 @@ async function handleRename(request) {
1252
1277
  if (!oldPath || !newName) {
1253
1278
  return jsonResponse({ error: "Path and new name are required" }, { status: 400 });
1254
1279
  }
1255
- const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, "").trim();
1256
- if (!sanitizedName) {
1257
- return jsonResponse({ error: "Invalid name" }, { status: 400 });
1258
- }
1259
1280
  const safePath = oldPath.replace(/\.\./g, "");
1260
1281
  const absoluteOldPath = getWorkspacePath(safePath);
1261
- const parentDir = path6.dirname(absoluteOldPath);
1262
- const absoluteNewPath = path6.join(parentDir, sanitizedName);
1263
1282
  if (!absoluteOldPath.startsWith(getPublicPath())) {
1264
1283
  return jsonResponse({ error: "Invalid path" }, { status: 400 });
1265
1284
  }
@@ -1268,14 +1287,20 @@ async function handleRename(request) {
1268
1287
  } catch {
1269
1288
  return jsonResponse({ error: "File or folder not found" }, { status: 404 });
1270
1289
  }
1290
+ const stats = await fs6.stat(absoluteOldPath);
1291
+ const isFile = stats.isFile();
1292
+ const isImage = isFile && isImageFile(path6.basename(oldPath));
1293
+ const sanitizedName = isFile ? slugifyFilename(newName) : slugifyFolderName(newName);
1294
+ if (!sanitizedName) {
1295
+ return jsonResponse({ error: "Invalid name" }, { status: 400 });
1296
+ }
1297
+ const parentDir = path6.dirname(absoluteOldPath);
1298
+ const absoluteNewPath = path6.join(parentDir, sanitizedName);
1271
1299
  try {
1272
1300
  await fs6.access(absoluteNewPath);
1273
1301
  return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
1274
1302
  } catch {
1275
1303
  }
1276
- const stats = await fs6.stat(absoluteOldPath);
1277
- const isFile = stats.isFile();
1278
- const isImage = isFile && isImageFile(path6.basename(oldPath));
1279
1304
  await fs6.rename(absoluteOldPath, absoluteNewPath);
1280
1305
  if (isImage) {
1281
1306
  const meta = await loadMeta();
@@ -1903,7 +1928,9 @@ async function handleReprocessStream(request) {
1903
1928
  const updatedEntry = await processImage(buffer, imageKey);
1904
1929
  if (isInOurR2) {
1905
1930
  updatedEntry.c = existingCdnIndex;
1931
+ await deleteOriginalFromCdn(imageKey);
1906
1932
  await deleteThumbnailsFromCdn(imageKey);
1933
+ await uploadOriginalToCdn(imageKey);
1907
1934
  await uploadToCdn(imageKey);
1908
1935
  await deleteLocalThumbnails(imageKey);
1909
1936
  try {
@@ -2363,6 +2390,26 @@ async function handleScanStream() {
2363
2390
  }
2364
2391
  continue;
2365
2392
  }
2393
+ const dirName = path8.dirname(relativePath);
2394
+ const originalFileName = path8.basename(relativePath);
2395
+ const sluggedFileName = slugifyFilename(originalFileName);
2396
+ if (sluggedFileName !== originalFileName) {
2397
+ const newRelativePath = dirName === "." ? sluggedFileName : `${dirName}/${sluggedFileName}`;
2398
+ const newFullPath = getPublicPath(newRelativePath);
2399
+ const newKey = "/" + newRelativePath;
2400
+ if (!meta[newKey] && !existingKeys.has(newKey)) {
2401
+ try {
2402
+ await fs8.mkdir(path8.dirname(newFullPath), { recursive: true });
2403
+ await fs8.rename(fullPath, newFullPath);
2404
+ renamed.push({ from: relativePath, to: newRelativePath });
2405
+ relativePath = newRelativePath;
2406
+ fullPath = newFullPath;
2407
+ imageKey = newKey;
2408
+ } catch (err) {
2409
+ console.error(`Failed to slugify ${relativePath}:`, err);
2410
+ }
2411
+ }
2412
+ }
2366
2413
  if (meta[imageKey]) {
2367
2414
  const ext = path8.extname(relativePath);
2368
2415
  const baseName = relativePath.slice(0, -ext.length);