@valentinkolb/filegate 2.3.1 → 2.3.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.
- package/README.md +9 -0
- package/package.json +2 -1
- package/src/handlers/upload.ts +50 -41
package/README.md
CHANGED
|
@@ -245,6 +245,15 @@ const result = await client.thumbnail.image({
|
|
|
245
245
|
|
|
246
246
|
**Supported input formats:** JPEG, PNG, WebP, AVIF, TIFF, GIF, SVG
|
|
247
247
|
|
|
248
|
+
**Caching:** Thumbnails include `ETag`, `Last-Modified`, and `Cache-Control: immutable` headers. Simply pass through the response:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
const result = await client.thumbnail.image({ path: "/data/photo.jpg" });
|
|
252
|
+
if (!result.ok) return c.json({ error: result.error }, result.status);
|
|
253
|
+
|
|
254
|
+
return result.data; // Response with all headers
|
|
255
|
+
```
|
|
256
|
+
|
|
248
257
|
### Chunked Uploads
|
|
249
258
|
|
|
250
259
|
For large files, use chunked uploads. They support:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valentinkolb/filegate",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"typescript": "^5"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@henrygd/semaphore": "^0.1.0",
|
|
43
44
|
"@hono/standard-validator": "^0.2.2",
|
|
44
45
|
"@scalar/hono-api-reference": "^0.9.37",
|
|
45
46
|
"@scalar/openapi-to-markdown": "^0.3.31",
|
package/src/handlers/upload.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Hono } from "hono";
|
|
|
2
2
|
import { describeRoute } from "hono-openapi";
|
|
3
3
|
import { mkdir, readdir, rm, stat, rename, readFile, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
|
+
import { getSemaphore } from "@henrygd/semaphore";
|
|
5
6
|
import { validatePath } from "../lib/path";
|
|
6
7
|
import { applyOwnership, type Ownership } from "../lib/ownership";
|
|
7
8
|
import { jsonResponse, requiresAuth } from "../lib/openapi";
|
|
@@ -80,56 +81,64 @@ const cleanupUpload = async (id: string): Promise<void> => {
|
|
|
80
81
|
};
|
|
81
82
|
|
|
82
83
|
const assembleFile = async (meta: UploadMeta): Promise<string | null> => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Verify all expected chunk indices are present (0 to totalChunks-1)
|
|
87
|
-
const expectedChunks = Array.from({ length: meta.totalChunks }, (_, i) => i);
|
|
88
|
-
const missingChunks = expectedChunks.filter((i) => !chunks.includes(i));
|
|
89
|
-
if (missingChunks.length > 0) {
|
|
90
|
-
return `missing chunk indices: ${missingChunks.join(", ")}`;
|
|
91
|
-
}
|
|
84
|
+
// Use semaphore to prevent concurrent assembly of the same upload
|
|
85
|
+
const semaphore = getSemaphore(`assemble:${meta.uploadId}`, 1);
|
|
86
|
+
await semaphore.acquire();
|
|
92
87
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
try {
|
|
89
|
+
const chunks = await getUploadedChunks(meta.uploadId);
|
|
90
|
+
if (chunks.length !== meta.totalChunks) return "missing chunks";
|
|
91
|
+
|
|
92
|
+
// Verify all expected chunk indices are present (0 to totalChunks-1)
|
|
93
|
+
const expectedChunks = Array.from({ length: meta.totalChunks }, (_, i) => i);
|
|
94
|
+
const missingChunks = expectedChunks.filter((i) => !chunks.includes(i));
|
|
95
|
+
if (missingChunks.length > 0) {
|
|
96
|
+
return `missing chunk indices: ${missingChunks.join(", ")}`;
|
|
97
|
+
}
|
|
96
98
|
|
|
97
|
-
|
|
99
|
+
const fullPath = join(meta.path, meta.filename);
|
|
100
|
+
const pathResult = await validatePath(fullPath);
|
|
101
|
+
if (!pathResult.ok) return pathResult.error;
|
|
98
102
|
|
|
99
|
-
|
|
100
|
-
const file = Bun.file(pathResult.realPath);
|
|
101
|
-
const writer = file.writer();
|
|
103
|
+
await mkdir(dirname(pathResult.realPath), { recursive: true });
|
|
102
104
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
const hasher = new Bun.CryptoHasher("sha256");
|
|
106
|
+
const file = Bun.file(pathResult.realPath);
|
|
107
|
+
const writer = file.writer();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
for (let i = 0; i < meta.totalChunks; i++) {
|
|
111
|
+
// Stream each chunk instead of loading entirely into memory
|
|
112
|
+
const chunkFile = Bun.file(chunkPath(meta.uploadId, i));
|
|
113
|
+
for await (const data of chunkFile.stream()) {
|
|
114
|
+
hasher.update(data);
|
|
115
|
+
writer.write(data);
|
|
116
|
+
}
|
|
110
117
|
}
|
|
118
|
+
await writer.end();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
writer.end();
|
|
121
|
+
await rm(pathResult.realPath).catch(() => {});
|
|
122
|
+
throw e;
|
|
111
123
|
}
|
|
112
|
-
await writer.end();
|
|
113
|
-
} catch (e) {
|
|
114
|
-
writer.end();
|
|
115
|
-
await rm(pathResult.realPath).catch(() => {});
|
|
116
|
-
throw e;
|
|
117
|
-
}
|
|
118
124
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
const finalChecksum = `sha256:${hasher.digest("hex")}`;
|
|
126
|
+
if (finalChecksum !== meta.checksum) {
|
|
127
|
+
await rm(pathResult.realPath).catch(() => {});
|
|
128
|
+
return `checksum mismatch: expected ${meta.checksum}, got ${finalChecksum}`;
|
|
129
|
+
}
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
const ownershipError = await applyOwnership(pathResult.realPath, meta.ownership);
|
|
132
|
+
if (ownershipError) {
|
|
133
|
+
await rm(pathResult.realPath).catch(() => {});
|
|
134
|
+
return ownershipError;
|
|
135
|
+
}
|
|
130
136
|
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
await cleanupUpload(meta.uploadId);
|
|
138
|
+
return null;
|
|
139
|
+
} finally {
|
|
140
|
+
semaphore.release();
|
|
141
|
+
}
|
|
133
142
|
};
|
|
134
143
|
|
|
135
144
|
// POST /upload/start
|