@gallop.software/studio 2.0.7 → 2.1.1

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.
@@ -1,14 +1,14 @@
1
- import {
2
- getAllThumbnailPaths,
3
- getThumbnailPath,
4
- isProcessed
5
- } from "../chunk-TRYWHLJ2.mjs";
1
+ // @ts-nocheck
6
2
 
7
- // src/handlers/index.ts
8
- import { NextResponse as NextResponse6 } from "next/server";
3
+ // src/server/index.ts
4
+ import express from "express";
5
+ import { resolve, join } from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname } from "path";
8
+ import { existsSync, readFileSync } from "fs";
9
+ import { config as loadEnv } from "dotenv";
9
10
 
10
11
  // src/handlers/list.ts
11
- import { NextResponse } from "next/server";
12
12
  import { promises as fs4 } from "fs";
13
13
  import path4 from "path";
14
14
 
@@ -197,6 +197,34 @@ async function processImage(buffer, imageKey) {
197
197
  // src/handlers/utils/cdn.ts
198
198
  import { promises as fs3 } from "fs";
199
199
  import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
200
+
201
+ // src/types.ts
202
+ function getThumbnailPath(originalPath, size) {
203
+ if (size === "full") {
204
+ const ext2 = originalPath.match(/\.\w+$/)?.[0] || ".jpg";
205
+ const base2 = originalPath.replace(/\.\w+$/, "");
206
+ const outputExt2 = ext2.toLowerCase() === ".png" ? ".png" : ".jpg";
207
+ return `/images${base2}${outputExt2}`;
208
+ }
209
+ const ext = originalPath.match(/\.\w+$/)?.[0] || ".jpg";
210
+ const base = originalPath.replace(/\.\w+$/, "");
211
+ const outputExt = ext.toLowerCase() === ".png" ? ".png" : ".jpg";
212
+ return `/images${base}-${size}${outputExt}`;
213
+ }
214
+ function getAllThumbnailPaths(originalPath) {
215
+ return [
216
+ getThumbnailPath(originalPath, "full"),
217
+ getThumbnailPath(originalPath, "lg"),
218
+ getThumbnailPath(originalPath, "md"),
219
+ getThumbnailPath(originalPath, "sm")
220
+ ];
221
+ }
222
+ function isProcessed(entry) {
223
+ if (!entry) return false;
224
+ return !!(entry.f || entry.lg || entry.md || entry.sm);
225
+ }
226
+
227
+ // src/handlers/utils/cdn.ts
200
228
  async function purgeCloudflareCache(urls) {
201
229
  const zoneId = process.env.CLOUDFLARE_ZONE_ID;
202
230
  const apiToken = process.env.CLOUDFLARE_API_TOKEN;
@@ -258,7 +286,7 @@ async function downloadFromCdn(originalPath) {
258
286
  } catch (error) {
259
287
  lastError = error;
260
288
  if (attempt < maxRetries - 1) {
261
- await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
289
+ await new Promise((resolve2) => setTimeout(resolve2, 500 * (attempt + 1)));
262
290
  }
263
291
  }
264
292
  }
@@ -307,7 +335,7 @@ async function downloadFromRemoteUrl(url) {
307
335
  } catch (error) {
308
336
  lastError = error;
309
337
  if (attempt < maxRetries - 1) {
310
- await new Promise((resolve) => setTimeout(resolve, 500 * (attempt + 1)));
338
+ await new Promise((resolve2) => setTimeout(resolve2, 500 * (attempt + 1)));
311
339
  }
312
340
  }
313
341
  }
@@ -372,6 +400,18 @@ async function deleteThumbnailsFromCdn(imageKey) {
372
400
  }
373
401
  }
374
402
 
403
+ // src/handlers/utils/response.ts
404
+ function jsonResponse(data, init) {
405
+ const headers = new Headers({
406
+ "Content-Type": "application/json",
407
+ ...init?.headers
408
+ });
409
+ return new Response(JSON.stringify(data), {
410
+ status: init?.status ?? 200,
411
+ headers
412
+ });
413
+ }
414
+
375
415
  // src/handlers/list.ts
376
416
  function getExistingThumbnails(originalPath, entry) {
377
417
  const thumbnails = [];
@@ -410,7 +450,7 @@ function countFileTypes(folderPrefix, fileEntries, cdnUrls, r2PublicUrl) {
410
450
  return { cloudCount, remoteCount, localCount };
411
451
  }
412
452
  async function handleList(request) {
413
- const searchParams = request.nextUrl.searchParams;
453
+ const searchParams = new URL(request.url).searchParams;
414
454
  const requestedPath = searchParams.get("path") || "public";
415
455
  try {
416
456
  const meta = await loadMeta();
@@ -515,7 +555,7 @@ async function handleList(request) {
515
555
  }
516
556
  }
517
557
  }
518
- return NextResponse.json({ items });
558
+ return jsonResponse({ items });
519
559
  }
520
560
  const absoluteDir = getWorkspacePath(requestedPath);
521
561
  try {
@@ -580,7 +620,7 @@ async function handleList(request) {
580
620
  }
581
621
  }
582
622
  if (fileEntries.length === 0 && items.length === 0) {
583
- return NextResponse.json({ items: [], isEmpty: true });
623
+ return jsonResponse({ items: [], isEmpty: true });
584
624
  }
585
625
  for (const [key, entry] of fileEntries) {
586
626
  if (!key.startsWith(pathPrefix) && pathPrefix !== "/") continue;
@@ -671,17 +711,17 @@ async function handleList(request) {
671
711
  });
672
712
  }
673
713
  }
