save-server
Version:
A powerful ShareX image and URL server, with support for multiple users.
199 lines (180 loc) • 5.82 kB
JavaScript
const express = require("express");
const files = express.Router();
const {
errorCatch,
generateFileName,
errorGenerator,
dest,
prettyError,
validFile,
getBase
} = require("../util");
const multer = require("multer");
const db = require("../util/db");
const auth = require("../middleware/auth");
const csrf = require("../middleware/csrf");
const ratelimit = require("../middleware/ratelimit");
const fs = require("fs");
const path = require("path");
const { isAlphanumeric, isLength, isAscii } = require("validator");
// Max size of extension (dot and all, i.e. .png, .jpeg)
// This is used in validation and just increases the total length of the valid token.
const DEFAULT_FILE_NAME_LENGTH = 6;
const MAX_EXTENSION_SIZE = 8;
let fileNameLength = DEFAULT_FILE_NAME_LENGTH;
// Get file name length
if (process.env.nameLength) {
if (!isNaN(process.env.nameLength)) {
const envNameLength = parseInt(process.env.nameLength, 10);
if (envNameLength > 1 && envNameLength <= 40) {
fileNameLength = envNameLength;
} else {
console.warn(`Warn: Rejected nameLength environment variable - Must be between 1 and 40. Value: ${envNameLength}`);
}
}
}
// Multer options
const storage = multer.diskStorage({
destination: dest,
filename: async function (req, file, cb) {
const tok = await generateFileName(fileNameLength);
file._tok = tok;
// Extract extension
const split = file.originalname.split(".");
if (split.length !== 1) {
const ext = split[split.length - 1];
if (ext.length > 5) {
return cb(null, tok);
} else {
file._ext = ext;
cb(null, `${tok}.${ext}`);
}
} else {
// There is no extension
cb(null, tok);
}
}
});
const removeExt = (str) => str.substring(0, str.indexOf("."));
const upload = multer({
storage: storage,
limits: {
fileSize: 10000000
}
});
const extensions = ["md", "js", "py", "ts", "Lua", "cpp", "c", "bat", "h", "pl", "java", "sh", "swift", "vb", "cs", "haml", "yml", "markdown", "hs", "pl", "ex", "yaml", "jsx", "tsx", "txt"];
async function getFile(req, res, next) {
const { id } = req.params;
// Check main length with allowances for extension length.
if (id && isLength(id, {
min: Math.min(3, fileNameLength),
max: Math.max(DEFAULT_FILE_NAME_LENGTH + MAX_EXTENSION_SIZE, fileNameLength + MAX_EXTENSION_SIZE)
}) && isAscii(id)) {
const without = removeExt(req.params.id);
const idStr = (without === "" ? req.params.id : without);
if (!isAlphanumeric(idStr)) {
return res.status(400).send(prettyError(400, "You provided an invalid file identifier, it should be alphanumeric."));
}
if (idStr.length > fileNameLength) {
return res.status(400).send(prettyError(400, "File name portion too large."));
}
const file = await db.getFile(idStr);
if (file) {
const loc = `${file.id}${file.extension ? `.${file.extension}` : ""}`;
if (file.extension && extensions.includes(file.extension.toLowerCase()) && !req.query.download) {
const content = await openFile(path.join(dest, loc));
return res.render(path.join(__dirname, "..", "client", "pages", "document.ejs"), {
content: content,
isRendered: (file.extension.toLowerCase() === "md" || file.extension.toLowerCase() === "markdown"),
fileName: loc,
owner: file.owner,
// Technically someone could try to pretend to be logged in,
// but all they get to see is a delete button. Nothing gained.
isUser: !!req.cookies.authorization
});
}
const options = {
root: dest
};
res.sendFile(loc, options, function (err) {
if (err) {
next(err);
}
});
} else {
// 404
next();
}
} else {
res.status(400).send(await prettyError(400, "You provided an invalid file identifier."));
}
}
files.get("/:id", errorCatch(getFile));
// Supports uploading multiple files, even though ShareX doesn't.
files.post("/", ratelimit(15, 60));
files.post("/", auth.header, upload.array("files", 10), errorCatch(async function (req, res) {
if (!req.user) {
return console.log("what??");
}
if (req.files && req.files.length !== 0) {
for (const file of req.files) {
db.addFile(file._tok, file._ext || undefined, req.user.username);
}
const base = getBase(req);
res.send({
url: `${base}/${req.files[0].filename}`,
deletionUrl: `${base}/dashboard`
});
} else {
res.status(400).send(errorGenerator(400, "No file upload detected!"));
}
}));
files.use(csrf);
files.use(auth);
files.delete("/:id", errorCatch(async function (req, res, next) {
if (req.params.id && validFile(req.params.id)) {
const without = removeExt(req.params.id);
const idStr = (without === "" ? req.params.id : without);
const file = await db.getFile(idStr);
if (file) {
if ((file.owner === req.user.username) || req.user.isAdmin) {
await db.removeFile(file.id);
const loc = `${file.id}${file.extension ? `.${file.extension}` : ""}`;
fs.unlink(path.join(dest, loc), (err) => {
if (err) {
if (err.code === "ENOENT") {
console.log(`Tried to delete file ${loc} but it was already removed.`);
} else {
return next(err);
}
}
return res.send({ success: true, message: "File deleted." });
});
} else {
return res.status(403).send(errorGenerator(403, "You are not allowed to edit that file."));
}
} else {
return res.status(400).send(errorGenerator(404, "File not found."));
}
} else {
return res.status(400).send(errorGenerator(400, "Invalid file id."));
}
}));
module.exports = {
router: files,
getFile
};
/*
File recieved;
- Name allocated
- File extension extracted
- Saved to database
*/
function openFile(path) {
return new Promise(function (resolve, reject) {
fs.readFile(path, "utf8", (err, data) => {
if (err) reject(err);
resolve(data);
});
});
}