@eggjs/koa-static-cache 6.2.0 → 7.0.0-beta.13

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/src/index.ts DELETED
@@ -1,342 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import { debuglog, promisify } from 'node:util';
3
- import fs from 'node:fs/promises';
4
- import { createReadStream, statSync, readFileSync } from 'node:fs';
5
- import zlib from 'node:zlib';
6
- import path from 'node:path';
7
-
8
- import mime from 'mime-types';
9
- import { compressible } from '@eggjs/compressible';
10
- import readDir from 'fs-readdir-recursive';
11
- import { exists, decodeURIComponent as safeDecodeURIComponent } from 'utility';
12
-
13
- const debug = debuglog('@eggjs/koa-static-cache');
14
-
15
- const gzip = promisify(zlib.gzip);
16
-
17
- export type FileFilter = (path: string) => boolean;
18
-
19
- export interface FileMeta {
20
- maxAge?: number;
21
- cacheControl?: string | ((path: string) => string);
22
- buffer?: Buffer;
23
- zipBuffer?: Buffer;
24
- type?: string;
25
- mime?: string;
26
- mtime?: Date;
27
- path?: string;
28
- md5?: string;
29
- length?: number;
30
- }
31
-
32
- export interface FileMap {
33
- [path: string]: FileMeta;
34
- }
35
-
36
- export interface FileStore {
37
- get(key: string): unknown;
38
- set(key: string, value: unknown): void;
39
- }
40
-
41
- export interface Options {
42
- /**
43
- * The root directory from which to serve static assets
44
- * Default to `process.cwd`
45
- */
46
- dir?: string;
47
- /**
48
- * The max age for cache control
49
- * Default to `0`
50
- */
51
- maxAge?: number;
52
- /**
53
- * The cache control header for static files
54
- * Default to `undefined`
55
- * Overrides `options.maxAge`
56
- */
57
- cacheControl?: string | ((path: string) => string);
58
- /**
59
- * store the files in memory instead of streaming from the filesystem on each request
60
- */
61
- buffer?: boolean;
62
- /**
63
- * when request's accept-encoding include gzip, files will compressed by gzip
64
- * Default to `false`
65
- */
66
- gzip?: boolean;
67
- /**
68
- * try use gzip files, loaded from disk, like nginx gzip_static
69
- * Default to `false`
70
- */
71
- usePrecompiledGzip?: boolean;
72
- /**
73
- * object map of aliases
74
- * Default to `{}`
75
- */
76
- alias?: Record<string, string>;
77
- /**
78
- * the url prefix you wish to add
79
- * Default to `''`
80
- */
81
- prefix?: string;
82
- /**
83
- * filter files at init dir, for example - skip non build (source) files.
84
- * If array set - allow only listed files
85
- * Default to `undefined`
86
- */
87
- filter?: FileFilter | string[];
88
- /**
89
- * dynamic load file which not cached on initialization
90
- * Default to `false
91
- */
92
- dynamic?: boolean;
93
- /**
94
- * caches the assets on initialization or not,
95
- * always work together with `options.dynamic`
96
- * Default to `true`
97
- */
98
- preload?: boolean;
99
- /**
100
- * file store for caching
101
- * Default to `undefined`
102
- */
103
- files?: FileMap | FileStore;
104
- }
105
-
106
- type Next = () => Promise<void>;
107
-
108
- export class FileManager {
109
- store?: FileStore;
110
- map?: FileMap;
111
-
112
- constructor(store?: FileStore | FileMap) {
113
- if (store && typeof store.set === 'function' && typeof store.get === 'function') {
114
- this.store = store as FileStore;
115
- } else {
116
- this.map = store || Object.create(null);
117
- }
118
- }
119
-
120
- get(key: string) {
121
- return this.store ? this.store.get(key) : this.map![key];
122
- }
123
-
124
- set(key: string, value: FileMeta) {
125
- if (this.store) {
126
- return this.store.set(key, value);
127
- }
128
- this.map![key] = value;
129
- }
130
- }
131
-
132
- type MiddlewareFunc = (ctx: any, next: Next) => Promise<void> | void;
133
-
134
- export function staticCache(): MiddlewareFunc;
135
- export function staticCache(dir: string): MiddlewareFunc;
136
- export function staticCache(options: Options): MiddlewareFunc;
137
- export function staticCache(dir: string, options: Options): MiddlewareFunc;
138
- export function staticCache(dir: string, options: Options, files: FileMap | FileStore): MiddlewareFunc;
139
- export function staticCache(
140
- dirOrOptions?: string | Options,
141
- options: Options = {},
142
- filesStoreOrMap?: FileMap | FileStore,
143
- ): MiddlewareFunc {
144
- let dir = '';
145
- if (typeof dirOrOptions === 'string') {
146
- // dir priority than options.dir
147
- dir = dirOrOptions;
148
- } else if (dirOrOptions) {
149
- options = dirOrOptions;
150
- }
151
- if (!dir && options.dir) {
152
- dir = options.dir;
153
- }
154
- if (!dir) {
155
- // default to process.cwd
156
- dir = process.cwd();
157
- }
158
- dir = path.normalize(dir);
159
- debug('staticCache dir: %s', dir);
160
-
161
- // prefix must be ASCII code
162
- options.prefix = (options.prefix ?? '').replace(/\/*$/, '/');
163
- const files = new FileManager(filesStoreOrMap ?? options.files);
164
- const enableGzip = !!options.gzip;
165
- const filePrefix = path.normalize(options.prefix.replace(/^\//, ''));
166
-
167
- // option.filter
168
- let fileFilter: FileFilter = () => { return true; };
169
- if (Array.isArray(options.filter)) {
170
- fileFilter = (file: string) => {
171
- return (options.filter as string[]).includes(file);
172
- };
173
- }
174
- if (typeof options.filter === 'function') {
175
- fileFilter = options.filter;
176
- }
177
-
178
- if (options.preload !== false) {
179
- readDir(dir).filter(fileFilter).forEach(name => {
180
- loadFile(name, dir, options, files);
181
- });
182
- }
183
-
184
- return async (ctx: any, next: Next) => {
185
- // only accept HEAD and GET
186
- if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return await next();
187
- // check prefix first to avoid calculate
188
- if (!ctx.path.startsWith(options.prefix)) return await next();
189
-
190
- // decode for `/%E4%B8%AD%E6%96%87`
191
- // normalize for `//index`
192
- let filename = path.normalize(safeDecodeURIComponent(ctx.path));
193
-
194
- // check alias
195
- if (options.alias && options.alias[filename]) {
196
- filename = options.alias[filename];
197
- }
198
-
199
- let file = files.get(filename) as FileMeta;
200
- // try to load file
201
- if (!file) {
202
- if (!options.dynamic) return await next();
203
- if (path.basename(filename)[0] === '.') return await next();
204
- if (filename.charAt(0) === path.sep) {
205
- filename = filename.slice(1);
206
- }
207
-
208
- // trim prefix
209
- if (options.prefix !== '/') {
210
- if (filename.indexOf(filePrefix) !== 0) {
211
- return await next();
212
- }
213
- filename = filename.slice(filePrefix.length);
214
- }
215
-
216
- const fullpath = path.join(dir, filename);
217
- // files that can be accessed should be under options.dir
218
- if (!fullpath.startsWith(dir)) {
219
- return await next();
220
- }
221
-
222
- const stats = await exists(fullpath);
223
- if (!stats) return await next();
224
- if (!stats.isFile()) return await next();
225
-
226
- file = loadFile(filename, dir, options, files);
227
- }
228
-
229
- ctx.status = 200;
230
-
231
- if (enableGzip) ctx.vary('Accept-Encoding');
232
-
233
- if (!file.buffer) {
234
- const stats = await fs.stat(file.path!);
235
- if (stats.mtime.getTime() !== file.mtime!.getTime()) {
236
- file.mtime = stats.mtime;
237
- file.md5 = undefined;
238
- file.length = stats.size;
239
- }
240
- }
241
-
242
- ctx.response.lastModified = file.mtime;
243
- if (file.md5) {
244
- ctx.response.etag = file.md5;
245
- }
246
-
247
- if (ctx.fresh) {
248
- ctx.status = 304;
249
- return;
250
- }
251
-
252
- ctx.type = file.type;
253
- ctx.length = file.zipBuffer ? file.zipBuffer.length : file.length!;
254
- ctx.set('cache-control', file.cacheControl ?? 'public, max-age=' + file.maxAge);
255
- if (file.md5) ctx.set('content-md5', file.md5);
256
-
257
- if (ctx.method === 'HEAD') {
258
- return;
259
- }
260
-
261
- const acceptGzip = ctx.acceptsEncodings('gzip') === 'gzip';
262
-
263
- if (file.zipBuffer) {
264
- if (acceptGzip) {
265
- ctx.set('content-encoding', 'gzip');
266
- ctx.body = file.zipBuffer;
267
- } else {
268
- ctx.body = file.buffer;
269
- }
270
- return;
271
- }
272
-
273
- const shouldGzip = enableGzip
274
- && file.length! > 1024
275
- && acceptGzip
276
- && file.type && compressible(file.type);
277
-
278
- if (file.buffer) {
279
- if (shouldGzip) {
280
-
281
- const gzFile = files.get(filename + '.gz') as FileMeta;
282
- if (options.usePrecompiledGzip && gzFile && gzFile.buffer) {
283
- // if .gz file already read from disk
284
- file.zipBuffer = gzFile.buffer;
285
- } else {
286
- file.zipBuffer = await gzip(file.buffer);
287
- }
288
- ctx.set('content-encoding', 'gzip');
289
- ctx.body = file.zipBuffer;
290
- } else {
291
- ctx.body = file.buffer;
292
- }
293
- return;
294
- }
295
-
296
- const stream = createReadStream(file.path!);
297
-
298
- // update file hash
299
- if (!file.md5) {
300
- const hash = crypto.createHash('md5');
301
- stream.on('data', hash.update.bind(hash));
302
- stream.on('end', () => {
303
- file.md5 = hash.digest('base64');
304
- });
305
- }
306
-
307
- ctx.body = stream;
308
- // enable gzip will remove content length
309
- if (shouldGzip) {
310
- ctx.remove('content-length');
311
- ctx.set('content-encoding', 'gzip');
312
- ctx.body = stream.pipe(zlib.createGzip());
313
- }
314
- };
315
- }
316
-
317
- /**
318
- * load file and add file content to cache
319
- */
320
- function loadFile(name: string, dir: string, options: Options, fileManager: FileManager) {
321
- const pathname = path.normalize(path.join(options.prefix!, name));
322
- if (!fileManager.get(pathname)) {
323
- fileManager.set(pathname, {});
324
- }
325
- const obj = fileManager.get(pathname) as FileMeta;
326
- const filename = obj.path = path.join(dir, name);
327
- const stats = statSync(filename);
328
- const buffer = readFileSync(filename);
329
-
330
- obj.cacheControl = typeof options.cacheControl === 'function' ? options.cacheControl(filename) : options.cacheControl; // if cacheControl is a function, it will be called with the filename
331
- obj.maxAge = (typeof obj.maxAge === 'number' ? obj.maxAge : options.maxAge) || 0;
332
- obj.type = obj.mime = mime.lookup(pathname) || 'application/octet-stream';
333
- obj.mtime = stats.mtime;
334
- obj.length = stats.size;
335
- obj.md5 = crypto.createHash('md5').update(buffer).digest('base64');
336
-
337
- debug('file: %s', JSON.stringify(obj, null, 2));
338
- if (options.buffer) {
339
- obj.buffer = buffer;
340
- }
341
- return obj;
342
- }