@gallop.software/studio 0.1.106 → 0.1.107
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-WUWW66GV.mjs → StudioUI-4HMRUI6W.mjs} +5 -6
- package/dist/StudioUI-4HMRUI6W.mjs.map +1 -0
- package/dist/{StudioUI-4MJ3CHCX.js → StudioUI-GNVTJIVG.js} +5 -6
- package/dist/StudioUI-GNVTJIVG.js.map +1 -0
- package/dist/handlers/index.js +175 -59
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/index.mjs +150 -34
- 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-4MJ3CHCX.js.map +0 -1
- package/dist/StudioUI-WUWW66GV.mjs.map +0 -1
package/dist/handlers/index.mjs
CHANGED
|
@@ -150,7 +150,7 @@ async function processImage(buffer, imageKey) {
|
|
|
150
150
|
// src/handlers/utils/cdn.ts
|
|
151
151
|
import { promises as fs3 } from "fs";
|
|
152
152
|
import path4 from "path";
|
|
153
|
-
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
153
|
+
import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
|
|
154
154
|
function getR2Client() {
|
|
155
155
|
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
156
156
|
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
@@ -210,6 +210,56 @@ async function deleteLocalThumbnails(imageKey) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
+
async function downloadFromRemoteUrl(url) {
|
|
214
|
+
const response = await fetch(url);
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
throw new Error(`Failed to download from ${url}: ${response.status}`);
|
|
217
|
+
}
|
|
218
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
219
|
+
return Buffer.from(arrayBuffer);
|
|
220
|
+
}
|
|
221
|
+
async function uploadOriginalToCdn(imageKey) {
|
|
222
|
+
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
223
|
+
if (!bucketName) throw new Error("R2 bucket not configured");
|
|
224
|
+
const r2 = getR2Client();
|
|
225
|
+
const localPath = path4.join(process.cwd(), "public", imageKey);
|
|
226
|
+
const fileBuffer = await fs3.readFile(localPath);
|
|
227
|
+
await r2.send(
|
|
228
|
+
new PutObjectCommand({
|
|
229
|
+
Bucket: bucketName,
|
|
230
|
+
Key: imageKey.replace(/^\//, ""),
|
|
231
|
+
Body: fileBuffer,
|
|
232
|
+
ContentType: getContentType(imageKey)
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
async function deleteFromCdn(imageKey, hasThumbnails) {
|
|
237
|
+
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
238
|
+
if (!bucketName) throw new Error("R2 bucket not configured");
|
|
239
|
+
const r2 = getR2Client();
|
|
240
|
+
try {
|
|
241
|
+
await r2.send(
|
|
242
|
+
new DeleteObjectCommand({
|
|
243
|
+
Bucket: bucketName,
|
|
244
|
+
Key: imageKey.replace(/^\//, "")
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
if (hasThumbnails) {
|
|
250
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
251
|
+
try {
|
|
252
|
+
await r2.send(
|
|
253
|
+
new DeleteObjectCommand({
|
|
254
|
+
Bucket: bucketName,
|
|
255
|
+
Key: thumbPath.replace(/^\//, "")
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
213
263
|
|
|
214
264
|
// src/handlers/list.ts
|
|
215
265
|
async function handleList(request) {
|
|
@@ -744,49 +794,103 @@ async function handleMove(request) {
|
|
|
744
794
|
if (!absoluteDestination.startsWith(path6.join(process.cwd(), "public"))) {
|
|
745
795
|
return NextResponse2.json({ error: "Invalid destination" }, { status: 400 });
|
|
746
796
|
}
|
|
747
|
-
|
|
748
|
-
const destStats = await fs5.stat(absoluteDestination);
|
|
749
|
-
if (!destStats.isDirectory()) {
|
|
750
|
-
return NextResponse2.json({ error: "Destination is not a folder" }, { status: 400 });
|
|
751
|
-
}
|
|
752
|
-
} catch {
|
|
753
|
-
return NextResponse2.json({ error: "Destination folder not found" }, { status: 404 });
|
|
754
|
-
}
|
|
797
|
+
await fs5.mkdir(absoluteDestination, { recursive: true });
|
|
755
798
|
const moved = [];
|
|
756
799
|
const errors = [];
|
|
757
800
|
const meta = await loadMeta();
|
|
801
|
+
const cdnUrls = getCdnUrls(meta);
|
|
802
|
+
const r2PublicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\/$/, "") || "";
|
|
758
803
|
let metaChanged = false;
|
|
759
804
|
for (const itemPath of paths) {
|
|
760
805
|
const safePath = itemPath.replace(/\.\./g, "");
|
|
761
|
-
const absolutePath = path6.join(process.cwd(), safePath);
|
|
762
806
|
const itemName = path6.basename(safePath);
|
|
763
807
|
const newAbsolutePath = path6.join(absoluteDestination, itemName);
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
await fs5.access(absolutePath);
|
|
770
|
-
} catch {
|
|
771
|
-
errors.push(`${itemName} not found`);
|
|
772
|
-
continue;
|
|
773
|
-
}
|
|
774
|
-
try {
|
|
775
|
-
await fs5.access(newAbsolutePath);
|
|
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]) {
|
|
776
813
|
errors.push(`${itemName} already exists in destination`);
|
|
777
814
|
continue;
|
|
778
|
-
} catch {
|
|
779
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;
|
|
780
823
|
try {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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);
|
|
887
|
+
errors.push(`${itemName} already exists in destination`);
|
|
888
|
+
continue;
|
|
889
|
+
} catch {
|
|
890
|
+
}
|
|
891
|
+
await fs5.rename(absolutePath, newAbsolutePath);
|
|
892
|
+
const stats = await fs5.stat(newAbsolutePath);
|
|
893
|
+
if (stats.isFile() && isImage && entry) {
|
|
790
894
|
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
791
895
|
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
792
896
|
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
@@ -801,10 +905,22 @@ async function handleMove(request) {
|
|
|
801
905
|
delete meta[oldKey];
|
|
802
906
|
meta[newKey] = entry;
|
|
803
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;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
804
919
|
}
|
|
920
|
+
moved.push(itemPath);
|
|
805
921
|
}
|
|
806
|
-
|
|
807
|
-
|
|
922
|
+
} catch (err) {
|
|
923
|
+
console.error(`Failed to move ${itemName}:`, err);
|
|
808
924
|
errors.push(`Failed to move ${itemName}`);
|
|
809
925
|
}
|
|
810
926
|
}
|