@valentinkolb/filegate 2.4.0 → 2.5.3
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/README.md +109 -512
- package/dist/activity.d.ts +15 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +21 -0
- package/dist/activity.js.map +1 -0
- package/dist/capabilities.d.ts +9 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +11 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/client.d.ts +37 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +77 -0
- package/dist/client.js.map +1 -0
- package/dist/core.d.ts +26 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +58 -0
- package/dist/core.js.map +1 -0
- package/dist/downloads.d.ts +9 -0
- package/dist/downloads.d.ts.map +1 -0
- package/dist/downloads.js +11 -0
- package/dist/downloads.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/index-client.d.ts +17 -0
- package/dist/index-client.d.ts.map +1 -0
- package/dist/index-client.js +29 -0
- package/dist/index-client.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes.d.ts +27 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +71 -0
- package/dist/nodes.js.map +1 -0
- package/dist/paths.d.ts +45 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +71 -0
- package/dist/paths.js.map +1 -0
- package/dist/search.d.ts +17 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +25 -0
- package/dist/search.js.map +1 -0
- package/dist/stats.d.ts +9 -0
- package/dist/stats.d.ts.map +1 -0
- package/dist/stats.js +11 -0
- package/dist/stats.js.map +1 -0
- package/dist/transfers.d.ts +9 -0
- package/dist/transfers.d.ts.map +1 -0
- package/dist/transfers.js +14 -0
- package/dist/transfers.js.map +1 -0
- package/dist/types.d.ts +285 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/uploads.d.ts +246 -0
- package/dist/uploads.d.ts.map +1 -0
- package/dist/uploads.js +580 -0
- package/dist/uploads.js.map +1 -0
- package/dist/utils.d.ts +25 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +41 -0
- package/dist/utils.js.map +1 -0
- package/dist/versions.d.ts +76 -0
- package/dist/versions.d.ts.map +1 -0
- package/dist/versions.js +82 -0
- package/dist/versions.js.map +1 -0
- package/package.json +36 -40
- package/LICENSE +0 -21
- package/src/client.ts +0 -436
- package/src/config.ts +0 -41
- package/src/handlers/files.ts +0 -696
- package/src/handlers/indexHandler.ts +0 -107
- package/src/handlers/search.ts +0 -144
- package/src/handlers/thumbnail.ts +0 -174
- package/src/handlers/upload.ts +0 -401
- package/src/index.ts +0 -131
- package/src/lib/index.ts +0 -325
- package/src/lib/openapi.ts +0 -48
- package/src/lib/ownership.ts +0 -133
- package/src/lib/path.ts +0 -128
- package/src/lib/response.ts +0 -10
- package/src/lib/scanner.ts +0 -121
- package/src/lib/validator.ts +0 -21
- package/src/schemas.ts +0 -376
- package/src/utils.ts +0 -282
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { describeRoute } from "hono-openapi";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { jsonResponse, requiresAuth } from "../lib/openapi";
|
|
5
|
-
import { v } from "../lib/validator";
|
|
6
|
-
import {
|
|
7
|
-
RescanResponseSchema,
|
|
8
|
-
IndexStatsSchema,
|
|
9
|
-
BulkResolveBodySchema,
|
|
10
|
-
BulkResolveResponseSchema,
|
|
11
|
-
ErrorSchema,
|
|
12
|
-
} from "../schemas";
|
|
13
|
-
import { config } from "../config";
|
|
14
|
-
import { bulkResolve, getIndexStats } from "../lib/index";
|
|
15
|
-
import { scanAll } from "../lib/scanner";
|
|
16
|
-
|
|
17
|
-
const app = new Hono();
|
|
18
|
-
|
|
19
|
-
app.post(
|
|
20
|
-
"/rescan",
|
|
21
|
-
describeRoute({
|
|
22
|
-
tags: ["Index"],
|
|
23
|
-
summary: "Rescan file index",
|
|
24
|
-
...requiresAuth,
|
|
25
|
-
responses: {
|
|
26
|
-
200: jsonResponse(RescanResponseSchema, "Rescan completed"),
|
|
27
|
-
400: jsonResponse(ErrorSchema, "Bad request"),
|
|
28
|
-
500: jsonResponse(ErrorSchema, "Internal error"),
|
|
29
|
-
},
|
|
30
|
-
}),
|
|
31
|
-
async (c) => {
|
|
32
|
-
if (!config.indexEnabled) {
|
|
33
|
-
return c.json({ error: "index disabled" }, 400);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
try {
|
|
37
|
-
const result = await scanAll();
|
|
38
|
-
return c.json(result);
|
|
39
|
-
} catch (err) {
|
|
40
|
-
console.error("[Filegate] Index rescan failed:", err);
|
|
41
|
-
return c.json({ error: "rescan failed" }, 500);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
app.get(
|
|
47
|
-
"/stats",
|
|
48
|
-
describeRoute({
|
|
49
|
-
tags: ["Index"],
|
|
50
|
-
summary: "Get index stats",
|
|
51
|
-
...requiresAuth,
|
|
52
|
-
responses: {
|
|
53
|
-
200: jsonResponse(IndexStatsSchema, "Index stats"),
|
|
54
|
-
400: jsonResponse(ErrorSchema, "Bad request"),
|
|
55
|
-
500: jsonResponse(ErrorSchema, "Internal error"),
|
|
56
|
-
},
|
|
57
|
-
}),
|
|
58
|
-
async (c) => {
|
|
59
|
-
if (!config.indexEnabled) {
|
|
60
|
-
return c.json({ error: "index disabled" }, 400);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const stats = await getIndexStats();
|
|
65
|
-
return c.json(stats);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
console.error("[Filegate] Index stats failed:", err);
|
|
68
|
-
return c.json({ error: "stats failed" }, 500);
|
|
69
|
-
}
|
|
70
|
-
},
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
app.post(
|
|
74
|
-
"/resolve",
|
|
75
|
-
describeRoute({
|
|
76
|
-
tags: ["Index"],
|
|
77
|
-
summary: "Resolve file IDs to paths",
|
|
78
|
-
...requiresAuth,
|
|
79
|
-
responses: {
|
|
80
|
-
200: jsonResponse(BulkResolveResponseSchema, "Resolved paths"),
|
|
81
|
-
400: jsonResponse(ErrorSchema, "Bad request"),
|
|
82
|
-
500: jsonResponse(ErrorSchema, "Internal error"),
|
|
83
|
-
},
|
|
84
|
-
}),
|
|
85
|
-
v("json", BulkResolveBodySchema),
|
|
86
|
-
async (c) => {
|
|
87
|
-
if (!config.indexEnabled) {
|
|
88
|
-
return c.json({ error: "index disabled" }, 400);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
const { ids } = c.req.valid("json");
|
|
93
|
-
const resolved = await bulkResolve(ids);
|
|
94
|
-
const response: Record<string, string | null> = {};
|
|
95
|
-
for (const id of ids) {
|
|
96
|
-
const entry = resolved[id];
|
|
97
|
-
response[id] = entry ? join(entry.basePath, entry.relPath) : null;
|
|
98
|
-
}
|
|
99
|
-
return c.json(response);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
console.error("[Filegate] Index resolve failed:", err);
|
|
102
|
-
return c.json({ error: "resolve failed" }, 500);
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
export default app;
|
package/src/handlers/search.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { describeRoute } from "hono-openapi";
|
|
3
|
-
import { Glob } from "bun";
|
|
4
|
-
import { stat } from "node:fs/promises";
|
|
5
|
-
import { join, basename, relative } from "node:path";
|
|
6
|
-
import { validatePath } from "../lib/path";
|
|
7
|
-
import { jsonResponse, requiresAuth } from "../lib/openapi";
|
|
8
|
-
import { v } from "../lib/validator";
|
|
9
|
-
import {
|
|
10
|
-
SearchResponseSchema,
|
|
11
|
-
ErrorSchema,
|
|
12
|
-
SearchQuerySchema,
|
|
13
|
-
countRecursiveWildcards,
|
|
14
|
-
type FileInfo,
|
|
15
|
-
type SearchResult,
|
|
16
|
-
} from "../schemas";
|
|
17
|
-
import { config } from "../config";
|
|
18
|
-
import { enrichFileInfoBatch } from "../lib/index";
|
|
19
|
-
|
|
20
|
-
const app = new Hono();
|
|
21
|
-
|
|
22
|
-
const getFileInfo = async (fullPath: string, basePath: string): Promise<FileInfo | null> => {
|
|
23
|
-
try {
|
|
24
|
-
const s = await stat(fullPath);
|
|
25
|
-
const name = basename(fullPath);
|
|
26
|
-
const file = Bun.file(fullPath);
|
|
27
|
-
|
|
28
|
-
return {
|
|
29
|
-
name,
|
|
30
|
-
path: relative(basePath, fullPath),
|
|
31
|
-
type: s.isDirectory() ? "directory" : "file",
|
|
32
|
-
size: s.isDirectory() ? 0 : s.size,
|
|
33
|
-
mtime: s.mtime.toISOString(),
|
|
34
|
-
isHidden: name.startsWith("."),
|
|
35
|
-
mimeType: s.isDirectory() ? undefined : file.type,
|
|
36
|
-
};
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const searchInPath = async (
|
|
43
|
-
basePath: string,
|
|
44
|
-
pattern: string,
|
|
45
|
-
showHidden: boolean,
|
|
46
|
-
limit: number,
|
|
47
|
-
includeFiles: boolean,
|
|
48
|
-
includeDirectories: boolean,
|
|
49
|
-
): Promise<SearchResult> => {
|
|
50
|
-
const glob = new Glob(pattern);
|
|
51
|
-
const files: FileInfo[] = [];
|
|
52
|
-
let hasMore = false;
|
|
53
|
-
|
|
54
|
-
// If we need directories, we must set onlyFiles: false
|
|
55
|
-
const onlyFiles = includeFiles && !includeDirectories;
|
|
56
|
-
|
|
57
|
-
for await (const match of glob.scan({ cwd: basePath, dot: showHidden, onlyFiles })) {
|
|
58
|
-
if (!showHidden && basename(match).startsWith(".")) continue;
|
|
59
|
-
|
|
60
|
-
if (files.length >= limit) {
|
|
61
|
-
hasMore = true;
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const info = await getFileInfo(join(basePath, match), basePath);
|
|
66
|
-
if (!info) continue;
|
|
67
|
-
|
|
68
|
-
// Filter based on type
|
|
69
|
-
if (info.type === "file" && !includeFiles) continue;
|
|
70
|
-
if (info.type === "directory" && !includeDirectories) continue;
|
|
71
|
-
|
|
72
|
-
files.push(info);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { basePath, files, total: files.length, hasMore };
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
app.get(
|
|
79
|
-
"/search",
|
|
80
|
-
describeRoute({
|
|
81
|
-
tags: ["Search"],
|
|
82
|
-
summary: "Search files with glob pattern",
|
|
83
|
-
description: "Search multiple paths in parallel using glob patterns",
|
|
84
|
-
...requiresAuth,
|
|
85
|
-
responses: {
|
|
86
|
-
200: jsonResponse(SearchResponseSchema, "Search results"),
|
|
87
|
-
400: jsonResponse(ErrorSchema, "Bad request"),
|
|
88
|
-
403: jsonResponse(ErrorSchema, "Forbidden"),
|
|
89
|
-
404: jsonResponse(ErrorSchema, "Not found"),
|
|
90
|
-
},
|
|
91
|
-
}),
|
|
92
|
-
v("query", SearchQuerySchema),
|
|
93
|
-
async (c) => {
|
|
94
|
-
const { paths: pathsParam, pattern, showHidden, limit: limitParam, files, directories } = c.req.valid("query");
|
|
95
|
-
|
|
96
|
-
// At least one of files or directories must be true
|
|
97
|
-
if (!files && !directories) {
|
|
98
|
-
return c.json({ error: "at least one of 'files' or 'directories' must be true" }, 400);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Validate recursive wildcard count
|
|
102
|
-
const wildcardCount = countRecursiveWildcards(pattern);
|
|
103
|
-
if (wildcardCount > config.searchMaxRecursiveWildcards) {
|
|
104
|
-
return c.json(
|
|
105
|
-
{ error: `too many recursive wildcards: ${wildcardCount} (max ${config.searchMaxRecursiveWildcards})` },
|
|
106
|
-
400,
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const limit = Math.min(limitParam ?? config.searchMaxResults, config.searchMaxResults);
|
|
111
|
-
const paths = pathsParam
|
|
112
|
-
.split(",")
|
|
113
|
-
.map((p) => p.trim())
|
|
114
|
-
.filter(Boolean);
|
|
115
|
-
const validPaths: string[] = [];
|
|
116
|
-
|
|
117
|
-
for (const p of paths) {
|
|
118
|
-
const result = await validatePath(p, { allowBasePath: true });
|
|
119
|
-
if (!result.ok) return c.json({ error: result.error }, result.status);
|
|
120
|
-
|
|
121
|
-
const s = await stat(result.realPath).catch(() => null);
|
|
122
|
-
if (!s) return c.json({ error: `path not found: ${p}` }, 404);
|
|
123
|
-
if (!s.isDirectory()) return c.json({ error: `not a directory: ${p}` }, 400);
|
|
124
|
-
|
|
125
|
-
validPaths.push(result.realPath);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const results = await Promise.all(
|
|
129
|
-
validPaths.map((p) => searchInPath(p, pattern, showHidden, limit, files, directories)),
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
if (config.indexEnabled) {
|
|
133
|
-
for (const result of results) {
|
|
134
|
-
result.files = await enrichFileInfoBatch(result.files, result.basePath);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const totalFiles = results.reduce((sum, r) => sum + r.total, 0);
|
|
139
|
-
|
|
140
|
-
return c.json({ results, totalFiles });
|
|
141
|
-
},
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
export default app;
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { Hono } from "hono";
|
|
2
|
-
import { describeRoute } from "hono-openapi";
|
|
3
|
-
import { stat } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import sharp from "sharp";
|
|
6
|
-
import { validatePath } from "../lib/path";
|
|
7
|
-
import { jsonResponse, requiresAuth } from "../lib/openapi";
|
|
8
|
-
import { v } from "../lib/validator";
|
|
9
|
-
import { ImageThumbnailQuerySchema, ErrorSchema } from "../schemas";
|
|
10
|
-
import { config } from "../config";
|
|
11
|
-
import { resolveId } from "../lib/index";
|
|
12
|
-
|
|
13
|
-
const app = new Hono();
|
|
14
|
-
|
|
15
|
-
// Generate ETag from path, mtime, and thumbnail parameters
|
|
16
|
-
const generateETag = (path: string, mtime: Date, params: string): string => {
|
|
17
|
-
const hasher = new Bun.CryptoHasher("sha256");
|
|
18
|
-
hasher.update(`${path}:${mtime.getTime()}:${params}`);
|
|
19
|
-
return `"${hasher.digest("hex").slice(0, 16)}"`;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
// Supported image MIME types
|
|
23
|
-
const SUPPORTED_IMAGE_TYPES = new Set([
|
|
24
|
-
"image/jpeg",
|
|
25
|
-
"image/png",
|
|
26
|
-
"image/webp",
|
|
27
|
-
"image/avif",
|
|
28
|
-
"image/tiff",
|
|
29
|
-
"image/gif",
|
|
30
|
-
"image/svg+xml",
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
// Format to MIME type mapping
|
|
34
|
-
const FORMAT_MIME: Record<string, string> = {
|
|
35
|
-
webp: "image/webp",
|
|
36
|
-
jpeg: "image/jpeg",
|
|
37
|
-
png: "image/png",
|
|
38
|
-
avif: "image/avif",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const resolveQueryPath = async (
|
|
42
|
-
path: string | undefined,
|
|
43
|
-
id: string | undefined,
|
|
44
|
-
): Promise<{ ok: true; path: string } | { ok: false; status: 400 | 404; error: string }> => {
|
|
45
|
-
if (id) {
|
|
46
|
-
if (!config.indexEnabled) {
|
|
47
|
-
return { ok: false, status: 400, error: "index disabled" };
|
|
48
|
-
}
|
|
49
|
-
const resolved = await resolveId(id);
|
|
50
|
-
if (!resolved) return { ok: false, status: 404, error: "not found" };
|
|
51
|
-
return { ok: true, path: join(resolved.basePath, resolved.relPath) };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!path) {
|
|
55
|
-
return { ok: false, status: 400, error: "path or id required" };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return { ok: true, path };
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// GET /thumbnail/image - Generate image thumbnail
|
|
62
|
-
app.get(
|
|
63
|
-
"/image",
|
|
64
|
-
describeRoute({
|
|
65
|
-
tags: ["Thumbnail"],
|
|
66
|
-
summary: "Generate image thumbnail",
|
|
67
|
-
description:
|
|
68
|
-
"Generate a thumbnail from an image file on-the-fly using Sharp. " +
|
|
69
|
-
"Supports JPEG, PNG, WebP, AVIF, TIFF, GIF, and SVG input formats.",
|
|
70
|
-
...requiresAuth,
|
|
71
|
-
responses: {
|
|
72
|
-
200: {
|
|
73
|
-
description: "Thumbnail image",
|
|
74
|
-
content: {
|
|
75
|
-
"image/webp": { schema: { type: "string", format: "binary" } },
|
|
76
|
-
"image/jpeg": { schema: { type: "string", format: "binary" } },
|
|
77
|
-
"image/png": { schema: { type: "string", format: "binary" } },
|
|
78
|
-
"image/avif": { schema: { type: "string", format: "binary" } },
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
400: jsonResponse(ErrorSchema, "Invalid parameters or not an image"),
|
|
82
|
-
403: jsonResponse(ErrorSchema, "Forbidden"),
|
|
83
|
-
404: jsonResponse(ErrorSchema, "Not found"),
|
|
84
|
-
},
|
|
85
|
-
}),
|
|
86
|
-
v("query", ImageThumbnailQuerySchema),
|
|
87
|
-
async (c) => {
|
|
88
|
-
const { path, id, width, height, fit, position, format, quality } = c.req.valid("query");
|
|
89
|
-
|
|
90
|
-
const resolved = await resolveQueryPath(path, id);
|
|
91
|
-
if (!resolved.ok) return c.json({ error: resolved.error }, resolved.status);
|
|
92
|
-
|
|
93
|
-
// Validate path
|
|
94
|
-
const result = await validatePath(resolved.path);
|
|
95
|
-
if (!result.ok) return c.json({ error: result.error }, result.status);
|
|
96
|
-
|
|
97
|
-
// Check if file exists and is a file
|
|
98
|
-
const file = Bun.file(result.realPath);
|
|
99
|
-
if (!(await file.exists())) {
|
|
100
|
-
return c.json({ error: "file not found" }, 404);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Check MIME type
|
|
104
|
-
if (!SUPPORTED_IMAGE_TYPES.has(file.type)) {
|
|
105
|
-
return c.json(
|
|
106
|
-
{ error: `unsupported image type: ${file.type}. Supported: ${[...SUPPORTED_IMAGE_TYPES].join(", ")}` },
|
|
107
|
-
400,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Get file stats for Last-Modified and ETag
|
|
112
|
-
const fileStat = await stat(result.realPath);
|
|
113
|
-
const lastModified = fileStat.mtime;
|
|
114
|
-
const paramsKey = `${width}x${height}:${fit}:${position}:${format}:${quality}`;
|
|
115
|
-
const etag = generateETag(result.realPath, lastModified, paramsKey);
|
|
116
|
-
|
|
117
|
-
// Check If-None-Match (ETag)
|
|
118
|
-
const ifNoneMatch = c.req.header("If-None-Match");
|
|
119
|
-
if (ifNoneMatch === etag) {
|
|
120
|
-
return new Response(null, { status: 304 });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Check If-Modified-Since
|
|
124
|
-
const ifModifiedSince = c.req.header("If-Modified-Since");
|
|
125
|
-
if (ifModifiedSince) {
|
|
126
|
-
const clientDate = new Date(ifModifiedSince);
|
|
127
|
-
if (lastModified <= clientDate) {
|
|
128
|
-
return new Response(null, { status: 304 });
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
// Read file and process with Sharp
|
|
134
|
-
const buffer = await file.arrayBuffer();
|
|
135
|
-
|
|
136
|
-
let pipeline = sharp(Buffer.from(buffer)).resize(width, height, {
|
|
137
|
-
fit,
|
|
138
|
-
position,
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
// Apply format and quality
|
|
142
|
-
switch (format) {
|
|
143
|
-
case "webp":
|
|
144
|
-
pipeline = pipeline.webp({ quality });
|
|
145
|
-
break;
|
|
146
|
-
case "jpeg":
|
|
147
|
-
pipeline = pipeline.jpeg({ quality });
|
|
148
|
-
break;
|
|
149
|
-
case "png":
|
|
150
|
-
pipeline = pipeline.png({ quality });
|
|
151
|
-
break;
|
|
152
|
-
case "avif":
|
|
153
|
-
pipeline = pipeline.avif({ quality });
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const thumbnail = await pipeline.toBuffer();
|
|
158
|
-
|
|
159
|
-
return new Response(thumbnail, {
|
|
160
|
-
headers: {
|
|
161
|
-
"Content-Type": FORMAT_MIME[format],
|
|
162
|
-
"Cache-Control": "public, max-age=31536000, immutable",
|
|
163
|
-
"Last-Modified": lastModified.toUTCString(),
|
|
164
|
-
ETag: etag,
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
} catch (err) {
|
|
168
|
-
console.error("[Filegate] Thumbnail error:", err);
|
|
169
|
-
return c.json({ error: "failed to generate thumbnail" }, 500);
|
|
170
|
-
}
|
|
171
|
-
},
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
export default app;
|