diff --git a/package.json b/package.json index 6663586b..786dc4b1 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "start": "node cli-boot-wrapper.js", "pack": "electron-builder --dir", "dist": "electron-builder", - "wizard": "npm install --only=prod && node cli-boot-wrapper.js" + "wizard": "npm install --only=prod && node cli-boot-wrapper.js", + "dev": "node --watch-path=./src cli-boot-wrapper.js" }, "repository": { "type": "git", diff --git a/src/api/auth.js b/src/api/auth.js index 7d9c2b2e..fed74e81 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -44,7 +44,8 @@ exports.setup = (mstream) => { req.user = { vpaths: Object.keys(config.program.folders), username: 'mstream-user', - admin: true + admin: true, + editFilePrivileges: false }; return next(); diff --git a/src/api/file-explorer.js b/src/api/file-explorer.js index 48e0b023..55e9e524 100644 --- a/src/api/file-explorer.js +++ b/src/api/file-explorer.js @@ -3,7 +3,6 @@ const fs = require('fs').promises; const fsOld = require('fs'); const busboy = require("busboy"); const Joi = require('joi'); -const mkdirp = require('make-dir'); const winston = require('winston'); const fileExplorer = require('../util/file-explorer'); const vpath = require('../util/vpath'); @@ -42,12 +41,6 @@ exports.setup = (mstream) => { // Get vPath Info const pathInfo = vpath.getVPathInfo(value.directory, req.user); - // Do not allow browsing outside the directory - if (pathInfo.fullPath.substring(0, pathInfo.basePath.length) !== pathInfo.basePath) { - winston.warn(`user '${req.user.username}' attempted to access a directory they don't have access to: ${pathInfo.fullPath}`) - throw new Error('Access to directory not allowed'); - } - // get directory contents const folderContents = await fileExplorer.getDirectoryContents(pathInfo.fullPath, config.program.supportedAudioFiles, value.sort, value.pullMetadata, value.directory, req.user); @@ -91,21 +84,15 @@ exports.setup = (mstream) => { // Get vPath Info const pathInfo = vpath.getVPathInfo(req.body.directory, req.user); - // Do not allow browsing outside the directory - if (pathInfo.fullPath.substring(0, pathInfo.basePath.length) !== pathInfo.basePath) { - winston.warn(`user '${req.user.username}' attempted to access a directory they don't have access to: ${pathInfo.fullPath}`) - throw new Error('Access to directory not allowed'); - } - res.json(await recursiveFileScan(pathInfo.fullPath, [], pathInfo.relativePath, pathInfo.vpath)); }); - mstream.post('/api/v1/file-explorer/upload', (req, res) => { + mstream.post('/api/v1/file-explorer/upload', async (req, res) => { if (config.program.noUpload === true) { throw new WebError('Uploading Disabled'); } if (!req.headers['data-location']) { throw new WebError('No Location Provided', 403); } const pathInfo = vpath.getVPathInfo(decodeURI(req.headers['data-location']), req.user); - mkdirp.sync(pathInfo.fullPath); + await fs.mkdir(pathInfo.fullPath, { recursive: true }); const bb = busboy({ headers: req.headers }); bb.on('file', (fieldname, file, info) => { @@ -134,4 +121,67 @@ exports.setup = (mstream) => { }) }); }); + + mstream.post("/api/v1/file-explorer/rename", async (req, res) => { + const schema = Joi.object({ + path: Joi.string().required(), + newName: Joi.string().required() + }); + joiValidate(schema, req.body); + + const pathInfo = vpath.getVPathInfo(req.body.path, req.user); + + // check permissions + if (config.program.folders[pathInfo.vpath].edit.rename !== true) { throw new WebError('Rename Disabled'); } + if (req.user.editFilePrivileges.rename !== true) { throw new WebError('Rename Disabled'); } + + // check if path exists + await fs.stat(pathInfo.fullPath); + + // rename + const newt = path.join(path.dirname(pathInfo.fullPath), req.body.newName); + await fs.rename(pathInfo.fullPath, newt); + + res.json({}); + }); + + mstream.delete("/api/v1/file-explorer/delete", async (req, res) => { + const schema = Joi.object({ path: Joi.string().required() }); + joiValidate(schema, req.body); + + const pathInfo = vpath.getVPathInfo(req.body.path, req.user); + + // check permissions + if (config.program.folders[pathInfo.vpath].edit.delete !== true) { throw new WebError('Rename Disabled'); } + if (req.user.editFilePrivileges.delete !== true) { throw new WebError('Rename Disabled'); } + + // check if path exists + const stat = await fs.stat(pathInfo.fullPath); + + if (stat.isDirectory()) { + await fs.rm(pathInfo.fullPath); + } else { + await fs.unlink(pathInfo.fullPath); + } + + // delete + res.json({}); + }); + + // mstream.post("/api/v1/file-explorer/move", async (req, res) => { + + // }); + + mstream.post("/api/v1/file-explorer/mkdir", async (req, res) => { + const schema = Joi.object({ path: Joi.string().required(), newDirName: Joi.string().required() }); + joiValidate(schema, req.body); + + const pathInfo = vpath.getVPathInfo(req.body.path, req.user); + + // check permissions + if (config.program.folders[pathInfo.vpath].edit.mkdir !== true) { throw new WebError('Rename Disabled'); } + if (req.user.editFilePrivileges.mkdir !== true) { throw new WebError('Rename Disabled'); } + + await fs.mkdir(pathInfo.fullPath, { recursive: true }); + }); } \ No newline at end of file diff --git a/src/state/config.js b/src/state/config.js index 7398c589..793b4683 100644 --- a/src/state/config.js +++ b/src/state/config.js @@ -54,6 +54,13 @@ const federationOptions = Joi.object({ federateUsersMode: Joi.boolean().default(false), }); +const editFileOptions = Joi.object({ + rename: Joi.boolean().default(false), + move: Joi.boolean().default(false), + delete: Joi.boolean().default(false), + mkdir: Joi.boolean().default(false), +}); + const schema = Joi.object({ address: Joi.string().ip({ cidr: 'forbidden' }).default('::'), port: Joi.number().default(3000), @@ -81,11 +88,13 @@ const schema = Joi.object({ Joi.object({ root: Joi.string().required(), type: Joi.string().valid('music', 'audio-books').default('music'), + edit: editFileOptions.default(editFileOptions.validate({}).value), }) ).default({}), users: Joi.object().pattern( Joi.string(), Joi.object({ + editFilePrivileges: Joi.boolean().default(false), password: Joi.string().required(), admin: Joi.boolean().default(false), salt: Joi.string().required(), diff --git a/src/util/vpath.js b/src/util/vpath.js index a1220c6a..bf52590b 100644 --- a/src/util/vpath.js +++ b/src/util/vpath.js @@ -1,5 +1,6 @@ const path = require('path'); const config = require('../state/config'); +const winston = require('winston'); exports.getVPathInfo = (url, user) => { if (!config.program) { throw new Error('Not Configured'); } @@ -17,10 +18,18 @@ exports.getVPathInfo = (url, user) => { } const baseDir = config.program.folders[vpath].root; + const fullPath = path.join(baseDir, path.relative(vpath, url)); + + // Do not allow browsing outside the directory + if (fullPath.substring(0, baseDir.length) !== baseDir) { + winston.warn(`user '${user.username}' attempted to access a directory they don't have access to: ${fullPath}`) + throw new Error('Access to directory not allowed'); + } + return { vpath: vpath, basePath: baseDir, relativePath: path.relative(vpath, url), - fullPath: path.join(baseDir, path.relative(vpath, url)) + fullPath: fullPath }; }