Skip to content

Instantly share code, notes, and snippets.

@image72
Created September 11, 2024 01:01
Show Gist options
  • Save image72/36d6da5f3b4c76ddf9e96074728e58b0 to your computer and use it in GitHub Desktop.
Save image72/36d6da5f3b4c76ddf9e96074728e58b0 to your computer and use it in GitHub Desktop.
simple http server
const http = require('node:http');
const { IncomingForm } = require('formidable');
const { promises: fs, constants: fsConstants } = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { parseArgs } = require('node:util');
const options = {
port: { type: 'string', short: 'p', default: process.env.PORT || '8080' },
'upload-dir': { type: 'string', short: 'd', default: process.env.UPLOAD_DIR || process.cwd() },
'upload-tmp-dir': { type: 'string', short: 't', default: process.env.UPLOAD_TMP_DIR },
'max-file-size': { type: 'string', short: 'm', default: process.env.MAX_FILE_SIZE || '200' },
token: { type: 'string', short: 'k', default: process.env.TOKEN || '' },
'path-regexp': { type: 'string', short: 'r', default: process.env.PATH_REGEXP || '^[a-zA-Z0-9-_/]*$' },
'enable-folder-creation': { type: 'boolean', short: 'e', default: !!process.env.ENABLE_FOLDER_CREATION },
help: { type: 'boolean', short: 'h' }
};
const { values: args, positionals } = parseArgs({ options, allowPositionals: true });
if (args.help) {
console.log(`HTTP Server Upload
Usage: http-server-upload [options] [uploadRootPath]
Options:
-p, --port <number> The port to use (default: 8080)
-d, --upload-dir <path> The directory where files should be uploaded
-t, --upload-tmp-dir <path> Temp directory for file upload
-m, --max-file-size <number> Maximum allowed file size for uploads in MB (default: 200)
-k, --token <string> An optional token which must be provided on upload
-r, --path-regexp <regexp> A regular expression to verify a given upload path
-e, --enable-folder-creation Enable automatic folder creation when uploading to non-existent folder
-h, --help Show this help text
Environment variables can also be used for configuration (e.g., PORT, UPLOAD_DIR, etc.).
For more information, visit: https://github.com/crycode-de/http-server-upload
`);
process.exit(0);
}
const config = {
port: parseInt(args.port, 10),
uploadDir: args['upload-dir'],
uploadTmpDir: args['upload-tmp-dir'] || args['upload-dir'],
token: args.token || false,
pathMatchRegExp: new RegExp(args['path-regexp']),
maxFileSize: (parseInt(args['max-file-size'], 10) || 200) * 1024 * 1024,
enableFolderCreation: args['enable-folder-creation']
};
if (positionals.length > 0) {
config.uploadDir = positionals[0];
config.uploadTmpDir = config.uploadDir;
}
console.log('HTTP Server Upload');
console.log(`Upload target dir is ${config.uploadDir}`);
const validateToken = (token) => {
if (config.token && token !== config.token) {
throw new Error('Wrong token!');
}
};
const validatePath = (uploadPath) => {
if (uploadPath && !uploadPath.match(config.pathMatchRegExp)) {
throw new Error('Invalid path!');
}
};
const ensureDir = async (dir) => {
try {
await fs.access(dir, fsConstants.W_OK);
} catch (err) {
if (config.enableFolderCreation) {
await fs.mkdir(dir, { recursive: true });
} else {
throw new Error('Path does not exist!');
}
}
};
const moveFile = async (file, targetPath) => {
const newPath = path.join(targetPath, file.originalFilename);
await fs.rename(file.filepath, newPath);
return newPath;
};
const handleUpload = async (req, res) => {
const form = new IncomingForm({
uploadDir: config.uploadTmpDir,
maxFileSize: config.maxFileSize,
multiples: true
});
try {
const [fields, files] = await form.parse(req);
validateToken(fields.token?.[0]);
const targetPath = fields.path ? path.join(config.uploadDir, fields.path[0]) : config.uploadDir;
validatePath(fields.path?.[0]);
await ensureDir(targetPath);
const uploadedFiles = Array.isArray(files.uploads) ? files.uploads : [files.uploads];
const movedFiles = await Promise.all(uploadedFiles.map(file => moveFile(file[0], targetPath)));
console.log(new Date().toUTCString(), '- Files uploaded:', movedFiles.length);
res.end(`${movedFiles.length} file(s) uploaded!`);
} catch (err) {
console.error(new Date().toUTCString(), '- Error:', err.message);
res.statusCode = 400;
res.end(`Error: ${err.message}`);
}
};
const formHTML = (config) => String.raw`
<!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="return validateForm()">
Files: <input id="fileinput" type="file" name="uploads" multiple="multiple"><br />
Upload path: <input type="text" name="path" value=""><br />
${config.token ? 'Token: <input type="text" name="token" value=""><br />' : ''}
<input type="submit" value="Upload!">
</form>
<script>
function validateForm() {
const files = document.getElementById('fileinput').files;
if (files.length === 0) {
alert('No file selected.');
return false;
}
const totalSize = Array.from(files).reduce((sum, file) => sum + file.size, 0);
if (totalSize > ${config.maxFileSize}) {
alert(\`Cannot upload. Input files \${(totalSize / 1024 / 1024).toFixed(2)} MB exceed ${config.maxFileSize / 1024 / 1024} MB limit.\`);
return false;
}
return true;
}
</script>
</body>
</html>
`;
const serveUploadForm = (res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(formHTML(config));
};
const server = http.createServer((req, res) => {
if (req.url === '/upload' && req.method.toLowerCase() === 'post') {
handleUpload(req, res);
} else {
serveUploadForm(res);
}
});
server.on('listening', () => {
const ifaces = os.networkInterfaces();
Object.values(ifaces).flat().forEach(addr => {
if (addr.family === 'IPv4' || addr.family === 'IPv6') {
console.log(` http${addr.family === 'IPv6' ? '://[' : '://'}${addr.address}${addr.family === 'IPv6' ? ']' : ''}:${config.port}/`);
}
});
console.log('Hit CTRL-C to stop the server');
});
server.on('error', console.error);
server.listen(config.port);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment