mirror of
https://github.com/Foltik/Shimapan
synced 2025-01-05 15:58:03 -05:00
Rewrite upload code to phase out Multer, allowing for more granular control
This commit is contained in:
parent
f7d51650c1
commit
50f37fd1a2
@ -2,86 +2,17 @@ const express = require('express');
|
||||
const router = express.Router();
|
||||
const config = require('config');
|
||||
|
||||
const fsPromises = require('fs').promises;
|
||||
|
||||
const ModelPath = '../models/';
|
||||
const User = require(ModelPath + 'User.js');
|
||||
const Upload = require(ModelPath + 'Upload.js');
|
||||
const Key = require(ModelPath + 'Key.js');
|
||||
|
||||
const verifyScope = require('../util/verifyScope.js');
|
||||
|
||||
const multer = require('multer');
|
||||
const fileUpload = multer({dest: config.get('Upload.path')}).single('file');
|
||||
const uploadMultipart = require('../util/upload/multipart');
|
||||
const updateStats = require('../util/upload/stats');
|
||||
|
||||
const wrap = require('../util/wrap.js');
|
||||
|
||||
const generatedIdExists = async id =>
|
||||
await Upload.countDocuments({id: id}) === 1;
|
||||
|
||||
const generateId = async() => {
|
||||
const charset = config.get('Upload.charset');
|
||||
const len = config.get('Upload.idLength');
|
||||
|
||||
const id = [...Array(len)]
|
||||
.map(() => charset.charAt(Math.floor(Math.random() * charset.length)))
|
||||
.join('');
|
||||
|
||||
return await generatedIdExists(id)
|
||||
? generateId()
|
||||
: id;
|
||||
};
|
||||
|
||||
const updateStats = async req =>
|
||||
Promise.all([
|
||||
User.updateOne({username: req.username}, {$inc: {uploadCount: 1, uploadSize: req.file.size}}),
|
||||
req.key
|
||||
? Key.updateOne({key: req.key}, {$inc: {uploadCount: 1, uploadSize: req.file.size}})
|
||||
: Promise.resolve()
|
||||
]);
|
||||
|
||||
|
||||
router.post('/', fileUpload, wrap(async(req, res) => {
|
||||
// We need to authenticate in place because the form data needs to be processed by multer first
|
||||
const deleteAndError = async (code, message) => {
|
||||
if (req.file)
|
||||
await fsPromises.unlink(req.file.path);
|
||||
res.status(code).json({message: message});
|
||||
};
|
||||
|
||||
if (req.isAuthenticated()) {
|
||||
if (verifyScope(req.session.passport.scope, 'file.upload')) {
|
||||
req.username = req.session.passport.user;
|
||||
req.displayname = req.session.passport.displayname;
|
||||
req.scope = req.session.passport.scope;
|
||||
req.key = null;
|
||||
} else {
|
||||
return await deleteAndError(403, 'Forbidden.');
|
||||
}
|
||||
} else if (req.body.key) {
|
||||
const key = await Key.findOne({key: req.body.key});
|
||||
if (verifyScope(key.scope, 'file.upload')) {
|
||||
req.username = key.issuer;
|
||||
req.displayname = key.issuer;
|
||||
req.scope = key.scope;
|
||||
req.key = key.key;
|
||||
} else {
|
||||
return await deleteAndError(403, 'Forbidden.');
|
||||
}
|
||||
} else {
|
||||
return await deleteAndError(401, 'Unauthorized.');
|
||||
}
|
||||
|
||||
if (!req.file)
|
||||
return res.status(400).json({message: 'No file specified.'});
|
||||
|
||||
if (req.file.size > config.get('Upload.maxSize')) {
|
||||
await fsPromises.unlink(req.file.path);
|
||||
return res.status(413).json({message: 'File too large.'});
|
||||
}
|
||||
|
||||
router.post('/', uploadMultipart, wrap(async (req, res) => {
|
||||
const upload = {
|
||||
id: await generateId(),
|
||||
id: req.file.name,
|
||||
uploader: req.username,
|
||||
uploaderKey: req.key,
|
||||
date: Date.now(),
|
||||
@ -95,7 +26,7 @@ router.post('/', fileUpload, wrap(async(req, res) => {
|
||||
|
||||
res.status(200).json({
|
||||
message: 'File uploaded.',
|
||||
id: upload.id,
|
||||
id: req.file.name,
|
||||
url: config.get('Server.hostname') + '/v/' + upload.id
|
||||
});
|
||||
}));
|
||||
|
28
app/util/upload/disk.js
Normal file
28
app/util/upload/disk.js
Normal file
@ -0,0 +1,28 @@
|
||||
const fs = require('fs');
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
const mkdir = path => new Promise((resolve, reject) => {
|
||||
fsPromises.mkdir(path)
|
||||
.then(resolve)
|
||||
.catch(err => {
|
||||
if (err.code === 'EEXIST')
|
||||
resolve();
|
||||
else
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
const write = (path, stream) => new Promise((resolve, reject) => {
|
||||
const outStream = fs.createWriteStream(path);
|
||||
stream.pipe(outStream);
|
||||
outStream.on('error', reject);
|
||||
outStream.on('close', () => resolve(outStream.bytesWritten));
|
||||
outStream.on('finish', () => resolve(outStream.bytesWritten));
|
||||
});
|
||||
|
||||
const remove = path =>
|
||||
fsPromises.unlink(path);
|
||||
|
||||
exports.mkdir = mkdir;
|
||||
exports.write = write;
|
||||
exports.remove = remove;
|
23
app/util/upload/id.js
Normal file
23
app/util/upload/id.js
Normal file
@ -0,0 +1,23 @@
|
||||
const config = require('config');
|
||||
|
||||
const ModelPath = '../../models/';
|
||||
const Upload = require(ModelPath + 'Upload.js');
|
||||
|
||||
const exists = async id =>
|
||||
await Upload.countDocuments({id: id}) === 1;
|
||||
|
||||
const generate = async () => {
|
||||
const charset = config.get('Upload.charset');
|
||||
const len = config.get('Upload.idLength');
|
||||
|
||||
const id = [...Array(len)]
|
||||
.map(() => charset.charAt(Math.floor(Math.random() * charset.length)))
|
||||
.join('');
|
||||
|
||||
return await exists(id)
|
||||
? generate()
|
||||
: id;
|
||||
};
|
||||
|
||||
exports.generate = generate;
|
||||
exports.exists = exists;
|
114
app/util/upload/multipart.js
Normal file
114
app/util/upload/multipart.js
Normal file
@ -0,0 +1,114 @@
|
||||
const Busboy = require('busboy');
|
||||
const is = require('type-is');
|
||||
const config = require('config');
|
||||
|
||||
const wrap = require('../wrap');
|
||||
const auth = require('../auth');
|
||||
const disk = require('./disk');
|
||||
const identifier = require('./id');
|
||||
|
||||
const uploadMultipart = wrap(async (req, res, next) => {
|
||||
if (!is(req, ['multipart']))
|
||||
return res.status(400).json({message: 'Bad request.'});
|
||||
|
||||
// Store whether the user has authenticated, because an api key might be included with the form later
|
||||
let authStatus = {
|
||||
authenticated: false,
|
||||
permission: false
|
||||
};
|
||||
// If not authenticated with a session, we'll have to wait for key authentication from the multipart form data
|
||||
await auth.checkSession(req, 'file.upload', authStatus);
|
||||
|
||||
// Function to call once the file is sent or an error is encountered
|
||||
let isDone = false;
|
||||
const done = async (err, data) => {
|
||||
if (isDone) return;
|
||||
isDone = true;
|
||||
|
||||
req.unpipe(busboy);
|
||||
busboy.removeAllListeners();
|
||||
req.on('readable', req.read.bind(req));
|
||||
|
||||
if (err) {
|
||||
if (data.path)
|
||||
await disk.remove(data.path);
|
||||
|
||||
if (err === 'LIMIT_FILE_SIZE')
|
||||
return res.status(413).json({message: 'File too large.'});
|
||||
else {
|
||||
console.log(err.stack);
|
||||
return res.status(500).json({message: 'Internal server error.'});
|
||||
}
|
||||
} else {
|
||||
req.file = data.file;
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
// Create the busboy object to parse the multipart data
|
||||
const busboy = new Busboy({
|
||||
headers: req.headers,
|
||||
limits: {
|
||||
fileSize: config.get('Upload.maxSize')
|
||||
}
|
||||
});
|
||||
|
||||
req.body = {};
|
||||
busboy.on('field', (fieldName, value) => {
|
||||
req.body[fieldName] = value;
|
||||
});
|
||||
|
||||
let fileCount = 0;
|
||||
let file;
|
||||
busboy.on('file', async (fieldName, stream, name, encoding, mime) => {
|
||||
// Only process one file
|
||||
fileCount++;
|
||||
if (fileCount > 1)
|
||||
return res.status(400).json({message: 'Bad request.'});
|
||||
|
||||
// If a key was encountered and we are not authenticated, try to authenticate with it before the final check
|
||||
if (req.body.key && !authStatus.authenticated)
|
||||
await auth.checkKey(req, 'file.upload', authStatus);
|
||||
|
||||
// Finally, check if we have auth before preceeding, keys should have been processed by now
|
||||
if (!authStatus.authenticated)
|
||||
return res.status(401).json({message: 'Unauthorized.'});
|
||||
if (!authStatus.permission)
|
||||
return res.status(403).json({message: 'Forbidden.'});
|
||||
|
||||
// Don't attach to the files object if there is no file
|
||||
if (!name) return stream.resume();
|
||||
|
||||
// Generate an ID for the file, saving it with that filename
|
||||
const path = config.get('Upload.path');
|
||||
const newName = await identifier.generate();
|
||||
const finalPath = path + '/' + newName;
|
||||
|
||||
// Set the file attributes
|
||||
file = {
|
||||
originalName: name,
|
||||
name: newName,
|
||||
path: finalPath,
|
||||
encoding: encoding,
|
||||
mime: mime
|
||||
};
|
||||
|
||||
// Ensure the output directory exists
|
||||
await disk.mkdir(path);
|
||||
|
||||
// Handle errors
|
||||
stream.on('error', err => done(err, {path: finalPath}));
|
||||
stream.on('limit', () => done('LIMIT_FILE_SIZE', {path: finalPath}));
|
||||
|
||||
file.size = await disk.write(finalPath, stream);
|
||||
|
||||
await done(null, {file: file});
|
||||
});
|
||||
|
||||
busboy.on('error', err => done(err));
|
||||
busboy.on('finished', () => done(null, {file: file}));
|
||||
|
||||
req.pipe(busboy);
|
||||
});
|
||||
|
||||
module.exports = uploadMultipart;
|
13
app/util/upload/stats.js
Normal file
13
app/util/upload/stats.js
Normal file
@ -0,0 +1,13 @@
|
||||
const ModelPath = '../../models/';
|
||||
const User = require(ModelPath + 'User.js');
|
||||
const Key = require(ModelPath + 'Key.js');
|
||||
|
||||
const updateStats = req =>
|
||||
Promise.all([
|
||||
User.updateOne({username: req.username}, {$inc: {uploadCount: 1, uploadSize: req.file.size}}),
|
||||
req.key
|
||||
? Key.updateOne({key: req.key}, {$inc: {uploadCount: 1, uploadSize: req.file.size}})
|
||||
: Promise.resolve()
|
||||
]);
|
||||
|
||||
module.exports = updateStats;
|
29
package-lock.json
generated
29
package-lock.json
generated
@ -152,11 +152,6 @@
|
||||
"buffer-equal": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"append-field": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-0.1.0.tgz",
|
||||
"integrity": "sha1-bdxY+gg8e8VF08WZWygwzCNm1Eo="
|
||||
},
|
||||
"archy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
|
||||
@ -1249,6 +1244,7 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz",
|
||||
"integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^2.2.2",
|
||||
@ -1258,12 +1254,14 @@
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"dev": true
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz",
|
||||
"integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@ -1278,6 +1276,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
|
||||
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@ -5864,21 +5863,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||
},
|
||||
"multer": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.3.1.tgz",
|
||||
"integrity": "sha512-JHdEoxkA/5NgZRo91RNn4UT+HdcJV9XUo01DTkKC7vo1erNIngtuaw9Y0WI8RdTlyi+wMIbunflhghzVLuGJyw==",
|
||||
"requires": {
|
||||
"append-field": "^0.1.0",
|
||||
"busboy": "^0.2.11",
|
||||
"concat-stream": "^1.5.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"object-assign": "^3.0.0",
|
||||
"on-finished": "^2.3.0",
|
||||
"type-is": "^1.6.4",
|
||||
"xtend": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"multipipe": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz",
|
||||
@ -11910,7 +11894,8 @@
|
||||
"typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
|
||||
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.4.5",
|
||||
|
@ -7,6 +7,7 @@
|
||||
"angular-ui-router": "^1.0.19",
|
||||
"async": "^2.6.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"busboy": "^0.2.14",
|
||||
"config": "^1.31.0",
|
||||
"connect-mongo": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
@ -17,12 +18,12 @@
|
||||
"method-override": "latest",
|
||||
"mongoose": "^5.2.5",
|
||||
"morgan": "^1.9.0",
|
||||
"multer": "^1.3.1",
|
||||
"ng-file-upload": "^12.2.13",
|
||||
"ngclipboard": "^2.0.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-local-mongoose": "^5.0.1",
|
||||
"type-is": "^1.6.16",
|
||||
"vinyl-source-stream": "^2.0.0"
|
||||
},
|
||||
"description": "A simple file sharing website.",
|
||||
|
Loading…
Reference in New Issue
Block a user