const utils = require('../../utils');
const time = require('../../utils/time');
const clamscan = require('../../utils/clamscan');
const ffmpeg = require('../../utils/ffmpeg');
const ws_utils = require('../../websocket/utils');
const broadcast = require('../../websocket/broadcast');
const upload_session = require('../../utils/upload_session');
const fs = require('fs');
const path = require('path');
const subtitle = require('subtitle');
const uploader = require('huge-uploader-nodejs');
const pretty_bytes = require('pretty-bytes');
module.exports = {
post: {
"subtitle": function(req, res) {
let file_path;
utils.validate.keys(req.body, [
['video_id', utils.validate.uuid]
]).then(function() {
file_path = path.resolve(VIDEO_ROOT, req.body.video_id, "subtitle.json");
return utils.fs.stat(file_path).catch(function() {
// Errors here just mean that the subtitle file does not exist
return utils.reject([utils.status.ok, {}]);
});
}).then(function() {
return utils.fs.readFile(file_path);
}).then(function(subtitle) {
res.send(utils.ok({
subtitles: JSON.parse(subtitle.toString())
}));
}).catch(utils.handle_err.res(res, "Error getting subtitles"));
},
"upload": {
"subtitle": function(req, res) {
let video_dir;
let subtitle_path;
utils.validate.keys(req.body, [
['video_id', utils.validate.uuid], 'subtitle_string'
]).then(function() {
video_dir = path.resolve(VIDEO_ROOT, req.body.video_id);
// Make sure the video we are uploading subtitles for actually exists
return utils.fs.stat(video_dir);
}).then(function() {
subtitle_path = path.resolve(video_dir, "subtitle.json");
return utils.fs.writeFile(
subtitle_path,
JSON.stringify(subtitle.parse(req.body.subtitle_string))
);
}).then(function() {
return clamscan.assert_safe(subtitle_path, {
ip: res.locals.ip,
user_id: req.user.user_id
});
}).then(function() {
res.send(utils.ok());
}).catch(utils.handle_err.res(res, "Error uploading subtitles"));
},
"request": function(req, res) {
const TEMP_UPLOAD_DIR_USER = path.join(TEMP_UPLOAD_DIR, req.user.user_id);
utils.validate.keys(req.body, [
['bytes', bytes => typeof bytes === "number"], 'name'
]).then(function() {
if (upload_session.get(req.user)) {
return utils.reject("You already have an upload session elsewhere");
}
return utils.fs.mkdir_if_not_exists(TEMP_UPLOAD_DIR_USER);
}).then(function() {
return utils.get_storage_remaining();
}).then(function(bytes) {
let reserved_bytes = Object.values(upload_session.get()).reduce(function(reserved_bytes, upload) {
return reserved_bytes + upload.size;
}, 0);
// Multiply by 2 because there must be
// room for the converted file as well
if (req.body.bytes * 2 + reserved_bytes > bytes) {
return utils.reject(
`Not enough storage available on server. Max upload size is ${pretty_bytes((bytes - reserved_bytes) / 2)}`
);
}
upload_session.create(
req.user, req.body.name, req.body.bytes,
TEMP_UPLOAD_DIR_USER
);
res.send(utils.ok());
}).catch(utils.handle_err.res(res, "Error requesting upload"));
},
"": [{
rate_limit: false
}, function(req, res) {
let ongoing_upload = upload_session.get(req.user);
if (!ongoing_upload) {
return utils.handle_err.res(res)("Error uploading file, no upload session");
}
uploader(
req,
ongoing_upload.temp_dir,
// bytes / 1000000
ongoing_upload.size, // Max total file size in MB
10 // Chunk size in MB
).then(function(assemble_chunks) {
// chunk written to disk
res.writeHead(204, 'No Content');
res.end();
if (!assemble_chunks) {
return utils.resolve();
}
// on last chunk, assembleChunks function is returned
// the response is already sent to the browser because it can take some time if the file is huge
res.locals.log.v("Final Chunk Uploaded");
let data = Object.assign({
user_id: req.user.user_id
}, ongoing_upload.video);
upload_session.delete(req.user);
const video_create_data = {
video_id: data.video_id,
name: data.name,
status: utils.status.video.reassembling,
expires: Date.now() + (time.one_day * 7),
created_by: req.user.user_id
};
return utils.query(
"INSERT INTO videos SET ?",
video_create_data
).catch(utils.handle_err.sql(res)).then(function() {
broadcast({
command: "new-video",
data: video_create_data
}, {
[req.user.user_id]: true
});
return assemble_chunks();
}).then(function(upload_data) {
Object.assign(data, {
temp_file_path: upload_data.filePath
});
return clamscan.assert_safe(data.temp_file_path, {
ip: res.locals.ip,
user_id: req.user.user_id
});
}).then(function() {
return utils.query("UPDATE videos" + utils.set_where({
name: data.name,
status: utils.status.video.converting
}, {
video_id: data.video_id
})).catch(utils.handle_err.sql(res));
}).then(function() {
// Big Boi
return ffmpeg.convert(data, {
delete_source: true
});
}).then(function() {
res.locals.log.v(data.name, "converted!");
broadcast({
command: "video-status",
data: {
video_id: data.video_id,
status: utils.status.video.converting,
progress: 100
}
}, {
[req.user.user_id]: true
});
}).then(function() {
return utils.query("UPDATE videos" + utils.set_where({
status: utils.status.video.ready
}, {
video_id: data.video_id
})).catch(utils.handle_err.sql(res));
}).then(function() {
broadcast({
command: "video-status",
data: {
video_id: data.video_id,
status: utils.status.video.ready
}
}, {
[req.user.user_id]: true
});
}).catch(function(error) {
res.locals.log.general.error(error);
upload_session.delete(req.user);
return utils.rimraf(ongoing_upload.temp_dir).then(function() {
return utils.query(
"DELETE FROM videos WHERE video_id=?",
[data.video_id]
).catch(utils.handle_err.sql(res, true));
}).then(function() {
broadcast({
command: "delete-video-response",
data: utils.ok({
video_id: data.video_id,
name: data.name,
reason: "upload failed"
})
}, {
[req.user.user_id]: true
});
});
});
}).catch(utils.handle_err.status_res(res)).finally(ws_utils.broadcast_storage);
}]
}
},
get: {
":video_id": {
"": [{
rate_limit: false
}, function(req, res) {
let file_path;
utils.validate.keys(req.params, [
['video_id', utils.validate.uuid]
]).then(function() {
file_path = path.resolve(VIDEO_ROOT, req.params.video_id, "video");
return utils.fs.stat(file_path);
}).then(function(stat) {
const range = req.headers.range;
if (range) {
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : stat.size - 1;
if (start >= stat.size) {
res.status(416).send('Requested range not satisfiable\n' + start + ' >= ' + stat.size);
return;
}
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${stat.size}`,
'Accept-Ranges': 'bytes',
'Content-Length': (end - start) + 1,
'Content-Type': 'video/mp4',
});
fs.createReadStream(file_path, {
start,
end
}).pipe(res);
} else {
res.writeHead(200, {
'Content-Length': stat.size,
'Content-Type': 'video/mp4',
});
fs.createReadStream(file_path).pipe(res);
}
}).catch(function(error) {
res.locals.log.general.error(error);
res.status(400).send("File not found");
});
}]
}
}
};