@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
package/src/handlers/upload.ts
DELETED
|
@@ -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`);
|