@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.
@@ -1,96 +1,228 @@
1
1
  import {
2
2
  getAllThumbnailPaths
3
- } from "./chunk-3RI33B7A.mjs";
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 GET(request) {
18
- if (process.env.NODE_ENV !== "development") {
19
- return NextResponse.json({ error: "Not available in production" }, { status: 403 });
20
- }
21
- const pathname = request.nextUrl.pathname;
22
- const route = pathname.replace(/^\/api\/studio\/?/, "");
23
- if (route === "list-folders") {
24
- return handleListFolders();
25
- }
26
- if (route === "list" || route.startsWith("list")) {
27
- return handleList(request);
28
- }
29
- if (route === "count-images") {
30
- return handleCountImages();
31
- }
32
- if (route === "folder-images") {
33
- return handleFolderImages(request);
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
- if (route === "search") {
36
- return handleSearch(request);
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
- return NextResponse.json({ error: "Not found" }, { status: 404 });
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
- async function POST(request) {
41
- if (process.env.NODE_ENV !== "development") {
42
- return NextResponse.json({ error: "Not available in production" }, { status: 403 });
43
- }
44
- const pathname = request.nextUrl.pathname;
45
- const route = pathname.replace(/^\/api\/studio\/?/, "");
46
- if (route === "upload") {
47
- return handleUpload(request);
48
- }
49
- if (route === "delete") {
50
- return handleDelete(request);
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
- if (route === "rename") {
65
- return handleRename(request);
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
- if (route === "move") {
68
- return handleMove(request);
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 DELETE(request) {
73
- if (process.env.NODE_ENV !== "development") {
74
- return NextResponse.json({ error: "Not available in production" }, { status: 403 });
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 = path.join(process.cwd(), safePath);
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 fs.readdir(absolutePath, { withFileTypes: true });
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 = path.join(safePath, entry.name);
223
+ const itemPath = path5.join(safePath, entry.name);
92
224
  if (entry.isDirectory()) {
93
- const folderStats = await getFolderStats(path.join(absolutePath, entry.name));
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 = path.join(absolutePath, entry.name);
103
- const stats = await fs.stat(filePath);
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 = path.extname(entry.name).toLowerCase();
115
- const baseName = path.basename(entry.name, ext);
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 = path.join(process.cwd(), "public", thumbnailDir, thumbnailName);
250
+ const thumbnailPath = path5.join(process.cwd(), "public", thumbnailDir, thumbnailName);
119
251
  try {
120
- await fs.access(thumbnailPath);
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 sharp(filePath).metadata();
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 = path.join(process.cwd(), "public");
295
+ const publicDir = path5.join(process.cwd(), "public");
164
296
  async function searchDir(dir, relativePath) {
165
297
  try {
166
- const entries = await fs.readdir(dir, { withFileTypes: true });
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 = path.join(dir, entry.name);
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 fs.stat(fullPath);
308
+ const stats = await fs5.stat(fullPath);
177
309
  let thumbnail;
178
310
  let hasThumbnail = false;
179
311
  let dimensions;
180
- const ext = path.extname(entry.name).toLowerCase();
181
- const baseName = path.basename(entry.name, ext);
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 = path.join(process.cwd(), "public", thumbnailDir, thumbnailName);
316
+ const thumbnailPath = path5.join(process.cwd(), "public", thumbnailDir, thumbnailName);
185
317
  try {
186
- await fs.access(thumbnailPath);
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 sharp(fullPath).metadata();
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 getFolderStats(folderPath) {
225
- let fileCount = 0;
226
- let totalSize = 0;
227
- async function scanFolder(dir) {
228
- try {
229
- const entries = await fs.readdir(dir, { withFileTypes: true });
230
- for (const entry of entries) {
231
- if (entry.name.startsWith(".")) continue;
232
- const fullPath = path.join(dir, entry.name);
233
- if (entry.isDirectory()) {
234
- await scanFolder(fullPath);
235
- } else if (isMediaFile(entry.name)) {
236
- fileCount++;
237
- const stats = await fs.stat(fullPath);
238
- totalSize += stats.size;
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 handleUpload(request) {
385
+ async function handleCountImages() {
248
386
  try {
249
- const formData = await request.formData();
250
- const file = formData.get("file");
251
- const targetPath = formData.get("path") || "public";
252
- if (!file) {
253
- return NextResponse.json({ error: "No file provided" }, { status: 400 });
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 bytes = await file.arrayBuffer();
256
- const buffer = Buffer.from(bytes);
257
- const fileName = file.name;
258
- const baseName = path.basename(fileName, path.extname(fileName));
259
- const ext = path.extname(fileName).toLowerCase();
260
- const isImage = isImageFile(fileName);
261
- const isSvg = ext === ".svg";
262
- const isProcessableImage = isImage && !isSvg;
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 NextResponse.json(
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 = path.join(process.cwd(), "public", relativeDir);
277
- await fs.mkdir(uploadDir, { recursive: true });
278
- await fs.writeFile(path.join(uploadDir, fileName), buffer);
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 NextResponse.json({
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 NextResponse.json(
504
+ return NextResponse2.json(
289
505
  { error: `File '${imageKey}' already exists in meta` },
290
506
  { status: 409 }
291
507
  );
292
508
  }
293
- const imagesPath = path.join(process.cwd(), "public", "images", relativeDir);
294
- await fs.mkdir(imagesPath, { recursive: true });
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 = path.join(imagesPath, fileName);
301
- await fs.writeFile(fullPath, buffer);
516
+ const fullPath = path6.join(imagesPath, fileName);
517
+ await fs6.writeFile(fullPath, buffer);
302
518
  } else if (isProcessableImage) {
303
- const sharpInstance = sharp(buffer);
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 = path.join(imagesPath, fullFileName);
525
+ const fullPath = path6.join(imagesPath, fullFileName);
310
526
  if (ext === ".png") {
311
- await sharp(buffer).png({ quality: 85 }).toFile(fullPath);
527
+ await sharp3(buffer).png({ quality: 85 }).toFile(fullPath);
312
528
  } else {
313
- await sharp(buffer).jpeg({ quality: 85 }).toFile(fullPath);
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 = path.join(imagesPath, sizeFileName);
539
+ const sizePath = path6.join(imagesPath, sizeFileName);
324
540
  if (ext === ".png") {
325
- await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
541
+ await sharp3(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
326
542
  } else {
327
- await sharp(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
543
+ await sharp3(buffer).resize(maxWidth, newHeight).jpeg({ quality: 80 }).toFile(sizePath);
328
544
  }
329
545
  }
330
- const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
331
- blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
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 NextResponse.json({ success: true, imageKey: originalPath, entry });
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 NextResponse.json({ error: `Failed to upload file: ${message}` }, { status: 500 });
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 NextResponse.json({ error: "No paths provided" }, { status: 400 });
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 = path.join(process.cwd(), itemPath);
363
- const stats = await fs.stat(absolutePath);
578
+ const absolutePath = path6.join(process.cwd(), itemPath);
579
+ const stats = await fs6.stat(absolutePath);
364
580
  if (stats.isDirectory()) {
365
- await fs.rm(absolutePath, { recursive: true });
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 fs.unlink(absolutePath);
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 = path.join(process.cwd(), "public", thumbPath);
595
+ const absoluteThumbPath = path6.join(process.cwd(), "public", thumbPath);
380
596
  try {
381
- await fs.unlink(absoluteThumbPath);
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 NextResponse.json({
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 NextResponse.json({ error: "Failed to delete files" }, { status: 500 });
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 NextResponse.json(
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 NextResponse.json({ error: "No image keys provided" }, { status: 400 });
823
+ return NextResponse3.json({ error: "No image keys provided" }, { status: 400 });
422
824
  }
423
825
  const meta = await loadMeta();
424
- const r2 = new S3Client({
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 = path.join(process.cwd(), "public", thumbPath);
845
+ const localPath = path7.join(process.cwd(), "public", thumbPath);
444
846
  try {
445
- const fileBuffer = await fs.readFile(localPath);
847
+ const fileBuffer = await fs7.readFile(localPath);
446
848
  await r2.send(
447
- new PutObjectCommand({
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 = path.join(process.cwd(), "public", thumbPath);
861
+ const localPath = path7.join(process.cwd(), "public", thumbPath);
460
862
  try {
461
- await fs.unlink(localPath);
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 NextResponse.json({
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 NextResponse.json({ error: "Failed to sync to CDN" }, { status: 500 });
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 NextResponse.json({ error: "No image keys provided" }, { status: 400 });
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 = path.join(process.cwd(), "public", imageKey);
897
+ const originalPath = path7.join(process.cwd(), "public", imageKey);
496
898
  try {
497
- buffer = await fs.readFile(originalPath);
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 NextResponse.json({
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 NextResponse.json({ error: "Failed to reprocess images" }, { status: 500 });
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 fs.readdir(dir, { withFileTypes: true });
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 = path.join(dir, entry.name);
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 = path.join(process.cwd(), "public");
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 fs.readFile(fullPath);
648
- const ext = path.extname(key).toLowerCase();
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 = path.dirname(key);
652
- const imagesPath = path.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
653
- await fs.mkdir(imagesPath, { recursive: true });
654
- const fileName = path.basename(key);
655
- const destPath = path.join(imagesPath, fileName);
656
- await fs.writeFile(destPath, buffer);
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 fs.readdir(dir, { withFileTypes: true });
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 = path.join(dir, entry.name);
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 fs.unlink(fullPath);
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 = path.join(process.cwd(), "public", "images");
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 fs.readdir(dir, { withFileTypes: true });
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(path.join(dir, entry.name));
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 fs.rmdir(dir);
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
- async function loadMeta() {
754
- const metaPath = path.join(process.cwd(), "_data", "_meta.json");
755
- try {
756
- const content = await fs.readFile(metaPath, "utf-8");
757
- return JSON.parse(content);
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
- async function saveMeta(meta) {
763
- const dataDir = path.join(process.cwd(), "_data");
764
- await fs.mkdir(dataDir, { recursive: true });
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
- async function processImage(buffer, imageKey) {
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
- for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {
819
- const { width: maxWidth, suffix } = sizeConfig;
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
- const { data, info } = await sharp(buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
835
- const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, 4, 4);
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
- const r2 = new S3Client({
851
- region: "auto",
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 Buffer.concat(chunks);
1107
+ return NextResponse4.json({ error: "Not found" }, { status: 404 });
867
1108
  }
868
- async function uploadToCdn(imageKey) {
869
- const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
870
- const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID;
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 r2 = new S3Client({
877
- region: "auto",
878
- endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
879
- credentials: { accessKeyId, secretAccessKey }
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
- async function deleteLocalThumbnails(imageKey) {
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
- async function handleCreateFolder(request) {
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
- async function handleRename(request) {
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
- async function handleMove(request) {
996
- try {
997
- const { paths, destination } = await request.json();
998
- if (!paths || !Array.isArray(paths) || paths.length === 0) {
999
- return NextResponse.json({ error: "Paths are required" }, { status: 400 });
1000
- }
1001
- if (!destination || typeof destination !== "string") {
1002
- return NextResponse.json({ error: "Destination is required" }, { status: 400 });
1003
- }
1004
- const safeDestination = destination.replace(/\.\./g, "");
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 handleListFolders() {
1087
- try {
1088
- const publicDir = path.join(process.cwd(), "public");
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=handlers.mjs.map
1152
+ //# sourceMappingURL=index.mjs.map