@gallop.software/studio 2.0.6 → 2.1.0
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 +16 -59
- package/dist/client/assets/index-TPS3nigu.js +74 -0
- package/dist/client/index.html +19 -0
- package/dist/{handlers/index.mjs → server/index.js} +232 -427
- package/dist/server/index.js.map +1 -0
- package/package.json +16 -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,13 @@
|
|
|
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
9
|
|
|
10
10
|
// src/handlers/list.ts
|
|
11
|
-
import { NextResponse } from "next/server";
|
|
12
11
|
import { promises as fs4 } from "fs";
|
|
13
12
|
import path4 from "path";
|
|
14
13
|
|
|
@@ -197,6 +196,34 @@ async function processImage(buffer, imageKey) {
|
|
|
197
196
|
// src/handlers/utils/cdn.ts
|
|
198
197
|
import { promises as fs3 } from "fs";
|
|
199
198
|
import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
199
|
+
|
|
200
|
+
// src/types.ts
|
|
201
|
+
function getThumbnailPath(originalPath, size) {
|
|
202
|
+
if (size === "full") {
|
|
203
|
+
const ext2 = originalPath.match(/\.\w+$/)?.[0] || ".jpg";
|
|
204
|
+
const base2 = originalPath.replace(/\.\w+$/, "");
|
|
205
|
+
const outputExt2 = ext2.toLowerCase() === ".png" ? ".png" : ".jpg";
|
|
206
|
+
return `/images${base2}${outputExt2}`;
|
|
207
|
+
}
|
|
208
|
+
const ext = originalPath.match(/\.\w+$/)?.[0] || ".jpg";
|
|
209
|
+
const base = originalPath.replace(/\.\w+$/, "");
|
|
210
|
+
const outputExt = ext.toLowerCase() === ".png" ? ".png" : ".jpg";
|
|
211
|
+
return `/images${base}-${size}${outputExt}`;
|
|
212
|
+
}
|
|
213
|
+
function getAllThumbnailPaths(originalPath) {
|
|
214
|
+
return [
|
|
215
|
+
getThumbnailPath(originalPath, "full"),
|
|
216
|
+
getThumbnailPath(originalPath, "lg"),
|
|
217
|
+
getThumbnailPath(originalPath, "md"),
|
|
218
|
+
getThumbnailPath(originalPath, "sm")
|
|
219
|
+
];
|
|
220
|
+
}
|
|
221
|
+
function isProcessed(entry) {
|
|
222
|
+
if (!entry) return false;
|
|
223
|
+
return !!(entry.f || entry.lg || entry.md || entry.sm);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/handlers/utils/cdn.ts
|
|
200
227
|
async function purgeCloudflareCache(urls) {
|
|
201
228
|
const zoneId = process.env.CLOUDFLARE_ZONE_ID;
|
|
202
229
|
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
|
|
@@ -258,7 +285,7 @@ async function downloadFromCdn(originalPath) {
|
|
|
258
285
|
} catch (error) {
|
|
259
286
|
lastError = error;
|
|
260
287
|
if (attempt < maxRetries - 1) {
|
|
261
|
-
await new Promise((
|
|
288
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500 * (attempt + 1)));
|
|
262
289
|
}
|
|
263
290
|
}
|
|
264
291
|
}
|
|
@@ -307,7 +334,7 @@ async function downloadFromRemoteUrl(url) {
|
|
|
307
334
|
} catch (error) {
|
|
308
335
|
lastError = error;
|
|
309
336
|
if (attempt < maxRetries - 1) {
|
|
310
|
-
await new Promise((
|
|
337
|
+
await new Promise((resolve2) => setTimeout(resolve2, 500 * (attempt + 1)));
|
|
311
338
|
}
|
|
312
339
|
}
|
|
313
340
|
}
|
|
@@ -372,6 +399,18 @@ async function deleteThumbnailsFromCdn(imageKey) {
|
|
|
372
399
|
}
|
|
373
400
|
}
|
|
374
401
|
|
|
402
|
+
// src/handlers/utils/response.ts
|
|
403
|
+
function jsonResponse(data, init) {
|
|
404
|
+
const headers = new Headers({
|
|
405
|
+
"Content-Type": "application/json",
|
|
406
|
+
...init?.headers
|
|
407
|
+
});
|
|
408
|
+
return new Response(JSON.stringify(data), {
|
|
409
|
+
status: init?.status ?? 200,
|
|
410
|
+
headers
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
375
414
|
// src/handlers/list.ts
|
|
376
415
|
function getExistingThumbnails(originalPath, entry) {
|
|
377
416
|
const thumbnails = [];
|
|
@@ -410,7 +449,7 @@ function countFileTypes(folderPrefix, fileEntries, cdnUrls, r2PublicUrl) {
|
|
|
410
449
|
return { cloudCount, remoteCount, localCount };
|
|
411
450
|
}
|
|
412
451
|
async function handleList(request) {
|
|
413
|
-
const searchParams = request.
|
|
452
|
+
const searchParams = new URL(request.url).searchParams;
|
|
414
453
|
const requestedPath = searchParams.get("path") || "public";
|
|
415
454
|
try {
|
|
416
455
|
const meta = await loadMeta();
|
|
@@ -515,7 +554,7 @@ async function handleList(request) {
|
|
|
515
554
|
}
|
|
516
555
|
}
|
|
517
556
|
}
|
|
518
|
-
return
|
|
557
|
+
return jsonResponse({ items });
|
|
519
558
|
}
|
|
520
559
|
const absoluteDir = getWorkspacePath(requestedPath);
|
|
521
560
|
try {
|
|
@@ -580,7 +619,7 @@ async function handleList(request) {
|
|
|
580
619
|
}
|
|
581
620
|
}
|
|
582
621
|
if (fileEntries.length === 0 && items.length === 0) {
|
|
583
|
-
return
|
|
622
|
+
return jsonResponse({ items: [], isEmpty: true });
|
|
584
623
|
}
|
|
585
624
|
for (const [key, entry] of fileEntries) {
|
|
586
625
|
if (!key.startsWith(pathPrefix) && pathPrefix !== "/") continue;
|
|
@@ -671,17 +710,17 @@ async function handleList(request) {
|
|
|
671
710
|
});
|
|
672
711
|
}
|
|
673
712
|
}
|
|
674
|
-
return
|
|
713
|
+
return jsonResponse({ items });
|
|
675
714
|
} catch (error) {
|
|
676
715
|
console.error("Failed to list directory:", error);
|
|
677
|
-
return
|
|
716
|
+
return jsonResponse({ error: "Failed to list directory" }, { status: 500 });
|
|
678
717
|
}
|
|
679
718
|
}
|
|
680
719
|
async function handleSearch(request) {
|
|
681
|
-
const searchParams = request.
|
|
720
|
+
const searchParams = new URL(request.url).searchParams;
|
|
682
721
|
const query = searchParams.get("q")?.toLowerCase() || "";
|
|
683
722
|
if (query.length < 2) {
|
|
684
|
-
return
|
|
723
|
+
return jsonResponse({ items: [] });
|
|
685
724
|
}
|
|
686
725
|
try {
|
|
687
726
|
const meta = await loadMeta();
|
|
@@ -741,10 +780,10 @@ async function handleSearch(request) {
|
|
|
741
780
|
dimensions: entry.o ? { width: entry.o.w, height: entry.o.h } : void 0
|
|
742
781
|
});
|
|
743
782
|
}
|
|
744
|
-
return
|
|
783
|
+
return jsonResponse({ items });
|
|
745
784
|
} catch (error) {
|
|
746
785
|
console.error("Failed to search:", error);
|
|
747
|
-
return
|
|
786
|
+
return jsonResponse({ error: "Failed to search" }, { status: 500 });
|
|
748
787
|
}
|
|
749
788
|
}
|
|
750
789
|
async function handleListFolders() {
|
|
@@ -787,10 +826,10 @@ async function handleListFolders() {
|
|
|
787
826
|
depth
|
|
788
827
|
});
|
|
789
828
|
}
|
|
790
|
-
return
|
|
829
|
+
return jsonResponse({ folders });
|
|
791
830
|
} catch (error) {
|
|
792
831
|
console.error("Failed to list folders:", error);
|
|
793
|
-
return
|
|
832
|
+
return jsonResponse({ error: "Failed to list folders" }, { status: 500 });
|
|
794
833
|
}
|
|
795
834
|
}
|
|
796
835
|
async function handleCountImages() {
|
|
@@ -804,21 +843,21 @@ async function handleCountImages() {
|
|
|
804
843
|
allImages.push(key.slice(1));
|
|
805
844
|
}
|
|
806
845
|
}
|
|
807
|
-
return
|
|
846
|
+
return jsonResponse({
|
|
808
847
|
count: allImages.length,
|
|
809
848
|
images: allImages
|
|
810
849
|
});
|
|
811
850
|
} catch (error) {
|
|
812
851
|
console.error("Failed to count images:", error);
|
|
813
|
-
return
|
|
852
|
+
return jsonResponse({ error: "Failed to count images" }, { status: 500 });
|
|
814
853
|
}
|
|
815
854
|
}
|
|
816
855
|
async function handleFolderImages(request) {
|
|
817
856
|
try {
|
|
818
|
-
const searchParams = request.
|
|
857
|
+
const searchParams = new URL(request.url).searchParams;
|
|
819
858
|
const foldersParam = searchParams.get("folders");
|
|
820
859
|
if (!foldersParam) {
|
|
821
|
-
return
|
|
860
|
+
return jsonResponse({ error: "No folders provided" }, { status: 400 });
|
|
822
861
|
}
|
|
823
862
|
const folders = foldersParam.split(",");
|
|
824
863
|
const meta = await loadMeta();
|
|
@@ -836,19 +875,18 @@ async function handleFolderImages(request) {
|
|
|
836
875
|
}
|
|
837
876
|
}
|
|
838
877
|
}
|
|
839
|
-
return
|
|
878
|
+
return jsonResponse({
|
|
840
879
|
count: allFiles.length,
|
|
841
880
|
images: allFiles
|
|
842
881
|
// Keep as 'images' for backwards compatibility
|
|
843
882
|
});
|
|
844
883
|
} catch (error) {
|
|
845
884
|
console.error("Failed to get folder files:", error);
|
|
846
|
-
return
|
|
885
|
+
return jsonResponse({ error: "Failed to get folder files" }, { status: 500 });
|
|
847
886
|
}
|
|
848
887
|
}
|
|
849
888
|
|
|
850
889
|
// src/handlers/files.ts
|
|
851
|
-
import { NextResponse as NextResponse2 } from "next/server";
|
|
852
890
|
import { promises as fs5 } from "fs";
|
|
853
891
|
import path5 from "path";
|
|
854
892
|
import sharp2 from "sharp";
|
|
@@ -858,7 +896,7 @@ async function handleUpload(request) {
|
|
|
858
896
|
const file = formData.get("file");
|
|
859
897
|
const targetPath = formData.get("path") || "public";
|
|
860
898
|
if (!file) {
|
|
861
|
-
return
|
|
899
|
+
return jsonResponse({ error: "No file provided" }, { status: 400 });
|
|
862
900
|
}
|
|
863
901
|
const bytes = await file.arrayBuffer();
|
|
864
902
|
const buffer = Buffer.from(bytes);
|
|
@@ -874,7 +912,7 @@ async function handleUpload(request) {
|
|
|
874
912
|
relativeDir = targetPath.replace("public/", "");
|
|
875
913
|
}
|
|
876
914
|
if (relativeDir === "images" || relativeDir.startsWith("images/")) {
|
|
877
|
-
return
|
|
915
|
+
return jsonResponse(
|
|
878
916
|
{ error: "Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically." },
|
|
879
917
|
{ status: 400 }
|
|
880
918
|
);
|
|
@@ -897,7 +935,7 @@ async function handleUpload(request) {
|
|
|
897
935
|
await fs5.mkdir(uploadDir, { recursive: true });
|
|
898
936
|
await fs5.writeFile(path5.join(uploadDir, actualFileName), buffer);
|
|
899
937
|
if (!isMedia) {
|
|
900
|
-
return
|
|
938
|
+
return jsonResponse({
|
|
901
939
|
success: true,
|
|
902
940
|
message: "File uploaded (not a media file)",
|
|
903
941
|
path: `public/${relativeDir ? relativeDir + "/" : ""}${actualFileName}`
|
|
@@ -916,7 +954,7 @@ async function handleUpload(request) {
|
|
|
916
954
|
meta[imageKey] = {};
|
|
917
955
|
}
|
|
918
956
|
await saveMeta(meta);
|
|
919
|
-
return
|
|
957
|
+
return jsonResponse({
|
|
920
958
|
success: true,
|
|
921
959
|
imageKey,
|
|
922
960
|
message: 'File uploaded. Run "Process Images" to generate thumbnails.'
|
|
@@ -924,14 +962,14 @@ async function handleUpload(request) {
|
|
|
924
962
|
} catch (error) {
|
|
925
963
|
console.error("Failed to upload:", error);
|
|
926
964
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
927
|
-
return
|
|
965
|
+
return jsonResponse({ error: `Failed to upload file: ${message}` }, { status: 500 });
|
|
928
966
|
}
|
|
929
967
|
}
|
|
930
968
|
async function handleDelete(request) {
|
|
931
969
|
try {
|
|
932
970
|
const { paths } = await request.json();
|
|
933
971
|
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
934
|
-
return
|
|
972
|
+
return jsonResponse({ error: "No paths provided" }, { status: 400 });
|
|
935
973
|
}
|
|
936
974
|
const meta = await loadMeta();
|
|
937
975
|
const deleted = [];
|
|
@@ -1007,68 +1045,68 @@ async function handleDelete(request) {
|
|
|
1007
1045
|
}
|
|
1008
1046
|
}
|
|
1009
1047
|
await saveMeta(meta);
|
|
1010
|
-
return
|
|
1048
|
+
return jsonResponse({
|
|
1011
1049
|
success: true,
|
|
1012
1050
|
deleted,
|
|
1013
1051
|
errors: errors.length > 0 ? errors : void 0
|
|
1014
1052
|
});
|
|
1015
1053
|
} catch (error) {
|
|
1016
1054
|
console.error("Failed to delete:", error);
|
|
1017
|
-
return
|
|
1055
|
+
return jsonResponse({ error: "Failed to delete files" }, { status: 500 });
|
|
1018
1056
|
}
|
|
1019
1057
|
}
|
|
1020
1058
|
async function handleCreateFolder(request) {
|
|
1021
1059
|
try {
|
|
1022
1060
|
const { parentPath, name } = await request.json();
|
|
1023
1061
|
if (!name || typeof name !== "string") {
|
|
1024
|
-
return
|
|
1062
|
+
return jsonResponse({ error: "Folder name is required" }, { status: 400 });
|
|
1025
1063
|
}
|
|
1026
1064
|
const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
1027
1065
|
if (!sanitizedName) {
|
|
1028
|
-
return
|
|
1066
|
+
return jsonResponse({ error: "Invalid folder name" }, { status: 400 });
|
|
1029
1067
|
}
|
|
1030
1068
|
const safePath = (parentPath || "public").replace(/\.\./g, "");
|
|
1031
1069
|
const folderPath = getWorkspacePath(safePath, sanitizedName);
|
|
1032
1070
|
if (!folderPath.startsWith(getPublicPath())) {
|
|
1033
|
-
return
|
|
1071
|
+
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1034
1072
|
}
|
|
1035
1073
|
try {
|
|
1036
1074
|
await fs5.access(folderPath);
|
|
1037
|
-
return
|
|
1075
|
+
return jsonResponse({ error: "A folder with this name already exists" }, { status: 400 });
|
|
1038
1076
|
} catch {
|
|
1039
1077
|
}
|
|
1040
1078
|
await fs5.mkdir(folderPath, { recursive: true });
|
|
1041
|
-
return
|
|
1079
|
+
return jsonResponse({ success: true, path: path5.join(safePath, sanitizedName) });
|
|
1042
1080
|
} catch (error) {
|
|
1043
1081
|
console.error("Failed to create folder:", error);
|
|
1044
|
-
return
|
|
1082
|
+
return jsonResponse({ error: "Failed to create folder" }, { status: 500 });
|
|
1045
1083
|
}
|
|
1046
1084
|
}
|
|
1047
1085
|
async function handleRename(request) {
|
|
1048
1086
|
try {
|
|
1049
1087
|
const { oldPath, newName } = await request.json();
|
|
1050
1088
|
if (!oldPath || !newName) {
|
|
1051
|
-
return
|
|
1089
|
+
return jsonResponse({ error: "Path and new name are required" }, { status: 400 });
|
|
1052
1090
|
}
|
|
1053
1091
|
const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
1054
1092
|
if (!sanitizedName) {
|
|
1055
|
-
return
|
|
1093
|
+
return jsonResponse({ error: "Invalid name" }, { status: 400 });
|
|
1056
1094
|
}
|
|
1057
1095
|
const safePath = oldPath.replace(/\.\./g, "");
|
|
1058
1096
|
const absoluteOldPath = getWorkspacePath(safePath);
|
|
1059
1097
|
const parentDir = path5.dirname(absoluteOldPath);
|
|
1060
1098
|
const absoluteNewPath = path5.join(parentDir, sanitizedName);
|
|
1061
1099
|
if (!absoluteOldPath.startsWith(getPublicPath())) {
|
|
1062
|
-
return
|
|
1100
|
+
return jsonResponse({ error: "Invalid path" }, { status: 400 });
|
|
1063
1101
|
}
|
|
1064
1102
|
try {
|
|
1065
1103
|
await fs5.access(absoluteOldPath);
|
|
1066
1104
|
} catch {
|
|
1067
|
-
return
|
|
1105
|
+
return jsonResponse({ error: "File or folder not found" }, { status: 404 });
|
|
1068
1106
|
}
|
|
1069
1107
|
try {
|
|
1070
1108
|
await fs5.access(absoluteNewPath);
|
|
1071
|
-
return
|
|
1109
|
+
return jsonResponse({ error: "An item with this name already exists" }, { status: 400 });
|
|
1072
1110
|
} catch {
|
|
1073
1111
|
}
|
|
1074
1112
|
const stats = await fs5.stat(absoluteOldPath);
|
|
@@ -1100,10 +1138,10 @@ async function handleRename(request) {
|
|
|
1100
1138
|
await saveMeta(meta);
|
|
1101
1139
|
}
|
|
1102
1140
|
const newPath = path5.join(path5.dirname(safePath), sanitizedName);
|
|
1103
|
-
return
|
|
1141
|
+
return jsonResponse({ success: true, newPath });
|
|
1104
1142
|
} catch (error) {
|
|
1105
1143
|
console.error("Failed to rename:", error);
|
|
1106
|
-
return
|
|
1144
|
+
return jsonResponse({ error: "Failed to rename" }, { status: 500 });
|
|
1107
1145
|
}
|
|
1108
1146
|
}
|
|
1109
1147
|
async function handleMoveStream(request) {
|
|
@@ -1287,7 +1325,6 @@ async function handleMoveStream(request) {
|
|
|
1287
1325
|
}
|
|
1288
1326
|
|
|
1289
1327
|
// src/handlers/images.ts
|
|
1290
|
-
import { NextResponse as NextResponse3 } from "next/server";
|
|
1291
1328
|
import { promises as fs6 } from "fs";
|
|
1292
1329
|
import path6 from "path";
|
|
1293
1330
|
import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
|
|
@@ -1298,7 +1335,7 @@ async function handleSync(request) {
|
|
|
1298
1335
|
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
1299
1336
|
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/\s*$/, "");
|
|
1300
1337
|
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
1301
|
-
return
|
|
1338
|
+
return jsonResponse(
|
|
1302
1339
|
{ error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." },
|
|
1303
1340
|
{ status: 400 }
|
|
1304
1341
|
);
|
|
@@ -1306,7 +1343,7 @@ async function handleSync(request) {
|
|
|
1306
1343
|
try {
|
|
1307
1344
|
const { imageKeys } = await request.json();
|
|
1308
1345
|
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1309
|
-
return
|
|
1346
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1310
1347
|
}
|
|
1311
1348
|
const meta = await loadMeta();
|
|
1312
1349
|
const cdnUrls = getCdnUrls(meta);
|
|
@@ -1401,91 +1438,14 @@ async function handleSync(request) {
|
|
|
1401
1438
|
if (urlsToPurge.length > 0) {
|
|
1402
1439
|
await purgeCloudflareCache(urlsToPurge);
|
|
1403
1440
|
}
|
|
1404
|
-
return
|
|
1441
|
+
return jsonResponse({
|
|
1405
1442
|
success: true,
|
|
1406
1443
|
pushed,
|
|
1407
1444
|
errors: errors.length > 0 ? errors : void 0
|
|
1408
1445
|
});
|
|
1409
1446
|
} catch (error) {
|
|
1410
1447
|
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 });
|
|
1448
|
+
return jsonResponse({ error: "Failed to push to CDN" }, { status: 500 });
|
|
1489
1449
|
}
|
|
1490
1450
|
}
|
|
1491
1451
|
async function handleUnprocessStream(request) {
|
|
@@ -1496,10 +1456,10 @@ async function handleUnprocessStream(request) {
|
|
|
1496
1456
|
const body = await request.json();
|
|
1497
1457
|
imageKeys = body.imageKeys;
|
|
1498
1458
|
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1499
|
-
return
|
|
1459
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1500
1460
|
}
|
|
1501
1461
|
} catch {
|
|
1502
|
-
return
|
|
1462
|
+
return jsonResponse({ error: "Invalid request body" }, { status: 400 });
|
|
1503
1463
|
}
|
|
1504
1464
|
const stream = new ReadableStream({
|
|
1505
1465
|
async start(controller) {
|
|
@@ -1605,10 +1565,10 @@ async function handleReprocessStream(request) {
|
|
|
1605
1565
|
const body = await request.json();
|
|
1606
1566
|
imageKeys = body.imageKeys;
|
|
1607
1567
|
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1608
|
-
return
|
|
1568
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1609
1569
|
}
|
|
1610
1570
|
} catch {
|
|
1611
|
-
return
|
|
1571
|
+
return jsonResponse({ error: "Invalid request body" }, { status: 400 });
|
|
1612
1572
|
}
|
|
1613
1573
|
const stream = new ReadableStream({
|
|
1614
1574
|
async start(controller) {
|
|
@@ -1735,203 +1695,10 @@ async function handleReprocessStream(request) {
|
|
|
1735
1695
|
}
|
|
1736
1696
|
});
|
|
1737
1697
|
}
|
|
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
1698
|
async function handleDownloadStream(request) {
|
|
1932
1699
|
const { imageKeys } = await request.json();
|
|
1933
1700
|
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
1934
|
-
return
|
|
1701
|
+
return jsonResponse({ error: "No image keys provided" }, { status: 400 });
|
|
1935
1702
|
}
|
|
1936
1703
|
const stream = new ReadableStream({
|
|
1937
1704
|
async start(controller) {
|
|
@@ -2026,7 +1793,6 @@ async function handleDownloadStream(request) {
|
|
|
2026
1793
|
}
|
|
2027
1794
|
|
|
2028
1795
|
// src/handlers/scan.ts
|
|
2029
|
-
import { NextResponse as NextResponse4 } from "next/server";
|
|
2030
1796
|
import { promises as fs7 } from "fs";
|
|
2031
1797
|
import path7 from "path";
|
|
2032
1798
|
import sharp3 from "sharp";
|
|
@@ -2200,7 +1966,7 @@ async function handleDeleteOrphans(request) {
|
|
|
2200
1966
|
try {
|
|
2201
1967
|
const { paths } = await request.json();
|
|
2202
1968
|
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
2203
|
-
return
|
|
1969
|
+
return jsonResponse({ error: "No paths provided" }, { status: 400 });
|
|
2204
1970
|
}
|
|
2205
1971
|
const deleted = [];
|
|
2206
1972
|
const errors = [];
|
|
@@ -2243,14 +2009,14 @@ async function handleDeleteOrphans(request) {
|
|
|
2243
2009
|
await removeEmptyDirs(imagesDir);
|
|
2244
2010
|
} catch {
|
|
2245
2011
|
}
|
|
2246
|
-
return
|
|
2012
|
+
return jsonResponse({
|
|
2247
2013
|
success: true,
|
|
2248
2014
|
deleted: deleted.length,
|
|
2249
2015
|
errors: errors.length
|
|
2250
2016
|
});
|
|
2251
2017
|
} catch (error) {
|
|
2252
2018
|
console.error("Failed to delete orphans:", error);
|
|
2253
|
-
return
|
|
2019
|
+
return jsonResponse({ error: "Failed to delete orphaned files" }, { status: 500 });
|
|
2254
2020
|
}
|
|
2255
2021
|
}
|
|
2256
2022
|
|
|
@@ -2379,7 +2145,6 @@ async function handleUpdateCdns(request) {
|
|
|
2379
2145
|
}
|
|
2380
2146
|
|
|
2381
2147
|
// src/handlers/favicon.ts
|
|
2382
|
-
import { NextResponse as NextResponse5 } from "next/server";
|
|
2383
2148
|
import sharp5 from "sharp";
|
|
2384
2149
|
import path8 from "path";
|
|
2385
2150
|
import fs8 from "fs/promises";
|
|
@@ -2395,14 +2160,14 @@ async function handleGenerateFavicon(request) {
|
|
|
2395
2160
|
const body = await request.json();
|
|
2396
2161
|
imagePath = body.imagePath;
|
|
2397
2162
|
if (!imagePath) {
|
|
2398
|
-
return
|
|
2163
|
+
return jsonResponse({ error: "No image path provided" }, { status: 400 });
|
|
2399
2164
|
}
|
|
2400
2165
|
} catch {
|
|
2401
|
-
return
|
|
2166
|
+
return jsonResponse({ error: "Invalid request body" }, { status: 400 });
|
|
2402
2167
|
}
|
|
2403
2168
|
const fileName = path8.basename(imagePath).toLowerCase();
|
|
2404
2169
|
if (fileName !== "favicon.png" && fileName !== "favicon.jpg") {
|
|
2405
|
-
return
|
|
2170
|
+
return jsonResponse({
|
|
2406
2171
|
error: "Source file must be named favicon.png or favicon.jpg"
|
|
2407
2172
|
}, { status: 400 });
|
|
2408
2173
|
}
|
|
@@ -2410,19 +2175,19 @@ async function handleGenerateFavicon(request) {
|
|
|
2410
2175
|
try {
|
|
2411
2176
|
await fs8.access(sourcePath);
|
|
2412
2177
|
} catch {
|
|
2413
|
-
return
|
|
2178
|
+
return jsonResponse({ error: "Source file not found" }, { status: 404 });
|
|
2414
2179
|
}
|
|
2415
2180
|
let metadata;
|
|
2416
2181
|
try {
|
|
2417
2182
|
metadata = await sharp5(sourcePath).metadata();
|
|
2418
2183
|
} catch {
|
|
2419
|
-
return
|
|
2184
|
+
return jsonResponse({ error: "Source file is not a valid image" }, { status: 400 });
|
|
2420
2185
|
}
|
|
2421
2186
|
const outputDir = getSrcAppPath();
|
|
2422
2187
|
try {
|
|
2423
2188
|
await fs8.access(outputDir);
|
|
2424
2189
|
} catch {
|
|
2425
|
-
return
|
|
2190
|
+
return jsonResponse({
|
|
2426
2191
|
error: "Output directory src/app/ not found"
|
|
2427
2192
|
}, { status: 500 });
|
|
2428
2193
|
}
|
|
@@ -2490,98 +2255,138 @@ async function handleGenerateFavicon(request) {
|
|
|
2490
2255
|
});
|
|
2491
2256
|
}
|
|
2492
2257
|
|
|
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
|
-
|
|
2258
|
+
// src/server/index.ts
|
|
2259
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
2260
|
+
var __dirname = dirname(__filename);
|
|
2261
|
+
async function startServer(options) {
|
|
2262
|
+
const { port, workspace, open } = options;
|
|
2263
|
+
const app = express();
|
|
2264
|
+
process.env.STUDIO_WORKSPACE = workspace;
|
|
2265
|
+
app.use(express.json({ limit: "50mb" }));
|
|
2266
|
+
app.use(express.urlencoded({ extended: true, limit: "50mb" }));
|
|
2267
|
+
app.get("/api/studio/list", wrapHandler(handleList));
|
|
2268
|
+
app.get("/api/studio/list-folders", wrapHandler(handleListFolders));
|
|
2269
|
+
app.get("/api/studio/search", wrapHandler(handleSearch));
|
|
2270
|
+
app.get("/api/studio/count-images", wrapHandler(handleCountImages));
|
|
2271
|
+
app.get("/api/studio/folder-images", wrapHandler(handleFolderImages));
|
|
2272
|
+
app.get("/api/studio/cdns", wrapHandler(handleGetCdns));
|
|
2273
|
+
app.post("/api/studio/upload", wrapHandler(handleUpload));
|
|
2274
|
+
app.post("/api/studio/create-folder", wrapHandler(handleCreateFolder));
|
|
2275
|
+
app.post("/api/studio/rename", wrapHandler(handleRename));
|
|
2276
|
+
app.post("/api/studio/move", wrapHandler(handleMoveStream, true));
|
|
2277
|
+
app.post("/api/studio/sync", wrapHandler(handleSync, true));
|
|
2278
|
+
app.post("/api/studio/reprocess-stream", wrapHandler(handleReprocessStream, true));
|
|
2279
|
+
app.post("/api/studio/unprocess-stream", wrapHandler(handleUnprocessStream, true));
|
|
2280
|
+
app.post("/api/studio/download-stream", wrapHandler(handleDownloadStream, true));
|
|
2281
|
+
app.post("/api/studio/scan", wrapHandler(handleScanStream, true));
|
|
2282
|
+
app.post("/api/studio/delete-orphans", wrapHandler(handleDeleteOrphans));
|
|
2283
|
+
app.post("/api/studio/import", wrapHandler(handleImportUrls, true));
|
|
2284
|
+
app.post("/api/studio/cdns", wrapHandler(handleUpdateCdns));
|
|
2285
|
+
app.post("/api/studio/generate-favicon", wrapHandler(handleGenerateFavicon, true));
|
|
2286
|
+
app.delete("/api/studio/delete", wrapHandler(handleDelete));
|
|
2287
|
+
app.use("/public", express.static(join(workspace, "public")));
|
|
2288
|
+
const clientDir = resolve(__dirname, "../client");
|
|
2289
|
+
app.get("/", (req, res) => {
|
|
2290
|
+
const htmlPath = join(clientDir, "index.html");
|
|
2291
|
+
if (existsSync(htmlPath)) {
|
|
2292
|
+
let html = readFileSync(htmlPath, "utf-8");
|
|
2293
|
+
const script = `<script>window.__STUDIO_WORKSPACE__ = ${JSON.stringify(workspace)};</script>`;
|
|
2294
|
+
html = html.replace("</head>", `${script}</head>`);
|
|
2295
|
+
res.type("html").send(html);
|
|
2296
|
+
} else {
|
|
2297
|
+
res.status(404).send("Client not built. Run npm run build first.");
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
app.use(express.static(clientDir));
|
|
2301
|
+
app.listen(port, () => {
|
|
2302
|
+
console.log(`
|
|
2303
|
+
\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
|
|
2304
|
+
\u2502 Studio - Media Manager \u2502
|
|
2305
|
+
\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
|
|
2306
|
+
\u2502 Workspace: ${workspace.length > 24 ? "..." + workspace.slice(-21) : workspace.padEnd(24)}\u2502
|
|
2307
|
+
\u2502 URL: http://localhost:${port} \u2502
|
|
2308
|
+
\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
|
|
2309
|
+
`);
|
|
2310
|
+
if (open) {
|
|
2311
|
+
import("open").then((mod) => {
|
|
2312
|
+
mod.default(`http://localhost:${port}`);
|
|
2313
|
+
}).catch(() => {
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
2519
2317
|
}
|
|
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);
|
|
2318
|
+
function wrapHandler(handler, streaming = false) {
|
|
2319
|
+
return async (req, res) => {
|
|
2320
|
+
try {
|
|
2321
|
+
const request = createFetchRequest(req);
|
|
2322
|
+
const response = await handler(request);
|
|
2323
|
+
if (streaming) {
|
|
2324
|
+
await sendStreamingResponse(res, response);
|
|
2325
|
+
} else {
|
|
2326
|
+
await sendResponse(res, response);
|
|
2327
|
+
}
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
console.error("Handler error:", error);
|
|
2330
|
+
res.status(500).json({ error: "Internal server error" });
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function createFetchRequest(req) {
|
|
2335
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
2336
|
+
const headers = new Headers();
|
|
2337
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
2338
|
+
if (value) {
|
|
2339
|
+
if (Array.isArray(value)) {
|
|
2340
|
+
value.forEach((v) => headers.append(key, v));
|
|
2341
|
+
} else {
|
|
2342
|
+
headers.set(key, value);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2570
2345
|
}
|
|
2571
|
-
|
|
2572
|
-
|
|
2346
|
+
const init = {
|
|
2347
|
+
method: req.method,
|
|
2348
|
+
headers
|
|
2349
|
+
};
|
|
2350
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
2351
|
+
if (req.body) {
|
|
2352
|
+
init.body = JSON.stringify(req.body);
|
|
2353
|
+
}
|
|
2573
2354
|
}
|
|
2574
|
-
return
|
|
2355
|
+
return new globalThis.Request(url.toString(), init);
|
|
2356
|
+
}
|
|
2357
|
+
async function sendResponse(res, response) {
|
|
2358
|
+
res.status(response.status);
|
|
2359
|
+
response.headers.forEach((value, key) => {
|
|
2360
|
+
res.setHeader(key, value);
|
|
2361
|
+
});
|
|
2362
|
+
const body = await response.text();
|
|
2363
|
+
res.send(body);
|
|
2575
2364
|
}
|
|
2576
|
-
async function
|
|
2577
|
-
|
|
2578
|
-
|
|
2365
|
+
async function sendStreamingResponse(res, response) {
|
|
2366
|
+
res.status(response.status);
|
|
2367
|
+
response.headers.forEach((value, key) => {
|
|
2368
|
+
res.setHeader(key, value);
|
|
2369
|
+
});
|
|
2370
|
+
if (response.body) {
|
|
2371
|
+
const reader = response.body.getReader();
|
|
2372
|
+
const decoder = new TextDecoder();
|
|
2373
|
+
try {
|
|
2374
|
+
while (true) {
|
|
2375
|
+
const { done, value } = await reader.read();
|
|
2376
|
+
if (done) break;
|
|
2377
|
+
res.write(decoder.decode(value, { stream: true }));
|
|
2378
|
+
}
|
|
2379
|
+
res.end();
|
|
2380
|
+
} catch (error) {
|
|
2381
|
+
console.error("Streaming error:", error);
|
|
2382
|
+
res.end();
|
|
2383
|
+
}
|
|
2384
|
+
} else {
|
|
2385
|
+
const body = await response.text();
|
|
2386
|
+
res.send(body);
|
|
2579
2387
|
}
|
|
2580
|
-
return handleDelete(request);
|
|
2581
2388
|
}
|
|
2582
2389
|
export {
|
|
2583
|
-
|
|
2584
|
-
GET,
|
|
2585
|
-
POST
|
|
2390
|
+
startServer
|
|
2586
2391
|
};
|
|
2587
|
-
//# sourceMappingURL=index.
|
|
2392
|
+
//# sourceMappingURL=index.js.map
|