@gallop.software/studio 0.1.107 → 0.1.109
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{StudioUI-GNVTJIVG.js → StudioUI-JE22LQN7.js} +77 -29
- package/dist/StudioUI-JE22LQN7.js.map +1 -0
- package/dist/{StudioUI-4HMRUI6W.mjs → StudioUI-OFQB3F3W.mjs} +76 -28
- package/dist/StudioUI-OFQB3F3W.mjs.map +1 -0
- package/dist/handlers/index.js +219 -169
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/index.mjs +196 -146
- package/dist/handlers/index.mjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/StudioUI-4HMRUI6W.mjs.map +0 -1
- package/dist/StudioUI-GNVTJIVG.js.map +0 -1
package/dist/handlers/index.mjs
CHANGED
|
@@ -270,14 +270,37 @@ async function handleList(request) {
|
|
|
270
270
|
const fileEntries = getFileEntries(meta);
|
|
271
271
|
const cdnUrls = getCdnUrls(meta);
|
|
272
272
|
const r2PublicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "") || "";
|
|
273
|
-
if (fileEntries.length === 0) {
|
|
274
|
-
return NextResponse.json({ items: [], isEmpty: true });
|
|
275
|
-
}
|
|
276
273
|
const relativePath = requestedPath.replace(/^public\/?/, "");
|
|
277
274
|
const pathPrefix = relativePath ? `/${relativePath}/` : "/";
|
|
278
275
|
const items = [];
|
|
279
276
|
const seenFolders = /* @__PURE__ */ new Set();
|
|
280
277
|
const metaKeys = fileEntries.map(([key]) => key);
|
|
278
|
+
const absoluteDir = path5.join(process.cwd(), requestedPath);
|
|
279
|
+
try {
|
|
280
|
+
const dirEntries = await fs4.readdir(absoluteDir, { withFileTypes: true });
|
|
281
|
+
for (const entry of dirEntries) {
|
|
282
|
+
if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "images") {
|
|
283
|
+
if (!seenFolders.has(entry.name)) {
|
|
284
|
+
seenFolders.add(entry.name);
|
|
285
|
+
const folderPrefix = pathPrefix === "/" ? `/${entry.name}/` : `${pathPrefix}${entry.name}/`;
|
|
286
|
+
let fileCount = 0;
|
|
287
|
+
for (const k of metaKeys) {
|
|
288
|
+
if (k.startsWith(folderPrefix)) fileCount++;
|
|
289
|
+
}
|
|
290
|
+
items.push({
|
|
291
|
+
name: entry.name,
|
|
292
|
+
path: relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`,
|
|
293
|
+
type: "folder",
|
|
294
|
+
fileCount
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
if (fileEntries.length === 0 && items.length === 0) {
|
|
302
|
+
return NextResponse.json({ items: [], isEmpty: true });
|
|
303
|
+
}
|
|
281
304
|
for (const [key, entry] of fileEntries) {
|
|
282
305
|
if (!key.startsWith(pathPrefix) && pathPrefix !== "/") continue;
|
|
283
306
|
if (pathPrefix === "/" && !key.startsWith("/")) continue;
|
|
@@ -780,162 +803,189 @@ async function handleRename(request) {
|
|
|
780
803
|
return NextResponse2.json({ error: "Failed to rename" }, { status: 500 });
|
|
781
804
|
}
|
|
782
805
|
}
|
|
783
|
-
async function
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const safeDestination = destination.replace(/\.\./g, "");
|
|
793
|
-
const absoluteDestination = path6.join(process.cwd(), safeDestination);
|
|
794
|
-
if (!absoluteDestination.startsWith(path6.join(process.cwd(), "public"))) {
|
|
795
|
-
return NextResponse2.json({ error: "Invalid destination" }, { status: 400 });
|
|
796
|
-
}
|
|
797
|
-
await fs5.mkdir(absoluteDestination, { recursive: true });
|
|
798
|
-
const moved = [];
|
|
799
|
-
const errors = [];
|
|
800
|
-
const meta = await loadMeta();
|
|
801
|
-
const cdnUrls = getCdnUrls(meta);
|
|
802
|
-
const r2PublicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "") || "";
|
|
803
|
-
let metaChanged = false;
|
|
804
|
-
for (const itemPath of paths) {
|
|
805
|
-
const safePath = itemPath.replace(/\.\./g, "");
|
|
806
|
-
const itemName = path6.basename(safePath);
|
|
807
|
-
const newAbsolutePath = path6.join(absoluteDestination, itemName);
|
|
808
|
-
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
809
|
-
const newRelativePath = path6.join(safeDestination.replace(/^public\//, ""), itemName);
|
|
810
|
-
const oldKey = "/" + oldRelativePath;
|
|
811
|
-
const newKey = "/" + newRelativePath;
|
|
812
|
-
if (meta[newKey]) {
|
|
813
|
-
errors.push(`${itemName} already exists in destination`);
|
|
814
|
-
continue;
|
|
815
|
-
}
|
|
816
|
-
const entry = meta[oldKey];
|
|
817
|
-
const isImage = isImageFile(itemName);
|
|
818
|
-
const isInCloud = entry?.c !== void 0;
|
|
819
|
-
const fileCdnUrl = isInCloud && entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
820
|
-
const isRemote = isInCloud && (!r2PublicUrl || fileCdnUrl !== r2PublicUrl);
|
|
821
|
-
const isPushedToR2 = isInCloud && r2PublicUrl && fileCdnUrl === r2PublicUrl;
|
|
822
|
-
const hasProcessedThumbnails = entry?.p === 1;
|
|
806
|
+
async function handleMoveStream(request) {
|
|
807
|
+
const encoder = new TextEncoder();
|
|
808
|
+
const stream = new ReadableStream({
|
|
809
|
+
async start(controller) {
|
|
810
|
+
const sendEvent = (data) => {
|
|
811
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
812
|
+
|
|
813
|
+
`));
|
|
814
|
+
};
|
|
823
815
|
try {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
newEntry.c = entry?.c;
|
|
869
|
-
delete meta[oldKey];
|
|
870
|
-
meta[newKey] = newEntry;
|
|
871
|
-
metaChanged = true;
|
|
872
|
-
moved.push(itemPath);
|
|
873
|
-
} else {
|
|
874
|
-
const absolutePath = path6.join(process.cwd(), safePath);
|
|
875
|
-
if (absoluteDestination.startsWith(absolutePath + path6.sep)) {
|
|
876
|
-
errors.push(`Cannot move ${itemName} into itself`);
|
|
877
|
-
continue;
|
|
878
|
-
}
|
|
879
|
-
try {
|
|
880
|
-
await fs5.access(absolutePath);
|
|
881
|
-
} catch {
|
|
882
|
-
errors.push(`${itemName} not found`);
|
|
883
|
-
continue;
|
|
884
|
-
}
|
|
885
|
-
try {
|
|
886
|
-
await fs5.access(newAbsolutePath);
|
|
816
|
+
const { paths, destination } = await request.json();
|
|
817
|
+
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
818
|
+
sendEvent({ type: "error", message: "Paths are required" });
|
|
819
|
+
controller.close();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (!destination || typeof destination !== "string") {
|
|
823
|
+
sendEvent({ type: "error", message: "Destination is required" });
|
|
824
|
+
controller.close();
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
const safeDestination = destination.replace(/\.\./g, "");
|
|
828
|
+
const absoluteDestination = path6.join(process.cwd(), safeDestination);
|
|
829
|
+
if (!absoluteDestination.startsWith(path6.join(process.cwd(), "public"))) {
|
|
830
|
+
sendEvent({ type: "error", message: "Invalid destination" });
|
|
831
|
+
controller.close();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
await fs5.mkdir(absoluteDestination, { recursive: true });
|
|
835
|
+
const meta = await loadMeta();
|
|
836
|
+
const cdnUrls = getCdnUrls(meta);
|
|
837
|
+
const r2PublicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "") || "";
|
|
838
|
+
const moved = [];
|
|
839
|
+
const errors = [];
|
|
840
|
+
const total = paths.length;
|
|
841
|
+
sendEvent({ type: "start", total });
|
|
842
|
+
for (let i = 0; i < paths.length; i++) {
|
|
843
|
+
const itemPath = paths[i];
|
|
844
|
+
const safePath = itemPath.replace(/\.\./g, "");
|
|
845
|
+
const itemName = path6.basename(safePath);
|
|
846
|
+
const newAbsolutePath = path6.join(absoluteDestination, itemName);
|
|
847
|
+
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
848
|
+
const newRelativePath = path6.join(safeDestination.replace(/^public\//, ""), itemName);
|
|
849
|
+
const oldKey = "/" + oldRelativePath;
|
|
850
|
+
const newKey = "/" + newRelativePath;
|
|
851
|
+
sendEvent({
|
|
852
|
+
type: "progress",
|
|
853
|
+
current: i + 1,
|
|
854
|
+
total,
|
|
855
|
+
percent: Math.round((i + 1) / total * 100),
|
|
856
|
+
currentFile: itemName
|
|
857
|
+
});
|
|
858
|
+
if (meta[newKey]) {
|
|
887
859
|
errors.push(`${itemName} already exists in destination`);
|
|
888
860
|
continue;
|
|
889
|
-
} catch {
|
|
890
861
|
}
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
862
|
+
const entry = meta[oldKey];
|
|
863
|
+
const isImage = isImageFile(itemName);
|
|
864
|
+
const isInCloud = entry?.c !== void 0;
|
|
865
|
+
const fileCdnUrl = isInCloud && entry.c !== void 0 ? cdnUrls[entry.c] : void 0;
|
|
866
|
+
const isRemote = isInCloud && (!r2PublicUrl || fileCdnUrl !== r2PublicUrl);
|
|
867
|
+
const isPushedToR2 = isInCloud && r2PublicUrl && fileCdnUrl === r2PublicUrl;
|
|
868
|
+
const hasProcessedThumbnails = entry?.p === 1;
|
|
869
|
+
try {
|
|
870
|
+
if (isRemote && isImage) {
|
|
871
|
+
const remoteUrl = `${fileCdnUrl}${oldKey}`;
|
|
872
|
+
const buffer = await downloadFromRemoteUrl(remoteUrl);
|
|
873
|
+
await fs5.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
|
|
874
|
+
await fs5.writeFile(newAbsolutePath, buffer);
|
|
875
|
+
const newEntry = {
|
|
876
|
+
w: entry?.w,
|
|
877
|
+
h: entry?.h,
|
|
878
|
+
b: entry?.b
|
|
879
|
+
};
|
|
880
|
+
delete meta[oldKey];
|
|
881
|
+
meta[newKey] = newEntry;
|
|
882
|
+
moved.push(itemPath);
|
|
883
|
+
} else if (isPushedToR2 && isImage) {
|
|
884
|
+
const buffer = await downloadFromCdn(oldKey);
|
|
885
|
+
await fs5.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
|
|
886
|
+
await fs5.writeFile(newAbsolutePath, buffer);
|
|
887
|
+
const newEntry = {
|
|
888
|
+
w: entry?.w,
|
|
889
|
+
h: entry?.h,
|
|
890
|
+
b: entry?.b
|
|
891
|
+
};
|
|
892
|
+
if (hasProcessedThumbnails) {
|
|
893
|
+
const processedEntry = await processImage(buffer, newKey);
|
|
894
|
+
newEntry.w = processedEntry.w;
|
|
895
|
+
newEntry.h = processedEntry.h;
|
|
896
|
+
newEntry.b = processedEntry.b;
|
|
897
|
+
newEntry.p = 1;
|
|
898
|
+
}
|
|
899
|
+
await uploadOriginalToCdn(newKey);
|
|
900
|
+
if (hasProcessedThumbnails) {
|
|
901
|
+
await uploadToCdn(newKey);
|
|
902
|
+
}
|
|
903
|
+
await deleteFromCdn(oldKey, hasProcessedThumbnails);
|
|
900
904
|
try {
|
|
901
|
-
await fs5.
|
|
905
|
+
await fs5.unlink(newAbsolutePath);
|
|
902
906
|
} catch {
|
|
903
907
|
}
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
908
|
+
if (hasProcessedThumbnails) {
|
|
909
|
+
await deleteLocalThumbnails(newKey);
|
|
910
|
+
}
|
|
911
|
+
newEntry.c = entry?.c;
|
|
912
|
+
delete meta[oldKey];
|
|
913
|
+
meta[newKey] = newEntry;
|
|
914
|
+
moved.push(itemPath);
|
|
915
|
+
} else {
|
|
916
|
+
const absolutePath = path6.join(process.cwd(), safePath);
|
|
917
|
+
if (absoluteDestination.startsWith(absolutePath + path6.sep)) {
|
|
918
|
+
errors.push(`Cannot move ${itemName} into itself`);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
try {
|
|
922
|
+
await fs5.access(absolutePath);
|
|
923
|
+
} catch {
|
|
924
|
+
errors.push(`${itemName} not found`);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
await fs5.access(newAbsolutePath);
|
|
929
|
+
errors.push(`${itemName} already exists in destination`);
|
|
930
|
+
continue;
|
|
931
|
+
} catch {
|
|
917
932
|
}
|
|
933
|
+
await fs5.rename(absolutePath, newAbsolutePath);
|
|
934
|
+
const stats = await fs5.stat(newAbsolutePath);
|
|
935
|
+
if (stats.isFile() && isImage && entry) {
|
|
936
|
+
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
937
|
+
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
938
|
+
for (let j = 0; j < oldThumbPaths.length; j++) {
|
|
939
|
+
const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[j]);
|
|
940
|
+
const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[j]);
|
|
941
|
+
await fs5.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
942
|
+
try {
|
|
943
|
+
await fs5.rename(oldThumbPath, newThumbPath);
|
|
944
|
+
} catch {
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
delete meta[oldKey];
|
|
948
|
+
meta[newKey] = entry;
|
|
949
|
+
} else if (stats.isDirectory()) {
|
|
950
|
+
const oldPrefix = oldKey + "/";
|
|
951
|
+
const newPrefix = newKey + "/";
|
|
952
|
+
for (const key of Object.keys(meta)) {
|
|
953
|
+
if (key.startsWith(oldPrefix)) {
|
|
954
|
+
const newMetaKey = newPrefix + key.slice(oldPrefix.length);
|
|
955
|
+
meta[newMetaKey] = meta[key];
|
|
956
|
+
delete meta[key];
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
moved.push(itemPath);
|
|
918
961
|
}
|
|
962
|
+
} catch (err) {
|
|
963
|
+
console.error(`Failed to move ${itemName}:`, err);
|
|
964
|
+
errors.push(`Failed to move ${itemName}`);
|
|
919
965
|
}
|
|
920
|
-
moved.push(itemPath);
|
|
921
966
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
967
|
+
await saveMeta(meta);
|
|
968
|
+
sendEvent({
|
|
969
|
+
type: "complete",
|
|
970
|
+
moved: moved.length,
|
|
971
|
+
errors: errors.length,
|
|
972
|
+
errorMessages: errors
|
|
973
|
+
});
|
|
974
|
+
} catch (error) {
|
|
975
|
+
console.error("Failed to move:", error);
|
|
976
|
+
sendEvent({ type: "error", message: "Failed to move items" });
|
|
977
|
+
} finally {
|
|
978
|
+
controller.close();
|
|
925
979
|
}
|
|
926
980
|
}
|
|
927
|
-
|
|
928
|
-
|
|
981
|
+
});
|
|
982
|
+
return new Response(stream, {
|
|
983
|
+
headers: {
|
|
984
|
+
"Content-Type": "text/event-stream",
|
|
985
|
+
"Cache-Control": "no-cache",
|
|
986
|
+
"Connection": "keep-alive"
|
|
929
987
|
}
|
|
930
|
-
|
|
931
|
-
success: errors.length === 0,
|
|
932
|
-
moved,
|
|
933
|
-
errors: errors.length > 0 ? errors : void 0
|
|
934
|
-
});
|
|
935
|
-
} catch (error) {
|
|
936
|
-
console.error("Failed to move:", error);
|
|
937
|
-
return NextResponse2.json({ error: "Failed to move items" }, { status: 500 });
|
|
938
|
-
}
|
|
988
|
+
});
|
|
939
989
|
}
|
|
940
990
|
|
|
941
991
|
// src/handlers/images.ts
|
|
@@ -1591,7 +1641,7 @@ async function POST(request) {
|
|
|
1591
1641
|
return handleRename(request);
|
|
1592
1642
|
}
|
|
1593
1643
|
if (route === "move") {
|
|
1594
|
-
return
|
|
1644
|
+
return handleMoveStream(request);
|
|
1595
1645
|
}
|
|
1596
1646
|
if (route === "scan") {
|
|
1597
1647
|
return handleScanStream();
|