@valentinkolb/filegate 2.3.1 → 2.3.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 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.1",
3
+ "version": "2.3.3",
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",
@@ -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,77 @@ const cleanupUpload = async (id: string): Promise<void> => {
80
81
  };
81
82
 
82
83
  const assembleFile = async (meta: UploadMeta): Promise<string | null> => {
83
- const chunks = await getUploadedChunks(meta.uploadId);
84
- if (chunks.length !== meta.totalChunks) return "missing chunks";
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
- const fullPath = join(meta.path, meta.filename);
94
- const pathResult = await validatePath(fullPath);
95
- if (!pathResult.ok) return pathResult.error;
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
96
 
97
- await mkdir(dirname(pathResult.realPath), { recursive: true });
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
+ }
98
103
 
99
- const hasher = new Bun.CryptoHasher("sha256");
100
- const file = Bun.file(pathResult.realPath);
101
- const writer = file.writer();
104
+ const fullPath = join(meta.path, meta.filename);
105
+ const pathResult = await validatePath(fullPath);
106
+ if (!pathResult.ok) return pathResult.error;
102
107
 
103
- try {
104
- for (let i = 0; i < meta.totalChunks; i++) {
105
- // Stream each chunk instead of loading entirely into memory
106
- const chunkFile = Bun.file(chunkPath(meta.uploadId, i));
107
- for await (const data of chunkFile.stream()) {
108
- hasher.update(data);
109
- writer.write(data);
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
+ // Stream each chunk instead of loading entirely into memory
117
+ const chunkFile = Bun.file(chunkPath(meta.uploadId, i));
118
+
119
+ // Verify chunk exists before streaming
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
+ for await (const data of chunkFile.stream()) {
127
+ hasher.update(data);
128
+ writer.write(data);
129
+ }
110
130
  }
131
+ await writer.end();
132
+ } catch (e) {
133
+ writer.end();
134
+ await rm(pathResult.realPath).catch(() => {});
135
+ throw e;
111
136
  }
112
- await writer.end();
113
- } catch (e) {
114
- writer.end();
115
- await rm(pathResult.realPath).catch(() => {});
116
- throw e;
117
- }
118
137
 
119
- const finalChecksum = `sha256:${hasher.digest("hex")}`;
120
- if (finalChecksum !== meta.checksum) {
121
- await rm(pathResult.realPath).catch(() => {});
122
- return `checksum mismatch: expected ${meta.checksum}, got ${finalChecksum}`;
123
- }
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
+ }
124
143
 
125
- const ownershipError = await applyOwnership(pathResult.realPath, meta.ownership);
126
- if (ownershipError) {
127
- await rm(pathResult.realPath).catch(() => {});
128
- return ownershipError;
129
- }
144
+ const ownershipError = await applyOwnership(pathResult.realPath, meta.ownership);
145
+ if (ownershipError) {
146
+ await rm(pathResult.realPath).catch(() => {});
147
+ return ownershipError;
148
+ }
130
149
 
131
- await cleanupUpload(meta.uploadId);
132
- return null;
150
+ await cleanupUpload(meta.uploadId);
151
+ return null;
152
+ } finally {
153
+ semaphore.release();
154
+ }
133
155
  };
134
156
 
135
157
  // POST /upload/start