@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.
@@ -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 handleMove(request) {
784
- try {
785
- const { paths, destination } = await request.json();
786
- if (!paths || !Array.isArray(paths) || paths.length === 0) {
787
- return NextResponse2.json({ error: "Paths are required" }, { status: 400 });
788
- }
789
- if (!destination || typeof destination !== "string") {
790
- return NextResponse2.json({ error: "Destination is required" }, { status: 400 });
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
- if (isRemote && isImage) {
825
- const remoteUrl = `${fileCdnUrl}${oldKey}`;
826
- const buffer = await downloadFromRemoteUrl(remoteUrl);
827
- await fs5.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
828
- await fs5.writeFile(newAbsolutePath, buffer);
829
- const newEntry = {
830
- w: entry?.w,
831
- h: entry?.h,
832
- b: entry?.b
833
- // Don't copy p since remote images don't have local thumbnails
834
- // Don't copy c since it's now local
835
- };
836
- delete meta[oldKey];
837
- meta[newKey] = newEntry;
838
- metaChanged = true;
839
- moved.push(itemPath);
840
- } else if (isPushedToR2 && isImage) {
841
- const buffer = await downloadFromCdn(oldKey);
842
- await fs5.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
843
- await fs5.writeFile(newAbsolutePath, buffer);
844
- const newEntry = {
845
- w: entry?.w,
846
- h: entry?.h,
847
- b: entry?.b
848
- };
849
- if (hasProcessedThumbnails) {
850
- const processedEntry = await processImage(buffer, newKey);
851
- newEntry.w = processedEntry.w;
852
- newEntry.h = processedEntry.h;
853
- newEntry.b = processedEntry.b;
854
- newEntry.p = 1;
855
- }
856
- await uploadOriginalToCdn(newKey);
857
- if (hasProcessedThumbnails) {
858
- await uploadToCdn(newKey);
859
- }
860
- await deleteFromCdn(oldKey, hasProcessedThumbnails);
861
- try {
862
- await fs5.unlink(newAbsolutePath);
863
- } catch {
864
- }
865
- if (hasProcessedThumbnails) {
866
- await deleteLocalThumbnails(newKey);
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
- await fs5.rename(absolutePath, newAbsolutePath);
892
- const stats = await fs5.stat(newAbsolutePath);
893
- if (stats.isFile() && isImage && entry) {
894
- const oldThumbPaths = getAllThumbnailPaths(oldKey);
895
- const newThumbPaths = getAllThumbnailPaths(newKey);
896
- for (let i = 0; i < oldThumbPaths.length; i++) {
897
- const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[i]);
898
- const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[i]);
899
- await fs5.mkdir(path6.dirname(newThumbPath), { recursive: true });
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.rename(oldThumbPath, newThumbPath);
905
+ await fs5.unlink(newAbsolutePath);
902
906
  } catch {
903
907
  }
904
- }
905
- delete meta[oldKey];
906
- meta[newKey] = entry;
907
- metaChanged = true;
908
- } else if (stats.isDirectory()) {
909
- const oldPrefix = oldKey + "/";
910
- const newPrefix = newKey + "/";
911
- for (const key of Object.keys(meta)) {
912
- if (key.startsWith(oldPrefix)) {
913
- const newMetaKey = newPrefix + key.slice(oldPrefix.length);
914
- meta[newMetaKey] = meta[key];
915
- delete meta[key];
916
- metaChanged = true;
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
- } catch (err) {
923
- console.error(`Failed to move ${itemName}:`, err);
924
- errors.push(`Failed to move ${itemName}`);
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
- if (metaChanged) {
928
- await saveMeta(meta);
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
- return NextResponse2.json({
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 handleMove(request);
1644
+ return handleMoveStream(request);
1595
1645
  }
1596
1646
  if (route === "scan") {
1597
1647
  return handleScanStream();