@gallop.software/studio 0.1.80 → 0.1.82
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-VPNL5NMI.mjs → StudioUI-VJVOSOPD.mjs} +485 -402
- package/dist/StudioUI-VJVOSOPD.mjs.map +1 -0
- package/dist/{StudioUI-OAZ7CTB4.js → StudioUI-YFDO5MGG.js} +420 -337
- package/dist/StudioUI-YFDO5MGG.js.map +1 -0
- package/dist/{handlers.d.ts → handlers/index.d.mts} +1 -1
- package/dist/{handlers.d.mts → handlers/index.d.ts} +1 -1
- package/dist/{handlers.js → handlers/index.js} +611 -579
- package/dist/handlers/index.js.map +1 -0
- package/dist/{handlers.mjs → handlers/index.mjs} +620 -588
- package/dist/handlers/index.mjs.map +1 -0
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +4 -4
- package/dist/StudioUI-OAZ7CTB4.js.map +0 -1
- package/dist/StudioUI-VPNL5NMI.mjs.map +0 -1
- package/dist/handlers.js.map +0 -1
- package/dist/handlers.mjs.map +0 -1
|
@@ -1,96 +1,228 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAllThumbnailPaths
|
|
3
|
-
} from "
|
|
3
|
+
} from "../chunk-3RI33B7A.mjs";
|
|
4
4
|
|
|
5
|
-
// src/handlers.ts
|
|
5
|
+
// src/handlers/index.ts
|
|
6
|
+
import { NextResponse as NextResponse4 } from "next/server";
|
|
7
|
+
|
|
8
|
+
// src/handlers/list.ts
|
|
6
9
|
import { NextResponse } from "next/server";
|
|
10
|
+
import { promises as fs5 } from "fs";
|
|
11
|
+
import path5 from "path";
|
|
12
|
+
import sharp2 from "sharp";
|
|
13
|
+
|
|
14
|
+
// src/handlers/utils/meta.ts
|
|
7
15
|
import { promises as fs } from "fs";
|
|
8
16
|
import path from "path";
|
|
17
|
+
async function loadMeta() {
|
|
18
|
+
const metaPath = path.join(process.cwd(), "_data", "_meta.json");
|
|
19
|
+
try {
|
|
20
|
+
const content = await fs.readFile(metaPath, "utf-8");
|
|
21
|
+
return JSON.parse(content);
|
|
22
|
+
} catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function saveMeta(meta) {
|
|
27
|
+
const dataDir = path.join(process.cwd(), "_data");
|
|
28
|
+
await fs.mkdir(dataDir, { recursive: true });
|
|
29
|
+
const metaPath = path.join(dataDir, "_meta.json");
|
|
30
|
+
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/handlers/utils/files.ts
|
|
34
|
+
import { promises as fs2 } from "fs";
|
|
35
|
+
import path2 from "path";
|
|
36
|
+
function isImageFile(filename) {
|
|
37
|
+
const ext = path2.extname(filename).toLowerCase();
|
|
38
|
+
return [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"].includes(ext);
|
|
39
|
+
}
|
|
40
|
+
function isMediaFile(filename) {
|
|
41
|
+
const ext = path2.extname(filename).toLowerCase();
|
|
42
|
+
if ([".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"].includes(ext)) return true;
|
|
43
|
+
if ([".mp4", ".webm", ".mov", ".avi", ".mkv", ".m4v"].includes(ext)) return true;
|
|
44
|
+
if ([".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac"].includes(ext)) return true;
|
|
45
|
+
if ([".pdf"].includes(ext)) return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
function getContentType(filePath) {
|
|
49
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
50
|
+
switch (ext) {
|
|
51
|
+
case ".jpg":
|
|
52
|
+
case ".jpeg":
|
|
53
|
+
return "image/jpeg";
|
|
54
|
+
case ".png":
|
|
55
|
+
return "image/png";
|
|
56
|
+
case ".gif":
|
|
57
|
+
return "image/gif";
|
|
58
|
+
case ".webp":
|
|
59
|
+
return "image/webp";
|
|
60
|
+
case ".svg":
|
|
61
|
+
return "image/svg+xml";
|
|
62
|
+
default:
|
|
63
|
+
return "application/octet-stream";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function getFolderStats(folderPath) {
|
|
67
|
+
let fileCount = 0;
|
|
68
|
+
let totalSize = 0;
|
|
69
|
+
async function scanFolder(dir) {
|
|
70
|
+
try {
|
|
71
|
+
const entries = await fs2.readdir(dir, { withFileTypes: true });
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (entry.name.startsWith(".")) continue;
|
|
74
|
+
const fullPath = path2.join(dir, entry.name);
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
await scanFolder(fullPath);
|
|
77
|
+
} else if (isMediaFile(entry.name)) {
|
|
78
|
+
fileCount++;
|
|
79
|
+
const stats = await fs2.stat(fullPath);
|
|
80
|
+
totalSize += stats.size;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
await scanFolder(folderPath);
|
|
87
|
+
return { fileCount, totalSize };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/handlers/utils/thumbnails.ts
|
|
91
|
+
import { promises as fs3 } from "fs";
|
|
92
|
+
import path3 from "path";
|
|
9
93
|
import sharp from "sharp";
|
|
10
94
|
import { encode } from "blurhash";
|
|
11
|
-
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
12
95
|
var DEFAULT_SIZES = {
|
|
13
96
|
small: { width: 300, suffix: "-sm" },
|
|
14
97
|
medium: { width: 700, suffix: "-md" },
|
|
15
98
|
large: { width: 1400, suffix: "-lg" }
|
|
16
99
|
};
|
|
17
|
-
async function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
100
|
+
async function processImage(buffer, imageKey) {
|
|
101
|
+
const sharpInstance = sharp(buffer);
|
|
102
|
+
const metadata = await sharpInstance.metadata();
|
|
103
|
+
const originalWidth = metadata.width || 0;
|
|
104
|
+
const originalHeight = metadata.height || 0;
|
|
105
|
+
const keyWithoutSlash = imageKey.startsWith("/") ? imageKey.slice(1) : imageKey;
|
|
106
|
+
const baseName = path3.basename(keyWithoutSlash, path3.extname(keyWithoutSlash));
|
|
107
|
+
const ext = path3.extname(keyWithoutSlash).toLowerCase();
|
|
108
|
+
const imageDir = path3.dirname(keyWithoutSlash);
|
|
109
|
+
const imagesPath = path3.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
|
|
110
|
+
await fs3.mkdir(imagesPath, { recursive: true });
|
|
111
|
+
const isPng = ext === ".png";
|
|
112
|
+
const outputExt = isPng ? ".png" : ".jpg";
|
|
113
|
+
const fullFileName = imageDir === "." ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`;
|
|
114
|
+
const fullPath = path3.join(process.cwd(), "public", "images", fullFileName);
|
|
115
|
+
if (isPng) {
|
|
116
|
+
await sharp(buffer).png({ quality: 85 }).toFile(fullPath);
|
|
117
|
+
} else {
|
|
118
|
+
await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath);
|
|
34
119
|
}
|
|
35
|
-
|
|
36
|
-
|
|
120
|
+
for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
|
|
121
|
+
const { width: maxWidth, suffix } = sizeConfig;
|
|
122
|
+
if (originalWidth <= maxWidth) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const ratio = originalHeight / originalWidth;
|
|
126
|
+
const newHeight = Math.round(maxWidth * ratio);
|
|
127
|
+
const sizeFileName = `${baseName}${suffix}${outputExt}`;
|
|
128
|
+
const sizeFilePath = imageDir === "." ? sizeFileName : `${imageDir}/${sizeFileName}`;
|
|
129
|
+
const sizePath = path3.join(process.cwd(), "public", "images", sizeFilePath);
|
|
130
|
+
if (isPng) {
|
|
131
|
+
await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
|
|
132
|
+
} else {
|
|
133
|
+
await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
|
|
134
|
+
}
|
|
37
135
|
}
|
|
38
|
-
|
|
136
|
+
const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
137
|
+
const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
|
|
138
|
+
return {
|
|
139
|
+
w: originalWidth,
|
|
140
|
+
h: originalHeight,
|
|
141
|
+
blur: blurhash
|
|
142
|
+
};
|
|
39
143
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (route === "sync") {
|
|
53
|
-
return handleSync(request);
|
|
54
|
-
}
|
|
55
|
-
if (route === "reprocess") {
|
|
56
|
-
return handleReprocess(request);
|
|
57
|
-
}
|
|
58
|
-
if (route === "process-all") {
|
|
59
|
-
return handleProcessAllStream();
|
|
60
|
-
}
|
|
61
|
-
if (route === "create-folder") {
|
|
62
|
-
return handleCreateFolder(request);
|
|
144
|
+
|
|
145
|
+
// src/handlers/utils/cdn.ts
|
|
146
|
+
import { promises as fs4 } from "fs";
|
|
147
|
+
import path4 from "path";
|
|
148
|
+
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
149
|
+
function getR2Client() {
|
|
150
|
+
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
151
|
+
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
152
|
+
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
153
|
+
if (!accountId || !accessKeyId || !secretAccessKey) {
|
|
154
|
+
throw new Error("R2 not configured");
|
|
63
155
|
}
|
|
64
|
-
|
|
65
|
-
|
|
156
|
+
return new S3Client({
|
|
157
|
+
region: "auto",
|
|
158
|
+
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
159
|
+
credentials: { accessKeyId, secretAccessKey }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function downloadFromCdn(originalPath) {
|
|
163
|
+
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
164
|
+
if (!bucketName) throw new Error("R2 bucket not configured");
|
|
165
|
+
const r2 = getR2Client();
|
|
166
|
+
const response = await r2.send(
|
|
167
|
+
new GetObjectCommand({
|
|
168
|
+
Bucket: bucketName,
|
|
169
|
+
Key: originalPath.replace(/^\//, "")
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
const stream = response.Body;
|
|
173
|
+
const chunks = [];
|
|
174
|
+
for await (const chunk of stream) {
|
|
175
|
+
chunks.push(Buffer.from(chunk));
|
|
66
176
|
}
|
|
67
|
-
|
|
68
|
-
|
|
177
|
+
return Buffer.concat(chunks);
|
|
178
|
+
}
|
|
179
|
+
async function uploadToCdn(imageKey) {
|
|
180
|
+
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
181
|
+
if (!bucketName) throw new Error("R2 bucket not configured");
|
|
182
|
+
const r2 = getR2Client();
|
|
183
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
184
|
+
const localPath = path4.join(process.cwd(), "public", thumbPath);
|
|
185
|
+
try {
|
|
186
|
+
const fileBuffer = await fs4.readFile(localPath);
|
|
187
|
+
await r2.send(
|
|
188
|
+
new PutObjectCommand({
|
|
189
|
+
Bucket: bucketName,
|
|
190
|
+
Key: thumbPath.replace(/^\//, ""),
|
|
191
|
+
Body: fileBuffer,
|
|
192
|
+
ContentType: getContentType(thumbPath)
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
} catch {
|
|
196
|
+
}
|
|
69
197
|
}
|
|
70
|
-
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
71
198
|
}
|
|
72
|
-
async function
|
|
73
|
-
|
|
74
|
-
|
|
199
|
+
async function deleteLocalThumbnails(imageKey) {
|
|
200
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
201
|
+
const localPath = path4.join(process.cwd(), "public", thumbPath);
|
|
202
|
+
try {
|
|
203
|
+
await fs4.unlink(localPath);
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
75
206
|
}
|
|
76
|
-
return handleDelete(request);
|
|
77
207
|
}
|
|
208
|
+
|
|
209
|
+
// src/handlers/list.ts
|
|
78
210
|
async function handleList(request) {
|
|
79
211
|
const searchParams = request.nextUrl.searchParams;
|
|
80
212
|
const requestedPath = searchParams.get("path") || "public";
|
|
81
213
|
try {
|
|
82
214
|
const safePath = requestedPath.replace(/\.\./g, "");
|
|
83
|
-
const absolutePath =
|
|
215
|
+
const absolutePath = path5.join(process.cwd(), safePath);
|
|
84
216
|
if (!absolutePath.startsWith(process.cwd())) {
|
|
85
217
|
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
86
218
|
}
|
|
87
219
|
const items = [];
|
|
88
|
-
const entries = await
|
|
220
|
+
const entries = await fs5.readdir(absolutePath, { withFileTypes: true });
|
|
89
221
|
for (const entry of entries) {
|
|
90
222
|
if (entry.name.startsWith(".")) continue;
|
|
91
|
-
const itemPath =
|
|
223
|
+
const itemPath = path5.join(safePath, entry.name);
|
|
92
224
|
if (entry.isDirectory()) {
|
|
93
|
-
const folderStats = await getFolderStats(
|
|
225
|
+
const folderStats = await getFolderStats(path5.join(absolutePath, entry.name));
|
|
94
226
|
items.push({
|
|
95
227
|
name: entry.name,
|
|
96
228
|
path: itemPath,
|
|
@@ -99,8 +231,8 @@ async function handleList(request) {
|
|
|
99
231
|
totalSize: folderStats.totalSize
|
|
100
232
|
});
|
|
101
233
|
} else if (isMediaFile(entry.name)) {
|
|
102
|
-
const filePath =
|
|
103
|
-
const stats = await
|
|
234
|
+
const filePath = path5.join(absolutePath, entry.name);
|
|
235
|
+
const stats = await fs5.stat(filePath);
|
|
104
236
|
const isImage = isImageFile(entry.name);
|
|
105
237
|
let thumbnail;
|
|
106
238
|
let hasThumbnail = false;
|
|
@@ -111,13 +243,13 @@ async function handleList(request) {
|
|
|
111
243
|
thumbnail = itemPath.replace("public", "");
|
|
112
244
|
hasThumbnail = true;
|
|
113
245
|
} else {
|
|
114
|
-
const ext =
|
|
115
|
-
const baseName =
|
|
246
|
+
const ext = path5.extname(entry.name).toLowerCase();
|
|
247
|
+
const baseName = path5.basename(entry.name, ext);
|
|
116
248
|
const thumbnailDir = relativePath ? `images/${relativePath}` : "images";
|
|
117
249
|
const thumbnailName = `${baseName}-sm${ext === ".png" ? ".png" : ".jpg"}`;
|
|
118
|
-
const thumbnailPath =
|
|
250
|
+
const thumbnailPath = path5.join(process.cwd(), "public", thumbnailDir, thumbnailName);
|
|
119
251
|
try {
|
|
120
|
-
await
|
|
252
|
+
await fs5.access(thumbnailPath);
|
|
121
253
|
thumbnail = `/${thumbnailDir}/${thumbnailName}`;
|
|
122
254
|
hasThumbnail = true;
|
|
123
255
|
} catch {
|
|
@@ -127,7 +259,7 @@ async function handleList(request) {
|
|
|
127
259
|
}
|
|
128
260
|
if (!entry.name.toLowerCase().endsWith(".svg")) {
|
|
129
261
|
try {
|
|
130
|
-
const metadata = await
|
|
262
|
+
const metadata = await sharp2(filePath).metadata();
|
|
131
263
|
if (metadata.width && metadata.height) {
|
|
132
264
|
dimensions = { width: metadata.width, height: metadata.height };
|
|
133
265
|
}
|
|
@@ -160,30 +292,30 @@ async function handleSearch(request) {
|
|
|
160
292
|
}
|
|
161
293
|
try {
|
|
162
294
|
const items = [];
|
|
163
|
-
const publicDir =
|
|
295
|
+
const publicDir = path5.join(process.cwd(), "public");
|
|
164
296
|
async function searchDir(dir, relativePath) {
|
|
165
297
|
try {
|
|
166
|
-
const entries = await
|
|
298
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
167
299
|
for (const entry of entries) {
|
|
168
300
|
if (entry.name.startsWith(".")) continue;
|
|
169
|
-
const fullPath =
|
|
301
|
+
const fullPath = path5.join(dir, entry.name);
|
|
170
302
|
const itemPath = relativePath ? `public/${relativePath}/${entry.name}` : `public/${entry.name}`;
|
|
171
303
|
const itemRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
172
304
|
if (entry.isDirectory()) {
|
|
173
305
|
await searchDir(fullPath, itemRelPath);
|
|
174
306
|
} else if (isImageFile(entry.name)) {
|
|
175
307
|
if (itemPath.toLowerCase().includes(query)) {
|
|
176
|
-
const stats = await
|
|
308
|
+
const stats = await fs5.stat(fullPath);
|
|
177
309
|
let thumbnail;
|
|
178
310
|
let hasThumbnail = false;
|
|
179
311
|
let dimensions;
|
|
180
|
-
const ext =
|
|
181
|
-
const baseName =
|
|
312
|
+
const ext = path5.extname(entry.name).toLowerCase();
|
|
313
|
+
const baseName = path5.basename(entry.name, ext);
|
|
182
314
|
const thumbnailDir = relativePath ? `images/${relativePath}` : "images";
|
|
183
315
|
const thumbnailName = `${baseName}-sm${ext === ".png" ? ".png" : ".jpg"}`;
|
|
184
|
-
const thumbnailPath =
|
|
316
|
+
const thumbnailPath = path5.join(process.cwd(), "public", thumbnailDir, thumbnailName);
|
|
185
317
|
try {
|
|
186
|
-
await
|
|
318
|
+
await fs5.access(thumbnailPath);
|
|
187
319
|
thumbnail = `/${thumbnailDir}/${thumbnailName}`;
|
|
188
320
|
hasThumbnail = true;
|
|
189
321
|
} catch {
|
|
@@ -192,7 +324,7 @@ async function handleSearch(request) {
|
|
|
192
324
|
}
|
|
193
325
|
if (!entry.name.toLowerCase().endsWith(".svg")) {
|
|
194
326
|
try {
|
|
195
|
-
const metadata = await
|
|
327
|
+
const metadata = await sharp2(fullPath).metadata();
|
|
196
328
|
if (metadata.width && metadata.height) {
|
|
197
329
|
dimensions = { width: metadata.width, height: metadata.height };
|
|
198
330
|
}
|
|
@@ -221,45 +353,129 @@ async function handleSearch(request) {
|
|
|
221
353
|
return NextResponse.json({ error: "Failed to search" }, { status: 500 });
|
|
222
354
|
}
|
|
223
355
|
}
|
|
224
|
-
async function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
356
|
+
async function handleListFolders() {
|
|
357
|
+
try {
|
|
358
|
+
const publicDir = path5.join(process.cwd(), "public");
|
|
359
|
+
const folders = [];
|
|
360
|
+
async function scanDir(dir, relativePath, depth) {
|
|
361
|
+
try {
|
|
362
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
363
|
+
for (const entry of entries) {
|
|
364
|
+
if (!entry.isDirectory()) continue;
|
|
365
|
+
if (entry.name.startsWith(".")) continue;
|
|
366
|
+
const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
367
|
+
folders.push({
|
|
368
|
+
path: `public/${folderRelativePath}`,
|
|
369
|
+
name: entry.name,
|
|
370
|
+
depth
|
|
371
|
+
});
|
|
372
|
+
await scanDir(path5.join(dir, entry.name), folderRelativePath, depth + 1);
|
|
239
373
|
}
|
|
374
|
+
} catch {
|
|
240
375
|
}
|
|
241
|
-
} catch {
|
|
242
376
|
}
|
|
377
|
+
folders.push({ path: "public", name: "public", depth: 0 });
|
|
378
|
+
await scanDir(publicDir, "", 1);
|
|
379
|
+
return NextResponse.json({ folders });
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error("Failed to list folders:", error);
|
|
382
|
+
return NextResponse.json({ error: "Failed to list folders" }, { status: 500 });
|
|
243
383
|
}
|
|
244
|
-
await scanFolder(folderPath);
|
|
245
|
-
return { fileCount, totalSize };
|
|
246
384
|
}
|
|
247
|
-
async function
|
|
385
|
+
async function handleCountImages() {
|
|
248
386
|
try {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
387
|
+
const allImages = [];
|
|
388
|
+
async function scanPublicFolder(dir, relativePath = "") {
|
|
389
|
+
try {
|
|
390
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
391
|
+
for (const entry of entries) {
|
|
392
|
+
if (entry.name.startsWith(".")) continue;
|
|
393
|
+
const fullPath = path5.join(dir, entry.name);
|
|
394
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
395
|
+
if (relPath === "images" || relPath.startsWith("images/")) continue;
|
|
396
|
+
if (entry.isDirectory()) {
|
|
397
|
+
await scanPublicFolder(fullPath, relPath);
|
|
398
|
+
} else if (isImageFile(entry.name)) {
|
|
399
|
+
allImages.push(relPath);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
254
404
|
}
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
405
|
+
const publicDir = path5.join(process.cwd(), "public");
|
|
406
|
+
await scanPublicFolder(publicDir);
|
|
407
|
+
return NextResponse.json({
|
|
408
|
+
count: allImages.length,
|
|
409
|
+
images: allImages
|
|
410
|
+
});
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error("Failed to count images:", error);
|
|
413
|
+
return NextResponse.json({ error: "Failed to count images" }, { status: 500 });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async function handleFolderImages(request) {
|
|
417
|
+
try {
|
|
418
|
+
const searchParams = request.nextUrl.searchParams;
|
|
419
|
+
const foldersParam = searchParams.get("folders");
|
|
420
|
+
if (!foldersParam) {
|
|
421
|
+
return NextResponse.json({ error: "No folders provided" }, { status: 400 });
|
|
422
|
+
}
|
|
423
|
+
const folders = foldersParam.split(",");
|
|
424
|
+
const allImages = [];
|
|
425
|
+
async function scanFolder(dir, relativePath = "") {
|
|
426
|
+
try {
|
|
427
|
+
const entries = await fs5.readdir(dir, { withFileTypes: true });
|
|
428
|
+
for (const entry of entries) {
|
|
429
|
+
if (entry.name.startsWith(".")) continue;
|
|
430
|
+
const fullPath = path5.join(dir, entry.name);
|
|
431
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
432
|
+
if (entry.isDirectory()) {
|
|
433
|
+
await scanFolder(fullPath, relPath);
|
|
434
|
+
} else if (isImageFile(entry.name)) {
|
|
435
|
+
allImages.push(relPath);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const folder of folders) {
|
|
442
|
+
const relativePath = folder.replace(/^public\/?/, "");
|
|
443
|
+
if (relativePath === "images" || relativePath.startsWith("images/")) continue;
|
|
444
|
+
const folderPath = path5.join(process.cwd(), folder);
|
|
445
|
+
await scanFolder(folderPath, relativePath);
|
|
446
|
+
}
|
|
447
|
+
return NextResponse.json({
|
|
448
|
+
count: allImages.length,
|
|
449
|
+
images: allImages
|
|
450
|
+
});
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error("Failed to get folder images:", error);
|
|
453
|
+
return NextResponse.json({ error: "Failed to get folder images" }, { status: 500 });
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/handlers/files.ts
|
|
458
|
+
import { NextResponse as NextResponse2 } from "next/server";
|
|
459
|
+
import { promises as fs6 } from "fs";
|
|
460
|
+
import path6 from "path";
|
|
461
|
+
import sharp3 from "sharp";
|
|
462
|
+
import { encode as encode2 } from "blurhash";
|
|
463
|
+
async function handleUpload(request) {
|
|
464
|
+
try {
|
|
465
|
+
const formData = await request.formData();
|
|
466
|
+
const file = formData.get("file");
|
|
467
|
+
const targetPath = formData.get("path") || "public";
|
|
468
|
+
if (!file) {
|
|
469
|
+
return NextResponse2.json({ error: "No file provided" }, { status: 400 });
|
|
470
|
+
}
|
|
471
|
+
const bytes = await file.arrayBuffer();
|
|
472
|
+
const buffer = Buffer.from(bytes);
|
|
473
|
+
const fileName = file.name;
|
|
474
|
+
const baseName = path6.basename(fileName, path6.extname(fileName));
|
|
475
|
+
const ext = path6.extname(fileName).toLowerCase();
|
|
476
|
+
const isImage = isImageFile(fileName);
|
|
477
|
+
const isSvg = ext === ".svg";
|
|
478
|
+
const isProcessableImage = isImage && !isSvg;
|
|
263
479
|
const meta = await loadMeta();
|
|
264
480
|
let relativeDir = "";
|
|
265
481
|
if (targetPath === "public") {
|
|
@@ -268,16 +484,16 @@ async function handleUpload(request) {
|
|
|
268
484
|
relativeDir = targetPath.replace("public/", "");
|
|
269
485
|
}
|
|
270
486
|
if (relativeDir === "images" || relativeDir.startsWith("images/")) {
|
|
271
|
-
return
|
|
487
|
+
return NextResponse2.json(
|
|
272
488
|
{ error: "Cannot upload to images/ folder. Upload to public/ instead - thumbnails are generated automatically." },
|
|
273
489
|
{ status: 400 }
|
|
274
490
|
);
|
|
275
491
|
}
|
|
276
|
-
const uploadDir =
|
|
277
|
-
await
|
|
278
|
-
await
|
|
492
|
+
const uploadDir = path6.join(process.cwd(), "public", relativeDir);
|
|
493
|
+
await fs6.mkdir(uploadDir, { recursive: true });
|
|
494
|
+
await fs6.writeFile(path6.join(uploadDir, fileName), buffer);
|
|
279
495
|
if (!isImage) {
|
|
280
|
-
return
|
|
496
|
+
return NextResponse2.json({
|
|
281
497
|
success: true,
|
|
282
498
|
message: "File uploaded successfully (non-image, no thumbnails generated)",
|
|
283
499
|
path: `public/${relativeDir ? relativeDir + "/" : ""}${fileName}`
|
|
@@ -285,32 +501,32 @@ async function handleUpload(request) {
|
|
|
285
501
|
}
|
|
286
502
|
const imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
|
|
287
503
|
if (meta[imageKey]) {
|
|
288
|
-
return
|
|
504
|
+
return NextResponse2.json(
|
|
289
505
|
{ error: `File '${imageKey}' already exists in meta` },
|
|
290
506
|
{ status: 409 }
|
|
291
507
|
);
|
|
292
508
|
}
|
|
293
|
-
const imagesPath =
|
|
294
|
-
await
|
|
509
|
+
const imagesPath = path6.join(process.cwd(), "public", "images", relativeDir);
|
|
510
|
+
await fs6.mkdir(imagesPath, { recursive: true });
|
|
295
511
|
let originalWidth = 0;
|
|
296
512
|
let originalHeight = 0;
|
|
297
513
|
let blurhash = "";
|
|
298
514
|
const originalPath = `/${relativeDir ? relativeDir + "/" : ""}${fileName}`;
|
|
299
515
|
if (isSvg) {
|
|
300
|
-
const fullPath =
|
|
301
|
-
await
|
|
516
|
+
const fullPath = path6.join(imagesPath, fileName);
|
|
517
|
+
await fs6.writeFile(fullPath, buffer);
|
|
302
518
|
} else if (isProcessableImage) {
|
|
303
|
-
const sharpInstance =
|
|
519
|
+
const sharpInstance = sharp3(buffer);
|
|
304
520
|
const metadata = await sharpInstance.metadata();
|
|
305
521
|
originalWidth = metadata.width || 0;
|
|
306
522
|
originalHeight = metadata.height || 0;
|
|
307
523
|
const outputExt = ext === ".png" ? ".png" : ".jpg";
|
|
308
524
|
const fullFileName = `${baseName}${outputExt}`;
|
|
309
|
-
const fullPath =
|
|
525
|
+
const fullPath = path6.join(imagesPath, fullFileName);
|
|
310
526
|
if (ext === ".png") {
|
|
311
|
-
await
|
|
527
|
+
await sharp3(buffer).png({ quality: 85 }).toFile(fullPath);
|
|
312
528
|
} else {
|
|
313
|
-
await
|
|
529
|
+
await sharp3(buffer).jpeg({ quality: 85 }).toFile(fullPath);
|
|
314
530
|
}
|
|
315
531
|
for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
|
|
316
532
|
const { width: maxWidth, suffix } = sizeConfig;
|
|
@@ -320,15 +536,15 @@ async function handleUpload(request) {
|
|
|
320
536
|
const ratio = originalHeight / originalWidth;
|
|
321
537
|
const newHeight = Math.round(maxWidth * ratio);
|
|
322
538
|
const sizeFileName = `${baseName}${suffix}${outputExt}`;
|
|
323
|
-
const sizePath =
|
|
539
|
+
const sizePath = path6.join(imagesPath, sizeFileName);
|
|
324
540
|
if (ext === ".png") {
|
|
325
|
-
await
|
|
541
|
+
await sharp3(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
|
|
326
542
|
} else {
|
|
327
|
-
await
|
|
543
|
+
await sharp3(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
|
|
328
544
|
}
|
|
329
545
|
}
|
|
330
|
-
const { data, info } = await
|
|
331
|
-
blurhash =
|
|
546
|
+
const { data, info } = await sharp3(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
547
|
+
blurhash = encode2(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
|
|
332
548
|
}
|
|
333
549
|
const entry = {
|
|
334
550
|
w: originalWidth,
|
|
@@ -337,18 +553,18 @@ async function handleUpload(request) {
|
|
|
337
553
|
};
|
|
338
554
|
meta[originalPath] = entry;
|
|
339
555
|
await saveMeta(meta);
|
|
340
|
-
return
|
|
556
|
+
return NextResponse2.json({ success: true, imageKey: originalPath, entry });
|
|
341
557
|
} catch (error) {
|
|
342
558
|
console.error("Failed to upload:", error);
|
|
343
559
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
344
|
-
return
|
|
560
|
+
return NextResponse2.json({ error: `Failed to upload file: ${message}` }, { status: 500 });
|
|
345
561
|
}
|
|
346
562
|
}
|
|
347
563
|
async function handleDelete(request) {
|
|
348
564
|
try {
|
|
349
565
|
const { paths } = await request.json();
|
|
350
566
|
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
351
|
-
return
|
|
567
|
+
return NextResponse2.json({ error: "No paths provided" }, { status: 400 });
|
|
352
568
|
}
|
|
353
569
|
const meta = await loadMeta();
|
|
354
570
|
const deleted = [];
|
|
@@ -359,10 +575,10 @@ async function handleDelete(request) {
|
|
|
359
575
|
errors.push(`Invalid path: ${itemPath}`);
|
|
360
576
|
continue;
|
|
361
577
|
}
|
|
362
|
-
const absolutePath =
|
|
363
|
-
const stats = await
|
|
578
|
+
const absolutePath = path6.join(process.cwd(), itemPath);
|
|
579
|
+
const stats = await fs6.stat(absolutePath);
|
|
364
580
|
if (stats.isDirectory()) {
|
|
365
|
-
await
|
|
581
|
+
await fs6.rm(absolutePath, { recursive: true });
|
|
366
582
|
const prefix = "/" + itemPath.replace(/^public\/images\/?/, "").replace(/^public\/?/, "");
|
|
367
583
|
for (const key of Object.keys(meta)) {
|
|
368
584
|
if (key.startsWith(prefix)) {
|
|
@@ -370,15 +586,15 @@ async function handleDelete(request) {
|
|
|
370
586
|
}
|
|
371
587
|
}
|
|
372
588
|
} else {
|
|
373
|
-
await
|
|
589
|
+
await fs6.unlink(absolutePath);
|
|
374
590
|
const isInImagesFolder = itemPath.startsWith("public/images/");
|
|
375
591
|
if (!isInImagesFolder) {
|
|
376
592
|
const imageKey = "/" + itemPath.replace(/^public\//, "");
|
|
377
593
|
if (meta[imageKey]) {
|
|
378
594
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
379
|
-
const absoluteThumbPath =
|
|
595
|
+
const absoluteThumbPath = path6.join(process.cwd(), "public", thumbPath);
|
|
380
596
|
try {
|
|
381
|
-
await
|
|
597
|
+
await fs6.unlink(absoluteThumbPath);
|
|
382
598
|
} catch {
|
|
383
599
|
}
|
|
384
600
|
}
|
|
@@ -393,16 +609,202 @@ async function handleDelete(request) {
|
|
|
393
609
|
}
|
|
394
610
|
}
|
|
395
611
|
await saveMeta(meta);
|
|
396
|
-
return
|
|
612
|
+
return NextResponse2.json({
|
|
397
613
|
success: true,
|
|
398
614
|
deleted,
|
|
399
615
|
errors: errors.length > 0 ? errors : void 0
|
|
400
616
|
});
|
|
401
617
|
} catch (error) {
|
|
402
618
|
console.error("Failed to delete:", error);
|
|
403
|
-
return
|
|
619
|
+
return NextResponse2.json({ error: "Failed to delete files" }, { status: 500 });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function handleCreateFolder(request) {
|
|
623
|
+
try {
|
|
624
|
+
const { parentPath, name } = await request.json();
|
|
625
|
+
if (!name || typeof name !== "string") {
|
|
626
|
+
return NextResponse2.json({ error: "Folder name is required" }, { status: 400 });
|
|
627
|
+
}
|
|
628
|
+
const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
629
|
+
if (!sanitizedName) {
|
|
630
|
+
return NextResponse2.json({ error: "Invalid folder name" }, { status: 400 });
|
|
631
|
+
}
|
|
632
|
+
const safePath = (parentPath || "public").replace(/\.\./g, "");
|
|
633
|
+
const folderPath = path6.join(process.cwd(), safePath, sanitizedName);
|
|
634
|
+
if (!folderPath.startsWith(path6.join(process.cwd(), "public"))) {
|
|
635
|
+
return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
await fs6.access(folderPath);
|
|
639
|
+
return NextResponse2.json({ error: "A folder with this name already exists" }, { status: 400 });
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
await fs6.mkdir(folderPath, { recursive: true });
|
|
643
|
+
return NextResponse2.json({ success: true, path: path6.join(safePath, sanitizedName) });
|
|
644
|
+
} catch (error) {
|
|
645
|
+
console.error("Failed to create folder:", error);
|
|
646
|
+
return NextResponse2.json({ error: "Failed to create folder" }, { status: 500 });
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async function handleRename(request) {
|
|
650
|
+
try {
|
|
651
|
+
const { oldPath, newName } = await request.json();
|
|
652
|
+
if (!oldPath || !newName) {
|
|
653
|
+
return NextResponse2.json({ error: "Path and new name are required" }, { status: 400 });
|
|
654
|
+
}
|
|
655
|
+
const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
656
|
+
if (!sanitizedName) {
|
|
657
|
+
return NextResponse2.json({ error: "Invalid name" }, { status: 400 });
|
|
658
|
+
}
|
|
659
|
+
const safePath = oldPath.replace(/\.\./g, "");
|
|
660
|
+
const absoluteOldPath = path6.join(process.cwd(), safePath);
|
|
661
|
+
const parentDir = path6.dirname(absoluteOldPath);
|
|
662
|
+
const absoluteNewPath = path6.join(parentDir, sanitizedName);
|
|
663
|
+
if (!absoluteOldPath.startsWith(path6.join(process.cwd(), "public"))) {
|
|
664
|
+
return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
await fs6.access(absoluteOldPath);
|
|
668
|
+
} catch {
|
|
669
|
+
return NextResponse2.json({ error: "File or folder not found" }, { status: 404 });
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
await fs6.access(absoluteNewPath);
|
|
673
|
+
return NextResponse2.json({ error: "An item with this name already exists" }, { status: 400 });
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
const stats = await fs6.stat(absoluteOldPath);
|
|
677
|
+
const isFile = stats.isFile();
|
|
678
|
+
const isImage = isFile && isImageFile(path6.basename(oldPath));
|
|
679
|
+
await fs6.rename(absoluteOldPath, absoluteNewPath);
|
|
680
|
+
if (isImage) {
|
|
681
|
+
const meta = await loadMeta();
|
|
682
|
+
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
683
|
+
const newRelativePath = path6.join(path6.dirname(oldRelativePath), sanitizedName);
|
|
684
|
+
const oldKey = "/" + oldRelativePath;
|
|
685
|
+
const newKey = "/" + newRelativePath;
|
|
686
|
+
if (meta[oldKey]) {
|
|
687
|
+
const entry = meta[oldKey];
|
|
688
|
+
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
689
|
+
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
690
|
+
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
691
|
+
const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
692
|
+
const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[i]);
|
|
693
|
+
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
694
|
+
try {
|
|
695
|
+
await fs6.rename(oldThumbPath, newThumbPath);
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
delete meta[oldKey];
|
|
700
|
+
meta[newKey] = entry;
|
|
701
|
+
}
|
|
702
|
+
await saveMeta(meta);
|
|
703
|
+
}
|
|
704
|
+
const newPath = path6.join(path6.dirname(safePath), sanitizedName);
|
|
705
|
+
return NextResponse2.json({ success: true, newPath });
|
|
706
|
+
} catch (error) {
|
|
707
|
+
console.error("Failed to rename:", error);
|
|
708
|
+
return NextResponse2.json({ error: "Failed to rename" }, { status: 500 });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
async function handleMove(request) {
|
|
712
|
+
try {
|
|
713
|
+
const { paths, destination } = await request.json();
|
|
714
|
+
if (!paths || !Array.isArray(paths) || paths.length === 0) {
|
|
715
|
+
return NextResponse2.json({ error: "Paths are required" }, { status: 400 });
|
|
716
|
+
}
|
|
717
|
+
if (!destination || typeof destination !== "string") {
|
|
718
|
+
return NextResponse2.json({ error: "Destination is required" }, { status: 400 });
|
|
719
|
+
}
|
|
720
|
+
const safeDestination = destination.replace(/\.\./g, "");
|
|
721
|
+
const absoluteDestination = path6.join(process.cwd(), safeDestination);
|
|
722
|
+
if (!absoluteDestination.startsWith(path6.join(process.cwd(), "public"))) {
|
|
723
|
+
return NextResponse2.json({ error: "Invalid destination" }, { status: 400 });
|
|
724
|
+
}
|
|
725
|
+
try {
|
|
726
|
+
const destStats = await fs6.stat(absoluteDestination);
|
|
727
|
+
if (!destStats.isDirectory()) {
|
|
728
|
+
return NextResponse2.json({ error: "Destination is not a folder" }, { status: 400 });
|
|
729
|
+
}
|
|
730
|
+
} catch {
|
|
731
|
+
return NextResponse2.json({ error: "Destination folder not found" }, { status: 404 });
|
|
732
|
+
}
|
|
733
|
+
const moved = [];
|
|
734
|
+
const errors = [];
|
|
735
|
+
const meta = await loadMeta();
|
|
736
|
+
let metaChanged = false;
|
|
737
|
+
for (const itemPath of paths) {
|
|
738
|
+
const safePath = itemPath.replace(/\.\./g, "");
|
|
739
|
+
const absolutePath = path6.join(process.cwd(), safePath);
|
|
740
|
+
const itemName = path6.basename(safePath);
|
|
741
|
+
const newAbsolutePath = path6.join(absoluteDestination, itemName);
|
|
742
|
+
if (absoluteDestination.startsWith(absolutePath + path6.sep)) {
|
|
743
|
+
errors.push(`Cannot move ${itemName} into itself`);
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
await fs6.access(absolutePath);
|
|
748
|
+
} catch {
|
|
749
|
+
errors.push(`${itemName} not found`);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
await fs6.access(newAbsolutePath);
|
|
754
|
+
errors.push(`${itemName} already exists in destination`);
|
|
755
|
+
continue;
|
|
756
|
+
} catch {
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
await fs6.rename(absolutePath, newAbsolutePath);
|
|
760
|
+
const stats = await fs6.stat(newAbsolutePath);
|
|
761
|
+
if (stats.isFile() && isImageFile(itemName)) {
|
|
762
|
+
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
763
|
+
const newRelativePath = path6.join(safeDestination.replace(/^public\//, ""), itemName);
|
|
764
|
+
const oldKey = "/" + oldRelativePath;
|
|
765
|
+
const newKey = "/" + newRelativePath;
|
|
766
|
+
if (meta[oldKey]) {
|
|
767
|
+
const entry = meta[oldKey];
|
|
768
|
+
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
769
|
+
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
770
|
+
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
771
|
+
const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
772
|
+
const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[i]);
|
|
773
|
+
await fs6.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
774
|
+
try {
|
|
775
|
+
await fs6.rename(oldThumbPath, newThumbPath);
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
delete meta[oldKey];
|
|
780
|
+
meta[newKey] = entry;
|
|
781
|
+
metaChanged = true;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
moved.push(itemPath);
|
|
785
|
+
} catch {
|
|
786
|
+
errors.push(`Failed to move ${itemName}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
if (metaChanged) {
|
|
790
|
+
await saveMeta(meta);
|
|
791
|
+
}
|
|
792
|
+
return NextResponse2.json({
|
|
793
|
+
success: errors.length === 0,
|
|
794
|
+
moved,
|
|
795
|
+
errors: errors.length > 0 ? errors : void 0
|
|
796
|
+
});
|
|
797
|
+
} catch (error) {
|
|
798
|
+
console.error("Failed to move:", error);
|
|
799
|
+
return NextResponse2.json({ error: "Failed to move items" }, { status: 500 });
|
|
404
800
|
}
|
|
405
801
|
}
|
|
802
|
+
|
|
803
|
+
// src/handlers/images.ts
|
|
804
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
805
|
+
import { promises as fs7 } from "fs";
|
|
806
|
+
import path7 from "path";
|
|
807
|
+
import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
|
|
406
808
|
async function handleSync(request) {
|
|
407
809
|
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
408
810
|
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
@@ -410,7 +812,7 @@ async function handleSync(request) {
|
|
|
410
812
|
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
411
813
|
const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL;
|
|
412
814
|
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {
|
|
413
|
-
return
|
|
815
|
+
return NextResponse3.json(
|
|
414
816
|
{ error: "R2 not configured. Set CLOUDFLARE_R2_* environment variables." },
|
|
415
817
|
{ status: 400 }
|
|
416
818
|
);
|
|
@@ -418,10 +820,10 @@ async function handleSync(request) {
|
|
|
418
820
|
try {
|
|
419
821
|
const { imageKeys } = await request.json();
|
|
420
822
|
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
421
|
-
return
|
|
823
|
+
return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
|
|
422
824
|
}
|
|
423
825
|
const meta = await loadMeta();
|
|
424
|
-
const r2 = new
|
|
826
|
+
const r2 = new S3Client2({
|
|
425
827
|
region: "auto",
|
|
426
828
|
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
427
829
|
credentials: { accessKeyId, secretAccessKey }
|
|
@@ -440,11 +842,11 @@ async function handleSync(request) {
|
|
|
440
842
|
}
|
|
441
843
|
try {
|
|
442
844
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
443
|
-
const localPath =
|
|
845
|
+
const localPath = path7.join(process.cwd(), "public", thumbPath);
|
|
444
846
|
try {
|
|
445
|
-
const fileBuffer = await
|
|
847
|
+
const fileBuffer = await fs7.readFile(localPath);
|
|
446
848
|
await r2.send(
|
|
447
|
-
new
|
|
849
|
+
new PutObjectCommand2({
|
|
448
850
|
Bucket: bucketName,
|
|
449
851
|
Key: thumbPath.replace(/^\//, ""),
|
|
450
852
|
Body: fileBuffer,
|
|
@@ -456,9 +858,9 @@ async function handleSync(request) {
|
|
|
456
858
|
}
|
|
457
859
|
entry.s = 1;
|
|
458
860
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
459
|
-
const localPath =
|
|
861
|
+
const localPath = path7.join(process.cwd(), "public", thumbPath);
|
|
460
862
|
try {
|
|
461
|
-
await
|
|
863
|
+
await fs7.unlink(localPath);
|
|
462
864
|
} catch {
|
|
463
865
|
}
|
|
464
866
|
}
|
|
@@ -469,21 +871,21 @@ async function handleSync(request) {
|
|
|
469
871
|
}
|
|
470
872
|
}
|
|
471
873
|
await saveMeta(meta);
|
|
472
|
-
return
|
|
874
|
+
return NextResponse3.json({
|
|
473
875
|
success: true,
|
|
474
876
|
synced,
|
|
475
877
|
errors: errors.length > 0 ? errors : void 0
|
|
476
878
|
});
|
|
477
879
|
} catch (error) {
|
|
478
880
|
console.error("Failed to sync:", error);
|
|
479
|
-
return
|
|
881
|
+
return NextResponse3.json({ error: "Failed to sync to CDN" }, { status: 500 });
|
|
480
882
|
}
|
|
481
883
|
}
|
|
482
884
|
async function handleReprocess(request) {
|
|
483
885
|
try {
|
|
484
886
|
const { imageKeys } = await request.json();
|
|
485
887
|
if (!imageKeys || !Array.isArray(imageKeys) || imageKeys.length === 0) {
|
|
486
|
-
return
|
|
888
|
+
return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
|
|
487
889
|
}
|
|
488
890
|
const meta = await loadMeta();
|
|
489
891
|
const processed = [];
|
|
@@ -492,9 +894,9 @@ async function handleReprocess(request) {
|
|
|
492
894
|
try {
|
|
493
895
|
let buffer;
|
|
494
896
|
const entry = meta[imageKey];
|
|
495
|
-
const originalPath =
|
|
897
|
+
const originalPath = path7.join(process.cwd(), "public", imageKey);
|
|
496
898
|
try {
|
|
497
|
-
buffer = await
|
|
899
|
+
buffer = await fs7.readFile(originalPath);
|
|
498
900
|
} catch {
|
|
499
901
|
if (entry?.s) {
|
|
500
902
|
buffer = await downloadFromCdn(imageKey);
|
|
@@ -516,85 +918,14 @@ async function handleReprocess(request) {
|
|
|
516
918
|
}
|
|
517
919
|
}
|
|
518
920
|
await saveMeta(meta);
|
|
519
|
-
return
|
|
921
|
+
return NextResponse3.json({
|
|
520
922
|
success: true,
|
|
521
923
|
processed,
|
|
522
924
|
errors: errors.length > 0 ? errors : void 0
|
|
523
925
|
});
|
|
524
926
|
} catch (error) {
|
|
525
927
|
console.error("Failed to reprocess:", error);
|
|
526
|
-
return
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
async function handleCountImages() {
|
|
530
|
-
try {
|
|
531
|
-
const allImages = [];
|
|
532
|
-
async function scanPublicFolder(dir, relativePath = "") {
|
|
533
|
-
try {
|
|
534
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
535
|
-
for (const entry of entries) {
|
|
536
|
-
if (entry.name.startsWith(".")) continue;
|
|
537
|
-
const fullPath = path.join(dir, entry.name);
|
|
538
|
-
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
539
|
-
if (relPath === "images" || relPath.startsWith("images/")) continue;
|
|
540
|
-
if (entry.isDirectory()) {
|
|
541
|
-
await scanPublicFolder(fullPath, relPath);
|
|
542
|
-
} else if (isImageFile(entry.name)) {
|
|
543
|
-
allImages.push(relPath);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
} catch {
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
const publicDir = path.join(process.cwd(), "public");
|
|
550
|
-
await scanPublicFolder(publicDir);
|
|
551
|
-
return NextResponse.json({
|
|
552
|
-
count: allImages.length,
|
|
553
|
-
images: allImages
|
|
554
|
-
});
|
|
555
|
-
} catch (error) {
|
|
556
|
-
console.error("Failed to count images:", error);
|
|
557
|
-
return NextResponse.json({ error: "Failed to count images" }, { status: 500 });
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
async function handleFolderImages(request) {
|
|
561
|
-
try {
|
|
562
|
-
const searchParams = request.nextUrl.searchParams;
|
|
563
|
-
const foldersParam = searchParams.get("folders");
|
|
564
|
-
if (!foldersParam) {
|
|
565
|
-
return NextResponse.json({ error: "No folders provided" }, { status: 400 });
|
|
566
|
-
}
|
|
567
|
-
const folders = foldersParam.split(",");
|
|
568
|
-
const allImages = [];
|
|
569
|
-
async function scanFolder(dir, relativePath = "") {
|
|
570
|
-
try {
|
|
571
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
572
|
-
for (const entry of entries) {
|
|
573
|
-
if (entry.name.startsWith(".")) continue;
|
|
574
|
-
const fullPath = path.join(dir, entry.name);
|
|
575
|
-
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
576
|
-
if (entry.isDirectory()) {
|
|
577
|
-
await scanFolder(fullPath, relPath);
|
|
578
|
-
} else if (isImageFile(entry.name)) {
|
|
579
|
-
allImages.push(relPath);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
} catch {
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
for (const folder of folders) {
|
|
586
|
-
const relativePath = folder.replace(/^public\/?/, "");
|
|
587
|
-
if (relativePath === "images" || relativePath.startsWith("images/")) continue;
|
|
588
|
-
const folderPath = path.join(process.cwd(), folder);
|
|
589
|
-
await scanFolder(folderPath, relativePath);
|
|
590
|
-
}
|
|
591
|
-
return NextResponse.json({
|
|
592
|
-
count: allImages.length,
|
|
593
|
-
images: allImages
|
|
594
|
-
});
|
|
595
|
-
} catch (error) {
|
|
596
|
-
console.error("Failed to get folder images:", error);
|
|
597
|
-
return NextResponse.json({ error: "Failed to get folder images" }, { status: 500 });
|
|
928
|
+
return NextResponse3.json({ error: "Failed to reprocess images" }, { status: 500 });
|
|
598
929
|
}
|
|
599
930
|
}
|
|
600
931
|
async function handleProcessAllStream() {
|
|
@@ -614,10 +945,10 @@ async function handleProcessAllStream() {
|
|
|
614
945
|
const allImages = [];
|
|
615
946
|
async function scanPublicFolder(dir, relativePath = "") {
|
|
616
947
|
try {
|
|
617
|
-
const entries = await
|
|
948
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
618
949
|
for (const entry of entries) {
|
|
619
950
|
if (entry.name.startsWith(".")) continue;
|
|
620
|
-
const fullPath =
|
|
951
|
+
const fullPath = path7.join(dir, entry.name);
|
|
621
952
|
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
622
953
|
if (relPath === "images" || relPath.startsWith("images/")) continue;
|
|
623
954
|
if (entry.isDirectory()) {
|
|
@@ -629,7 +960,7 @@ async function handleProcessAllStream() {
|
|
|
629
960
|
} catch {
|
|
630
961
|
}
|
|
631
962
|
}
|
|
632
|
-
const publicDir =
|
|
963
|
+
const publicDir = path7.join(process.cwd(), "public");
|
|
633
964
|
await scanPublicFolder(publicDir);
|
|
634
965
|
const total = allImages.length;
|
|
635
966
|
sendEvent({ type: "start", total });
|
|
@@ -644,16 +975,16 @@ async function handleProcessAllStream() {
|
|
|
644
975
|
currentFile: key
|
|
645
976
|
});
|
|
646
977
|
try {
|
|
647
|
-
const buffer = await
|
|
648
|
-
const ext =
|
|
978
|
+
const buffer = await fs7.readFile(fullPath);
|
|
979
|
+
const ext = path7.extname(key).toLowerCase();
|
|
649
980
|
const isSvg = ext === ".svg";
|
|
650
981
|
if (isSvg) {
|
|
651
|
-
const imageDir =
|
|
652
|
-
const imagesPath =
|
|
653
|
-
await
|
|
654
|
-
const fileName =
|
|
655
|
-
const destPath =
|
|
656
|
-
await
|
|
982
|
+
const imageDir = path7.dirname(key);
|
|
983
|
+
const imagesPath = path7.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
|
|
984
|
+
await fs7.mkdir(imagesPath, { recursive: true });
|
|
985
|
+
const fileName = path7.basename(key);
|
|
986
|
+
const destPath = path7.join(imagesPath, fileName);
|
|
987
|
+
await fs7.writeFile(destPath, buffer);
|
|
657
988
|
meta[imageKey] = {
|
|
658
989
|
w: 0,
|
|
659
990
|
h: 0,
|
|
@@ -682,10 +1013,10 @@ async function handleProcessAllStream() {
|
|
|
682
1013
|
}
|
|
683
1014
|
async function findOrphans(dir, relativePath = "") {
|
|
684
1015
|
try {
|
|
685
|
-
const entries = await
|
|
1016
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
686
1017
|
for (const entry of entries) {
|
|
687
1018
|
if (entry.name.startsWith(".")) continue;
|
|
688
|
-
const fullPath =
|
|
1019
|
+
const fullPath = path7.join(dir, entry.name);
|
|
689
1020
|
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
690
1021
|
if (entry.isDirectory()) {
|
|
691
1022
|
await findOrphans(fullPath, relPath);
|
|
@@ -693,7 +1024,7 @@ async function handleProcessAllStream() {
|
|
|
693
1024
|
const publicPath = `/images/${relPath}`;
|
|
694
1025
|
if (!trackedPaths.has(publicPath)) {
|
|
695
1026
|
try {
|
|
696
|
-
await
|
|
1027
|
+
await fs7.unlink(fullPath);
|
|
697
1028
|
orphansRemoved.push(publicPath);
|
|
698
1029
|
} catch (err) {
|
|
699
1030
|
console.error(`Failed to remove orphan ${publicPath}:`, err);
|
|
@@ -704,22 +1035,22 @@ async function handleProcessAllStream() {
|
|
|
704
1035
|
} catch {
|
|
705
1036
|
}
|
|
706
1037
|
}
|
|
707
|
-
const imagesDir =
|
|
1038
|
+
const imagesDir = path7.join(process.cwd(), "public", "images");
|
|
708
1039
|
await findOrphans(imagesDir);
|
|
709
1040
|
async function removeEmptyDirs(dir) {
|
|
710
1041
|
try {
|
|
711
|
-
const entries = await
|
|
1042
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
712
1043
|
let isEmpty = true;
|
|
713
1044
|
for (const entry of entries) {
|
|
714
1045
|
if (entry.isDirectory()) {
|
|
715
|
-
const subDirEmpty = await removeEmptyDirs(
|
|
1046
|
+
const subDirEmpty = await removeEmptyDirs(path7.join(dir, entry.name));
|
|
716
1047
|
if (!subDirEmpty) isEmpty = false;
|
|
717
1048
|
} else {
|
|
718
1049
|
isEmpty = false;
|
|
719
1050
|
}
|
|
720
1051
|
}
|
|
721
1052
|
if (isEmpty && dir !== imagesDir) {
|
|
722
|
-
await
|
|
1053
|
+
await fs7.rmdir(dir);
|
|
723
1054
|
}
|
|
724
1055
|
return isEmpty;
|
|
725
1056
|
} catch {
|
|
@@ -750,371 +1081,72 @@ async function handleProcessAllStream() {
|
|
|
750
1081
|
}
|
|
751
1082
|
});
|
|
752
1083
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
return
|
|
758
|
-
} catch {
|
|
759
|
-
return {};
|
|
1084
|
+
|
|
1085
|
+
// src/handlers/index.ts
|
|
1086
|
+
async function GET(request) {
|
|
1087
|
+
if (process.env.NODE_ENV !== "development") {
|
|
1088
|
+
return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
|
|
760
1089
|
}
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const metaPath = path.join(dataDir, "_meta.json");
|
|
766
|
-
await fs.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
767
|
-
}
|
|
768
|
-
function isImageFile(filename) {
|
|
769
|
-
const ext = path.extname(filename).toLowerCase();
|
|
770
|
-
return [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"].includes(ext);
|
|
771
|
-
}
|
|
772
|
-
function isMediaFile(filename) {
|
|
773
|
-
const ext = path.extname(filename).toLowerCase();
|
|
774
|
-
if ([".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico", ".bmp", ".tiff", ".tif"].includes(ext)) return true;
|
|
775
|
-
if ([".mp4", ".webm", ".mov", ".avi", ".mkv", ".m4v"].includes(ext)) return true;
|
|
776
|
-
if ([".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac"].includes(ext)) return true;
|
|
777
|
-
if ([".pdf"].includes(ext)) return true;
|
|
778
|
-
return false;
|
|
779
|
-
}
|
|
780
|
-
function getContentType(filePath) {
|
|
781
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
782
|
-
switch (ext) {
|
|
783
|
-
case ".jpg":
|
|
784
|
-
case ".jpeg":
|
|
785
|
-
return "image/jpeg";
|
|
786
|
-
case ".png":
|
|
787
|
-
return "image/png";
|
|
788
|
-
case ".gif":
|
|
789
|
-
return "image/gif";
|
|
790
|
-
case ".webp":
|
|
791
|
-
return "image/webp";
|
|
792
|
-
case ".svg":
|
|
793
|
-
return "image/svg+xml";
|
|
794
|
-
default:
|
|
795
|
-
return "application/octet-stream";
|
|
1090
|
+
const pathname = request.nextUrl.pathname;
|
|
1091
|
+
const route = pathname.replace(/^\/api\/studio\/?/, "");
|
|
1092
|
+
if (route === "list-folders") {
|
|
1093
|
+
return handleListFolders();
|
|
796
1094
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
const sharpInstance = sharp(buffer);
|
|
800
|
-
const metadata = await sharpInstance.metadata();
|
|
801
|
-
const originalWidth = metadata.width || 0;
|
|
802
|
-
const originalHeight = metadata.height || 0;
|
|
803
|
-
const keyWithoutSlash = imageKey.startsWith("/") ? imageKey.slice(1) : imageKey;
|
|
804
|
-
const baseName = path.basename(keyWithoutSlash, path.extname(keyWithoutSlash));
|
|
805
|
-
const ext = path.extname(keyWithoutSlash).toLowerCase();
|
|
806
|
-
const imageDir = path.dirname(keyWithoutSlash);
|
|
807
|
-
const imagesPath = path.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
|
|
808
|
-
await fs.mkdir(imagesPath, { recursive: true });
|
|
809
|
-
const isPng = ext === ".png";
|
|
810
|
-
const outputExt = isPng ? ".png" : ".jpg";
|
|
811
|
-
const fullFileName = imageDir === "." ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`;
|
|
812
|
-
const fullPath = path.join(process.cwd(), "public", "images", fullFileName);
|
|
813
|
-
if (isPng) {
|
|
814
|
-
await sharp(buffer).png({ quality: 85 }).toFile(fullPath);
|
|
815
|
-
} else {
|
|
816
|
-
await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath);
|
|
1095
|
+
if (route === "list" || route.startsWith("list")) {
|
|
1096
|
+
return handleList(request);
|
|
817
1097
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (originalWidth <= maxWidth) {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
const ratio = originalHeight / originalWidth;
|
|
824
|
-
const newHeight = Math.round(maxWidth * ratio);
|
|
825
|
-
const sizeFileName = `${baseName}${suffix}${outputExt}`;
|
|
826
|
-
const sizeFilePath = imageDir === "." ? sizeFileName : `${imageDir}/${sizeFileName}`;
|
|
827
|
-
const sizePath = path.join(process.cwd(), "public", "images", sizeFilePath);
|
|
828
|
-
if (isPng) {
|
|
829
|
-
await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
|
|
830
|
-
} else {
|
|
831
|
-
await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
|
|
832
|
-
}
|
|
1098
|
+
if (route === "count-images") {
|
|
1099
|
+
return handleCountImages();
|
|
833
1100
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
return {
|
|
837
|
-
w: originalWidth,
|
|
838
|
-
h: originalHeight,
|
|
839
|
-
blur: blurhash
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
async function downloadFromCdn(originalPath) {
|
|
843
|
-
const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
|
|
844
|
-
const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
|
|
845
|
-
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
846
|
-
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
847
|
-
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {
|
|
848
|
-
throw new Error("R2 not configured");
|
|
1101
|
+
if (route === "folder-images") {
|
|
1102
|
+
return handleFolderImages(request);
|
|
849
1103
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
853
|
-
credentials: { accessKeyId, secretAccessKey }
|
|
854
|
-
});
|
|
855
|
-
const response = await r2.send(
|
|
856
|
-
new GetObjectCommand({
|
|
857
|
-
Bucket: bucketName,
|
|
858
|
-
Key: originalPath.replace(/^\//, "")
|
|
859
|
-
})
|
|
860
|
-
);
|
|
861
|
-
const stream = response.Body;
|
|
862
|
-
const chunks = [];
|
|
863
|
-
for await (const chunk of stream) {
|
|
864
|
-
chunks.push(Buffer.from(chunk));
|
|
1104
|
+
if (route === "search") {
|
|
1105
|
+
return handleSearch(request);
|
|
865
1106
|
}
|
|
866
|
-
return
|
|
1107
|
+
return NextResponse4.json({ error: "Not found" }, { status: 404 });
|
|
867
1108
|
}
|
|
868
|
-
async function
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY;
|
|
872
|
-
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
873
|
-
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {
|
|
874
|
-
throw new Error("R2 not configured");
|
|
1109
|
+
async function POST(request) {
|
|
1110
|
+
if (process.env.NODE_ENV !== "development") {
|
|
1111
|
+
return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
|
|
875
1112
|
}
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
});
|
|
881
|
-
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
882
|
-
const localPath = path.join(process.cwd(), "public", thumbPath);
|
|
883
|
-
try {
|
|
884
|
-
const fileBuffer = await fs.readFile(localPath);
|
|
885
|
-
await r2.send(
|
|
886
|
-
new PutObjectCommand({
|
|
887
|
-
Bucket: bucketName,
|
|
888
|
-
Key: thumbPath.replace(/^\//, ""),
|
|
889
|
-
Body: fileBuffer,
|
|
890
|
-
ContentType: getContentType(thumbPath)
|
|
891
|
-
})
|
|
892
|
-
);
|
|
893
|
-
} catch {
|
|
894
|
-
}
|
|
1113
|
+
const pathname = request.nextUrl.pathname;
|
|
1114
|
+
const route = pathname.replace(/^\/api\/studio\/?/, "");
|
|
1115
|
+
if (route === "upload") {
|
|
1116
|
+
return handleUpload(request);
|
|
895
1117
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
899
|
-
const localPath = path.join(process.cwd(), "public", thumbPath);
|
|
900
|
-
try {
|
|
901
|
-
await fs.unlink(localPath);
|
|
902
|
-
} catch {
|
|
903
|
-
}
|
|
1118
|
+
if (route === "delete") {
|
|
1119
|
+
return handleDelete(request);
|
|
904
1120
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
try {
|
|
908
|
-
const { parentPath, name } = await request.json();
|
|
909
|
-
if (!name || typeof name !== "string") {
|
|
910
|
-
return NextResponse.json({ error: "Folder name is required" }, { status: 400 });
|
|
911
|
-
}
|
|
912
|
-
const sanitizedName = name.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
913
|
-
if (!sanitizedName) {
|
|
914
|
-
return NextResponse.json({ error: "Invalid folder name" }, { status: 400 });
|
|
915
|
-
}
|
|
916
|
-
const safePath = (parentPath || "public").replace(/\.\./g, "");
|
|
917
|
-
const folderPath = path.join(process.cwd(), safePath, sanitizedName);
|
|
918
|
-
if (!folderPath.startsWith(path.join(process.cwd(), "public"))) {
|
|
919
|
-
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
920
|
-
}
|
|
921
|
-
try {
|
|
922
|
-
await fs.access(folderPath);
|
|
923
|
-
return NextResponse.json({ error: "A folder with this name already exists" }, { status: 400 });
|
|
924
|
-
} catch {
|
|
925
|
-
}
|
|
926
|
-
await fs.mkdir(folderPath, { recursive: true });
|
|
927
|
-
return NextResponse.json({ success: true, path: path.join(safePath, sanitizedName) });
|
|
928
|
-
} catch (error) {
|
|
929
|
-
console.error("Failed to create folder:", error);
|
|
930
|
-
return NextResponse.json({ error: "Failed to create folder" }, { status: 500 });
|
|
1121
|
+
if (route === "sync") {
|
|
1122
|
+
return handleSync(request);
|
|
931
1123
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
try {
|
|
935
|
-
const { oldPath, newName } = await request.json();
|
|
936
|
-
if (!oldPath || !newName) {
|
|
937
|
-
return NextResponse.json({ error: "Path and new name are required" }, { status: 400 });
|
|
938
|
-
}
|
|
939
|
-
const sanitizedName = newName.replace(/[<>:"/\\|?*]/g, "").trim();
|
|
940
|
-
if (!sanitizedName) {
|
|
941
|
-
return NextResponse.json({ error: "Invalid name" }, { status: 400 });
|
|
942
|
-
}
|
|
943
|
-
const safePath = oldPath.replace(/\.\./g, "");
|
|
944
|
-
const absoluteOldPath = path.join(process.cwd(), safePath);
|
|
945
|
-
const parentDir = path.dirname(absoluteOldPath);
|
|
946
|
-
const absoluteNewPath = path.join(parentDir, sanitizedName);
|
|
947
|
-
if (!absoluteOldPath.startsWith(path.join(process.cwd(), "public"))) {
|
|
948
|
-
return NextResponse.json({ error: "Invalid path" }, { status: 400 });
|
|
949
|
-
}
|
|
950
|
-
try {
|
|
951
|
-
await fs.access(absoluteOldPath);
|
|
952
|
-
} catch {
|
|
953
|
-
return NextResponse.json({ error: "File or folder not found" }, { status: 404 });
|
|
954
|
-
}
|
|
955
|
-
try {
|
|
956
|
-
await fs.access(absoluteNewPath);
|
|
957
|
-
return NextResponse.json({ error: "An item with this name already exists" }, { status: 400 });
|
|
958
|
-
} catch {
|
|
959
|
-
}
|
|
960
|
-
const stats = await fs.stat(absoluteOldPath);
|
|
961
|
-
const isFile = stats.isFile();
|
|
962
|
-
const isImage = isFile && isImageFile(path.basename(oldPath));
|
|
963
|
-
await fs.rename(absoluteOldPath, absoluteNewPath);
|
|
964
|
-
if (isImage) {
|
|
965
|
-
const meta = await loadMeta();
|
|
966
|
-
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
967
|
-
const newRelativePath = path.join(path.dirname(oldRelativePath), sanitizedName);
|
|
968
|
-
const oldKey = "/" + oldRelativePath;
|
|
969
|
-
const newKey = "/" + newRelativePath;
|
|
970
|
-
if (meta[oldKey]) {
|
|
971
|
-
const entry = meta[oldKey];
|
|
972
|
-
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
973
|
-
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
974
|
-
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
975
|
-
const oldThumbPath = path.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
976
|
-
const newThumbPath = path.join(process.cwd(), "public", newThumbPaths[i]);
|
|
977
|
-
await fs.mkdir(path.dirname(newThumbPath), { recursive: true });
|
|
978
|
-
try {
|
|
979
|
-
await fs.rename(oldThumbPath, newThumbPath);
|
|
980
|
-
} catch {
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
delete meta[oldKey];
|
|
984
|
-
meta[newKey] = entry;
|
|
985
|
-
}
|
|
986
|
-
await saveMeta(meta);
|
|
987
|
-
}
|
|
988
|
-
const newPath = path.join(path.dirname(safePath), sanitizedName);
|
|
989
|
-
return NextResponse.json({ success: true, newPath });
|
|
990
|
-
} catch (error) {
|
|
991
|
-
console.error("Failed to rename:", error);
|
|
992
|
-
return NextResponse.json({ error: "Failed to rename" }, { status: 500 });
|
|
1124
|
+
if (route === "reprocess") {
|
|
1125
|
+
return handleReprocess(request);
|
|
993
1126
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
const absoluteDestination = path.join(process.cwd(), safeDestination);
|
|
1006
|
-
if (!absoluteDestination.startsWith(path.join(process.cwd(), "public"))) {
|
|
1007
|
-
return NextResponse.json({ error: "Invalid destination" }, { status: 400 });
|
|
1008
|
-
}
|
|
1009
|
-
try {
|
|
1010
|
-
const destStats = await fs.stat(absoluteDestination);
|
|
1011
|
-
if (!destStats.isDirectory()) {
|
|
1012
|
-
return NextResponse.json({ error: "Destination is not a folder" }, { status: 400 });
|
|
1013
|
-
}
|
|
1014
|
-
} catch {
|
|
1015
|
-
return NextResponse.json({ error: "Destination folder not found" }, { status: 404 });
|
|
1016
|
-
}
|
|
1017
|
-
const moved = [];
|
|
1018
|
-
const errors = [];
|
|
1019
|
-
const meta = await loadMeta();
|
|
1020
|
-
let metaChanged = false;
|
|
1021
|
-
for (const itemPath of paths) {
|
|
1022
|
-
const safePath = itemPath.replace(/\.\./g, "");
|
|
1023
|
-
const absolutePath = path.join(process.cwd(), safePath);
|
|
1024
|
-
const itemName = path.basename(safePath);
|
|
1025
|
-
const newAbsolutePath = path.join(absoluteDestination, itemName);
|
|
1026
|
-
if (absoluteDestination.startsWith(absolutePath + path.sep)) {
|
|
1027
|
-
errors.push(`Cannot move ${itemName} into itself`);
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
try {
|
|
1031
|
-
await fs.access(absolutePath);
|
|
1032
|
-
} catch {
|
|
1033
|
-
errors.push(`${itemName} not found`);
|
|
1034
|
-
continue;
|
|
1035
|
-
}
|
|
1036
|
-
try {
|
|
1037
|
-
await fs.access(newAbsolutePath);
|
|
1038
|
-
errors.push(`${itemName} already exists in destination`);
|
|
1039
|
-
continue;
|
|
1040
|
-
} catch {
|
|
1041
|
-
}
|
|
1042
|
-
try {
|
|
1043
|
-
await fs.rename(absolutePath, newAbsolutePath);
|
|
1044
|
-
const stats = await fs.stat(newAbsolutePath);
|
|
1045
|
-
if (stats.isFile() && isImageFile(itemName)) {
|
|
1046
|
-
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
1047
|
-
const newRelativePath = path.join(safeDestination.replace(/^public\//, ""), itemName);
|
|
1048
|
-
const oldKey = "/" + oldRelativePath;
|
|
1049
|
-
const newKey = "/" + newRelativePath;
|
|
1050
|
-
if (meta[oldKey]) {
|
|
1051
|
-
const entry = meta[oldKey];
|
|
1052
|
-
const oldThumbPaths = getAllThumbnailPaths(oldKey);
|
|
1053
|
-
const newThumbPaths = getAllThumbnailPaths(newKey);
|
|
1054
|
-
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
1055
|
-
const oldThumbPath = path.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
1056
|
-
const newThumbPath = path.join(process.cwd(), "public", newThumbPaths[i]);
|
|
1057
|
-
await fs.mkdir(path.dirname(newThumbPath), { recursive: true });
|
|
1058
|
-
try {
|
|
1059
|
-
await fs.rename(oldThumbPath, newThumbPath);
|
|
1060
|
-
} catch {
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
delete meta[oldKey];
|
|
1064
|
-
meta[newKey] = entry;
|
|
1065
|
-
metaChanged = true;
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
moved.push(itemPath);
|
|
1069
|
-
} catch (error) {
|
|
1070
|
-
errors.push(`Failed to move ${itemName}`);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
if (metaChanged) {
|
|
1074
|
-
await saveMeta(meta);
|
|
1075
|
-
}
|
|
1076
|
-
return NextResponse.json({
|
|
1077
|
-
success: errors.length === 0,
|
|
1078
|
-
moved,
|
|
1079
|
-
errors: errors.length > 0 ? errors : void 0
|
|
1080
|
-
});
|
|
1081
|
-
} catch (error) {
|
|
1082
|
-
console.error("Failed to move:", error);
|
|
1083
|
-
return NextResponse.json({ error: "Failed to move items" }, { status: 500 });
|
|
1127
|
+
if (route === "process-all") {
|
|
1128
|
+
return handleProcessAllStream();
|
|
1129
|
+
}
|
|
1130
|
+
if (route === "create-folder") {
|
|
1131
|
+
return handleCreateFolder(request);
|
|
1132
|
+
}
|
|
1133
|
+
if (route === "rename") {
|
|
1134
|
+
return handleRename(request);
|
|
1135
|
+
}
|
|
1136
|
+
if (route === "move") {
|
|
1137
|
+
return handleMove(request);
|
|
1084
1138
|
}
|
|
1139
|
+
return NextResponse4.json({ error: "Not found" }, { status: 404 });
|
|
1085
1140
|
}
|
|
1086
|
-
async function
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const folders = [];
|
|
1090
|
-
async function scanDir(dir, relativePath, depth) {
|
|
1091
|
-
try {
|
|
1092
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1093
|
-
for (const entry of entries) {
|
|
1094
|
-
if (!entry.isDirectory()) continue;
|
|
1095
|
-
if (entry.name.startsWith(".")) continue;
|
|
1096
|
-
const folderRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1097
|
-
folders.push({
|
|
1098
|
-
path: `public/${folderRelativePath}`,
|
|
1099
|
-
name: entry.name,
|
|
1100
|
-
depth
|
|
1101
|
-
});
|
|
1102
|
-
await scanDir(path.join(dir, entry.name), folderRelativePath, depth + 1);
|
|
1103
|
-
}
|
|
1104
|
-
} catch {
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
folders.push({ path: "public", name: "public", depth: 0 });
|
|
1108
|
-
await scanDir(publicDir, "", 1);
|
|
1109
|
-
return NextResponse.json({ folders });
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
console.error("Failed to list folders:", error);
|
|
1112
|
-
return NextResponse.json({ error: "Failed to list folders" }, { status: 500 });
|
|
1141
|
+
async function DELETE(request) {
|
|
1142
|
+
if (process.env.NODE_ENV !== "development") {
|
|
1143
|
+
return NextResponse4.json({ error: "Not available in production" }, { status: 403 });
|
|
1113
1144
|
}
|
|
1145
|
+
return handleDelete(request);
|
|
1114
1146
|
}
|
|
1115
1147
|
export {
|
|
1116
1148
|
DELETE,
|
|
1117
1149
|
GET,
|
|
1118
1150
|
POST
|
|
1119
1151
|
};
|
|
1120
|
-
//# sourceMappingURL=
|
|
1152
|
+
//# sourceMappingURL=index.mjs.map
|