@gallop.software/studio 0.1.93 → 0.1.95
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-2JGXVWM4.mjs → StudioUI-GKE5ZWKW.mjs} +1124 -574
- package/dist/StudioUI-GKE5ZWKW.mjs.map +1 -0
- package/dist/{StudioUI-77EWSAVJ.js → StudioUI-QBIGDYYL.js} +779 -229
- package/dist/StudioUI-QBIGDYYL.js.map +1 -0
- package/dist/{chunk-DTVEVFQ2.mjs → chunk-IHXG2EE4.mjs} +1 -1
- package/dist/chunk-IHXG2EE4.mjs.map +1 -0
- package/dist/{chunk-L36EH3PM.js → chunk-MCJNUXQ6.js} +1 -1
- package/dist/chunk-MCJNUXQ6.js.map +1 -0
- package/dist/handlers/index.d.mts +1 -15
- package/dist/handlers/index.d.ts +1 -15
- package/dist/handlers/index.js +226 -43
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/index.mjs +213 -30
- package/dist/handlers/index.mjs.map +1 -1
- package/dist/index.d.mts +62 -2
- package/dist/index.d.ts +62 -2
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/package.json +1 -1
- package/dist/StudioUI-2JGXVWM4.mjs.map +0 -1
- package/dist/StudioUI-77EWSAVJ.js.map +0 -1
- package/dist/chunk-DTVEVFQ2.mjs.map +0 -1
- package/dist/chunk-L36EH3PM.js.map +0 -1
- package/dist/types-C4hCz2w8.d.mts +0 -62
- package/dist/types-C4hCz2w8.d.ts +0 -62
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Meta entry - works for images and non-images\n * Images have w, h, b (after processing)\n * c is the index into _cdns array (omit if not on CDN)\n */\nexport interface MetaEntry {\n w?: number // original width (images only)\n h?: number // original height (images only)\n b?: string // blurhash (images only, after processing)\n p?: 1 // processed (has thumbnails and blurhash)\n c?: number // CDN index - index into _cdns array (omit if not on CDN)\n}\n\n/**\n * Full meta schema including special keys\n * _cdns: Array of CDN base URLs\n * Other keys: file paths from public folder\n */\nexport interface FullMeta {\n _cdns?: string[] // Array of CDN base URLs\n [key: string]: MetaEntry | string[] | undefined\n}\n\n/**\n * Meta schema - keyed by path from public folder (legacy type)\n * Example: { \"/portfolio/photo.jpg\": { w: 2400, h: 1600, b: \"...\" } }\n */\nexport type LeanMeta = Record<string, MetaEntry>\n\n// Legacy alias for compatibility\nexport type LeanImageEntry = MetaEntry\n\n/**\n * File/folder item for browser\n */\nexport interface FileItem {\n name: string\n path: string\n type: 'file' | 'folder'\n size?: number\n dimensions?: { width: number; height: number }\n isProcessed?: boolean\n cdnPushed?: boolean\n // Folder-specific properties\n fileCount?: number\n totalSize?: number\n // For showing thumbnails - path to -sm version if exists\n thumbnail?: string\n // Whether a processed thumbnail exists\n hasThumbnail?: boolean\n}\n\n/**\n * Studio configuration\n */\nexport interface StudioConfig {\n r2AccountId?: string\n r2AccessKeyId?: string\n r2SecretAccessKey?: string\n r2BucketName?: string\n r2PublicUrl?: string\n thumbnailSizes?: {\n small: number\n medium: number\n large: number\n }\n}\n\n/**\n * Get thumbnail path from original image path\n */\nexport function getThumbnailPath(originalPath: string, size: 'sm' | 'md' | 'lg' | 'full'): string {\n if (size === 'full') {\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}${outputExt}`\n }\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}-${size}${outputExt}`\n}\n\n/**\n * Get all thumbnail paths for an image\n */\nexport function getAllThumbnailPaths(originalPath: string): string[] {\n return [\n getThumbnailPath(originalPath, 'full'),\n getThumbnailPath(originalPath, 'lg'),\n getThumbnailPath(originalPath, 'md'),\n getThumbnailPath(originalPath, 'sm'),\n ]\n}\n"],"mappings":";AAuEO,SAAS,iBAAiB,cAAsB,MAA2C;AAChG,MAAI,SAAS,QAAQ;AACnB,UAAMA,OAAM,aAAa,MAAM,QAAQ,IAAI,CAAC,KAAK;AACjD,UAAMC,QAAO,aAAa,QAAQ,UAAU,EAAE;AAC9C,UAAMC,aAAYF,KAAI,YAAY,MAAM,SAAS,SAAS;AAC1D,WAAO,UAAUC,KAAI,GAAGC,UAAS;AAAA,EACnC;AACA,QAAM,MAAM,aAAa,MAAM,QAAQ,IAAI,CAAC,KAAK;AACjD,QAAM,OAAO,aAAa,QAAQ,UAAU,EAAE;AAC9C,QAAM,YAAY,IAAI,YAAY,MAAM,SAAS,SAAS;AAC1D,SAAO,UAAU,IAAI,IAAI,IAAI,GAAG,SAAS;AAC3C;AAKO,SAAS,qBAAqB,cAAgC;AACnE,SAAO;AAAA,IACL,iBAAiB,cAAc,MAAM;AAAA,IACrC,iBAAiB,cAAc,IAAI;AAAA,IACnC,iBAAiB,cAAc,IAAI;AAAA,IACnC,iBAAiB,cAAc,IAAI;AAAA,EACrC;AACF;","names":["ext","base","outputExt"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/chrisb/Sites/studio/dist/chunk-MCJNUXQ6.js","../src/types.ts"],"names":["ext","base","outputExt"],"mappings":"AAAA;ACuEO,SAAS,gBAAA,CAAiB,YAAA,EAAsB,IAAA,EAA2C;AAChG,EAAA,GAAA,CAAI,KAAA,IAAS,MAAA,EAAQ;AACnB,IAAA,MAAMA,KAAAA,kBAAM,YAAA,mBAAa,KAAA,mBAAM,QAAQ,CAAA,4BAAA,CAAI,CAAC,IAAA,GAAK,MAAA;AACjD,IAAA,MAAMC,MAAAA,EAAO,YAAA,CAAa,OAAA,CAAQ,QAAA,EAAU,EAAE,CAAA;AAC9C,IAAA,MAAMC,WAAAA,EAAYF,IAAAA,CAAI,WAAA,CAAY,EAAA,IAAM,OAAA,EAAS,OAAA,EAAS,MAAA;AAC1D,IAAA,OAAO,CAAA,OAAA,EAAUC,KAAI,CAAA,EAAA;AACvB,EAAA;AACyB,EAAA;AACZ,EAAA;AACS,EAAA;AACG,EAAA;AAC3B;AAKgB;AACP,EAAA;AACY,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACnB,EAAA;AACF;ADzE2B;AACA;AACA;AACA;AACA","file":"/Users/chrisb/Sites/studio/dist/chunk-MCJNUXQ6.js","sourcesContent":[null,"/**\n * Meta entry - works for images and non-images\n * Images have w, h, b (after processing)\n * c is the index into _cdns array (omit if not on CDN)\n */\nexport interface MetaEntry {\n w?: number // original width (images only)\n h?: number // original height (images only)\n b?: string // blurhash (images only, after processing)\n p?: 1 // processed (has thumbnails and blurhash)\n c?: number // CDN index - index into _cdns array (omit if not on CDN)\n}\n\n/**\n * Full meta schema including special keys\n * _cdns: Array of CDN base URLs\n * Other keys: file paths from public folder\n */\nexport interface FullMeta {\n _cdns?: string[] // Array of CDN base URLs\n [key: string]: MetaEntry | string[] | undefined\n}\n\n/**\n * Meta schema - keyed by path from public folder (legacy type)\n * Example: { \"/portfolio/photo.jpg\": { w: 2400, h: 1600, b: \"...\" } }\n */\nexport type LeanMeta = Record<string, MetaEntry>\n\n// Legacy alias for compatibility\nexport type LeanImageEntry = MetaEntry\n\n/**\n * File/folder item for browser\n */\nexport interface FileItem {\n name: string\n path: string\n type: 'file' | 'folder'\n size?: number\n dimensions?: { width: number; height: number }\n isProcessed?: boolean\n cdnPushed?: boolean\n // Folder-specific properties\n fileCount?: number\n totalSize?: number\n // For showing thumbnails - path to -sm version if exists\n thumbnail?: string\n // Whether a processed thumbnail exists\n hasThumbnail?: boolean\n}\n\n/**\n * Studio configuration\n */\nexport interface StudioConfig {\n r2AccountId?: string\n r2AccessKeyId?: string\n r2SecretAccessKey?: string\n r2BucketName?: string\n r2PublicUrl?: string\n thumbnailSizes?: {\n small: number\n medium: number\n large: number\n }\n}\n\n/**\n * Get thumbnail path from original image path\n */\nexport function getThumbnailPath(originalPath: string, size: 'sm' | 'md' | 'lg' | 'full'): string {\n if (size === 'full') {\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}${outputExt}`\n }\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || '.jpg'\n const base = originalPath.replace(/\\.\\w+$/, '')\n const outputExt = ext.toLowerCase() === '.png' ? '.png' : '.jpg'\n return `/images${base}-${size}${outputExt}`\n}\n\n/**\n * Get all thumbnail paths for an image\n */\nexport function getAllThumbnailPaths(originalPath: string): string[] {\n return [\n getThumbnailPath(originalPath, 'full'),\n getThumbnailPath(originalPath, 'lg'),\n getThumbnailPath(originalPath, 'md'),\n getThumbnailPath(originalPath, 'sm'),\n ]\n}\n"]}
|
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
import { F as FileItem } from '../types-C4hCz2w8.mjs';
|
|
2
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Unified GET handler for all Studio API routes
|
|
6
5
|
*/
|
|
7
|
-
declare function GET(request: NextRequest): Promise<
|
|
8
|
-
error: string;
|
|
9
|
-
}> | NextResponse<{
|
|
10
|
-
folders: {
|
|
11
|
-
path: string;
|
|
12
|
-
name: string;
|
|
13
|
-
depth: number;
|
|
14
|
-
}[];
|
|
15
|
-
}> | NextResponse<{
|
|
16
|
-
items: FileItem[];
|
|
17
|
-
}> | NextResponse<{
|
|
18
|
-
count: number;
|
|
19
|
-
images: string[];
|
|
20
|
-
}>>;
|
|
6
|
+
declare function GET(request: NextRequest): Promise<Response>;
|
|
21
7
|
/**
|
|
22
8
|
* Unified POST handler for all Studio API routes
|
|
23
9
|
*/
|
package/dist/handlers/index.d.ts
CHANGED
|
@@ -1,23 +1,9 @@
|
|
|
1
|
-
import { F as FileItem } from '../types-C4hCz2w8.js';
|
|
2
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Unified GET handler for all Studio API routes
|
|
6
5
|
*/
|
|
7
|
-
declare function GET(request: NextRequest): Promise<
|
|
8
|
-
error: string;
|
|
9
|
-
}> | NextResponse<{
|
|
10
|
-
folders: {
|
|
11
|
-
path: string;
|
|
12
|
-
name: string;
|
|
13
|
-
depth: number;
|
|
14
|
-
}[];
|
|
15
|
-
}> | NextResponse<{
|
|
16
|
-
items: FileItem[];
|
|
17
|
-
}> | NextResponse<{
|
|
18
|
-
count: number;
|
|
19
|
-
images: string[];
|
|
20
|
-
}>>;
|
|
6
|
+
declare function GET(request: NextRequest): Promise<Response>;
|
|
21
7
|
/**
|
|
22
8
|
* Unified POST handler for all Studio API routes
|
|
23
9
|
*/
|
package/dist/handlers/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var _chunkMCJNUXQ6js = require('../chunk-MCJNUXQ6.js');
|
|
5
5
|
|
|
6
6
|
// src/handlers/index.ts
|
|
7
7
|
var _server = require('next/server');
|
|
@@ -29,6 +29,35 @@ async function saveMeta(meta) {
|
|
|
29
29
|
const metaPath = _path2.default.join(dataDir, "_meta.json");
|
|
30
30
|
await _fs.promises.writeFile(metaPath, JSON.stringify(meta, null, 2));
|
|
31
31
|
}
|
|
32
|
+
function getCdnUrls(meta) {
|
|
33
|
+
return meta._cdns || [];
|
|
34
|
+
}
|
|
35
|
+
function getOrAddCdnIndex(meta, cdnUrl) {
|
|
36
|
+
if (!meta._cdns) {
|
|
37
|
+
meta._cdns = [];
|
|
38
|
+
}
|
|
39
|
+
const normalizedUrl = cdnUrl.replace(/\/$/, "");
|
|
40
|
+
const existingIndex = meta._cdns.indexOf(normalizedUrl);
|
|
41
|
+
if (existingIndex >= 0) {
|
|
42
|
+
return existingIndex;
|
|
43
|
+
}
|
|
44
|
+
meta._cdns.push(normalizedUrl);
|
|
45
|
+
return meta._cdns.length - 1;
|
|
46
|
+
}
|
|
47
|
+
function getMetaEntry(meta, key) {
|
|
48
|
+
if (key.startsWith("_")) return void 0;
|
|
49
|
+
const value = meta[key];
|
|
50
|
+
if (Array.isArray(value)) return void 0;
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
function setMetaEntry(meta, key, entry) {
|
|
54
|
+
meta[key] = entry;
|
|
55
|
+
}
|
|
56
|
+
function getFileEntries(meta) {
|
|
57
|
+
return Object.entries(meta).filter(
|
|
58
|
+
([key, value]) => !key.startsWith("_") && !Array.isArray(value)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
32
61
|
|
|
33
62
|
// src/handlers/utils/files.ts
|
|
34
63
|
|
|
@@ -156,7 +185,7 @@ async function uploadToCdn(imageKey) {
|
|
|
156
185
|
const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
|
|
157
186
|
if (!bucketName) throw new Error("R2 bucket not configured");
|
|
158
187
|
const r2 = getR2Client();
|
|
159
|
-
for (const thumbPath of
|
|
188
|
+
for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
|
|
160
189
|
const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
|
|
161
190
|
try {
|
|
162
191
|
const fileBuffer = await _fs.promises.readFile(localPath);
|
|
@@ -173,7 +202,7 @@ async function uploadToCdn(imageKey) {
|
|
|
173
202
|
}
|
|
174
203
|
}
|
|
175
204
|
async function deleteLocalThumbnails(imageKey) {
|
|
176
|
-
for (const thumbPath of
|
|
205
|
+
for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
|
|
177
206
|
const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
|
|
178
207
|
try {
|
|
179
208
|
await _fs.promises.unlink(localPath);
|
|
@@ -188,16 +217,17 @@ async function handleList(request) {
|
|
|
188
217
|
const requestedPath = searchParams.get("path") || "public";
|
|
189
218
|
try {
|
|
190
219
|
const meta = await loadMeta();
|
|
191
|
-
const
|
|
192
|
-
|
|
220
|
+
const fileEntries = getFileEntries(meta);
|
|
221
|
+
const cdnUrls = getCdnUrls(meta);
|
|
222
|
+
if (fileEntries.length === 0) {
|
|
193
223
|
return _server.NextResponse.json({ items: [], isEmpty: true });
|
|
194
224
|
}
|
|
195
225
|
const relativePath = requestedPath.replace(/^public\/?/, "");
|
|
196
226
|
const pathPrefix = relativePath ? `/${relativePath}/` : "/";
|
|
197
227
|
const items = [];
|
|
198
228
|
const seenFolders = /* @__PURE__ */ new Set();
|
|
199
|
-
|
|
200
|
-
|
|
229
|
+
const metaKeys = fileEntries.map(([key]) => key);
|
|
230
|
+
for (const [key, entry] of fileEntries) {
|
|
201
231
|
if (!key.startsWith(pathPrefix) && pathPrefix !== "/") continue;
|
|
202
232
|
if (pathPrefix === "/" && !key.startsWith("/")) continue;
|
|
203
233
|
const remaining = pathPrefix === "/" ? key.slice(1) : key.slice(pathPrefix.length);
|
|
@@ -222,14 +252,14 @@ async function handleList(request) {
|
|
|
222
252
|
} else {
|
|
223
253
|
const fileName = remaining;
|
|
224
254
|
const isImage = isImageFile(fileName);
|
|
225
|
-
const isPushedToCloud = entry.c
|
|
255
|
+
const isPushedToCloud = entry.c !== void 0;
|
|
226
256
|
let thumbnail;
|
|
227
257
|
let hasThumbnail = false;
|
|
228
258
|
let fileSize;
|
|
229
|
-
if (isImage &&
|
|
230
|
-
const thumbPath =
|
|
231
|
-
if (isPushedToCloud) {
|
|
232
|
-
const cdnUrl =
|
|
259
|
+
if (isImage && entry.p === 1) {
|
|
260
|
+
const thumbPath = _chunkMCJNUXQ6js.getThumbnailPath.call(void 0, key, "sm");
|
|
261
|
+
if (isPushedToCloud && entry.c !== void 0) {
|
|
262
|
+
const cdnUrl = cdnUrls[entry.c];
|
|
233
263
|
if (cdnUrl) {
|
|
234
264
|
thumbnail = `${cdnUrl}${thumbPath}`;
|
|
235
265
|
hasThumbnail = true;
|
|
@@ -246,7 +276,12 @@ async function handleList(request) {
|
|
|
246
276
|
}
|
|
247
277
|
}
|
|
248
278
|
} else if (isImage) {
|
|
249
|
-
|
|
279
|
+
if (isPushedToCloud && entry.c !== void 0) {
|
|
280
|
+
const cdnUrl = cdnUrls[entry.c];
|
|
281
|
+
thumbnail = cdnUrl ? `${cdnUrl}${key}` : key;
|
|
282
|
+
} else {
|
|
283
|
+
thumbnail = key;
|
|
284
|
+
}
|
|
250
285
|
hasThumbnail = false;
|
|
251
286
|
}
|
|
252
287
|
if (!isPushedToCloud) {
|
|
@@ -284,19 +319,21 @@ async function handleSearch(request) {
|
|
|
284
319
|
}
|
|
285
320
|
try {
|
|
286
321
|
const meta = await loadMeta();
|
|
322
|
+
const fileEntries = getFileEntries(meta);
|
|
323
|
+
const cdnUrls = getCdnUrls(meta);
|
|
287
324
|
const items = [];
|
|
288
|
-
for (const [key, entry] of
|
|
325
|
+
for (const [key, entry] of fileEntries) {
|
|
289
326
|
if (!key.toLowerCase().includes(query)) continue;
|
|
290
327
|
const fileName = _path2.default.basename(key);
|
|
291
328
|
const relativePath = key.slice(1);
|
|
292
329
|
const isImage = isImageFile(fileName);
|
|
293
|
-
const isPushedToCloud = entry.c
|
|
330
|
+
const isPushedToCloud = entry.c !== void 0;
|
|
294
331
|
let thumbnail;
|
|
295
332
|
let hasThumbnail = false;
|
|
296
|
-
if (isImage &&
|
|
297
|
-
const thumbPath =
|
|
298
|
-
if (isPushedToCloud) {
|
|
299
|
-
const cdnUrl =
|
|
333
|
+
if (isImage && entry.p === 1) {
|
|
334
|
+
const thumbPath = _chunkMCJNUXQ6js.getThumbnailPath.call(void 0, key, "sm");
|
|
335
|
+
if (isPushedToCloud && entry.c !== void 0) {
|
|
336
|
+
const cdnUrl = cdnUrls[entry.c];
|
|
300
337
|
if (cdnUrl) {
|
|
301
338
|
thumbnail = `${cdnUrl}${thumbPath}`;
|
|
302
339
|
hasThumbnail = true;
|
|
@@ -313,7 +350,12 @@ async function handleSearch(request) {
|
|
|
313
350
|
}
|
|
314
351
|
}
|
|
315
352
|
} else if (isImage) {
|
|
316
|
-
|
|
353
|
+
if (isPushedToCloud && entry.c !== void 0) {
|
|
354
|
+
const cdnUrl = cdnUrls[entry.c];
|
|
355
|
+
thumbnail = cdnUrl ? `${cdnUrl}${key}` : key;
|
|
356
|
+
} else {
|
|
357
|
+
thumbnail = key;
|
|
358
|
+
}
|
|
317
359
|
hasThumbnail = false;
|
|
318
360
|
}
|
|
319
361
|
items.push({
|
|
@@ -336,8 +378,9 @@ async function handleSearch(request) {
|
|
|
336
378
|
async function handleListFolders() {
|
|
337
379
|
try {
|
|
338
380
|
const meta = await loadMeta();
|
|
381
|
+
const fileEntries = getFileEntries(meta);
|
|
339
382
|
const folderSet = /* @__PURE__ */ new Set();
|
|
340
|
-
for (const key of
|
|
383
|
+
for (const [key] of fileEntries) {
|
|
341
384
|
const parts = key.split("/");
|
|
342
385
|
let current = "";
|
|
343
386
|
for (let i = 1; i < parts.length - 1; i++) {
|
|
@@ -366,8 +409,9 @@ async function handleListFolders() {
|
|
|
366
409
|
async function handleCountImages() {
|
|
367
410
|
try {
|
|
368
411
|
const meta = await loadMeta();
|
|
412
|
+
const fileEntries = getFileEntries(meta);
|
|
369
413
|
const allImages = [];
|
|
370
|
-
for (const key of
|
|
414
|
+
for (const [key] of fileEntries) {
|
|
371
415
|
const fileName = _path2.default.basename(key);
|
|
372
416
|
if (isImageFile(fileName)) {
|
|
373
417
|
allImages.push(key.slice(1));
|
|
@@ -391,12 +435,13 @@ async function handleFolderImages(request) {
|
|
|
391
435
|
}
|
|
392
436
|
const folders = foldersParam.split(",");
|
|
393
437
|
const meta = await loadMeta();
|
|
438
|
+
const fileEntries = getFileEntries(meta);
|
|
394
439
|
const allImages = [];
|
|
395
440
|
const prefixes = folders.map((f) => {
|
|
396
441
|
const rel = f.replace(/^public\/?/, "");
|
|
397
442
|
return rel ? `/${rel}/` : "/";
|
|
398
443
|
});
|
|
399
|
-
for (const key of
|
|
444
|
+
for (const [key] of fileEntries) {
|
|
400
445
|
const fileName = _path2.default.basename(key);
|
|
401
446
|
if (!isImageFile(fileName)) continue;
|
|
402
447
|
for (const prefix of prefixes) {
|
|
@@ -524,7 +569,7 @@ async function handleDelete(request) {
|
|
|
524
569
|
for (const key of Object.keys(meta)) {
|
|
525
570
|
if (key.startsWith(prefix) || key === imageKey) {
|
|
526
571
|
if (!meta[key].c) {
|
|
527
|
-
for (const thumbPath of
|
|
572
|
+
for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, key)) {
|
|
528
573
|
const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
|
|
529
574
|
try {
|
|
530
575
|
await _fs.promises.unlink(absoluteThumbPath);
|
|
@@ -540,7 +585,7 @@ async function handleDelete(request) {
|
|
|
540
585
|
const isInImagesFolder = itemPath.startsWith("public/images/");
|
|
541
586
|
if (!isInImagesFolder && entry) {
|
|
542
587
|
if (!isPushedToCloud) {
|
|
543
|
-
for (const thumbPath of
|
|
588
|
+
for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
|
|
544
589
|
const absoluteThumbPath = _path2.default.join(process.cwd(), "public", thumbPath);
|
|
545
590
|
try {
|
|
546
591
|
await _fs.promises.unlink(absoluteThumbPath);
|
|
@@ -652,8 +697,8 @@ async function handleRename(request) {
|
|
|
652
697
|
const newKey = "/" + newRelativePath;
|
|
653
698
|
if (meta[oldKey]) {
|
|
654
699
|
const entry = meta[oldKey];
|
|
655
|
-
const oldThumbPaths =
|
|
656
|
-
const newThumbPaths =
|
|
700
|
+
const oldThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, oldKey);
|
|
701
|
+
const newThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, newKey);
|
|
657
702
|
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
658
703
|
const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
659
704
|
const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
|
|
@@ -732,8 +777,8 @@ async function handleMove(request) {
|
|
|
732
777
|
const newKey = "/" + newRelativePath;
|
|
733
778
|
if (meta[oldKey]) {
|
|
734
779
|
const entry = meta[oldKey];
|
|
735
|
-
const oldThumbPaths =
|
|
736
|
-
const newThumbPaths =
|
|
780
|
+
const oldThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, oldKey);
|
|
781
|
+
const newThumbPaths = _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, newKey);
|
|
737
782
|
for (let i = 0; i < oldThumbPaths.length; i++) {
|
|
738
783
|
const oldThumbPath = _path2.default.join(process.cwd(), "public", oldThumbPaths[i]);
|
|
739
784
|
const newThumbPath = _path2.default.join(process.cwd(), "public", newThumbPaths[i]);
|
|
@@ -790,6 +835,7 @@ async function handleSync(request) {
|
|
|
790
835
|
return _server.NextResponse.json({ error: "No image keys provided" }, { status: 400 });
|
|
791
836
|
}
|
|
792
837
|
const meta = await loadMeta();
|
|
838
|
+
const cdnIndex = getOrAddCdnIndex(meta, publicUrl);
|
|
793
839
|
const r2 = new (0, _clients3.S3Client)({
|
|
794
840
|
region: "auto",
|
|
795
841
|
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
@@ -798,12 +844,12 @@ async function handleSync(request) {
|
|
|
798
844
|
const pushed = [];
|
|
799
845
|
const errors = [];
|
|
800
846
|
for (const imageKey of imageKeys) {
|
|
801
|
-
const entry = meta
|
|
847
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
802
848
|
if (!entry) {
|
|
803
849
|
errors.push(`Image not found in meta: ${imageKey}. Run Scan first.`);
|
|
804
850
|
continue;
|
|
805
851
|
}
|
|
806
|
-
if (entry.c) {
|
|
852
|
+
if (entry.c !== void 0) {
|
|
807
853
|
pushed.push(imageKey);
|
|
808
854
|
continue;
|
|
809
855
|
}
|
|
@@ -827,7 +873,7 @@ async function handleSync(request) {
|
|
|
827
873
|
errors.push(`Original file not found: ${imageKey}`);
|
|
828
874
|
continue;
|
|
829
875
|
}
|
|
830
|
-
for (const thumbPath of
|
|
876
|
+
for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
|
|
831
877
|
const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
|
|
832
878
|
try {
|
|
833
879
|
const fileBuffer = await _fs.promises.readFile(localPath);
|
|
@@ -842,8 +888,8 @@ async function handleSync(request) {
|
|
|
842
888
|
} catch (e20) {
|
|
843
889
|
}
|
|
844
890
|
}
|
|
845
|
-
entry.c =
|
|
846
|
-
for (const thumbPath of
|
|
891
|
+
entry.c = cdnIndex;
|
|
892
|
+
for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
|
|
847
893
|
const localPath = _path2.default.join(process.cwd(), "public", thumbPath);
|
|
848
894
|
try {
|
|
849
895
|
await _fs.promises.unlink(localPath);
|
|
@@ -883,8 +929,9 @@ async function handleReprocess(request) {
|
|
|
883
929
|
for (const imageKey of imageKeys) {
|
|
884
930
|
try {
|
|
885
931
|
let buffer;
|
|
886
|
-
const entry = meta
|
|
887
|
-
const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _6 => _6.c])
|
|
932
|
+
const entry = getMetaEntry(meta, imageKey);
|
|
933
|
+
const isPushedToCloud = _optionalChain([entry, 'optionalAccess', _6 => _6.c]) !== void 0;
|
|
934
|
+
const existingCdnIndex = _optionalChain([entry, 'optionalAccess', _7 => _7.c]);
|
|
888
935
|
const originalPath = _path2.default.join(process.cwd(), "public", imageKey);
|
|
889
936
|
try {
|
|
890
937
|
buffer = await _fs.promises.readFile(originalPath);
|
|
@@ -900,7 +947,7 @@ async function handleReprocess(request) {
|
|
|
900
947
|
}
|
|
901
948
|
const updatedEntry = await processImage(buffer, imageKey);
|
|
902
949
|
if (isPushedToCloud) {
|
|
903
|
-
updatedEntry.c =
|
|
950
|
+
updatedEntry.c = existingCdnIndex;
|
|
904
951
|
await uploadToCdn(imageKey);
|
|
905
952
|
await deleteLocalThumbnails(imageKey);
|
|
906
953
|
try {
|
|
@@ -942,7 +989,7 @@ async function handleProcessAllStream() {
|
|
|
942
989
|
const orphansRemoved = [];
|
|
943
990
|
let alreadyProcessed = 0;
|
|
944
991
|
const imagesToProcess = [];
|
|
945
|
-
for (const [key, entry] of
|
|
992
|
+
for (const [key, entry] of getFileEntries(meta)) {
|
|
946
993
|
const fileName = _path2.default.basename(key);
|
|
947
994
|
if (!isImageFile(fileName)) continue;
|
|
948
995
|
if (!entry.p) {
|
|
@@ -956,7 +1003,8 @@ async function handleProcessAllStream() {
|
|
|
956
1003
|
for (let i = 0; i < imagesToProcess.length; i++) {
|
|
957
1004
|
const { key, entry } = imagesToProcess[i];
|
|
958
1005
|
const fullPath = _path2.default.join(process.cwd(), "public", key);
|
|
959
|
-
const isInCloud = entry.c
|
|
1006
|
+
const isInCloud = entry.c !== void 0;
|
|
1007
|
+
const existingCdnIndex = entry.c;
|
|
960
1008
|
sendEvent({
|
|
961
1009
|
type: "progress",
|
|
962
1010
|
current: i + 1,
|
|
@@ -996,7 +1044,7 @@ async function handleProcessAllStream() {
|
|
|
996
1044
|
meta[key] = {
|
|
997
1045
|
...processedEntry,
|
|
998
1046
|
p: 1,
|
|
999
|
-
...isInCloud ? { c:
|
|
1047
|
+
...isInCloud ? { c: existingCdnIndex } : {}
|
|
1000
1048
|
};
|
|
1001
1049
|
}
|
|
1002
1050
|
if (isInCloud) {
|
|
@@ -1015,9 +1063,9 @@ async function handleProcessAllStream() {
|
|
|
1015
1063
|
}
|
|
1016
1064
|
sendEvent({ type: "cleanup", message: "Removing orphaned thumbnails..." });
|
|
1017
1065
|
const trackedPaths = /* @__PURE__ */ new Set();
|
|
1018
|
-
for (const imageKey of
|
|
1019
|
-
if (
|
|
1020
|
-
for (const thumbPath of
|
|
1066
|
+
for (const [imageKey, entry] of getFileEntries(meta)) {
|
|
1067
|
+
if (entry.c === void 0) {
|
|
1068
|
+
for (const thumbPath of _chunkMCJNUXQ6js.getAllThumbnailPaths.call(void 0, imageKey)) {
|
|
1021
1069
|
trackedPaths.add(thumbPath);
|
|
1022
1070
|
}
|
|
1023
1071
|
}
|
|
@@ -1236,6 +1284,132 @@ async function handleScanStream() {
|
|
|
1236
1284
|
});
|
|
1237
1285
|
}
|
|
1238
1286
|
|
|
1287
|
+
// src/handlers/import.ts
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
function parseImageUrl(url) {
|
|
1291
|
+
const parsed = new URL(url);
|
|
1292
|
+
const base = `${parsed.protocol}//${parsed.host}`;
|
|
1293
|
+
const path9 = parsed.pathname;
|
|
1294
|
+
return { base, path: path9 };
|
|
1295
|
+
}
|
|
1296
|
+
async function processRemoteImage(url) {
|
|
1297
|
+
const response = await fetch(url);
|
|
1298
|
+
if (!response.ok) {
|
|
1299
|
+
throw new Error(`Failed to fetch: ${response.status}`);
|
|
1300
|
+
}
|
|
1301
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
1302
|
+
const metadata = await _sharp2.default.call(void 0, buffer).metadata();
|
|
1303
|
+
const { data, info } = await _sharp2.default.call(void 0, buffer).resize(32, 32, { fit: "inside" }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
1304
|
+
const blurhash = _blurhash.encode.call(void 0, new Uint8ClampedArray(data), info.width, info.height, 4, 4);
|
|
1305
|
+
return {
|
|
1306
|
+
w: metadata.width || 0,
|
|
1307
|
+
h: metadata.height || 0,
|
|
1308
|
+
b: blurhash
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
async function handleImportUrls(request) {
|
|
1312
|
+
const encoder = new TextEncoder();
|
|
1313
|
+
const stream = new ReadableStream({
|
|
1314
|
+
async start(controller) {
|
|
1315
|
+
const sendEvent = (data) => {
|
|
1316
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}
|
|
1317
|
+
|
|
1318
|
+
`));
|
|
1319
|
+
};
|
|
1320
|
+
try {
|
|
1321
|
+
const { urls } = await request.json();
|
|
1322
|
+
if (!urls || !Array.isArray(urls) || urls.length === 0) {
|
|
1323
|
+
sendEvent({ type: "error", message: "No URLs provided" });
|
|
1324
|
+
controller.close();
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const meta = await loadMeta();
|
|
1328
|
+
const added = [];
|
|
1329
|
+
const skipped = [];
|
|
1330
|
+
const errors = [];
|
|
1331
|
+
const total = urls.length;
|
|
1332
|
+
sendEvent({ type: "start", total });
|
|
1333
|
+
for (let i = 0; i < urls.length; i++) {
|
|
1334
|
+
const url = urls[i].trim();
|
|
1335
|
+
if (!url) continue;
|
|
1336
|
+
sendEvent({
|
|
1337
|
+
type: "progress",
|
|
1338
|
+
current: i + 1,
|
|
1339
|
+
total,
|
|
1340
|
+
percent: Math.round((i + 1) / total * 100),
|
|
1341
|
+
currentFile: url
|
|
1342
|
+
});
|
|
1343
|
+
try {
|
|
1344
|
+
const { base, path: path9 } = parseImageUrl(url);
|
|
1345
|
+
const existingEntry = getMetaEntry(meta, path9);
|
|
1346
|
+
if (existingEntry) {
|
|
1347
|
+
skipped.push(path9);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const cdnIndex = getOrAddCdnIndex(meta, base);
|
|
1351
|
+
const imageData = await processRemoteImage(url);
|
|
1352
|
+
setMetaEntry(meta, path9, {
|
|
1353
|
+
w: imageData.w,
|
|
1354
|
+
h: imageData.h,
|
|
1355
|
+
b: imageData.b,
|
|
1356
|
+
c: cdnIndex
|
|
1357
|
+
});
|
|
1358
|
+
added.push(path9);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
console.error(`Failed to import ${url}:`, error);
|
|
1361
|
+
errors.push(url);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
await saveMeta(meta);
|
|
1365
|
+
sendEvent({
|
|
1366
|
+
type: "complete",
|
|
1367
|
+
added: added.length,
|
|
1368
|
+
skipped: skipped.length,
|
|
1369
|
+
errors: errors.length
|
|
1370
|
+
});
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
console.error("Import failed:", error);
|
|
1373
|
+
sendEvent({ type: "error", message: "Import failed" });
|
|
1374
|
+
} finally {
|
|
1375
|
+
controller.close();
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
return new Response(stream, {
|
|
1380
|
+
headers: {
|
|
1381
|
+
"Content-Type": "text/event-stream",
|
|
1382
|
+
"Cache-Control": "no-cache",
|
|
1383
|
+
"Connection": "keep-alive"
|
|
1384
|
+
}
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
async function handleGetCdns() {
|
|
1388
|
+
try {
|
|
1389
|
+
const meta = await loadMeta();
|
|
1390
|
+
const cdns = meta._cdns || [];
|
|
1391
|
+
return Response.json({ cdns });
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
console.error("Failed to get CDNs:", error);
|
|
1394
|
+
return Response.json({ error: "Failed to get CDNs" }, { status: 500 });
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
async function handleUpdateCdns(request) {
|
|
1398
|
+
try {
|
|
1399
|
+
const { cdns } = await request.json();
|
|
1400
|
+
if (!Array.isArray(cdns)) {
|
|
1401
|
+
return Response.json({ error: "Invalid CDN array" }, { status: 400 });
|
|
1402
|
+
}
|
|
1403
|
+
const meta = await loadMeta();
|
|
1404
|
+
meta._cdns = cdns.map((url) => url.replace(/\/$/, ""));
|
|
1405
|
+
await saveMeta(meta);
|
|
1406
|
+
return Response.json({ success: true, cdns: meta._cdns });
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
console.error("Failed to update CDNs:", error);
|
|
1409
|
+
return Response.json({ error: "Failed to update CDNs" }, { status: 500 });
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1239
1413
|
// src/handlers/index.ts
|
|
1240
1414
|
async function GET(request) {
|
|
1241
1415
|
if (process.env.NODE_ENV !== "development") {
|
|
@@ -1258,6 +1432,9 @@ async function GET(request) {
|
|
|
1258
1432
|
if (route === "search") {
|
|
1259
1433
|
return handleSearch(request);
|
|
1260
1434
|
}
|
|
1435
|
+
if (route === "cdns") {
|
|
1436
|
+
return handleGetCdns();
|
|
1437
|
+
}
|
|
1261
1438
|
return _server.NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
1262
1439
|
}
|
|
1263
1440
|
async function POST(request) {
|
|
@@ -1293,6 +1470,12 @@ async function POST(request) {
|
|
|
1293
1470
|
if (route === "scan") {
|
|
1294
1471
|
return handleScanStream();
|
|
1295
1472
|
}
|
|
1473
|
+
if (route === "import") {
|
|
1474
|
+
return handleImportUrls(request);
|
|
1475
|
+
}
|
|
1476
|
+
if (route === "cdns") {
|
|
1477
|
+
return handleUpdateCdns(request);
|
|
1478
|
+
}
|
|
1296
1479
|
return _server.NextResponse.json({ error: "Not found" }, { status: 404 });
|
|
1297
1480
|
}
|
|
1298
1481
|
async function DELETE(request) {
|