@valentinkolb/filegate 2.4.0 → 2.5.2

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.
Files changed (88) hide show
  1. package/dist/activity.d.ts +15 -0
  2. package/dist/activity.d.ts.map +1 -0
  3. package/dist/activity.js +21 -0
  4. package/dist/activity.js.map +1 -0
  5. package/dist/capabilities.d.ts +9 -0
  6. package/dist/capabilities.d.ts.map +1 -0
  7. package/dist/capabilities.js +11 -0
  8. package/dist/capabilities.js.map +1 -0
  9. package/dist/client.d.ts +37 -0
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +77 -0
  12. package/dist/client.js.map +1 -0
  13. package/dist/core.d.ts +26 -0
  14. package/dist/core.d.ts.map +1 -0
  15. package/dist/core.js +58 -0
  16. package/dist/core.js.map +1 -0
  17. package/dist/downloads.d.ts +9 -0
  18. package/dist/downloads.d.ts.map +1 -0
  19. package/dist/downloads.js +11 -0
  20. package/dist/downloads.js.map +1 -0
  21. package/dist/errors.d.ts +18 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +42 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/index-client.d.ts +17 -0
  26. package/dist/index-client.d.ts.map +1 -0
  27. package/dist/index-client.js +29 -0
  28. package/dist/index-client.js.map +1 -0
  29. package/dist/index.d.ts +14 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +7 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/nodes.d.ts +27 -0
  34. package/dist/nodes.d.ts.map +1 -0
  35. package/dist/nodes.js +71 -0
  36. package/dist/nodes.js.map +1 -0
  37. package/dist/paths.d.ts +45 -0
  38. package/dist/paths.d.ts.map +1 -0
  39. package/dist/paths.js +71 -0
  40. package/dist/paths.js.map +1 -0
  41. package/dist/search.d.ts +17 -0
  42. package/dist/search.d.ts.map +1 -0
  43. package/dist/search.js +25 -0
  44. package/dist/search.js.map +1 -0
  45. package/dist/stats.d.ts +9 -0
  46. package/dist/stats.d.ts.map +1 -0
  47. package/dist/stats.js +11 -0
  48. package/dist/stats.js.map +1 -0
  49. package/dist/transfers.d.ts +9 -0
  50. package/dist/transfers.d.ts.map +1 -0
  51. package/dist/transfers.js +14 -0
  52. package/dist/transfers.js.map +1 -0
  53. package/dist/types.d.ts +285 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +2 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/uploads.d.ts +246 -0
  58. package/dist/uploads.d.ts.map +1 -0
  59. package/dist/uploads.js +580 -0
  60. package/dist/uploads.js.map +1 -0
  61. package/dist/utils.d.ts +25 -0
  62. package/dist/utils.d.ts.map +1 -0
  63. package/dist/utils.js +41 -0
  64. package/dist/utils.js.map +1 -0
  65. package/dist/versions.d.ts +76 -0
  66. package/dist/versions.d.ts.map +1 -0
  67. package/dist/versions.js +82 -0
  68. package/dist/versions.js.map +1 -0
  69. package/package.json +36 -41
  70. package/LICENSE +0 -21
  71. package/README.md +0 -569
  72. package/src/client.ts +0 -436
  73. package/src/config.ts +0 -41
  74. package/src/handlers/files.ts +0 -696
  75. package/src/handlers/indexHandler.ts +0 -107
  76. package/src/handlers/search.ts +0 -144
  77. package/src/handlers/thumbnail.ts +0 -174
  78. package/src/handlers/upload.ts +0 -401
  79. package/src/index.ts +0 -131
  80. package/src/lib/index.ts +0 -325
  81. package/src/lib/openapi.ts +0 -48
  82. package/src/lib/ownership.ts +0 -133
  83. package/src/lib/path.ts +0 -128
  84. package/src/lib/response.ts +0 -10
  85. package/src/lib/scanner.ts +0 -121
  86. package/src/lib/validator.ts +0 -21
  87. package/src/schemas.ts +0 -376
  88. package/src/utils.ts +0 -282
