diff --git a/app/controllers/install.js b/app/controllers/install.js index ae958c25..7cd32fe2 100644 --- a/app/controllers/install.js +++ b/app/controllers/install.js @@ -7,6 +7,9 @@ const cache = require('../utils/cache') module.exports = { + /** + * Install index handler + */ async home(ctx , next){ if(config.installed() ){ ctx.redirect('/') @@ -16,6 +19,9 @@ module.exports = { } } , + /** + * Save config handler + */ async save(ctx){ let { token , name , path , vendor , title = 'ShareList'} = ctx.request.body let cfg = {token , title} diff --git a/app/controllers/manage.js b/app/controllers/manage.js index 1868d8bc..c484bf61 100644 --- a/app/controllers/manage.js +++ b/app/controllers/manage.js @@ -5,6 +5,14 @@ const cache = require('../utils/cache') const { getVendors , reload } = require('../services/plugin') const service = require('../services/sharelist') +/** + * Hanlders hub + * + * @param {string} [a] action + * @param {object} [body] formdata + * @param {object} [ctx] ctx + * @return {object} + */ const handlers = async (a, body , ctx) => { let result = { status: 0, message: 'Success', data: '', a } @@ -144,6 +152,9 @@ const handlers = async (a, body , ctx) => { module.exports = { + /** + * Manage page index handler + */ async home(ctx, next) { let token = ctx.request.body.token @@ -162,6 +173,9 @@ module.exports = { }, + /** + * API router handler + */ async api(ctx) { let body = ctx.request.body @@ -210,6 +224,9 @@ module.exports = { }, + /** + * Shell page handler + */ async shell(ctx){ let access = !!ctx.session.admin if(access){ @@ -219,6 +236,12 @@ module.exports = { } }, + /** + * Shell exection + * + * @param {object} [ctx] + * @return {void} + */ async shell_exec(ctx){ let body = ctx.request.body let { command , path = '/' } = body diff --git a/app/controllers/sharelist.js b/app/controllers/sharelist.js index 3f108457..733fc59f 100644 --- a/app/controllers/sharelist.js +++ b/app/controllers/sharelist.js @@ -4,8 +4,13 @@ const qs = require('querystring') const { sendRedirect } = require('../utils/sendfile') const { parsePath , pathNormalize , enablePreview, enableRange , isRelativePath , markdownParse , md5 } = require('../utils/base') -const requireAuth = (data) => !!(data.children && data.children.find(i=>(i.name == '.passwd'))) - +/** + * Check path + * + * @param {string} [path] current path + * @param {array} [paths] allow proxy paths + * @return [boolean] + */ const isProxyPath = (path , paths) => { return ( path == '' || path == '/' || @@ -14,6 +19,13 @@ const isProxyPath = (path , paths) => { ) ? true : false } +/** + * Check download condition + * + * @param {object} [ctx] + * @param {object} [data] folder/file data + * @return [boolean] + */ const enableDownload = (ctx, data) => { if(ctx.runtime.isAdmin) return true let result = true @@ -29,6 +41,12 @@ const enableDownload = (ctx, data) => { return result } +/** + * Output handler + * + * @param {object} [ctx] + * @param {object} [data] folder/file data + */ const output = async (ctx , data)=>{ const download = enableDownload(ctx, data) @@ -131,6 +149,9 @@ const output = async (ctx , data)=>{ } module.exports = { + /** + * Index handler + */ async index(ctx){ let downloadLinkAge = config.getConfig('max_age_download') let cursign = md5(config.getConfig('max_age_download_sign') + Math.floor(Date.now() / downloadLinkAge)) @@ -261,6 +282,9 @@ module.exports = { }, + /** + * API handler + */ async api(ctx){ let ignoreexts = (config.getConfig('ignore_file_extensions') || '').split(',') let ignorefiles = (config.getConfig('ignore_files') || '').split(',') diff --git a/app/controllers/webdav.js b/app/controllers/webdav.js index d13a5c27..9d92dbdb 100644 --- a/app/controllers/webdav.js +++ b/app/controllers/webdav.js @@ -9,6 +9,9 @@ var virtualFile = { } +/** + * Webdav props default options + */ const default_options = { ns:{ name:'D', @@ -24,6 +27,12 @@ const default_options = { } } +/** + * Conv date to GMT + * + * @param {string} [d] + * @return {mixed} + */ const dateFormat = (d) => { let nd = new Date(d) if (nd instanceof Date && !isNaN(nd)) { @@ -33,6 +42,17 @@ const dateFormat = (d) => { } } +/** + * Create webdav xml response + * + * @param {object} [data] + * @param {object} [options] + * @param {object} [optiosn.props] + * @param {object} [optiosn.ns] + * @param {string} [optiosn.ns.name] + * @param {string} [optiosn.ns.value] + * @return {string} XML string + */ const propsCreate = (data, options) => { let out = '' let { props, ns: { name, value } } = options @@ -71,6 +91,12 @@ const propsCreate = (data, options) => { return out } +/** + * Parse prop from webdab request + * + * @param {object} [data] + * @return {object} + */ const propfindParse = (data, ns) => { if(!data){ return default_options @@ -98,6 +124,12 @@ const propfindParse = (data, ns) => { } } +/** + * Parse props from webdav request + * + * @param {object} [data] + * @return {object|boolean} + */ const nsParse = (data) => { if(!data) return false @@ -116,8 +148,16 @@ const nsParse = (data) => { return false } - -// http://www.domain.example.com/public/ 0 James Smith Infinite opaquelocktoken:f81de2ad-7f3d-a1b3-4f3c-00a0c91a9d76 HTTP/1.1 200 OK +/** + * Create webdav responese xml by data and props options + * + * @param {object} [data] file data + * @param {object} [options] + * @param {object} [options.props] Available props + * @param {object} [options.path] Current folder path + * @param {object} [options.ns] + * @return {string} XML string + */ const respCreate = (data, options) => { let { props, path, ns: { name, value } } = options @@ -136,44 +176,15 @@ const respCreate = (data, options) => { body += `` body = body.replace(/^\s+/g,'').replace(/[\r\n]/g,'') - // console.log(body) - /*return ` - - - /%E6%BC%94%E7%A4%BA%E7%9B%AE%E5%BD%95/example/filesystem_windows_disk_c/Users/abcdef - - HTTP/1.1 200 OK - - Mon, 25 Feb 2019 12:20:01 GMT - 100 - Mon, 25 Feb 2019 12:20:01 GMT - - - - 你好 - - - - - /%E6%BC%94%E7%A4%BA%E7%9B%AE%E5%BD%95/example/filesystem_windows_disk_c/Users/abcdef2 - - HTTP/1.1 200 OK - - Mon, 25 Feb 2019 12:20:01 GMT - 100 - Mon, 25 Feb 2019 12:20:01 GMT - - - - 你好2 - - - -`*/ return body } class Request { + /** + * Initialize a new Request for WebDAV + * + * @param {Object} ctx + */ constructor(ctx) { this.ctx = ctx this.davPoweredBy = null @@ -181,6 +192,11 @@ class Request { this.allows = ['GET', 'PUT', 'HEAD', 'OPTIONS', 'PROPFIND'] } + /** + * Execute handler + * + * @api private + */ async exec(){ let { ctx } = this @@ -205,16 +221,36 @@ class Request { return false } } + + /** + * Set header + * + * @param {string} [k] key + * @param {string} [v] value + * @return void + */ setHeader(k, v) { this.ctx.set(k, v) } + /** + * Set body + * + * @param {mixed} [body] + * @return void + */ setBody(body) { this.ctx.type = 'text/xml; charset="utf-8"' this.setHeader('Content-Length', body.length); this.ctx.body = body } + /** + * Set body status + * + * @param {string|boolean} [status] + * @return void + */ setStatus(status) { if (status === true) { status = "200 OK" @@ -225,6 +261,12 @@ class Request { this.setHeader('X-WebDAV-Status', status) } + /** + * OPTIONS method + * + * @param void + * @return void + */ async http_options() { const allows = this.allows @@ -308,35 +350,6 @@ class Request { async http_get() { await api(this.ctx) } - /* - //create - async http_put() { - let ret = await api(this.ctx) - this.setStatus("200 Success") - } - - // put时 webdav 将lock文件 此方法没有实现 - async http_lock(){ - if( !virtualFile[this.ctx.path] ){ - virtualFile[this.ctx.path] = {} - } - - virtualFile[this.ctx.path]['locked'] = true - this.setStatus("200 Success") - this.setBody(` ${this.ctx.path} 0 ShareList Infinite opaquelocktoken:f81de2ad-7f3d-a1b3-4f3c-00a0c91a9d76 HTTP/1.1 200 OK `) - - } - - async http_unlock(){ - if( !virtualFile[this.ctx.path] ){ - virtualFile[this.ctx.path] = {} - } - virtualFile[this.ctx.path]['locked'] = false - - this.setStatus("200 Success") - } - */ - /* http_head() {} diff --git a/app/plugins/drive.fs.js b/app/plugins/drive.fs.js index 33e10fd1..95798aa6 100644 --- a/app/plugins/drive.fs.js +++ b/app/plugins/drive.fs.js @@ -1,113 +1,138 @@ -/* - * 提供对本地文件系统的支持 - * file:linux风格路径 +/** + * Mount file system */ +const path = require('path') +const fs = require('fs') +const os = require('os') -const name = 'FileSystem' +const isWinOS = os.platform() == 'win32' -const version = '1.0' +/** + * Convert posix style path to windows style + * + * @param {string} [p] + * @return {string} + */ +const winStyle = (p) => p.replace(/^\/([^\/]+?)/, '$1:\\').replace(/\//g, '\\').replace(/(? p.split('\\').join('/').replace(/^([a-z])\:/i,'/$1') -const defaultProtocol = 'fs' +/** + * normalize path(posix style) and replace current path + * + * @param {string} [p] + * @return {string} + */ +const normalize = (p) => path.posix.normalize(p.replace(/^\.\//, slpath(process.cwd()) + '/')) -const path = require('path') -const fs = require('fs') -const os = require('os') +const slpath = (p) => (isWinOS ? posixStyle(p) : p) + +const realpath = (p) => (isWinOS ? winStyle(p) : p) -const isWinOS = os.platform() == 'win32' -const l2w = (p) => p.replace(/^\/([^\/]+?)/,'$1:\\').replace(/\//g,'\\').replace(/(? (isWinOS ? l2w(p) : p) + this.version = '1.0' + this.protocol = 'fs' + } + + mkdir(p) { + if (fs.existsSync(p) == false) { + this.mkdir(path.dirname(p)); + fs.mkdirSync(p); + } + } -module.exports = ({datetime , extname , pathNormalize}) => { + path(id) { + let { datetime, extname } = this.helper - const normalize = (p) => pathNormalize(p).replace(/^\.\//,process.cwd()+'/') + let { protocol } = this - const folder = async(id , {_path=[]} = {}) => { let dir = normalize(id) - if( _path.length > 0) { - dir = _path.length.join('/') - } - let resp = { id : dir , type:'folder', protocol:defaultProtocol} let realdir = realpath(dir) - if( fs.existsSync(realdir) ){ - let children = [] - fs.readdirSync(realdir).forEach(function(filename){ - let path = normalize(dir) + '/' + filename + let stat = fs.statSync(realdir) + + if (stat.isDirectory()) { + let children = [] + fs.readdirSync(realdir).forEach((filename) => { + let path = normalize(dir + '/' + filename) let stat - try{ + try { stat = fs.statSync(realpath(path)) - }catch(e){} + } catch (e) {} let obj = { - id:path , - name:filename, - protocol:defaultProtocol, - type:'other' + id: path, + name: filename, + protocol: this.protocol, + type: 'other' } - if(stat){ + if (stat) { obj.created_at = datetime(stat.ctime) obj.updated_at = datetime(stat.mtime) - if(stat.isDirectory()){ + if (stat.isDirectory()) { obj.type = 'folder' - } - else if(stat.isFile()){ + } else if (stat.isFile()) { obj.ext = extname(filename) obj.size = stat.size } } children.push(obj) }) - resp.children = children - return resp - }else{ + + return { id: dir, type: 'folder', protocol: this.protocol , children } + + } else if (stat.isFile()) { + return { + id, + name: path.basename(id), + protocol: this.protocol, + ext: extname(id), + url: realpath(id), + size: stat.size, + outputType: 'file', + proxy: true + } + } else { return false } - } - const file = async(id)=>{ - let realdir = realpath(normalize(id)) - let stat = {} - try{ - stat = fs.statSync(realpath(realdir)) - }catch(e){} - - return { - id, - name: path.basename(id), - ext: extname(id), - url: realpath(id), - size:stat.size, - protocol:defaultProtocol, - outputType:'file', - proxy:true - } } - const createReadStream = ({id , options = {}} = {}) => { - return fs.createReadStream(realpath(id) , {...options,highWaterMark:64*1024}) + folder(id) { + return this.path(id) } - const mkdir = (p) => { - if (fs.existsSync(p) == false) { - mkdir(path.dirname(p)); - fs.mkdirSync(p); - } - }; + file(id) { + return this.path(id) + } + async createReadStream({ id, options = {} } = {}) { + return fs.createReadStream(realpath(id), { ...options, highWaterMark: 64 * 1024 }) + } - const createWriteStream = async ({ id , options = {} , target = ''} = {}) => { - let fullpath = pathNormalize(id +'/' + target) - let parent = (fullpath.split('/').slice(0,-1).join('/') + '/').replace(/\/+$/g,'/') - mkdir(parent) - return fs.createWriteStream(realpath(fullpath) , options) + async createWriteStream({ id, options = {}, target = '' } = {}) { + let fullpath = path.join(id , target) + let parent = (fullpath.split('/').slice(0, -1).join('/') + '/').replace(/\/+$/g, '/') + this.mkdir(parent) + return fs.createWriteStream(realpath(fullpath), options) } +} + - return { name , label:'本地文件',version , drive:{ protocols , folder , file , cache:false , createReadStream , createWriteStream } } -} \ No newline at end of file +module.exports = FileSystem \ No newline at end of file diff --git a/app/services/plugin.js b/app/services/plugin.js index 8d36f30a..c6c46aa7 100644 --- a/app/services/plugin.js +++ b/app/services/plugin.js @@ -45,6 +45,8 @@ const whenReady = (handler) => { } } +const isClass = fn => typeof fn == 'function' && /^\s*class/.test(fn.toString()); + var ready = false var resourcesCount = 0 @@ -300,56 +302,75 @@ const load = (options) => { const type = name.split('.')[0] const id = 'plugin_' + pluginName.replace(/\./g,'_') const helpers = getHelpers(id) - const resource = require(filepath).call(helpers,helpers) - + let ins = require(filepath) console.log('Load Plugins: ',pluginName) - resources[id] = resource - - if( resource.auth ){ - for(let key in resource.auth){ - authMap.set(key , id) + let resource + if( isClass(ins) ){ + let driver = new ins() + let { protocol , mountable , createReadStream , createWriteStream } = driver + driver.helper = helpers + resources[id] = { + label:driver.label, + mountable,protocol, + drive:driver, + name:driver.name } - } + driveMap.set(protocol , id) - if( resource.drive ){ - let protocols = [].concat(resource.drive.protocols || []) - let mountable = resource.drive.mountable !== false - protocols.forEach( protocol => { - driveMap.set(protocol,id) - if(mountable) driveMountableMap.set(protocol , id) - }) + if(mountable) driveMountableMap.set(protocol , id) + if(createReadStream) readstreamMap.set(protocol , driver.createReadStream) + }else{ + resource = ins.call(helpers,helpers) - if( resource.drive.createReadStream ){ - protocols.forEach( protocol => { - readstreamMap.set(protocol , resource.drive.createReadStream) - }) + resources[id] = resource + + if( resource.auth ){ + for(let key in resource.auth){ + authMap.set(key , id) + } } - if( resource.drive.createWriteStream ){ + if( resource.drive ){ + let protocols = [].concat(resource.drive.protocols || []) + let mountable = resource.drive.mountable !== false protocols.forEach( protocol => { - writestreamMap.set(protocol , resource.drive.createWriteStream) + driveMap.set(protocol,id) + if(mountable) driveMountableMap.set(protocol , id) }) + + if( resource.drive.createReadStream ){ + protocols.forEach( protocol => { + readstreamMap.set(protocol , resource.drive.createReadStream) + }) + } + + if( resource.drive.createWriteStream ){ + protocols.forEach( protocol => { + writestreamMap.set(protocol , resource.drive.createWriteStream) + }) + } } - } - - if(resource.format){ - for(let key in resource.format){ - formatMap.set(key , id) + + if(resource.format){ + for(let key in resource.format){ + formatMap.set(key , id) + } } - } - if(resource.preview){ - for(let key in resource.preview){ - previewMap.set(key , id) + if(resource.preview){ + for(let key in resource.preview){ + previewMap.set(key , id) + } } - } - if(resource.cmd){ - for(let key in resource.cmd){ - cmdMap.set(key , id) + if(resource.cmd){ + for(let key in resource.cmd){ + cmdMap.set(key , id) + } } } + } } } @@ -365,7 +386,7 @@ const reload = () => { load(loadOptions) } /** - * 根据扩展名获取可处理的驱动 + * 根据协议获取可处理的驱动 */ const getDrive = (protocol) => { if( driveMap.has(protocol)){ @@ -512,7 +533,7 @@ const getVendors = () => [...new Set(driveMountableMap.values())].map(id => { return { name : resources[id].name, label: resources[id].label || resources[id].name, - protocol : resources[id].drive.protocols[0] + protocol : resources[id].protocol || resources[id].drive.protocols[0] } }) @@ -548,7 +569,11 @@ const createReadStream = async (options) => { const createWriteStream = async (options) => { let { id , protocol } = options - if( writestreamMap.has(protocol) ){ + + let drive = getDrive(protocol) + if(drive.createWriteStream){ + return await drive.createWriteStream(options) + }else if( writestreamMap.has(protocol) ){ return await writestreamMap.get(protocol)(options) } } diff --git a/app/services/sharelist.js b/app/services/sharelist.js index 0b648523..35e418ae 100644 --- a/app/services/sharelist.js +++ b/app/services/sharelist.js @@ -14,6 +14,14 @@ class ShareList { this.passwdPaths = new Set() } + /** + * Path diff + * + * @param {string} [a] path_a + * @param {string} [b] path_b + * @return {boolean} + * @api private + */ async diff(a,b){ let ret = [] b.forEach((v, i) => { @@ -24,10 +32,24 @@ class ShareList { return ret } + /** + * Check folder contain passwd file + * + * @param {object} [data] + * @return {boolean} + * @api private + */ hasPasswdFile(data){ return !!(data.children && data.children.find(i=>(i.name == '.passwd'))) } + /** + * Search the first serect path + * + * @param {object} [req] + * @return {mixed} + * @api private + */ searchPasswdPath(req){ for(let i = 1 ; i <= req.paths.length ; i++){ let path = '/'+req.paths.slice(0,i).join('/') @@ -39,12 +61,19 @@ class ShareList { return null } + /** + * Find the folder(file) data by path + * + * @param {object} [req] + * @return {object} + * @api public + */ async path(req) { if( req.body && req.body.act == 'auth' ){ let ra = await this.auth(req) return { type:'auth_response' , result: ra } } - //上传 + // upload request else if(req.upload){ if(!req.upload.enable){ return { @@ -113,7 +142,13 @@ class ShareList { } } - + /** + * Verify auth by path + * + * @param {object} [req] + * @return {boolean} + * @api private + */ async auth(req) { let data = await command('ls' , req.paths.join('/')) let hit = data.children.find(i => i.name == '.passwd') @@ -129,31 +164,64 @@ class ShareList { } return false } - /* - * 获取文件预览页面 + + /** + * Get previewable data + * + * @param {object} + * @return {object} + * @api public */ async preview(data){ return await getPreview(data) } - /* - * 根据文件ID和协议获取可读流 + + /** + * Get readable stream by file id + * + * @param {object} [ctx] * required + * @param {string} [id] * required file id + * @param {type} [type] * required stream type + * @return {stream} + * @api public */ async stream(ctx , id , type , protocol , data){ return await getStream(ctx , id , type , protocol , data) } + /** + * Get file content by file id + * + * @param {string} [id] * required file id + * @param {type} [protocol] * required stream type + * @param {object} [data] file data + * @return {stream} + * @api public + */ async source(id , protocol , data){ return await getSource(id , protocol , data) } + /** + * Execute core command + * + * @param {string} [v] command and args + * @param {string} [path] command execution path + * @return {mixed} + * @api public + */ async exec(v , path){ // TODO yargs let [cmd , ...options] = v.split(/\s+/) return await command(cmd , path , options , true) } - /* - * 检测文件是否支持预览 + /** + * Check the file support preview + * + * @param {object} [data] file data + * @return {boolean} + * @api public */ async isPreviewable(data){ return await isPreviewable(data) @@ -161,4 +229,4 @@ class ShareList { } -module.exports = new ShareList() +module.exports = new ShareList() \ No newline at end of file