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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-Disposition header builder (RFC 6266 + RFC 8187).
|
|
3
|
+
*
|
|
4
|
+
* Builds Content-Disposition headers for HTTP responses that serve files.
|
|
5
|
+
* Handles both inline (in-app preview) and attachment (download) dispositions
|
|
6
|
+
* with proper RFC compliance for non-ASCII filenames.
|
|
7
|
+
*
|
|
8
|
+
* Security: Prevents CRLF header injection, path traversal, bidi override
|
|
9
|
+
* spoofing, and control character injection in filenames from untrusted
|
|
10
|
+
* sources (integration APIs, user uploads).
|
|
11
|
+
*
|
|
12
|
+
* Emits dual filename parameters for cross-browser compatibility:
|
|
13
|
+
* - `filename="ascii-safe.pdf"` for legacy browsers
|
|
14
|
+
* - `filename*=UTF-8''%C3%85rlig.pdf` for modern browsers (RFC 8187)
|
|
15
|
+
*/
|
|
16
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
17
|
+
/**
|
|
18
|
+
* RFC 2616 token characters. A filename that matches this pattern can be
|
|
19
|
+
* emitted unquoted in the `filename=` parameter (e.g. `filename=plans.pdf`).
|
|
20
|
+
* Anything outside this set requires quoted-string encoding.
|
|
21
|
+
*
|
|
22
|
+
* token = 1*<any CHAR except CTLs or separators>
|
|
23
|
+
* separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
|
|
24
|
+
* | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
|
|
25
|
+
*/
|
|
26
|
+
const TOKEN_REGEXP = /^[!#$%&'*+.0-9A-Z^_`a-z|~-]+$/;
|
|
27
|
+
/**
|
|
28
|
+
* Characters that are valid US-ASCII text (printable range 0x20-0x7E).
|
|
29
|
+
* Filenames matching this can use the simple `filename=` parameter with
|
|
30
|
+
* quoted-string encoding. Non-ASCII filenames need `filename*=` (RFC 8187).
|
|
31
|
+
*/
|
|
32
|
+
const ASCII_TEXT_REGEXP = /^[\x20-\x7e]*$/;
|
|
33
|
+
/**
|
|
34
|
+
* Characters that need escaping inside an RFC 2616 quoted-string.
|
|
35
|
+
* Per Section 2.2: quoted-pair = "\" CHAR, and the only characters
|
|
36
|
+
* that MUST be escaped are `\` and `"`.
|
|
37
|
+
*/
|
|
38
|
+
const QUOTED_PAIR_REGEXP = /[\\"]/g;
|
|
39
|
+
/**
|
|
40
|
+
* Characters that are not valid in RFC 8187 attr-char, applied AFTER
|
|
41
|
+
* encodeURIComponent (so `%` is already handled). This catches characters
|
|
42
|
+
* that encodeURIComponent leaves unescaped but RFC 8187 requires encoded.
|
|
43
|
+
*/
|
|
44
|
+
const ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g; // eslint-disable-line no-control-regex
|
|
45
|
+
/**
|
|
46
|
+
* Non-US-ASCII characters. Replaced with `?` in the ASCII fallback
|
|
47
|
+
* for maximum compatibility with legacy HTTP clients.
|
|
48
|
+
*/
|
|
49
|
+
const NON_ASCII_REGEXP = /[^\x20-\x7e]/g;
|
|
50
|
+
/**
|
|
51
|
+
* Filenames containing percent-encoded sequences (like `the%20plans.pdf`)
|
|
52
|
+
* should not use the simple `filename=` parameter because legacy clients
|
|
53
|
+
* may decode them. Force `filename*` for these.
|
|
54
|
+
*/
|
|
55
|
+
const HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
|
|
56
|
+
/**
|
|
57
|
+
* Matches lone surrogate code units (unpaired high or low surrogates).
|
|
58
|
+
* These occur when filenames are truncated mid-emoji with .slice().
|
|
59
|
+
* encodeURIComponent throws URIError on lone surrogates, so we must
|
|
60
|
+
* strip them before encoding.
|
|
61
|
+
*/
|
|
62
|
+
const LONE_SURROGATE_REGEXP = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
63
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
64
|
+
/**
|
|
65
|
+
* Build a Content-Disposition header value with RFC compliance and
|
|
66
|
+
* security sanitization.
|
|
67
|
+
*
|
|
68
|
+
* Accepts untrusted filenames from integration APIs, user uploads, and
|
|
69
|
+
* database records. Sanitizes for:
|
|
70
|
+
* - CRLF injection (strips \r, \n, and all control characters)
|
|
71
|
+
* - Path traversal (strips ../ and ..\\ sequences)
|
|
72
|
+
* - Basename extraction (strips directory paths)
|
|
73
|
+
* - Bidi override stripping (prevents RLO filename spoofing)
|
|
74
|
+
* - Double-quote and backslash escaping (RFC 2616 quoted-pair)
|
|
75
|
+
*
|
|
76
|
+
* Emits dual filename parameters for cross-browser compatibility:
|
|
77
|
+
* - `filename="ascii-safe.pdf"` for legacy browsers (IE, old Safari)
|
|
78
|
+
* - `filename*=UTF-8''%C3%85rlig.pdf` for modern browsers (RFC 8187)
|
|
79
|
+
*
|
|
80
|
+
* Token optimization: Simple ASCII filenames like `plans.pdf` are emitted
|
|
81
|
+
* unquoted (`filename=plans.pdf`).
|
|
82
|
+
*
|
|
83
|
+
* @param filename - Raw filename from untrusted source (upload, API, DB)
|
|
84
|
+
* @param options - Disposition type and fallback configuration
|
|
85
|
+
* @returns Complete Content-Disposition header value string
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Simple ASCII attachment
|
|
89
|
+
* buildContentDisposition("report.pdf")
|
|
90
|
+
* // => 'attachment; filename=report.pdf'
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Non-ASCII filename (Danish)
|
|
94
|
+
* buildContentDisposition("Årlig_Rapport.pdf")
|
|
95
|
+
* // => 'attachment; filename="?rlig_Rapport.pdf"; filename*=UTF-8\'\'%C3%85rlig_Rapport.pdf'
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Inline disposition for preview
|
|
99
|
+
* buildContentDisposition("slides.pdf", { type: "inline" })
|
|
100
|
+
* // => 'inline; filename=slides.pdf'
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Null input with custom fallback
|
|
104
|
+
* buildContentDisposition(null, { fallback: "export.csv" })
|
|
105
|
+
* // => 'attachment; filename=export.csv'
|
|
106
|
+
*/
|
|
107
|
+
export function buildContentDisposition(filename, options) {
|
|
108
|
+
// Never trust the disposition type: a JS caller (or a mistyped
|
|
109
|
+
// `disposition` extractor returning a computed string) could otherwise
|
|
110
|
+
// smuggle header parameters through it. Anything but "inline" is
|
|
111
|
+
// "attachment" -- the safe default.
|
|
112
|
+
const type = options?.type === "inline" ? "inline" : "attachment";
|
|
113
|
+
// Sanitize BOTH the untrusted filename and the fallback through the same
|
|
114
|
+
// cleaner, then fall back to a constant. A filename that reduces to
|
|
115
|
+
// nothing (only controls/bidi/path components) must NOT emit a raw
|
|
116
|
+
// fallback into filename* -- that path leaked un-neutralized bidi
|
|
117
|
+
// overrides, defeating the anti-spoofing guarantee.
|
|
118
|
+
const sanitized = sanitizeFilename(filename ?? "")
|
|
119
|
+
|| sanitizeFilename(options?.fallback ?? "")
|
|
120
|
+
|| "document";
|
|
121
|
+
return `${type}${buildFilenameParams(sanitized)}`;
|
|
122
|
+
}
|
|
123
|
+
// ─── Internal Helpers ───────────────────────────────────────────────────────
|
|
124
|
+
/**
|
|
125
|
+
* Sanitize an untrusted filename for safe use in HTTP headers.
|
|
126
|
+
*
|
|
127
|
+
* Applied BEFORE any RFC encoding. This is the security layer that
|
|
128
|
+
* prevents header injection and path traversal attacks.
|
|
129
|
+
*/
|
|
130
|
+
function sanitizeFilename(raw) {
|
|
131
|
+
// 1. Strip control characters and Unicode formatting controls.
|
|
132
|
+
// C0 controls (\x00-\x1F), DEL (\x7F): prevents CRLF header injection.
|
|
133
|
+
// C1 controls (\x80-\x9F): invisible formatting, no valid use in filenames.
|
|
134
|
+
// Bidi overrides (U+202A-202E, U+2066-2069): prevents RLO filename
|
|
135
|
+
// spoofing where "report\u202Efdp.exe" renders as "reportexe.pdf".
|
|
136
|
+
// Zero-width chars (U+200B-200F): invisible joiners/marks, strips
|
|
137
|
+
// directional marks that could confuse filename display.
|
|
138
|
+
// NBSP (U+00A0): often mistaken for regular space, normalize away.
|
|
139
|
+
const noControls = raw.replace(/[\r\n\x00-\x1F\x7F\x80-\x9F\u00A0\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, "");
|
|
140
|
+
// 2. Strip path traversal sequences (../ and ..\).
|
|
141
|
+
// Some integration APIs return full paths or relative paths.
|
|
142
|
+
// Even though HTTP clients shouldn't interpret these, defense-in-depth.
|
|
143
|
+
const noTraversal = noControls.replace(/\.\.[/\\]/g, "");
|
|
144
|
+
// 3. Extract basename (strip directory components).
|
|
145
|
+
// Integration APIs sometimes return full paths like "/uploads/2024/report.pdf".
|
|
146
|
+
// Uses lastIndexOf instead of split().pop() to avoid array allocation.
|
|
147
|
+
const lastSep = Math.max(noTraversal.lastIndexOf("/"), noTraversal.lastIndexOf("\\"));
|
|
148
|
+
const basename = lastSep >= 0 ? noTraversal.slice(lastSep + 1) : noTraversal;
|
|
149
|
+
// May be empty: the caller composes the fallback chain so both the
|
|
150
|
+
// filename and the fallback pass through this same sanitizer.
|
|
151
|
+
return basename;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the filename parameter string for Content-Disposition.
|
|
155
|
+
*
|
|
156
|
+
* Encoding strategy:
|
|
157
|
+
* 1. Pure ASCII token (no separators): emit unquoted `filename=report.pdf`
|
|
158
|
+
* 2. ASCII with special chars: emit quoted `filename="my report.pdf"`
|
|
159
|
+
* 3. Non-ASCII: emit both `filename="?rlig.pdf"` and `filename*=UTF-8''...`
|
|
160
|
+
*
|
|
161
|
+
* RFC 2616 Section 2.2 quoted-string escaping:
|
|
162
|
+
* - `\` -> `\\` (backslash must be escaped)
|
|
163
|
+
* - `"` -> `\"` (double-quote must be escaped)
|
|
164
|
+
*/
|
|
165
|
+
function buildFilenameParams(filename) {
|
|
166
|
+
// Case 1: Pure ASCII that fits in a token -- emit unquoted.
|
|
167
|
+
// Also reject filenames with hex-encoded sequences (%20) which could
|
|
168
|
+
// be decoded by legacy clients.
|
|
169
|
+
if (ASCII_TEXT_REGEXP.test(filename) && !HEX_ESCAPE_REGEXP.test(filename)) {
|
|
170
|
+
if (TOKEN_REGEXP.test(filename)) {
|
|
171
|
+
return `; filename=${filename}`;
|
|
172
|
+
}
|
|
173
|
+
// ASCII but contains separators/spaces -- use quoted-string
|
|
174
|
+
return `; filename=${quoteString(filename)}`;
|
|
175
|
+
}
|
|
176
|
+
// Case 2: Contains non-ASCII characters or hex escapes.
|
|
177
|
+
// Emit both parameters for cross-browser compatibility:
|
|
178
|
+
// - filename="ascii-fallback" for legacy clients
|
|
179
|
+
// - filename*=UTF-8''percent-encoded for modern clients (RFC 8187)
|
|
180
|
+
const asciiFallback = toAsciiFallback(filename);
|
|
181
|
+
const encoded = encodeRfc8187(filename);
|
|
182
|
+
// When filename has hex escapes but is pure ASCII, we still need
|
|
183
|
+
// filename* because the hex escape could be decoded by legacy clients.
|
|
184
|
+
// Percent-encode % in the fallback to prevent unintended decoding.
|
|
185
|
+
const safeFallback = asciiFallback.replace(/%/g, "%25");
|
|
186
|
+
if (TOKEN_REGEXP.test(safeFallback)) {
|
|
187
|
+
return `; filename=${safeFallback}; filename*=${encoded}`;
|
|
188
|
+
}
|
|
189
|
+
return `; filename=${quoteString(safeFallback)}; filename*=${encoded}`;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Quote a string per RFC 2616 Section 2.2.
|
|
193
|
+
*
|
|
194
|
+
* quoted-string = ( <"> *(qdtext | quoted-pair) <"> )
|
|
195
|
+
* quoted-pair = "\" CHAR
|
|
196
|
+
*
|
|
197
|
+
* Both `\` and `"` MUST be escaped with a preceding backslash.
|
|
198
|
+
*
|
|
199
|
+
* In practice, `\` is stripped by sanitizeFilename (treated as path separator),
|
|
200
|
+
* so only `"` escaping is exercised through the normal code path. The `\`
|
|
201
|
+
* escaping is defense-in-depth for correctness if sanitization ever changes.
|
|
202
|
+
*/
|
|
203
|
+
function quoteString(str) {
|
|
204
|
+
return `"${str.replace(QUOTED_PAIR_REGEXP, "\\$&")}"`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Generate a US-ASCII fallback for non-ASCII filenames.
|
|
208
|
+
*
|
|
209
|
+
* Replaces non-ASCII characters with `?`. This is more honest than `_` --
|
|
210
|
+
* it signals to the user that characters were lost in translation, rather
|
|
211
|
+
* than suggesting underscores were in the original.
|
|
212
|
+
*/
|
|
213
|
+
function toAsciiFallback(filename) {
|
|
214
|
+
return filename.replace(NON_ASCII_REGEXP, "?");
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Encode a string per RFC 8187 (formerly RFC 5987).
|
|
218
|
+
*
|
|
219
|
+
* Format: charset'language'value-chars
|
|
220
|
+
* We always use UTF-8 and leave language empty.
|
|
221
|
+
*
|
|
222
|
+
* The encoding differs from encodeURIComponent in that RFC 8187
|
|
223
|
+
* defines its own set of "attr-char" that don't need encoding.
|
|
224
|
+
* We first apply encodeURIComponent (which handles all non-ASCII),
|
|
225
|
+
* then encode the characters that encodeURIComponent leaves unescaped
|
|
226
|
+
* but RFC 8187 requires encoded (including single quotes, which are
|
|
227
|
+
* the charset/language delimiter in the RFC 8187 format).
|
|
228
|
+
*/
|
|
229
|
+
function encodeRfc8187(str) {
|
|
230
|
+
// encodeURIComponent itself throws URIError on lone surrogates
|
|
231
|
+
// (ECMA-262), so the exception IS the surrogate detector: clean strings
|
|
232
|
+
// (the overwhelming majority, including valid emoji pairs) pay no
|
|
233
|
+
// pre-scan at all. Lone surrogates only occur when upstream code
|
|
234
|
+
// truncates filenames mid-emoji with .slice() on UTF-16 code units;
|
|
235
|
+
// that pathological path eats the exception cost and retries stripped.
|
|
236
|
+
// Benchmarked against a surrogate-range pre-scan regex: this is ~6x
|
|
237
|
+
// faster for emoji filenames (a pre-scan cannot tell valid pairs from
|
|
238
|
+
// lone surrogates, so it triggered the strip for every emoji).
|
|
239
|
+
let encoded;
|
|
240
|
+
try {
|
|
241
|
+
encoded = encodeURIComponent(str);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
encoded = encodeURIComponent(str.replace(LONE_SURROGATE_REGEXP, ""));
|
|
245
|
+
}
|
|
246
|
+
encoded = encoded.replace(ENCODE_URL_ATTR_CHAR_REGEXP, percentEncode);
|
|
247
|
+
return `UTF-8''${encoded}`;
|
|
248
|
+
}
|
|
249
|
+
/** Percent-encode a single character. */
|
|
250
|
+
function percentEncode(char) {
|
|
251
|
+
return "%" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=content-disposition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-disposition.js","sourceRoot":"","sources":["../src/content-disposition.ts"],"names":[],"mappings":"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"}
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local filesystem ObjectStore adapter for partial-content.
|
|
3
|
+
*
|
|
4
|
+
* Implements the read-only {@link ObjectStore} interface using Node.js
|
|
5
|
+
* `fs.stat()` and `fs.createReadStream()`. Suitable for:
|
|
6
|
+
* - Development servers
|
|
7
|
+
* - Small/medium deployments
|
|
8
|
+
* - Hybrid architectures (local cache + cloud primary)
|
|
9
|
+
* - Testing and CI pipelines
|
|
10
|
+
*
|
|
11
|
+
* Security: all keys are resolved relative to a fixed root directory.
|
|
12
|
+
* Path traversal attempts (`..`), absolute paths, and null bytes are
|
|
13
|
+
* rejected with ObjectNotFoundError. Symbolic links inside the root ARE
|
|
14
|
+
* followed (matching nginx/caddy defaults); do not place untrusted
|
|
15
|
+
* symlinks under the root if it must be a strict sandbox.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { fsStore } from "partial-content/fs";
|
|
20
|
+
* import { serveObject } from "partial-content/web";
|
|
21
|
+
*
|
|
22
|
+
* const store = fsStore({ root: "/var/data/uploads" });
|
|
23
|
+
*
|
|
24
|
+
* // Use with the web adapter:
|
|
25
|
+
* const handler = serveObject(store, { disposition: "inline" });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
import { ObjectNotFoundError, type ObjectStore } from "./index.js";
|
|
31
|
+
export { ObjectNotFoundError };
|
|
32
|
+
export interface FsStoreOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Root directory. All keys are resolved relative to this path.
|
|
35
|
+
* Must be an absolute path.
|
|
36
|
+
*/
|
|
37
|
+
root: string;
|
|
38
|
+
/**
|
|
39
|
+
* Opt-in hot-object cache (metadata + small bodies), following the
|
|
40
|
+
* nginx `open_file_cache` model: entries revalidate on a TTL rather
|
|
41
|
+
* than a change watcher, so a served representation can lag a
|
|
42
|
+
* filesystem overwrite by up to `ttlMs`. Metadata and bytes are always
|
|
43
|
+
* captured together from one read, so responses are internally
|
|
44
|
+
* coherent (headers always describe the bytes actually sent).
|
|
45
|
+
*
|
|
46
|
+
* Off by default: correctness-first. Enable for hot small files
|
|
47
|
+
* (thumbnails, documents, content-addressed assets -- use a long TTL
|
|
48
|
+
* for immutable keys). Bodies at or below the single-read limit
|
|
49
|
+
* (128 KiB) are cached; larger objects always stream fresh from disk.
|
|
50
|
+
*
|
|
51
|
+
* Memory bound: `maxEntries` caps the entry count and `maxBytes` caps
|
|
52
|
+
* the total cached BODY bytes, so the worst case is
|
|
53
|
+
* `min(maxEntries * 128 KiB, maxBytes)` plus per-entry metadata.
|
|
54
|
+
*/
|
|
55
|
+
cache?: {
|
|
56
|
+
/** How long an entry may serve before revalidating against disk. */
|
|
57
|
+
ttlMs: number;
|
|
58
|
+
/**
|
|
59
|
+
* Entry cap, evicted least-recently-used.
|
|
60
|
+
* @default 1024
|
|
61
|
+
*/
|
|
62
|
+
maxEntries?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Total byte budget for cached bodies, evicted least-recently-used.
|
|
65
|
+
* Metadata-only entries cost nothing against it. A single body larger
|
|
66
|
+
* than the budget is served normally but cached metadata-only, so an
|
|
67
|
+
* oversized object can never flush the whole cache to still not fit.
|
|
68
|
+
* `0` keeps the cache metadata-only (stat elision without body memory).
|
|
69
|
+
* @default 67108864 (64 MiB)
|
|
70
|
+
*/
|
|
71
|
+
maxBytes?: number;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create an {@link ObjectStore} backed by the local filesystem.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* import { fsStore } from "partial-content/fs";
|
|
80
|
+
*
|
|
81
|
+
* const store = fsStore({ root: "/var/data/uploads" });
|
|
82
|
+
* const meta = await store.headObject("reports/q4.pdf");
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare function fsStore(opts: FsStoreOptions): ObjectStore;
|
|
86
|
+
//# sourceMappingURL=fs.d.ts.map
|
package/dist/fs.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAIH,OAAO,EACL,mBAAmB,EAEnB,KAAK,WAAW,EAIjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAI/B,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE;QACN,oEAAoE;QACpE,KAAK,EAAE,MAAM,CAAC;QACd;;;WAGG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB;;;;;;;WAOG;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAeD;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,cAAc,GAAG,WAAW,CA6TzD"}
|