koa-files 2.3.8 → 3.0.1

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.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @package koa-files
3
+ * @license MIT
4
+ * @version 3.0.1
5
+ * @author nuintun <nuintun@qq.com>
6
+ * @description A static files serving middleware for koa.
7
+ * @see https://github.com/nuintun/koa-files#readme
8
+ */
9
+
10
+ import { generate } from './hash.js';
11
+ import parseRange from 'range-parser';
12
+
13
+ /**
14
+ * @module http
15
+ */
16
+ /**
17
+ * @function isETag
18
+ * @description Check if etag is valid.
19
+ * @param value The value to check.
20
+ */
21
+ function isETag(value) {
22
+ return /^(?:W\/)?"[\s\S]+"$/.test(value);
23
+ }
24
+ /**
25
+ * @function parseTokens
26
+ * @description Parse HTTP tokens.
27
+ * @param value The tokens value string.
28
+ */
29
+ function parseTokens(value) {
30
+ let end = 0;
31
+ let start = 0;
32
+ let tokens = [];
33
+ // Gather tokens.
34
+ for (let i = 0, length = value.length; i < length; i++) {
35
+ switch (value.charCodeAt(i)) {
36
+ case 0x20:
37
+ // ' '.
38
+ if (start === end) {
39
+ start = end = i + 1;
40
+ }
41
+ break;
42
+ case 0x2c:
43
+ // ','.
44
+ tokens.push(value.substring(start, end));
45
+ start = end = i + 1;
46
+ break;
47
+ default:
48
+ end = i + 1;
49
+ break;
50
+ }
51
+ }
52
+ // Final token.
53
+ tokens.push(value.substring(start, end));
54
+ return tokens;
55
+ }
56
+ /**
57
+ * @function isRangeFresh
58
+ * @description Check if request range fresh.
59
+ * @param context Koa context.
60
+ */
61
+ function isRangeFresh(context) {
62
+ const { request, response } = context;
63
+ const ifRange = request.get('If-Range');
64
+ // No If-Range.
65
+ if (!ifRange) {
66
+ return true;
67
+ }
68
+ // If-Range as etag.
69
+ if (isETag(ifRange)) {
70
+ const etag = response.get('ETag');
71
+ return !!(etag && isETagFresh(ifRange, etag));
72
+ }
73
+ // If-Range as modified date.
74
+ const lastModified = response.get('Last-Modified');
75
+ return Date.parse(lastModified) <= Date.parse(ifRange);
76
+ }
77
+ /**
78
+ * @function isETagFresh
79
+ * @description Check if etag is fresh.
80
+ * @param match The match value.
81
+ * @param etag The etag value.
82
+ */
83
+ function isETagFresh(match, etag) {
84
+ return parseTokens(match).some(match => {
85
+ return match === etag || match === 'W/' + etag || 'W/' + match === etag;
86
+ });
87
+ }
88
+ /**
89
+ * @function decodeURI
90
+ * @description Decode URI component.
91
+ * @param URI The URI to decode.
92
+ */
93
+ function decodeURI(URI) {
94
+ try {
95
+ return decodeURIComponent(URI);
96
+ } catch (_a) {
97
+ return -1;
98
+ }
99
+ }
100
+ /**
101
+ * @function isConditionalGET
102
+ * @description Check if request is conditional GET.
103
+ * @param context Koa context.
104
+ */
105
+ function isConditionalGET(context) {
106
+ const { request } = context;
107
+ return !!(
108
+ request.get('If-Match') ||
109
+ request.get('If-None-Match') ||
110
+ request.get('If-Modified-Since') ||
111
+ request.get('If-Unmodified-Since')
112
+ );
113
+ }
114
+ /**
115
+ * @function isPreconditionFailure
116
+ * @description Check if request precondition failure.
117
+ * @param context Koa context.
118
+ */
119
+ function isPreconditionFailure({ request, response }) {
120
+ // If-Match.
121
+ const match = request.get('If-Match');
122
+ // Check if request match.
123
+ if (match) {
124
+ // Etag.
125
+ const etag = response.get('ETag');
126
+ return !etag || match === '*' || !isETagFresh(match, etag);
127
+ }
128
+ // If-Unmodified-Since.
129
+ const unmodifiedSince = Date.parse(request.get('If-Unmodified-Since'));
130
+ // Check if request unmodified.
131
+ if (!Number.isNaN(unmodifiedSince)) {
132
+ // Last-Modified.
133
+ const lastModified = Date.parse(response.get('Last-Modified'));
134
+ return Number.isNaN(lastModified) || lastModified > unmodifiedSince;
135
+ }
136
+ // Check precondition passed.
137
+ return false;
138
+ }
139
+ /**
140
+ * @function parseRanges
141
+ * @description Parse ranges.
142
+ * @param context Koa context.
143
+ * @param stats File stats.
144
+ */
145
+ function parseRanges(context, stats) {
146
+ const { size } = stats;
147
+ // Range support.
148
+ if (/^bytes$/i.test(context.response.get('Accept-Ranges'))) {
149
+ const range = context.request.get('Range');
150
+ // Range fresh.
151
+ if (range && isRangeFresh(context)) {
152
+ // Parse range -1 -2 or [].
153
+ const parsed = parseRange(size, range, { combine: true });
154
+ // -1 signals an unsatisfiable range.
155
+ // -2 signals a malformed header string.
156
+ if (parsed === -1 || parsed === -2) {
157
+ return parsed;
158
+ }
159
+ // Ranges ok, support multiple ranges.
160
+ if (parsed.type === 'bytes') {
161
+ // Set 206 status.
162
+ context.status = 206;
163
+ const { length } = parsed;
164
+ // Multiple ranges.
165
+ if (length > 1) {
166
+ // Content-Length.
167
+ let contentLength = 0;
168
+ // Ranges.
169
+ const ranges = [];
170
+ // Range boundary.
171
+ const boundary = `<${generate()}>`;
172
+ // Range suffix.
173
+ const suffix = `\r\n--${boundary}--\r\n`;
174
+ // Multipart Content-Type.
175
+ const contentType = `Content-Type: ${context.type}`;
176
+ // Override Content-Type.
177
+ context.type = `multipart/byteranges; boundary=${boundary}`;
178
+ // Map ranges.
179
+ for (let index = 0; index < length; index++) {
180
+ const { start, end } = parsed[index];
181
+ // The first prefix boundary no \r\n.
182
+ const head = index > 0 ? '\r\n' : '';
183
+ const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
184
+ const prefix = `${head}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`;
185
+ // Compute Content-Length
186
+ contentLength += end - start + 1 + Buffer.byteLength(prefix);
187
+ // Cache range.
188
+ ranges.push({ start, end, prefix });
189
+ }
190
+ // The last add suffix boundary.
191
+ ranges[length - 1].suffix = suffix;
192
+ // Compute Content-Length.
193
+ contentLength += Buffer.byteLength(suffix);
194
+ // Set Content-Length.
195
+ context.length = contentLength;
196
+ // Return ranges.
197
+ return ranges;
198
+ } else {
199
+ const [{ start, end }] = parsed;
200
+ // Set Content-Length.
201
+ context.length = end - start + 1;
202
+ // Set Content-Range.
203
+ context.set('Content-Range', `bytes ${start}-${end}/${size}`);
204
+ // Return ranges.
205
+ return parsed;
206
+ }
207
+ }
208
+ }
209
+ }
210
+ // Set Content-Length.
211
+ context.length = size;
212
+ // Return ranges.
213
+ return [{ start: 0, end: Math.max(size - 1) }];
214
+ }
215
+
216
+ export { decodeURI, isConditionalGET, isPreconditionFailure, parseRanges };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @package koa-files
3
+ * @license MIT
4
+ * @version 3.0.1
5
+ * @author nuintun <nuintun@qq.com>
6
+ * @description A static files serving middleware for koa.
7
+ * @see https://github.com/nuintun/koa-files#readme
8
+ */
9
+
10
+ import { relative, isAbsolute } from 'path';
11
+
12
+ /**
13
+ * @module path
14
+ */
15
+ /**
16
+ * @function unixify
17
+ * @description Convert path to unix style.
18
+ * @param path The path to convert.
19
+ */
20
+ function unixify(path) {
21
+ return path.replace(/\\/g, '/');
22
+ }
23
+ /**
24
+ * @function hasTrailingSlash
25
+ * @description Check if path has trailing slash.
26
+ * @param path The path to check.
27
+ */
28
+ function hasTrailingSlash(path) {
29
+ return /\/$/.test(path);
30
+ }
31
+ /**
32
+ * @function isOutRoot
33
+ * @description Check if path is out of root.
34
+ * @param path The path to check.
35
+ * @param root The root path.
36
+ */
37
+ function isOutRoot(path, root) {
38
+ path = relative(root, path);
39
+ return /\.\.(?:[\\/]|$)/.test(path) || isAbsolute(path);
40
+ }
41
+
42
+ export { hasTrailingSlash, isOutRoot, unixify };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa-files",
3
- "version": "2.3.8",
3
+ "version": "3.0.1",
4
4
  "description": "A static files serving middleware for koa.",
