trilium/src/routes/api/files.js

235 lines
6.5 KiB
JavaScript

"use strict";
const protectedSessionService = require('../../services/protected_session.js');
const utils = require('../../services/utils.js');
const log = require('../../services/log.js');
const noteService = require('../../services/notes.js');
const tmp = require('tmp');
const fs = require('fs');
const { Readable } = require('stream');
const chokidar = require('chokidar');
const ws = require('../../services/ws.js');
const becca = require('../../becca/becca.js');
const ValidationError = require('../../errors/validation_error.js');
function updateFile(req) {
const note = becca.getNoteOrThrow(req.params.noteId);
const file = req.file;
note.saveRevision();
note.mime = file.mimetype.toLowerCase();
note.save();
note.setContent(file.buffer);
note.setLabel('originalFileName', file.originalname);
noteService.asyncPostProcessContent(note, file.buffer);
return {
uploaded: true
};
}
function updateAttachment(req) {
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
const file = req.file;
attachment.getNote().saveRevision();
attachment.mime = file.mimetype.toLowerCase();
attachment.setContent(file.buffer, {forceSave: true});
return {
uploaded: true
};
}
/**
* @param {BNote|BAttachment} noteOrAttachment
* @param res
* @param {boolean} contentDisposition
*/
function downloadData(noteOrAttachment, res, contentDisposition) {
if (noteOrAttachment.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
return res.status(401).send("Protected session not available");
}
if (contentDisposition) {
const fileName = noteOrAttachment.getFileName();
res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
}
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', noteOrAttachment.mime);
res.send(noteOrAttachment.getContent());
}
function downloadNoteInt(noteId, res, contentDisposition = true) {
const note = becca.getNote(noteId);
if (!note) {
return res.setHeader("Content-Type", "text/plain")
.status(404)
.send(`Note '${noteId}' doesn't exist.`);
}
return downloadData(note, res, contentDisposition);
}
function downloadAttachmentInt(attachmentId, res, contentDisposition = true) {
const attachment = becca.getAttachment(attachmentId);
if (!attachment) {
return res.setHeader("Content-Type", "text/plain")
.status(404)
.send(`Attachment '${attachmentId}' doesn't exist.`);
}
return downloadData(attachment, res, contentDisposition);
}
const downloadFile = (req, res) => downloadNoteInt(req.params.noteId, res, true);
const openFile = (req, res) => downloadNoteInt(req.params.noteId, res, false);
const downloadAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, true);
const openAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, false);
function fileContentProvider(req) {
// Read the file name from route params.
const note = becca.getNoteOrThrow(req.params.noteId);
return streamContent(note.getContent(), note.getFileName(), note.mime);
}
function attachmentContentProvider(req) {
// Read the file name from route params.
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime);
}
function streamContent(content, fileName, mimeType) {
if (typeof content === "string") {
content = Buffer.from(content, 'utf8');
}
const totalSize = content.byteLength;
const getStream = range => {
if (!range) {
// Request if for complete content.
return Readable.from(content);
}
// Partial content request.
const {start, end} = range;
return Readable.from(content.slice(start, end + 1));
}
return {
fileName,
totalSize,
mimeType,
getStream
};
}
function saveNoteToTmpDir(req) {
const note = becca.getNoteOrThrow(req.params.noteId);
const fileName = note.getFileName();
const content = note.getContent();
return saveToTmpDir(fileName, content, 'notes', note.noteId);
}
function saveAttachmentToTmpDir(req) {
const attachment = becca.getAttachmentOrThrow(req.params.attachmentId);
const fileName = attachment.getFileName();
const content = attachment.getContent();
return saveToTmpDir(fileName, content, 'attachments', attachment.attachmentId);
}
function saveToTmpDir(fileName, content, entityType, entityId) {
const tmpObj = tmp.fileSync({ postfix: fileName });
fs.writeSync(tmpObj.fd, content);
fs.closeSync(tmpObj.fd);
log.info(`Saved temporary file ${tmpObj.name}`);
if (utils.isElectron()) {
chokidar.watch(tmpObj.name).on('change', (path, stats) => {
ws.sendMessageToAllClients({
type: 'openedFileUpdated',
entityType: entityType,
entityId: entityId,
lastModifiedMs: stats.atimeMs,
filePath: tmpObj.name
});
});
}
return {
tmpFilePath: tmpObj.name
};
}
function uploadModifiedFileToNote(req) {
const noteId = req.params.noteId;
const {filePath} = req.body;
const note = becca.getNoteOrThrow(noteId);
log.info(`Updating note '${noteId}' with content from '${filePath}'`);
note.saveRevision();
const fileContent = fs.readFileSync(filePath);
if (!fileContent) {
throw new ValidationError(`File '${fileContent}' is empty`);
}
note.setContent(fileContent);
}
function uploadModifiedFileToAttachment(req) {
const {attachmentId} = req.params;
const {filePath} = req.body;
const attachment = becca.getAttachmentOrThrow(attachmentId);
log.info(`Updating attachment '${attachmentId}' with content from '${filePath}'`);
attachment.getNote().saveRevision();
const fileContent = fs.readFileSync(filePath);
if (!fileContent) {
throw new ValidationError(`File '${fileContent}' is empty`);
}
attachment.setContent(fileContent);
}
module.exports = {
updateFile,
updateAttachment,
openFile,
fileContentProvider,
downloadFile,
downloadNoteInt,
saveNoteToTmpDir,
openAttachment,
downloadAttachment,
saveAttachmentToTmpDir,
attachmentContentProvider,
uploadModifiedFileToNote,
uploadModifiedFileToAttachment
};