From dcbeab0aef10d6cd5eb7487f8eb355b372feefd1 Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 12 Apr 2024 19:53:46 +0300 Subject: [PATCH] Fix absolute paths for data root. Allow setting data root via console args. --- server.js | 7 ++- src/endpoints/assets.js | 9 ++-- src/endpoints/characters.js | 52 +++++++++++-------- src/endpoints/content-manager.js | 2 +- src/endpoints/thumbnails.js | 85 +++++++++++++++++++++----------- src/users.js | 13 ++++- 6 files changed, 112 insertions(+), 56 deletions(-) diff --git a/server.js b/server.js index a71d5470a..da695927d 100644 --- a/server.js +++ b/server.js @@ -102,6 +102,10 @@ const cliArguments = yargs(hideBin(process.argv)) type: 'boolean', default: false, describe: 'Enables whitelist mode', + }).option('dataRoot', { + type: 'string', + default: null, + describe: 'Root directory for data storage', }).parseSync(); // change all relative paths @@ -121,6 +125,7 @@ const autorun = (cliArguments.autorun ?? getConfigValue('autorun', DEFAULT_AUTOR const listen = cliArguments.listen ?? getConfigValue('listen', DEFAULT_LISTEN); const enableCorsProxy = cliArguments.corsProxy ?? getConfigValue('enableCorsProxy', DEFAULT_CORS_PROXY); const enableWhitelist = cliArguments.whitelist ?? getConfigValue('whitelistMode', DEFAULT_WHITELIST); +const dataRoot = cliArguments.dataRoot ?? getConfigValue('dataRoot', './data'); const basicAuthMode = getConfigValue('basicAuthMode', false); const enableAccounts = getConfigValue('enableUserAccounts', false); @@ -526,7 +531,7 @@ const setupTasks = async function () { // TODO: do endpoint init functions depend on certain directories existing or not existing? They should be callable // in any order for encapsulation reasons, but right now it's unknown if that would break anything. - await userModule.initUserStorage(); + await userModule.initUserStorage(dataRoot); await settingsEndpoint.init(); const directories = await userModule.ensurePublicDirectoriesExist(); await userModule.migrateUserData(); diff --git a/src/endpoints/assets.js b/src/endpoints/assets.js index 352eb2476..a9dc317d6 100644 --- a/src/endpoints/assets.js +++ b/src/endpoints/assets.js @@ -1,5 +1,6 @@ const path = require('path'); const fs = require('fs'); +const mime = require('mime-types'); const express = require('express'); const sanitize = require('sanitize-filename'); const fetch = require('node-fetch').default; @@ -216,9 +217,11 @@ router.post('/download', jsonParser, async (request, response) => { await finished(res.body.pipe(fileStream)); if (category === 'character') { - response.sendFile(temp_path, { root: process.cwd() }, () => { - fs.rmSync(temp_path); - }); + const fileContent = fs.readFileSync(temp_path); + const contentType = mime.lookup(temp_path) || 'application/octet-stream'; + response.setHeader('Content-Type', contentType); + response.send(fileContent); + fs.rmSync(temp_path); return; } diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 4ed9e7c0c..ee586168b 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -1,11 +1,13 @@ const path = require('path'); const fs = require('fs'); +const fsPromises = require('fs').promises; const readline = require('readline'); const express = require('express'); const sanitize = require('sanitize-filename'); const writeFileAtomicSync = require('write-file-atomic').sync; const yaml = require('yaml'); const _ = require('lodash'); +const mime = require('mime-types'); const jimp = require('jimp'); @@ -1078,33 +1080,43 @@ router.post('/duplicate', jsonParser, async function (request, response) { }); router.post('/export', jsonParser, async function (request, response) { - if (!request.body.format || !request.body.avatar_url) { - return response.sendStatus(400); - } + try { + if (!request.body.format || !request.body.avatar_url) { + return response.sendStatus(400); + } - let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url)); + let filename = path.join(request.user.directories.characters, sanitize(request.body.avatar_url)); - if (!fs.existsSync(filename)) { - return response.sendStatus(404); - } + if (!fs.existsSync(filename)) { + return response.sendStatus(404); + } - switch (request.body.format) { - case 'png': - return response.sendFile(filename, { root: process.cwd() }); - case 'json': { - try { - let json = await readCharacterData(filename); - if (json === undefined) return response.sendStatus(400); - let jsonObject = getCharaCardV2(JSON.parse(json), request.user.directories); - return response.type('json').send(JSON.stringify(jsonObject, null, 4)); + switch (request.body.format) { + case 'png': { + const fileContent = await fsPromises.readFile(filename); + const contentType = mime.lookup(filename) || 'image/png'; + response.setHeader('Content-Type', contentType); + response.setHeader('Content-Disposition', `attachment; filename=${path.basename(filename)}`); + return response.send(fileContent); } - catch { - return response.sendStatus(400); + case 'json': { + try { + let json = await readCharacterData(filename); + if (json === undefined) return response.sendStatus(400); + let jsonObject = getCharaCardV2(JSON.parse(json), request.user.directories); + return response.type('json').send(JSON.stringify(jsonObject, null, 4)); + } + catch { + return response.sendStatus(400); + } } } - } - return response.sendStatus(400); + return response.sendStatus(400); + } catch (err) { + console.error('Character export failed', err); + response.sendStatus(500); + } }); module.exports = { router }; diff --git a/src/endpoints/content-manager.js b/src/endpoints/content-manager.js index 6149c9e70..6d736b9a8 100644 --- a/src/endpoints/content-manager.js +++ b/src/endpoints/content-manager.js @@ -122,7 +122,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) { } const basePath = path.parse(contentItem.filename).base; - const targetPath = path.join(process.cwd(), contentTarget, basePath); + const targetPath = path.join(contentTarget, basePath); if (fs.existsSync(targetPath)) { console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`); diff --git a/src/endpoints/thumbnails.js b/src/endpoints/thumbnails.js index 6815efa4c..6ec1c6c81 100644 --- a/src/endpoints/thumbnails.js +++ b/src/endpoints/thumbnails.js @@ -1,5 +1,7 @@ const fs = require('fs'); +const fsPromises = require('fs').promises; const path = require('path'); +const mime = require('mime-types'); const express = require('express'); const sanitize = require('sanitize-filename'); const jimp = require('jimp'); @@ -165,38 +167,63 @@ const router = express.Router(); // Important: This route must be mounted as '/thumbnail'. It is used in the client code and saved to chat files. router.get('/', jsonParser, async function (request, response) { - if (typeof request.query.file !== 'string' || typeof request.query.type !== 'string') return response.sendStatus(400); + try{ + if (typeof request.query.file !== 'string' || typeof request.query.type !== 'string') { + return response.sendStatus(400); + } - const type = request.query.type; - const file = sanitize(request.query.file); + const type = request.query.type; + const file = sanitize(request.query.file); - if (!type || !file) { - return response.sendStatus(400); + if (!type || !file) { + return response.sendStatus(400); + } + + if (!(type == 'bg' || type == 'avatar')) { + return response.sendStatus(400); + } + + if (sanitize(file) !== file) { + console.error('Malicious filename prevented'); + return response.sendStatus(403); + } + + const thumbnailsDisabled = getConfigValue('disableThumbnails', false); + if (thumbnailsDisabled) { + const folder = getOriginalFolder(request.user.directories, type); + + if (folder === undefined) { + return response.sendStatus(400); + } + + const pathToOriginalFile = path.join(folder, file); + if (!fs.existsSync(pathToOriginalFile)) { + return response.sendStatus(404); + } + const contentType = mime.lookup(pathToOriginalFile) || 'image/png'; + const originalFile = await fsPromises.readFile(pathToOriginalFile); + response.setHeader('Content-Type', contentType); + return response.send(originalFile); + } + + const pathToCachedFile = await generateThumbnail(request.user.directories, type, file); + + if (!pathToCachedFile) { + return response.sendStatus(404); + } + + if (!fs.existsSync(pathToCachedFile)) { + return response.sendStatus(404); + } + + const contentType = mime.lookup(pathToCachedFile) || 'image/jpeg'; + const cachedFile = await fsPromises.readFile(pathToCachedFile); + response.setHeader('Content-Type', contentType); + return response.send(cachedFile); + } catch (error) { + console.error('Failed getting thumbnail', error); + return response.sendStatus(500); } - - if (!(type == 'bg' || type == 'avatar')) { - return response.sendStatus(400); - } - - if (sanitize(file) !== file) { - console.error('Malicious filename prevented'); - return response.sendStatus(403); - } - - if (getConfigValue('disableThumbnails', false) == true) { - let folder = getOriginalFolder(request.user.directories, type); - if (folder === undefined) return response.sendStatus(400); - const pathToOriginalFile = path.join(folder, file); - return response.sendFile(pathToOriginalFile, { root: process.cwd() }); - } - - const pathToCachedFile = await generateThumbnail(request.user.directories, type, file); - - if (!pathToCachedFile) { - return response.sendStatus(404); - } - - return response.sendFile(pathToCachedFile, { root: process.cwd() }); }); module.exports = { diff --git a/src/users.js b/src/users.js index 1290c67ca..9adbb1d67 100644 --- a/src/users.js +++ b/src/users.js @@ -15,10 +15,15 @@ const { getConfigValue, color, delay, setConfigValue, generateTimestamp } = requ const { readSecret, writeSecret } = require('./endpoints/secrets'); const KEY_PREFIX = 'user:'; -const DATA_ROOT = getConfigValue('dataRoot', './data'); const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false); const ANON_CSRF_SECRET = crypto.randomBytes(64).toString('base64'); +/** + * The root directory for user data. + * @type {string} + */ +let DATA_ROOT = './data'; + /** * Cache for user directories. * @type {Map} @@ -312,9 +317,13 @@ function toKey(handle) { /** * Initializes the user storage. Currently a no-op. + * @param {string} dataRoot The root directory for user data * @returns {Promise} */ -async function initUserStorage() { +async function initUserStorage(dataRoot) { + DATA_ROOT = dataRoot; + console.log('Using data root:', color.green(DATA_ROOT)); + console.log(); await storage.init({ dir: path.join(DATA_ROOT, '_storage'), ttl: true,