@gallop.software/studio 0.1.59 → 0.1.60

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/handlers.mjs CHANGED
@@ -31,6 +31,9 @@ async function GET(request) {
31
31
  if (route === "search") {
32
32
  return handleSearch(request);
33
33
  }
34
+ if (route === "list-folders") {
35
+ return handleListFolders();
36
+ }
34
37
  return NextResponse.json({ error: "Not found" }, { status: 404 });
35
38
  }
36
39
  async function POST(request) {
@@ -54,6 +57,15 @@ async function POST(request) {
54
57
  if (route === "process-all") {
55
58
  return handleProcessAllStream();
56
59
  }
60
+ if (route === "create-folder") {
61
+ return handleCreateFolder(request);
62
+ }
63
+ if (route === "rename") {
64
+ return handleRename(request);
65
+ }
66
+ if (route === "move") {
67
+ return handleMove(request);
68
+ }
57
69
  return NextResponse.json({ error: "Not found" }, { status: 404 });
58
70
  }
59
71
  async function DELETE(request) {
@@ -1069,6 +1081,237 @@ async function deleteLocalFiles(entry) {
1069
1081
  }
1070
1082
  }
1071
1083
  }
1084
+ async function handleCreateFolder(request) {
1085
+ try {
1086
+ const { parentPath, name } = await request.json();
1087
+ if (!name || typeof name !== "string") {
1088
+ return NextResponse.json({ error: "Folder name is required" }, { status: 400 });
1089
+ }
1090
+ const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "").trim();
1091
+ if (!sanitizedName) {
1092
+ return NextResponse.json({ error: "Invalid folder name" }, { status: 400 });
1093
+ }
1094
+ const safePath = (parentPath || "public").replace(/\.\./g, "");
1095
+ const folderPath = path.join(process.cwd(), safePath, sanitizedName);
1096
+ if (!folderPath.startsWith(path.join(process.cwd(), "public"))) {
1097
+ return NextResponse.json({ error: "Invalid path" }, { status: 400 });
1098
+ }
1099
+ try {
1100
+ await fs.access(folderPath);
1101
+ return NextResponse.json({ error: "A folder with this name already exists" }, { status: 400 });
1102
+ } catch {
1103
+ }
1104
+ await fs.mkdir(folderPath, { recursive: true });
1105
+ return NextResponse.json({ success: true, path: path.join(safePath, sanitizedName) });
1106
+ } catch (error) {
1107
+ console.error("Failed to create folder:", error);
1108
+ return NextResponse.json({ error: "Failed to create folder" }, { status: 500 });
1109
+ }
1110
+ }
1111
+ async function handleRename(request) {
1112
+ try {
1113
+ const { oldPath, newName } = await request.json();
1114
+ if (!oldPath || !newName) {
1115
+ return NextResponse.json({ error: "Path and new name are required" }, { status: 400 });
1116
+ }
1117
+ const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, "").trim();
1118
+ if (!sanitizedName) {
1119
+ return NextResponse.json({ error: "Invalid name" }, { status: 400 });
1120
+ }
1121
+ const safePath = oldPath.replace(/\.\./g, "");
1122
+ const absoluteOldPath = path.join(process.cwd(), safePath);
1123
+ const parentDir = path.dirname(absoluteOldPath);
1124
+ const absoluteNewPath = path.join(parentDir, sanitizedName);
1125
+ if (!absoluteOldPath.startsWith(path.join(process.cwd(), "public"))) {
1126
+ return NextResponse.json({ error: "Invalid path" }, { status: 400 });
1127
+ }
1128
+ try {
1129
+ await fs.access(absoluteOldPath);
1130
+ } catch {
1131
+ return NextResponse.json({ error: "File or folder not found" }, { status: 404 });
1132
+ }
1133
+ try {
1134
+ await fs.access(absoluteNewPath);
1135
+ return NextResponse.json({ error: "An item with this name already exists" }, { status: 400 });
1136
+ } catch {
1137
+ }
1138
+ const stats = await fs.stat(absoluteOldPath);
1139
+ const isFile = stats.isFile();
1140
+ const isImage = isFile && isImageFile(path.basename(oldPath));
1141
+ await fs.rename(absoluteOldPath, absoluteNewPath);
1142
+ if (isImage) {
1143
+ const meta = await loadMeta();
1144
+ const oldRelativePath = safePath.replace(/^public\//, "");
1145
+ const newRelativePath = path.join(path.dirname(oldRelativePath), sanitizedName);
1146
+ for (const [key, entry] of Object.entries(meta.images)) {
1147
+ if (entry.original.path === `/${oldRelativePath}`) {
1148
+ entry.original.path = `/${newRelativePath}`;
1149
+ const oldExt = path.extname(path.basename(oldPath));
1150
+ const oldBaseName = path.basename(oldPath, oldExt);
1151
+ const newExt = path.extname(sanitizedName);
1152
+ const newBaseName = path.basename(sanitizedName, newExt);
1153
+ const oldDirRelative = path.dirname(oldRelativePath);
1154
+ const thumbnailDir = path.join(process.cwd(), "public", "images", oldDirRelative);
1155
+ for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {
1156
+ const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`;
1157
+ const oldThumbName = `${oldBaseName}${suffix}${oldExt === ".png" ? ".png" : ".jpg"}`;
1158
+ const newThumbName = `${newBaseName}${suffix}${newExt === ".png" ? ".png" : ".jpg"}`;
1159
+ const oldThumbPath = path.join(thumbnailDir, oldThumbName);
1160
+ const newThumbPath = path.join(thumbnailDir, newThumbName);
1161
+ try {
1162
+ await fs.rename(oldThumbPath, newThumbPath);
1163
+ sizeData.path = `/images/${oldDirRelative}/${newThumbName}`.replace(/\/+/g, "/");
1164
+ } catch {
1165
+ }
1166
+ }
1167
+ const newKey = `/${newRelativePath}`;
1168
+ delete meta.images[key];
1169
+ meta.images[newKey] = entry;
1170
+ break;
1171
+ }
1172
+ }
1173
+ await saveMeta(meta);
1174
+ }
1175
+ const newPath = path.join(path.dirname(safePath), sanitizedName);
1176
+ return NextResponse.json({ success: true, newPath });
1177
+ } catch (error) {
1178
+ console.error("Failed to rename:", error);
1179
+ return NextResponse.json({ error: "Failed to rename" }, { status: 500 });
1180
+ }
1181
+ }
1182
+ async function handleMove(request) {
1183
+ try {
1184
+ const { paths, destination } = await request.json();
1185
+ if (!paths || !Array.isArray(paths) || paths.length === 0) {
1186
+ return NextResponse.json({ error: "Paths are required" }, { status: 400 });
1187
+ }
1188
+ if (!destination || typeof destination !== "string") {
1189
+ return NextResponse.json({ error: "Destination is required" }, { status: 400 });
1190
+ }
1191
+ const safeDestination = destination.replace(/\.\./g, "");
1192
+ const absoluteDestination = path.join(process.cwd(), safeDestination);
1193
+ if (!absoluteDestination.startsWith(path.join(process.cwd(), "public"))) {
1194
+ return NextResponse.json({ error: "Invalid destination" }, { status: 400 });
1195
+ }
1196
+ if (safeDestination === "public/images" || safeDestination.startsWith("public/images/")) {
1197
+ return NextResponse.json({ error: "Cannot move items to the protected images folder" }, { status: 400 });
1198
+ }
1199
+ try {
1200
+ const destStats = await fs.stat(absoluteDestination);
1201
+ if (!destStats.isDirectory()) {
1202
+ return NextResponse.json({ error: "Destination is not a folder" }, { status: 400 });
1203
+ }
1204
+ } catch {
1205
+ return NextResponse.json({ error: "Destination folder not found" }, { status: 404 });
1206
+ }
1207
+ const moved = [];
1208
+ const errors = [];
1209
+ const meta = await loadMeta();
1210
+ let metaChanged = false;
1211
+ for (const itemPath of paths) {
1212
+ const safePath = itemPath.replace(/\.\./g, "");
1213
+ const absolutePath = path.join(process.cwd(), safePath);
1214
+ const itemName = path.basename(safePath);
1215
+ const newAbsolutePath = path.join(absoluteDestination, itemName);
1216
+ if (absoluteDestination.startsWith(absolutePath + path.sep)) {
1217
+ errors.push(`Cannot move ${itemName} into itself`);
1218
+ continue;
1219
+ }
1220
+ try {
1221
+ await fs.access(absolutePath);
1222
+ } catch {
1223
+ errors.push(`${itemName} not found`);
1224
+ continue;
1225
+ }
1226
+ try {
1227
+ await fs.access(newAbsolutePath);
1228
+ errors.push(`${itemName} already exists in destination`);
1229
+ continue;
1230
+ } catch {
1231
+ }
1232
+ try {
1233
+ await fs.rename(absolutePath, newAbsolutePath);
1234
+ const stats = await fs.stat(newAbsolutePath);
1235
+ if (stats.isFile() && isImageFile(itemName)) {
1236
+ const oldRelativePath = safePath.replace(/^public\//, "");
1237
+ const newRelativePath = path.join(safeDestination.replace(/^public\//, ""), itemName);
1238
+ for (const [key, entry] of Object.entries(meta.images)) {
1239
+ if (entry.original.path === `/${oldRelativePath}`) {
1240
+ entry.original.path = `/${newRelativePath}`;
1241
+ const oldDir = path.dirname(oldRelativePath);
1242
+ const newDir = path.dirname(newRelativePath);
1243
+ const ext = path.extname(itemName);
1244
+ const baseName = path.basename(itemName, ext);
1245
+ const oldThumbDir = path.join(process.cwd(), "public", "images", oldDir);
1246
+ const newThumbDir = path.join(process.cwd(), "public", "images", newDir);
1247
+ await fs.mkdir(newThumbDir, { recursive: true });
1248
+ for (const [sizeName, sizeData] of Object.entries(entry.sizes)) {
1249
+ const suffix = DEFAULT_SIZES[sizeName]?.suffix || `-${sizeName}`;
1250
+ const thumbName = `${baseName}${suffix}${ext === ".png" ? ".png" : ".jpg"}`;
1251
+ const oldThumbPath = path.join(oldThumbDir, thumbName);
1252
+ const newThumbPath = path.join(newThumbDir, thumbName);
1253
+ try {
1254
+ await fs.rename(oldThumbPath, newThumbPath);
1255
+ sizeData.path = `/images/${newDir}/${thumbName}`.replace(/\/+/g, "/");
1256
+ } catch {
1257
+ }
1258
+ }
1259
+ const newKey = `/${newRelativePath}`;
1260
+ delete meta.images[key];
1261
+ meta.images[newKey] = entry;
1262
+ metaChanged = true;
1263
+ break;
1264
+ }
1265
+ }
1266
+ }
1267
+ moved.push(itemPath);
1268
+ } catch (error) {
1269
+ errors.push(`Failed to move ${itemName}`);
1270
+ }
1271
+ }
1272
+ if (metaChanged) {
1273
+ await saveMeta(meta);
1274
+ }
1275
+ return NextResponse.json({
1276
+ success: errors.length === 0,
1277
+ moved,
1278
+ errors: errors.length > 0 ? errors : void 0
1279
+ });
1280
+ } catch (error) {
1281
+ console.error("Failed to move:", error);
1282
+ return NextResponse.json({ error: "Failed to move items" }, { status: 500 });
1283
+ }
1284
+ }
1285
+ async function handleListFolders() {
1286
+ try {
1287
+ const publicDir = path.join(process.cwd(), "public");
1288
+ const folders = [];
1289
+ async function scanDir(dir, relativePath, depth) {
1290
+ try {
1291
+ const entries = await fs.readdir(dir, { withFileTypes: true });
1292
+ for (const entry of entries) {
1293
+ if (!entry.isDirectory()) continue;
1294
+ if (entry.name.startsWith(".")) continue;
1295
+ if (relativePath === "" && entry.name === "images") continue;
1296
+ const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
1297
+ folders.push({
1298
+ path: `public/${folderRelativePath}`,
1299
+ name: entry.name,
1300
+ depth
1301
+ });
1302
+ await scanDir(path.join(dir, entry.name), folderRelativePath, depth + 1);
1303
+ }
1304
+ } catch {
1305
+ }
1306
+ }
1307
+ folders.push({ path: "public", name: "public", depth: 0 });
1308
+ await scanDir(publicDir, "", 1);
1309
+ return NextResponse.json({ folders });
1310
+ } catch (error) {
1311
+ console.error("Failed to list folders:", error);
1312
+ return NextResponse.json({ error: "Failed to list folders" }, { status: 500 });
1313
+ }
1314
+ }
1072
1315
  export {
1073
1316
  DELETE,
1074
1317
  GET,