hardlounge/src/plugins/uploader.js

152 lines
3.7 KiB
JavaScript
Raw Normal View History

"use strict";
const SocketIOFileUploadServer = require("socketio-file-upload/server");
const Helper = require("../helper");
const path = require("path");
const fsextra = require("fs-extra");
const fs = require("fs");
const fileType = require("file-type");
const readChunk = require("read-chunk");
const crypto = require("crypto");
const isUtf8 = require("is-utf8");
const log = require("../log");
const whitelist = [
"application/ogg",
"audio/midi",
"audio/mpeg",
"audio/ogg",
2018-12-19 15:20:47 +00:00
"audio/vnd.wave",
"image/bmp",
"image/gif",
"image/jpeg",
"image/png",
"image/webp",
"text/plain",
"video/mp4",
"video/ogg",
"video/webm",
];
class Uploader {
constructor(socket) {
const uploader = new SocketIOFileUploadServer();
const folder = path.join(Helper.getFileUploadPath(), ".tmp");
fsextra.ensureDir(folder, (err) => {
if (err) {
log.err(`Error ensuring ${folder} exists for uploads.`);
} else {
uploader.dir = folder;
}
});
uploader.on("complete", (data) => {
let randomName;
let destPath;
// Generate a random file name for storage on disk
do {
randomName = crypto.randomBytes(8).toString("hex");
destPath = path.join(Helper.getFileUploadPath(), randomName.substring(0, 2), randomName);
2019-01-19 10:00:09 +00:00
} while (fs.existsSync(destPath));
fsextra.move(data.file.pathName, destPath).then(() => {
2018-10-13 21:23:41 +00:00
const slug = encodeURIComponent(path.basename(data.file.pathName));
const url = `uploads/${randomName}/${slug}`;
socket.emit("upload:success", url);
}).catch(() => {
log.warn(`Unable to move uploaded file "${data.file.pathName}"`);
// Emit failed upload to the client if file move fails
socket.emit("siofu_error", {
id: data.file.id,
message: "Unable to move uploaded file",
});
});
});
uploader.on("error", (data) => {
log.error(`File upload failed: ${data.error}`);
});
// maxFileSize is in bytes, but config option is passed in as KB
uploader.maxFileSize = Uploader.getMaxFileSize();
uploader.listen(socket);
}
static isValidType(mimeType) {
return whitelist.includes(mimeType);
}
static router(express) {
express.get("/uploads/:name/:slug*?", (req, res) => {
const name = req.params.name;
const nameRegex = /^[0-9a-f]{16}$/;
if (!nameRegex.test(name)) {
return res.status(404).send("Not found");
}
const folder = name.substring(0, 2);
const uploadPath = Helper.getFileUploadPath();
const filePath = path.join(uploadPath, folder, name);
const detectedMimeType = Uploader.getFileType(filePath);
// doesn't exist
if (detectedMimeType === null) {
return res.status(404).send("Not found");
}
// Force a download in the browser if it's not a whitelisted type (binary or otherwise unknown)
const contentDisposition = Uploader.isValidType(detectedMimeType) ? "inline" : "attachment";
res.setHeader("Content-Disposition", contentDisposition);
res.setHeader("Cache-Control", "max-age=86400");
res.contentType(detectedMimeType);
return res.sendFile(filePath);
});
}
static getMaxFileSize() {
const configOption = Helper.config.fileUpload.maxFileSize;
if (configOption === -1) { // no file size limit
return null;
}
return configOption * 1024;
}
// Returns null if an error occurred (e.g. file not found)
// Returns a string with the type otherwise
static getFileType(filePath) {
try {
const buffer = readChunk.sync(filePath, 0, fileType.minimumBytes);
// returns {ext, mime} if found, null if not.
const file = fileType(buffer);
if (file) {
return file.mime;
}
if (isUtf8(buffer)) {
return "text/plain";
}
return "application/octet-stream";
} catch (e) {
if (e.code !== "ENOENT") {
log.warn(`Failed to read ${filePath}: ${e.message}`);
}
}
return null;
}
}
module.exports = Uploader;