5
5
  "type": "module",
6
6
  "main": "cjs/index.cjs",
@@ -51,13 +51,13 @@
51
51
  "format": "prettier --write . --ignore-path .prettierignore",
52
52
  "build": "npm run clean && npm run build:main && npm run format",
53
53
  "build:main": "npm run build:cjs && npm run build:esm && npm run build:types",
54
- "build:types": "tsc --declaration --emitDeclarationOnly --declarationDir types"
54
+ "build:types": "tsc --declaration --emitDeclarationOnly --declarationDir types && node tools/fix-types.js"
55
55
  },
56
56
  "dependencies": {
57
57
  "destroy": "^1.2.0",
58
58
  "etag": "^1.8.1",
59
59
  "range-parser": "^1.2.1",
60
- "tslib": "^2.6.2"
60
+ "tslib": "^2.6.3"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@rollup/plugin-typescript": "^11.1.6",
@@ -65,12 +65,13 @@
65
65
  "@types/etag": "^1.8.3",
66
66
  "@types/koa": "^2.15.0",
67
67
  "@types/range-parser": "^1.2.7",
68
- "koa": "^2.15.1",
69
- "magic-string": "^0.30.8",
70
- "prettier": "^3.2.5",
71
- "rimraf": "^5.0.5",
72
- "rollup": "^4.13.0",
73
- "typescript": "^5.4.2"
68
+ "koa": "^2.15.3",
69
+ "magic-string": "^0.30.10",
70
+ "prettier": "^3.3.1",
71
+ "rimraf": "^5.0.7",
72
+ "rollup": "^4.18.0",
73
+ "tsc-alias": "^1.8.10",
74
+ "typescript": "^5.4.5"
74
75
  },
