@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.
- package/bin/studio.mjs +17 -124
- package/dist/client/assets/index-TPS3nigu.js +74 -0
- package/dist/client/index.html +19 -0
- package/dist/{handlers/index.mjs → server/index.js} +242 -427
- package/dist/server/index.js.map +1 -0
- package/package.json +17 -32
- package/app/api/studio/[...path]/route.js +0 -1
- package/app/layout.jsx +0 -19
- package/app/page.jsx +0 -90
- package/dist/chunk-TRYWHLJ2.mjs +0 -32
- package/dist/chunk-TRYWHLJ2.mjs.map +0 -1
- package/dist/chunk-VI6QG6WT.js +0 -32
- package/dist/chunk-VI6QG6WT.js.map +0 -1
- package/dist/components/StudioUI.d.mts +0 -15
- package/dist/components/StudioUI.d.ts +0 -15
- package/dist/components/StudioUI.js +0 -6572
- package/dist/components/StudioUI.js.map +0 -1
- package/dist/components/StudioUI.mjs +0 -6572
- package/dist/components/StudioUI.mjs.map +0 -1
- package/dist/handlers/index.d.mts +0 -22
- package/dist/handlers/index.d.ts +0 -22
- package/dist/handlers/index.js +0 -2587
- package/dist/handlers/index.js.map +0 -1
- package/dist/handlers/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -90
- package/dist/index.d.ts +0 -90
- package/dist/index.js +0 -11
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -11
- package/dist/index.mjs.map +0 -1
- package/next.config.mjs +0 -20
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
getAllThumbnailPaths,
|
|
3
|
-
getThumbnailPath,
|
|
4
|
-
isProcessed
|
|
5
|
-
} from "../chunk-TRYWHLJ2.mjs";
|
|
1
|
+
// @ts-nocheck
|
|
6
2
|
|
|
7
|
-
// src/
|
|
8
|
-
import
|
|
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((
|
|
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((
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
714
|
+
return jsonResponse({ items });
|
|
675
715
|
} catch (error) {
|
|
676
716
|
console.error("Failed to list directory:", error);
|
|
677
|
-
return
|
|
717
|
+
return jsonResponse({ error: "Failed to list directory" }, { status: 500 });
|
|
678
718
|
}
|
|
679
719
|
}
|
|
680
720
|
async function handleSearch(request) {
|
|
681
|
-
const searchParams = request.
|
|
721
|
+
const searchParams = new URL(request.url).searchParams;
|
|
682
722
|
const query = searchParams.get("q")?.toLowerCase() || "";
|
|
683
723
|
if (query.length < 2) {
|
|
684
|
-
return
|
|
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
|
|
784
|
+
return jsonResponse({ items });
|
|
745
785
|
} catch (error) {
|
|
746
786
|
console.error("Failed to search:", error);
|
|
747
|
-
return
|
|
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
|
|
830
|
+
return jsonResponse({ folders });
|
|
791
831
|
} catch (error) {
|
|
792
832
|
console.error("Failed to list folders:", error);
|
|
793
|
-
return
|
|
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
|
|
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
|
|
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.
|
|
858
|
+
const searchParams = new URL(request.url).searchParams;
|
|
819
859
|
const foldersParam = searchParams.get("folders");
|
|
820
860
|
if (!foldersParam) {
|
|
821
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1072
|
+
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1034
1073
|
}
|
|
1035
1074
|
try {
|
|
1036
1075
|
await fs5.access(folderPath);
|
|
1037
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1101
|
+
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1063
1102
|
}
|
|
1064
1103
|
try {
|
|
1065
1104
|
await fs5.access(absoluteOldPath);
|
|
1066
1105
|
} catch {
|
|
1067
|
-
return
|
|
1106
|
+
return jsonResponse({ error: "File or folder not found" }, { status: 404 });
|
|
1068
1107
|
}
|
|
1069
1108
|
try {
|
|
1070
1109
|
await fs5.access(absoluteNewPath);
|
|
1071
|
-
return
|
|
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
|
|
1142
|
+
return jsonResponse({ success: true, newPath });
|
|
1104
1143
|
} catch (error) {
|
|
1105
1144
|
console.error("Failed to rename:", error);
|
|
1106
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1460
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1500
1461
|
}
|
|
1501
1462
|
} catch {
|
|
1502
|
-
return
|
|
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
|
|
1569
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1609
1570
|
}
|
|
1610
1571
|
} catch {
|
|
1611
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2164
|
+
return jsonResponse({ error: "No image path provided" }, { status: 400 });
|
|
2399
2165
|
}
|
|
2400
2166
|
} catch {
|
|
2401
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
}
|
|
2498
|
-
const
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
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
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
}
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
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
|
-
|
|
2572
|
-
|
|
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
|
|
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
|
|
2577
|
-
|
|
2578
|
-
|
|
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
|
-
|
|
2584
|
-
GET,
|
|
2585
|
-
POST
|
|
2400
|
+
startServer
|
|
2586
2401
|
};
|
|
2587
|
-
//# sourceMappingURL=index.
|
|
2402
|
+
//# sourceMappingURL=index.js.map
|