Created
September 11, 2024 01:01
-
-
Save image72/36d6da5f3b4c76ddf9e96074728e58b0 to your computer and use it in GitHub Desktop.
simple http server
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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