@valentinkolb/filegate 2.3.6 → 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 (85) 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 -37
  74. package/src/handlers/files.ts +0 -513
  75. package/src/handlers/search.ts +0 -137
  76. package/src/handlers/thumbnail.ts +0 -148
  77. package/src/handlers/upload.ts +0 -379
  78. package/src/index.ts +0 -94
  79. package/src/lib/openapi.ts +0 -47
  80. package/src/lib/ownership.ts +0 -133
  81. package/src/lib/path.ts +0 -128
  82. package/src/lib/response.ts +0 -10
  83. package/src/lib/validator.ts +0 -21
  84. package/src/schemas.ts +0 -329
  85. package/src/utils.ts +0 -282
@@ -1,148 +0,0 @@
1
- import { Hono } from "hono";
2
- import { describeRoute } from "hono-openapi";
3
- import { stat } from "node:fs/promises";
4
- import sharp from "sharp";
5
- import { validatePath } from "../lib/path";
6
- import { jsonResponse, requiresAuth } from "../lib/openapi";
7
- import { v } from "../lib/validator";
8
- import { ImageThumbnailQuerySchema, ErrorSchema } from "../schemas";
9
-
10
- const app = new Hono();
11
-
12
- // Generate ETag from path, mtime, and thumbnail parameters
13
- const generateETag = (path: string, mtime: Date, params: string): string => {
14
- const hasher = new Bun.CryptoHasher("sha256");
15
- hasher.update(`${path}:${mtime.getTime()}:${params}`);
16
- return `"${hasher.digest("hex").slice(0, 16)}"`;
17
- };
18
-
19
- // Supported image MIME types
20
- const SUPPORTED_IMAGE_TYPES = new Set([
21
- "image/jpeg",
22
- "image/png",
23
- "image/webp",
24
- "image/avif",
25
- "image/tiff",
26
- "image/gif",
27
- "image/svg+xml",
28
- ]);
29
-
30
- // Format to MIME type mapping
31
- const FORMAT_MIME: Record<string, string> = {
32
- webp: "image/webp",
33
- jpeg: "image/jpeg",
34
- png: "image/png",
35
- avif: "image/avif",
36
- };
37
-
38
- // GET /thumbnail/image - Generate image thumbnail
39
- app.get(
40
- "/image",
41
- describeRoute({
42
- tags: ["Thumbnail"],
43
- summary: "Generate image thumbnail",
44
- description:
45
- "Generate a thumbnail from an image file on-the-fly using Sharp. " +
46
- "Supports JPEG, PNG, WebP, AVIF, TIFF, GIF, and SVG input formats.",
47
- ...requiresAuth,
48
- responses: {
49
- 200: {
50
- description: "Thumbnail image",
51
- content: {
52
- "image/webp": { schema: { type: "string", format: "binary" } },
53
- "image/jpeg": { schema: { type: "string", format: "binary" } },
54
- "image/png": { schema: { type: "string", format: "binary" } },
55
- "image/avif": { schema: { type: "string", format: "binary" } },
56
- },
57
- },
58
- 400: jsonResponse(ErrorSchema, "Invalid parameters or not an image"),
59
- 403: jsonResponse(ErrorSchema, "Forbidden"),
60
- 404: jsonResponse(ErrorSchema, "Not found"),
61
- },
62
- }),
63
- v("query", ImageThumbnailQuerySchema),
64
- async (c) => {
65
- const { path, width, height, fit, position, format, quality } = c.req.valid("query");
66
-
67
- // Validate path
68
- const result = await validatePath(path);
69
- if (!result.ok) return c.json({ error: result.error }, result.status);
70
-
71
- // Check if file exists and is a file
72
- const file = Bun.file(result.realPath);
73
- if (!(await file.exists())) {
74
- return c.json({ error: "file not found" }, 404);
75
- }
76
-
77
- // Check MIME type
78
- if (!SUPPORTED_IMAGE_TYPES.has(file.type)) {
79
- return c.json(
80
- { error: `unsupported image type: ${file.type}. Supported: ${[...SUPPORTED_IMAGE_TYPES].join(", ")}` },
81
- 400,
82
- );
83
- }
84
-
85
- // Get file stats for Last-Modified and ETag
86
- const fileStat = await stat(result.realPath);
87
- const lastModified = fileStat.mtime;
88
- const paramsKey = `${width}x${height}:${fit}:${position}:${format}:${quality}`;
89
- const etag = generateETag(result.realPath, lastModified, paramsKey);
90
-
91
- // Check If-None-Match (ETag)
92
- const ifNoneMatch = c.req.header("If-None-Match");
93
- if (ifNoneMatch === etag) {
94
- return new Response(null, { status: 304 });
95
- }
96
-
97
- // Check If-Modified-Since
98
- const ifModifiedSince = c.req.header("If-Modified-Since");
99
- if (ifModifiedSince) {
100
- const clientDate = new Date(ifModifiedSince);
101
- if (lastModified <= clientDate) {
102
- return new Response(null, { status: 304 });
103
- }
104
- }
105
-
106
- try {
107
- // Read file and process with Sharp
108
- const buffer = await file.arrayBuffer();
109
-
110
- let pipeline = sharp(Buffer.from(buffer)).resize(width, height, {
111
- fit,
112
- position,
113
- });
114
-
115
- // Apply format and quality
116
- switch (format) {
117
- case "webp":
118
- pipeline = pipeline.webp({ quality });
119
- break;
120
- case "jpeg":
121
- pipeline = pipeline.jpeg({ quality });
122
- break;
123
- case "png":
124
- pipeline = pipeline.png({ quality });
125
- break;
126
- case "avif":
127
- pipeline = pipeline.avif({ quality });
128
- break;
129
- }
130
-
131
- const thumbnail = await pipeline.toBuffer();
132
-
133
- return new Response(thumbnail, {
134
- headers: {
135
- "Content-Type": FORMAT_MIME[format],
136
- "Cache-Control": "public, max-age=31536000, immutable",
137
- "Last-Modified": lastModified.toUTCString(),
138
- ETag: etag,
139
- },
140
- });
141
- } catch (err) {
142
- console.error("[Filegate] Thumbnail error:", err);
143
- return c.json({ error: "failed to generate thumbnail" }, 500);
144
- }
145
- },
146
- );
147
-
148
- export default app;
@@ -1,379 +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 } 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 {
11
- UploadStartBodySchema,
12
- UploadStartResponseSchema,
13
- UploadChunkResponseSchema,
14
- ErrorSchema,
15
- UploadChunkHeadersSchema,
16
- } from "../schemas";
17
- import { config } from "../config";
18
-
19
- const app = new Hono();
20
-
21
- // Deterministic upload ID from path + filename + checksum
22
- const computeUploadId = (path: string, filename: string, checksum: string): string => {
23
- const hasher = new Bun.CryptoHasher("sha256");
24
- hasher.update(`${path}:${filename}:${checksum}`);
25
- return hasher.digest("hex").slice(0, 16);
26
- };
27
-
28
- // Chunk storage paths
29
- const chunksDir = (id: string) => join(config.uploadTempDir, id);
30
- const chunkPath = (id: string, idx: number) => join(chunksDir(id), String(idx));
31
- const metaPath = (id: string) => join(chunksDir(id), "meta.json");
32
-
33
- type UploadMeta = {
34
- uploadId: string;
35
- path: string;
36
- filename: string;
37
- size: number;
38
- checksum: string;
39
- chunkSize: number;
40
- totalChunks: number;
41
- ownership: Ownership | null;
42
- createdAt: number; // Unix timestamp for expiry check
43
- };
44
-
45
- const saveMeta = async (meta: UploadMeta): Promise<void> => {
46
- await mkdir(chunksDir(meta.uploadId), { recursive: true });
47
- await writeFile(metaPath(meta.uploadId), JSON.stringify(meta));
48
- };
49
-
50
- const loadMeta = async (id: string): Promise<UploadMeta | null> => {
51
- try {
52
- const data = await readFile(metaPath(id), "utf-8");
53
- return JSON.parse(data) as UploadMeta;
54
- } catch {
55
- return null;
56
- }
57
- };
58
-
59
- const refreshExpiry = async (id: string, meta: UploadMeta): Promise<void> => {
60
- // Update createdAt to extend expiry
61
- meta.createdAt = Date.now();
62
- await saveMeta(meta);
63
- };
64
-
65
- // Get uploaded chunks from filesystem
66
- const getUploadedChunks = async (id: string): Promise<number[]> => {
67
- try {
68
- const files = await readdir(chunksDir(id));
69
- return files
70
- .filter((f) => f !== "meta.json")
71
- .map((f) => parseInt(f, 10))
72
- .filter((n) => !isNaN(n))
73
- .sort((a, b) => a - b);
74
- } catch {
75
- return [];
76
- }
77
- };
78
-
79
- const cleanupUpload = async (id: string): Promise<void> => {
80
- await rm(chunksDir(id), { recursive: true }).catch(() => {});
81
- };
82
-
83
- const assembleFile = async (meta: UploadMeta): Promise<string | null> => {
84
- // Use semaphore to prevent concurrent assembly of the same upload
85
- const semaphore = getSemaphore(`assemble:${meta.uploadId}`, 1);
86
- await semaphore.acquire();
87
-
88
- try {
89
- // Check if assembly was already completed by another request while we waited
90
- const chunks = await getUploadedChunks(meta.uploadId);
91
- if (chunks.length === 0) {
92
- // Chunks were cleaned up - assembly was already completed
93
- return null;
94
- }
95
- if (chunks.length !== meta.totalChunks) return "missing chunks";
96
-
97
- // Verify all expected chunk indices are present (0 to totalChunks-1)
98
- const expectedChunks = Array.from({ length: meta.totalChunks }, (_, i) => i);
99
- const missingChunks = expectedChunks.filter((i) => !chunks.includes(i));
100
- if (missingChunks.length > 0) {
101
- return `missing chunk indices: ${missingChunks.join(", ")}`;
102
- }
103
-
104
- const fullPath = join(meta.path, meta.filename);
105
- const pathResult = await validatePath(fullPath);
106
- if (!pathResult.ok) return pathResult.error;
107
-
108
- await mkdir(dirname(pathResult.realPath), { recursive: true });
109
-
110
- const hasher = new Bun.CryptoHasher("sha256");
111
- const file = Bun.file(pathResult.realPath);
112
- const writer = file.writer();
113
-
114
- try {
115
- for (let i = 0; i < meta.totalChunks; i++) {
116
- const chunkFilePath = chunkPath(meta.uploadId, i);
117
- const chunkFile = Bun.file(chunkFilePath);
118
-
119
- // Verify chunk exists before reading
120
- if (!(await chunkFile.exists())) {
121
- writer.end();
122
- await rm(pathResult.realPath).catch(() => {});
123
- return `chunk ${i} not found during assembly`;
124
- }
125
-
126
- // Read chunk as buffer (more reliable than streaming)
127
- const data = new Uint8Array(await chunkFile.arrayBuffer());
128
- hasher.update(data);
129
- writer.write(data);
130
- }
131
- await writer.end();
132
- } catch (e) {
133
- writer.end();
134
- await rm(pathResult.realPath).catch(() => {});
135
- throw e;
136
- }
137
-
138
- const finalChecksum = `sha256:${hasher.digest("hex")}`;
139
- if (finalChecksum !== meta.checksum) {
140
- await rm(pathResult.realPath).catch(() => {});
141
- return `checksum mismatch: expected ${meta.checksum}, got ${finalChecksum}`;
142
- }
143
-
144
- const ownershipError = await applyOwnership(pathResult.realPath, meta.ownership);
145
- if (ownershipError) {
146
- await rm(pathResult.realPath).catch(() => {});
147
- return ownershipError;
148
- }
149
-
150
- await cleanupUpload(meta.uploadId);
151
- return null;
152
- } finally {
153
- semaphore.release();
154
- }
155
- };
156
-
157
- // POST /upload/start
158
- app.post(
159
- "/start",
160
- describeRoute({
161
- tags: ["Upload"],
162
- summary: "Start or resume chunked upload",
163
- description: "Initialize a new upload or get status of existing upload with same checksum",
164
- ...requiresAuth,
165
- responses: {
166
- 200: jsonResponse(UploadStartResponseSchema, "Upload initialized or resumed"),
167
- 400: jsonResponse(ErrorSchema, "Bad request"),
168
- 403: jsonResponse(ErrorSchema, "Forbidden"),
169
- },
170
- }),
171
- v("json", UploadStartBodySchema),
172
- async (c) => {
173
- const body = c.req.valid("json");
174
-
175
- // Validate size limits
176
- if (body.size > config.maxUploadBytes) {
177
- return c.json({ error: `file size exceeds maximum (${config.maxUploadBytes / 1024 / 1024}MB)` }, 413);
178
- }
179
- if (body.chunkSize > config.maxChunkBytes) {
180
- return c.json({ error: `chunk size exceeds maximum (${config.maxChunkBytes / 1024 / 1024}MB)` }, 400);
181
- }
182
-
183
- const fullPath = join(body.path, body.filename);
184
-
185
- // Build ownership from body
186
- const ownership: Ownership | null =
187
- body.ownerUid != null && body.ownerGid != null && body.mode
188
- ? {
189
- uid: body.ownerUid,
190
- gid: body.ownerGid,
191
- mode: parseInt(body.mode, 8),
192
- dirMode: body.dirMode ? parseInt(body.dirMode, 8) : undefined,
193
- }
194
- : null;
195
-
196
- // Validate path and create parent directories with ownership
197
- const pathResult = await validatePath(fullPath, { createParents: true, ownership });
198
- if (!pathResult.ok) return c.json({ error: pathResult.error }, pathResult.status);
199
-
200
- // Deterministic upload ID - same file/path/checksum = same ID (enables resume)
201
- const uploadId = computeUploadId(body.path, body.filename, body.checksum);
202
-
203
- // Check for existing upload (resume)
204
- const existingMeta = await loadMeta(uploadId);
205
- if (existingMeta) {
206
- // Refresh expiry on resume
207
- await refreshExpiry(uploadId, existingMeta);
208
- // Get chunks from filesystem
209
- const uploadedChunks = await getUploadedChunks(uploadId);
210
- return c.json({
211
- uploadId,
212
- totalChunks: existingMeta.totalChunks,
213
- chunkSize: existingMeta.chunkSize,
214
- uploadedChunks,
215
- completed: false as const,
216
- });
217
- }
218
-
219
- // New upload
220
- const chunkSize = body.chunkSize;
221
- const totalChunks = Math.ceil(body.size / chunkSize);
222
-
223
- const meta: UploadMeta = {
224
- uploadId,
225
- path: body.path,
226
- filename: body.filename,
227
- size: body.size,
228
- checksum: body.checksum,
229
- chunkSize,
230
- totalChunks,
231
- ownership,
232
- createdAt: Date.now(),
233
- };
234
-
235
- await saveMeta(meta);
236
-
237
- return c.json({
238
- uploadId,
239
- totalChunks,
240
- chunkSize,
241
- uploadedChunks: [],
242
- completed: false as const,
243
- });
244
- },
245
- );
246
-
247
- // POST /upload/chunk
248
- app.post(
249
- "/chunk",
250
- describeRoute({
251
- tags: ["Upload"],
252
- summary: "Upload a chunk",
253
- description: "Upload a single chunk. Auto-completes when all chunks received.",
254
- ...requiresAuth,
255
- responses: {
256
- 200: jsonResponse(UploadChunkResponseSchema, "Chunk received"),
257
- 400: jsonResponse(ErrorSchema, "Bad request"),
258
- 404: jsonResponse(ErrorSchema, "Upload not found"),
259
- },
260
- }),
261
- v("header", UploadChunkHeadersSchema),
262
- async (c) => {
263
- const headers = c.req.valid("header");
264
- const uploadId = headers["x-upload-id"];
265
- const chunkIndex = headers["x-chunk-index"];
266
- const chunkChecksum = headers["x-chunk-checksum"];
267
-
268
- const meta = await loadMeta(uploadId);
269
- if (!meta) return c.json({ error: "upload not found" }, 404);
270
-
271
- if (chunkIndex >= meta.totalChunks) {
272
- return c.json({ error: `chunk index ${chunkIndex} exceeds total ${meta.totalChunks}` }, 400);
273
- }
274
-
275
- // Read body as ArrayBuffer (more reliable than streaming)
276
- let bodyBuffer: ArrayBuffer;
277
- try {
278
- bodyBuffer = await c.req.arrayBuffer();
279
- } catch {
280
- return c.json({ error: "failed to read request body" }, 400);
281
- }
282
-
283
- if (bodyBuffer.byteLength === 0) {
284
- return c.json({ error: "missing body" }, 400);
285
- }
286
-
287
- if (bodyBuffer.byteLength > config.maxChunkBytes) {
288
- return c.json({ error: `chunk size exceeds maximum (${config.maxChunkBytes / 1024 / 1024}MB)` }, 413);
289
- }
290
-
291
- const bodyData = new Uint8Array(bodyBuffer);
292
- const hasher = new Bun.CryptoHasher("sha256");
293
- hasher.update(bodyData);
294
-
295
- // Write chunk to temporary file
296
- const tempChunkPath = chunkPath(uploadId, chunkIndex) + ".tmp";
297
- await mkdir(chunksDir(uploadId), { recursive: true });
298
-
299
- try {
300
- await Bun.write(tempChunkPath, bodyData);
301
- } catch (e) {
302
- await rm(tempChunkPath).catch(() => {});
303
- throw e;
304
- }
305
-
306
- // Verify checksum if provided
307
- if (chunkChecksum) {
308
- const computed = `sha256:${hasher.digest("hex")}`;
309
- if (computed !== chunkChecksum) {
310
- await rm(tempChunkPath).catch(() => {});
311
- return c.json({ error: `chunk checksum mismatch: expected ${chunkChecksum}, got ${computed}` }, 400);
312
- }
313
- }
314
-
315
- // Move temp file to final location (atomic rename, no memory copy)
316
- const finalChunkPath = chunkPath(uploadId, chunkIndex);
317
- await rename(tempChunkPath, finalChunkPath);
318
-
319
- // Get uploaded chunks from filesystem
320
- const uploadedChunks = await getUploadedChunks(uploadId);
321
-
322
- if (uploadedChunks.length === meta.totalChunks) {
323
- const assembleError = await assembleFile(meta);
324
- if (assembleError) return c.json({ error: assembleError }, 500);
325
-
326
- const fullPath = join(meta.path, meta.filename);
327
- const file = Bun.file(fullPath);
328
- const s = await stat(fullPath);
329
-
330
- return c.json({
331
- completed: true as const,
332
- file: {
333
- name: meta.filename,
334
- path: fullPath,
335
- type: "file" as const,
336
- size: s.size,
337
- mtime: s.mtime.toISOString(),
338
- isHidden: meta.filename.startsWith("."),
339
- checksum: meta.checksum,
340
- mimeType: file.type,
341
- },
342
- });
343
- }
344
-
345
- return c.json({
346
- chunkIndex,
347
- uploadedChunks,
348
- completed: false as const,
349
- });
350
- },
351
- );
352
-
353
- // Cleanup expired upload directories
354
- export const cleanupOrphanedChunks = async () => {
355
- try {
356
- const dirs = await readdir(config.uploadTempDir);
357
- let cleaned = 0;
358
- const now = Date.now();
359
- const expiryMs = config.uploadExpirySecs * 1000;
360
-
361
- for (const dir of dirs) {
362
- const meta = await loadMeta(dir);
363
-
364
- // Remove if no meta or expired
365
- if (!meta || now - meta.createdAt > expiryMs) {
366
- await rm(chunksDir(dir), { recursive: true }).catch(() => {});
367
- cleaned++;
368
- }
369
- }
370
-
371
- if (cleaned > 0) {
372
- console.log(`[Filegate] Cleaned up ${cleaned} expired upload${cleaned === 1 ? "" : "s"}`);
373
- }
374
- } catch {
375
- // Directory doesn't exist yet
376
- }
377
- };
378
-
379
- export default app;
package/src/index.ts DELETED
@@ -1,94 +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
-
16
- // Dev mode warning
17
- if (config.isDev) {
18
- console.log("╔════════════════════════════════════════════════════════════════╗");
19
- console.log("║ WARNING: DEV MODE - UID/GID OVERRIDES ENABLED ║");
20
- console.log("║ DO NOT USE IN PRODUCTION! ║");
21
- console.log(`║ DEV_UID_OVERRIDE: ${String(config.devUid ?? "not set").padEnd(43)}║`);
22
- console.log(`║ DEV_GID_OVERRIDE: ${String(config.devGid ?? "not set").padEnd(43)}║`);
23
- console.log("╚════════════════════════════════════════════════════════════════╝");
24
- }
25
-
26
- console.log(`[Filegate] ALLOWED_BASE_PATHS: ${config.allowedPaths.join(", ")}`);
27
- console.log(`[Filegate] MAX_UPLOAD_MB: ${config.maxUploadBytes / 1024 / 1024}`);
28
- console.log(`[Filegate] PORT: ${config.port}`);
29
-
30
- // Periodic disk cleanup for orphaned chunks (every 6h by default)
31
- setInterval(cleanupOrphanedChunks, config.diskCleanupIntervalMs);
32
- setTimeout(cleanupOrphanedChunks, 10_000); // Run 10s after startup
33
-
34
- // Main app
35
- const app = new Hono();
36
-
37
- // Request logging
38
- app.use("*", logger());
39
-
40
- // Security headers
41
- app.use(
42
- "*",
43
- secureHeaders({
44
- xFrameOptions: "DENY",
45
- xContentTypeOptions: "nosniff",
46
- referrerPolicy: "strict-origin-when-cross-origin",
47
- crossOriginOpenerPolicy: "same-origin",
48
- crossOriginResourcePolicy: "same-origin",
49
- }),
50
- );
51
-
52
- // Health check (public)
53
- app.get("/health", (c) => c.text("OK"));
54
-
55
- // Protected routes
56
- const api = new Hono()
57
- .use("/*", bearerAuth({ token: config.token }))
58
- .route("/", filesRoutes)
59
- .route("/", searchRoutes)
60
- .route("/upload", uploadRoutes)
61
- .route("/thumbnail", thumbnailRoutes);
62
-
63
- app.route("/files", api);
64
-
65
- // Generate OpenAPI spec
66
- const spec = await generateSpecs(app, openApiMeta);
67
- const llmsTxt = await createMarkdownFromOpenApi(JSON.stringify(spec));
68
-
69
- // Documentation endpoints (public)
70
- app.get("/openapi.json", (c) => c.json(spec));
71
- app.get("/docs", Scalar({ theme: "saturn", url: "/openapi.json" }));
72
- app.get("/llms.txt", (c) => c.text(llmsTxt));
73
-
74
- // 404 fallback
75
- app.notFound((c) => c.json({ error: "not found" }, 404));
76
-
77
- // Error handler
78
- app.onError((err, c) => {
79
- // HTTPException (from bearerAuth, etc.) - return the proper response
80
- if (err instanceof HTTPException) {
81
- return err.getResponse();
82
- }
83
-
84
- console.error("[Filegate] Error:", err);
85
- return c.json({ error: "internal error" }, 500);
86
- });
87
-
88
- export default {
89
- port: config.port,
90
- fetch: app.fetch,
91
- };
92
-
93
- console.log(`[Filegate] Listening on http://localhost:${config.port}`);
94
- console.log(`[Filegate] Docs: http://localhost:${config.port}/docs`);
@@ -1,47 +0,0 @@
1
- import { resolver, type GenerateSpecOptions } from "hono-openapi";
2
- import type { ZodType } from "zod";
3
- import { config } from "../config";
4
-
5
- /** JSON response helper for OpenAPI docs */
6
- export const jsonResponse = <T extends ZodType>(schema: T, description: string) => ({
7
- description,
8
- content: { "application/json": { schema: resolver(schema) } },
9
- });
10
-
11
- /** Binary/stream response helper */
12
- export const binaryResponse = (mimeType: string, description: string) => ({
13
- description,
14
- content: { [mimeType]: { schema: { type: "string" as const, format: "binary" } } },
15
- });
16
-
17
- /** OpenAPI spec metadata */
18
- export const openApiMeta: Partial<GenerateSpecOptions> = {
19
- documentation: {
20
- info: {
21
- title: "File Proxy API",
22
- version: "1.0.0",
23
- description: "Secure file proxy with streaming uploads/downloads and resumable chunked uploads",
24
- },
25
- servers: [{ url: `http://localhost:${config.port}`, description: "File Proxy Server" }],
26
- tags: [
27
- { name: "Health", description: "Health check endpoint" },
28
- { name: "Files", description: "File operations (info, download, upload, mkdir, move, copy, delete)" },
29
- { name: "Search", description: "File search with glob patterns" },
30
- { name: "Upload", description: "Resumable chunked uploads" },
31
- ],
32
- components: {
33
- securitySchemes: {
34
- bearerAuth: {
35
- type: "http",
36
- scheme: "bearer",
37
- description: "Bearer token authentication",
38
- },
39
- },
40
- },
41
- },
42
- };
43
-
44
- /** Security requirement for authenticated routes */
45
- export const requiresAuth = {
46
- security: [{ bearerAuth: [] as string[] }],
47
- };