lieko-express 0.0.7 → 0.0.9
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/lib/cors.js +100 -0
- package/lib/schema.js +418 -0
- package/lib/static.js +247 -0
- package/lieko-express.js +213 -804
- package/package.json +4 -3
package/lib/static.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = function serveStatic(root, options = {}) {
|
|
5
|
+
const opts = {
|
|
6
|
+
maxAge: options.maxAge || 0,
|
|
7
|
+
index: options.index !== undefined ? options.index : 'index.html',
|
|
8
|
+
dotfiles: options.dotfiles || 'ignore',
|
|
9
|
+
etag: options.etag !== undefined ? options.etag : true,
|
|
10
|
+
extensions: options.extensions || false,
|
|
11
|
+
fallthrough: options.fallthrough !== undefined ? options.fallthrough : true,
|
|
12
|
+
immutable: options.immutable || false,
|
|
13
|
+
lastModified: options.lastModified !== undefined ? options.lastModified : true,
|
|
14
|
+
redirect: options.redirect !== undefined ? options.redirect : true,
|
|
15
|
+
setHeaders: options.setHeaders || null,
|
|
16
|
+
cacheControl: options.cacheControl !== undefined ? options.cacheControl : true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const mimeTypes = {
|
|
20
|
+
'.html': 'text/html; charset=utf-8',
|
|
21
|
+
'.htm': 'text/html; charset=utf-8',
|
|
22
|
+
'.css': 'text/css; charset=utf-8',
|
|
23
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
24
|
+
'.mjs': 'application/javascript; charset=utf-8',
|
|
25
|
+
'.json': 'application/json; charset=utf-8',
|
|
26
|
+
'.xml': 'application/xml; charset=utf-8',
|
|
27
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
28
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
29
|
+
'.jpg': 'image/jpeg',
|
|
30
|
+
'.jpeg': 'image/jpeg',
|
|
31
|
+
'.png': 'image/png',
|
|
32
|
+
'.gif': 'image/gif',
|
|
33
|
+
'.svg': 'image/svg+xml',
|
|
34
|
+
'.webp': 'image/webp',
|
|
35
|
+
'.ico': 'image/x-icon',
|
|
36
|
+
'.bmp': 'image/bmp',
|
|
37
|
+
'.tiff': 'image/tiff',
|
|
38
|
+
'.tif': 'image/tiff',
|
|
39
|
+
'.mp3': 'audio/mpeg',
|
|
40
|
+
'.wav': 'audio/wav',
|
|
41
|
+
'.ogg': 'audio/ogg',
|
|
42
|
+
'.m4a': 'audio/mp4',
|
|
43
|
+
'.aac': 'audio/aac',
|
|
44
|
+
'.flac': 'audio/flac',
|
|
45
|
+
'.mp4': 'video/mp4',
|
|
46
|
+
'.webm': 'video/webm',
|
|
47
|
+
'.ogv': 'video/ogg',
|
|
48
|
+
'.avi': 'video/x-msvideo',
|
|
49
|
+
'.mov': 'video/quicktime',
|
|
50
|
+
'.wmv': 'video/x-ms-wmv',
|
|
51
|
+
'.flv': 'video/x-flv',
|
|
52
|
+
'.mkv': 'video/x-matroska',
|
|
53
|
+
'.woff': 'font/woff',
|
|
54
|
+
'.woff2': 'font/woff2',
|
|
55
|
+
'.ttf': 'font/ttf',
|
|
56
|
+
'.otf': 'font/otf',
|
|
57
|
+
'.eot': 'application/vnd.ms-fontobject',
|
|
58
|
+
'.zip': 'application/zip',
|
|
59
|
+
'.rar': 'application/x-rar-compressed',
|
|
60
|
+
'.tar': 'application/x-tar',
|
|
61
|
+
'.gz': 'application/gzip',
|
|
62
|
+
'.7z': 'application/x-7z-compressed',
|
|
63
|
+
'.pdf': 'application/pdf',
|
|
64
|
+
'.doc': 'application/msword',
|
|
65
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
66
|
+
'.xls': 'application/vnd.ms-excel',
|
|
67
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
68
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
69
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
70
|
+
'.wasm': 'application/wasm',
|
|
71
|
+
'.csv': 'text/csv; charset=utf-8'
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getMimeType = (filePath) => {
|
|
75
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
76
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const generateETag = (stats) => {
|
|
80
|
+
const mtime = stats.mtime.getTime().toString(16);
|
|
81
|
+
const size = stats.size.toString(16);
|
|
82
|
+
return `W/"${size}-${mtime}"`;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return async (req, res, next) => {
|
|
86
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
87
|
+
return next();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
let pathname = req.url;
|
|
92
|
+
const qIndex = pathname.indexOf('?');
|
|
93
|
+
if (qIndex !== -1) {
|
|
94
|
+
pathname = pathname.substring(0, qIndex);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pathname === '') {
|
|
98
|
+
pathname = '/';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
pathname = decodeURIComponent(pathname);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (opts.fallthrough) return next();
|
|
105
|
+
return res.status(400).send('Bad Request');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let filePath = pathname === '/' ? root : path.join(root, pathname);
|
|
109
|
+
|
|
110
|
+
const resolvedPath = path.resolve(filePath);
|
|
111
|
+
const resolvedRoot = path.resolve(root);
|
|
112
|
+
|
|
113
|
+
if (!resolvedPath.startsWith(resolvedRoot)) {
|
|
114
|
+
if (opts.fallthrough) return next();
|
|
115
|
+
return res.status(403).send('Forbidden');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let stats;
|
|
119
|
+
try {
|
|
120
|
+
stats = await fs.promises.stat(filePath);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (pathname === '/' && opts.index) {
|
|
123
|
+
const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
|
|
124
|
+
for (const indexFile of indexes) {
|
|
125
|
+
const indexPath = path.join(root, indexFile);
|
|
126
|
+
try {
|
|
127
|
+
stats = await fs.promises.stat(indexPath);
|
|
128
|
+
if (stats.isFile()) {
|
|
129
|
+
filePath = indexPath;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
} catch (e) { }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!stats && opts.extensions && Array.isArray(opts.extensions)) {
|
|
137
|
+
let found = false;
|
|
138
|
+
for (const ext of opts.extensions) {
|
|
139
|
+
const testPath = filePath + (ext.startsWith('.') ? ext : '.' + ext);
|
|
140
|
+
try {
|
|
141
|
+
stats = await fs.promises.stat(testPath);
|
|
142
|
+
filePath = testPath;
|
|
143
|
+
found = true;
|
|
144
|
+
break;
|
|
145
|
+
} catch (e) { }
|
|
146
|
+
}
|
|
147
|
+
if (!found) return next();
|
|
148
|
+
} else if (!stats) {
|
|
149
|
+
return next();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (stats.isDirectory()) {
|
|
154
|
+
if (opts.redirect && !pathname.endsWith('/')) {
|
|
155
|
+
const query = qIndex !== -1 ? req.url.substring(qIndex) : '';
|
|
156
|
+
const redirectUrl = pathname + '/' + query;
|
|
157
|
+
return res.redirect(redirectUrl, 301);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (opts.index) {
|
|
161
|
+
const indexes = Array.isArray(opts.index) ? opts.index : [opts.index];
|
|
162
|
+
|
|
163
|
+
for (const indexFile of indexes) {
|
|
164
|
+
const indexPath = path.join(filePath, indexFile);
|
|
165
|
+
try {
|
|
166
|
+
const indexStats = await fs.promises.stat(indexPath);
|
|
167
|
+
if (indexStats.isFile()) {
|
|
168
|
+
filePath = indexPath;
|
|
169
|
+
stats = indexStats;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
} catch (e) { }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (stats.isDirectory()) {
|
|
176
|
+
if (opts.fallthrough) return next();
|
|
177
|
+
return res.status(404).send('Not Found');
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
if (opts.fallthrough) return next();
|
|
181
|
+
return res.status(404).send('Not Found');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (opts.etag) {
|
|
186
|
+
const etag = generateETag(stats);
|
|
187
|
+
const ifNoneMatch = req.headers['if-none-match'];
|
|
188
|
+
|
|
189
|
+
if (ifNoneMatch === etag) {
|
|
190
|
+
res.statusCode = 304;
|
|
191
|
+
res.end();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
res.setHeader('ETag', etag);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (opts.lastModified) {
|
|
199
|
+
const lastModified = stats.mtime.toUTCString();
|
|
200
|
+
const ifModifiedSince = req.headers['if-modified-since'];
|
|
201
|
+
|
|
202
|
+
if (ifModifiedSince === lastModified) {
|
|
203
|
+
res.statusCode = 304;
|
|
204
|
+
res.end();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
res.setHeader('Last-Modified', lastModified);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (opts.cacheControl) {
|
|
212
|
+
let cacheControl = 'public';
|
|
213
|
+
|
|
214
|
+
if (opts.maxAge > 0) {
|
|
215
|
+
cacheControl += `, max-age=${opts.maxAge}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (opts.immutable) {
|
|
219
|
+
cacheControl += ', immutable';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
res.setHeader('Cache-Control', cacheControl);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const mimeType = getMimeType(filePath);
|
|
226
|
+
res.setHeader('Content-Type', mimeType);
|
|
227
|
+
res.setHeader('Content-Length', stats.size);
|
|
228
|
+
|
|
229
|
+
if (typeof opts.setHeaders === 'function') {
|
|
230
|
+
opts.setHeaders(res, filePath, stats);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (req.method === 'HEAD') {
|
|
234
|
+
return res.end();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const data = await fs.promises.readFile(filePath);
|
|
238
|
+
res.end(data);
|
|
239
|
+
return;
|
|
240
|
+
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error('Static middleware error:', error);
|
|
243
|
+
if (opts.fallthrough) return next();
|
|
244
|
+
res.status(500).send('Internal Server Error');
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
};
|