partial-content 1.0.0
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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/SECURITY.md +92 -0
- package/dist/azure.d.ts +79 -0
- package/dist/azure.d.ts.map +1 -0
- package/dist/azure.js +251 -0
- package/dist/azure.js.map +1 -0
- package/dist/content-disposition.d.ts +74 -0
- package/dist/content-disposition.d.ts.map +1 -0
- package/dist/content-disposition.js +253 -0
- package/dist/content-disposition.js.map +1 -0
- package/dist/fs.d.ts +86 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +375 -0
- package/dist/fs.js.map +1 -0
- package/dist/gcs.d.ts +72 -0
- package/dist/gcs.d.ts.map +1 -0
- package/dist/gcs.js +202 -0
- package/dist/gcs.js.map +1 -0
- package/dist/hono.d.ts +92 -0
- package/dist/hono.d.ts.map +1 -0
- package/dist/hono.js +61 -0
- package/dist/hono.js.map +1 -0
- package/dist/http.d.ts +70 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +281 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel.d.ts +541 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1218 -0
- package/dist/kernel.js.map +1 -0
- package/dist/memory.d.ts +55 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +107 -0
- package/dist/memory.js.map +1 -0
- package/dist/mime.d.ts +49 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +150 -0
- package/dist/mime.js.map +1 -0
- package/dist/node.d.ts +84 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +215 -0
- package/dist/node.js.map +1 -0
- package/dist/object-store.d.ts +472 -0
- package/dist/object-store.d.ts.map +1 -0
- package/dist/object-store.js +335 -0
- package/dist/object-store.js.map +1 -0
- package/dist/r2.d.ts +94 -0
- package/dist/r2.d.ts.map +1 -0
- package/dist/r2.js +150 -0
- package/dist/r2.js.map +1 -0
- package/dist/s3.d.ts +49 -0
- package/dist/s3.d.ts.map +1 -0
- package/dist/s3.js +263 -0
- package/dist/s3.js.map +1 -0
- package/dist/web.d.ts +336 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +1094 -0
- package/dist/web.js.map +1 -0
- package/docs/DESIGN.md +426 -0
- package/package.json +182 -0
- package/src/azure.ts +329 -0
- package/src/content-disposition.ts +300 -0
- package/src/fs.ts +469 -0
- package/src/gcs.ts +290 -0
- package/src/hono.ts +123 -0
- package/src/http.ts +351 -0
- package/src/index.ts +85 -0
- package/src/kernel.ts +1498 -0
- package/src/memory.ts +148 -0
- package/src/mime.ts +160 -0
- package/src/node.ts +261 -0
- package/src/object-store.ts +665 -0
- package/src/r2.ts +232 -0
- package/src/s3.ts +324 -0
- package/src/web.ts +1603 -0
package/src/gcs.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Cloud Storage ObjectStore adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `@google-cloud/storage` to implement the {@link ObjectStore} interface.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { Storage } from "@google-cloud/storage";
|
|
9
|
+
* import { gcsStore } from "partial-content/gcs";
|
|
10
|
+
*
|
|
11
|
+
* const storage = new Storage();
|
|
12
|
+
* const store = gcsStore({ storage, bucket: "my-bucket" });
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
ObjectNotFoundError,
|
|
20
|
+
ObjectChangedError,
|
|
21
|
+
StoreUnavailableError,
|
|
22
|
+
classifyStoreRead,
|
|
23
|
+
nodeStreamToWeb,
|
|
24
|
+
type ObjectStore,
|
|
25
|
+
type ObjectMetadata,
|
|
26
|
+
type ObjectStream,
|
|
27
|
+
type ParsedRange,
|
|
28
|
+
type StoreErrorClassifiers,
|
|
29
|
+
} from "./index.js";
|
|
30
|
+
|
|
31
|
+
// Re-export for convenience
|
|
32
|
+
export { ObjectNotFoundError, ObjectChangedError, StoreUnavailableError };
|
|
33
|
+
|
|
34
|
+
// ─── GCS Types ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Minimal GCS Storage interface.
|
|
38
|
+
*
|
|
39
|
+
* Declared locally to avoid a hard dependency on `@google-cloud/storage`.
|
|
40
|
+
* Users who import `partial-content/gcs` will have it installed as an
|
|
41
|
+
* optional peer dependency.
|
|
42
|
+
*/
|
|
43
|
+
interface GcsStorage {
|
|
44
|
+
bucket(name: string): GcsBucket;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface GcsBucket {
|
|
48
|
+
file(name: string, opts?: { generation?: string | number }): GcsFile;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface GcsFile {
|
|
52
|
+
getMetadata(): Promise<[GcsFileMetadata]>;
|
|
53
|
+
createReadStream(opts?: { start?: number; end?: number }): NodeJS.ReadableStream & AsyncIterable<Buffer>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface GcsFileMetadata {
|
|
57
|
+
size: string; // GCS returns size as a string
|
|
58
|
+
etag?: string;
|
|
59
|
+
/** Object generation: changes on every overwrite. Used to pin reads. */
|
|
60
|
+
generation?: string | number;
|
|
61
|
+
updated?: string; // ISO 8601
|
|
62
|
+
md5Hash?: string; // base64-encoded MD5
|
|
63
|
+
crc32c?: string;
|
|
64
|
+
metadata?: Record<string, string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Options ────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export interface GcsStoreOptions {
|
|
70
|
+
/** Pre-configured @google-cloud/storage instance. */
|
|
71
|
+
storage: GcsStorage;
|
|
72
|
+
/** GCS bucket name. */
|
|
73
|
+
bucket: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Create an {@link ObjectStore} backed by a Google Cloud Storage bucket.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* import { Storage } from "@google-cloud/storage";
|
|
84
|
+
* import { gcsStore } from "partial-content/gcs";
|
|
85
|
+
*
|
|
86
|
+
* const storage = new Storage();
|
|
87
|
+
* const store = gcsStore({ storage, bucket: "my-bucket" });
|
|
88
|
+
*
|
|
89
|
+
* const meta = await store.headObject("reports/q4.pdf");
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function gcsStore(opts: GcsStoreOptions): ObjectStore {
|
|
93
|
+
const bucket = opts.storage.bucket(opts.bucket);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
supportsRange: true,
|
|
97
|
+
|
|
98
|
+
async headObject(key: string, opts?: { signal?: AbortSignal }): Promise<ObjectMetadata> {
|
|
99
|
+
opts?.signal?.throwIfAborted();
|
|
100
|
+
const [metadata] = await classifyStoreRead(key, () => bucket.file(key).getMetadata(), gcsClassifiers);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
contentLength: parseInt(metadata.size, 10),
|
|
104
|
+
etag: metadata.etag,
|
|
105
|
+
lastModified: metadata.updated
|
|
106
|
+
? new Date(metadata.updated).toUTCString()
|
|
107
|
+
: undefined,
|
|
108
|
+
// A generation names an immutable object version, so it can carry
|
|
109
|
+
// everything getObject would otherwise re-fetch. No generation
|
|
110
|
+
// (emulators, mocks) means no pin: getObject falls back to its own
|
|
111
|
+
// metadata read.
|
|
112
|
+
pin: metadata.generation !== undefined
|
|
113
|
+
? encodeGcsPin(metadata)
|
|
114
|
+
: undefined,
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async getObject(key: string, opts?: { range?: ParsedRange; signal?: AbortSignal; ifMatch?: string; pin?: string }): Promise<ObjectStream> {
|
|
119
|
+
const { range, signal, ifMatch, pin } = opts ?? {};
|
|
120
|
+
signal?.throwIfAborted();
|
|
121
|
+
|
|
122
|
+
// A pin from our own headObject carries the generation, size, and
|
|
123
|
+
// validators of the exact representation the caller validated, so the
|
|
124
|
+
// metadata re-fetch is skipped entirely: one backend read per GET.
|
|
125
|
+
// A pin whose etag disagrees with ifMatch (or that fails to decode)
|
|
126
|
+
// is ignored and the metadata path revalidates from scratch. When an
|
|
127
|
+
// ifMatch is present the pin must carry a matching etag to be trusted:
|
|
128
|
+
// a pin lacking the validator cannot silently satisfy the precondition,
|
|
129
|
+
// it falls through to the metadata path that actually enforces ifMatch.
|
|
130
|
+
const pinned = pin ? decodeGcsPin(pin) : null;
|
|
131
|
+
const usablePin = pinned && (!ifMatch || pinned.etag === ifMatch)
|
|
132
|
+
? pinned
|
|
133
|
+
: null;
|
|
134
|
+
|
|
135
|
+
let generation: string | number | undefined;
|
|
136
|
+
let totalSize: number;
|
|
137
|
+
let etag: string | undefined;
|
|
138
|
+
let lastModified: string | undefined;
|
|
139
|
+
|
|
140
|
+
if (usablePin) {
|
|
141
|
+
generation = usablePin.generation;
|
|
142
|
+
totalSize = usablePin.size;
|
|
143
|
+
etag = usablePin.etag;
|
|
144
|
+
lastModified = usablePin.lastModified;
|
|
145
|
+
} else {
|
|
146
|
+
// GCS streams carry no total size, the ifMatch check needs the
|
|
147
|
+
// current etag, and generation pinning needs the current generation.
|
|
148
|
+
const [metadata] = await classifyStoreRead(key, () => bucket.file(key).getMetadata(), gcsClassifiers);
|
|
149
|
+
|
|
150
|
+
// Caller pin: the object must still match the validator captured at
|
|
151
|
+
// HEAD time. GCS etags change on every overwrite, so a mismatch means
|
|
152
|
+
// the representation the caller validated no longer exists.
|
|
153
|
+
if (ifMatch && metadata.etag && metadata.etag !== ifMatch) {
|
|
154
|
+
throw new ObjectChangedError(key);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
generation = metadata.generation;
|
|
158
|
+
totalSize = parseInt(metadata.size, 10);
|
|
159
|
+
etag = metadata.etag;
|
|
160
|
+
lastModified = metadata.updated
|
|
161
|
+
? new Date(metadata.updated).toUTCString()
|
|
162
|
+
: undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (range && range.start >= totalSize) {
|
|
166
|
+
// Unsatisfiable ranges are the orchestrator's job to reject
|
|
167
|
+
// (parseRangeHeader 416s them first); a direct caller gets a loud
|
|
168
|
+
// error instead of a truncated body under an inflated Content-Length.
|
|
169
|
+
throw new RangeError(
|
|
170
|
+
`gcsStore ${key}: range start ${range.start} is beyond object size ${totalSize}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
// Clamp the end so contentLength and the reported range match what
|
|
174
|
+
// the backend stream will actually deliver (createReadStream clamps
|
|
175
|
+
// to EOF silently; the reported bounds must not diverge from it).
|
|
176
|
+
const end = range ? Math.min(range.end, totalSize - 1) : totalSize - 1;
|
|
177
|
+
const streamOpts = range
|
|
178
|
+
? { start: range.start, end }
|
|
179
|
+
: undefined;
|
|
180
|
+
|
|
181
|
+
// Pin the stream to the generation just measured (or the caller's
|
|
182
|
+
// pinned generation): metadata and bytes come from the SAME object
|
|
183
|
+
// version even if it is overwritten between the two calls (reading a
|
|
184
|
+
// specific generation is immutable in GCS).
|
|
185
|
+
const pinnedFile = generation !== undefined
|
|
186
|
+
? bucket.file(key, { generation })
|
|
187
|
+
: bucket.file(key);
|
|
188
|
+
const nodeStream = pinnedFile.createReadStream(streamOpts);
|
|
189
|
+
|
|
190
|
+
const contentLength = range
|
|
191
|
+
? (end - range.start + 1)
|
|
192
|
+
: totalSize;
|
|
193
|
+
|
|
194
|
+
// nodeStreamToWeb auto-detects the stream's destroy() capability.
|
|
195
|
+
// expectedBytes guards a graceful short-read: a truncated GCS download
|
|
196
|
+
// that ends cleanly below the committed length errors the body instead
|
|
197
|
+
// of under-running the Content-Length undetected.
|
|
198
|
+
const webStream = nodeStreamToWeb(nodeStream, { signal, expectedBytes: contentLength });
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
body: webStream,
|
|
202
|
+
contentLength,
|
|
203
|
+
totalSize,
|
|
204
|
+
range: range ? { start: range.start, end } : undefined,
|
|
205
|
+
etag,
|
|
206
|
+
lastModified,
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
/** Shape carried inside the opaque {@link ObjectMetadata.pin} token. */
|
|
215
|
+
interface GcsPin {
|
|
216
|
+
generation: string | number;
|
|
217
|
+
size: number;
|
|
218
|
+
etag?: string;
|
|
219
|
+
lastModified?: string;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function encodeGcsPin(metadata: GcsFileMetadata): string {
|
|
223
|
+
const pin: GcsPin = {
|
|
224
|
+
generation: metadata.generation!,
|
|
225
|
+
size: parseInt(metadata.size, 10),
|
|
226
|
+
etag: metadata.etag,
|
|
227
|
+
lastModified: metadata.updated
|
|
228
|
+
? new Date(metadata.updated).toUTCString()
|
|
229
|
+
: undefined,
|
|
230
|
+
};
|
|
231
|
+
return JSON.stringify(pin);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Decode a pin token; null for foreign/corrupt tokens (fall back to metadata). */
|
|
235
|
+
function decodeGcsPin(pin: string): GcsPin | null {
|
|
236
|
+
try {
|
|
237
|
+
const parsed: unknown = JSON.parse(pin);
|
|
238
|
+
if (
|
|
239
|
+
typeof parsed === "object" && parsed !== null &&
|
|
240
|
+
"generation" in parsed && "size" in parsed &&
|
|
241
|
+
typeof (parsed as GcsPin).size === "number" &&
|
|
242
|
+
Number.isFinite((parsed as GcsPin).size) &&
|
|
243
|
+
// `generation` flows straight into bucket.file(key, { generation }) and
|
|
244
|
+
// selects the immutable version served. Only a non-empty string or a
|
|
245
|
+
// finite number is a real generation; anything else (object, array,
|
|
246
|
+
// null) is a corrupt/hostile token that must revalidate from scratch,
|
|
247
|
+
// never index an arbitrary version.
|
|
248
|
+
isValidGeneration((parsed as GcsPin).generation)
|
|
249
|
+
) {
|
|
250
|
+
return parsed as GcsPin;
|
|
251
|
+
}
|
|
252
|
+
} catch { /* not our token: revalidate via metadata instead */ }
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** A GCS generation is a non-empty string or a finite number; nothing else. */
|
|
257
|
+
function isValidGeneration(gen: unknown): gen is string | number {
|
|
258
|
+
return (typeof gen === "string" && gen.length > 0) ||
|
|
259
|
+
(typeof gen === "number" && Number.isFinite(gen));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isGcsNotFound(err: unknown): boolean {
|
|
263
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
264
|
+
return (err as { code: unknown }).code === 404;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Whether a GCS error is a transient throttle/overload the client should retry
|
|
271
|
+
* (mapped to a 503, not a 502). The Cloud Storage client surfaces the HTTP
|
|
272
|
+
* status on `err.code`: 429 (rate limit) or 503 (backend unavailable).
|
|
273
|
+
*/
|
|
274
|
+
function isGcsThrottled(err: unknown): boolean {
|
|
275
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
276
|
+
const code = (err as { code: unknown }).code;
|
|
277
|
+
return code === 429 || code === 503;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The error-classification set shared by both metadata reads. GCS has no
|
|
284
|
+
* `changed` predicate: its pin is an etag comparison (handled inline, throwing
|
|
285
|
+
* {@link ObjectChangedError} directly), not a native conditional error.
|
|
286
|
+
*/
|
|
287
|
+
const gcsClassifiers: StoreErrorClassifiers = {
|
|
288
|
+
notFound: isGcsNotFound,
|
|
289
|
+
throttled: isGcsThrottled,
|
|
290
|
+
};
|
package/src/hono.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hono middleware adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Wraps an {@link ObjectStore} into a Hono-compatible middleware that handles
|
|
5
|
+
* the full HTTP file-serving protocol (200, 206, 304, 412, 416, HEAD).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { Hono } from "hono";
|
|
10
|
+
* import { serveObject } from "partial-content/hono";
|
|
11
|
+
* import { s3Store } from "partial-content/s3";
|
|
12
|
+
*
|
|
13
|
+
* const store = s3Store({ client, bucket: "documents" });
|
|
14
|
+
* const app = new Hono();
|
|
15
|
+
*
|
|
16
|
+
* app.get("/files/:key", serveObject(store, {
|
|
17
|
+
* key: (c) => c.req.param("key"),
|
|
18
|
+
* disposition: "inline",
|
|
19
|
+
* }));
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @packageDocumentation
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { serveObject as serveObjectWeb, type ServeObjectOptions, type ServeContext } from "./web.js";
|
|
26
|
+
import type { ObjectStore } from "./index.js";
|
|
27
|
+
|
|
28
|
+
// Re-export for convenience
|
|
29
|
+
export type { ServeObjectOptions, ServeContext } from "./web.js";
|
|
30
|
+
export type { ObjectStore } from "./index.js";
|
|
31
|
+
|
|
32
|
+
// ─── Hono Types ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Minimal Hono context interface.
|
|
36
|
+
*
|
|
37
|
+
* We declare just enough of the Hono Context shape to avoid a hard
|
|
38
|
+
* dependency on the `hono` package. Users who import `partial-content/hono`
|
|
39
|
+
* will have `hono` installed (it's an optional peer dep), but we don't
|
|
40
|
+
* want to force TypeScript resolution on it at build time for the
|
|
41
|
+
* published package.
|
|
42
|
+
*/
|
|
43
|
+
interface HonoContext {
|
|
44
|
+
req: {
|
|
45
|
+
raw: Request;
|
|
46
|
+
param: (name: string) => string;
|
|
47
|
+
header: (name: string) => string | undefined;
|
|
48
|
+
method: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Hono handler return type. */
|
|
53
|
+
type HonoResponse = Response | Promise<Response>;
|
|
54
|
+
|
|
55
|
+
// ─── Options ────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface HonoServeOptions extends ServeObjectOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Extract the storage key from the Hono context.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* key: (c) => c.req.param("key")
|
|
64
|
+
* key: (c) => c.req.param("path")
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
key: (c: HonoContext) => string | Promise<string>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract the MIME type from the Hono context.
|
|
71
|
+
* When omitted, defaults to "application/octet-stream".
|
|
72
|
+
*/
|
|
73
|
+
mime?: (c: HonoContext) => string | undefined;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extract the filename from the Hono context (for Content-Disposition).
|
|
77
|
+
* When omitted, no filename is set.
|
|
78
|
+
*/
|
|
79
|
+
filename?: (c: HonoContext) => string | undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── Factory ────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create a Hono middleware handler that serves files from an {@link ObjectStore}.
|
|
86
|
+
*
|
|
87
|
+
* Uses the standard Request/Response API under the hood, so it works on
|
|
88
|
+
* all Hono runtimes (Cloudflare Workers, Bun, Deno, Node.js).
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* import { Hono } from "hono";
|
|
93
|
+
* import { serveObject } from "partial-content/hono";
|
|
94
|
+
* import { s3Store } from "partial-content/s3";
|
|
95
|
+
*
|
|
96
|
+
* const app = new Hono();
|
|
97
|
+
* const store = s3Store({ client, bucket: "media" });
|
|
98
|
+
*
|
|
99
|
+
* app.get("/media/:key", serveObject(store, {
|
|
100
|
+
* key: (c) => c.req.param("key"),
|
|
101
|
+
* disposition: "inline",
|
|
102
|
+
* cacheControl: "public, max-age=31536000, immutable",
|
|
103
|
+
* }));
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export function serveObject(
|
|
107
|
+
store: ObjectStore,
|
|
108
|
+
opts: HonoServeOptions,
|
|
109
|
+
): (c: HonoContext) => HonoResponse {
|
|
110
|
+
const { key: keyFn, mime: mimeFn, filename: filenameFn, ...serveOpts } = opts;
|
|
111
|
+
const handler = serveObjectWeb(store, serveOpts);
|
|
112
|
+
|
|
113
|
+
return async function honoHandler(c: HonoContext): Promise<Response> {
|
|
114
|
+
const key = await keyFn(c);
|
|
115
|
+
const ctx: ServeContext = {
|
|
116
|
+
key,
|
|
117
|
+
mime: mimeFn?.(c),
|
|
118
|
+
filename: filenameFn?.(c),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return handler(c.req.raw, ctx);
|
|
122
|
+
};
|
|
123
|
+
}
|