koa-files 2.0.0 → 2.1.2
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/cjs/Send.js +354 -0
- package/cjs/index.js +28 -0
- package/cjs/utils.js +138 -0
- package/esm/Send.js +352 -0
- package/esm/index.js +26 -0
- package/esm/utils.js +129 -0
- package/package.json +28 -14
- package/typings/Send.d.ts +69 -69
- package/typings/index.d.ts +1 -1
- package/.prettierignore +0 -5
- package/index.js +0 -512
package/cjs/Send.js
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var etag = require('etag');
|
|
4
|
+
var destroy = require('destroy');
|
|
5
|
+
var stream = require('stream');
|
|
6
|
+
var fs = require('fs');
|
|
7
|
+
var path = require('path');
|
|
8
|
+
var parseRange = require('range-parser');
|
|
9
|
+
var utils = require('./utils.js');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @module Send
|
|
13
|
+
* @license MIT
|
|
14
|
+
* @author nuintun
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @class Send
|
|
18
|
+
*/
|
|
19
|
+
class Send {
|
|
20
|
+
/**
|
|
21
|
+
* @constructor
|
|
22
|
+
* @param {Context} ctx
|
|
23
|
+
* @param {string} root
|
|
24
|
+
* @param {Options} options
|
|
25
|
+
*/
|
|
26
|
+
constructor(ctx, root = '.', options) {
|
|
27
|
+
let { cacheControl } = options;
|
|
28
|
+
const path$1 = utils.decodeURI(ctx.path);
|
|
29
|
+
this.ctx = ctx;
|
|
30
|
+
this.root = utils.unixify(path.resolve(root));
|
|
31
|
+
this.options = Object.assign(Object.assign({}, options), { cacheControl });
|
|
32
|
+
this.path = path$1 === -1 ? -1 : utils.unixify(path.join(this.root, path$1));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @method isConditionalGET
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
isConditionalGET() {
|
|
39
|
+
const { request } = this.ctx;
|
|
40
|
+
return !!(
|
|
41
|
+
request.get('If-Match') ||
|
|
42
|
+
request.get('If-None-Match') ||
|
|
43
|
+
request.get('If-Modified-Since') ||
|
|
44
|
+
request.get('if-Unmodified-Since')
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* @method isPreconditionFailure
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
isPreconditionFailure() {
|
|
52
|
+
const { request, response } = this.ctx;
|
|
53
|
+
// If-Match
|
|
54
|
+
const match = request.get('If-Match');
|
|
55
|
+
if (match) {
|
|
56
|
+
const etag = response.get('ETag');
|
|
57
|
+
return !etag || (match !== '*' && !utils.isETagFresh(match, etag));
|
|
58
|
+
}
|
|
59
|
+
// If-Unmodified-Since
|
|
60
|
+
const unmodifiedSince = Date.parse(request.get('If-Unmodified-Since'));
|
|
61
|
+
if (!isNaN(unmodifiedSince)) {
|
|
62
|
+
const lastModified = Date.parse(response.get('Last-Modified'));
|
|
63
|
+
return isNaN(lastModified) || lastModified > unmodifiedSince;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* @method isRangeFresh
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
isRangeFresh() {
|
|
72
|
+
const { request, response } = this.ctx;
|
|
73
|
+
const ifRange = request.get('If-Range');
|
|
74
|
+
// No If-Range
|
|
75
|
+
if (!ifRange) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
// If-Range as etag
|
|
79
|
+
if (utils.isETag(ifRange)) {
|
|
80
|
+
const etag = response.get('ETag');
|
|
81
|
+
return !!(etag && utils.isETagFresh(ifRange, etag));
|
|
82
|
+
}
|
|
83
|
+
// If-Range as modified date
|
|
84
|
+
const lastModified = response.get('Last-Modified');
|
|
85
|
+
return Date.parse(lastModified) <= Date.parse(ifRange);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @method isIgnore
|
|
89
|
+
* @param {string} path
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
isIgnore(path) {
|
|
93
|
+
const { ignore } = this.options;
|
|
94
|
+
return (typeof ignore === 'function' ? ignore(path) : false) === true;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* @method parseRange
|
|
98
|
+
* @param {Stats} stats
|
|
99
|
+
* @returns {Ranges}
|
|
100
|
+
*/
|
|
101
|
+
parseRange(stats) {
|
|
102
|
+
const { ctx } = this;
|
|
103
|
+
const result = [];
|
|
104
|
+
const { size } = stats;
|
|
105
|
+
const { request } = ctx;
|
|
106
|
+
// Content-Length
|
|
107
|
+
let contentLength = size;
|
|
108
|
+
// Range support
|
|
109
|
+
if (this.options.acceptRanges !== false) {
|
|
110
|
+
const range = request.get('Range');
|
|
111
|
+
// Range fresh
|
|
112
|
+
if (range && this.isRangeFresh()) {
|
|
113
|
+
// Parse range -1 -2 or []
|
|
114
|
+
const ranges = parseRange(size, range, { combine: true });
|
|
115
|
+
// -1 signals an unsatisfiable range
|
|
116
|
+
// -2 signals a malformed header string
|
|
117
|
+
if (ranges === -1 || ranges === -2) {
|
|
118
|
+
return ranges;
|
|
119
|
+
}
|
|
120
|
+
// Ranges ok, support multiple ranges
|
|
121
|
+
if (ranges.type === 'bytes') {
|
|
122
|
+
// Set 206 status
|
|
123
|
+
ctx.status = 206;
|
|
124
|
+
// Multiple ranges
|
|
125
|
+
if (ranges.length > 1) {
|
|
126
|
+
// Reset content-length
|
|
127
|
+
contentLength = 0;
|
|
128
|
+
// Range boundary
|
|
129
|
+
const boundary = `<${utils.boundaryGenerator()}>`;
|
|
130
|
+
const suffix = `\r\n--${boundary}--\r\n`;
|
|
131
|
+
const contentType = `Content-Type: ${ctx.type}`;
|
|
132
|
+
ctx.type = `multipart/byteranges; boundary=${boundary}`;
|
|
133
|
+
// Map ranges
|
|
134
|
+
ranges.forEach(({ start, end }, index) => {
|
|
135
|
+
// The first prefix boundary no \r\n
|
|
136
|
+
const prefixHead = index > 0 ? '\r\n' : '';
|
|
137
|
+
const contentRange = `Content-Range: bytes ${start}-${end}/${size}`;
|
|
138
|
+
const prefix = `${prefixHead}--${boundary}\r\n${contentType}\r\n${contentRange}\r\n\r\n`;
|
|
139
|
+
// Compute content-length
|
|
140
|
+
contentLength += end - start + 1 + Buffer.byteLength(prefix);
|
|
141
|
+
// Cache range
|
|
142
|
+
result.push({ start, end, prefix });
|
|
143
|
+
});
|
|
144
|
+
// The last add suffix boundary
|
|
145
|
+
result[result.length - 1].suffix = suffix;
|
|
146
|
+
// Compute content-length
|
|
147
|
+
contentLength += Buffer.byteLength(suffix);
|
|
148
|
+
} else {
|
|
149
|
+
const { start, end } = ranges[0];
|
|
150
|
+
ctx.set('Content-Range', `bytes ${start}-${end}/${size}`);
|
|
151
|
+
// Compute content-length
|
|
152
|
+
contentLength = end - start + 1;
|
|
153
|
+
// Cache range
|
|
154
|
+
result.push({ start, end });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Set Content-Length
|
|
160
|
+
ctx.length = contentLength;
|
|
161
|
+
return result.length ? result : [{ start: 0 }];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* @method setupHeaders
|
|
165
|
+
* @param {string} path
|
|
166
|
+
* @param {Stats} stats
|
|
167
|
+
*/
|
|
168
|
+
setupHeaders(path$1, stats) {
|
|
169
|
+
const { ctx, options } = this;
|
|
170
|
+
const { toString } = Object.prototype;
|
|
171
|
+
const { acceptRanges, cacheControl, lastModified } = options;
|
|
172
|
+
// Set status
|
|
173
|
+
ctx.status = 200;
|
|
174
|
+
// Set Content-Type
|
|
175
|
+
ctx.type = path.extname(path$1);
|
|
176
|
+
// ETag
|
|
177
|
+
if (options.etag !== false) {
|
|
178
|
+
// Set ETag
|
|
179
|
+
ctx.set('ETag', etag(stats));
|
|
180
|
+
}
|
|
181
|
+
// Accept-Ranges
|
|
182
|
+
if (acceptRanges !== false) {
|
|
183
|
+
// Set Accept-Ranges
|
|
184
|
+
ctx.set('Accept-Ranges', 'bytes');
|
|
185
|
+
}
|
|
186
|
+
// Last-Modified
|
|
187
|
+
if (lastModified !== false) {
|
|
188
|
+
// Set mtime utc string
|
|
189
|
+
ctx.set('Last-Modified', stats.mtime.toUTCString());
|
|
190
|
+
}
|
|
191
|
+
// Cache-Control
|
|
192
|
+
if (cacheControl && toString.call(cacheControl) === '[object String]') {
|
|
193
|
+
// Set Cache-Control
|
|
194
|
+
ctx.set('Cache-Control', cacheControl);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* @method read
|
|
199
|
+
* @param {string} path
|
|
200
|
+
* @param {Range} range
|
|
201
|
+
* @param {PassThrough} buffer
|
|
202
|
+
* @param {boolean} end
|
|
203
|
+
* @returns {Promise<true>}
|
|
204
|
+
*/
|
|
205
|
+
read(path, range, buffer, end) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
// Create file stream
|
|
208
|
+
const file = fs.createReadStream(path, range);
|
|
209
|
+
// File read stream open
|
|
210
|
+
if (range.prefix) {
|
|
211
|
+
file.once('open', () => {
|
|
212
|
+
// Write prefix boundary
|
|
213
|
+
buffer.write(range.prefix);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// File read stream error
|
|
217
|
+
file.once('error', error => {
|
|
218
|
+
// Unpipe
|
|
219
|
+
file.unpipe(buffer);
|
|
220
|
+
// Destroy file stream
|
|
221
|
+
destroy(file);
|
|
222
|
+
// Reject
|
|
223
|
+
reject(error);
|
|
224
|
+
});
|
|
225
|
+
// File read stream end
|
|
226
|
+
if (range.suffix) {
|
|
227
|
+
file.once('end', () => {
|
|
228
|
+
// Push suffix boundary
|
|
229
|
+
buffer.write(range.suffix);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
// File read stream close
|
|
233
|
+
file.once('close', () => {
|
|
234
|
+
// Unpipe
|
|
235
|
+
file.unpipe(buffer);
|
|
236
|
+
// Destroy file stream
|
|
237
|
+
destroy(file);
|
|
238
|
+
// Resolve
|
|
239
|
+
resolve(true);
|
|
240
|
+
});
|
|
241
|
+
// Write data to buffer
|
|
242
|
+
file.pipe(buffer, { end });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* @method send
|
|
247
|
+
* @param {string} path
|
|
248
|
+
* @param {Range[]} ranges
|
|
249
|
+
*/
|
|
250
|
+
async send(path, ranges) {
|
|
251
|
+
// Ranges count
|
|
252
|
+
let count = ranges.length;
|
|
253
|
+
// Set stream body, highWaterMark 64kb
|
|
254
|
+
const stream$1 = new stream.PassThrough({ highWaterMark: 65536 });
|
|
255
|
+
// Set response body
|
|
256
|
+
this.ctx.body = stream$1;
|
|
257
|
+
// Read file ranges
|
|
258
|
+
try {
|
|
259
|
+
for (const range of ranges) {
|
|
260
|
+
await this.read(path, range, stream$1, --count === 0);
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// End stream when read exception
|
|
264
|
+
stream$1.end();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* @method response
|
|
269
|
+
* @returns {Promise<boolean>}
|
|
270
|
+
*/
|
|
271
|
+
async response() {
|
|
272
|
+
const { ctx, root, path } = this;
|
|
273
|
+
// Only support GET and HEAD (405)
|
|
274
|
+
if (ctx.method !== 'GET' && ctx.method !== 'HEAD') {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
// Path -1 or null byte(s)
|
|
278
|
+
if (path === -1 || path.includes('\0')) {
|
|
279
|
+
return ctx.throw(400);
|
|
280
|
+
}
|
|
281
|
+
// Malicious path (403)
|
|
282
|
+
if (utils.isOutRoot(path, root)) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
// Is ignore path or file (403)
|
|
286
|
+
if (this.isIgnore(path)) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
// File stats
|
|
290
|
+
let stats;
|
|
291
|
+
// Get file stats
|
|
292
|
+
try {
|
|
293
|
+
stats = await utils.fstat(path);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// 404 | 500
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
// File not exist (404 | 500)
|
|
299
|
+
if (!stats) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
// Is directory (403)
|
|
303
|
+
if (stats.isDirectory()) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
// Not a directory but has trailing slash (404)
|
|
307
|
+
if (utils.hasTrailingSlash(path)) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
// Setup headers
|
|
311
|
+
this.setupHeaders(path, stats);
|
|
312
|
+
// Conditional get support
|
|
313
|
+
if (this.isConditionalGET()) {
|
|
314
|
+
// Request precondition failure
|
|
315
|
+
if (this.isPreconditionFailure()) {
|
|
316
|
+
return ctx.throw(412);
|
|
317
|
+
}
|
|
318
|
+
// Request fresh (304)
|
|
319
|
+
if (ctx.fresh) {
|
|
320
|
+
// Set status
|
|
321
|
+
ctx.status = 304;
|
|
322
|
+
// Set body null
|
|
323
|
+
ctx.body = null;
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// Head request
|
|
328
|
+
if (ctx.method === 'HEAD') {
|
|
329
|
+
// Set content-length
|
|
330
|
+
ctx.length = stats.size;
|
|
331
|
+
// Set body null
|
|
332
|
+
ctx.body = null;
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
// Parse ranges
|
|
336
|
+
const ranges = this.parseRange(stats);
|
|
337
|
+
// 416
|
|
338
|
+
if (ranges === -1) {
|
|
339
|
+
// Set content-range
|
|
340
|
+
ctx.set('Content-Range', `bytes */${stats.size}`);
|
|
341
|
+
// Unsatisfiable 416
|
|
342
|
+
return ctx.throw(416);
|
|
343
|
+
}
|
|
344
|
+
// 400
|
|
345
|
+
if (ranges === -2) {
|
|
346
|
+
return ctx.throw(400);
|
|
347
|
+
}
|
|
348
|
+
// Send file
|
|
349
|
+
this.send(path, ranges);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
module.exports = Send;
|
package/cjs/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var Send = require('./Send.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module index
|
|
7
|
+
* @license MIT
|
|
8
|
+
* @author nuintun
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @function server
|
|
12
|
+
* @param {string} root
|
|
13
|
+
* @param {Options} options
|
|
14
|
+
*/
|
|
15
|
+
function server(root, options = {}) {
|
|
16
|
+
if (options.defer) {
|
|
17
|
+
return async (ctx, next) => {
|
|
18
|
+
await next();
|
|
19
|
+
await new Send(ctx, root, options).response();
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return async (ctx, next) => {
|
|
23
|
+
const respond = await new Send(ctx, root, options).response();
|
|
24
|
+
!respond && (await next());
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = server;
|
package/cjs/utils.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @module utils
|
|
8
|
+
* @license MIT
|
|
9
|
+
* @author nuintun
|
|
10
|
+
*/
|
|
11
|
+
const CHARS = Array.from('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz');
|
|
12
|
+
/**
|
|
13
|
+
* @function isOutRoot
|
|
14
|
+
* @description Test path is out of bound of root
|
|
15
|
+
* @param {string} path
|
|
16
|
+
* @param {string} root
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function isOutRoot(path$1, root) {
|
|
20
|
+
path$1 = path.relative(root, path$1);
|
|
21
|
+
return /\.\.(?:[\\/]|$)/.test(path$1) || path.isAbsolute(path$1);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* @function unixify
|
|
25
|
+
* @description Convert path separators to posix/unix-style forward slashes
|
|
26
|
+
* @param {string} path
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
function unixify(path) {
|
|
30
|
+
return path.replace(/\\/g, '/');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @function decodeURI
|
|
34
|
+
* @description Decode URI component.
|
|
35
|
+
* @param {string} URI
|
|
36
|
+
* @returns {string|-1}
|
|
37
|
+
*/
|
|
38
|
+
function decodeURI(URI) {
|
|
39
|
+
try {
|
|
40
|
+
return decodeURIComponent(URI);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return -1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* @function boundaryGenerator
|
|
47
|
+
* @description Create boundary
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function boundaryGenerator() {
|
|
51
|
+
let boundary = '';
|
|
52
|
+
// Create boundary
|
|
53
|
+
for (let i = 0; i < 38; i++) {
|
|
54
|
+
boundary += CHARS[Math.floor(Math.random() * 62)];
|
|
55
|
+
}
|
|
56
|
+
// Return boundary
|
|
57
|
+
return boundary;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* @function fstat
|
|
61
|
+
* @param {string} path
|
|
62
|
+
* @returns {Promise<Stats>}
|
|
63
|
+
*/
|
|
64
|
+
function fstat(path) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
fs.stat(path, (error, stats) => {
|
|
67
|
+
error ? reject(error) : resolve(stats);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* @function hasTrailingSlash
|
|
73
|
+
* @param {string} path
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function hasTrailingSlash(path) {
|
|
77
|
+
return /\/$/.test(path);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* @function parseTokens
|
|
81
|
+
* @description Parse a HTTP tokens.
|
|
82
|
+
* @param {string[]} value
|
|
83
|
+
*/
|
|
84
|
+
function parseTokens(value) {
|
|
85
|
+
let start = 0;
|
|
86
|
+
let end = 0;
|
|
87
|
+
let tokens = [];
|
|
88
|
+
// gather tokens
|
|
89
|
+
for (let i = 0, length = value.length; i < length; i++) {
|
|
90
|
+
switch (value.charCodeAt(i)) {
|
|
91
|
+
case 0x20:
|
|
92
|
+
// ' '
|
|
93
|
+
if (start === end) {
|
|
94
|
+
start = end = i + 1;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case 0x2c:
|
|
98
|
+
// ','
|
|
99
|
+
tokens.push(value.substring(start, end));
|
|
100
|
+
start = end = i + 1;
|
|
101
|
+
break;
|
|
102
|
+
default:
|
|
103
|
+
end = i + 1;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// final token
|
|
108
|
+
tokens.push(value.substring(start, end));
|
|
109
|
+
return tokens;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @function isETagFresh
|
|
113
|
+
* @param {string} match
|
|
114
|
+
* @param {string} etag
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function isETagFresh(match, etag) {
|
|
118
|
+
return parseTokens(match).some(match => {
|
|
119
|
+
return match === etag || match === 'W/' + etag || 'W/' + match === etag;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* @function isETag
|
|
124
|
+
* @param {string} etag
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
function isETag(etag) {
|
|
128
|
+
return /(?:W\/)?"(?:[ !#-\x7E\x80-\xFF]*|\r\n[\t ]|\\.)*"/.test(etag);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
exports.boundaryGenerator = boundaryGenerator;
|
|
132
|
+
exports.decodeURI = decodeURI;
|
|
133
|
+
exports.fstat = fstat;
|
|
134
|
+
exports.hasTrailingSlash = hasTrailingSlash;
|
|
135
|
+
exports.isETag = isETag;
|
|
136
|
+
exports.isETagFresh = isETagFresh;
|
|
137
|
+
exports.isOutRoot = isOutRoot;
|
|
138
|
+
exports.unixify = unixify;
|