UNPKG

node-smb-server

Version:

A Pure JavaScript SMB Server Implementation

785 lines (727 loc) 24.9 kB
/* * Copyright 2015 Adobe Systems Incorporated. All rights reserved. * This file is licensed to you under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. You may obtain a copy * of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ 'use strict'; var util = require('util'); var Path = require('path'); var async = require('async'); var logger = require('winston').loggers.get('spi'); var Tree = require('../../spi/tree'); var utils = require('../../utils'); var consts = require('./common'); var RQLocalFile = require('./localfile'); var RQLocalTree = function (share, localTree, rqTree) { if (!(this instanceof RQLocalTree)) { return new RQLocalTree(share, localTree, rqTree); } this.source = localTree; this.rqTree = rqTree; this.share = share; this.cacheInfoOnly = share.config.cacheInfoOnly ? true : false; Tree.call(this, share.config); }; util.inherits(RQLocalTree, Tree); /** * Retrieves the path to a given file's working information. * @param {string} path The path whose work path should be retrieved. * @return {string} Path to a file's work file. * @private */ function _getWorkingFilePath(path) { var parent = utils.getParentPath(path); var name = utils.getPathName(path); return Path.join(parent, consts.WORK_DIR, name); } /** * Writes information to a work data file. * @param {File} workFile The work file to which to write. * @param {object} cacheInfo Will be converted to a JSON string and written to the file. * @param {function} cb Will be invoked when the operation is complete. * @param {Error} cb.err Will be truthy if there were errors during the operation. * @private */ function _writeWorkData(workFile, cacheInfo, cb) { var filePath = workFile.getPath(); logger.debug('_writeWorkData: entering with filePath=%s', filePath); var writeData = new Buffer(JSON.stringify(cacheInfo)); workFile.setLength(writeData.length, function (lengthErr) { if (lengthErr) { logger.error('unable to set length of work file %s', filePath, lengthErr); cb(lengthErr); } else { workFile.write(writeData, 0, function (writeErr) { workFile.close(function (closeErr) { if (writeErr) { logger.error('unable to write to work file %s', filePath, writeErr); cb(writeErr); } else { if (closeErr) { logger.warn('unexpected error while closing work file %s', filePath, closeErr); } logger.debug('finished writing data to %s', filePath); cb(); } }); }); } }); } /** * Creates a work file for a corresponding file, if needed. * @param {string} name The file for which a work file will be created. * @param {boolean} created The to write to the work file indicating whether or not the file was created locally. * @param {int} remoteModified The remote modified date that will be written to the work file. * @param {function} cb Will be invoked when the operation is complete. * @param {Error} cb.err Will be truthy if there were errors during the operation. * @private */ function _createWorkFile(targetFile, created, isRefreshed, remoteFile, cb) { var name = targetFile.getPath(); var self = this; function _openFile(toOpen) { self.source.open(toOpen, cb); } function _createFile(path) { self.source.createFile(path, function (err, workFile) { if (err) { cb(err); } else { logger.debug('%s work file created', path); _writeWorkData.call(self, workFile, RQLocalFile.getCacheInfo(targetFile, remoteFile, created, isRefreshed), function (err) { if (err) { cb(err); } else { _openFile(path); } }); } }); } if (!self.isTempFileName(name) && !targetFile.isDirectory()) { var filePath = self.getInfoFilePath(name); self.source.exists(filePath, function (err, exists) { if (err) { cb(err); } else if (exists) { logger.debug('%s work file already exists', filePath); if (created) { logger.warn('%s work file already exists and file is being created. re-creating work file', filePath); self.source.delete(filePath, function (err) { if (err) { cb(err); } else { _createFile(filePath); } }); } else { _openFile(filePath); } } else { _createFile(filePath); } }); } else { // temp files and directories don't need work files logger.debug('%s does not need a work file', targetFile.getPath()); cb(); } } /** * Retrieves information about a given path. * @param {string} path The path to analyze. * @param {function} cb Will be invoked when the operation is finished. * @param {Error} cb.err Will be truthy if there were errors. * @param {boolean} cb.exists Will be truthy if the path exists locally. * @param {boolean} cb.isDir Will be truthy if the path exists locally and is a directory. */ RQLocalTree.prototype.getPathInfo = function (path, cb) { var self = this; function _exists(callback) { self.source.exists(path, function (err, exists) { if (err) { cb(err); } else { self.cacheInfoExists(path, function (err, cacheExists) { var checkDir = exists; var doesExist = exists; if (self.isCacheInfoOnly()) { if (self.isTempFileName(path)) { // in the case of temp files, a cache info file will never exist. Always report that temp files exist // so that the rest of the workflow will be run on temp files doesExist = true; } else { doesExist = cacheExists; } } callback(doesExist, checkDir); }); } }); } _exists(function (exists, checkDir) { if (exists && checkDir) { // path exists. open it to determine if it's a directory self.source.open(path, function (err, item) { if (err) { cb(err); } else { cb(null, true, item.isDirectory()); } }); } else { // path is not a directory cb(null, exists, false); } }); }; RQLocalTree.prototype.isCacheInfoOnly = function () { return this.cacheInfoOnly; }; /** * Retrieves a value indicating whether or not a path fits patterns identifying temporary files or directories. * @param {string} name The path to test. * @return {string} TRUE if the path is a temp path, FALSE if not. */ RQLocalTree.prototype.isTempFileName = function (name) { return this.rqTree.isTempFileName(name); }; /** * Determines if cache information exists at the given path. * @param {string} path The path to determine existence. * @param {function} cb Will be invoked when the operation is complete. * @param {Error} cb.err Will be truthy if there were errors during the operation. * @param {boolean} cb.exists Will be TRUE if cache information exists, otherwise FALSE. * */ RQLocalTree.prototype.cacheInfoExists = function (path, cb) { if (!this.isTempFileName(path)) { // small optimization for temp files. only check for info file if it's not a temp file this.source.exists(this.getInfoFilePath(path), cb); } else { cb(null, false); } }; /** * Given a file path, retrieves the path to its cache info file. * @param {string} path The path whose cache info file path should be retrieved. * @return {string} The path to a cache info file. */ RQLocalTree.prototype.getInfoFilePath = function (path) { return _getWorkingFilePath(path) + '.json'; }; /** * Determines if a given path was created locally in the cache. * @param {string} path The path to check. * @param {function} cb Will be invoked when the operation is complete. * @param {Error} cb.err Will be truthy if there were errors during the operation. * @param {boolean} cb.createdLocally Will be TRUE if the file was created locally, otherwise FALSE. */ RQLocalTree.prototype.isCreatedLocally = function (path, cb) { var self = this; self.cacheInfoExists(path, function (err, exists) { if (err) { cb(err); } else if (!exists) { cb(null, false); } else { var infoFile = self.getInfoFilePath(path); self.source.open(infoFile, function (err, localFile) { if (err) { cb(err); } else { RQLocalFile.readCacheInfo(localFile, function (err, cacheInfo) { if (err) { cb(err); } else { cb(null, cacheInfo.created ? true : false); } }); } }); } }); }; /** * If a given path is currently downloading, this method will "block" until the download is finished and then will * invoke its callback. * @param {string} path The path to the file. * @param {function} cb Will be invoked when the download is finished, if applicable. If the file is not downloading * then the callback will be invoked immediately. * @param {string|Error} cb.err Will be truthy if there were errors while waiting. */ RQLocalTree.prototype.waitOnDownload = function (path, cb) { this.share.waitOnDownload(this, path, cb); }; /** * Retrieves a value indicating whether or not a file is downloading. * @param {string} path The path to the file. * @return {boolean} TRUE if the file is currently downloading, false if not. */ RQLocalTree.prototype.isDownloading = function (path) { return this.share.isDownloading(this, path); }; /** * Sets a given file's status to downloading. * @param {string} path Path to the file. * @param {boolean} isDownloading TRUE if the file is downloading, FALSE if it is not. */ RQLocalTree.prototype.setDownloading = function (path, isDownloading) { this.share.setDownloading(this, path, isDownloading); }; /** * Downloads a file from a different tree and stores it in the local tree. * @param {Tree} remote The tree from which the file will be retrieved. * @param {string} name The path of the file to download. * @param {function} cb Will be invoked when the operation is complete. * @param {Error} cb.err Will be truthy if there were errors during the download. * @param {File} cb.File The locally downloaded file. */ RQLocalTree.prototype.download = function (remote, name, cb) { var self = this; function sendResult(err, file) { self.setDownloading(name, false); cb(err, file); } self.setDownloading(name, true); remote.open(self.rqTree.remoteEncodePath(name), function (err, remoteFile) { if (err) { sendResult(err); } else { logger.info('downloading file %s from remote', name); self.share.fetchResource(self.rqTree.remoteEncodePath(name), function (err, localPath) { if (err) { logger.error('error while downloading file %s from remote', name, err); sendResult(err); } else { logger.info('successfully downloaded %s to %s', name, localPath); self.source.open(name, function (err, file) { if (err) { sendResult(err); } else { self.createFromSource(file, remoteFile, false, sendResult); } }); } }); } }); }; /** * Refreshes the cache information about a file by removing its work data file and recreating it. * @param {string} name The path whose cache info should be refreshed. * @param {File} remoteFile The remote file whose information will be used in the cache info. * @param {function} cb Will be invoked when the operation is complete. * @param {Error} cb.err Will be truthy if there were errors during the operation. */ RQLocalTree.prototype.refreshCacheInfo = function(name, remoteFile, cb) { var self = this; var filePath = self.getInfoFilePath(name); function _deleteWork(delCb) { self.cacheInfoExists(name, function (err, exists) { if (err) { cb(err); } else if (exists) { self.source.delete(filePath, function (err) { if (err) { cb(err); } else { delCb(); } }); } else { delCb(); } }); } _deleteWork(function () { self.source.open(name, function (err, localFile) { if (err) { cb(err); } else { _createWorkFile.call(self, localFile, false, true, remoteFile, function (err, workFile) { localFile.close(function (closeErr) { if (closeErr) { logger.error('unable to close local file %s', name, closeErr); } if (err) { cb(err); } else { workFile.close(function (err) { if (err) { logger.error('unable to close work file %s', filePath, err); } cb(); }); } }); }); } }); }); }; /** * Creates a new File instance for the tree based on a source File instance that has already been created. * @param {File} source The file from which the new instance will be created. * @param {File} remote File whose information will be used as remote data in the cache info. * @param {boolean} isCreated Will be used as the local created flag of the new file instance. * @param {function} cb Will be invoked when the operation is complete. * @param {Error} cb.err Will be truthy if there were errors during the operation. * @param {File} cb.file The newly created file instance. */ RQLocalTree.prototype.createFromSource = function (source, remote, isCreated, cb) { var self = this; _createWorkFile.call(self, source, isCreated, false, remote, function (err, workFile) { if (err) { cb(err); } else { RQLocalFile.createInstance(source, workFile, self, cb); } }); }; /** * Test whether or not the specified file exists. * * @param {String} name file name * @param {Function} cb callback called with the result * @param {SMBError} cb.error error (non-null if an error occurred) * @param {Boolean} cb.exists true if the file exists; false otherwise */ RQLocalTree.prototype.exists = function (name, cb) { if (this.isDownloading(name)) { // downloading files will not be reported as existing cb(null, false); } else { this.source.exists(name, cb); } }; /** * Open an existing file. * * @param {String} name file name * @param {Function} cb callback called with the opened file * @param {SMBError} cb.error error (non-null if an error occurred) * @param {File} cb.file opened file */ RQLocalTree.prototype.open = function (name, cb) { var self = this; if (self.isDownloading(name)) { logger.error('attempting to open downloading file %s', name) cb('attempting to open downloading file ' + name); } else { self.source.open(name, function (err, file) { if (err) { cb(err); } else { self.createFromSource(file, null, false, cb); } }); } }; /** * List entries, matching a specified pattern. * * @param {String} pattern pattern * @param {Function} cb callback called with an array of matching files * @param {SMBError} cb.error error (non-null if an error occurred) * @param {File[]} cb.files array of matching files */ RQLocalTree.prototype.list = function (pattern, cb) { var self = this; self.source.list(pattern, function (err, files) { if (err) { cb(err); } else { var result = []; async.each(files, function (file, eachCb) { if (file.getName() != consts.WORK_DIR && !self.isDownloading(file.getPath())) { self.createFromSource(file, null, false, function (err, localFile) { if (err) { eachCb(err); } else { result.push(localFile); eachCb(); } }); } else { // don't include work dir and downloading files eachCb(); } }, function (err) { if (err) { cb(err); } else { cb(null, result); } }); } }); }; /** * Create a new file. * * @param {String} name file name * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) * @param {File} cb.file created file */ RQLocalTree.prototype.createFile = function (name, cb) { var self = this; function _handleFile(err, toHandle) { if (err) { cb(err); } else { if (self.cacheInfoOnly) { toHandle.dirty = true; } self.createFromSource(toHandle, null, true, cb); } } if (self.isDownloading(name)) { logger.error('attempting to create file %s that is downloading', name); cb('attempting to create file ' + name + ' that is downloading'); } else { if (self.cacheInfoOnly) { self.source.open(name, _handleFile); } else { self.source.createFile(name, _handleFile); } } }; /** * Create a new directory. * * @param {String} name directory name * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) * @param {File} cb.file created directory */ RQLocalTree.prototype.createDirectory = function (name, cb) { this.source.createDirectory(name, cb); }; /** * Delete a file. * * @param {String} name file name * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQLocalTree.prototype.delete = function (name, cb) { var self = this; function doDelete(deleteCb) { if (self.cacheInfoOnly) { // don't attempt to delete source deleteCb(); } else { self.source.delete(name, function (err) { if (err) { cb(err); } else { deleteCb(); } }); } } if (self.isDownloading(name)) { logger.error('attempting to delete downloading file %s', name); cb('attempting to delete downloading file ' + name); } else { doDelete(function () { self.cacheInfoExists(name, function (err, exists) { if (err) { cb(err); } else if (exists) { self.source.delete(self.getInfoFilePath(name), cb); } else { cb(); } }); }); } }; /** * Delete a directory. It must be empty in order to be deleted. * * @param {String} name directory name * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQLocalTree.prototype.deleteDirectory = function (name, cb) { var self = this; function _deleteWorkDir(delCb) { var workDir = Path.join(name, consts.WORK_DIR); self.source.exists(workDir, function (err, exists) { if (err) { cb(err); } else if (exists) { logger.debug('%s processing work directory for removal', workDir); // delete any dangling work files self.source.list(Path.join(workDir, '*'), function (err, workFiles) { if (err) { cb(err); } else { async.eachSeries(workFiles, function (workFile, eachCb) { logger.debug('%s removing dangling workfile', workFile.getPath()); self.source.delete(workFile.getPath(), eachCb); }, function (err) { if (err) { cb(err); } else { logger.debug('%s deleting work directory', workDir); self.source.deleteDirectory(workDir, function (err) { if (err) { cb(err); } else { delCb(); } }); } }); } }); } else { logger.debug('%s has no work directory', name); delCb(); } }); } self.list(Path.join(name, '*'), function (err, items) { if (err) { cb(err); } else if (items.length && !self.cacheInfoOnly) { // if the directory is not empty then delegate to the underlying source whether or not it can be deleted. if // non-empty deletion is supported then the work dir will be cleared out automatically logger.debug('%s is not empty, delegating delete to underlying source', name); self.source.deleteDirectory(name, cb); } else { // directory is empty, so clear out and remove the work dir to safely support backends that require directories // to be empty before deleting _deleteWorkDir(function() { logger.debug('%s deleting directory after clearing work dir', name); if (self.cacheInfoOnly) { // only delete cache info directories if cacheInfoOnly is set cb(); } else { self.source.deleteDirectory(name, cb); } }); } }); }; /** * Rename a file or directory. * * @param {String} oldName old name * @param {String} newName new name * @param {File} newRemote If truthy, the existing remote File instance of the new path. * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQLocalTree.prototype.renameExt = function (oldName, newName, newRemote, cb) { logger.debug('[%s] tree.rename %s to %s', this.share.config.backend, oldName, newName); var self = this; var newCreated = newRemote ? false : true; function _deleteCacheInfo(deleteName, delCb) { self.cacheInfoExists(deleteName, function (err, exists) { if (err) { cb(err); } else if (exists) { logger.debug('%s rename involved cache info file already exists, removing', deleteName); self.source.delete(self.getInfoFilePath(deleteName), function (err) { if (err) { cb(err); } else { delCb(); } }); } else { delCb(); } }); } function _doRename(renameCb) { // skip the actual rename if dealing with cache info only if (self.cacheInfoOnly) { renameCb(); } else { self.source.rename(oldName, newName, function (err) { if (err) { cb(err); } else { renameCb(); } }); } } if (self.isDownloading(oldName)) { logger.error('attempting to rename downloading file %s', oldName); cb('attempting to rename downloading file ' + oldName); } else { _doRename(function () { _deleteCacheInfo(oldName, function () { _deleteCacheInfo(newName, function () { logger.debug('%s work file does not exist, creating', newName); self.source.open(newName, function (err, renamed) { if (err) { cb(err); } else { // if the target work file already existed, don't flag the file as newly created. _createWorkFile.call(self, renamed, newCreated, !newCreated, newRemote, function (createErr) { renamed.close(function (closeErr) { if (closeErr) { logger.error('unable to close new file after rename %s', newName, closeErr); } if (createErr) { logger.error('unable to create new work file after rename %s', newName, createErr); cb(createErr); } else { cb(); } }); }); } }); }); }); }); } }; /** * Rename a file or directory. * * @param {String} oldName old name * @param {String} newName new name * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQLocalTree.prototype.rename = function (oldName, newName, cb) { this.renameExt(oldName, newName, false, cb); }; /** * Refresh a specific folder. * * @param {String} folderPath * @param {Boolean} deep * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQLocalTree.prototype.refresh = function (folderPath, deep, cb) { this.source.refresh(folderPath, deep, cb); }; /** * Disconnect this tree. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQLocalTree.prototype.disconnect = function (cb) { this.source.disconnect(cb); }; module.exports = RQLocalTree;