@gallop.software/studio 0.1.88 → 0.1.90
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-Y35A2T7S.js → StudioUI-HWUO2H6J.js} +228 -37
- package/dist/StudioUI-HWUO2H6J.js.map +1 -0
- package/dist/{StudioUI-6CQ7MX7R.mjs → StudioUI-LWHNOTSN.mjs} +221 -30
- package/dist/StudioUI-LWHNOTSN.mjs.map +1 -0
- package/dist/{chunk-CN5NRNWB.js → chunk-JWAAU3NN.js} +1 -1
- package/dist/chunk-JWAAU3NN.js.map +1 -0
- package/dist/{chunk-3RI33B7A.mjs → chunk-ZGXOYJKZ.mjs} +1 -1
- package/dist/chunk-ZGXOYJKZ.mjs.map +1 -0
- package/dist/handlers/index.d.mts +1 -1
- package/dist/handlers/index.d.ts +1 -1
- package/dist/handlers/index.js +447 -329
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/index.mjs +457 -339
- package/dist/handlers/index.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{types-C9CMIJLW.d.mts → types-DzM_J-55.d.mts} +11 -8
- package/dist/{types-C9CMIJLW.d.ts → types-DzM_J-55.d.ts} +11 -8
- package/package.json +1 -1
- package/dist/StudioUI-6CQ7MX7R.mjs.map +0 -1
- package/dist/StudioUI-Y35A2T7S.js.map +0 -1
- package/dist/chunk-3RI33B7A.mjs.map +0 -1
- package/dist/chunk-CN5NRNWB.js.map +0 -1
package/dist/handlers/index.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
getAllThumbnailPaths
|
|
3
|
-
|
|
2
|
+
getAllThumbnailPaths,
|
|
3
|
+
getThumbnailPath
|
|
4
|
+
} from "../chunk-ZGXOYJKZ.mjs";
|
|
4
5
|
|
|
5
6
|
// src/handlers/index.ts
|
|
6
7
|
import { NextResponse as NextResponse4 } from "next/server";
|
|
7
8
|
|
|
8
9
|
// src/handlers/list.ts
|
|
9
10
|
import { NextResponse } from "next/server";
|
|
10
|
-
import { promises as
|
|
11
|
+
import { promises as fs4 } from "fs";
|
|
11
12
|
import path5 from "path";
|
|
12
|
-
import sharp2 from "sharp";
|
|
13
13
|
|
|
14
14
|
// src/handlers/utils/meta.ts
|
|
15
15
|
import { promises as fs } from "fs";
|
|
@@ -31,7 +31,6 @@ async function saveMeta(meta) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// src/handlers/utils/files.ts
|
|
34
|
-
import { promises as fs2 } from "fs";
|
|
35
34
|
import path2 from "path";
|
|
36
35
|
function isImageFile(filename) {
|
|
37
36
|
const ext = path2.extname(filename).toLowerCase();
|
|
@@ -63,32 +62,9 @@ function getContentType(filePath) {
|
|
|
63
62
|
return "application/octet-stream";
|
|
64
63
|
}
|
|
65
64
|
}
|
|
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
65
|
|
|
90
66
|
// src/handlers/utils/thumbnails.ts
|
|
91
|
-
import { promises as
|
|
67
|
+
import { promises as fs2 } from "fs";
|
|
92
68
|
import path3 from "path";
|
|
93
69
|
import sharp from "sharp";
|
|
94
70
|
import { encode } from "blurhash";
|
|
@@ -107,7 +83,7 @@ async function processImage(buffer, imageKey) {
|
|
|
107
83
|
const ext = path3.extname(keyWithoutSlash).toLowerCase();
|
|
108
84
|
const imageDir = path3.dirname(keyWithoutSlash);
|
|
109
85
|
const imagesPath = path3.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
|
|
110
|
-
await
|
|
86
|
+
await fs2.mkdir(imagesPath, { recursive: true });
|
|
111
87
|
const isPng = ext === ".png";
|
|
112
88
|
const outputExt = isPng ? ".png" : ".jpg";
|
|
113
89
|
const fullFileName = imageDir === "." ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`;
|
|
@@ -138,12 +114,12 @@ async function processImage(buffer, imageKey) {
|
|
|
138
114
|
return {
|
|
139
115
|
w: originalWidth,
|
|
140
116
|
h: originalHeight,
|
|
141
|
-
|
|
117
|
+
b: blurhash
|
|
142
118
|
};
|
|
143
119
|
}
|
|
144
120
|
|
|
145
121
|
// src/handlers/utils/cdn.ts
|
|
146
|
-
import { promises as
|
|
122
|
+
import { promises as fs3 } from "fs";
|
|
147
123
|
import path4 from "path";
|
|
148
124
|
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
|
|
149
125
|
function getR2Client() {
|
|
@@ -183,7 +159,7 @@ async function uploadToCdn(imageKey) {
|
|
|
183
159
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
184
160
|
const localPath = path4.join(process.cwd(), "public", thumbPath);
|
|
185
161
|
try {
|
|
186
|
-
const fileBuffer = await
|
|
162
|
+
const fileBuffer = await fs3.readFile(localPath);
|
|
187
163
|
await r2.send(
|
|
188
164
|
new PutObjectCommand({
|
|
189
165
|
Bucket: bucketName,
|
|
@@ -200,7 +176,7 @@ async function deleteLocalThumbnails(imageKey) {
|
|
|
200
176
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
201
177
|
const localPath = path4.join(process.cwd(), "public", thumbPath);
|
|
202
178
|
try {
|
|
203
|
-
await
|
|
179
|
+
await fs3.unlink(localPath);
|
|
204
180
|
} catch {
|
|
205
181
|
}
|
|
206
182
|
}
|
|
@@ -211,70 +187,85 @@ async function handleList(request) {
|
|
|
211
187
|
const searchParams = request.nextUrl.searchParams;
|
|
212
188
|
const requestedPath = searchParams.get("path") || "public";
|
|
213
189
|
try {
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
if (
|
|
217
|
-
return NextResponse.json({
|
|
190
|
+
const meta = await loadMeta();
|
|
191
|
+
const metaKeys = Object.keys(meta);
|
|
192
|
+
if (metaKeys.length === 0) {
|
|
193
|
+
return NextResponse.json({ items: [], isEmpty: true });
|
|
218
194
|
}
|
|
195
|
+
const relativePath = requestedPath.replace(/^public\/?/, "");
|
|
196
|
+
const pathPrefix = relativePath ? `/${relativePath}/` : "/";
|
|
219
197
|
const items = [];
|
|
220
|
-
const
|
|
221
|
-
for (const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
198
|
+
const seenFolders = /* @__PURE__ */ new Set();
|
|
199
|
+
for (const key of metaKeys) {
|
|
200
|
+
const entry = meta[key];
|
|
201
|
+
if (!key.startsWith(pathPrefix) && pathPrefix !== "/") continue;
|
|
202
|
+
if (pathPrefix === "/" && !key.startsWith("/")) continue;
|
|
203
|
+
const remaining = pathPrefix === "/" ? key.slice(1) : key.slice(pathPrefix.length);
|
|
204
|
+
if (!remaining) continue;
|
|
205
|
+
const slashIndex = remaining.indexOf("/");
|
|
206
|
+
if (slashIndex !== -1) {
|
|
207
|
+
const folderName = remaining.slice(0, slashIndex);
|
|
208
|
+
if (!seenFolders.has(folderName)) {
|
|
209
|
+
seenFolders.add(folderName);
|
|
210
|
+
const folderPrefix = pathPrefix === "/" ? `/${folderName}/` : `${pathPrefix}${folderName}/`;
|
|
211
|
+
let fileCount = 0;
|
|
212
|
+
for (const k of metaKeys) {
|
|
213
|
+
if (k.startsWith(folderPrefix)) fileCount++;
|
|
214
|
+
}
|
|
215
|
+
items.push({
|
|
216
|
+
name: folderName,
|
|
217
|
+
path: relativePath ? `public/${relativePath}/${folderName}` : `public/${folderName}`,
|
|
218
|
+
type: "folder",
|
|
219
|
+
fileCount
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
const fileName = remaining;
|
|
224
|
+
const isImage = isImageFile(fileName);
|
|
225
|
+
const isSynced = entry.c === 1;
|
|
237
226
|
let thumbnail;
|
|
238
227
|
let hasThumbnail = false;
|
|
239
|
-
let
|
|
240
|
-
if (isImage) {
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
228
|
+
let fileSize;
|
|
229
|
+
if (isImage && (entry.w || entry.b)) {
|
|
230
|
+
const thumbPath = getThumbnailPath(key, "sm");
|
|
231
|
+
if (isSynced) {
|
|
232
|
+
const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL;
|
|
233
|
+
if (cdnUrl) {
|
|
234
|
+
thumbnail = `${cdnUrl}${thumbPath}`;
|
|
235
|
+
hasThumbnail = true;
|
|
236
|
+
}
|
|
245
237
|
} else {
|
|
246
|
-
const
|
|
247
|
-
const baseName = path5.basename(entry.name, ext);
|
|
248
|
-
const thumbnailDir = relativePath ? `images/${relativePath}` : "images";
|
|
249
|
-
const thumbnailName = `${baseName}-sm${ext === ".png" ? ".png" : ".jpg"}`;
|
|
250
|
-
const thumbnailPath = path5.join(process.cwd(), "public", thumbnailDir, thumbnailName);
|
|
238
|
+
const localThumbPath = path5.join(process.cwd(), "public", thumbPath);
|
|
251
239
|
try {
|
|
252
|
-
await
|
|
253
|
-
thumbnail =
|
|
240
|
+
await fs4.access(localThumbPath);
|
|
241
|
+
thumbnail = thumbPath;
|
|
254
242
|
hasThumbnail = true;
|
|
255
243
|
} catch {
|
|
256
|
-
thumbnail =
|
|
244
|
+
thumbnail = key;
|
|
257
245
|
hasThumbnail = false;
|
|
258
246
|
}
|
|
259
247
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
248
|
+
} else if (isImage) {
|
|
249
|
+
thumbnail = key;
|
|
250
|
+
hasThumbnail = false;
|
|
251
|
+
}
|
|
252
|
+
if (!isSynced) {
|
|
253
|
+
try {
|
|
254
|
+
const filePath = path5.join(process.cwd(), "public", key);
|
|
255
|
+
const stats = await fs4.stat(filePath);
|
|
256
|
+
fileSize = stats.size;
|
|
257
|
+
} catch {
|
|
268
258
|
}
|
|
269
259
|
}
|
|
270
260
|
items.push({
|
|
271
|
-
name:
|
|
272
|
-
path:
|
|
261
|
+
name: fileName,
|
|
262
|
+
path: relativePath ? `public/${relativePath}/${fileName}` : `public/${fileName}`,
|
|
273
263
|
type: "file",
|
|
274
|
-
size:
|
|
264
|
+
size: fileSize,
|
|
275
265
|
thumbnail,
|
|
276
266
|
hasThumbnail,
|
|
277
|
-
|
|
267
|
+
cdnSynced: isSynced,
|
|
268
|
+
dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
|
|
278
269
|
});
|
|
279
270
|
}
|
|
280
271
|
}
|
|
@@ -291,62 +282,49 @@ async function handleSearch(request) {
|
|
|
291
282
|
return NextResponse.json({ items: [] });
|
|
292
283
|
}
|
|
293
284
|
try {
|
|
285
|
+
const meta = await loadMeta();
|
|
294
286
|
const items = [];
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
hasThumbnail = true;
|
|
321
|
-
} catch {
|
|
322
|
-
thumbnail = `/${itemRelPath}`;
|
|
323
|
-
hasThumbnail = false;
|
|
324
|
-
}
|
|
325
|
-
if (!entry.name.toLowerCase().endsWith(".svg")) {
|
|
326
|
-
try {
|
|
327
|
-
const metadata = await sharp2(fullPath).metadata();
|
|
328
|
-
if (metadata.width && metadata.height) {
|
|
329
|
-
dimensions = { width: metadata.width, height: metadata.height };
|
|
330
|
-
}
|
|
331
|
-
} catch {
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
items.push({
|
|
335
|
-
name: entry.name,
|
|
336
|
-
path: itemPath,
|
|
337
|
-
type: "file",
|
|
338
|
-
size: stats.size,
|
|
339
|
-
thumbnail,
|
|
340
|
-
hasThumbnail,
|
|
341
|
-
dimensions
|
|
342
|
-
});
|
|
343
|
-
}
|
|
287
|
+
for (const [key, entry] of Object.entries(meta)) {
|
|
288
|
+
if (!key.toLowerCase().includes(query)) continue;
|
|
289
|
+
const fileName = path5.basename(key);
|
|
290
|
+
const relativePath = key.slice(1);
|
|
291
|
+
const isImage = isImageFile(fileName);
|
|
292
|
+
const isSynced = entry.c === 1;
|
|
293
|
+
let thumbnail;
|
|
294
|
+
let hasThumbnail = false;
|
|
295
|
+
if (isImage && (entry.w || entry.b)) {
|
|
296
|
+
const thumbPath = getThumbnailPath(key, "sm");
|
|
297
|
+
if (isSynced) {
|
|
298
|
+
const cdnUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL || process.env.NEXT_PUBLIC_CLOUDFLARE_R2_PUBLIC_URL;
|
|
299
|
+
if (cdnUrl) {
|
|
300
|
+
thumbnail = `${cdnUrl}${thumbPath}`;
|
|
301
|
+
hasThumbnail = true;
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
const localThumbPath = path5.join(process.cwd(), "public", thumbPath);
|
|
305
|
+
try {
|
|
306
|
+
await fs4.access(localThumbPath);
|
|
307
|
+
thumbnail = thumbPath;
|
|
308
|
+
hasThumbnail = true;
|
|
309
|
+
} catch {
|
|
310
|
+
thumbnail = key;
|
|
311
|
+
hasThumbnail = false;
|
|
344
312
|
}
|
|
345
313
|
}
|
|
346
|
-
}
|
|
314
|
+
} else if (isImage) {
|
|
315
|
+
thumbnail = key;
|
|
316
|
+
hasThumbnail = false;
|
|
347
317
|
}
|
|
318
|
+
items.push({
|
|
319
|
+
name: fileName,
|
|
320
|
+
path: `public/${relativePath}`,
|
|
321
|
+
type: "file",
|
|
322
|
+
thumbnail,
|
|
323
|
+
hasThumbnail,
|
|
324
|
+
cdnSynced: isSynced,
|
|
325
|
+
dimensions: entry.w && entry.h ? { width: entry.w, height: entry.h } : void 0
|
|
326
|
+
});
|
|
348
327
|
}
|
|
349
|
-
await searchDir(publicDir, "");
|
|
350
328
|
return NextResponse.json({ items });
|
|
351
329
|
} catch (error) {
|
|
352
330
|
console.error("Failed to search:", error);
|
|
@@ -355,27 +333,28 @@ async function handleSearch(request) {
|
|
|
355
333
|
}
|
|
356
334
|
async function handleListFolders() {
|
|
357
335
|
try {
|
|
358
|
-
const
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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);
|
|
373
|
-
}
|
|
374
|
-
} catch {
|
|
336
|
+
const meta = await loadMeta();
|
|
337
|
+
const folderSet = /* @__PURE__ */ new Set();
|
|
338
|
+
for (const key of Object.keys(meta)) {
|
|
339
|
+
const parts = key.split("/");
|
|
340
|
+
let current = "";
|
|
341
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
342
|
+
current = current ? `${current}/${parts[i]}` : parts[i];
|
|
343
|
+
folderSet.add(current);
|
|
375
344
|
}
|
|
376
345
|
}
|
|
346
|
+
const folders = [];
|
|
377
347
|
folders.push({ path: "public", name: "public", depth: 0 });
|
|
378
|
-
|
|
348
|
+
const sortedFolders = Array.from(folderSet).sort();
|
|
349
|
+
for (const folderPath of sortedFolders) {
|
|
350
|
+
const depth = folderPath.split("/").length;
|
|
351
|
+
const name = folderPath.split("/").pop() || folderPath;
|
|
352
|
+
folders.push({
|
|
353
|
+
path: `public/${folderPath}`,
|
|
354
|
+
name,
|
|
355
|
+
depth
|
|
356
|
+
});
|
|
357
|
+
}
|
|
379
358
|
return NextResponse.json({ folders });
|
|
380
359
|
} catch (error) {
|
|
381
360
|
console.error("Failed to list folders:", error);
|
|
@@ -384,26 +363,14 @@ async function handleListFolders() {
|
|
|
384
363
|
}
|
|
385
364
|
async function handleCountImages() {
|
|
386
365
|
try {
|
|
366
|
+
const meta = await loadMeta();
|
|
387
367
|
const allImages = [];
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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 {
|
|
368
|
+
for (const key of Object.keys(meta)) {
|
|
369
|
+
const fileName = path5.basename(key);
|
|
370
|
+
if (isImageFile(fileName)) {
|
|
371
|
+
allImages.push(key.slice(1));
|
|
403
372
|
}
|
|
404
373
|
}
|
|
405
|
-
const publicDir = path5.join(process.cwd(), "public");
|
|
406
|
-
await scanPublicFolder(publicDir);
|
|
407
374
|
return NextResponse.json({
|
|
408
375
|
count: allImages.length,
|
|
409
376
|
images: allImages
|
|
@@ -421,29 +388,22 @@ async function handleFolderImages(request) {
|
|
|
421
388
|
return NextResponse.json({ error: "No folders provided" }, { status: 400 });
|
|
422
389
|
}
|
|
423
390
|
const folders = foldersParam.split(",");
|
|
391
|
+
const meta = await loadMeta();
|
|
424
392
|
const allImages = [];
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
393
|
+
const prefixes = folders.map((f) => {
|
|
394
|
+
const rel = f.replace(/^public\/?/, "");
|
|
395
|
+
return rel ? `/${rel}/` : "/";
|
|
396
|
+
});
|
|
397
|
+
for (const key of Object.keys(meta)) {
|
|
398
|
+
const fileName = path5.basename(key);
|
|
399
|
+
if (!isImageFile(fileName)) continue;
|
|
400
|
+
for (const prefix of prefixes) {
|
|
401
|
+
if (key.startsWith(prefix) || prefix === "/" && key.startsWith("/")) {
|
|
402
|
+
allImages.push(key.slice(1));
|
|
403
|
+
break;
|
|
437
404
|
}
|
|
438
|
-
} catch {
|
|
439
405
|
}
|
|
440
406
|
}
|
|
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
407
|
return NextResponse.json({
|
|
448
408
|
count: allImages.length,
|
|
449
409
|
images: allImages
|
|
@@ -456,10 +416,9 @@ async function handleFolderImages(request) {
|
|
|
456
416
|
|
|
457
417
|
// src/handlers/files.ts
|
|
458
418
|
import { NextResponse as NextResponse2 } from "next/server";
|
|
459
|
-
import { promises as
|
|
419
|
+
import { promises as fs5 } from "fs";
|
|
460
420
|
import path6 from "path";
|
|
461
|
-
import
|
|
462
|
-
import { encode as encode2 } from "blurhash";
|
|
421
|
+
import sharp2 from "sharp";
|
|
463
422
|
async function handleUpload(request) {
|
|
464
423
|
try {
|
|
465
424
|
const formData = await request.formData();
|
|
@@ -471,11 +430,9 @@ async function handleUpload(request) {
|
|
|
471
430
|
const bytes = await file.arrayBuffer();
|
|
472
431
|
const buffer = Buffer.from(bytes);
|
|
473
432
|
const fileName = file.name;
|
|
474
|
-
const baseName = path6.basename(fileName, path6.extname(fileName));
|
|
475
433
|
const ext = path6.extname(fileName).toLowerCase();
|
|
476
434
|
const isImage = isImageFile(fileName);
|
|
477
|
-
const
|
|
478
|
-
const isProcessableImage = isImage && !isSvg;
|
|
435
|
+
const isMedia = isMediaFile(fileName);
|
|
479
436
|
const meta = await loadMeta();
|
|
480
437
|
let relativeDir = "";
|
|
481
438
|
if (targetPath === "public") {
|
|
@@ -489,71 +446,49 @@ async function handleUpload(request) {
|
|
|
489
446
|
{ status: 400 }
|
|
490
447
|
);
|
|
491
448
|
}
|
|
449
|
+
let imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
|
|
450
|
+
if (meta[imageKey]) {
|
|
451
|
+
const baseName = path6.basename(fileName, ext);
|
|
452
|
+
let counter = 1;
|
|
453
|
+
let newFileName = `${baseName}-${counter}${ext}`;
|
|
454
|
+
let newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
|
|
455
|
+
while (meta[newKey]) {
|
|
456
|
+
counter++;
|
|
457
|
+
newFileName = `${baseName}-${counter}${ext}`;
|
|
458
|
+
newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
|
|
459
|
+
}
|
|
460
|
+
imageKey = newKey;
|
|
461
|
+
}
|
|
462
|
+
const actualFileName = path6.basename(imageKey);
|
|
492
463
|
const uploadDir = path6.join(process.cwd(), "public", relativeDir);
|
|
493
|
-
await
|
|
494
|
-
await
|
|
495
|
-
if (!
|
|
464
|
+
await fs5.mkdir(uploadDir, { recursive: true });
|
|
465
|
+
await fs5.writeFile(path6.join(uploadDir, actualFileName), buffer);
|
|
466
|
+
if (!isMedia) {
|
|
496
467
|
return NextResponse2.json({
|
|
497
468
|
success: true,
|
|
498
|
-
message: "File uploaded
|
|
499
|
-
path: `public/${relativeDir ? relativeDir + "/" : ""}${
|
|
469
|
+
message: "File uploaded (not a media file)",
|
|
470
|
+
path: `public/${relativeDir ? relativeDir + "/" : ""}${actualFileName}`
|
|
500
471
|
});
|
|
501
472
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
let originalWidth = 0;
|
|
512
|
-
let originalHeight = 0;
|
|
513
|
-
let blurhash = "";
|
|
514
|
-
const originalPath = `/${relativeDir ? relativeDir + "/" : ""}${fileName}`;
|
|
515
|
-
if (isSvg) {
|
|
516
|
-
const fullPath = path6.join(imagesPath, fileName);
|
|
517
|
-
await fs6.writeFile(fullPath, buffer);
|
|
518
|
-
} else if (isProcessableImage) {
|
|
519
|
-
const sharpInstance = sharp3(buffer);
|
|
520
|
-
const metadata = await sharpInstance.metadata();
|
|
521
|
-
originalWidth = metadata.width || 0;
|
|
522
|
-
originalHeight = metadata.height || 0;
|
|
523
|
-
const outputExt = ext === ".png" ? ".png" : ".jpg";
|
|
524
|
-
const fullFileName = `${baseName}${outputExt}`;
|
|
525
|
-
const fullPath = path6.join(imagesPath, fullFileName);
|
|
526
|
-
if (ext === ".png") {
|
|
527
|
-
await sharp3(buffer).png({ quality: 85 }).toFile(fullPath);
|
|
528
|
-
} else {
|
|
529
|
-
await sharp3(buffer).jpeg({ quality: 85 }).toFile(fullPath);
|
|
530
|
-
}
|
|
531
|
-
for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
|
|
532
|
-
const { width: maxWidth, suffix } = sizeConfig;
|
|
533
|
-
if (originalWidth <= maxWidth) {
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
const ratio = originalHeight / originalWidth;
|
|
537
|
-
const newHeight = Math.round(maxWidth * ratio);
|
|
538
|
-
const sizeFileName = `${baseName}${suffix}${outputExt}`;
|
|
539
|
-
const sizePath = path6.join(imagesPath, sizeFileName);
|
|
540
|
-
if (ext === ".png") {
|
|
541
|
-
await sharp3(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
|
|
542
|
-
} else {
|
|
543
|
-
await sharp3(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
|
|
544
|
-
}
|
|
473
|
+
if (isImage && ext !== ".svg") {
|
|
474
|
+
try {
|
|
475
|
+
const metadata = await sharp2(buffer).metadata();
|
|
476
|
+
meta[imageKey] = {
|
|
477
|
+
w: metadata.width || 0,
|
|
478
|
+
h: metadata.height || 0
|
|
479
|
+
};
|
|
480
|
+
} catch {
|
|
481
|
+
meta[imageKey] = { w: 0, h: 0 };
|
|
545
482
|
}
|
|
546
|
-
|
|
547
|
-
|
|
483
|
+
} else {
|
|
484
|
+
meta[imageKey] = {};
|
|
548
485
|
}
|
|
549
|
-
const entry = {
|
|
550
|
-
w: originalWidth,
|
|
551
|
-
h: originalHeight,
|
|
552
|
-
blur: blurhash
|
|
553
|
-
};
|
|
554
|
-
meta[originalPath] = entry;
|
|
555
486
|
await saveMeta(meta);
|
|
556
|
-
return NextResponse2.json({
|
|
487
|
+
return NextResponse2.json({
|
|
488
|
+
success: true,
|
|
489
|
+
imageKey,
|
|
490
|
+
message: 'File uploaded. Run "Process Images" to generate thumbnails.'
|
|
491
|
+
});
|
|
557
492
|
} catch (error) {
|
|
558
493
|
console.error("Failed to upload:", error);
|
|
559
494
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
@@ -576,31 +511,61 @@ async function handleDelete(request) {
|
|
|
576
511
|
continue;
|
|
577
512
|
}
|
|
578
513
|
const absolutePath = path6.join(process.cwd(), itemPath);
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
514
|
+
const imageKey = "/" + itemPath.replace(/^public\//, "");
|
|
515
|
+
const entry = meta[imageKey];
|
|
516
|
+
const isSynced = entry?.c === 1;
|
|
517
|
+
try {
|
|
518
|
+
const stats = await fs5.stat(absolutePath);
|
|
519
|
+
if (stats.isDirectory()) {
|
|
520
|
+
await fs5.rm(absolutePath, { recursive: true });
|
|
521
|
+
const prefix = imageKey + "/";
|
|
522
|
+
for (const key of Object.keys(meta)) {
|
|
523
|
+
if (key.startsWith(prefix) || key === imageKey) {
|
|
524
|
+
if (!meta[key].c) {
|
|
525
|
+
for (const thumbPath of getAllThumbnailPaths(key)) {
|
|
526
|
+
const absoluteThumbPath = path6.join(process.cwd(), "public", thumbPath);
|
|
527
|
+
try {
|
|
528
|
+
await fs5.unlink(absoluteThumbPath);
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
delete meta[key];
|
|
534
|
+
}
|
|
586
535
|
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
} catch {
|
|
536
|
+
} else {
|
|
537
|
+
await fs5.unlink(absolutePath);
|
|
538
|
+
const isInImagesFolder = itemPath.startsWith("public/images/");
|
|
539
|
+
if (!isInImagesFolder && entry) {
|
|
540
|
+
if (!isSynced) {
|
|
541
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
542
|
+
const absoluteThumbPath = path6.join(process.cwd(), "public", thumbPath);
|
|
543
|
+
try {
|
|
544
|
+
await fs5.unlink(absoluteThumbPath);
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
599
547
|
}
|
|
600
548
|
}
|
|
601
549
|
delete meta[imageKey];
|
|
602
550
|
}
|
|
603
551
|
}
|
|
552
|
+
} catch {
|
|
553
|
+
if (entry) {
|
|
554
|
+
delete meta[imageKey];
|
|
555
|
+
} else {
|
|
556
|
+
const prefix = imageKey + "/";
|
|
557
|
+
let foundAny = false;
|
|
558
|
+
for (const key of Object.keys(meta)) {
|
|
559
|
+
if (key.startsWith(prefix)) {
|
|
560
|
+
delete meta[key];
|
|
561
|
+
foundAny = true;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (!foundAny) {
|
|
565
|
+
errors.push(`Not found: ${itemPath}`);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
604
569
|
}
|
|
605
570
|
deleted.push(itemPath);
|
|
606
571
|
} catch (error) {
|
|
@@ -635,11 +600,11 @@ async function handleCreateFolder(request) {
|
|
|
635
600
|
return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
|
|
636
601
|
}
|
|
637
602
|
try {
|
|
638
|
-
await
|
|
603
|
+
await fs5.access(folderPath);
|
|
639
604
|
return NextResponse2.json({ error: "A folder with this name already exists" }, { status: 400 });
|
|
640
605
|
} catch {
|
|
641
606
|
}
|
|
642
|
-
await
|
|
607
|
+
await fs5.mkdir(folderPath, { recursive: true });
|
|
643
608
|
return NextResponse2.json({ success: true, path: path6.join(safePath, sanitizedName) });
|
|
644
609
|
} catch (error) {
|
|
645
610
|
console.error("Failed to create folder:", error);
|
|
@@ -664,19 +629,19 @@ async function handleRename(request) {
|
|
|
664
629
|
return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
|
|
665
630
|
}
|
|
666
631
|
try {
|
|
667
|
-
await
|
|
632
|
+
await fs5.access(absoluteOldPath);
|
|
668
633
|
} catch {
|
|
669
634
|
return NextResponse2.json({ error: "File or folder not found" }, { status: 404 });
|
|
670
635
|
}
|
|
671
636
|
try {
|
|
672
|
-
await
|
|
637
|
+
await fs5.access(absoluteNewPath);
|
|
673
638
|
return NextResponse2.json({ error: "An item with this name already exists" }, { status: 400 });
|
|
674
639
|
} catch {
|
|
675
640
|
}
|
|
676
|
-
const stats = await
|
|
641
|
+
const stats = await fs5.stat(absoluteOldPath);
|
|
677
642
|
const isFile = stats.isFile();
|
|
678
643
|
const isImage = isFile && isImageFile(path6.basename(oldPath));
|
|
679
|
-
await
|
|
644
|
+
await fs5.rename(absoluteOldPath, absoluteNewPath);
|
|
680
645
|
if (isImage) {
|
|
681
646
|
const meta = await loadMeta();
|
|
682
647
|
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
@@ -690,9 +655,9 @@ async function handleRename(request) {
|
|
|
690
655
|
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
691
656
|
const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
692
657
|
const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[i]);
|
|
693
|
-
await
|
|
658
|
+
await fs5.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
694
659
|
try {
|
|
695
|
-
await
|
|
660
|
+
await fs5.rename(oldThumbPath, newThumbPath);
|
|
696
661
|
} catch {
|
|
697
662
|
}
|
|
698
663
|
}
|
|
@@ -723,7 +688,7 @@ async function handleMove(request) {
|
|
|
723
688
|
return NextResponse2.json({ error: "Invalid destination" }, { status: 400 });
|
|
724
689
|
}
|
|
725
690
|
try {
|
|
726
|
-
const destStats = await
|
|
691
|
+
const destStats = await fs5.stat(absoluteDestination);
|
|
727
692
|
if (!destStats.isDirectory()) {
|
|
728
693
|
return NextResponse2.json({ error: "Destination is not a folder" }, { status: 400 });
|
|
729
694
|
}
|
|
@@ -744,20 +709,20 @@ async function handleMove(request) {
|
|
|
744
709
|
continue;
|
|
745
710
|
}
|
|
746
711
|
try {
|
|
747
|
-
await
|
|
712
|
+
await fs5.access(absolutePath);
|
|
748
713
|
} catch {
|
|
749
714
|
errors.push(`${itemName} not found`);
|
|
750
715
|
continue;
|
|
751
716
|
}
|
|
752
717
|
try {
|
|
753
|
-
await
|
|
718
|
+
await fs5.access(newAbsolutePath);
|
|
754
719
|
errors.push(`${itemName} already exists in destination`);
|
|
755
720
|
continue;
|
|
756
721
|
} catch {
|
|
757
722
|
}
|
|
758
723
|
try {
|
|
759
|
-
await
|
|
760
|
-
const stats = await
|
|
724
|
+
await fs5.rename(absolutePath, newAbsolutePath);
|
|
725
|
+
const stats = await fs5.stat(newAbsolutePath);
|
|
761
726
|
if (stats.isFile() && isImageFile(itemName)) {
|
|
762
727
|
const oldRelativePath = safePath.replace(/^public\//, "");
|
|
763
728
|
const newRelativePath = path6.join(safeDestination.replace(/^public\//, ""), itemName);
|
|
@@ -770,9 +735,9 @@ async function handleMove(request) {
|
|
|
770
735
|
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
771
736
|
const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
772
737
|
const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[i]);
|
|
773
|
-
await
|
|
738
|
+
await fs5.mkdir(path6.dirname(newThumbPath), { recursive: true });
|
|
774
739
|
try {
|
|
775
|
-
await
|
|
740
|
+
await fs5.rename(oldThumbPath, newThumbPath);
|
|
776
741
|
} catch {
|
|
777
742
|
}
|
|
778
743
|
}
|
|
@@ -802,7 +767,7 @@ async function handleMove(request) {
|
|
|
802
767
|
|
|
803
768
|
// src/handlers/images.ts
|
|
804
769
|
import { NextResponse as NextResponse3 } from "next/server";
|
|
805
|
-
import { promises as
|
|
770
|
+
import { promises as fs6 } from "fs";
|
|
806
771
|
import path7 from "path";
|
|
807
772
|
import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
|
|
808
773
|
async function handleSync(request) {
|
|
@@ -833,18 +798,33 @@ async function handleSync(request) {
|
|
|
833
798
|
for (const imageKey of imageKeys) {
|
|
834
799
|
const entry = meta[imageKey];
|
|
835
800
|
if (!entry) {
|
|
836
|
-
errors.push(`Image not found in meta: ${imageKey}
|
|
801
|
+
errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
|
|
837
802
|
continue;
|
|
838
803
|
}
|
|
839
|
-
if (entry.
|
|
804
|
+
if (entry.c) {
|
|
840
805
|
synced.push(imageKey);
|
|
841
806
|
continue;
|
|
842
807
|
}
|
|
843
808
|
try {
|
|
809
|
+
const originalLocalPath = path7.join(process.cwd(), "public", imageKey);
|
|
810
|
+
try {
|
|
811
|
+
const originalBuffer = await fs6.readFile(originalLocalPath);
|
|
812
|
+
await r2.send(
|
|
813
|
+
new PutObjectCommand2({
|
|
814
|
+
Bucket: bucketName,
|
|
815
|
+
Key: imageKey.replace(/^\//, ""),
|
|
816
|
+
Body: originalBuffer,
|
|
817
|
+
ContentType: getContentType(imageKey)
|
|
818
|
+
})
|
|
819
|
+
);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
errors.push(`Original file not found: ${imageKey}`);
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
844
824
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
845
825
|
const localPath = path7.join(process.cwd(), "public", thumbPath);
|
|
846
826
|
try {
|
|
847
|
-
const fileBuffer = await
|
|
827
|
+
const fileBuffer = await fs6.readFile(localPath);
|
|
848
828
|
await r2.send(
|
|
849
829
|
new PutObjectCommand2({
|
|
850
830
|
Bucket: bucketName,
|
|
@@ -856,18 +836,22 @@ async function handleSync(request) {
|
|
|
856
836
|
} catch {
|
|
857
837
|
}
|
|
858
838
|
}
|
|
859
|
-
entry.
|
|
839
|
+
entry.c = 1;
|
|
860
840
|
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
861
841
|
const localPath = path7.join(process.cwd(), "public", thumbPath);
|
|
862
842
|
try {
|
|
863
|
-
await
|
|
843
|
+
await fs6.unlink(localPath);
|
|
864
844
|
} catch {
|
|
865
845
|
}
|
|
866
846
|
}
|
|
847
|
+
try {
|
|
848
|
+
await fs6.unlink(originalLocalPath);
|
|
849
|
+
} catch {
|
|
850
|
+
}
|
|
867
851
|
synced.push(imageKey);
|
|
868
852
|
} catch (error) {
|
|
869
853
|
console.error(`Failed to sync ${imageKey}:`, error);
|
|
870
|
-
errors.push(imageKey);
|
|
854
|
+
errors.push(`Failed to sync: ${imageKey}`);
|
|
871
855
|
}
|
|
872
856
|
}
|
|
873
857
|
await saveMeta(meta);
|
|
@@ -894,21 +878,29 @@ async function handleReprocess(request) {
|
|
|
894
878
|
try {
|
|
895
879
|
let buffer;
|
|
896
880
|
const entry = meta[imageKey];
|
|
881
|
+
const isSynced = entry?.c === 1;
|
|
897
882
|
const originalPath = path7.join(process.cwd(), "public", imageKey);
|
|
898
883
|
try {
|
|
899
|
-
buffer = await
|
|
884
|
+
buffer = await fs6.readFile(originalPath);
|
|
900
885
|
} catch {
|
|
901
|
-
if (
|
|
886
|
+
if (isSynced) {
|
|
902
887
|
buffer = await downloadFromCdn(imageKey);
|
|
888
|
+
const dir = path7.dirname(originalPath);
|
|
889
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
890
|
+
await fs6.writeFile(originalPath, buffer);
|
|
903
891
|
} else {
|
|
904
892
|
throw new Error(`File not found: ${imageKey}`);
|
|
905
893
|
}
|
|
906
894
|
}
|
|
907
895
|
const updatedEntry = await processImage(buffer, imageKey);
|
|
908
|
-
if (
|
|
909
|
-
updatedEntry.
|
|
896
|
+
if (isSynced) {
|
|
897
|
+
updatedEntry.c = 1;
|
|
910
898
|
await uploadToCdn(imageKey);
|
|
911
899
|
await deleteLocalThumbnails(imageKey);
|
|
900
|
+
try {
|
|
901
|
+
await fs6.unlink(originalPath);
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
912
904
|
}
|
|
913
905
|
meta[imageKey] = updatedEntry;
|
|
914
906
|
processed.push(imageKey);
|
|
@@ -942,78 +934,66 @@ async function handleProcessAllStream() {
|
|
|
942
934
|
const processed = [];
|
|
943
935
|
const errors = [];
|
|
944
936
|
const orphansRemoved = [];
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
953
|
-
if (relPath === "images" || relPath.startsWith("images/")) continue;
|
|
954
|
-
if (entry.isDirectory()) {
|
|
955
|
-
await scanPublicFolder(fullPath, relPath);
|
|
956
|
-
} else if (isImageFile(entry.name)) {
|
|
957
|
-
allImages.push({ key: relPath, fullPath });
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
} catch {
|
|
937
|
+
const imagesToProcess = [];
|
|
938
|
+
for (const [key, entry] of Object.entries(meta)) {
|
|
939
|
+
if (entry.c) continue;
|
|
940
|
+
const fileName = path7.basename(key);
|
|
941
|
+
if (!isImageFile(fileName)) continue;
|
|
942
|
+
if (!entry.b) {
|
|
943
|
+
imagesToProcess.push({ key, entry });
|
|
961
944
|
}
|
|
962
945
|
}
|
|
963
|
-
const
|
|
964
|
-
await scanPublicFolder(publicDir);
|
|
965
|
-
const total = allImages.length;
|
|
946
|
+
const total = imagesToProcess.length;
|
|
966
947
|
sendEvent({ type: "start", total });
|
|
967
|
-
for (let i = 0; i <
|
|
968
|
-
const { key
|
|
969
|
-
const
|
|
948
|
+
for (let i = 0; i < imagesToProcess.length; i++) {
|
|
949
|
+
const { key } = imagesToProcess[i];
|
|
950
|
+
const fullPath = path7.join(process.cwd(), "public", key);
|
|
970
951
|
sendEvent({
|
|
971
952
|
type: "progress",
|
|
972
953
|
current: i + 1,
|
|
973
954
|
total,
|
|
974
955
|
percent: Math.round((i + 1) / total * 100),
|
|
975
|
-
currentFile: key
|
|
956
|
+
currentFile: key.slice(1)
|
|
957
|
+
// Remove leading /
|
|
976
958
|
});
|
|
977
959
|
try {
|
|
978
|
-
const buffer = await
|
|
960
|
+
const buffer = await fs6.readFile(fullPath);
|
|
979
961
|
const ext = path7.extname(key).toLowerCase();
|
|
980
962
|
const isSvg = ext === ".svg";
|
|
981
963
|
if (isSvg) {
|
|
982
|
-
const imageDir = path7.dirname(key);
|
|
964
|
+
const imageDir = path7.dirname(key.slice(1));
|
|
983
965
|
const imagesPath = path7.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
|
|
984
|
-
await
|
|
966
|
+
await fs6.mkdir(imagesPath, { recursive: true });
|
|
985
967
|
const fileName = path7.basename(key);
|
|
986
968
|
const destPath = path7.join(imagesPath, fileName);
|
|
987
|
-
await
|
|
988
|
-
meta[
|
|
969
|
+
await fs6.writeFile(destPath, buffer);
|
|
970
|
+
meta[key] = {
|
|
989
971
|
w: 0,
|
|
990
972
|
h: 0,
|
|
991
|
-
|
|
973
|
+
b: ""
|
|
992
974
|
};
|
|
993
975
|
} else {
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
if (existingEntry?.s) {
|
|
997
|
-
processedEntry.s = 1;
|
|
998
|
-
}
|
|
999
|
-
meta[imageKey] = processedEntry;
|
|
976
|
+
const processedEntry = await processImage(buffer, key);
|
|
977
|
+
meta[key] = processedEntry;
|
|
1000
978
|
}
|
|
1001
|
-
processed.push(key);
|
|
979
|
+
processed.push(key.slice(1));
|
|
1002
980
|
} catch (error) {
|
|
1003
981
|
console.error(`Failed to process ${key}:`, error);
|
|
1004
|
-
errors.push(key);
|
|
982
|
+
errors.push(key.slice(1));
|
|
1005
983
|
}
|
|
1006
984
|
}
|
|
1007
985
|
sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
|
|
1008
986
|
const trackedPaths = /* @__PURE__ */ new Set();
|
|
1009
987
|
for (const imageKey of Object.keys(meta)) {
|
|
1010
|
-
|
|
1011
|
-
|
|
988
|
+
if (!meta[imageKey].c) {
|
|
989
|
+
for (const thumbPath of getAllThumbnailPaths(imageKey)) {
|
|
990
|
+
trackedPaths.add(thumbPath);
|
|
991
|
+
}
|
|
1012
992
|
}
|
|
1013
993
|
}
|
|
1014
994
|
async function findOrphans(dir, relativePath = "") {
|
|
1015
995
|
try {
|
|
1016
|
-
const entries = await
|
|
996
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1017
997
|
for (const entry of entries) {
|
|
1018
998
|
if (entry.name.startsWith(".")) continue;
|
|
1019
999
|
const fullPath = path7.join(dir, entry.name);
|
|
@@ -1024,7 +1004,7 @@ async function handleProcessAllStream() {
|
|
|
1024
1004
|
const publicPath = `/images/${relPath}`;
|
|
1025
1005
|
if (!trackedPaths.has(publicPath)) {
|
|
1026
1006
|
try {
|
|
1027
|
-
await
|
|
1007
|
+
await fs6.unlink(fullPath);
|
|
1028
1008
|
orphansRemoved.push(publicPath);
|
|
1029
1009
|
} catch (err) {
|
|
1030
1010
|
console.error(`Failed to remove orphan ${publicPath}:`, err);
|
|
@@ -1036,10 +1016,13 @@ async function handleProcessAllStream() {
|
|
|
1036
1016
|
}
|
|
1037
1017
|
}
|
|
1038
1018
|
const imagesDir = path7.join(process.cwd(), "public", "images");
|
|
1039
|
-
|
|
1019
|
+
try {
|
|
1020
|
+
await findOrphans(imagesDir);
|
|
1021
|
+
} catch {
|
|
1022
|
+
}
|
|
1040
1023
|
async function removeEmptyDirs(dir) {
|
|
1041
1024
|
try {
|
|
1042
|
-
const entries = await
|
|
1025
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true });
|
|
1043
1026
|
let isEmpty = true;
|
|
1044
1027
|
for (const entry of entries) {
|
|
1045
1028
|
if (entry.isDirectory()) {
|
|
@@ -1050,14 +1033,17 @@ async function handleProcessAllStream() {
|
|
|
1050
1033
|
}
|
|
1051
1034
|
}
|
|
1052
1035
|
if (isEmpty && dir !== imagesDir) {
|
|
1053
|
-
await
|
|
1036
|
+
await fs6.rmdir(dir);
|
|
1054
1037
|
}
|
|
1055
1038
|
return isEmpty;
|
|
1056
1039
|
} catch {
|
|
1057
1040
|
return true;
|
|
1058
1041
|
}
|
|
1059
1042
|
}
|
|
1060
|
-
|
|
1043
|
+
try {
|
|
1044
|
+
await removeEmptyDirs(imagesDir);
|
|
1045
|
+
} catch {
|
|
1046
|
+
}
|
|
1061
1047
|
await saveMeta(meta);
|
|
1062
1048
|
sendEvent({
|
|
1063
1049
|
type: "complete",
|
|
@@ -1082,6 +1068,135 @@ async function handleProcessAllStream() {
|
|
|
1082
1068
|
});
|
|
1083
1069
|
}
|
|
1084
1070
|
|
|
1071
|
+
// src/handlers/scan.ts
|
|
1072
|
+
import { promises as fs7 } from "fs";
|
|
1073
|
+
import path8 from "path";
|
|
1074
|
+
import sharp3 from "sharp";
|
|
1075
|
+
async function handleScanStream() {
|
|
1076
|
+
const encoder = new TextEncoder();
|
|
1077
|
+
const stream = new ReadableStream({
|
|
1078
|
+
async start(controller) {
|
|
1079
|
+
const sendEvent = (data) => {
|
|
1080
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
1081
|
+
|
|
1082
|
+
`));
|
|
1083
|
+
};
|
|
1084
|
+
try {
|
|
1085
|
+
const meta = await loadMeta();
|
|
1086
|
+
const existingKeys = new Set(Object.keys(meta));
|
|
1087
|
+
const added = [];
|
|
1088
|
+
const renamed = [];
|
|
1089
|
+
const errors = [];
|
|
1090
|
+
const allFiles = [];
|
|
1091
|
+
async function scanDir(dir, relativePath = "") {
|
|
1092
|
+
try {
|
|
1093
|
+
const entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
1094
|
+
for (const entry of entries) {
|
|
1095
|
+
if (entry.name.startsWith(".")) continue;
|
|
1096
|
+
const fullPath = path8.join(dir, entry.name);
|
|
1097
|
+
const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
1098
|
+
if (relPath === "images" || relPath.startsWith("images/")) continue;
|
|
1099
|
+
if (entry.isDirectory()) {
|
|
1100
|
+
await scanDir(fullPath, relPath);
|
|
1101
|
+
} else if (isMediaFile(entry.name)) {
|
|
1102
|
+
allFiles.push({ relativePath: relPath, fullPath });
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
} catch {
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
const publicDir = path8.join(process.cwd(), "public");
|
|
1109
|
+
await scanDir(publicDir);
|
|
1110
|
+
const total = allFiles.length;
|
|
1111
|
+
sendEvent({ type: "start", total });
|
|
1112
|
+
for (let i = 0; i < allFiles.length; i++) {
|
|
1113
|
+
let { relativePath, fullPath } = allFiles[i];
|
|
1114
|
+
let imageKey = "/" + relativePath;
|
|
1115
|
+
sendEvent({
|
|
1116
|
+
type: "progress",
|
|
1117
|
+
current: i + 1,
|
|
1118
|
+
total,
|
|
1119
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1120
|
+
currentFile: relativePath
|
|
1121
|
+
});
|
|
1122
|
+
if (existingKeys.has(imageKey)) {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
if (meta[imageKey]) {
|
|
1126
|
+
const ext = path8.extname(relativePath);
|
|
1127
|
+
const baseName = relativePath.slice(0, -ext.length);
|
|
1128
|
+
let counter = 1;
|
|
1129
|
+
let newKey = `/${baseName}-${counter}${ext}`;
|
|
1130
|
+
while (meta[newKey]) {
|
|
1131
|
+
counter++;
|
|
1132
|
+
newKey = `/${baseName}-${counter}${ext}`;
|
|
1133
|
+
}
|
|
1134
|
+
const newRelativePath = `${baseName}-${counter}${ext}`;
|
|
1135
|
+
const newFullPath = path8.join(process.cwd(), "public", newRelativePath);
|
|
1136
|
+
try {
|
|
1137
|
+
await fs7.rename(fullPath, newFullPath);
|
|
1138
|
+
renamed.push({ from: relativePath, to: newRelativePath });
|
|
1139
|
+
relativePath = newRelativePath;
|
|
1140
|
+
fullPath = newFullPath;
|
|
1141
|
+
imageKey = newKey;
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.error(`Failed to rename ${relativePath}:`, err);
|
|
1144
|
+
errors.push(`Failed to rename ${relativePath}`);
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
try {
|
|
1149
|
+
const isImage = isImageFile(relativePath);
|
|
1150
|
+
if (isImage) {
|
|
1151
|
+
const ext = path8.extname(relativePath).toLowerCase();
|
|
1152
|
+
if (ext === ".svg") {
|
|
1153
|
+
meta[imageKey] = { w: 0, h: 0 };
|
|
1154
|
+
} else {
|
|
1155
|
+
try {
|
|
1156
|
+
const metadata = await sharp3(fullPath).metadata();
|
|
1157
|
+
meta[imageKey] = {
|
|
1158
|
+
w: metadata.width || 0,
|
|
1159
|
+
h: metadata.height || 0
|
|
1160
|
+
};
|
|
1161
|
+
} catch {
|
|
1162
|
+
meta[imageKey] = { w: 0, h: 0 };
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
} else {
|
|
1166
|
+
meta[imageKey] = {};
|
|
1167
|
+
}
|
|
1168
|
+
existingKeys.add(imageKey);
|
|
1169
|
+
added.push(imageKey);
|
|
1170
|
+
} catch (error) {
|
|
1171
|
+
console.error(`Failed to process ${relativePath}:`, error);
|
|
1172
|
+
errors.push(relativePath);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
await saveMeta(meta);
|
|
1176
|
+
sendEvent({
|
|
1177
|
+
type: "complete",
|
|
1178
|
+
added: added.length,
|
|
1179
|
+
renamed: renamed.length,
|
|
1180
|
+
errors: errors.length,
|
|
1181
|
+
renamedFiles: renamed
|
|
1182
|
+
});
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
console.error("Scan failed:", error);
|
|
1185
|
+
sendEvent({ type: "error", message: "Scan failed" });
|
|
1186
|
+
} finally {
|
|
1187
|
+
controller.close();
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
return new Response(stream, {
|
|
1192
|
+
headers: {
|
|
1193
|
+
"Content-Type": "text/event-stream",
|
|
1194
|
+
"Cache-Control": "no-cache",
|
|
1195
|
+
"Connection": "keep-alive"
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1085
1200
|
// src/handlers/index.ts
|
|
1086
1201
|
async function GET(request) {
|
|
1087
1202
|
if (process.env.NODE_ENV !== "development") {
|
|
@@ -1136,6 +1251,9 @@ async function POST(request) {
|
|
|
1136
1251
|
if (route === "move") {
|
|
1137
1252
|
return handleMove(request);
|
|
1138
1253
|
}
|
|
1254
|
+
if (route === "scan") {
|
|
1255
|
+
return handleScanStream();
|
|
1256
|
+
}
|
|
1139
1257
|
return NextResponse4.json({ error: "Not found" }, { status: 404 });
|
|
1140
1258
|
}
|
|
1141
1259
|
async function DELETE(request) {
|