1
1
import path from "path" ;
2
2
3
3
import mime from "mime-types" ;
4
+ import parseRange from "range-parser" ;
4
5
5
6
import getFilenameFromUrl from "./utils/getFilenameFromUrl" ;
6
- import handleRangeHeaders from "./utils/handleRangeHeaders" ;
7
+ import {
8
+ getHeaderNames ,
9
+ getHeaderFromRequest ,
10
+ getHeaderFromResponse ,
11
+ setHeaderForResponse ,
12
+ setStatusCode ,
13
+ send ,
14
+ } from "./utils/compatibleAPI" ;
7
15
import ready from "./utils/ready" ;
8
16
17
+ function getValueContentRangeHeader ( type , size , range ) {
18
+ return `${ type } ${ range ? `${ range . start } -${ range . end } ` : "*" } /${ size } ` ;
19
+ }
20
+
21
+ function createHtmlDocument ( title , body ) {
22
+ return (
23
+ `${
24
+ "<!DOCTYPE html>\n" +
25
+ '<html lang="en">\n' +
26
+ "<head>\n" +
27
+ '<meta charset="utf-8">\n' +
28
+ "<title>"
29
+ } ${ title } </title>\n` +
30
+ `</head>\n` +
31
+ `<body>\n` +
32
+ `<pre>${ body } </pre>\n` +
33
+ `</body>\n` +
34
+ `</html>\n`
35
+ ) ;
36
+ }
37
+
38
+ const BYTES_RANGE_REGEXP = / ^ * b y t e s / i;
39
+
9
40
export default function wrapper ( context ) {
10
41
return async function middleware ( req , res , next ) {
11
42
const acceptedMethods = context . options . methods || [ "GET" , "HEAD" ] ;
@@ -16,6 +47,7 @@ export default function wrapper(context) {
16
47
17
48
if ( ! acceptedMethods . includes ( req . method ) ) {
18
49
await goNext ( ) ;
50
+
19
51
return ;
20
52
}
21
53
@@ -42,80 +74,146 @@ export default function wrapper(context) {
42
74
43
75
async function processRequest ( ) {
44
76
const filename = getFilenameFromUrl ( context , req . url ) ;
45
- let { headers } = context . options ;
46
-
47
- if ( typeof headers === "function" ) {
48
- headers = headers ( req , res , context ) ;
49
- }
50
-
51
- let content ;
52
77
53
78
if ( ! filename ) {
54
79
await goNext ( ) ;
80
+
55
81
return ;
56
82
}
57
83
58
- try {
59
- content = context . outputFileSystem . readFileSync ( filename ) ;
60
- } catch ( _ignoreError ) {
61
- await goNext ( ) ;
62
- return ;
84
+ let { headers } = context . options ;
85
+
86
+ if ( typeof headers === "function" ) {
87
+ headers = headers ( req , res , context ) ;
63
88
}
64
89
65
- const contentTypeHeader = res . get
66
- ? res . get ( "Content-Type" )
67
- : res . getHeader ( "Content-Type" ) ;
90
+ if ( headers ) {
91
+ const names = Object . keys ( headers ) ;
92
+
93
+ for ( const name of names ) {
94
+ setHeaderForResponse ( res , name , headers [ name ] ) ;
95
+ }
96
+ }
68
97
69
- if ( ! contentTypeHeader ) {
98
+ if ( ! getHeaderFromResponse ( res , "Content-Type" ) ) {
70
99
// content-type name(like application/javascript; charset=utf-8) or false
71
100
const contentType = mime . contentType ( path . extname ( filename ) ) ;
72
101
73
102
// Only set content-type header if media type is known
74
103
// https://tools.ietf.org/html/rfc7231#section-3.1.1.5
75
104
if ( contentType ) {
76
- // Express API
77
- if ( res . set ) {
78
- res . set ( "Content-Type" , contentType ) ;
79
- }
80
- // Node.js API
81
- else {
82
- res . setHeader ( "Content-Type" , contentType ) ;
83
- }
105
+ setHeaderForResponse ( res , "Content-Type" , contentType ) ;
84
106
}
85
107
}
86
108
87
- if ( headers ) {
88
- const names = Object . keys ( headers ) ;
109
+ if ( ! getHeaderFromResponse ( res , "Accept-Ranges" ) ) {
110
+ setHeaderForResponse ( res , "Accept-Ranges" , "bytes" ) ;
111
+ }
89
112
90
- for ( const name of names ) {
91
- // Express API
92
- if ( res . set ) {
93
- res . set ( name , headers [ name ] ) ;
94
- }
95
- // Node.js API
96
- else {
97
- res . setHeader ( name , headers [ name ] ) ;
113
+ const rangeHeader = getHeaderFromRequest ( req , "range" ) ;
114
+
115
+ let start ;
116
+ let end ;
117
+
118
+ if ( rangeHeader && BYTES_RANGE_REGEXP . test ( rangeHeader ) ) {
119
+ const size = await new Promise ( ( resolve ) => {
120
+ context . outputFileSystem . lstat ( filename , ( error , stats ) => {
121
+ if ( error ) {
122
+ context . logger . error ( error ) ;
123
+
124
+ return ;
125
+ }
126
+
127
+ resolve ( stats . size ) ;
128
+ } ) ;
129
+ } ) ;
130
+
131
+ const parsedRanges = parseRange ( size , rangeHeader , {
132
+ combine : true ,
133
+ } ) ;
134
+
135
+ if ( parsedRanges === - 1 ) {
136
+ const message = "Unsatisfiable range for 'Range' header." ;
137
+
138
+ context . logger . error ( message ) ;
139
+
140
+ const existingHeaders = getHeaderNames ( res ) ;
141
+
142
+ for ( let i = 0 ; i < existingHeaders . length ; i ++ ) {
143
+ res . removeHeader ( existingHeaders [ i ] ) ;
98
144
}
145
+
146
+ setStatusCode ( res , 416 ) ;
147
+ setHeaderForResponse (
148
+ res ,
149
+ "Content-Range" ,
150
+ getValueContentRangeHeader ( "bytes" , size )
151
+ ) ;
152
+ setHeaderForResponse ( res , "Content-Type" , "text/html; charset=utf-8" ) ;
153
+
154
+ const document = createHtmlDocument ( 416 , `Error: ${ message } ` ) ;
155
+ const byteLength = Buffer . byteLength ( document ) ;
156
+
157
+ setHeaderForResponse (
158
+ res ,
159
+ "Content-Length" ,
160
+ Buffer . byteLength ( document )
161
+ ) ;
162
+
163
+ send ( req , res , document , byteLength ) ;
164
+
165
+ return ;
166
+ } else if ( parsedRanges === - 2 ) {
167
+ context . logger . error (
168
+ "A malformed 'Range' header was provided. A regular response will be sent for this request."
169
+ ) ;
170
+ } else if ( parsedRanges . length > 1 ) {
171
+ context . logger . error (
172
+ "A 'Range' header with multiple ranges was provided. Multiple ranges are not supported, so a regular response will be sent for this request."
173
+ ) ;
99
174
}
100
- }
101
175
102
- // Buffer
103
- content = handleRangeHeaders ( context , content , req , res ) ;
176
+ if ( parsedRanges !== - 2 && parsedRanges . length === 1 ) {
177
+ // Content-Range
178
+ setStatusCode ( res , 206 ) ;
179
+ setHeaderForResponse (
180
+ res ,
181
+ "Content-Range" ,
182
+ getValueContentRangeHeader ( "bytes" , size , parsedRanges [ 0 ] )
183
+ ) ;
104
184
105
- // Express API
106
- if ( res . send ) {
107
- res . send ( content ) ;
185
+ [ { start, end } ] = parsedRanges ;
186
+ }
108
187
}
109
- // Node.js API
110
- else {
111
- res . setHeader ( "Content-Length" , content . length ) ;
112
188
113
- if ( req . method === "HEAD" ) {
114
- res . end ( ) ;
189
+ const isFsSupportsStream =
190
+ typeof context . outputFileSystem . createReadStream === "function" ;
191
+
192
+ let bufferOtStream ;
193
+ let byteLength ;
194
+
195
+ try {
196
+ if (
197
+ typeof start !== "undefined" &&
198
+ typeof end !== "undefined" &&
199
+ isFsSupportsStream
200
+ ) {
201
+ bufferOtStream = context . outputFileSystem . createReadStream ( filename , {
202
+ start,
203
+ end,
204
+ } ) ;
205
+ byteLength = end - start + 1 ;
115
206
} else {
116
- res . end ( content ) ;
207
+ bufferOtStream = context . outputFileSystem . readFileSync ( filename ) ;
208
+ byteLength = Buffer . byteLength ( bufferOtStream ) ;
117
209
}
210
+ } catch ( _ignoreError ) {
211
+ await goNext ( ) ;
212
+
213
+ return ;
118
214
}
215
+
216
+ send ( req , res , bufferOtStream , byteLength ) ;
119
217
}
120
218
} ;
121
219
}
0 commit comments