674
- return NextResponse.json({ items });
714
+ return jsonResponse({ items });
675
715
  } catch (error) {
676
716
  console.error("Failed to list directory:", error);
677
- return NextResponse.json({ error: "Failed to list directory" }, { status: 500 });
717
+ return jsonResponse({ error: "Failed to list directory" }, { status: 500 });
678
718
  }
679
719
  }
680
720
  async function handleSearch(request) {
681
- const searchParams = request.nextUrl.searchParams;
721
+ const searchParams = new URL(request.url).searchParams;
682
722
  const query = searchParams.get("q")?.toLowerCase() || "";
683
723
  if (query.length < 2) {
684
- return NextResponse.json({ items: [] });
724
+ return jsonResponse({ items: [] });
685
725
  }
686
726
  try {
687
727
  const meta = await loadMeta();
@@ -741,10 +781,10 @@ async function handleSearch(request) {
741
781
  dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0
742
782
  });
743
783
  }
744
- return NextResponse.json({ items });
784
+ return jsonResponse({ items });
745
785
  } catch (error) {
746
786
  console.error("Failed to search:", error);
747
- return NextResponse.json({ error: "Failed to search" }, { status: 500 });
787
+ return jsonResponse({ error: "Failed to search" }, { status: 500 });
748
788
  }
749
789
  }
750
790
  async function handleListFolders() {
@@ -787,10 +827,10 @@ async function handleListFolders() {
787
827
  depth
788
828
  });
789
829
  }
790
- return NextResponse.json({ folders });
830
+ return jsonResponse({ folders });
791
831
  } catch (error) {
792
832
  console.error("Failed to list folders:", error);
793
- return NextResponse.json({ error: "Failed to list folders" }, { status: 500 });
833
+ return jsonResponse({ error: "Failed to list folders" }, { status: 500 });
794
834
  }
795
835
  }
796
836
  async function handleCountImages() {
@@ -804,21 +844,21 @@ async function handleCountImages() {
804
844
  allImages.push(key.slice(1));
805
845
  }
806
846
  }
807
- return NextResponse.json({
847
+ return jsonResponse({
808
848
  count: allImages.length,
809
849
  images: allImages
810
850
  });
811
851
  } catch (error) {
812
852
  console.error("Failed to count images:", error);
813
- return NextResponse.json({ error: "Failed to count images" }, { status: 500 });
853
+ return jsonResponse({ error: "Failed to count images" }, { status: 500 });
814
854
  }
815
855
  }
816
856
  async function handleFolderImages(request) {
817
857
  try {
818
- const searchParams = request.nextUrl.searchParams;
858
+ const searchParams = new URL(request.url).searchParams;
819
859
  const foldersParam = searchParams.get("folders");
820
860
  if (!foldersParam) {
821
- return NextResponse.json({ error: "No folders provided" }, { status: 400 });
861
+ return jsonResponse({ error: "No folders provided" }, { status: 400 });
822
862
  }
823
863
  const folders = foldersParam.split(",");
824
864
  const meta = await loadMeta();
@@ -836,19 +876,18 @@ async function handleFolderImages(request) {
836
876
  }
837
877
  }
838
878
  }
839
- return NextResponse.json({
879
+ return jsonResponse({
840
880
  count: allFiles.length,
841
881
  images: allFiles
842
882
  // Keep as 'images' for backwards compatibility
843
883
  });
844
884
  } catch (error) {
845
885
  console.error("Failed to get folder files:", error);
846
- return NextResponse.json({ error: "Failed to get folder files" }, { status: 500 });
886
+ return jsonResponse({ error: "Failed to get folder files" }, { status: 500 });
847
887
  }
848
888
  }
849
889
 
850
890
  // src/handlers/files.ts
851
- import { NextResponse as NextResponse2 } from "next/server";
852
891
  import { promises as fs5 } from "fs";
853
892
  import path5 from "path";
854
893
  import sharp2 from "sharp";
