@remix-run/static-middleware 0.2.0 → 0.4.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/README.md +2 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/directory-listing.d.ts +2 -0
- package/dist/lib/directory-listing.d.ts.map +1 -0
- package/dist/lib/directory-listing.js +278 -0
- package/dist/lib/static.d.ts +51 -23
- package/dist/lib/static.d.ts.map +1 -1
- package/dist/lib/static.js +22 -22
- package/package.json +10 -8
- package/src/index.ts +1 -3
- package/src/lib/directory-listing.ts +297 -0
- package/src/lib/static.ts +76 -28
package/README.md
CHANGED
|
@@ -35,6 +35,8 @@ router.get('/', () => new Response('Home'))
|
|
|
35
35
|
|
|
36
36
|
### With Cache Control
|
|
37
37
|
|
|
38
|
+
Internally, the `staticFiles()` middleware uses the [`createFileResponse()` helper from `@remix-run/response`](https://github.com/remix-run/remix/tree/main/packages/response/README.md#file-responses) to send files with full HTTP semantics. This means it also accepts the same options as the `createFileResponse()` helper.
|
|
39
|
+
|
|
38
40
|
```ts
|
|
39
41
|
let router = createRouter({
|
|
40
42
|
middleware: [
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"directory-listing.d.ts","sourceRoot":"","sources":["../../src/lib/directory-listing.ts"],"names":[],"mappings":"AAaA,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,QAAQ,CAAC,CAqPnB"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as fsp from 'node:fs/promises';
|
|
3
|
+
import { html } from '@remix-run/html-template';
|
|
4
|
+
import { createHtmlResponse } from '@remix-run/response/html';
|
|
5
|
+
import { detectMimeType } from '@remix-run/mime';
|
|
6
|
+
export async function generateDirectoryListing(dirPath, pathname) {
|
|
7
|
+
let entries = [];
|
|
8
|
+
try {
|
|
9
|
+
let dirents = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
10
|
+
for (let dirent of dirents) {
|
|
11
|
+
let fullPath = path.join(dirPath, dirent.name);
|
|
12
|
+
let isDirectory = dirent.isDirectory();
|
|
13
|
+
let size = 0;
|
|
14
|
+
let type = '';
|
|
15
|
+
if (isDirectory) {
|
|
16
|
+
size = await calculateDirectorySize(fullPath);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
try {
|
|
20
|
+
let stats = await fsp.stat(fullPath);
|
|
21
|
+
size = stats.size;
|
|
22
|
+
let mimeType = detectMimeType(dirent.name);
|
|
23
|
+
type = mimeType || 'application/octet-stream';
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Unable to stat file, use defaults
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
entries.push({
|
|
30
|
+
name: dirent.name,
|
|
31
|
+
isDirectory,
|
|
32
|
+
size,
|
|
33
|
+
type,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return new Response('Error reading directory', { status: 500 });
|
|
39
|
+
}
|
|
40
|
+
// Sort: directories first, then alphabetically
|
|
41
|
+
entries.sort((a, b) => {
|
|
42
|
+
if (a.isDirectory && !b.isDirectory)
|
|
43
|
+
return -1;
|
|
44
|
+
if (!a.isDirectory && b.isDirectory)
|
|
45
|
+
return 1;
|
|
46
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true });
|
|
47
|
+
});
|
|
48
|
+
// Build table rows
|
|
49
|
+
let tableRows = [];
|
|
50
|
+
let folderIcon = html.raw `<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 3.5h4.5l1.5 1.5h6v8h-12z"/></svg>`;
|
|
51
|
+
let fileIcon = html.raw `<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h7l3 3v9H3z"/><path d="M10 2v3h3"/></svg>`;
|
|
52
|
+
// Add parent directory link if not at root
|
|
53
|
+
if (pathname !== '/' && pathname !== '') {
|
|
54
|
+
let parentPath = pathname.replace(/\/$/, '').split('/').slice(0, -1).join('/') || '/';
|
|
55
|
+
tableRows.push(html `
|
|
56
|
+
<tr class="file-row">
|
|
57
|
+
<td class="name-cell">
|
|
58
|
+
<a href="${parentPath}">${folderIcon} ..</a>
|
|
59
|
+
</td>
|
|
60
|
+
<td class="size-cell"></td>
|
|
61
|
+
<td class="type-cell"></td>
|
|
62
|
+
</tr>
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
for (let entry of entries) {
|
|
66
|
+
let icon = entry.isDirectory ? folderIcon : fileIcon;
|
|
67
|
+
let href = pathname.endsWith('/') ? pathname + entry.name : pathname + '/' + entry.name;
|
|
68
|
+
let sizeDisplay = formatFileSize(entry.size);
|
|
69
|
+
let typeDisplay = entry.isDirectory ? 'Folder' : entry.type;
|
|
70
|
+
tableRows.push(html `
|
|
71
|
+
<tr class="file-row">
|
|
72
|
+
<td class="name-cell">
|
|
73
|
+
<a href="${href}">${icon} ${entry.name}</a>
|
|
74
|
+
</td>
|
|
75
|
+
<td class="size-cell">${sizeDisplay}</td>
|
|
76
|
+
<td class="type-cell">${typeDisplay}</td>
|
|
77
|
+
</tr>
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
return createHtmlResponse(html `
|
|
81
|
+
<html lang="en">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="utf-8" />
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
85
|
+
<title>Index of ${pathname}</title>
|
|
86
|
+
<style>
|
|
87
|
+
* {
|
|
88
|
+
box-sizing: border-box;
|
|
89
|
+
}
|
|
90
|
+
body {
|
|
91
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
|
92
|
+
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
93
|
+
margin: 0;
|
|
94
|
+
padding: 2rem 1rem;
|
|
95
|
+
background: #fff;
|
|
96
|
+
color: #333;
|
|
97
|
+
font-size: 14px;
|
|
98
|
+
}
|
|
99
|
+
.container {
|
|
100
|
+
max-width: 1200px;
|
|
101
|
+
margin: 0 auto;
|
|
102
|
+
}
|
|
103
|
+
h1 {
|
|
104
|
+
margin: 0 0 1rem 0;
|
|
105
|
+
padding: 0;
|
|
106
|
+
font-size: 1.25rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
color: #333;
|
|
109
|
+
}
|
|
110
|
+
.icon {
|
|
111
|
+
width: 14px;
|
|
112
|
+
height: 14px;
|
|
113
|
+
display: inline-block;
|
|
114
|
+
vertical-align: text-top;
|
|
115
|
+
margin-right: 0.5rem;
|
|
116
|
+
color: #666;
|
|
117
|
+
}
|
|
118
|
+
table {
|
|
119
|
+
width: 100%;
|
|
120
|
+
border-collapse: collapse;
|
|
121
|
+
border: 1px solid #e0e0e0;
|
|
122
|
+
}
|
|
123
|
+
thead tr {
|
|
124
|
+
background: #fafafa;
|
|
125
|
+
}
|
|
126
|
+
th {
|
|
127
|
+
padding: 0.5rem 1rem;
|
|
128
|
+
text-align: left;
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
font-size: 0.8125rem;
|
|
131
|
+
color: #666;
|
|
132
|
+
border-bottom: 1px solid #e0e0e0;
|
|
133
|
+
}
|
|
134
|
+
td {
|
|
135
|
+
border-bottom: 1px solid #f0f0f0;
|
|
136
|
+
}
|
|
137
|
+
tr:last-child td {
|
|
138
|
+
border-bottom: none;
|
|
139
|
+
}
|
|
140
|
+
.file-row {
|
|
141
|
+
position: relative;
|
|
142
|
+
}
|
|
143
|
+
.file-row:hover {
|
|
144
|
+
background: #fafafa;
|
|
145
|
+
}
|
|
146
|
+
.name-cell {
|
|
147
|
+
width: 50%;
|
|
148
|
+
padding: 0;
|
|
149
|
+
}
|
|
150
|
+
.name-cell a {
|
|
151
|
+
display: block;
|
|
152
|
+
padding: 0.375rem 1rem;
|
|
153
|
+
color: #0066cc;
|
|
154
|
+
text-decoration: none;
|
|
155
|
+
}
|
|
156
|
+
.name-cell a::after {
|
|
157
|
+
content: '';
|
|
158
|
+
position: absolute;
|
|
159
|
+
top: 0;
|
|
160
|
+
left: 0;
|
|
161
|
+
right: 0;
|
|
162
|
+
bottom: 0;
|
|
163
|
+
}
|
|
164
|
+
.file-row:hover .name-cell a {
|
|
165
|
+
text-decoration: underline;
|
|
166
|
+
}
|
|
167
|
+
.size-cell {
|
|
168
|
+
width: 25%;
|
|
169
|
+
padding: 0.375rem 1rem;
|
|
170
|
+
text-align: left;
|
|
171
|
+
color: #666;
|
|
172
|
+
font-variant-numeric: tabular-nums;
|
|
173
|
+
}
|
|
174
|
+
.type-cell {
|
|
175
|
+
width: 25%;
|
|
176
|
+
padding: 0.375rem 1rem;
|
|
177
|
+
color: #666;
|
|
178
|
+
white-space: nowrap;
|
|
179
|
+
}
|
|
180
|
+
@media (max-width: 768px) {
|
|
181
|
+
body {
|
|
182
|
+
padding: 1rem 0.5rem;
|
|
183
|
+
}
|
|
184
|
+
h1 {
|
|
185
|
+
font-size: 1.125rem;
|
|
186
|
+
margin-bottom: 0.75rem;
|
|
187
|
+
}
|
|
188
|
+
thead {
|
|
189
|
+
display: none;
|
|
190
|
+
}
|
|
191
|
+
.name-cell a,
|
|
192
|
+
.size-cell,
|
|
193
|
+
.type-cell {
|
|
194
|
+
padding: 0.375rem 0.75rem;
|
|
195
|
+
}
|
|
196
|
+
.type-cell {
|
|
197
|
+
display: none;
|
|
198
|
+
}
|
|
199
|
+
.name-cell {
|
|
200
|
+
width: 60%;
|
|
201
|
+
}
|
|
202
|
+
.size-cell {
|
|
203
|
+
width: 40%;
|
|
204
|
+
text-align: right;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
@media (max-width: 480px) {
|
|
208
|
+
body {
|
|
209
|
+
font-size: 13px;
|
|
210
|
+
}
|
|
211
|
+
.name-cell a,
|
|
212
|
+
.size-cell,
|
|
213
|
+
.type-cell {
|
|
214
|
+
padding: 0.375rem 0.5rem;
|
|
215
|
+
}
|
|
216
|
+
h1 {
|
|
217
|
+
font-size: 1rem;
|
|
218
|
+
}
|
|
219
|
+
.icon {
|
|
220
|
+
width: 12px;
|
|
221
|
+
height: 12px;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
</style>
|
|
225
|
+
</head>
|
|
226
|
+
<body>
|
|
227
|
+
<div class="container">
|
|
228
|
+
<h1>Index of ${pathname}</h1>
|
|
229
|
+
<table>
|
|
230
|
+
<thead>
|
|
231
|
+
<tr>
|
|
232
|
+
<th class="name">Name</th>
|
|
233
|
+
<th class="size">Size</th>
|
|
234
|
+
<th class="type">Type</th>
|
|
235
|
+
</tr>
|
|
236
|
+
</thead>
|
|
237
|
+
<tbody>
|
|
238
|
+
${tableRows}
|
|
239
|
+
</tbody>
|
|
240
|
+
</table>
|
|
241
|
+
</div>
|
|
242
|
+
</body>
|
|
243
|
+
</html>
|
|
244
|
+
`);
|
|
245
|
+
}
|
|
246
|
+
async function calculateDirectorySize(dirPath) {
|
|
247
|
+
let totalSize = 0;
|
|
248
|
+
try {
|
|
249
|
+
let dirents = await fsp.readdir(dirPath, { withFileTypes: true });
|
|
250
|
+
for (let dirent of dirents) {
|
|
251
|
+
let fullPath = path.join(dirPath, dirent.name);
|
|
252
|
+
try {
|
|
253
|
+
if (dirent.isDirectory()) {
|
|
254
|
+
totalSize += await calculateDirectorySize(fullPath);
|
|
255
|
+
}
|
|
256
|
+
else if (dirent.isFile()) {
|
|
257
|
+
let stats = await fsp.stat(fullPath);
|
|
258
|
+
totalSize += stats.size;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Skip files/folders we can't access
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// If we can't read the directory, return 0
|
|
268
|
+
}
|
|
269
|
+
return totalSize;
|
|
270
|
+
}
|
|
271
|
+
function formatFileSize(bytes) {
|
|
272
|
+
if (bytes === 0)
|
|
273
|
+
return '0 B';
|
|
274
|
+
let units = ['B', 'kB', 'MB', 'GB', 'TB'];
|
|
275
|
+
let i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
276
|
+
let size = bytes / Math.pow(1024, i);
|
|
277
|
+
return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i];
|
|
278
|
+
}
|
package/dist/lib/static.d.ts
CHANGED
|
@@ -1,46 +1,74 @@
|
|
|
1
1
|
import type { Middleware } from '@remix-run/fetch-router';
|
|
2
|
-
import { type FileResponseOptions } from '@remix-run/
|
|
3
|
-
|
|
2
|
+
import { type FileResponseOptions } from '@remix-run/response/file';
|
|
3
|
+
/**
|
|
4
|
+
* Function that determines if HTTP Range requests should be supported for a given file.
|
|
5
|
+
*
|
|
6
|
+
* @param file - The File object being served
|
|
7
|
+
* @returns true if range requests should be supported
|
|
8
|
+
*/
|
|
9
|
+
export type AcceptRangesFunction = (file: File) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Options for the `staticFiles` middleware.
|
|
12
|
+
*/
|
|
13
|
+
export interface StaticFilesOptions extends Omit<FileResponseOptions, 'acceptRanges'> {
|
|
4
14
|
/**
|
|
5
15
|
* Filter function to determine which files should be served.
|
|
6
16
|
*
|
|
7
17
|
* @param path The relative path being requested
|
|
8
|
-
* @
|
|
18
|
+
* @return Whether to serve the file
|
|
9
19
|
*/
|
|
10
20
|
filter?: (path: string) => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Whether to support HTTP Range requests for partial content.
|
|
23
|
+
*
|
|
24
|
+
* Can be a boolean or a function that receives the file.
|
|
25
|
+
* When enabled, includes Accept-Ranges header and handles Range requests
|
|
26
|
+
* with 206 Partial Content responses.
|
|
27
|
+
*
|
|
28
|
+
* Defaults to enabling ranges only for non-compressible MIME types,
|
|
29
|
+
* as defined by `isCompressibleMimeType()` from `@remix-run/mime`.
|
|
30
|
+
*
|
|
31
|
+
* Note: Range requests and compression are mutually exclusive. When
|
|
32
|
+
* `Accept-Ranges: bytes` is present in the response headers, the compression
|
|
33
|
+
* middleware will not compress the response. This is why the default behavior
|
|
34
|
+
* enables ranges only for non-compressible types.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Force range request support for all files
|
|
38
|
+
* acceptRanges: true
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Enable ranges for videos only
|
|
42
|
+
* acceptRanges: (file) => file.type.startsWith('video/')
|
|
43
|
+
*/
|
|
44
|
+
acceptRanges?: boolean | AcceptRangesFunction;
|
|
11
45
|
/**
|
|
12
46
|
* Files to try and serve as the index file when the request path targets a directory.
|
|
13
47
|
*
|
|
14
|
-
* - `true
|
|
48
|
+
* - `true`: Use default index files `['index.html', 'index.htm']`
|
|
15
49
|
* - `false`: Disable index file serving
|
|
16
50
|
* - `string[]`: Custom list of index files to try in order
|
|
51
|
+
*
|
|
52
|
+
* @default true
|
|
17
53
|
*/
|
|
18
54
|
index?: boolean | string[];
|
|
55
|
+
/**
|
|
56
|
+
* Whether to return an HTML page listing the files in a directory when the request path
|
|
57
|
+
* targets a directory. If both this and `index` are set, `index` takes precedence.
|
|
58
|
+
*
|
|
59
|
+
* @default false
|
|
60
|
+
*/
|
|
61
|
+
listFiles?: boolean;
|
|
19
62
|
}
|
|
20
63
|
/**
|
|
21
64
|
* Creates a middleware that serves static files from the filesystem.
|
|
22
65
|
*
|
|
23
|
-
* Uses the URL pathname to resolve files, removing the leading slash to make it
|
|
24
|
-
*
|
|
25
|
-
* is not found or an error occurs.
|
|
66
|
+
* Uses the URL pathname to resolve files, removing the leading slash to make it a relative path.
|
|
67
|
+
* The middleware always falls through to the handler if the file is not found or an error occurs.
|
|
26
68
|
*
|
|
27
69
|
* @param root The root directory to serve files from (absolute or relative to cwd)
|
|
28
|
-
* @param options
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* let router = createRouter({
|
|
32
|
-
* middleware: [staticFiles('./public')],
|
|
33
|
-
* })
|
|
34
|
-
*
|
|
35
|
-
* @example
|
|
36
|
-
* // With cache control
|
|
37
|
-
* let router = createRouter({
|
|
38
|
-
* middleware: [
|
|
39
|
-
* staticFiles('./public', {
|
|
40
|
-
* cacheControl: 'public, max-age=3600',
|
|
41
|
-
* }),
|
|
42
|
-
* ],
|
|
43
|
-
* })
|
|
70
|
+
* @param options Configuration for file responses
|
|
71
|
+
* @return The static files middleware
|
|
44
72
|
*/
|
|
45
73
|
export declare function staticFiles(root: string, options?: StaticFilesOptions): Middleware;
|
|
46
74
|
//# sourceMappingURL=static.d.ts.map
|
package/dist/lib/static.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/lib/static.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,
|
|
1
|
+
{"version":3,"file":"static.d.ts","sourceRoot":"","sources":["../../src/lib/static.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAA;AACzD,OAAO,EAAkC,KAAK,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAInG;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAA;AAE1D;;GAEG;AACH,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,cAAc,CAAC;IACnF;;;;;OAKG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;IAElC;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,YAAY,CAAC,EAAE,OAAO,GAAG,oBAAoB,CAAA;IAE7C;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,EAAE,CAAA;IAC1B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,UAAU,CA8EtF"}
|
package/dist/lib/static.js
CHANGED
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import * as fsp from 'node:fs/promises';
|
|
3
3
|
import { openFile } from '@remix-run/fs';
|
|
4
|
-
import {
|
|
4
|
+
import { createFileResponse as sendFile } from '@remix-run/response/file';
|
|
5
|
+
import { generateDirectoryListing } from "./directory-listing.js";
|
|
5
6
|
/**
|
|
6
7
|
* Creates a middleware that serves static files from the filesystem.
|
|
7
8
|
*
|
|
8
|
-
* Uses the URL pathname to resolve files, removing the leading slash to make it
|
|
9
|
-
*
|
|
10
|
-
* is not found or an error occurs.
|
|
9
|
+
* Uses the URL pathname to resolve files, removing the leading slash to make it a relative path.
|
|
10
|
+
* The middleware always falls through to the handler if the file is not found or an error occurs.
|
|
11
11
|
*
|
|
12
12
|
* @param root The root directory to serve files from (absolute or relative to cwd)
|
|
13
|
-
* @param options
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* let router = createRouter({
|
|
17
|
-
* middleware: [staticFiles('./public')],
|
|
18
|
-
* })
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // With cache control
|
|
22
|
-
* let router = createRouter({
|
|
23
|
-
* middleware: [
|
|
24
|
-
* staticFiles('./public', {
|
|
25
|
-
* cacheControl: 'public, max-age=3600',
|
|
26
|
-
* }),
|
|
27
|
-
* ],
|
|
28
|
-
* })
|
|
13
|
+
* @param options Configuration for file responses
|
|
14
|
+
* @return The static files middleware
|
|
29
15
|
*/
|
|
30
16
|
export function staticFiles(root, options = {}) {
|
|
31
17
|
// Ensure root is an absolute path
|
|
32
18
|
root = path.resolve(root);
|
|
33
|
-
let { filter, index: indexOption, ...fileOptions } = options;
|
|
19
|
+
let { acceptRanges, filter, index: indexOption, listFiles, ...fileOptions } = options;
|
|
34
20
|
// Normalize index option
|
|
35
21
|
let index;
|
|
36
22
|
if (indexOption === false) {
|
|
@@ -72,6 +58,10 @@ export function staticFiles(root, options = {}) {
|
|
|
72
58
|
// Index file doesn't exist, continue to next
|
|
73
59
|
}
|
|
74
60
|
}
|
|
61
|
+
// If no index file found and listFiles is enabled, show directory listing
|
|
62
|
+
if (!filePath && listFiles) {
|
|
63
|
+
return generateDirectoryListing(targetPath, context.url.pathname);
|
|
64
|
+
}
|
|
75
65
|
}
|
|
76
66
|
}
|
|
77
67
|
catch {
|
|
@@ -80,7 +70,17 @@ export function staticFiles(root, options = {}) {
|
|
|
80
70
|
if (filePath) {
|
|
81
71
|
let fileName = path.relative(root, filePath);
|
|
82
72
|
let file = openFile(filePath, { name: fileName });
|
|
83
|
-
|
|
73
|
+
let finalFileOptions = { ...fileOptions };
|
|
74
|
+
// If acceptRanges is a function, evaluate it with the file
|
|
75
|
+
// Otherwise, pass it directly to sendFile
|
|
76
|
+
if (typeof acceptRanges === 'function') {
|
|
77
|
+
finalFileOptions.acceptRanges = acceptRanges(file);
|
|
78
|
+
}
|
|
79
|
+
else if (acceptRanges !== undefined) {
|
|
80
|
+
finalFileOptions.acceptRanges = acceptRanges;
|
|
81
|
+
}
|
|
82
|
+
return sendFile(file, context.request, finalFileOptions);
|
|
84
83
|
}
|
|
84
|
+
return next();
|
|
85
85
|
};
|
|
86
86
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix-run/static-middleware",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Middleware for serving static files from the filesystem",
|
|
5
5
|
"author": "Michael Jackson <mjijackson@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,16 +28,18 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^24.6.0",
|
|
30
30
|
"typescript": "^5.9.3",
|
|
31
|
-
"@remix-run/fetch-router": "0.
|
|
31
|
+
"@remix-run/fetch-router": "0.12.0",
|
|
32
32
|
"@remix-run/form-data-middleware": "0.1.0",
|
|
33
|
-
"@remix-run/
|
|
34
|
-
"@remix-run/
|
|
35
|
-
"@remix-run/
|
|
33
|
+
"@remix-run/method-override-middleware": "0.1.1",
|
|
34
|
+
"@remix-run/response": "^0.2.0",
|
|
35
|
+
"@remix-run/mime": "^0.1.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@remix-run/fetch-router": "^0.
|
|
39
|
-
"@remix-run/fs": "^0.
|
|
40
|
-
"@remix-run/
|
|
38
|
+
"@remix-run/fetch-router": "^0.12.0",
|
|
39
|
+
"@remix-run/fs": "^0.2.0",
|
|
40
|
+
"@remix-run/mime": "^0.1.0",
|
|
41
|
+
"@remix-run/response": "^0.2.0",
|
|
42
|
+
"@remix-run/html-template": "^0.3.0"
|
|
41
43
|
},
|
|
42
44
|
"keywords": [
|
|
43
45
|
"fetch",
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import * as fsp from 'node:fs/promises'
|
|
3
|
+
import { html } from '@remix-run/html-template'
|
|
4
|
+
import { createHtmlResponse } from '@remix-run/response/html'
|
|
5
|
+
import { detectMimeType } from '@remix-run/mime'
|
|
6
|
+
|
|
7
|
+
interface DirectoryEntry {
|
|
8
|
+
name: string
|
|
9
|
+
isDirectory: boolean
|
|
10
|
+
size: number
|
|
11
|
+
type: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function generateDirectoryListing(
|
|
15
|
+
dirPath: string,
|
|
16
|
+
pathname: string,
|
|
17
|
+
): Promise<Response> {
|
|
18
|
+
let entries: DirectoryEntry[] = []
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
let dirents = await fsp.readdir(dirPath, { withFileTypes: true })
|
|
22
|
+
|
|
23
|
+
for (let dirent of dirents) {
|
|
24
|
+
let fullPath = path.join(dirPath, dirent.name)
|
|
25
|
+
let isDirectory = dirent.isDirectory()
|
|
26
|
+
let size = 0
|
|
27
|
+
let type = ''
|
|
28
|
+
|
|
29
|
+
if (isDirectory) {
|
|
30
|
+
size = await calculateDirectorySize(fullPath)
|
|
31
|
+
} else {
|
|
32
|
+
try {
|
|
33
|
+
let stats = await fsp.stat(fullPath)
|
|
34
|
+
size = stats.size
|
|
35
|
+
let mimeType = detectMimeType(dirent.name)
|
|
36
|
+
type = mimeType || 'application/octet-stream'
|
|
37
|
+
} catch {
|
|
38
|
+
// Unable to stat file, use defaults
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
entries.push({
|
|
43
|
+
name: dirent.name,
|
|
44
|
+
isDirectory,
|
|
45
|
+
size,
|
|
46
|
+
type,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
return new Response('Error reading directory', { status: 500 })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Sort: directories first, then alphabetically
|
|
54
|
+
entries.sort((a, b) => {
|
|
55
|
+
if (a.isDirectory && !b.isDirectory) return -1
|
|
56
|
+
if (!a.isDirectory && b.isDirectory) return 1
|
|
57
|
+
return a.name.localeCompare(b.name, undefined, { numeric: true })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Build table rows
|
|
61
|
+
let tableRows = []
|
|
62
|
+
|
|
63
|
+
let folderIcon = html.raw`<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 3.5h4.5l1.5 1.5h6v8h-12z"/></svg>`
|
|
64
|
+
let fileIcon = html.raw`<svg class="icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 2h7l3 3v9H3z"/><path d="M10 2v3h3"/></svg>`
|
|
65
|
+
|
|
66
|
+
// Add parent directory link if not at root
|
|
67
|
+
if (pathname !== '/' && pathname !== '') {
|
|
68
|
+
let parentPath = pathname.replace(/\/$/, '').split('/').slice(0, -1).join('/') || '/'
|
|
69
|
+
tableRows.push(html`
|
|
70
|
+
<tr class="file-row">
|
|
71
|
+
<td class="name-cell">
|
|
72
|
+
<a href="${parentPath}">${folderIcon} ..</a>
|
|
73
|
+
</td>
|
|
74
|
+
<td class="size-cell"></td>
|
|
75
|
+
<td class="type-cell"></td>
|
|
76
|
+
</tr>
|
|
77
|
+
`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let entry of entries) {
|
|
81
|
+
let icon = entry.isDirectory ? folderIcon : fileIcon
|
|
82
|
+
let href = pathname.endsWith('/') ? pathname + entry.name : pathname + '/' + entry.name
|
|
83
|
+
let sizeDisplay = formatFileSize(entry.size)
|
|
84
|
+
let typeDisplay = entry.isDirectory ? 'Folder' : entry.type
|
|
85
|
+
|
|
86
|
+
tableRows.push(html`
|
|
87
|
+
<tr class="file-row">
|
|
88
|
+
<td class="name-cell">
|
|
89
|
+
<a href="${href}">${icon} ${entry.name}</a>
|
|
90
|
+
</td>
|
|
91
|
+
<td class="size-cell">${sizeDisplay}</td>
|
|
92
|
+
<td class="type-cell">${typeDisplay}</td>
|
|
93
|
+
</tr>
|
|
94
|
+
`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return createHtmlResponse(html`
|
|
98
|
+
<html lang="en">
|
|
99
|
+
<head>
|
|
100
|
+
<meta charset="utf-8" />
|
|
101
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
102
|
+
<title>Index of ${pathname}</title>
|
|
103
|
+
<style>
|
|
104
|
+
* {
|
|
105
|
+
box-sizing: border-box;
|
|
106
|
+
}
|
|
107
|
+
body {
|
|
108
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
|
109
|
+
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
110
|
+
margin: 0;
|
|
111
|
+
padding: 2rem 1rem;
|
|
112
|
+
background: #fff;
|
|
113
|
+
color: #333;
|
|
114
|
+
font-size: 14px;
|
|
115
|
+
}
|
|
116
|
+
.container {
|
|
117
|
+
max-width: 1200px;
|
|
118
|
+
margin: 0 auto;
|
|
119
|
+
}
|
|
120
|
+
h1 {
|
|
121
|
+
margin: 0 0 1rem 0;
|
|
122
|
+
padding: 0;
|
|
123
|
+
font-size: 1.25rem;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
color: #333;
|
|
126
|
+
}
|
|
127
|
+
.icon {
|
|
128
|
+
width: 14px;
|
|
129
|
+
height: 14px;
|
|
130
|
+
display: inline-block;
|
|
131
|
+
vertical-align: text-top;
|
|
132
|
+
margin-right: 0.5rem;
|
|
133
|
+
color: #666;
|
|
134
|
+
}
|
|
135
|
+
table {
|
|
136
|
+
width: 100%;
|
|
137
|
+
border-collapse: collapse;
|
|
138
|
+
border: 1px solid #e0e0e0;
|
|
139
|
+
}
|
|
140
|
+
thead tr {
|
|
141
|
+
background: #fafafa;
|
|
142
|
+
}
|
|
143
|
+
th {
|
|
144
|
+
padding: 0.5rem 1rem;
|
|
145
|
+
text-align: left;
|
|
146
|
+
font-weight: 600;
|
|
147
|
+
font-size: 0.8125rem;
|
|
148
|
+
color: #666;
|
|
149
|
+
border-bottom: 1px solid #e0e0e0;
|
|
150
|
+
}
|
|
151
|
+
td {
|
|
152
|
+
border-bottom: 1px solid #f0f0f0;
|
|
153
|
+
}
|
|
154
|
+
tr:last-child td {
|
|
155
|
+
border-bottom: none;
|
|
156
|
+
}
|
|
157
|
+
.file-row {
|
|
158
|
+
position: relative;
|
|
159
|
+
}
|
|
160
|
+
.file-row:hover {
|
|
161
|
+
background: #fafafa;
|
|
162
|
+
}
|
|
163
|
+
.name-cell {
|
|
164
|
+
width: 50%;
|
|
165
|
+
padding: 0;
|
|
166
|
+
}
|
|
167
|
+
.name-cell a {
|
|
168
|
+
display: block;
|
|
169
|
+
padding: 0.375rem 1rem;
|
|
170
|
+
color: #0066cc;
|
|
171
|
+
text-decoration: none;
|
|
172
|
+
}
|
|
173
|
+
.name-cell a::after {
|
|
174
|
+
content: '';
|
|
175
|
+
position: absolute;
|
|
176
|
+
top: 0;
|
|
177
|
+
left: 0;
|
|
178
|
+
right: 0;
|
|
179
|
+
bottom: 0;
|
|
180
|
+
}
|
|
181
|
+
.file-row:hover .name-cell a {
|
|
182
|
+
text-decoration: underline;
|
|
183
|
+
}
|
|
184
|
+
.size-cell {
|
|
185
|
+
width: 25%;
|
|
186
|
+
padding: 0.375rem 1rem;
|
|
187
|
+
text-align: left;
|
|
188
|
+
color: #666;
|
|
189
|
+
font-variant-numeric: tabular-nums;
|
|
190
|
+
}
|
|
191
|
+
.type-cell {
|
|
192
|
+
width: 25%;
|
|
193
|
+
padding: 0.375rem 1rem;
|
|
194
|
+
color: #666;
|
|
195
|
+
white-space: nowrap;
|
|
196
|
+
}
|
|
197
|
+
@media (max-width: 768px) {
|
|
198
|
+
body {
|
|
199
|
+
padding: 1rem 0.5rem;
|
|
200
|
+
}
|
|
201
|
+
h1 {
|
|
202
|
+
font-size: 1.125rem;
|
|
203
|
+
margin-bottom: 0.75rem;
|
|
204
|
+
}
|
|
205
|
+
thead {
|
|
206
|
+
display: none;
|
|
207
|
+
}
|
|
208
|
+
.name-cell a,
|
|
209
|
+
.size-cell,
|
|
210
|
+
.type-cell {
|
|
211
|
+
padding: 0.375rem 0.75rem;
|
|
212
|
+
}
|
|
213
|
+
.type-cell {
|
|
214
|
+
display: none;
|
|
215
|
+
}
|
|
216
|
+
.name-cell {
|
|
217
|
+
width: 60%;
|
|
218
|
+
}
|
|
219
|
+
.size-cell {
|
|
220
|
+
width: 40%;
|
|
221
|
+
text-align: right;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
@media (max-width: 480px) {
|
|
225
|
+
body {
|
|
226
|
+
font-size: 13px;
|
|
227
|
+
}
|
|
228
|
+
.name-cell a,
|
|
229
|
+
.size-cell,
|
|
230
|
+
.type-cell {
|
|
231
|
+
padding: 0.375rem 0.5rem;
|
|
232
|
+
}
|
|
233
|
+
h1 {
|
|
234
|
+
font-size: 1rem;
|
|
235
|
+
}
|
|
236
|
+
.icon {
|
|
237
|
+
width: 12px;
|
|
238
|
+
height: 12px;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
</style>
|
|
242
|
+
</head>
|
|
243
|
+
<body>
|
|
244
|
+
<div class="container">
|
|
245
|
+
<h1>Index of ${pathname}</h1>
|
|
246
|
+
<table>
|
|
247
|
+
<thead>
|
|
248
|
+
<tr>
|
|
249
|
+
<th class="name">Name</th>
|
|
250
|
+
<th class="size">Size</th>
|
|
251
|
+
<th class="type">Type</th>
|
|
252
|
+
</tr>
|
|
253
|
+
</thead>
|
|
254
|
+
<tbody>
|
|
255
|
+
${tableRows}
|
|
256
|
+
</tbody>
|
|
257
|
+
</table>
|
|
258
|
+
</div>
|
|
259
|
+
</body>
|
|
260
|
+
</html>
|
|
261
|
+
`)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function calculateDirectorySize(dirPath: string): Promise<number> {
|
|
265
|
+
let totalSize = 0
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
let dirents = await fsp.readdir(dirPath, { withFileTypes: true })
|
|
269
|
+
|
|
270
|
+
for (let dirent of dirents) {
|
|
271
|
+
let fullPath = path.join(dirPath, dirent.name)
|
|
272
|
+
|
|
273
|
+
try {
|
|
274
|
+
if (dirent.isDirectory()) {
|
|
275
|
+
totalSize += await calculateDirectorySize(fullPath)
|
|
276
|
+
} else if (dirent.isFile()) {
|
|
277
|
+
let stats = await fsp.stat(fullPath)
|
|
278
|
+
totalSize += stats.size
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
// Skip files/folders we can't access
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// If we can't read the directory, return 0
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return totalSize
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function formatFileSize(bytes: number): string {
|
|
292
|
+
if (bytes === 0) return '0 B'
|
|
293
|
+
let units = ['B', 'kB', 'MB', 'GB', 'TB']
|
|
294
|
+
let i = Math.floor(Math.log(bytes) / Math.log(1024))
|
|
295
|
+
let size = bytes / Math.pow(1024, i)
|
|
296
|
+
return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i]
|
|
297
|
+
}
|
package/src/lib/static.ts
CHANGED
|
@@ -2,59 +2,89 @@ import * as path from 'node:path'
|
|
|
2
2
|
import * as fsp from 'node:fs/promises'
|
|
3
3
|
import { openFile } from '@remix-run/fs'
|
|
4
4
|
import type { Middleware } from '@remix-run/fetch-router'
|
|
5
|
-
import {
|
|
6
|
-
file as sendFile,
|
|
7
|
-
type FileResponseOptions,
|
|
8
|
-
} from '@remix-run/fetch-router/response-helpers'
|
|
5
|
+
import { createFileResponse as sendFile, type FileResponseOptions } from '@remix-run/response/file'
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
import { generateDirectoryListing } from './directory-listing.ts'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Function that determines if HTTP Range requests should be supported for a given file.
|
|
11
|
+
*
|
|
12
|
+
* @param file - The File object being served
|
|
13
|
+
* @returns true if range requests should be supported
|
|
14
|
+
*/
|
|
15
|
+
export type AcceptRangesFunction = (file: File) => boolean
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for the `staticFiles` middleware.
|
|
19
|
+
*/
|
|
20
|
+
export interface StaticFilesOptions extends Omit<FileResponseOptions, 'acceptRanges'> {
|
|
11
21
|
/**
|
|
12
22
|
* Filter function to determine which files should be served.
|
|
13
23
|
*
|
|
14
24
|
* @param path The relative path being requested
|
|
15
|
-
* @
|
|
25
|
+
* @return Whether to serve the file
|
|
16
26
|
*/
|
|
17
27
|
filter?: (path: string) => boolean
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether to support HTTP Range requests for partial content.
|
|
31
|
+
*
|
|
32
|
+
* Can be a boolean or a function that receives the file.
|
|
33
|
+
* When enabled, includes Accept-Ranges header and handles Range requests
|
|
34
|
+
* with 206 Partial Content responses.
|
|
35
|
+
*
|
|
36
|
+
* Defaults to enabling ranges only for non-compressible MIME types,
|
|
37
|
+
* as defined by `isCompressibleMimeType()` from `@remix-run/mime`.
|
|
38
|
+
*
|
|
39
|
+
* Note: Range requests and compression are mutually exclusive. When
|
|
40
|
+
* `Accept-Ranges: bytes` is present in the response headers, the compression
|
|
41
|
+
* middleware will not compress the response. This is why the default behavior
|
|
42
|
+
* enables ranges only for non-compressible types.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Force range request support for all files
|
|
46
|
+
* acceptRanges: true
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // Enable ranges for videos only
|
|
50
|
+
* acceptRanges: (file) => file.type.startsWith('video/')
|
|
51
|
+
*/
|
|
52
|
+
acceptRanges?: boolean | AcceptRangesFunction
|
|
53
|
+
|
|
18
54
|
/**
|
|
19
55
|
* Files to try and serve as the index file when the request path targets a directory.
|
|
20
56
|
*
|
|
21
|
-
* - `true
|
|
57
|
+
* - `true`: Use default index files `['index.html', 'index.htm']`
|
|
22
58
|
* - `false`: Disable index file serving
|
|
23
59
|
* - `string[]`: Custom list of index files to try in order
|
|
60
|
+
*
|
|
61
|
+
* @default true
|
|
24
62
|
*/
|
|
25
63
|
index?: boolean | string[]
|
|
64
|
+
/**
|
|
65
|
+
* Whether to return an HTML page listing the files in a directory when the request path
|
|
66
|
+
* targets a directory. If both this and `index` are set, `index` takes precedence.
|
|
67
|
+
*
|
|
68
|
+
* @default false
|
|
69
|
+
*/
|
|
70
|
+
listFiles?: boolean
|
|
26
71
|
}
|
|
27
72
|
|
|
28
73
|
/**
|
|
29
74
|
* Creates a middleware that serves static files from the filesystem.
|
|
30
75
|
*
|
|
31
|
-
* Uses the URL pathname to resolve files, removing the leading slash to make it
|
|
32
|
-
*
|
|
33
|
-
* is not found or an error occurs.
|
|
76
|
+
* Uses the URL pathname to resolve files, removing the leading slash to make it a relative path.
|
|
77
|
+
* The middleware always falls through to the handler if the file is not found or an error occurs.
|
|
34
78
|
*
|
|
35
79
|
* @param root The root directory to serve files from (absolute or relative to cwd)
|
|
36
|
-
* @param options
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* let router = createRouter({
|
|
40
|
-
* middleware: [staticFiles('./public')],
|
|
41
|
-
* })
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* // With cache control
|
|
45
|
-
* let router = createRouter({
|
|
46
|
-
* middleware: [
|
|
47
|
-
* staticFiles('./public', {
|
|
48
|
-
* cacheControl: 'public, max-age=3600',
|
|
49
|
-
* }),
|
|
50
|
-
* ],
|
|
51
|
-
* })
|
|
80
|
+
* @param options Configuration for file responses
|
|
81
|
+
* @return The static files middleware
|
|
52
82
|
*/
|
|
53
83
|
export function staticFiles(root: string, options: StaticFilesOptions = {}): Middleware {
|
|
54
84
|
// Ensure root is an absolute path
|
|
55
85
|
root = path.resolve(root)
|
|
56
86
|
|
|
57
|
-
let { filter, index: indexOption, ...fileOptions } = options
|
|
87
|
+
let { acceptRanges, filter, index: indexOption, listFiles, ...fileOptions } = options
|
|
58
88
|
|
|
59
89
|
// Normalize index option
|
|
60
90
|
let index: string[]
|
|
@@ -99,6 +129,11 @@ export function staticFiles(root: string, options: StaticFilesOptions = {}): Mid
|
|
|
99
129
|
// Index file doesn't exist, continue to next
|
|
100
130
|
}
|
|
101
131
|
}
|
|
132
|
+
|
|
133
|
+
// If no index file found and listFiles is enabled, show directory listing
|
|
134
|
+
if (!filePath && listFiles) {
|
|
135
|
+
return generateDirectoryListing(targetPath, context.url.pathname)
|
|
136
|
+
}
|
|
102
137
|
}
|
|
103
138
|
} catch {
|
|
104
139
|
// Path doesn't exist or isn't accessible, fall through
|
|
@@ -107,7 +142,20 @@ export function staticFiles(root: string, options: StaticFilesOptions = {}): Mid
|
|
|
107
142
|
if (filePath) {
|
|
108
143
|
let fileName = path.relative(root, filePath)
|
|
109
144
|
let file = openFile(filePath, { name: fileName })
|
|
110
|
-
|
|
145
|
+
|
|
146
|
+
let finalFileOptions: FileResponseOptions = { ...fileOptions }
|
|
147
|
+
|
|
148
|
+
// If acceptRanges is a function, evaluate it with the file
|
|
149
|
+
// Otherwise, pass it directly to sendFile
|
|
150
|
+
if (typeof acceptRanges === 'function') {
|
|
151
|
+
finalFileOptions.acceptRanges = acceptRanges(file)
|
|
152
|
+
} else if (acceptRanges !== undefined) {
|
|
153
|
+
finalFileOptions.acceptRanges = acceptRanges
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return sendFile(file, context.request, finalFileOptions)
|
|
111
157
|
}
|
|
158
|
+
|
|
159
|
+
return next()
|
|
112
160
|
}
|
|
113
161
|
}
|