UNPKG

node-smb-server

Version:

A Pure JavaScript SMB Server Implementation

471 lines (439 loc) 16.2 kB
/* * Copyright 2016 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 logger = require('winston').loggers.get('spi'); var rqlog = require('winston').loggers.get('rq'); var File = require('../../spi/file'); /** * Creates an instance of RQFile. * * @constructor * @private * @this {RQFile} * @param {File} openFile file object * @param {RQTree} tree tree object */ var RQFile = function (openFile, tree) { logger.debug('[rq] file.construct %s', openFile.getPath()); if (!(this instanceof RQFile)) { return new RQFile(openFile, tree); } this.openFile = openFile; this.tree = tree; this.remote = tree.remote; this.local = tree.local; this.dirty = openFile.dirty ? true : false; this.syncDone = false; File.call(this, openFile.getPath(), tree); }; // the RQFile prototype inherits from File util.inherits(RQFile, File); /** * Async factory method for initializing a new RQFile from a remote file. * * @param {File} openFile file object * @param {RQTree} tree tree object * @param {Function} cb callback called with the bytes actually read * @param {SMBError} cb.error error (non-null if an error occurred) * @param {RQFile} cb.file RQFile instance */ RQFile.createInstance = function (openFile, tree, cb) { cb(null, new RQFile(openFile, tree)); }; /** * If necessary, caches a remote file locally for use in cached operations. * @param {Function} cb callback called after caching is complete * @param {SMBError} cb.error error (non-null if an error occurred) * @param {File} cb.file File instance for consumption */ RQFile.prototype.cacheFile = function (cb) { rqlog.debug('RQFile.cacheFile %s', this.filePath); var self = this; function _setOpenFile(newOpenFile, setCb) { if (self.openFile) { self.openFile.close(function (err) { if (err) { setCb(err); } else { self.openFile = newOpenFile; setCb(); } }); } else { self.openFile = newOpenFile; setCb(); } } if (self.syncDone) { logger.debug('%s sync already done for this file instance. returning currently open file', self.filePath); cb(null, self.openFile); } else if (self.tree.isTempFileName(self.filePath)) { logger.debug('%s is a temporary file, do not attempt to cache it', self.filePath); cb(null, self.openFile); } else { self.syncDone = true; // wait for any in-progress downloads of the file self.local.waitOnDownload(self.filePath, function (err) { if (err) { self.tree.handleErr(cb, err); } else { // TODO use async.js to simplify code // first, determine if a local cache of the file already exists self.local.exists(self.filePath, function (err, exists) { if (err) { self.tree.handleErr(cb, err); } else if (exists) { // local cached file exists. determine if it was created locally or is cached from remote logger.debug('%d %s to cache exists locally', new Date().getTime(), self.filePath); self.local.open(self.filePath, function (err, localFile) { if (err) { self.tree.handleErr(cb, err); } else { if (localFile.lastModified() === self.lastModified() && self.openFile.isCreatedLocally) { logger.debug('%s file in cache is the same as in-memory, using in-memory', self.getPath()); var currOpenFile = self.openFile; self.openFile = localFile; localFile = currOpenFile; } self.openFile.close(function (err) { if (err) { self.tree.handleErr(cb, err); } else { self.openFile = localFile; if (localFile.isCreatedLocally()) { logger.debug('file %s was created locally, returning cached file', self.filePath); cb(null, localFile); } else { logger.debug('file %s was cached from remote, check to see if it needs to be updated', self.filePath); rqlog.debug('RQFile.cacheFile.remote.open %s', self.filePath); self.remote.open(self.tree.remoteEncodePath(self.filePath), function (err, remoteFile) { if (err) { self.tree.handleErr(cb, err); } else { localFile.canDelete(function (err, canDelete) { if (err) { self.tree.handleErr(cb, err); } else if (!canDelete) { logger.debug('file %s cannot be safely deleted. check to see if it is in the queue', self.filePath); self.tree.queueDataExists(self.getPath(), function (err, exists) { if (err) { self.tree.handleErr(cb, err); } else { if (!exists) { logger.debug('file %s is not queued, send conflict event', self.filePath); self.tree.emitSyncConflict(self.getPath()); } cb(null, localFile); } }); } else { logger.debug('file %s can be safely deleted. check to see if it needs to be re-synced', self.filePath); var remoteModified = remoteFile.lastModified(); // check if the remote version has been modified since it was cached if (localFile.getDownloadedRemoteModifiedDate() != remoteModified) { logger.info('file %s has been modified remotely (%d) since it was last synced (%d). re-syncing', self.filePath, remoteModified, localFile.getLastSyncDate()); localFile.close(function (err) { if (err) { self.tree.handleErr(cb, err); } else { remoteFile.close(function (err) { if (err) { self.tree.handleErr(cb, err); } else { self.local.delete(self.filePath, function (err) { if (err) { self.tree.handleErr(cb, err); } else { self.syncDone = false; self.cacheFile(cb); } }); } }); } }); } else { logger.debug('file %s has not been modified remotely (%d) since it was last synced (%d). using cache.', self.filePath, remoteModified, localFile.getLastSyncDate()); cb(null, localFile); } } }); } }); } } }); } }); } else { logger.debug('%d local file %s does not exist. fetching from remote', new Date().getTime(), self.filePath); rqlog.debug('RQFile.cacheFile.share.fetchResource %s', self.filePath); self.local.download(self.remote, self.getPath(), function (err, cachedFile) { if (err) { self.tree.handleErr(cb, err); } else { _setOpenFile(cachedFile, function (err) { if (err) { self.tree.handleErr(cb, err); } else { cb(null, cachedFile); } }); } }); } }); } }); } }; //---------------------------------------------------------------------< File > /** * Return a flag indicating whether this is a file. * * @return {Boolean} <code>true</code> if this is a file; * <code>false</code> otherwise */ RQFile.prototype.isFile = function () { return this.openFile.isFile(); }; /** * Return a flag indicating whether this is a directory. * * @return {Boolean} <code>true</code> if this is a directory; * <code>false</code> otherwise */ RQFile.prototype.isDirectory = function () { return this.openFile.isDirectory(); }; /** * Return a flag indicating whether this file is read-only. * * @return {Boolean} <code>true</code> if this file is read-only; * <code>false</code> otherwise */ RQFile.prototype.isReadOnly = function () { return false; }; /** * Return the file size. * * @return {Number} file size, in bytes */ RQFile.prototype.size = function () { logger.debug('[rq] size ', this.openFile.getPath()); return this.openFile.size(); }; /** * Return the number of bytes that are allocated to the file. * * @return {Number} allocation size, in bytes */ RQFile.prototype.allocationSize = function () { return this.openFile.allocationSize(); }; /** * Return the time of last modification, in milliseconds since * Jan 1, 1970, 00:00:00.0. * * @return {Number} time of last modification */ RQFile.prototype.lastModified = function () { return this.openFile.lastModified(); }; /** * Sets the time of last modification, in milliseconds since * Jan 1, 1970, 00:00:00.0. * * @param {Number} ms * @return {Number} time of last modification */ RQFile.prototype.setLastModified = function (ms) { this.openFile.setLastModified(ms); }; /** * Return the time when file status was last changed, in milliseconds since * Jan 1, 1970, 00:00:00.0. * * @return {Number} when file status was last changed */ RQFile.prototype.lastChanged = function () { return this.openFile.lastChanged(); }; /** * Return the create time, in milliseconds since Jan 1, 1970, 00:00:00.0. * Jan 1, 1970, 00:00:00.0. * * @return {Number} time created */ RQFile.prototype.created = function () { return this.openFile.created(); }; /** * Return the time of last access, in milliseconds since Jan 1, 1970, 00:00:00.0. * Jan 1, 1970, 00:00:00.0. * * @return {Number} time of last access */ RQFile.prototype.lastAccessed = function () { return this.openFile.lastAccessed(); }; /** * Read bytes at a certain position inside the file. * * @param {Buffer} buffer the buffer that the data will be written to * @param {Number} offset the offset in the buffer to start writing at * @param {Number} length the number of bytes to read * @param {Number} position offset where to begin reading from in the file * @param {Function} cb callback called with the bytes actually read * @param {SMBError} cb.error error (non-null if an error occurred) * @param {Number} cb.bytesRead number of bytes actually read * @param {Buffer} cb.buffer buffer holding the bytes actually read */ RQFile.prototype.read = function (buffer, offset, length, position, cb) { rqlog.debug('RQFile.read [%s] [%d]', this.filePath, length); var self = this; this.cacheFile(function (err, file) { if (err) { self.tree.handleErr(cb, err); } else { file.read(buffer, offset, length, position, cb); } }); }; /** * Write bytes at a certain position inside the file. * * @param {Buffer} data buffer to write * @param {Number} position position inside file * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQFile.prototype.write = function (data, position, cb) { rqlog.debug('RQFile.write [%s] [%d] [%d]', this.filePath, data.length, position); var self = this; this.cacheFile(function (err, file) { if (err) { self.tree.handleErr(cb, err); } else { file.write(data, position, function (err) { if (err) { self.tree.handleErr(cb, err); } else { self.dirty = true; cb(null); } }); } }); }; /** * Sets the file length. * * @param {Number} length file length * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQFile.prototype.setLength = function (length, cb) { rqlog.debug('RQFile.setLength [%s] [%d]', this.filePath, length); var self = this; this.cacheFile(function (err, file) { if (err) { self.tree.handleErr(cb, err); } else { self.dirty = true; file.setLength(length, cb); } }); }; /** * Delete this file or directory. If this file denotes a directory, it must * be empty in order to be deleted. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQFile.prototype.delete = function (cb) { rqlog.debug('RQFile.delete %s', this.filePath); var self = this; logger.debug('deleting file %s', self.getPath()); var sendResult = function(err) { if (err) { self.tree.handleErr(cb, err); } else { logger.debug('successfully deleted %s', self.getPath()); self.dirty = false; cb(); } }; if (self.isDirectory()) { self.tree.deleteDirectory(self.getPath(), function (err) { sendResult(err); }); } else { self.tree.delete(self.getPath(), function (err) { sendResult(err); }); } }; /** * Flush the contents of the file to disk. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQFile.prototype.flush = function (cb) { rqlog.debug('RQFile.flush %s', this.filePath); var self = this; this.cacheFile(function (err, file) { if (err) { self.tree.handleErr(cb, err); } else { file.flush(cb); } }); }; /** * Close this file, releasing any resources. * * @param {Function} cb callback called on completion * @param {SMBError} cb.error error (non-null if an error occurred) */ RQFile.prototype.close = function (cb) { rqlog.debug('RQFile.close %s', this.filePath); var self = this; this.openFile.close(function (err) { if (err) { self.tree.handleErr(cb, err); } else { if (self.dirty) { logger.debug('%s is dirty, queueing method', self.getPath()); self.tree.isCreatedLocally(self.getPath(), function (err, isLocal) { if (err) { self.tree.handleErr(cb, err); } else if (isLocal) { logger.debug('%s is newly created, queuing creation', self.getPath()); self.tree.queueData(self.getPath(), 'PUT', false, cb); } else { logger.debug('%s is being updated, queuing update', self.getPath()); self.tree.queueData(self.getPath(), 'POST', false, cb); } }); } else { logger.debug('%s is not dirty, closing', self.getPath()); cb(); } } }); }; module.exports = RQFile;