75
76
  "peerDependencies": {
76
77
  "koa": "^2.0.0"
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @module Service
3
+ */
4
+ /// <reference types="node" />
5
+ import { Stats } from 'fs';
6
+ import { Context } from 'koa';
7
+ import { FileSystem } from './utils/fs';
8
+ interface IgnoreFunction {
9
+ (path: string): boolean;
10
+ }
11
+ interface Headers {
12
+ [key: string]: string | string[];
13
+ }
14
+ interface HeaderFunction {
15
+ (path: string, stats: Stats): Headers | void;
16
+ }
17
+ export interface Options {
18
+ fs: FileSystem;
19
+ etag?: boolean;
20
+ acceptRanges?: boolean;
21
+ lastModified?: boolean;
22
+ ignore?: IgnoreFunction;
23
+ headers?: Headers | HeaderFunction;
24
+ }
25
+ /**
26
+ * @class Service
27
+ */
28
+ export default class Service {
29
+ private root;
30
+ private options;
31
+ /**
32
+ * @constructor
33
+ * @description Create file service.
34
+ * @param root File service root.
35
+ * @param options File service options.
36
+ */
37
+ constructor(root: string, options: Options);
38
+ /**
39
+ * @private
40
+ * @method isIgnore
41
+ * @description Check if path is ignore.
42
+ * @param path File path.
43
+ */
44
+ private isIgnore;
45
+ /**
46
+ * @private
47
+ * @method setupHeaders
48
+ * @description Setup headers
49
+ * @param context Koa context
50
+ * @param path File path
51
+ * @param stats File stats
52
+ */
53
+ private setupHeaders;
54
+ /**
55
+ * @private
56
+ * @method write
57
+ * @description Write file to stream.
58
+ * @param stream Destination stream.
59
+ * @param path The file path to read.
60
+ * @param range The range to read.
61
+ * @param end Is destory destination stream after read complete.
62
+ */
63
+ private write;
64
+ /**
65
+ * @private
66
+ * @method send
67
+ * @description Send file.
68
+ * @param context Koa context.
69
+ * @param path File path.
70
+ * @param ranges Read ranges.
71
+ */
72
+ private send;
73
+ /**
74
+ * @public
75
+ * @method response
76
+ * @description Response to koa context.
77
+ * @param context Koa context.
78
+ */
79
+ response(context: Context): Promise<boolean>;
80
+ }
81
+ export {};
package/types/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * @module index
3
- * @license MIT
4
- * @author nuintun
5
3
  */
6
4
  import { Middleware } from 'koa';
7
- import { Options as FilesOptions } from './Files';
8
- export interface Options extends FilesOptions {
5
+ import { FileSystem } from './utils/fs';
6
+ import { Options as ServiceOptions } from './Service';
7
+ export interface Options extends Omit<ServiceOptions, 'fs'> {
8
+ fs?: FileSystem;
9
9
  defer?: boolean;
10
10
  }
11
11
  /**
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @module fs
3
+ */
4
+ /// <reference types="node" />
5
+ /// <reference types="node" />
6
+ import { ReadStream, Stats } from 'fs';
7
+ type PathLike = string | Buffer | URL;
8
+ /**
9
+ * @function stat
10
+ * @description Get file stats.
11
+ * @param fs The file system to used.
12
+ * @param path The file path.
13
+ */
14
+ export declare function stat(fs: FileSystem, path: string): Promise<Stats | null>;
15
+ export interface FileSystem {
16
+ stat(path: PathLike, callback: (error: Error | null, stats: Stats) => void): void;
17
+ createReadStream(
18
+ path: PathLike,
19
+ options?: {
20
+ start?: number;
21
+ end?: number;
22
+ }
23
+ ): ReadStream;
24
+ }
25
+ export {};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @module hash
3
+ */
4
+ /**
5
+ * @function generate
6
+ * @description Generate a hash.
7
+ * @param length The length of hash.
8
+ */
9
+ export declare function generate(length?: number): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @module http
3
+ */
4
+ /// <reference types="node" />
5
+ import { Stats } from 'fs';
6
+ import { Context } from 'koa';
7
+ export interface Range {
8
+ start: number;
9
+ end?: number;
10
+ prefix?: string;
11
+ suffix?: string;
12
+ }
13
+ type Ranges = Range[] | -1 | -2;
14
+ /**
15
+ * @function decodeURI
16
+ * @description Decode URI component.
17
+ * @param URI The URI to decode.
18
+ */
19
+ export declare function decodeURI(URI: string): string | -1;
20
+ /**
21
+ * @function isConditionalGET
22
+ * @description Check if request is conditional GET.
23
+ * @param context Koa context.
24
+ */
25
+ export declare function isConditionalGET(context: Context): boolean;
26
+ /**
27
+ * @function isPreconditionFailure
28
+ * @description Check if request precondition failure.
29
+ * @param context Koa context.
30
+ */
31
+ export declare function isPreconditionFailure({ request, response }: Context): boolean;
32
+ /**
33
+ * @function parseRanges
34
+ * @description Parse ranges.
35
+ * @param context Koa context.
36
+ * @param stats File stats.
37
+ */
38
+ export declare function parseRanges(context: Context, stats: Stats): Ranges;
39
+ export {};
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @module path
3
+ */
4
+ /**
5
+ * @function unixify
6
+ * @description Convert path to unix style.
7
+ * @param path The path to convert.
8
+ */
9
+ export declare function unixify(path: string): string;
10
+ /**
11
+ * @function hasTrailingSlash
12
+ * @description Check if path has trailing slash.
13
+ * @param path The path to check.
14
+ */
15
+ export declare function hasTrailingSlash(path: string): boolean;
16
+ /**
17
+ * @function isOutRoot
18
+ * @description Check if path is out of root.
19
+ * @param path The path to check.
20
+ * @param root The root path.
21
+ */
22
+ export declare function isOutRoot(path: string, root: string): boolean;