@valentinkolb/filegate 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valentin Kolb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,429 @@
1
+ # Filegate
2
+
3
+ A secure, high-performance file proxy server built with Bun + Hono + Zod.
4
+
5
+ Features:
6
+ - Streaming uploads/downloads
7
+ - Directory download as ZIP
8
+ - Resumable chunked uploads with SHA-256 verification
9
+ - Glob-based file search
10
+ - Type-safe client API with config objects
11
+ - Browser-compatible utils for chunked uploads
12
+ - OpenAPI documentation with Scalar UI
13
+ - LLM-friendly markdown docs at `/llms.txt`
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ export FILE_PROXY_TOKEN=your-secret-token
19
+ export ALLOWED_BASE_PATHS=/export/homes,/export/groups
20
+
21
+ bun run src/index.ts
22
+ ```
23
+
24
+ ## Documentation
25
+
26
+ - **Scalar UI:** http://localhost:4000/docs
27
+ - **OpenAPI JSON:** http://localhost:4000/openapi.json
28
+ - **LLM Markdown:** http://localhost:4000/llms.txt
29
+
30
+ ## Configuration
31
+
32
+ | Variable | Required | Default | Description |
33
+ |----------|----------|---------|-------------|
34
+ | `FILE_PROXY_TOKEN` | Yes | - | Bearer token for authentication |
35
+ | `ALLOWED_BASE_PATHS` | Yes | - | Comma-separated allowed paths |
36
+ | `REDIS_URL` | No | localhost:6379 | Redis connection URL (for chunked uploads) |
37
+ | `PORT` | No | 4000 | Server port |
38
+
39
+ **Size Limits:**
40
+
41
+ | Variable | Default | Description |
42
+ |----------|---------|-------------|
43
+ | `MAX_UPLOAD_MB` | 500 | Maximum file size for uploads (simple + chunked) |
44
+ | `MAX_DOWNLOAD_MB` | 5000 | Maximum file/directory size for downloads |
45
+ | `MAX_CHUNK_SIZE_MB` | 50 | Maximum chunk size (server rejects larger chunks) |
46
+
47
+ **Search:**
48
+
49
+ | Variable | Default | Description |
50
+ |----------|---------|-------------|
51
+ | `SEARCH_MAX_RESULTS` | 100 | Max files returned by search |
52
+ | `SEARCH_MAX_RECURSIVE_WILDCARDS` | 10 | Max `**` wildcards allowed in glob patterns (prevents DoS) |
53
+
54
+ **Other:**
55
+
56
+ | Variable | Default | Description |
57
+ |----------|---------|-------------|
58
+ | `UPLOAD_EXPIRY_HOURS` | 24 | Chunked upload expiry (resets on each chunk) |
59
+ | `DISK_CLEANUP_INTERVAL_HOURS` | 6 | Interval to clean orphaned chunk files |
60
+ | `DEV_UID_OVERRIDE` | - | Override file ownership UID (dev mode only) |
61
+ | `DEV_GID_OVERRIDE` | - | Override file ownership GID (dev mode only) |
62
+
63
+ ## API Endpoints
64
+
65
+ All `/files/*` endpoints require `Authorization: Bearer <token>`.
66
+
67
+ | Method | Path | Description |
68
+ |--------|------|-------------|
69
+ | GET | `/health` | Health check (public) |
70
+ | GET | `/docs` | Scalar API docs (public) |
71
+ | GET | `/openapi.json` | OpenAPI spec (public) |
72
+ | GET | `/llms.txt` | LLM-friendly docs (public) |
73
+ | GET | `/files/info` | File/directory info |
74
+ | GET | `/files/content` | Download file or directory (ZIP) |
75
+ | PUT | `/files/content` | Upload file |
76
+ | POST | `/files/mkdir` | Create directory |
77
+ | DELETE | `/files/delete` | Delete file/directory |
78
+ | POST | `/files/move` | Move (same basepath) |
79
+ | POST | `/files/copy` | Copy (same basepath) |
80
+ | GET | `/files/search` | Glob search |
81
+ | POST | `/files/upload/start` | Start/resume chunked upload |
82
+ | POST | `/files/upload/chunk` | Upload chunk |
83
+
84
+ ## Examples
85
+
86
+ ### Get Directory Info
87
+ ```bash
88
+ curl -H "Authorization: Bearer $TOKEN" \
89
+ "http://localhost:4000/files/info?path=/export/homes/alice&showHidden=false"
90
+ ```
91
+
92
+ ### Upload File
93
+ ```bash
94
+ curl -X PUT \
95
+ -H "Authorization: Bearer $TOKEN" \
96
+ -H "X-File-Path: /export/homes/alice" \
97
+ -H "X-File-Name: report.pdf" \
98
+ -H "X-Owner-UID: 1000" \
99
+ -H "X-Owner-GID: 1000" \
100
+ -H "X-File-Mode: 600" \
101
+ --data-binary @report.pdf \
102
+ "http://localhost:4000/files/content"
103
+ ```
104
+
105
+ ### Search Files
106
+ ```bash
107
+ curl -H "Authorization: Bearer $TOKEN" \
108
+ "http://localhost:4000/files/search?paths=/export/homes/alice,/export/groups/team&pattern=**/*.pdf"
109
+ ```
110
+
111
+ ### Chunked Upload
112
+
113
+ ```bash
114
+ # 1. Start upload
115
+ curl -X POST \
116
+ -H "Authorization: Bearer $TOKEN" \
117
+ -H "Content-Type: application/json" \
118
+ -d '{
119
+ "path": "/export/homes/alice",
120
+ "filename": "large.zip",
121
+ "size": 104857600,
122
+ "checksum": "sha256:abc123...",
123
+ "chunkSize": 10485760
124
+ }' \
125
+ "http://localhost:4000/files/upload/start"
126
+
127
+ # 2. Upload chunks
128
+ curl -X POST \
129
+ -H "Authorization: Bearer $TOKEN" \
130
+ -H "X-Upload-Id: <uploadId>" \
131
+ -H "X-Chunk-Index: 0" \
132
+ -H "X-Chunk-Checksum: sha256:def456..." \
133
+ --data-binary @chunk0.bin \
134
+ "http://localhost:4000/files/upload/chunk"
135
+
136
+ # Repeat for all chunks - auto-completes on last chunk
137
+ ```
138
+
139
+ ## Client API
140
+
141
+ Type-safe client for server-side usage. All methods use config objects for better readability.
142
+
143
+ ### Installation
144
+
145
+ ```typescript
146
+ import { Filegate } from "@valentinkolb/filegate/client";
147
+ import { chunks, formatBytes } from "@valentinkolb/filegate/utils";
148
+ ```
149
+
150
+ ### Default Instance (via env vars)
151
+
152
+ Set `FILEGATE_URL` and `FILEGATE_TOKEN`, then import the pre-configured instance:
153
+
154
+ ```typescript
155
+ import { filegate } from "@valentinkolb/filegate/client";
156
+
157
+ const info = await filegate.info({ path: "/export/homes/alice" });
158
+ ```
159
+
160
+ ### Custom Instance
161
+
162
+ ```typescript
163
+ import { Filegate } from "@valentinkolb/filegate/client";
164
+
165
+ const client = new Filegate({
166
+ url: "http://localhost:4000",
167
+ token: "your-token",
168
+ });
169
+ ```
170
+
171
+ ### Client Methods
172
+
173
+ ```typescript
174
+ // Get file/directory info
175
+ const info = await client.info({ path: "/export/homes/alice", showHidden: true });
176
+
177
+ // Download file (returns Response with streaming body)
178
+ const file = await client.download({ path: "/export/homes/alice/report.pdf" });
179
+ if (file.ok) {
180
+ const text = await file.data.text();
181
+ const buffer = await file.data.arrayBuffer();
182
+ // Or stream directly: file.data.body
183
+ }
184
+
185
+ // Download directory as ZIP
186
+ const zip = await client.download({ path: "/export/homes/alice/documents" });
187
+
188
+ // Upload file (simple)
189
+ await client.upload.single({
190
+ path: "/export/homes/alice",
191
+ filename: "report.pdf",
192
+ data: fileData,
193
+ uid: 1000,
194
+ gid: 1000,
195
+ mode: "644",
196
+ });
197
+
198
+ // Upload file (chunked) - for large files
199
+ const startResult = await client.upload.chunked.start({
200
+ path: "/export/homes/alice",
201
+ filename: "large.zip",
202
+ size: file.size,
203
+ checksum: "sha256:...",
204
+ chunkSize: 5 * 1024 * 1024,
205
+ uid: 1000,
206
+ gid: 1000,
207
+ });
208
+
209
+ // Send chunks
210
+ await client.upload.chunked.send({
211
+ uploadId: startResult.data.uploadId,
212
+ index: 0,
213
+ data: chunkData,
214
+ checksum: "sha256:...",
215
+ });
216
+
217
+ // Create directory
218
+ await client.mkdir({ path: "/export/homes/alice/new-folder", mode: "750" });
219
+
220
+ // Move/copy (within same basepath)
221
+ await client.move({ from: "/export/homes/alice/old.txt", to: "/export/homes/alice/new.txt" });
222
+ await client.copy({ from: "/export/homes/alice/file.txt", to: "/export/homes/alice/backup.txt" });
223
+
224
+ // Delete
225
+ await client.delete({ path: "/export/homes/alice/trash" });
226
+
227
+ // Search with glob patterns
228
+ const results = await client.glob({
229
+ paths: ["/export/homes/alice", "/export/groups/team"],
230
+ pattern: "**/*.pdf",
231
+ showHidden: false,
232
+ limit: 100,
233
+ });
234
+ ```
235
+
236
+ ## Browser Utils
237
+
238
+ Browser-compatible utilities for chunked uploads. Framework-agnostic with reactive state management.
239
+
240
+ ### Basic Usage
241
+
242
+ ```typescript
243
+ import { chunks, formatBytes } from "@valentinkolb/filegate/utils";
244
+
245
+ // Prepare upload (calculates checksum and chunk info)
246
+ const upload = await chunks.prepare({ file, chunkSize: 5 * 1024 * 1024 });
247
+
248
+ // Access properties
249
+ upload.file // Original File/Blob
250
+ upload.fileSize // Total size in bytes
251
+ upload.chunkSize // Chunk size in bytes
252
+ upload.totalChunks // Number of chunks
253
+ upload.checksum // "sha256:..." of entire file
254
+
255
+ // Format bytes for display
256
+ formatBytes({ bytes: upload.fileSize }); // "52.43 MB"
257
+ ```
258
+
259
+ ### State Management
260
+
261
+ ```typescript
262
+ // Subscribe to state changes (framework-agnostic)
263
+ const unsubscribe = upload.subscribe((state) => {
264
+ console.log(`${state.percent}% - ${state.status}`);
265
+ // state: { uploaded: number, total: number, percent: number, status: "pending" | "uploading" | "completed" | "error" }
266
+ });
267
+
268
+ // Mark chunk as completed
269
+ upload.complete({ index: 0 });
270
+
271
+ // Reset state
272
+ upload.reset();
273
+
274
+ // Unsubscribe when done
275
+ unsubscribe();
276
+ ```
277
+
278
+ ### Chunk Access
279
+
280
+ ```typescript
281
+ // Get specific chunk (sync, returns Blob)
282
+ const chunk = upload.get({ index: 0 });
283
+
284
+ // Calculate chunk checksum
285
+ const hash = await upload.hash({ data: chunk });
286
+
287
+ // Iterate over all chunks
288
+ for await (const { index, data, total } of upload) {
289
+ console.log(`Chunk ${index + 1}/${total}`);
290
+ }
291
+ ```
292
+
293
+ ### Upload Helpers
294
+
295
+ ```typescript
296
+ // Send single chunk with retry
297
+ await upload.send({
298
+ index: 0,
299
+ retries: 3,
300
+ fn: async ({ index, data }) => {
301
+ await fetch("/api/upload/chunk", {
302
+ method: "POST",
303
+ headers: { "X-Chunk-Index": String(index) },
304
+ body: data,
305
+ });
306
+ },
307
+ });
308
+
309
+ // Send all chunks (with skip for resume, concurrency, retries)
310
+ await upload.sendAll({
311
+ skip: [0, 1], // Already uploaded chunks (from resume)
312
+ retries: 3,
313
+ concurrency: 3, // Parallel uploads
314
+ fn: async ({ index, data }) => {
315
+ await fetch("/api/upload/chunk", { ... });
316
+ },
317
+ });
318
+ ```
319
+
320
+ ### Complete Example (Browser)
321
+
322
+ ```typescript
323
+ import { chunks } from "@valentinkolb/filegate/utils";
324
+
325
+ async function uploadFile(file: File, targetPath: string, onProgress?: (state) => void) {
326
+ const upload = await chunks.prepare({ file, chunkSize: 5 * 1024 * 1024 });
327
+
328
+ if (onProgress) upload.subscribe(onProgress);
329
+
330
+ // 1. Start/Resume upload
331
+ const { uploadId, uploadedChunks, completed } = await fetch("/api/upload/start", {
332
+ method: "POST",
333
+ headers: { "Content-Type": "application/json" },
334
+ body: JSON.stringify({
335
+ path: targetPath,
336
+ filename: file.name,
337
+ size: upload.fileSize,
338
+ checksum: upload.checksum,
339
+ chunkSize: upload.chunkSize,
340
+ }),
341
+ }).then(r => r.json());
342
+
343
+ if (completed) return; // Already done
344
+
345
+ // 2. Upload all chunks (skipping already uploaded)
346
+ await upload.sendAll({
347
+ skip: uploadedChunks,
348
+ retries: 3,
349
+ fn: async ({ index, data }) => {
350
+ await fetch("/api/upload/chunk", {
351
+ method: "POST",
352
+ headers: {
353
+ "X-Upload-Id": uploadId,
354
+ "X-Chunk-Index": String(index),
355
+ "X-Chunk-Checksum": await upload.hash({ data }),
356
+ },
357
+ body: data,
358
+ });
359
+ },
360
+ });
361
+ }
362
+
363
+ // Usage with React
364
+ function UploadButton() {
365
+ const [progress, setProgress] = useState({ percent: 0, status: "pending" });
366
+
367
+ const handleUpload = async (file: File) => {
368
+ await uploadFile(file, "/documents", setProgress);
369
+ };
370
+
371
+ return <div>{progress.percent}% - {progress.status}</div>;
372
+ }
373
+ ```
374
+
375
+ ### Streaming Proxy (Server-Side)
376
+
377
+ The download response streams directly - perfect for proxying without buffering:
378
+
379
+ ```typescript
380
+ // In your proxy server (e.g., with Hono, Express, etc.)
381
+ app.get("/api/files/download", async (c) => {
382
+ const path = c.req.query("path");
383
+
384
+ // Get streaming response from Filegate
385
+ const response = await client.download({ path });
386
+
387
+ if (!response.ok) {
388
+ return c.json({ error: response.error }, response.status);
389
+ }
390
+
391
+ // Stream directly to client - no buffering!
392
+ return new Response(response.data.body, {
393
+ headers: {
394
+ "Content-Type": response.data.headers.get("Content-Type") || "application/octet-stream",
395
+ "Content-Disposition": response.data.headers.get("Content-Disposition") || "",
396
+ },
397
+ });
398
+ });
399
+ ```
400
+
401
+ ## Security
402
+
403
+ - Path validation with symlink protection
404
+ - Base path escape prevention
405
+ - Same-basepath enforcement for move/copy
406
+ - SHA-256 checksum verification
407
+ - Configurable upload/download size limits
408
+ - Glob pattern limits (max length 500 chars, configurable recursive wildcard limit)
409
+ - Security headers (X-Frame-Options, X-Content-Type-Options, etc.)
410
+
411
+ ## Testing
412
+
413
+ ```bash
414
+ # Run unit tests
415
+ bun run test:unit
416
+
417
+ # Run integration tests (requires Docker)
418
+ bun run test:integration:run
419
+
420
+ # Run all tests
421
+ bun run test:all
422
+ ```
423
+
424
+ ## Tech Stack
425
+
426
+ - **Runtime:** Bun
427
+ - **Framework:** Hono
428
+ - **Validation:** Zod
429
+ - **Docs:** hono-openapi + Scalar
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@valentinkolb/filegate",
3
+ "version": "0.0.1",
4
+ "description": "Secure, high-performance file proxy server with streaming uploads, chunked uploads, and TAR downloads",
5
+ "module": "index.ts",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/valentinkolb/filegate.git"
11
+ },
12
+ "keywords": [
13
+ "file-server",
14
+ "file-proxy",
15
+ "chunked-upload",
16
+ "streaming",
17
+ "bun",
18
+ "hono"
19
+ ],
20
+ "files": [
21
+ "src",
22
+ "package.json",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "test": "bun test --preload ./tests/setup.ts tests/lib tests/schemas.test.ts tests/config.test.ts tests/utils.test.ts",
27
+ "test:watch": "bun test --preload ./tests/setup.ts --watch",
28
+ "test:unit": "bun test --preload ./tests/setup.ts tests/lib tests/schemas.test.ts tests/config.test.ts tests/utils.test.ts",
29
+ "test:integration": "bun test tests/integration",
30
+ "test:integration:up": "docker compose -f compose.test.yml up -d --build --wait",
31
+ "test:integration:down": "docker compose -f compose.test.yml down -v",
32
+ "test:integration:run": "bun run test:integration:up && bun run test:integration && bun run test:integration:down",
33
+ "test:all": "bun run test:unit && bun run test:integration:run"
34
+ },
35
+ "exports": {
36
+ ".": "./src/index.ts",
37
+ "./client": "./src/client.ts",
38
+ "./utils": "./src/utils.ts",
39
+ "./schemas": "./src/schemas.ts"
40
+ },
41
+ "devDependencies": {
42
+ "@types/bun": "latest"
43
+ },
44
+ "peerDependencies": {
45
+ "typescript": "^5"
46
+ },
47
+ "dependencies": {
48
+ "@hono/standard-validator": "^0.2.2",
49
+ "@scalar/hono-api-reference": "^0.9.37",
50
+ "@scalar/openapi-to-markdown": "^0.3.31",
51
+ "hono": "^4.11.7",
52
+ "hono-openapi": "^1.2.0",
53
+ "sanitize-filename": "^1.6.3",
54
+ "zod": "^4.3.6"
55
+ }
56
+ }