UNPKG

rich-filemanager

Version:

Highly customizable open-source file manager

473 lines (434 loc) 16.7 kB
/** * Created by Joshua.Austill on 8/11/2016. This connector is actually an api, so you can use it on a seperate server from your ui. However, because of this, it will require a bit more setup than your average connector. Second, you will need to add path-posix and multer to your project, just run npm install --save path-posix && npm install --save multer And you should be good to go there. I named it paths instead of path in this file because RichFilemanager is passing in a path variable and I wanted to keep them as clear as possible. Next, you will need a copy of your filemanager.config.json file in /config . This is to keep from having to do an ajax request back to the ui for every single request. Hopefully in future we will get the server side and client side config seperated into two seperate files. In the mean-time, this means keeping two copies of your config when using nodejs, not ideal, sorry. Lastly, you will need to require this file and use it as a route. My call looks like this const path = require('path'); router.use('/filemanager', require('./filemanager')(path.normalize(`${__dirname}/public`))); */ /* eslint-disable prefer-template */ const express = require('express'); const fs = require('fs'); const paths = require('path'); const multer = require('multer'); const config = require('../app/config/filemanager.config.json'); paths.posix = require('path-posix'); const router = express.Router(); // eslint-disable-line const upload = multer({dest: 'public/'}); module.exports = (__appRoot) => { // eslint-disable-line max-statements // We will handle errors consistently by using a function that returns an error object function errors(err) { const error = err || {}; // This allows us to call errors and just get a default error return { Error: error.Error, nodeCode: error.errno, Code: -1, };// return }// errors // This is a seperate function because branch new files are uploaded and won't have an existing file // to get information from function parseNewPath(inputPath, callback) { let path = inputPath; const parsedPath = {}; const fileRoot = config.options.fileRoot || ''; parsedPath.uiPath = path; // if the passed in path isn't in the fileRoot path, make it so // This should go away and every path should be relative to the fileRoot if (path.substring(0, fileRoot.length) !== fileRoot) { path = paths.posix.join(fileRoot, path); } parsedPath.relativePath = paths.posix.normalize(path); parsedPath.filename = paths.posix.basename(parsedPath.relativePath); parsedPath.osRelativePath = paths.normalize(path); parsedPath.osExecutionPath = __appRoot; parsedPath.osFullPath = paths.join(parsedPath.osExecutionPath, parsedPath.osRelativePath); parsedPath.osFullDirectory = paths.parse(parsedPath.osFullPath).dir; callback(parsedPath); }// parseNewPath // because of windows, we are going to start by parsing out all the needed path information // this will include original values, as well as OS specific values function parsePath(path, callback) { parseNewPath(path, (parsedPath) => { fs.stat(parsedPath.osFullPath, (err, stats) => { if (err) { callback(errors(err)); } else if (stats.isDirectory()) { parsedPath.isDirectory = true; parsedPath.stats = stats; callback(parsedPath); } else if (stats.isFile()) { parsedPath.isDirectory = false; parsedPath.stats = stats; callback(parsedPath); } else { callback(errors(err)); } }); });// parseNewPath }// parsePath // This function will create the return object for a file. This keeps it consistent and // adheres to the DRY principle function fileInfo(pp, callback) { const result = { id: pp.uiPath, type: 'file', attributes: { created: pp.stats.birthtime, modified: pp.stats.mtime, name: pp.filename, path: pp.uiPath, readable: 1, writable: 1, timestamp: '', }, }; callback(result); }// fileInfo // This function will create the return object for a directory. This keeps it consistent and // adheres to the DRY principle function directoryInfo(pp, callback) { const result = { id: pp.uiPath.replace(/([\s\S^/])\/?$/, '$1/'), type: 'folder', attributes: { created: pp.stats.birthtime, modified: pp.stats.mtime, name: pp.filename, path: pp.uiPath.replace(/([\s\S^/])\/?$/, '$1/'), readable: 1, writable: 1, timestamp: '', }, }; callback(result); }// directoryInfo // Getting information is different for a file than it is for a directory, so here // we make sure we are calling the right function. function getinfo(pp, callback) { if (pp.isDirectory) { directoryInfo(pp, (result) => { callback(result); }); } else { fileInfo(pp, (result) => { callback(result); }); }// if }// getinfo // Here we get the information for a folder, which is a content listing // This function exists merely to capture the index and and pp(parsedPath) information in the for loop // otherwise the for loop would finish before our async functions function getIndividualFileInfo(pp, files, loopInfo, callback, $index) { parsePath(paths.posix.join(pp.uiPath, files[$index]), (ipp) => { getinfo(ipp, (result) => { loopInfo.results.push(result); if ($index + 1 >= loopInfo.total) { callback(loopInfo.results); }// if });// getinfo });// parsePath }// getIndividualFileInfo function readfolder(pp, callback) { fs.readdir(pp.osFullPath, (err, files) => { if (err) { console.log('err -> ', err); // eslint-disable-line no-console callback(errors(err)); } else { const loopInfo = { results: [], total: files.length, }; if (loopInfo.total === 0) { callback(loopInfo.results); } for (let i = 0; i < loopInfo.total; i++) { getIndividualFileInfo(pp, files, loopInfo, callback, i); }// for }// if });// fs.readdir }// getinfo // function to delete a file/folder function deleteItem(pp, callback) { if (pp.isDirectory === true) { fs.rmdir(pp.osFullPath, (err) => { if (err) { callback(errors(err)); } else { directoryInfo(pp, callback); }// if });// fs.rmdir } else { fs.unlink(pp.osFullPath, (err) => { if (err) { callback(errors(err)); } else { fileInfo(pp, callback); }// if });// fs.unlink }// if }// deleteItem // function to add a new folder function addfolder(pp, name, callback) { fs.mkdir(paths.join(pp.osFullPath, name), (err) => { if (err) { callback(errors(err)); } else { const result = { id: `${pp.relativePath}${name}/`, type: 'folder', attributes: { name, created: pp.stats.birthtime, modified: pp.stats.mtime, path: `${pp.relativePath}${name}/`, readable: 1, writable: 1, timestamp: '', }, }; callback(result); }// if });// fs.mkdir }// addfolder // function to save uploaded files to their proper locations function renameIndividualFile(loopInfo, files, pp, callback, $index) { if (loopInfo.error === false) { // const oldfilename = paths.join(__appRoot, files[$index].path); const oldfilename = paths.resolve(files[$index].path); // new files comes with a directory, replaced files with a filename. I think there is a better way to handle this // but this works as a starting point const newfilename = paths.join( __appRoot, pp.isDirectory ? pp.relativePath : '', pp.isDirectory ? files[$index].originalname : pp.filename ); // not sure if this is the best way to handle this or not fs.rename(oldfilename, newfilename, (err) => { if (err) { loopInfo.error = true; console.log('savefiles error -> ', err); // eslint-disable-line no-console callback(errors(err)); return; } const name = paths.parse(newfilename).base; const result = { id: `${pp.relativePath}${name}`, type: 'file', attributes: { name, created: pp.stats.birthtime, modified: pp.stats.mtime, path: `${pp.relativePath}${name}`, readable: 1, writable: 1, timestamp: '', }, }; loopInfo.results.push(result); if ($index + 1 >= loopInfo.total) { callback(loopInfo.results); } });// fs.rename }// if not loop error }// renameIndividualFile function savefiles(pp, files, callback) { const loopInfo = { results: [], total: files.length, error: false, }; for (let i = 0; i < loopInfo.total; i++) { renameIndividualFile(loopInfo, files, pp, callback, i); }// for }// savefiles // function to rename files function rename(old, newish, callback) { fs.rename(old.osFullPath, newish.osFullPath, (err) => { if (err) { callback(errors(err)); } else { const name = paths.parse(newish.osFullPath).base; const result = { id: `${newish.relativePath}`, type: 'file', attributes: { name, created: '', modified: '', path: `${newish.relativePath}`, readable: 1, writable: 1, timestamp: '', }, }; callback(result); }// if }); // fs.rename }// rename // function to copy files function copy(source, target, callback) { fs.readFile(source.osFullPath, (err, file) => { if (err) { callback(errors(err)); return; } fs.writeFile(target.osFullPath, file, (error) => { if (err) { callback(errors(error)); return; } const name = paths.parse(target.osFullPath).base; const result = { id: `${target.relativePath}`, type: 'file', attributes: { name, created: '', modified: '', path: `${target.relativePath}`, readable: 1, writable: 1, timestamp: '', }, }; callback(result); }); }); }// copy // RichFilemanager expects a pretified string and not a json object, so this will do that // This results in numbers getting recieved as 0 instead of '0' function respond(res, obj) { res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(obj)); }// respond // finally, our main route handling that calls the above functions :) router.get('/', (req, res) => { // eslint-disable-line complexity const mode = req.query.mode; const path = req.query.path; switch (mode.trim()) { case 'getinfo': parsePath(path, (pp) => { getinfo(pp, (result) => { respond(res, {data: result}); });// getinfo });// parsePath break; case 'readFolder': parsePath(path, (pp) => { readfolder(pp, (result) => { respond(res, {data: result}); });// readfolder });// parsePath break; case 'getimage': parsePath(path, (pp) => { res.sendFile(paths.resolve(pp.osFullPath)); });// parsePath break; case 'readfile': parsePath(path, (pp) => { res.sendFile(paths.resolve(pp.osFullPath)); });// parsePath break; case 'download': parsePath(path, (pp) => { res.setHeader('content-type', 'text/html; charset=UTF-8'); res.setHeader('content-description', 'File Transfer'); res.setHeader('content-disposition', 'attachment; filename="' + pp.filename + '"'); res.sendFile(paths.resolve(pp.osFullPath)); });// parsePath break; case 'addfolder': parsePath(path, (pp) => { addfolder(pp, req.query.name, (result) => { respond(res, {data: result}); });// addfolder });// parsePath break; case 'delete': parsePath(path, (pp) => { deleteItem(pp, (result) => { respond(res, {data: result}); });// parsePath });// parsePath break; case 'rename': parsePath(req.query.old, (opp) => { const newPath = paths.posix.parse(opp.uiPath).dir; const newish = paths.posix.join(newPath, req.query.new); parseNewPath(newish, (npp) => { rename(opp, npp, (result) => { respond(res, {data: result}); });// rename });// parseNewPath });// parsePath break; case 'move': parsePath(req.query.old, (opp) => { parseNewPath(paths.posix.join('/', req.query.new, opp.filename), (npp) => { rename(opp, npp, (result) => { respond(res, {data: result}); });// rename });// parseNewPath });// parsePath break; case 'copy': parsePath(req.query.source, (opp) => { parseNewPath(paths.posix.join('/', req.query.target, opp.filename), (npp) => { copy(opp, npp, (result) => { respond(res, {data: result}); });// rename });// parseNewPath });// parsePath break; default: // eslint-disable-next-line no-console console.log('no matching GET route found with mode: \'', mode.trim(), '\' query -> ', req.query); respond(res, {Code: 0}); }// switch });// get router.post('/', upload.array('files', config.upload.maxNumberOfFiles), (req, res) => { const mode = req.body.mode; const path = req.body.path; switch (mode.trim()) { case 'upload': parsePath(req.body.path, (pp) => { savefiles(pp, req.files, (result) => { respond(res, {data: result}); });// savefiles });// parsePath break; case 'savefile': parsePath(path, (pp) => { getinfo(pp, (result) => { fs.writeFile(paths.resolve(pp.osFullPath), req.body.content, (error) => { if (error) { res.status(500).send(error); } fs.readFile(paths.resolve(pp.osFullPath), (err, f) => { if (err) { res.status(500).send(err); } result.attributes.content = f.toString(); respond(res, {data: result}); }); }); });// getinfo });// parsePath break; default: // eslint-disable-next-line no-console console.log("no matching POST route found with mode: '", mode.trim(), '\' query -> ', req.query); respond(res, {Code: 0}); }// switch }); // post return router; };// module.exports