http-server-upload
Version:
A Simple zero-configuration command-line http server for uploading files
361 lines (314 loc) • 11.4 kB
JavaScript
/*
* 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-2026 Peter Müller <peter@crycode.de> https://crycode.de
*/
;
import fs from 'node:fs/promises';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { IncomingForm } from 'formidable';
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 ...
// eslint-disable-next-line prefer-const
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;
}
}
}
// resolve the upload dir to get the real path
uploadDir = path.resolve(uploadDir);
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);
// eslint-disable-next-line no-unused-vars
} 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 safeName = path.basename(file.originalFilename);
const newPath = path.resolve(path.join(targetPath, safeName));
if (!newPath.startsWith(uploadDir + path.sep)) {
console.log(`Error: File path ${newPath} is outside upload directory!`);
continue;
}
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);