548 lines
15 KiB
JavaScript
548 lines
15 KiB
JavaScript
/*eslint strict:["error", "global"]*/
|
|
'use strict';
|
|
|
|
/*!
|
|
* OS.js - JavaScript Cloud/Web Desktop Platform
|
|
*
|
|
* Copyright (c) 2011-2017, Anders Evenrud <andersevenrud@gmail.com>
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
* list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*
|
|
* @author Anders Evenrud <andersevenrud@gmail.com>
|
|
* @licence Simplified BSD License
|
|
*/
|
|
const _fs = require('node-fs-extra');
|
|
const _nfs = require('fs');
|
|
const _path = require('path');
|
|
const _chokidar = require('chokidar');
|
|
|
|
const _utils = require('./../../core/utils.js');
|
|
const _vfs = require('./../../core/vfs.js');
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// HELPERS
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
/*
|
|
* Create a read stream
|
|
*/
|
|
function createReadStream(http, path) {
|
|
const resolved = _vfs.parseVirtualPath(path, http);
|
|
return new Promise(function(resolve, reject) {
|
|
/*eslint new-cap: "off"*/
|
|
resolve(_fs.createReadStream(resolved.real, {
|
|
bufferSize: 64 * 1024
|
|
}));
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Create a write stream
|
|
*/
|
|
function createWriteStream(http, path) {
|
|
const resolved = _vfs.parseVirtualPath(path, http);
|
|
return new Promise(function(resolve, reject) {
|
|
/*eslint new-cap: "off"*/
|
|
resolve(_fs.createWriteStream(resolved.real));
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Creates watch
|
|
*/
|
|
function createWatch(name, mount, callback) {
|
|
const path = mount.destination;
|
|
const matches = path.match(/%(\w+)%/g);
|
|
const dir = _vfs.resolvePathArguments(path, {
|
|
username: '**',
|
|
uid: '**'
|
|
});
|
|
|
|
const re = new RegExp('^' + dir.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&').replace('\\*\\*', '([^/]+)'));
|
|
|
|
const watchMap = {
|
|
add: 'write',
|
|
change: 'write'
|
|
};
|
|
|
|
function _onChange(evname, wpath) {
|
|
var args = {};
|
|
(wpath.match(re) || []).forEach(function(a, idx) {
|
|
if ( matches[idx] ) {
|
|
args[matches[idx]] = a;
|
|
}
|
|
});
|
|
|
|
const relative = wpath.replace(re, '');
|
|
const virtual = name + '://' + relative.replace(/^\/?/, '/');
|
|
|
|
callback(name, mount, {
|
|
event: watchMap[evname] || evname,
|
|
real: wpath,
|
|
path: virtual,
|
|
args: args
|
|
});
|
|
}
|
|
|
|
_chokidar.watch(dir, {ignoreInitial: true, persistent: true}).on('all', function(evname, wpath) {
|
|
if ( ['change', 'error'].indexOf(evname) === -1 ) {
|
|
_onChange(evname, wpath);
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Reads EXIF data
|
|
*/
|
|
function readExif(path, mime) {
|
|
mime = mime || '';
|
|
|
|
var _read = function defaultRead(resolve, reject) {
|
|
resolve(null);
|
|
};
|
|
|
|
if ( mime.match(/^image/) ) {
|
|
try {
|
|
_read = function exifRead(resolve, reject) {
|
|
/*eslint no-new: "off"*/
|
|
new require('exif').ExifImage({image: path}, function(err, result) {
|
|
if ( err ) {
|
|
reject(err);
|
|
} else {
|
|
resolve(JSON.stringify(result, null, 4));
|
|
}
|
|
});
|
|
};
|
|
} catch ( e ) {}
|
|
}
|
|
|
|
return new Promise(_read);
|
|
}
|
|
|
|
/*
|
|
* Creates file information in a format OS.js understands
|
|
*/
|
|
function createFileIter(query, real, iter, stat) {
|
|
if ( !stat ) {
|
|
try {
|
|
stat = _fs.statSync(real);
|
|
} catch ( e ) {
|
|
stat = {
|
|
isFile: function() {
|
|
return true
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
var mime = '';
|
|
const filename = iter ? iter : _path.basename(query);
|
|
const type = stat.isFile() ? 'file' : 'dir';
|
|
|
|
if ( type === 'file' ) {
|
|
mime = _vfs.getMime(filename);
|
|
}
|
|
|
|
const perm = _utils.permissionToString(stat.mode);
|
|
const filepath = !iter ? query : (function() {
|
|
const spl = query.split('://');
|
|
const proto = spl[0];
|
|
const ppath = (spl[1] || '').replace(/\/?$/, '/');
|
|
return proto + '://' + _path.join(ppath, iter);
|
|
})();
|
|
|
|
return {
|
|
filename: filename,
|
|
path: filepath,
|
|
size: stat.size || 0,
|
|
mime: mime,
|
|
type: type,
|
|
permissions: perm,
|
|
ctime: stat.ctime || null,
|
|
mtime: stat.mtime || null
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Check if given file exists or not
|
|
*/
|
|
function existsWrapper(checkFound, real, resolve, reject) {
|
|
_fs.exists(real, function(exists) {
|
|
if ( checkFound ) {
|
|
if ( exists ) {
|
|
reject('File or directory already exist.');
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
} else {
|
|
if ( exists ) {
|
|
resolve(true);
|
|
} else {
|
|
reject('No such file or directory');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Reads a directory and returns in a format OS.js understands
|
|
*/
|
|
function readDir(query, real, filter) {
|
|
filter = filter || function(iter) {
|
|
return ['.', '..'].indexOf(iter) === -1;
|
|
};
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
_fs.readdir(real, function(err, list) {
|
|
if ( err ) {
|
|
reject(err);
|
|
} else {
|
|
resolve(list.filter(filter).map(function(iter) {
|
|
return createFileIter(query, _path.join(real, iter), iter);
|
|
}));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// VFS METHODS
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
const VFS = {
|
|
|
|
exists: function(http, args, resolve, reject) {
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
_fs.exists(resolved.real, function(exists) {
|
|
resolve(exists);
|
|
});
|
|
},
|
|
|
|
read: function(http, args, resolve, reject) {
|
|
/*eslint new-cap: "off"*/
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
const options = args.options || {};
|
|
|
|
if ( options.raw !== false ) {
|
|
if ( options.stream !== false ) {
|
|
resolve(resolved.real);
|
|
} else {
|
|
_fs.readFile(resolved.real, function(e, r) {
|
|
if ( e ) {
|
|
reject(e);
|
|
} else {
|
|
resolve(r);
|
|
}
|
|
});
|
|
}
|
|
} else {
|
|
const mime = _vfs.getMime(args.path);
|
|
_fs.readFile(resolved.real, function(e, data) {
|
|
if ( e ) {
|
|
reject(e);
|
|
} else {
|
|
const enc = 'data:' + mime + ';base64,' + (new Buffer(data).toString('base64'));
|
|
resolve(enc.toString());
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
upload: function(http, args, resolve, reject) {
|
|
function _proceed(source, dest) {
|
|
const streamIn = _fs.createReadStream(source);
|
|
const streamOut = _fs.createWriteStream(dest, {flags: 'w'});
|
|
|
|
function streamDone() {
|
|
streamIn.destroy();
|
|
streamOut.destroy();
|
|
|
|
_fs.unlink(source, function() {
|
|
http.respond.raw(String(1), 200, {
|
|
'Content-Type': 'text/plain'
|
|
});
|
|
});
|
|
}
|
|
|
|
function streamError(err) {
|
|
streamIn.destroy();
|
|
streamOut.destroy();
|
|
streamOut.removeListener('close', streamDone);
|
|
reject(err);
|
|
}
|
|
|
|
streamIn.on('error', streamError);
|
|
streamOut.on('error', streamError);
|
|
streamOut.once('close', streamDone);
|
|
streamIn.pipe(streamOut);
|
|
}
|
|
|
|
const source = http.files.upload.path;
|
|
const dresolved = _vfs.parseVirtualPath(http.data.path, http);
|
|
const dest = _path.join(dresolved.real, http.files.upload.name);
|
|
|
|
existsWrapper(false, source, function() {
|
|
if ( String(http.data.overwrite) === 'true' ) {
|
|
_proceed(source, dest);
|
|
} else {
|
|
existsWrapper(true, dest, function() {
|
|
_proceed(source, dest);
|
|
}, reject);
|
|
}
|
|
}, reject);
|
|
},
|
|
|
|
write: function(http, args, resolve, reject) {
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
const options = args.options || {};
|
|
var data = args.data || ''; // FIXME
|
|
|
|
function writeFile(d, e) {
|
|
_fs.writeFile(resolved.real, d, e || 'utf8', function(error) {
|
|
if ( error ) {
|
|
reject('Error writing file: ' + error);
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
}
|
|
|
|
/*existsWrapper(true, resolved.real, function() {
|
|
}, reject);*/
|
|
if ( options.raw ) {
|
|
writeFile(data, options.rawtype || 'binary');
|
|
} else {
|
|
data = unescape(data.substring(data.indexOf(',') + 1));
|
|
data = new Buffer(data, 'base64');
|
|
writeFile(data);
|
|
}
|
|
},
|
|
|
|
delete: function(http, args, resolve, reject) {
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
if ( ['', '.', '/'].indexOf() !== -1 ) {
|
|
return reject('Permission denied');
|
|
}
|
|
|
|
existsWrapper(false, resolved.real, function() {
|
|
_fs.remove(resolved.real, function(err) {
|
|
if ( err ) {
|
|
reject('Error deleting: ' + err);
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
}, reject);
|
|
},
|
|
|
|
copy: function(http, args, resolve, reject) {
|
|
const sresolved = _vfs.parseVirtualPath(args.src, http);
|
|
const dresolved = _vfs.parseVirtualPath(args.dest, http);
|
|
|
|
existsWrapper(false, sresolved.real, function() {
|
|
existsWrapper(true, dresolved.real, function() {
|
|
_fs.access(_path.dirname(dresolved.real), _nfs.W_OK, function(err) {
|
|
if ( err ) {
|
|
reject('Cannot write to destination');
|
|
} else {
|
|
_fs.copy(sresolved.real, dresolved.real, function(error, data) {
|
|
if ( error ) {
|
|
reject('Error copying: ' + error);
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}, reject);
|
|
}, reject);
|
|
},
|
|
|
|
move: function(http, args, resolve, reject) {
|
|
const sresolved = _vfs.parseVirtualPath(args.src, http);
|
|
const dresolved = _vfs.parseVirtualPath(args.dest, http);
|
|
|
|
_fs.access(sresolved.real, _nfs.R_OK, function(err) {
|
|
if ( err ) {
|
|
reject('Cannot read source');
|
|
} else {
|
|
_fs.access(_path.dirname(dresolved.real), _nfs.W_OK, function(err) {
|
|
if ( err ) {
|
|
reject('Cannot write to destination');
|
|
} else {
|
|
_fs.rename(sresolved.real, dresolved.real, function(error, data) {
|
|
if ( error ) {
|
|
reject('Error renaming/moving: ' + error);
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
mkdir: function(http, args, resolve, reject) {
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
|
|
existsWrapper(true, resolved.real, function() {
|
|
_fs.mkdir(resolved.real, function(err) {
|
|
if ( err ) {
|
|
reject('Error creating directory: ' + err);
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
}, reject);
|
|
},
|
|
|
|
find: function(http, args, resolve, reject) {
|
|
const qargs = args.args || {};
|
|
const query = (qargs.query || '').toLowerCase();
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
|
|
if ( !qargs.recursive ) {
|
|
return readDir(resolved.path, resolved.real, function(iter) {
|
|
if ( ['.', '..'].indexOf(iter) === -1 ) {
|
|
return iter.toLowerCase().indexOf(query) !== -1;
|
|
}
|
|
return false;
|
|
}).then(resolve).catch(reject);
|
|
}
|
|
|
|
var find;
|
|
try {
|
|
find = require('findit')(resolved.real);
|
|
} catch ( e ) {
|
|
return reject('Failed to load findit node library: ' + e.toString());
|
|
}
|
|
|
|
var list = [];
|
|
|
|
function addIter(file, stat) {
|
|
const filename = _path.basename(file).toLowerCase();
|
|
const fpath = resolved.path + file.substr(resolved.real.length).replace(/^\//, '');
|
|
if ( filename.indexOf(query) !== -1 ) {
|
|
const qpath = resolved.query + fpath.replace(/^\//, '');
|
|
list.push(createFileIter(qpath, resolved.real, null, stat));
|
|
}
|
|
}
|
|
|
|
find.on('path', function() {
|
|
if ( qargs.limit && list.length >= qargs.limit ) {
|
|
find.stop();
|
|
}
|
|
});
|
|
|
|
find.on('directory', addIter);
|
|
find.on('file', addIter);
|
|
|
|
find.on('end', function() {
|
|
resolve(list);
|
|
});
|
|
|
|
find.on('stop', function() {
|
|
resolve(list);
|
|
});
|
|
},
|
|
|
|
fileinfo: function(http, args, resolve, reject) {
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
|
|
existsWrapper(false, resolved.real, function() {
|
|
const info = createFileIter(resolved.query, resolved.real, null);
|
|
const mime = _vfs.getMime(resolved.real);
|
|
|
|
readExif(resolved.real, mime).then(function(result) {
|
|
info.exif = result || 'No EXIF data available';
|
|
resolve(info);
|
|
}).catch(function(error) {
|
|
info.exif = error;
|
|
resolve(info);
|
|
});
|
|
}, reject);
|
|
},
|
|
|
|
scandir: function(http, args, resolve, reject) {
|
|
const resolved = _vfs.parseVirtualPath(args.path, http);
|
|
const opts = args.options || {};
|
|
|
|
readDir(resolved.query, resolved.real).then(function(list) {
|
|
|
|
if ( opts.shortcuts !== false ) {
|
|
const filename = typeof opts.shortcuts === 'string' ? opts.shortcuts.replace(/\/+g/, '') : '.shortcuts.json';
|
|
const path = args.path.replace(/\/?$/, '/' + filename);
|
|
const realMeta = _vfs.parseVirtualPath(path, http);
|
|
|
|
_fs.readFile(realMeta.real, function(err, contents) {
|
|
var additions = [];
|
|
if ( !err ) {
|
|
try {
|
|
additions = JSON.parse(contents.toString());
|
|
if ( !(additions instanceof Array) ) {
|
|
additions = [];
|
|
}
|
|
} catch ( e ) {}
|
|
}
|
|
|
|
resolve(list.concat(additions));
|
|
});
|
|
} else {
|
|
resolve(list);
|
|
}
|
|
|
|
}).catch(reject);
|
|
},
|
|
|
|
freeSpace: function(http, args, resolve, reject) {
|
|
const resolved = _vfs.parseVirtualPath(args.root, http);
|
|
|
|
try {
|
|
require('diskspace').check(resolved.real, function(err, total, free, stat) {
|
|
resolve(free);
|
|
});
|
|
} catch ( e ) {
|
|
reject('Failed to load diskspace node library: ' + e.toString());
|
|
}
|
|
}
|
|
};
|
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
// EXPORTS
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
module.exports.request = function(http, method, args) {
|
|
return new Promise(function(resolve, reject) {
|
|
if ( typeof VFS[method] === 'function' ) {
|
|
VFS[method](http, args, resolve, reject);
|
|
} else {
|
|
reject('No such VFS method: ' + method);
|
|
}
|
|
});
|
|
};
|
|
|
|
module.exports.createReadStream = createReadStream;
|
|
module.exports.createWriteStream = createWriteStream;
|
|
module.exports.createWatch = createWatch;
|
|
module.exports.name = '__default__';
|