@@ -858,7 +897,7 @@ async function handleUpload(request) {
858
897
  const file = formData.get("file");
859
898
  const targetPath = formData.get("path") || "public";
860
899
  if (!file) {
861
- return NextResponse2.json({ error: "No file provided" }, { status: 400 });
900
+ return jsonResponse({ error: "No file provided" }, { status: 400 });
862
901
  }
863
902
  const bytes = await file.arrayBuffer();
864
903
  const buffer = Buffer.from(bytes);
@@ -874,7 +913,7 @@ async function handleUpload(request) {
874
913
  relativeDir = targetPath.replace("public/", "");
875
914
  }
876
915
  if (relativeDir === "images" || relativeDir.startsWith("images/")) {
877
- return NextResponse2.json(
916
+ return jsonResponse(
878
917
  { error: "Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically." },
879
918
  { status: 400 }
880
919
  );
@@ -897,7 +936,7 @@ async function handleUpload(request) {
897
936
  await fs5.mkdir(uploadDir, { recursive: true });
898
937
  await fs5.writeFile(path5.join(uploadDir, actualFileName), buffer);
899
938
  if (!isMedia) {
900
- return NextResponse2.json({
939
+ return jsonResponse({
901
940
  success: true,
902
941
  message: "File uploaded (not a media file)",
903
942
  path: `public/${relativeDir ? relativeDir + "/" : ""}${actualFileName}`
@@ -916,7 +955,7 @@ async function handleUpload(request) {
916
955
  meta[imageKey] = {};
917
956
  }
918
957
  await saveMeta(meta);
919
- return NextResponse2.json({
958
+ return jsonResponse({
920
959
  success: true,
921
960
  imageKey,
922
961
  message: 'File uploaded. Run "Process Images" to generate thumbnails.'
@@ -924,14 +963,14 @@ async function handleUpload(request) {
924
963
  } catch (error) {
925
964
  console.error("Failed to upload:", error);
926
965
  const message = error instanceof Error ? error.message : "Unknown error";
927
- return NextResponse2.json({ error: `Failed to upload file: ${message}` }, { status: 500 });
966
+ return jsonResponse({ error: `Failed to upload file: ${message}` }, { status: 500 });
928
967
  }
929
968
  }
930
969
  async function handleDelete(request) {
931
970
  try {
932
971
  const { paths } = await request.json();
933
972
  if (!paths || !Array.isArray(paths) || paths.length === 0) {
934
- return NextResponse2.json({ error: "No paths provided" }, { status: 400 });
973
+ return jsonResponse({ error: "No paths provided" }, { status: 400 });
935
974
  }
936
975
  const meta = await loadMeta();
937
976
  const deleted = [];
@@ -1007,68 +1046,68 @@ async function handleDelete(request) {
1007
1046
  }
1008
1047
  }
1009
1048
  await saveMeta(meta);
1010
- return NextResponse2.json({
1049
+ return jsonResponse({
1011
1050
  success: true,
1012
1051
  deleted,
1013
1052
  errors: errors.length > 0 ? errors : void 0
1014
1053
  });
1015
1054
  } catch (error) {
1016
1055
  console.error("Failed to delete:", error);
1017
- return NextResponse2.json({ error: "Failed to delete files" }, { status: 500 });
1056
+ return jsonResponse({ error: "Failed to delete files" }, { status: 500 });
1018
1057
  }
1019
1058
  }
1020
1059
  async function handleCreateFolder(request) {
1021
1060
  try {
1022
1061
  const { parentPath, name } = await request.json();
1023
1062
  if (!name || typeof name !== "string") {
1024
- return NextResponse2.json({ error: "Folder name is required" }, { status: 400 });
1063
+ return jsonResponse({ error: "Folder name is required" }, { status: 400 });
1025
1064
  }
1026
1065
  const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "").trim();
1027
1066
  if (!sanitizedName) {
1028
- return NextResponse2.json({ error: "Invalid folder name" }, { status: 400 });
1067
+ return jsonResponse({ error: "Invalid folder name" }, { status: 400 });
1029
1068
  }
1030
1069
  const safePath = (parentPath || "public").replace(/\.\./g, "");
1031
1070
  const folderPath = getWorkspacePath(safePath, sanitizedName);
1032
1071
  if (!folderPath.startsWith(getPublicPath())) {
1033
- return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
1072
+ return jsonResponse({ error: "Invalid path" }, { status: 400 });
1034
1073
  }
1035
1074
  try {
1036
1075
  await fs5.access(folderPath);
1037
- return NextResponse2.json({ error: "A folder with this name already exists" }, { status: 400 });
1076
+ return jsonResponse({ error: "A folder with this name already exists" }, { status: 400 });
1038
1077
  } catch {
1039
1078
  }
1040
1079
  await fs5.mkdir(folderPath, { recursive: true });
1041
- return NextResponse2.json({ success: true, path: path5.join(safePath, sanitizedName) });
1080
+ return jsonResponse({ success: true, path: path5.join(safePath, sanitizedName) });
1042
1081
  } catch (error) {
1043
1082
  console.error("Failed to create folder:", error);
1044
- return NextResponse2.json({ error: "Failed to create folder" }, { status: 500 });
1083
+ return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
1045
1084
  }
1046
1085
  }
1047
1086
  async function handleRename(request) {
1048
1087
  try {
1049
1088
  const { oldPath, newName } = await request.json();
1050
1089
  if (!oldPath || !newName) {
1051
- return NextResponse2.json({ error: "Path and new name are required" }, { status: 400 });
1090
+ return jsonResponse({ error: "Path and new name are required" }, { status: 400 });
1052
1091
  }
1053
1092
  const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, "").trim();
1054
1093
  if (!sanitizedName) {
1055
- return NextResponse2.json({ error: "Invalid name" }, { status: 400 });
1094
+ return jsonResponse({ error: "Invalid name" }, { status: 400 });
1056
1095
  }
1057
1096
  const safePath = oldPath.replace(/\.\./g, "");
1058
1097
  const absoluteOldPath = getWorkspacePath(safePath);
1059
1098
  const parentDir = path5.dirname(absoluteOldPath);
1060
1099
  const absoluteNewPath = path5.join(parentDir, sanitizedName);
1061
1100
  if (!absoluteOldPath.startsWith(getPublicPath())) {
1062
- return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
1101
+ return jsonResponse({ error: "Invalid path" }, { status: 400 });
1063
1102
  }
1064
1103
  try {
1065
1104
  await fs5.access(absoluteOldPath);
1066
1105
  } catch {
1067
- return NextResponse2.json({ error: "File or folder not found" }, { status: 404 });
1106
+ return jsonResponse({ error: "File or folder not found" }, { status: 404 });
1068
1107
  }
1069
1108
  try {
1070
1109
  await fs5.access(absoluteNewPath);
1071
- return NextResponse2.json({ error: "An item with this name already exists" }, { status: 400 });
1110
+ return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
1072
1111
  } catch {
1073
1112
  }
1074
1113
  const stats = await fs5.stat(absoluteOldPath);
@@ -1100,10 +1139,10 @@ async function handleRename(request) {
1100
1139
  await saveMeta(meta);
1101
1140
  }
1102
1141
  const newPath = path5.join(path5.dirname(safePath), sanitizedName);
1103
- return NextResponse2.json({ success: true, newPath });
1142
+ return jsonResponse({ success: true, newPath });
1104
1143
  } catch (error) {
1105
1144
  console.error("Failed to rename:", error);
1106
- return NextResponse2.json({ error: "Failed to rename" }, { status: 500 });
1145
+ return jsonResponse({ error: "Failed to rename" }, { status: 500 });
1107
1146
  }
1108
1147
  }
1109
1148
  async function handleMoveStream(request) {
@@ -1287,7 +1326,6 @@ async function handleMoveStream(request) {
1287
1326
  }
1288
1327
 
1289
1328
  // src/handlers/images.ts
1290
- import { NextResponse as NextResponse3 } from "next/server";
1291
1329
  import { promises as fs6 } from "fs";
1292
1330
  import path6 from "path";
1293
1331
  import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
@@ -1298,7 +1336,7 @@ async function handleSync(request) {
1298
1336
  const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
1299
1337
  const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
1300
1338
  if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
1301
- return NextResponse3.json(
1339
+ return jsonResponse(
1302
1340
  { error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." },
1303
1341
  { status: 400 }
1304
1342
  );
@@ -1306,7 +1344,7 @@ async function handleSync(request) {
1306
1344
  try {
1307
1345
  const { imageKeys } = await request.json();
1308
1346
  if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1309
- return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
1347
+ return jsonResponse({ error: "No image keys provided" }, { status: 400 });
1310
1348
  }
1311
1349
  const meta = await loadMeta();
1312
1350
  const cdnUrls = getCdnUrls(meta);
@@ -1401,91 +1439,14 @@ async function handleSync(request) {
1401
1439
  if (urlsToPurge.length > 0) {
1402
1440
  await purgeCloudflareCache(urlsToPurge);
1403
1441
  }
1404
- return NextResponse3.json({
1442
+ return jsonResponse({
1405
1443
  success: true,
1406
1444
  pushed,
1407
1445
  errors: errors.length > 0 ? errors : void 0
1408
1446
  });
1409
1447
  } catch (error) {
1410
1448
  console.error("Failed to push:", error);
1411
- return NextResponse3.json({ error: "Failed to push to CDN" }, { status: 500 });
1412
- }
1413
- }
1414
- async function handleReprocess(request) {
1415
- const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
1416
- try {
1417
- const { imageKeys } = await request.json();
1418
- if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1419
- return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
1420
- }
1421
- const meta = await loadMeta();
1422
- const cdnUrls = getCdnUrls(meta);
1423
- const processed = [];
1424
- const errors = [];
1425
- const urlsToPurge = [];
1426
- for (let imageKey of imageKeys) {
1427
- if (!imageKey.startsWith("/")) {
1428
- imageKey = `/${imageKey}`;
1429
- }
1430
- try {
1431
- let buffer;
1432
- const entry = getMetaEntry(meta, imageKey);
1433
- const existingCdnIndex = entry?.c;
1434
- const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1435
- const isInOurR2 = existingCdnUrl === publicUrl;
1436
- const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1437
- const originalPath = getPublicPath(imageKey);
1438
- try {
1439
- buffer = await fs6.readFile(originalPath);
1440
- } catch {
1441
- if (isInOurR2) {
1442
- buffer = await downloadFromCdn(imageKey);
1443
- const dir = path6.dirname(originalPath);
1444
- await fs6.mkdir(dir, { recursive: true });
1445
- await fs6.writeFile(originalPath, buffer);
1446
- } else if (isRemote && existingCdnUrl) {
1447
- const remoteUrl = `${existingCdnUrl}${imageKey}`;
1448
- buffer = await downloadFromRemoteUrl(remoteUrl);
1449
- const dir = path6.dirname(originalPath);
1450
- await fs6.mkdir(dir, { recursive: true });
1451
- await fs6.writeFile(originalPath, buffer);
1452
- } else {
1453
- throw new Error(`File not found: ${imageKey}`);
1454
- }
1455
- }
1456
- const updatedEntry = await processImage(buffer, imageKey);
1457
- if (isInOurR2) {
1458
- updatedEntry.c = existingCdnIndex;
1459
- await uploadToCdn(imageKey);
1460
- for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1461
- urlsToPurge.push(`${publicUrl}${thumbPath}`);
1462
- }
1463
- await deleteLocalThumbnails(imageKey);
1464
- try {
1465
- await fs6.unlink(originalPath);
1466
- } catch {
1467
- }
1468
- } else if (isRemote) {
1469
- }
1470
- meta[imageKey] = updatedEntry;
1471
- processed.push(imageKey);
1472
- } catch (error) {
1473
- console.error(`Failed to reprocess ${imageKey}:`, error);
1474
- errors.push(imageKey);
1475
- }
1476
- }
1477
- await saveMeta(meta);
1478
- if (urlsToPurge.length > 0) {
1479
- await purgeCloudflareCache(urlsToPurge);
1480
- }
1481
- return NextResponse3.json({
1482
- success: true,
1483
- processed,
1484
- errors: errors.length > 0 ? errors : void 0
1485
- });
1486
- } catch (error) {
1487
- console.error("Failed to reprocess:", error);
1488
- return NextResponse3.json({ error: "Failed to reprocess images" }, { status: 500 });
1449
+ return jsonResponse({ error: "Failed to push to CDN" }, { status: 500 });
1489
1450
  }
1490
1451
  }
1491
1452
  async function handleUnprocessStream(request) {
@@ -1496,10 +1457,10 @@ async function handleUnprocessStream(request) {
1496
1457
  const body = await request.json();
1497
1458
  imageKeys = body.imageKeys;
1498
1459
  if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1499
- return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
1460
+ return jsonResponse({ error: "No image keys provided" }, { status: 400 });
1500
1461
  }
1501
1462
  } catch {
1502
- return NextResponse3.json({ error: "Invalid request body" }, { status: 400 });
1463
+ return jsonResponse({ error: "Invalid request body" }, { status: 400 });
1503
1464
  }
1504
1465
  const stream = new ReadableStream({
1505
1466
  async start(controller) {
@@ -1605,10 +1566,10 @@ async function handleReprocessStream(request) {
1605
1566
  const body = await request.json();
1606
1567
  imageKeys = body.imageKeys;
1607
1568
  if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1608
- return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
1569
+ return jsonResponse({ error: "No image keys provided" }, { status: 400 });
1609
1570
  }
1610
1571
  } catch {
1611
- return NextResponse3.json({ error: "Invalid request body" }, { status: 400 });
1572
+ return jsonResponse({ error: "Invalid request body" }, { status: 400 });
1612
1573
  }
1613
1574
  const stream = new ReadableStream({
1614
1575
  async start(controller) {
@@ -1735,203 +1696,10 @@ async function handleReprocessStream(request) {
1735
1696
  }
1736
1697
  });
1737
1698
  }
1738
- async function handleProcessAllStream() {
1739
- const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
1740
- const encoder = new TextEncoder();
1741
- const stream = new ReadableStream({
1742
- async start(controller) {
1743
- const sendEvent = (data) => {
1744
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
1745
-
1746
- `));
1747
- };
1748
- try {
1749
- const meta = await loadMeta();
1750
- const cdnUrls = getCdnUrls(meta);
1751
- const processed = [];
1752
- const errors = [];
1753
- const orphansRemoved = [];
1754
- const urlsToPurge = [];
1755
- let alreadyProcessed = 0;
1756
- const imagesToProcess = [];
1757
- for (const [key, entry] of getFileEntries(meta)) {
1758
- const fileName = path6.basename(key);
1759
- if (!isImageFile(fileName)) continue;
1760
- if (!isProcessed(entry)) {
1761
- imagesToProcess.push({ key, entry });
1762
- } else {
1763
- alreadyProcessed++;
1764
- }
1765
- }
1766
- const total = imagesToProcess.length;
1767
- sendEvent({ type: "start", total });
1768
- for (let i = 0; i < imagesToProcess.length; i++) {
1769
- const { key, entry } = imagesToProcess[i];
1770
- const fullPath = getPublicPath(key);
1771
- const existingCdnIndex = entry.c;
1772
- const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1773
- const isInOurR2 = existingCdnUrl === publicUrl;
1774
- const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1775
- sendEvent({
1776
- type: "progress",
1777
- current: i + 1,
1778
- total,
1779
- percent: Math.round((i + 1) / total * 100),
1780
- currentFile: key.slice(1)
1781
- // Remove leading /
1782
- });
1783
- try {
1784
- let buffer;
1785
- if (isInOurR2) {
1786
- buffer = await downloadFromCdn(key);
1787
- const dir = path6.dirname(fullPath);
1788
- await fs6.mkdir(dir, { recursive: true });
1789
- await fs6.writeFile(fullPath, buffer);
1790
- } else if (isRemote && existingCdnUrl) {
1791
- const remoteUrl = `${existingCdnUrl}${key}`;
1792
- buffer = await downloadFromRemoteUrl(remoteUrl);
1793
- const dir = path6.dirname(fullPath);
1794
- await fs6.mkdir(dir, { recursive: true });
1795
- await fs6.writeFile(fullPath, buffer);
1796
- } else {
1797
- buffer = await fs6.readFile(fullPath);
1798
- }
1799
- const ext = path6.extname(key).toLowerCase();
1800
- const isSvg = ext === ".svg";
1801
- if (isSvg) {
1802
- const imageDir = path6.dirname(key.slice(1));
1803
- const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
1804
- await fs6.mkdir(imagesPath, { recursive: true });
1805
- const fileName = path6.basename(key);
1806
- const destPath = path6.join(imagesPath, fileName);
1807
- await fs6.writeFile(destPath, buffer);
1808
- meta[key] = {
1809
- ...entry,
1810
- o: { w: 0, h: 0 },
1811
- b: "",
1812
- f: { w: 0, h: 0 }
1813
- // SVG has "full" to indicate processed
1814
- };
1815
- if (isRemote) {
1816
- delete meta[key].c;
1817
- }
1818
- } else {
1819
- const processedEntry = await processImage(buffer, key);
1820
- meta[key] = {
1821
- ...processedEntry,
1822
- ...isInOurR2 ? { c: existingCdnIndex } : {}
1823
- };
1824
- }
1825
- if (isInOurR2) {
1826
- await uploadToCdn(key);
1827
- for (const thumbPath of getAllThumbnailPaths(key)) {
1828
- urlsToPurge.push(`${publicUrl}${thumbPath}`);
1829
- }
1830
- await deleteLocalThumbnails(key);
1831
- try {
1832
- await fs6.unlink(fullPath);
1833
- } catch {
1834
- }
1835
- }
1836
- processed.push(key.slice(1));
1837
- } catch (error) {
1838
- console.error(`Failed to process ${key}:`, error);
1839
- errors.push(key.slice(1));
1840
- }
1841
- }
1842
- sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
1843
- const trackedPaths = /* @__PURE__ */ new Set();
1844
- for (const [imageKey, entry] of getFileEntries(meta)) {
1845
- if (entry.c === void 0) {
1846
- for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1847
- trackedPaths.add(thumbPath);
1848
- }
1849
- }
1850
- }
1851
- async function findOrphans(dir, relativePath = "") {
1852
- try {
1853
- const entries = await fs6.readdir(dir, { withFileTypes: true });
1854
- for (const fsEntry of entries) {
1855
- if (fsEntry.name.startsWith(".")) continue;
1856
- const entryFullPath = path6.join(dir, fsEntry.name);
1857
- const relPath = relativePath ? `${relativePath}/${fsEntry.name}` : fsEntry.name;
1858
- if (fsEntry.isDirectory()) {
1859
- await findOrphans(entryFullPath, relPath);
1860
- } else if (isImageFile(fsEntry.name)) {
1861
- const publicPath = `/images/${relPath}`;
1862
- if (!trackedPaths.has(publicPath)) {
1863
- try {
1864
- await fs6.unlink(entryFullPath);
1865
- orphansRemoved.push(publicPath);
1866
- } catch (err) {
1867
- console.error(`Failed to remove orphan ${publicPath}:`, err);
1868
- }
1869
- }
1870
- }
1871
- }
1872
- } catch {
1873
- }
1874
- }
1875
- const imagesDir = getPublicPath("images");
1876
- try {
1877
- await findOrphans(imagesDir);
1878
- } catch {
1879
- }
1880
- async function removeEmptyDirs(dir) {
1881
- try {
1882
- const entries = await fs6.readdir(dir, { withFileTypes: true });
1883
- let isEmpty = true;
1884
- for (const fsEntry of entries) {
1885
- if (fsEntry.isDirectory()) {
1886
- const subDirEmpty = await removeEmptyDirs(path6.join(dir, fsEntry.name));
1887
- if (!subDirEmpty) isEmpty = false;
1888
- } else {
1889
- isEmpty = false;
1890
- }
1891
- }
1892
- if (isEmpty && dir !== imagesDir) {
1893
- await fs6.rmdir(dir);
1894
- }
1895
- return isEmpty;
1896
- } catch {
1897
- return true;
1898
- }
1899
- }
1900
- try {
1901
- await removeEmptyDirs(imagesDir);
1902
- } catch {
1903
- }
1904
- await saveMeta(meta);
1905
- if (urlsToPurge.length > 0) {
1906
- await purgeCloudflareCache(urlsToPurge);
1907
- }
1908
- sendEvent({
1909
- type: "complete",
1910
- processed: processed.length,
1911
- alreadyProcessed,
1912
- orphansRemoved: orphansRemoved.length,
1913
- errors: errors.length
1914
- });
1915
- } catch (error) {
1916
- console.error("Failed to process all:", error);
1917
- sendEvent({ type: "error", message: "Failed to process images" });
1918
- } finally {
1919
- controller.close();
1920
- }
1921
- }
1922
- });
1923
- return new Response(stream, {
1924
- headers: {
1925
- "Content-Type": "text/event-stream",
1926
- "Cache-Control": "no-cache",
1927
- "Connection": "keep-alive"
1928
- }
1929
- });
1930
- }
1931
1699
  async function handleDownloadStream(request) {
1932
1700
  const { imageKeys } = await request.json();
1933
1701
  if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
1934
- return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
1702
+ return jsonResponse({ error: "No image keys provided" }, { status: 400 });
1935
1703
  }
1936
1704
  const stream = new ReadableStream({
1937
1705
  async start(controller) {
@@ -2026,7 +1794,6 @@ async function handleDownloadStream(request) {
2026
1794
  }
2027
1795
 
2028
1796
  // src/handlers/scan.ts
2029
- import { NextResponse as NextResponse4 } from "next/server";
2030
1797
  import { promises as fs7 } from "fs";
2031
1798
  import path7 from "path";
2032
1799
  import sharp3 from "sharp";
@@ -2200,7 +1967,7 @@ async function handleDeleteOrphans(request) {
2200
1967
  try {
2201
1968
  const { paths } = await request.json();
2202
1969
  if (!paths || !Array.isArray(paths) || paths.length === 0) {
2203
- return NextResponse4.json({ error: "No paths provided" }, { status: 400 });
1970
+ return jsonResponse({ error: "No paths provided" }, { status: 400 });
2204
1971
  }
2205
1972
  const deleted = [];
2206
1973
  const errors = [];
@@ -2243,14 +2010,14 @@ async function handleDeleteOrphans(request) {
2243
2010
  await removeEmptyDirs(imagesDir);
2244
2011
  } catch {
2245
2012
  }
2246
- return NextResponse4.json({
2013
+ return jsonResponse({
2247
2014
  success: true,
2248
2015
  deleted: deleted.length,
2249
2016
  errors: errors.length
2250
2017
  });
2251
2018
  } catch (error) {
2252
2019
  console.error("Failed to delete orphans:", error);
2253
- return NextResponse4.json({ error: "Failed to delete orphaned files" }, { status: 500 });
2020
+ return jsonResponse({ error: "Failed to delete orphaned files" }, { status: 500 });
2254
2021
  }
2255
2022
  }
2256
2023
 
@@ -2379,7 +2146,6 @@ async function handleUpdateCdns(request) {
2379
2146
  }
2380
2147
 
2381
2148
  // src/handlers/favicon.ts
2382
- import { NextResponse as NextResponse5 } from "next/server";
2383
2149
  import sharp5 from "sharp";
2384
2150
  import path8 from "path";
2385
2151
  import fs8 from "fs/promises";
@@ -2395,14 +2161,14 @@ async function handleGenerateFavicon(request) {
2395
2161
  const body = await request.json();
2396
2162
  imagePath = body.imagePath;
2397
2163
  if (!imagePath) {
2398
- return NextResponse5.json({ error: "No image path provided" }, { status: 400 });
2164
+ return jsonResponse({ error: "No image path provided" }, { status: 400 });
2399
2165
  }
2400
2166
  } catch {
2401
- return NextResponse5.json({ error: "Invalid request body" }, { status: 400 });
2167
+ return jsonResponse({ error: "Invalid request body" }, { status: 400 });
2402
2168
  }
2403
2169
  const fileName = path8.basename(imagePath).toLowerCase();
2404
2170
  if (fileName !== "favicon.png" && fileName !== "favicon.jpg") {
2405
- return NextResponse5.json({
2171
+ return jsonResponse({
2406
2172
  error: "Source file must be named favicon.png or favicon.jpg"
2407
2173
  }, { status: 400 });
2408
2174
  }
@@ -2410,19 +2176,19 @@ async function handleGenerateFavicon(request) {
2410
2176
  try {
2411
2177
  await fs8.access(sourcePath);
2412
2178
  } catch {
2413
- return NextResponse5.json({ error: "Source file not found" }, { status: 404 });
2179
+ return jsonResponse({ error: "Source file not found" }, { status: 404 });
2414
2180
  }
2415
2181
  let metadata;
2416
2182
  try {
2417
2183
  metadata = await sharp5(sourcePath).metadata();
2418
2184
  } catch {
2419
- return NextResponse5.json({ error: "Source file is not a valid image" }, { status: 400 });
2185
+ return jsonResponse({ error: "Source file is not a valid image" }, { status: 400 });
2420
2186
  }
2421
2187
  const outputDir = getSrcAppPath();
2422
2188
  try {
2423
2189
  await fs8.access(outputDir);
2424
2190
  } catch {
2425
- return NextResponse5.json({
2191
+ return jsonResponse({
2426
2192
  error: "Output directory src/app/ not found"
2427
2193
  }, { status: 500 });
2428
2194
  }
@@ -2490,98 +2256,147 @@ async function handleGenerateFavicon(request) {
2490
2256
  });
2491
2257
  }
2492
2258
 
2493
- // src/handlers/index.ts
2494
- async function GET(request) {
2495
- if (process.env.NODE_ENV !== "development") {
2496
- return NextResponse6.json({ error: "Not available in production" }, { status: 403 });
2497
- }
2498
- const pathname = request.nextUrl.pathname;
2499
- const route = pathname.replace(/^\/api\/studio\/?/, "");
2500
- if (route === "list-folders") {
2501
- return handleListFolders();
2502
- }
2503
- if (route === "list" || route.startsWith("list")) {
2504
- return handleList(request);
2505
- }
2506
- if (route === "count-images") {
2507
- return handleCountImages();
2508
- }
2509
- if (route === "folder-images") {
2510
- return handleFolderImages(request);
2511
- }
2512
- if (route === "search") {
2513
- return handleSearch(request);
2514
- }
2515
- if (route === "cdns") {
2516
- return handleGetCdns();
2517
- }
2518
- return NextResponse6.json({ error: "Not found" }, { status: 404 });
2259
+ // src/server/index.ts
2260
+ var __filename = fileURLToPath(import.meta.url);
2261
+ var __dirname = dirname(__filename);
2262
+ async function startServer(options) {
2263
+ const { port, workspace, open } = options;
2264
+ const app = express();
2265
+ process.env.STUDIO_WORKSPACE = workspace;
2266
+ const envLocalPath = join(workspace, ".env.local");
2267
+ const envPath = join(workspace, ".env");
2268
+ if (existsSync(envLocalPath)) {
2269
+ loadEnv({ path: envLocalPath });
2270
+ console.log("Loaded environment from .env.local");
2271
+ } else if (existsSync(envPath)) {
2272
+ loadEnv({ path: envPath });
2273
+ console.log("Loaded environment from .env");
2274
+ }
2275
+ app.use(express.json({ limit: "50mb" }));
2276
+ app.use(express.urlencoded({ extended: true, limit: "50mb" }));
2277
+ app.get("/api/studio/list", wrapHandler(handleList));
2278
+ app.get("/api/studio/list-folders", wrapHandler(handleListFolders));
2279
+ app.get("/api/studio/search", wrapHandler(handleSearch));
2280
+ app.get("/api/studio/count-images", wrapHandler(handleCountImages));
2281
+ app.get("/api/studio/folder-images", wrapHandler(handleFolderImages));
2282
+ app.get("/api/studio/cdns", wrapHandler(handleGetCdns));
2283
+ app.post("/api/studio/upload", wrapHandler(handleUpload));
2284
+ app.post("/api/studio/create-folder", wrapHandler(handleCreateFolder));
2285
+ app.post("/api/studio/rename", wrapHandler(handleRename));
2286
+ app.post("/api/studio/move", wrapHandler(handleMoveStream, true));
2287
+ app.post("/api/studio/sync", wrapHandler(handleSync, true));
2288
+ app.post("/api/studio/reprocess-stream", wrapHandler(handleReprocessStream, true));
2289
+ app.post("/api/studio/unprocess-stream", wrapHandler(handleUnprocessStream, true));
2290
+ app.post("/api/studio/download-stream", wrapHandler(handleDownloadStream, true));
2291
+ app.post("/api/studio/scan", wrapHandler(handleScanStream, true));
2292
+ app.post("/api/studio/delete-orphans", wrapHandler(handleDeleteOrphans));
2293
+ app.post("/api/studio/import", wrapHandler(handleImportUrls, true));
2294
+ app.post("/api/studio/cdns", wrapHandler(handleUpdateCdns));
2295
+ app.post("/api/studio/generate-favicon", wrapHandler(handleGenerateFavicon, true));
2296
+ app.delete("/api/studio/delete", wrapHandler(handleDelete));
2297
+ app.use("/public", express.static(join(workspace, "public")));
2298
+ const clientDir = resolve(__dirname, "../client");
2299
+ app.get("/", (req, res) => {
2300
+ const htmlPath = join(clientDir, "index.html");
2301
+ if (existsSync(htmlPath)) {
2302
+ let html = readFileSync(htmlPath, "utf-8");
2303
+ const script = `<script>window.__STUDIO_WORKSPACE__ = ${JSON.stringify(workspace)};</script>`;
2304
+ html = html.replace("</head>", `${script}</head>`);
2305
+ res.type("html").send(html);
2306
+ } else {
2307
+ res.status(404).send("Client not built. Run npm run build first.");
2308
+ }
2309
+ });
2310
+ app.use(express.static(clientDir));
2311
+ app.listen(port, () => {
2312
+ console.log(`
2313
+ \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
2314
+ \u2502 Studio - Media Manager \u2502
2315
+ \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524
2316
+ \u2502 Workspace: ${workspace.length > 24 ? "..." + workspace.slice(-21) : workspace.padEnd(24)}\u2502
2317
+ \u2502 URL: http://localhost:${port} \u2502
2318
+ \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
2319
+ `);
2320
+ if (open) {
2321
+ import("open").then((mod) => {
2322
+ mod.default(`http://localhost:${port}`);
2323
+ }).catch(() => {
2324
+ });
2325
+ }
2326
+ });
2519
2327
  }
2520
- async function POST(request) {
2521
- if (process.env.NODE_ENV !== "development") {
2522
- return NextResponse6.json({ error: "Not available in production" }, { status: 403 });
2523
- }
2524
- const pathname = request.nextUrl.pathname;
2525
- const route = pathname.replace(/^\/api\/studio\/?/, "");
2526
- if (route === "upload") {
2527
- return handleUpload(request);
2528
- }
2529
- if (route === "delete") {
2530
- return handleDelete(request);
2531
- }
2532
- if (route === "sync") {
2533
- return handleSync(request);
2534
- }
2535
- if (route === "reprocess") {
2536
- return handleReprocess(request);
2537
- }
2538
- if (route === "reprocess-stream") {
2539
- return handleReprocessStream(request);
2540
- }
2541
- if (route === "unprocess-stream") {
2542
- return handleUnprocessStream(request);
2543
- }
2544
- if (route === "process-all") {
2545
- return handleProcessAllStream();
2546
- }
2547
- if (route === "download-stream") {
2548
- return handleDownloadStream(request);
2549
- }
2550
- if (route === "create-folder") {
2551
- return handleCreateFolder(request);
2552
- }
2553
- if (route === "rename") {
2554
- return handleRename(request);
2555
- }
2556
- if (route === "move") {
2557
- return handleMoveStream(request);
2558
- }
2559
- if (route === "scan") {
2560
- return handleScanStream();
2561
- }
2562
- if (route === "delete-orphans") {
2563
- return handleDeleteOrphans(request);
2564
- }
2565
- if (route === "import") {
2566
- return handleImportUrls(request);
2567
- }
2568
- if (route === "cdns") {
2569
- return handleUpdateCdns(request);
2328
+ function wrapHandler(handler, streaming = false) {
2329
+ return async (req, res) => {
2330
+ try {
2331
+ const request = createFetchRequest(req);
2332
+ const response = await handler(request);
2333
+ if (streaming) {
2334
+ await sendStreamingResponse(res, response);
2335
+ } else {
2336
+ await sendResponse(res, response);
2337
+ }
2338
+ } catch (error) {
2339
+ console.error("Handler error:", error);
2340
+ res.status(500).json({ error: "Internal server error" });
2341
+ }
2342
+ };
2343
+ }
2344
+ function createFetchRequest(req) {
2345
+ const url = new URL(req.url, `http://${req.headers.host}`);
2346
+ const headers = new Headers();
2347
+ for (const [key, value] of Object.entries(req.headers)) {
2348
+ if (value) {
2349
+ if (Array.isArray(value)) {
2350
+ value.forEach((v) => headers.append(key, v));
2351
+ } else {
2352
+ headers.set(key, value);
2353
+ }
2354
+ }
2570
2355
  }
2571
- if (route === "generate-favicon") {
2572
- return handleGenerateFavicon(request);
2356
+ const init = {
2357
+ method: req.method,
2358
+ headers
2359
+ };
2360
+ if (req.method !== "GET" && req.method !== "HEAD") {
2361
+ if (req.body) {
2362
+ init.body = JSON.stringify(req.body);
2363
+ }
2573
2364
  }
2574
- return NextResponse6.json({ error: "Not found" }, { status: 404 });
2365
+ return new globalThis.Request(url.toString(), init);
2366
+ }
2367
+ async function sendResponse(res, response) {
2368
+ res.status(response.status);
2369
+ response.headers.forEach((value, key) => {
2370
+ res.setHeader(key, value);
2371
+ });
2372
+ const body = await response.text();
2373
+ res.send(body);
2575
2374
  }
2576
- async function DELETE(request) {
2577
- if (process.env.NODE_ENV !== "development") {
2578
- return NextResponse6.json({ error: "Not available in production" }, { status: 403 });
2375
+ async function sendStreamingResponse(res, response) {
2376
+ res.status(response.status);
2377
+ response.headers.forEach((value, key) => {
2378
+ res.setHeader(key, value);
2379
+ });
2380
+ if (response.body) {
2381
+ const reader = response.body.getReader();
2382
+ const decoder = new TextDecoder();
2383
+ try {
2384
+ while (true) {
2385
+ const { done, value } = await reader.read();
2386
+ if (done) break;
2387
+ res.write(decoder.decode(value, { stream: true }));
2388
+ }
2389
+ res.end();
2390
+ } catch (error) {
2391
+ console.error("Streaming error:", error);
2392
+ res.end();
2393
+ }
2394
+ } else {
2395
+ const body = await response.text();
2396
+ res.send(body);
2579
2397
  }
2580
- return handleDelete(request);
2581
2398
  }
2582
2399
  export {
2583
- DELETE,
2584
- GET,
2585
- POST
2400
+ startServer
2586
2401
  };
2587
- //# sourceMappingURL=index.mjs.map
2402
+ //# sourceMappingURL=index.js.map