-
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathhttp-server-upload.js
More file actions
executable file
·350 lines (305 loc) · 10.8 KB
/
http-server-upload.js
File metadata and controls
executable file
·350 lines (305 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
#!/usr/bin/env node
/*
* HTTP Server Upload
*
* Simple zero-configuration command-line http server which provides a lightweight interface to upload files.
*
* https://github.com/crycode-de/http-server-upload
*
* MIT license
* Copyright (c) 2019-2024 Peter Müller <peter@crycode.de> https://crycode.de
*/
'use strict';
import http from 'node:http';
import { IncomingForm } from 'formidable';
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
let port = process.env.PORT || 8080;
let disableAutoPort = !!process.env.DISABLE_AUTO_PORT;
let uploadDir = process.env.UPLOAD_DIR || process.cwd();
let uploadTmpDir = process.env.UPLOAD_TMP_DIR || uploadDir;
let token = process.env.TOKEN || false;
let pathMatchRegExp = (process.env.PATH_REGEXP) ? new RegExp(process.env.PATH_REGEXP) : /^[a-zA-Z0-9-_/]*$/;
let maxFileSize = (parseInt(process.env.MAX_FILE_SIZE, 10) || 200) * 1024 * 1024;
let enableFolderCreation = !!process.env.ENABLE_FOLDER_CREATION;
let indexFile = process.env.INDEX_FILE || false;
console.log('HTTP Server Upload');
// parse arguments
let uploadDirSetFromArg = false;
const myArgs = process.argv.slice(2);
// help requested?
if (myArgs.includes('--help') || myArgs.includes('-h')) {
console.log(`A Simple zero-configuration command-line http server for uploading files.
The optional configuration is done by command line arguments or environment variables.
If both are used, the arguments have higher priority and the value from the corresponding environment variable will be ignored.
Usage:
http-server-upload [arguments] [uploadRootPath]
Argument | Environmen variable
Description [Default value]
--port | PORT
The port to use. [8080]
--upload-dir | UPLOAD_DIR
The directory where the files should be uploaded to.
This overrides the uploadRootPath argument.
[uploadRootPath argument or the current working directory]
--upload-tmp-dir | UPLOAD_TMP_DIR
Temp directory for the file upload. [The upload directory]
--max-file-size | MAX_FILE_SIZE
The maximum allowed file size for uploads in Megabyte. [200]
--token | TOKEN
An optional token which must be provided on upload. [Nothing]
--path-regexp | PATH_REGEXP
A regular expression to verify a given upload path.
This should be set with care, because it may allow write access
to outside the upload directory. [/^[a-zA-Z0-9-_/]*$/]
--disable-auto-port | DISABLE_AUTO_PORT
Disable automatic port increase if the port is nor available. [Not set]
--enable-folder-creation | ENABLE_FOLDER_CREATION
Enable automatic folder creation when uploading file to non-existent folder. [Not set]
--index-file | INDEX_FILE
Use a custom html file as index instead of the default internal index.
If used, the form fields need to have the same names as in
the original index. [Not set]
--help or -h
Show this help text.
Examples:
PORT=9000 UPLOAD_DIR=~/uploads/ UPLOAD_TMP_DIR=/tmp/ TOKEN=my-super-secret-token http-server-upload
http-server-upload --port=9000 --upload-dir="c:\\users\\peter\\Path With Whitespaces\\"
PORT=9000 http-server-upload --disable-auto-port --enable-folder-creation ./
Additional information:
https://github.com/crycode-de/http-server-upload
`);
process.exit(0);
}
while (myArgs.length > 0) {
const arg = myArgs.shift();
if (arg.startsWith('--')) {
// it's an option ...
let [ key, val ] = arg.split(/=(.*)/); // --dir=test=123 will give ['--dir','test=123','']
// options without values
if (key === '--disable-auto-port') {
disableAutoPort = true;
continue;
}
if (key === '--enable-folder-creation') {
enableFolderCreation = true;
continue;
}
// options with values - get value from next arg if not provided by --arg=val
if (typeof val === 'undefined') {
val = myArgs.shift();
if (typeof val === 'undefined') {
console.warn(`WANRING: No value given for command line argument: ${key}`);
continue;
}
}
switch (key) {
case '--port':
port = val;
break;
case '--dir':
case '--upload-dir':
if (uploadDir === uploadTmpDir) {
uploadTmpDir = val;
}
uploadDir = val;
uploadDirSetFromArg = true;
break;
case '--tmp-dir':
case '--upload-tmp-dir':
uploadTmpDir = val;
break;
case '--token':
token = val;
break;
case '--path-regexp':
pathMatchRegExp = new RegExp(val);
break;
case '--max-size':
case '--max-file-size':
maxFileSize = (parseInt(val, 10) || 200) * 1024 * 1024;
break;
case '--index-file':
indexFile = val;
break;
default:
console.warn(`WANRING: Unknown command line argument: ${key}`);
}
} else {
// only set the upload dir from an argument if not already set
if (!uploadDirSetFromArg) {
if (uploadDir === uploadTmpDir) {
uploadTmpDir = arg;
}
uploadDir = arg;
uploadDirSetFromArg = true;
}
}
}
console.log(`Upload target dir is ${uploadDir}`);
/**
* Cleanup uploaded temp files.
* @param {import('formidable').File[] | undefined} uploads
*/
async function cleanupUploads (uploads) {
if (!uploads) return;
for (const file of uploads) {
if (!file) continue;
try {
await fs.unlink(file.filepath);
} catch (err) {
console.log(`Error removing temporary file! ${file.filepath} ${err}`);
}
}
}
// create the server instance
const server = http.createServer();
// handle requests
server.on('request', async (req, res) => {
if (req.url === '/upload' && req.method.toLowerCase() === 'post') {
// handle upload
const form = new IncomingForm({
uploadDir: uploadTmpDir,
multiples: true,
maxFileSize: maxFileSize,
});
let fields;
let files;
try {
[ fields, files ] = await form.parse(req);
} catch (err) {
console.log(new Date().toUTCString(), `- Error parsing form data: ${err.message}`);
res.statusCode = 400; // Bad Request
res.write(`Error parsing form data! ${err.message}`);
return res.end();
}
if (token && (!fields.token || fields.token[0] !== token)) {
res.statusCode = 401; // Unauthorized
res.write('Wrong token!');
await cleanupUploads(files.uploads);
return res.end();
}
// check if any files are uploaded
if (!files.uploads) {
res.statusCode = 400; // Bad Request
// If a file is uploaded without Content-Type given for the multipart part
// it's parsed as a string field and not as file. Send a detailed error
// message in this case.
if (fields.uploads) {
res.write('No files uploaded! A field called "uploads" is preset but there was probably missing the "Content-Type" for it. Check the docs how to solve this.');
} else {
res.write('No files uploaded!');
}
await cleanupUploads(files.uploads);
return res.end();
}
let targetPath = uploadDir;
if (fields.path && typeof fields.path[0] === 'string') {
if (!fields.path[0].match(pathMatchRegExp)) {
res.statusCode = 400; // Bad Request
res.write('Invalid path!');
await cleanupUploads(files.uploads);
return res.end();
}
targetPath = path.join(uploadDir, fields.path[0]);
}
// check if target folder exists - if not create it if folder creation is enabled
try {
// get path stats and expect an error if not exists
await fs.stat(targetPath);
} catch (err) {
// path does not exist
if (enableFolderCreation) {
console.log(`Target path ${targetPath} does not exist, creating it`);
try {
await fs.mkdir(targetPath, { recursive: true });
} catch (err2) {
console.log(`Error creating target path! ${err2}`);
res.statusCode = 500; // Internal Server Error
res.write(`Error creating target path! ${err2.message}`);
return res.end();
}
} else {
res.statusCode = 400; // Bad Request
res.write('Path does not exist!');
await cleanupUploads(files.uploads);
return res.end();
}
}
// move uploaded files to target path
let count = 0;
for (const file of files.uploads) {
if (!file) continue;
const newPath = path.join(targetPath, file.originalFilename);
try {
await fs.rename(file.filepath, newPath);
console.log(new Date().toUTCString(), '- File uploaded', newPath);
count++;
} catch (err) {
console.log(`Error moving temporary file to target path! ${err}`);
}
}
res.statusCode = 200; // OK
res.write(count > 1 ? `${count} files uploaded!` : 'File uploaded!');
res.end();
} else {
// show index page
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
if (indexFile) {
// try to load custom index file
try {
const index = await fs.readFile(indexFile, { encoding: 'utf-8' });
res.write(index);
return res.end();
} catch (err) {
console.log(`Error serving custom index file! ${err}`);
}
}
// default index file
res.write(`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>http-server-upload</title>
</head>
<body>
<form action="upload" method="post" enctype="multipart/form-data" onsubmit="let totalSize=0, files=document.getElementById('fileinput').files; for (let file of files){totalSize+=file.size} if(files.length){return (totalSize<${maxFileSize})?true:(alert(\`Cannot upload. Input files \${(totalSize/1024/1024).toFixed(2)} MB exceed ${maxFileSize / 1024 / 1024} MB limit.\`),false) }else{alert('No file selected.');return false}">
Files: <input id="fileinput" type="file" name="uploads" multiple="multiple"><br />
Upload path: <input type="text" name="path" value=""><br />
${token ? 'Token: <input type="text" name="token" value=""><br />' : ''}
<input type="submit" value="Upload!">
</form>
</body>
</html>`);
return res.end();
}
});
// handle listening events when the server is ready
server.on('listening', () => {
const ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach((dev) => {
ifaces[dev].forEach((addr) => {
if (addr.family === 'IPv4') {
console.log(` http://${addr.address}:${port}/`);
} else if (addr.family === 'IPv6') {
console.log(` http://[${addr.address}]:${port}/`);
}
});
});
console.log('Hit CTRL-C to stop the server');
});
// handle server errors
server.on('error', (err) => {
if (!disableAutoPort && err.code === 'EADDRINUSE') {
// auto port is enabled and address is in use
// try next port
port++;
server.listen(port);
} else {
console.error(err);
}
});
// start server listening
server.listen(port);