@@ -1,401 +0,0 @@
1
- import { Hono } from "hono";
2
- import { describeRoute } from "hono-openapi";
3
- import { mkdir, readdir, rm, stat, rename, readFile, writeFile } from "node:fs/promises";
4
- import { join, dirname, relative } from "node:path";
5
- import { getSemaphore } from "@henrygd/semaphore";
6
- import { validatePath } from "../lib/path";
7
- import { applyOwnership, type Ownership } from "../lib/ownership";
8
- import { jsonResponse, requiresAuth } from "../lib/openapi";
9
- import { v } from "../lib/validator";
10
- import { indexFile } from "../lib/index";
11
- import {
12
- UploadStartBodySchema,
13
- UploadStartResponseSchema,
14
- UploadChunkResponseSchema,
15
- ErrorSchema,
16
- UploadChunkHeadersSchema,
17
- } from "../schemas";
18
- import { config } from "../config";
19
-
20
- const app = new Hono();
21
-
22
- // Deterministic upload ID from path + filename + checksum
23
- const computeUploadId = (path: string, filename: string, checksum: string): string => {
24
- const hasher = new Bun.CryptoHasher("sha256");
25
- hasher.update(`${path}:${filename}:${checksum}`);
26
- return hasher.digest("hex").slice(0, 16);
27
- };
28
-
29
- // Chunk storage paths
30
- const chunksDir = (id: string) => join(config.uploadTempDir, id);
31
- const chunkPath = (id: string, idx: number) => join(chunksDir(id), String(idx));
32
- const metaPath = (id: string) => join(chunksDir(id), "meta.json");
33
-
34
- type UploadMeta = {
35
- uploadId: string;
36
- path: string;
37
- filename: string;
38
- size: number;
39
- checksum: string;
40
- chunkSize: number;
41
- totalChunks: number;
42
- ownership: Ownership | null;
43
- createdAt: number; // Unix timestamp for expiry check
44
- };
45
-
46
- const saveMeta = async (meta: UploadMeta): Promise<void> => {
47
- await mkdir(chunksDir(meta.uploadId), { recursive: true });
48
- await writeFile(metaPath(meta.uploadId), JSON.stringify(meta));
49
- };
50
-
51
- const loadMeta = async (id: string): Promise<UploadMeta | null> => {
52
- try {
53
- const data = await readFile(metaPath(id), "utf-8");
54
- return JSON.parse(data) as UploadMeta;
55
- } catch {
56
- return null;
57
- }
58
- };
59
-
60
- const refreshExpiry = async (id: string, meta: UploadMeta): Promise<void> => {
61
- // Update createdAt to extend expiry
62
- meta.createdAt = Date.now();
63
- await saveMeta(meta);
64
- };
65
-
66
- // Get uploaded chunks from filesystem
67
- const getUploadedChunks = async (id: string): Promise<number[]> => {
68
- try {
69
- const files = await readdir(chunksDir(id));
70
- return files
71
- .filter((f) => f !== "meta.json")
72
- .map((f) => parseInt(f, 10))
73
- .filter((n) => !isNaN(n))
74
- .sort((a, b) => a - b);
75
- } catch {
76
- return [];
77
- }
78
- };
79
-
80
- const cleanupUpload = async (id: string): Promise<void> => {
81
- await rm(chunksDir(id), { recursive: true }).catch(() => {});
82
- };
83
-
84
- const assembleFile = async (meta: UploadMeta): Promise<string | null> => {
85
- // Use semaphore to prevent concurrent assembly of the same upload
86
- const semaphore = getSemaphore(`assemble:${meta.uploadId}`, 1);
87
- await semaphore.acquire();
88
-
89
- try {
90
- // Check if assembly was already completed by another request while we waited
91
- const chunks = await getUploadedChunks(meta.uploadId);
92
- if (chunks.length === 0) {
93
- // Chunks were cleaned up - assembly was already completed
94
- return null;
95
- }
96
- if (chunks.length !== meta.totalChunks) return "missing chunks";
97
-
98
- // Verify all expected chunk indices are present (0 to totalChunks-1)
99
- const expectedChunks = Array.from({ length: meta.totalChunks }, (_, i) => i);
100
- const missingChunks = expectedChunks.filter((i) => !chunks.includes(i));
101
- if (missingChunks.length > 0) {
102
- return `missing chunk indices: ${missingChunks.join(", ")}`;
103
- }
104
-
105
- const fullPath = join(meta.path, meta.filename);
106
- const pathResult = await validatePath(fullPath);
107
- if (!pathResult.ok) return pathResult.error;
108
-
109
- await mkdir(dirname(pathResult.realPath), { recursive: true });
110
-
111
- const hasher = new Bun.CryptoHasher("sha256");
112
- const file = Bun.file(pathResult.realPath);
113
- const writer = file.writer();
114
-
115
- try {
116
- for (let i = 0; i < meta.totalChunks; i++) {
117
- const chunkFilePath = chunkPath(meta.uploadId, i);
118
- const chunkFile = Bun.file(chunkFilePath);
119
-
120
- // Verify chunk exists before reading
121
- if (!(await chunkFile.exists())) {
122
- writer.end();
123
- await rm(pathResult.realPath).catch(() => {});
124
- return `chunk ${i} not found during assembly`;
125
- }
126
-
127
- // Read chunk as buffer (more reliable than streaming)
128
- const data = new Uint8Array(await chunkFile.arrayBuffer());
129
- hasher.update(data);
130
- writer.write(data);
131
- }
132
- await writer.end();
133
- } catch (e) {
134
- writer.end();
135
- await rm(pathResult.realPath).catch(() => {});
136
- throw e;
137
- }
138
-
139
- const finalChecksum = `sha256:${hasher.digest("hex")}`;
140
- if (finalChecksum !== meta.checksum) {
141
- await rm(pathResult.realPath).catch(() => {});
142
- return `checksum mismatch: expected ${meta.checksum}, got ${finalChecksum}`;
143
- }
144
-
145
- const ownershipError = await applyOwnership(pathResult.realPath, meta.ownership);
146
- if (ownershipError) {
147
- await rm(pathResult.realPath).catch(() => {});
148
- return ownershipError;
149
- }
150
-
151
- await cleanupUpload(meta.uploadId);
152
- return null;
153
- } finally {
154
- semaphore.release();
155
- }
156
- };
157
-
158
- // POST /upload/start
159
- app.post(
160
- "/start",
161
- describeRoute({
162
- tags: ["Upload"],
163
- summary: "Start or resume chunked upload",
164
- description: "Initialize a new upload or get status of existing upload with same checksum",
165
- ...requiresAuth,
166
- responses: {
167
- 200: jsonResponse(UploadStartResponseSchema, "Upload initialized or resumed"),
168
- 400: jsonResponse(ErrorSchema, "Bad request"),
169
- 403: jsonResponse(ErrorSchema, "Forbidden"),
170
- },
171
- }),
172
- v("json", UploadStartBodySchema),
173
- async (c) => {
174
- const body = c.req.valid("json");
175
-
176
- // Validate size limits
177
- if (body.size > config.maxUploadBytes) {
178
- return c.json({ error: `file size exceeds maximum (${config.maxUploadBytes / 1024 / 1024}MB)` }, 413);
179
- }
180
- if (body.chunkSize > config.maxChunkBytes) {
181
- return c.json({ error: `chunk size exceeds maximum (${config.maxChunkBytes / 1024 / 1024}MB)` }, 400);
182
- }
183
-
184
- const fullPath = join(body.path, body.filename);
185
-
186
- // Build ownership from body
187
- const ownership: Ownership | null =
188
- body.ownerUid != null && body.ownerGid != null && body.mode
189
- ? {
190
- uid: body.ownerUid,
191
- gid: body.ownerGid,
192
- mode: parseInt(body.mode, 8),
193
- dirMode: body.dirMode ? parseInt(body.dirMode, 8) : undefined,
194
- }
195
- : null;
196
-
197
- // Validate path and create parent directories with ownership
198
- const pathResult = await validatePath(fullPath, { createParents: true, ownership });
199
- if (!pathResult.ok) return c.json({ error: pathResult.error }, pathResult.status);
200
-
201
- // Deterministic upload ID - same file/path/checksum = same ID (enables resume)
202
- const uploadId = computeUploadId(body.path, body.filename, body.checksum);
203
-
204
- // Check for existing upload (resume)
205
- const existingMeta = await loadMeta(uploadId);
206
- if (existingMeta) {
207
- // Refresh expiry on resume
208
- await refreshExpiry(uploadId, existingMeta);
209
- // Get chunks from filesystem
210
- const uploadedChunks = await getUploadedChunks(uploadId);
211
- return c.json({
212
- uploadId,
213
- totalChunks: existingMeta.totalChunks,
214
- chunkSize: existingMeta.chunkSize,
215
- uploadedChunks,
216
- completed: false as const,
217
- });
218
- }
219
-
220
- // New upload
221
- const chunkSize = body.chunkSize;
222
- const totalChunks = Math.ceil(body.size / chunkSize);
223
-
224
- const meta: UploadMeta = {
225
- uploadId,
226
- path: body.path,
227
- filename: body.filename,
228
- size: body.size,
229
- checksum: body.checksum,
230
- chunkSize,
231
- totalChunks,
232
- ownership,
233
- createdAt: Date.now(),
234
- };
235
-
236
- await saveMeta(meta);
237
-
238
- return c.json({
239
- uploadId,
240
- totalChunks,
241
- chunkSize,
242
- uploadedChunks: [],
243
- completed: false as const,
244
- });
245
- },
246
- );
247
-
248
- // POST /upload/chunk
249
- app.post(
250
- "/chunk",
251
- describeRoute({
252
- tags: ["Upload"],
253
- summary: "Upload a chunk",
254
- description: "Upload a single chunk. Auto-completes when all chunks received.",
255
- ...requiresAuth,
256
- responses: {
257
- 200: jsonResponse(UploadChunkResponseSchema, "Chunk received"),
258
- 400: jsonResponse(ErrorSchema, "Bad request"),
259
- 404: jsonResponse(ErrorSchema, "Upload not found"),
260
- },
261
- }),
262
- v("header", UploadChunkHeadersSchema),
263
- async (c) => {
264
- const headers = c.req.valid("header");
265
- const uploadId = headers["x-upload-id"];
266
- const chunkIndex = headers["x-chunk-index"];
267
- const chunkChecksum = headers["x-chunk-checksum"];
268
-
269
- const meta = await loadMeta(uploadId);
270
- if (!meta) return c.json({ error: "upload not found" }, 404);
271
-
272
- if (chunkIndex >= meta.totalChunks) {
273
- return c.json({ error: `chunk index ${chunkIndex} exceeds total ${meta.totalChunks}` }, 400);
274
- }
275
-
276
- // Read body as ArrayBuffer (more reliable than streaming)
277
- let bodyBuffer: ArrayBuffer;
278
- try {
279
- bodyBuffer = await c.req.arrayBuffer();
280
- } catch {
281
- return c.json({ error: "failed to read request body" }, 400);
282
- }
283
-
284
- if (bodyBuffer.byteLength === 0) {
285
- return c.json({ error: "missing body" }, 400);
286
- }
287
-
288
- if (bodyBuffer.byteLength > config.maxChunkBytes) {
289
- return c.json({ error: `chunk size exceeds maximum (${config.maxChunkBytes / 1024 / 1024}MB)` }, 413);
290
- }
291
-
292
- const bodyData = new Uint8Array(bodyBuffer);
293
- const hasher = new Bun.CryptoHasher("sha256");
294
- hasher.update(bodyData);
295
-
296
- // Write chunk to temporary file
297
- const tempChunkPath = chunkPath(uploadId, chunkIndex) + ".tmp";
298
- await mkdir(chunksDir(uploadId), { recursive: true });
299
-
300
- try {
301
- await Bun.write(tempChunkPath, bodyData);
302
- } catch (e) {
303
- await rm(tempChunkPath).catch(() => {});
304
- throw e;
305
- }
306
-
307
- // Verify checksum if provided
308
- if (chunkChecksum) {
309
- const computed = `sha256:${hasher.digest("hex")}`;
310
- if (computed !== chunkChecksum) {
311
- await rm(tempChunkPath).catch(() => {});
312
- return c.json({ error: `chunk checksum mismatch: expected ${chunkChecksum}, got ${computed}` }, 400);
313
- }
314
- }
315
-
316
- // Move temp file to final location (atomic rename, no memory copy)
317
- const finalChunkPath = chunkPath(uploadId, chunkIndex);
318
- await rename(tempChunkPath, finalChunkPath);
319
-
320
- // Get uploaded chunks from filesystem
321
- const uploadedChunks = await getUploadedChunks(uploadId);
322
-
323
- if (uploadedChunks.length === meta.totalChunks) {
324
- const assembleError = await assembleFile(meta);
325
- if (assembleError) return c.json({ error: assembleError }, 500);
326
-
327
- const fullPath = join(meta.path, meta.filename);
328
- const file = Bun.file(fullPath);
329
- const s = await stat(fullPath);
330
- let fileId: string | undefined;
331
-
332
- if (config.indexEnabled) {
333
- const pathResult = await validatePath(fullPath);
334
- if (pathResult.ok) {
335
- try {
336
- const relPath = relative(pathResult.basePath, pathResult.realPath);
337
- const outcome = await indexFile(pathResult.basePath, relPath, {
338
- dev: s.dev,
339
- ino: s.ino,
340
- size: s.size,
341
- mtimeMs: s.mtimeMs,
342
- isDirectory: s.isDirectory(),
343
- });
344
- fileId = outcome.id;
345
- } catch (err) {
346
- console.error("[Filegate] Index update failed:", err);
347
- }
348
- }
349
- }
350
-
351
- return c.json({
352
- completed: true as const,
353
- file: {
354
- name: meta.filename,
355
- path: fullPath,
356
- type: "file" as const,
357
- size: s.size,
358
- mtime: s.mtime.toISOString(),
359
- isHidden: meta.filename.startsWith("."),
360
- checksum: meta.checksum,
361
- mimeType: file.type,
362
- ...(fileId ? { fileId } : {}),
363
- },
364
- });
365
- }
366
-
367
- return c.json({
368
- chunkIndex,
369
- uploadedChunks,
370
- completed: false as const,
371
- });
372
- },
373
- );
374
-
375
- // Cleanup expired upload directories
376
- export const cleanupOrphanedChunks = async () => {
377
- try {
378
- const dirs = await readdir(config.uploadTempDir);
379
- let cleaned = 0;
380
- const now = Date.now();
381
- const expiryMs = config.uploadExpirySecs * 1000;
382
-
383
- for (const dir of dirs) {
384
- const meta = await loadMeta(dir);
385
-
386
- // Remove if no meta or expired
387
- if (!meta || now - meta.createdAt > expiryMs) {
388
- await rm(chunksDir(dir), { recursive: true }).catch(() => {});
389
- cleaned++;
390
- }
391
- }
392
-
393
- if (cleaned > 0) {
394
- console.log(`[Filegate] Cleaned up ${cleaned} expired upload${cleaned === 1 ? "" : "s"}`);
395
- }
396
- } catch {
397
- // Directory doesn't exist yet
398
- }
399
- };
400
-
401
- export default app;
package/src/index.ts DELETED
@@ -1,131 +0,0 @@
1
- import { Hono } from "hono";
2
- import { HTTPException } from "hono/http-exception";
3
- import { Scalar } from "@scalar/hono-api-reference";
4
- import { generateSpecs } from "hono-openapi";
5
- import { createMarkdownFromOpenApi } from "@scalar/openapi-to-markdown";
6
- import { bearerAuth } from "hono/bearer-auth";
7
- import { secureHeaders } from "hono/secure-headers";
8
- import { logger } from "hono/logger";
9
- import { config } from "./config";
10
- import { openApiMeta } from "./lib/openapi";
11
- import filesRoutes from "./handlers/files";
12
- import searchRoutes from "./handlers/search";
13
- import uploadRoutes, { cleanupOrphanedChunks } from "./handlers/upload";
14
- import thumbnailRoutes from "./handlers/thumbnail";
15
- import indexRoutes from "./handlers/indexHandler";
16
- import { initIndex, closeIndex } from "./lib/index";
17
- import { scanAll } from "./lib/scanner";
18
-
19
- // Dev mode warning
20
- if (config.isDev) {
21
- console.log("╔════════════════════════════════════════════════════════════════╗");
22
- console.log("║ WARNING: DEV MODE - UID/GID OVERRIDES ENABLED ║");
23
- console.log("║ DO NOT USE IN PRODUCTION! ║");
24
- console.log(`║ DEV_UID_OVERRIDE: ${String(config.devUid ?? "not set").padEnd(43)}║`);
25
- console.log(`║ DEV_GID_OVERRIDE: ${String(config.devGid ?? "not set").padEnd(43)}║`);
26
- console.log("╚════════════════════════════════════════════════════════════════╝");
27
- }
28
-
29
- console.log(`[Filegate] ALLOWED_BASE_PATHS: ${config.allowedPaths.join(", ")}`);
30
- console.log(`[Filegate] MAX_UPLOAD_MB: ${config.maxUploadBytes / 1024 / 1024}`);
31
- console.log(`[Filegate] PORT: ${config.port}`);
32
-
33
- if (config.indexEnabled) {
34
- await initIndex(config.indexDatabaseUrl);
35
- console.log(`[Filegate] INDEX_DATABASE_URL: ${config.indexDatabaseUrl}`);
36
- }
37
-
38
- // Periodic disk cleanup for orphaned chunks (every 6h by default)
39
- setInterval(cleanupOrphanedChunks, config.diskCleanupIntervalMs);
40
- setTimeout(cleanupOrphanedChunks, 10_000); // Run 10s after startup
41
-
42
- if (config.indexEnabled) {
43
- const runRescan = async () => {
44
- try {
45
- const result = await scanAll();
46
- console.log(
47
- `[Filegate] Index rescan: scanned=${result.scanned} skipped=${result.skipped} added=${result.added} moved=${result.moved} removed=${result.removed} durationMs=${result.durationMs}`,
48
- );
49
- } catch (err) {
50
- console.error("[Filegate] Index rescan failed:", err);
51
- }
52
- };
53
-
54
- setInterval(runRescan, config.indexRescanIntervalMs);
55
- setTimeout(runRescan, 15_000);
56
-
57
- const shutdown = async () => {
58
- try {
59
- await closeIndex();
60
- } catch (err) {
61
- console.error("[Filegate] Index shutdown failed:", err);
62
- }
63
- };
64
-
65
- process.on("SIGINT", shutdown);
66
- process.on("SIGTERM", shutdown);
67
- }
68
-
69
- // Main app
70
- const app = new Hono();
71
-
72
- // Request logging
73
- app.use("*", logger());
74
-
75
- // Security headers
76
- app.use(
77
- "*",
78
- secureHeaders({
79
- xFrameOptions: "DENY",
80
- xContentTypeOptions: "nosniff",
81
- referrerPolicy: "strict-origin-when-cross-origin",
82
- crossOriginOpenerPolicy: "same-origin",
83
- crossOriginResourcePolicy: "same-origin",
84
- }),
85
- );
86
-
87
- // Health check (public)
88
- app.get("/health", (c) => c.text("OK"));
89
-
90
- // Protected routes
91
- const api = new Hono()
92
- .use("/*", bearerAuth({ token: config.token }))
93
- .route("/", filesRoutes)
94
- .route("/", searchRoutes)
95
- .route("/upload", uploadRoutes)
96
- .route("/thumbnail", thumbnailRoutes)
97
- .route("/index", indexRoutes);
98
-
99
- app.route("/files", api);
100
-
101
- // Generate OpenAPI spec
102
- const spec = await generateSpecs(app, openApiMeta);
103
- const llmsTxt = await createMarkdownFromOpenApi(JSON.stringify(spec));
104
-
105
- // Documentation endpoints (public)
106
- app.get("/openapi.json", (c) => c.json(spec));
107
- app.get("/docs", Scalar({ theme: "saturn", url: "/openapi.json" }));
108
- app.get("/llms.txt", (c) => c.text(llmsTxt));
109
-
110
- // 404 fallback
111
- app.notFound((c) => c.json({ error: "not found" }, 404));
112
-
113
- // Error handler
114
- app.onError((err, c) => {
115
- // HTTPException (from bearerAuth, etc.) - return the proper response
116
- if (err instanceof HTTPException) {
117
- return err.getResponse();
118
- }
119
-
120
- console.error("[Filegate] Error:", err);
121
- return c.json({ error: "internal error" }, 500);
122
- });
123
-
124
- export default {
125
- port: config.port,
126
- fetch: app.fetch,
127
- development: false,
128
- };
129
-
130
- console.log(`[Filegate] Listening on http://localhost:${config.port}`);
131
- console.log(`[Filegate] Docs: http://localhost:${config.port}/docs`);