node-smb-server
Version:
A Pure JavaScript SMB Server Implementation
398 lines (355 loc) • 12.3 kB
JavaScript
/*
* 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 logger = require('winston').loggers.get('spi');
var File = require('../../spi/file');
/**
* Creates an instance of RQLocalFile.
*
* @constructor
* @private
* @this {RQLocalFile}
* @param {String} filePath normalized file path
* @param {File} sourceFile The wrapped file that the work file will use as its underlying file.
* @param {RQLocalTree} tree tree object
*/
var RQLocalFile = function (source, cacheData, tree) {
logger.debug('[rq] RQLocalFile.construct %s', source.getPath());
if (!(this instanceof RQLocalFile)) {
return new RQLocalFile(source, cacheData, tree);
}
this.source = source;
this.data = cacheData || {};
this.dirty = source.dirty ? true : false;
this.cacheInfoOnly = tree.isCacheInfoOnly();
if (!this.data.local) {
this.data['local'] = {};
}
if (!this.data.remote) {
this.data['remote'] = {};
}
File.call(this, source.getPath(), tree);
};
// the RQWorkFile prototype inherits from File
util.inherits(RQLocalFile, File);
module.exports = RQLocalFile;
/**
* Reads the contents of a cache info file and converts them to an object.
* @param {File} infoFile The file to be read.
* @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 {object} cb.cacheInfo Object containing the read cache information.
*/
RQLocalFile.readCacheInfo = function (infoFile, cb) {
var buffer = new Buffer(infoFile.size());
infoFile.read(buffer, 0, infoFile.size(), 0, function (err) {
if (err) {
cb(err);
} else {
var cacheInfo = {};
try {
cacheInfo = JSON.parse(buffer.toString('utf8', 0, infoFile.size()));
logger.debug('%s read work file contents', infoFile.getPath());
} catch (e) {
logger.warn('trying to read work file whose contents are not json %s', infoFile.getPath(), e);
}
infoFile.close(function (err) {
if (err) {
logger.warn('unable to close work file %s', infoFile.getPath(), err);
}
cb(null, cacheInfo);
});
}
});
};
/**
* Creates a new instance of an RQLocalFile from required information.
* @param {File} sourceFile The source file whose information will be used for much of the local file's functionality.
* @param {File} infoFile A file that will be read and whose contents will be used to provide certain info about the
* local file.
* @param {Tree} tree The tree to which the local file belongs.
* @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 {RQLocalFile} cb.file Will be the new file instance.
*/
RQLocalFile.createInstance = function (sourceFile, infoFile, tree, cb) {
if (infoFile) {
// only try to read cache info file if there is one.
RQLocalFile.readCacheInfo(infoFile, function (err, cacheInfo) {
if (err) {
cb(err);
} else {
cb(null, new RQLocalFile(sourceFile, cacheInfo, tree));
}
});
} else {
cb(null, new RQLocalFile(sourceFile, {}, tree));
}
};
/**
* Uses local and remote File information to create an object containing cache information in the expected format.
* @param {File} local The local File whose information will be used as the local info.
* @param {File} remote The remote File whose information will be used as remote info.
* @param {boolean} created A flag indicating whether or not the file was created locally.
* @param [boolean] refreshed A flag indicating whether or not the cache info file was created due to a refresh.
* @returns {object} Cache information based on the provided objects.
*/
RQLocalFile.getCacheInfo = function (local, remote, created, refreshed) {
var writeData = {
local: {
lastModified: local.lastModified()
},
created: created ? true : false,
refreshed: refreshed ? true : false,
synced: new Date().getTime()
};
if (remote) {
writeData['remote'] = {
lastModified: remote.lastModified(),
created: remote.created()
};
}
return writeData;
};
/**
* Returns a value indicating whether the local file has been created locally.
* @return {boolean} TRUE if the file is created locally, otherwise FALSE.
*/
RQLocalFile.prototype.isCreatedLocally = function () {
return this.data.created ? true : false;
};
/**
* Retrieves the modified date of the remote file that was downloaded locally.
* @return {int} A timestamp.
*/
RQLocalFile.prototype.getDownloadedRemoteModifiedDate = function () {
var modified = this.data.remote.lastModified;
return modified ? modified : 0;
};
/**
* Retrieves the date that the local file was last synced.
* @return {int} A timestamp.
*/
RQLocalFile.prototype.getLastSyncDate = function () {
return this.data.synced ? this.data.synced : 0;
};
/**
* Determines if the local file can be safely deleted.
* @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.canDelete Will be TRUE if the file can be deleted, otherwise FALSE.
*/
RQLocalFile.prototype.canDelete = function (cb) {
var self = this;
var path = self.getPath();
if (self.isDirectory()) {
logger.debug('%s is a directory, so it can be deleted', path);
// the file is a directory
cb(null, true);
} else if (self.tree.isTempFileName(path)) {
logger.debug('%s is a temp file, so it can be deleted', path);
cb(null, true);
} else {
var lastModifiedAtCache = self.data.local.lastModified ? self.data.local.lastModified : 0;
logger.debug('%s is a file, checking lastModified(%d)==lastModifiedAtCache(%d), not isCreatedLocally(%s), and has a downloadedRemoteModifiedDate(%d)', path, self.lastModified(), lastModifiedAtCache, self.isCreatedLocally(), self.getDownloadedRemoteModifiedDate());
var hasRemoteModified = self.getDownloadedRemoteModifiedDate() ? true : false;
cb(null, self.source.lastModified() == lastModifiedAtCache && !self.isCreatedLocally() && hasRemoteModified);
}
};
/**
* Return a flag indicating whether this is a file.
*
* @return {Boolean} <code>true</code> if this is a file;
* <code>false</code> otherwise
*/
RQLocalFile.prototype.isFile = function () {
return this.source.isFile();
};
/**
* Return a flag indicating whether this is a directory.
*
* @return {Boolean} <code>true</code> if this is a directory;
* <code>false</code> otherwise
*/
RQLocalFile.prototype.isDirectory = function () {
return this.source.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
*/
RQLocalFile.prototype.isReadOnly = function () {
return this.source.isReadOnly();
};
/**
* Return the file size.
*
* @return {Number} file size, in bytes
*/
RQLocalFile.prototype.size = function () {
return this.source.size();
};
/**
* Return the number of bytes that are allocated to the file.
*
* @return {Number} allocation size, in bytes
*/
RQLocalFile.prototype.allocationSize = function () {
return this.source.allocationSize();
};
/**
* Return the time of last modification, in milliseconds since
* Jan 1, 1970, 00:00:00.0.
*
* @return {Number} time of last modification
*/
RQLocalFile.prototype.lastModified = function () {
var date = this.source.lastModified();
if (this.data.remote['lastModified'] && !this.isCreatedLocally()) {
if (date == this.data.local['lastModified'] && date > this.data.remote['lastModified'] && !this.data.refreshed) {
date = this.data.remote['lastModified'];
}
}
return date;
};
/**
* 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
*/
RQLocalFile.prototype.setLastModified = function (ms) {
this.source.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
*/
RQLocalFile.prototype.lastChanged = function () {
return this.lastModified();
};
/**
* Return the create time, in seconds since Jan 1, 1970, 00:00:00.0.
* Jan 1, 1970, 00:00:00.0.
*
* @return {Number} time created
*/
RQLocalFile.prototype.created = function () {
var date = this.source.created();
// always use remote created if it's available and is older than the local file
if (this.data.remote['created'] && this.data.remote['created'] < date) {
date = this.data.remote.created;
}
return date;
};
/**
* Return the time of last access, in seconds since Jan 1, 1970, 00:00:00.0.
* Jan 1, 1970, 00:00:00.0.
*
* @return {Number} time of last access
*/
RQLocalFile.prototype.lastAccessed = function () {
return this.source.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
*/
RQLocalFile.prototype.read = function (buffer, offset, length, position, cb) {
this.source.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)
*/
RQLocalFile.prototype.write = function (data, position, cb) {
if (this.cacheInfoOnly) {
cb();
} else {
this.source.write(data, position, cb);
}
};
/**
* 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)
*/
RQLocalFile.prototype.setLength = function (length, cb) {
if (this.cacheInfoOnly) {
cb();
} else {
this.source.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)
*/
RQLocalFile.prototype.delete = function (cb) {
var self = this;
var path = self.getPath();
var isDir = self.isDirectory();
self.close(function (err) {
if (err) {
cb(err);
} else if (isDir) {
self.tree.deleteDirectory(path, cb);
} else {
self.tree.delete(path, cb);
}
});
};
/**
* 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)
*/
RQLocalFile.prototype.flush = function (cb) {
if (this.cacheInfoOnly) {
cb();
} else {
this.source.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)
*/
RQLocalFile.prototype.close = function (cb) {
this.source.close(cb);
};