var _ = require('../../lib/nodash.js'); var Path = require('../path.js'); var normalize = Path.normalize; var dirname = Path.dirname; var basename = Path.basename; var isAbsolutePath = Path.isAbsolute; var isNullPath = Path.isNull; var Constants = require('../constants.js'); var NODE_TYPE_FILE = Constants.NODE_TYPE_FILE; var NODE_TYPE_DIRECTORY = Constants.NODE_TYPE_DIRECTORY; var NODE_TYPE_SYMBOLIC_LINK = Constants.NODE_TYPE_SYMBOLIC_LINK; var NODE_TYPE_META = Constants.NODE_TYPE_META; var P9_QTDIR = Constants.P9.QTDIR; var P9_QTFILE = Constants.P9.QTFILE; var P9_QTSYMLINK = Constants.P9.QTSYMLINK; var ROOT_DIRECTORY_NAME = Constants.ROOT_DIRECTORY_NAME; var SUPER_NODE_ID = Constants.SUPER_NODE_ID; var SYMLOOP_MAX = Constants.SYMLOOP_MAX; var O_READ = Constants.O_READ; var O_WRITE = Constants.O_WRITE; var O_CREATE = Constants.O_CREATE; var O_EXCLUSIVE = Constants.O_EXCLUSIVE; var O_TRUNCATE = Constants.O_TRUNCATE; var O_APPEND = Constants.O_APPEND; var O_FLAGS = Constants.O_FLAGS; var XATTR_CREATE = Constants.XATTR_CREATE; var XATTR_REPLACE = Constants.XATTR_REPLACE; var FS_NOMTIME = Constants.FS_NOMTIME; var FS_NOCTIME = Constants.FS_NOCTIME; var Encoding = require('../encoding.js'); var Errors = require('../errors.js'); var DirectoryEntry = require('../directory-entry.js'); var OpenFileDescription = require('../open-file-description.js'); var SuperNode = require('../super-node.js'); var Node = require('../node.js'); var Stats = require('../stats.js'); var Buffer = require('../buffer.js'); /** * Update node times. Only passed times are modified (undefined times are ignored) * and filesystem flags are examined in order to override update logic. */ function update_node_times(context, path, node, times, callback) { var update = false; // If updating the P9 info only, don't send change events (honour flags). var queueChangeEvent = false; if(times.ctime || times.mtime) { // Update the qid's version field, since node has changed. // TODO: this might be more than I need to do... node.p9.qid.version = times.ctime || times.mtime; update = true; } // Honour mount flags for how we update times var flags = context.flags; if(_(flags).contains(FS_NOCTIME)) { delete times.ctime; } if(_(flags).contains(FS_NOMTIME)) { delete times.mtime; } if(times.ctime) { node.ctime = times.ctime; // We don't do atime tracking for perf reasons, but do mirror ctime node.atime = times.ctime; update = true; queueChangeEvent = true; } if(times.atime) { // The only time we explicitly pass atime is when utimes(), futimes() is called. // Override ctime mirror here if so node.atime = times.atime; update = true; queueChangeEvent = true; } if(times.mtime) { node.mtime = times.mtime; update = true; queueChangeEvent = true; } function complete(error) { // Queue this change so we can send watch events. // Unlike node.js, we send the full path vs. basename/dirname only. if(queueChangeEvent) { context.changes.push({ event: 'change', path: path }); } callback(error); } if(update) { context.putObject(node.id, node, complete); } else { complete(); } } /** * make_node() */ // in: file or directory path // out: new node representing file/directory function make_node(context, path, mode, callback) { if(mode !== NODE_TYPE_DIRECTORY && mode !== NODE_TYPE_FILE) { return callback(new Errors.EINVAL('mode must be a directory or file', path)); } path = normalize(path); var name = basename(path); var parentPath = dirname(path); var parentNode; var parentNodeData; var node; // Check if the parent node exists function create_node_in_parent(error, parentDirectoryNode) { if(error) { callback(error); } else if(parentDirectoryNode.mode !== NODE_TYPE_DIRECTORY) { callback(new Errors.ENOTDIR('a component of the path prefix is not a directory', path)); } else { parentNode = parentDirectoryNode; find_node(context, path, check_if_node_exists); } } // Check if the node to be created already exists function check_if_node_exists(error, result) { if(!error && result) { callback(new Errors.EEXIST('path name already exists', path)); } else if(error && !(error instanceof Errors.ENOENT)) { callback(error); } else { context.getObject(parentNode.data, create_node); } } // Create the new node function create_node(error, result) { if(error) { callback(error); } else { parentNodeData = result; Node.create({ path: path, guid: context.guid, mode: mode }, function(error, result) { if(error) { callback(error); return; } node = result; node.nlinks += 1; context.putObject(node.id, node, update_parent_node_data); }); } } // Update parent node time function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, parentPath, node, { mtime: now, ctime: now }, callback); } } // Update the parent nodes data function update_parent_node_data(error) { if(error) { callback(error); } else { parentNodeData[name] = new DirectoryEntry(node.id, mode); context.putObject(parentNode.data, parentNodeData, update_time); } } // Find the parent node find_node(context, parentPath, create_node_in_parent); } /** * find_node */ // in: file or directory path // out: node structure, or error function find_node(context, path, callback) { path = normalize(path); if(!path) { return callback(new Errors.ENOENT('path is an empty string')); } var name = basename(path); var parentPath = dirname(path); var followedCount = 0; function read_root_directory_node(error, superNode) { if(error) { callback(error); } else if(!superNode || superNode.mode !== NODE_TYPE_META || !superNode.rnode) { callback(new Errors.EFILESYSTEMERROR()); } else { context.getObject(superNode.rnode, check_root_directory_node); } } function check_root_directory_node(error, rootDirectoryNode) { if(error) { callback(error); } else if(!rootDirectoryNode) { callback(new Errors.ENOENT()); } else { callback(null, rootDirectoryNode); } } // in: parent directory node // out: parent directory data function read_parent_directory_data(error, parentDirectoryNode) { if(error) { callback(error); } else if(parentDirectoryNode.mode !== NODE_TYPE_DIRECTORY || !parentDirectoryNode.data) { callback(new Errors.ENOTDIR('a component of the path prefix is not a directory', path)); } else { context.getObject(parentDirectoryNode.data, get_node_from_parent_directory_data); } } // in: parent directory data // out: searched node function get_node_from_parent_directory_data(error, parentDirectoryData) { if(error) { callback(error); } else { if(!_(parentDirectoryData).has(name)) { callback(new Errors.ENOENT(null, path)); } else { var nodeId = parentDirectoryData[name].id; context.getObject(nodeId, is_symbolic_link); } } } function is_symbolic_link(error, node) { if(error) { callback(error); } else { if(node.mode == NODE_TYPE_SYMBOLIC_LINK) { followedCount++; if(followedCount > SYMLOOP_MAX){ callback(new Errors.ELOOP(null, path)); } else { follow_symbolic_link(node.data); } } else { callback(null, node); } } } function follow_symbolic_link(data) { data = normalize(data); parentPath = dirname(data); name = basename(data); if(ROOT_DIRECTORY_NAME == name) { context.getObject(SUPER_NODE_ID, read_root_directory_node); } else { find_node(context, parentPath, read_parent_directory_data); } } if(ROOT_DIRECTORY_NAME == name) { context.getObject(SUPER_NODE_ID, read_root_directory_node); } else { find_node(context, parentPath, read_parent_directory_data); } } /** * set extended attribute (refactor) */ function set_extended_attribute (context, path, node, name, value, flag, callback) { function update_time(error) { if(error) { callback(error); } else { update_node_times(context, path, node, { ctime: Date.now() }, callback); } } var xattrs = node.xattrs; if (flag === XATTR_CREATE && xattrs.hasOwnProperty(name)) { callback(new Errors.EEXIST('attribute already exists', path)); } else if (flag === XATTR_REPLACE && !xattrs.hasOwnProperty(name)) { callback(new Errors.ENOATTR(null, path)); } else { xattrs[name] = value; context.putObject(node.id, node, update_time); } } /** * ensure_root_directory. Creates a root node if necessary. * * Note: this should only be invoked when formatting a new file system. * Multiple invocations of this by separate instances will still result * in only a single super node. */ function ensure_root_directory(context, callback) { var superNode; var directoryNode; var directoryData; function ensure_super_node(error, existingNode) { if(!error && existingNode) { // Another instance has beat us and already created the super node. callback(); } else if(error && !(error instanceof Errors.ENOENT)) { callback(error); } else { SuperNode.create({ guid: context.guid }, function(error, result) { if(error) { callback(error); return; } superNode = result; context.putObject(superNode.id, superNode, write_directory_node); }); } } function write_directory_node(error) { if(error) { callback(error); } else { Node.create({ guid: context.guid, id: superNode.rnode, mode: NODE_TYPE_DIRECTORY, path: ROOT_DIRECTORY_NAME }, function(error, result) { if(error) { callback(error); return; } directoryNode = result; directoryNode.nlinks += 1; context.putObject(directoryNode.id, directoryNode, write_directory_data); }); } } function write_directory_data(error) { if(error) { callback(error); } else { directoryData = {}; context.putObject(directoryNode.data, directoryData, callback); } } context.getObject(SUPER_NODE_ID, ensure_super_node); } /** * make_directory */ function make_directory(context, path, callback) { path = normalize(path); var name = basename(path); var parentPath = dirname(path); var directoryNode; var directoryData; var parentDirectoryNode; var parentDirectoryData; function check_if_directory_exists(error, result) { if(!error && result) { callback(new Errors.EEXIST(null, path)); } else if(error && !(error instanceof Errors.ENOENT)) { callback(error); } else { find_node(context, parentPath, read_parent_directory_data); } } function read_parent_directory_data(error, result) { if(error) { callback(error); } else { parentDirectoryNode = result; context.getObject(parentDirectoryNode.data, write_directory_node); } } function write_directory_node(error, result) { if(error) { callback(error); } else { parentDirectoryData = result; Node.create({ guid: context.guid, mode: NODE_TYPE_DIRECTORY, path: path }, function(error, result) { if(error) { callback(error); return; } directoryNode = result; directoryNode.nlinks += 1; context.putObject(directoryNode.id, directoryNode, write_directory_data); }); } } function write_directory_data(error) { if(error) { callback(error); } else { directoryData = {}; context.putObject(directoryNode.data, directoryData, update_parent_directory_data); } } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, parentPath, parentDirectoryNode, { mtime: now, ctime: now }, callback); } } function update_parent_directory_data(error) { if(error) { callback(error); } else { parentDirectoryData[name] = new DirectoryEntry(directoryNode.id, NODE_TYPE_DIRECTORY); context.putObject(parentDirectoryNode.data, parentDirectoryData, update_time); } } find_node(context, path, check_if_directory_exists); } /** * remove_directory */ function remove_directory(context, path, callback) { path = normalize(path); var name = basename(path); var parentPath = dirname(path); var directoryNode; var directoryData; var parentDirectoryNode; var parentDirectoryData; function read_parent_directory_data(error, result) { if(error) { callback(error); } else { parentDirectoryNode = result; context.getObject(parentDirectoryNode.data, check_if_node_exists); } } function check_if_node_exists(error, result) { if(error) { callback(error); } else if(ROOT_DIRECTORY_NAME == name) { callback(new Errors.EBUSY(null, path)); } else if(!_(result).has(name)) { callback(new Errors.ENOENT(null, path)); } else { parentDirectoryData = result; directoryNode = parentDirectoryData[name].id; context.getObject(directoryNode, check_if_node_is_directory); } } function check_if_node_is_directory(error, result) { if(error) { callback(error); } else if(result.mode != NODE_TYPE_DIRECTORY) { callback(new Errors.ENOTDIR(null, path)); } else { directoryNode = result; context.getObject(directoryNode.data, check_if_directory_is_empty); } } function check_if_directory_is_empty(error, result) { if(error) { callback(error); } else { directoryData = result; if(_(directoryData).size() > 0) { callback(new Errors.ENOTEMPTY(null, path)); } else { remove_directory_entry_from_parent_directory_node(); } } } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, parentPath, parentDirectoryNode, { mtime: now, ctime: now }, remove_directory_node); } } function remove_directory_entry_from_parent_directory_node() { delete parentDirectoryData[name]; context.putObject(parentDirectoryNode.data, parentDirectoryData, update_time); } function remove_directory_node(error) { if(error) { callback(error); } else { context.delete(directoryNode.id, remove_directory_data); } } function remove_directory_data(error) { if(error) { callback(error); } else { context.delete(directoryNode.data, callback); } } find_node(context, parentPath, read_parent_directory_data); } function open_file(context, path, flags, callback) { path = normalize(path); var name = basename(path); var parentPath = dirname(path); var directoryNode; var directoryData; var directoryEntry; var fileNode; var fileData; var followedCount = 0; if(ROOT_DIRECTORY_NAME == name) { if(_(flags).contains(O_WRITE)) { callback(new Errors.EISDIR('the named file is a directory and O_WRITE is set', path)); } else { find_node(context, path, set_file_node); } } else { find_node(context, parentPath, read_directory_data); } function read_directory_data(error, result) { if(error) { callback(error); } else if(result.mode !== NODE_TYPE_DIRECTORY) { callback(new Errors.ENOENT(null, path)); } else { directoryNode = result; context.getObject(directoryNode.data, check_if_file_exists); } } function check_if_file_exists(error, result) { if(error) { callback(error); } else { directoryData = result; if(_(directoryData).has(name)) { if(_(flags).contains(O_EXCLUSIVE)) { callback(new Errors.ENOENT('O_CREATE and O_EXCLUSIVE are set, and the named file exists', path)); } else { directoryEntry = directoryData[name]; if(directoryEntry.type == NODE_TYPE_DIRECTORY && _(flags).contains(O_WRITE)) { callback(new Errors.EISDIR('the named file is a directory and O_WRITE is set', path)); } else { context.getObject(directoryEntry.id, check_if_symbolic_link); } } } else { if(!_(flags).contains(O_CREATE)) { callback(new Errors.ENOENT('O_CREATE is not set and the named file does not exist', path)); } else { write_file_node(); } } } } function check_if_symbolic_link(error, result) { if(error) { callback(error); } else { var node = result; if(node.mode == NODE_TYPE_SYMBOLIC_LINK) { followedCount++; if(followedCount > SYMLOOP_MAX){ callback(new Errors.ELOOP(null, path)); } else { follow_symbolic_link(node.data); } } else { set_file_node(undefined, node); } } } function follow_symbolic_link(data) { data = normalize(data); parentPath = dirname(data); name = basename(data); if(ROOT_DIRECTORY_NAME == name) { if(_(flags).contains(O_WRITE)) { callback(new Errors.EISDIR('the named file is a directory and O_WRITE is set', path)); } else { find_node(context, path, set_file_node); } } find_node(context, parentPath, read_directory_data); } function set_file_node(error, result) { if(error) { callback(error); } else { fileNode = result; callback(null, fileNode); } } function write_file_node() { Node.create({ guid: context.guid, mode: NODE_TYPE_FILE, path: path }, function(error, result) { if(error) { callback(error); return; } fileNode = result; fileNode.nlinks += 1; context.putObject(fileNode.id, fileNode, write_file_data); }); } function write_file_data(error) { if(error) { callback(error); } else { fileData = new Buffer(0); fileData.fill(0); context.putBuffer(fileNode.data, fileData, update_directory_data); } } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, parentPath, directoryNode, { mtime: now, ctime: now }, handle_update_result); } } function update_directory_data(error) { if(error) { callback(error); } else { directoryData[name] = new DirectoryEntry(fileNode.id, NODE_TYPE_FILE); context.putObject(directoryNode.data, directoryData, update_time); } } function handle_update_result(error) { if(error) { callback(error); } else { callback(null, fileNode); } } } function replace_data(context, ofd, buffer, offset, length, callback) { var fileNode; function return_nbytes(error) { if(error) { callback(error); } else { callback(null, length); } } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, ofd.path, fileNode, { mtime: now, ctime: now }, return_nbytes); } } function update_file_node(error) { if(error) { callback(error); } else { context.putObject(fileNode.id, fileNode, update_time); } } function write_file_data(error, result) { if(error) { callback(error); } else { fileNode = result; var newData = new Buffer(length); newData.fill(0); buffer.copy(newData, 0, offset, offset + length); ofd.position = length; fileNode.size = length; fileNode.version += 1; context.putBuffer(fileNode.data, newData, update_file_node); } } context.getObject(ofd.id, write_file_data); } function write_data(context, ofd, buffer, offset, length, position, callback) { var fileNode; var fileData; function return_nbytes(error) { if(error) { callback(error); } else { callback(null, length); } } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, ofd.path, fileNode, { mtime: now, ctime: now }, return_nbytes); } } function update_file_node(error) { if(error) { callback(error); } else { context.putObject(fileNode.id, fileNode, update_time); } } function update_file_data(error, result) { if(error) { callback(error); } else { fileData = result; if(!fileData) { return callback(new Errors.EIO('Expected Buffer')); } var _position = (!(undefined === position || null === position)) ? position : ofd.position; var newSize = Math.max(fileData.length, _position + length); var newData = new Buffer(newSize); newData.fill(0); if(fileData) { fileData.copy(newData); } buffer.copy(newData, _position, offset, offset + length); if(undefined === position) { ofd.position += length; } fileNode.size = newSize; fileNode.version += 1; context.putBuffer(fileNode.data, newData, update_file_node); } } function read_file_data(error, result) { if(error) { callback(error); } else { fileNode = result; context.getBuffer(fileNode.data, update_file_data); } } context.getObject(ofd.id, read_file_data); } function read_data(context, ofd, buffer, offset, length, position, callback) { var fileNode; var fileData; function handle_file_data(error, result) { if(error) { callback(error); } else { fileData = result; if(!fileData) { return callback(new Errors.EIO('Expected Buffer')); } var _position = (!(undefined === position || null === position)) ? position : ofd.position; length = (_position + length > buffer.length) ? length - _position : length; fileData.copy(buffer, offset, _position, _position + length); if(undefined === position) { ofd.position += length; } callback(null, length); } } function read_file_data(error, result) { if(error) { callback(error); } else if(result.mode === NODE_TYPE_DIRECTORY) { callback(new Errors.EISDIR('the named file is a directory', ofd.path)); } else { fileNode = result; context.getBuffer(fileNode.data, handle_file_data); } } context.getObject(ofd.id, read_file_data); } function stat_file(context, path, callback) { path = normalize(path); var name = basename(path); find_node(context, path, callback); } function fstat_file(context, ofd, callback) { ofd.getNode(context, callback); } function lstat_file(context, path, callback) { path = normalize(path); var name = basename(path); var parentPath = dirname(path); var directoryNode; var directoryData; if(ROOT_DIRECTORY_NAME == name) { find_node(context, path, callback); } else { find_node(context, parentPath, read_directory_data); } function read_directory_data(error, result) { if(error) { callback(error); } else { directoryNode = result; context.getObject(directoryNode.data, check_if_file_exists); } } function check_if_file_exists(error, result) { if(error) { callback(error); } else { directoryData = result; if(!_(directoryData).has(name)) { callback(new Errors.ENOENT('a component of the path does not name an existing file', path)); } else { context.getObject(directoryData[name].id, callback); } } } } function link_node(context, oldpath, newpath, callback) { oldpath = normalize(oldpath); var oldname = basename(oldpath); var oldParentPath = dirname(oldpath); newpath = normalize(newpath); var newname = basename(newpath); var newParentPath = dirname(newpath); var ctime = Date.now(); var oldDirectoryNode; var oldDirectoryData; var newDirectoryNode; var newDirectoryData; var fileNode; function update_time(error) { if(error) { callback(error); } else { update_node_times(context, newpath, fileNode, { ctime: ctime }, callback); } } function update_file_node(error, result) { if(error) { callback(error); } else { fileNode = Node.fromObject(result); fileNode.nlinks += 1; fileNode.updatePathInfo(newpath, ctime); context.putObject(fileNode.id, fileNode, update_time); } } function read_directory_entry(error, result) { if(error) { callback(error); } else { context.getObject(newDirectoryData[newname].id, update_file_node); } } function check_if_new_file_exists(error, result) { if(error) { callback(error); } else { newDirectoryData = result; if(_(newDirectoryData).has(newname)) { callback(new Errors.EEXIST('newpath resolves to an existing file', newname)); } else { newDirectoryData[newname] = oldDirectoryData[oldname]; context.putObject(newDirectoryNode.data, newDirectoryData, read_directory_entry); } } } function read_new_directory_data(error, result) { if(error) { callback(error); } else { newDirectoryNode = result; context.getObject(newDirectoryNode.data, check_if_new_file_exists); } } function check_if_old_file_exists(error, result) { if(error) { callback(error); } else { oldDirectoryData = result; if(!_(oldDirectoryData).has(oldname)) { callback(new Errors.ENOENT('a component of either path prefix does not exist', oldname)); } else if(oldDirectoryData[oldname].type === NODE_TYPE_DIRECTORY) { callback(new Errors.EPERM('oldpath refers to a directory')); } else { find_node(context, newParentPath, read_new_directory_data); } } } function read_old_directory_data(error, result) { if(error) { callback(error); } else { oldDirectoryNode = result; context.getObject(oldDirectoryNode.data, check_if_old_file_exists); } } find_node(context, oldParentPath, read_old_directory_data); } function unlink_node(context, path, callback) { path = normalize(path); var name = basename(path); var parentPath = dirname(path); var directoryNode; var directoryData; var fileNode; function update_directory_data(error) { if(error) { callback(error); } else { delete directoryData[name]; context.putObject(directoryNode.data, directoryData, function(error) { var now = Date.now(); update_node_times(context, parentPath, directoryNode, { mtime: now, ctime: now }, callback); }); } } function delete_file_data(error) { if(error) { callback(error); } else { context.delete(fileNode.data, update_directory_data); } } function update_file_node(error, result) { if(error) { callback(error); } else { fileNode = result; fileNode.nlinks -= 1; if(fileNode.nlinks < 1) { context.delete(fileNode.id, delete_file_data); } else { context.putObject(fileNode.id, fileNode, function(error) { update_node_times(context, path, fileNode, { ctime: Date.now() }, update_directory_data); }); } } } function check_if_node_is_directory(error, result) { if(error) { callback(error); } else if(result.mode === NODE_TYPE_DIRECTORY) { callback(new Errors.EPERM('unlink not permitted on directories', name)); } else { update_file_node(null, result); } } function check_if_file_exists(error, result) { if(error) { callback(error); } else { directoryData = result; if(!_(directoryData).has(name)) { callback(new Errors.ENOENT('a component of the path does not name an existing file', name)); } else { context.getObject(directoryData[name].id, check_if_node_is_directory); } } } function read_directory_data(error, result) { if(error) { callback(error); } else { directoryNode = result; context.getObject(directoryNode.data, check_if_file_exists); } } find_node(context, parentPath, read_directory_data); } function read_directory(context, path, callback) { path = normalize(path); var name = basename(path); var directoryNode; var directoryData; function handle_directory_data(error, result) { if(error) { callback(error); } else { directoryData = result; var files = Object.keys(directoryData); callback(null, files); } } function read_directory_data(error, result) { if(error) { callback(error); } else if(result.mode !== NODE_TYPE_DIRECTORY) { callback(new Errors.ENOTDIR(null, path)); } else { directoryNode = result; context.getObject(directoryNode.data, handle_directory_data); } } find_node(context, path, read_directory_data); } function make_symbolic_link(context, srcpath, dstpath, callback) { dstpath = normalize(dstpath); var name = basename(dstpath); var parentPath = dirname(dstpath); var directoryNode; var directoryData; var fileNode; if(ROOT_DIRECTORY_NAME == name) { callback(new Errors.EEXIST(null, name)); } else { find_node(context, parentPath, read_directory_data); } function read_directory_data(error, result) { if(error) { callback(error); } else { directoryNode = result; context.getObject(directoryNode.data, check_if_file_exists); } } function check_if_file_exists(error, result) { if(error) { callback(error); } else { directoryData = result; if(_(directoryData).has(name)) { callback(new Errors.EEXIST(null, name)); } else { write_file_node(); } } } function write_file_node() { Node.create({ guid: context.guid, mode: NODE_TYPE_SYMBOLIC_LINK, path: dstpath }, function(error, result) { if(error) { callback(error); return; } fileNode = result; fileNode.nlinks += 1; fileNode.size = srcpath.length; fileNode.data = srcpath; context.putObject(fileNode.id, fileNode, update_directory_data); }); } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, parentPath, directoryNode, { mtime: now, ctime: now }, callback); } } function update_directory_data(error) { if(error) { callback(error); } else { directoryData[name] = new DirectoryEntry(fileNode.id, NODE_TYPE_SYMBOLIC_LINK); context.putObject(directoryNode.data, directoryData, update_time); } } } function read_link(context, path, callback) { path = normalize(path); var name = basename(path); var parentPath = dirname(path); var directoryNode; var directoryData; find_node(context, parentPath, read_directory_data); function read_directory_data(error, result) { if(error) { callback(error); } else { directoryNode = result; context.getObject(directoryNode.data, check_if_file_exists); } } function check_if_file_exists(error, result) { if(error) { callback(error); } else { directoryData = result; if(!_(directoryData).has(name)) { callback(new Errors.ENOENT('a component of the path does not name an existing file', name)); } else { context.getObject(directoryData[name].id, check_if_symbolic); } } } function check_if_symbolic(error, result) { if(error) { callback(error); } else { if(result.mode != NODE_TYPE_SYMBOLIC_LINK) { callback(new Errors.EINVAL('path not a symbolic link', path)); } else { callback(null, result.data); } } } } function truncate_file(context, path, length, callback) { path = normalize(path); var fileNode; function read_file_data (error, node) { if (error) { callback(error); } else if(node.mode == NODE_TYPE_DIRECTORY ) { callback(new Errors.EISDIR(null, path)); } else{ fileNode = node; context.getBuffer(fileNode.data, truncate_file_data); } } function truncate_file_data(error, fileData) { if (error) { callback(error); } else { if(!fileData) { return callback(new Errors.EIO('Expected Buffer')); } var data = new Buffer(length); data.fill(0); if(fileData) { fileData.copy(data); } context.putBuffer(fileNode.data, data, update_file_node); } } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, path, fileNode, { mtime: now, ctime: now }, callback); } } function update_file_node (error) { if(error) { callback(error); } else { fileNode.size = length; fileNode.version += 1; context.putObject(fileNode.id, fileNode, update_time); } } if(length < 0) { callback(new Errors.EINVAL('length cannot be negative')); } else { find_node(context, path, read_file_data); } } function ftruncate_file(context, ofd, length, callback) { var fileNode; function read_file_data (error, node) { if (error) { callback(error); } else if(node.mode == NODE_TYPE_DIRECTORY ) { callback(new Errors.EISDIR()); } else{ fileNode = node; context.getBuffer(fileNode.data, truncate_file_data); } } function truncate_file_data(error, fileData) { if (error) { callback(error); } else { var data; if(!fileData) { return callback(new Errors.EIO('Expected Buffer')); } if(fileData) { data = fileData.slice(0, length); } else { data = new Buffer(length); data.fill(0); } context.putBuffer(fileNode.data, data, update_file_node); } } function update_time(error) { if(error) { callback(error); } else { var now = Date.now(); update_node_times(context, ofd.path, fileNode, { mtime: now, ctime: now }, callback); } } function update_file_node (error) { if(error) { callback(error); } else { fileNode.size = length; fileNode.version += 1; context.putObject(fileNode.id, fileNode, update_time); } } if(length < 0) { callback(new Errors.EINVAL('length cannot be negative')); } else { ofd.getNode(context, read_file_data); } } function utimes_file(context, path, atime, mtime, callback) { path = normalize(path); function update_times(error, node) { if (error) { callback(error); } else { update_node_times(context, path, node, { atime: atime, ctime: mtime, mtime: mtime }, callback); } } if (typeof atime != 'number' || typeof mtime != 'number') { callback(new Errors.EINVAL('atime and mtime must be number', path)); } else if (atime < 0 || mtime < 0) { callback(new Errors.EINVAL('atime and mtime must be positive integers', path)); } else { find_node(context, path, update_times); } } function futimes_file(context, ofd, atime, mtime, callback) { function update_times (error, node) { if (error) { callback(error); } else { update_node_times(context, ofd.path, node, { atime: atime, ctime: mtime, mtime: mtime }, callback); } } if (typeof atime != 'number' || typeof mtime != 'number') { callback(new Errors.EINVAL('atime and mtime must be a number')); } else if (atime < 0 || mtime < 0) { callback(new Errors.EINVAL('atime and mtime must be positive integers')); } else { ofd.getNode(context, update_times); } } function setxattr_file(context, path, name, value, flag, callback) { path = normalize(path); function setxattr(error, node) { if(error) { return callback(error); } set_extended_attribute(context, path, node, name, value, flag, callback); } if (typeof name != 'string') { callback(new Errors.EINVAL('attribute name must be a string', path)); } else if (!name) { callback(new Errors.EINVAL('attribute name cannot be an empty string', path)); } else if (flag !== null && flag !== XATTR_CREATE && flag !== XATTR_REPLACE) { callback(new Errors.EINVAL('invalid flag, must be null, XATTR_CREATE or XATTR_REPLACE', path)); } else { find_node(context, path, setxattr); } } function fsetxattr_file (context, ofd, name, value, flag, callback) { function setxattr(error, node) { if(error) { return callback(error); } set_extended_attribute(context, ofd.path, node, name, value, flag, callback); } if (typeof name !== 'string') { callback(new Errors.EINVAL('attribute name must be a string')); } else if (!name) { callback(new Errors.EINVAL('attribute name cannot be an empty string')); } else if (flag !== null && flag !== XATTR_CREATE && flag !== XATTR_REPLACE) { callback(new Errors.EINVAL('invalid flag, must be null, XATTR_CREATE or XATTR_REPLACE')); } else { ofd.getNode(context, setxattr); } } function getxattr_file (context, path, name, callback) { path = normalize(path); function get_xattr(error, node) { if(error) { return callback(error); } var xattrs = node.xattrs; if (!xattrs.hasOwnProperty(name)) { callback(new Errors.ENOATTR(null, path)); } else { callback(null, xattrs[name]); } } if (typeof name != 'string') { callback(new Errors.EINVAL('attribute name must be a string', path)); } else if (!name) { callback(new Errors.EINVAL('attribute name cannot be an empty string', path)); } else { find_node(context, path, get_xattr); } } function fgetxattr_file (context, ofd, name, callback) { function get_xattr (error, node) { if (error) { return callback(error); } var xattrs = node.xattrs; if (!xattrs.hasOwnProperty(name)) { callback(new Errors.ENOATTR()); } else { callback(null, xattrs[name]); } } if (typeof name != 'string') { callback(new Errors.EINVAL()); } else if (!name) { callback(new Errors.EINVAL('attribute name cannot be an empty string')); } else { ofd.getNode(context, get_xattr); } } function removexattr_file (context, path, name, callback) { path = normalize(path); function remove_xattr (error, node) { if (error) { return callback(error); } function update_time(error) { if(error) { callback(error); } else { update_node_times(context, path, node, { ctime: Date.now() }, callback); } } var xattrs = node.xattrs; if (!xattrs.hasOwnProperty(name)) { callback(new Errors.ENOATTR(null, path)); } else { delete xattrs[name]; context.putObject(node.id, node, update_time); } } if (typeof name !== 'string') { callback(new Errors.EINVAL('attribute name must be a string', path)); } else if (!name) { callback(new Errors.EINVAL('attribute name cannot be an empty string', path)); } else { find_node(context, path, remove_xattr); } } function fremovexattr_file (context, ofd, name, callback) { function remove_xattr (error, node) { if (error) { return callback(error); } function update_time(error) { if(error) { callback(error); } else { update_node_times(context, ofd.path, node, { ctime: Date.now() }, callback); } } var xattrs = node.xattrs; if (!xattrs.hasOwnProperty(name)) { callback(new Errors.ENOATTR()); } else { delete xattrs[name]; context.putObject(node.id, node, update_time); } } if (typeof name != 'string') { callback(new Errors.EINVAL('attribute name must be a string')); } else if (!name) { callback(new Errors.EINVAL('attribute name cannot be an empty string')); } else { ofd.getNode(context, remove_xattr); } } function validate_flags(flags) { if(!_(O_FLAGS).has(flags)) { return null; } return O_FLAGS[flags]; } function validate_file_options(options, enc, fileMode){ if(!options) { options = { encoding: enc, flag: fileMode }; } else if(typeof options === "function") { options = { encoding: enc, flag: fileMode }; } else if(typeof options === "string") { options = { encoding: options, flag: fileMode }; } return options; } function pathCheck(path, callback) { var err; if(!path) { err = new Errors.EINVAL('Path must be a string', path); } else if(isNullPath(path)) { err = new Errors.EINVAL('Path must be a string without null bytes.', path); } else if(!isAbsolutePath(path)) { err = new Errors.EINVAL('Path must be absolute.', path); } if(err) { callback(err); return false; } return true; } function open(fs, context, path, flags, mode, callback) { // NOTE: we support the same signature as node with a `mode` arg, // but ignore it. callback = arguments[arguments.length - 1]; if(!pathCheck(path, callback)) return; function check_result(error, fileNode) { if(error) { callback(error); } else { var position; if(_(flags).contains(O_APPEND)) { position = fileNode.size; } else { position = 0; } var openFileDescription = new OpenFileDescription(path, fileNode.id, flags, position); var fd = fs.allocDescriptor(openFileDescription); callback(null, fd); } } flags = validate_flags(flags); if(!flags) { callback(new Errors.EINVAL('flags is not valid'), path); } open_file(context, path, flags, check_result); } function close(fs, context, fd, callback) { if(!_(fs.openFiles).has(fd)) { callback(new Errors.EBADF()); } else { fs.releaseDescriptor(fd); callback(null); } } function mknod(fs, context, path, mode, callback) { if(!pathCheck(path, callback)) return; make_node(context, path, mode, callback); } function mkdir(fs, context, path, mode, callback) { // NOTE: we support passing a mode arg, but we ignore it internally for now. callback = arguments[arguments.length - 1]; if(!pathCheck(path, callback)) return; make_directory(context, path, callback); } function rmdir(fs, context, path, callback) { if(!pathCheck(path, callback)) return; remove_directory(context, path, callback); } function stat(fs, context, path, callback) { if(!pathCheck(path, callback)) return; function check_result(error, result) { if(error) { callback(error); } else { var stats = new Stats(result, fs.name); callback(null, stats); } } stat_file(context, path, check_result); } function fstat(fs, context, fd, callback) { function check_result(error, result) { if(error) { callback(error); } else { var stats = new Stats(result, fs.name); callback(null, stats); } } var ofd = fs.openFiles[fd]; if(!ofd) { callback(new Errors.EBADF()); } else { fstat_file(context, ofd, check_result); } } function link(fs, context, oldpath, newpath, callback) { if(!pathCheck(oldpath, callback)) return; if(!pathCheck(newpath, callback)) return; link_node(context, oldpath, newpath, callback); } function unlink(fs, context, path, callback) { if(!pathCheck(path, callback)) return; unlink_node(context, path, callback); } function read(fs, context, fd, buffer, offset, length, position, callback) { // Follow how node.js does this function wrapped_cb(err, bytesRead) { // Retain a reference to buffer so that it can't be GC'ed too soon. callback(err, bytesRead || 0, buffer); } offset = (undefined === offset) ? 0 : offset; length = (undefined === length) ? buffer.length - offset : length; callback = arguments[arguments.length - 1]; var ofd = fs.openFiles[fd]; if(!ofd) { callback(new Errors.EBADF()); } else if(!_(ofd.flags).contains(O_READ)) { callback(new Errors.EBADF('descriptor does not permit reading')); } else { read_data(context, ofd, buffer, offset, length, position, wrapped_cb); } } function readFile(fs, context, path, options, callback) { callback = arguments[arguments.length - 1]; options = validate_file_options(options, null, 'r'); if(!pathCheck(path, callback)) return; var flags = validate_flags(options.flag || 'r'); if(!flags) { return callback(new Errors.EINVAL('flags is not valid', path)); } open_file(context, path, flags, function(err, fileNode) { if(err) { return callback(err); } var ofd = new OpenFileDescription(path, fileNode.id, flags, 0); var fd = fs.allocDescriptor(ofd); function cleanup() { fs.releaseDescriptor(fd); } fstat_file(context, ofd, function(err, fstatResult) { if(err) { cleanup(); return callback(err); } var stats = new Stats(fstatResult, fs.name); if(stats.isDirectory()) { cleanup(); return callback(new Errors.EISDIR('illegal operation on directory', path)); } var size = stats.size; var buffer = new Buffer(size); buffer.fill(0); read_data(context, ofd, buffer, 0, size, 0, function(err, nbytes) { cleanup(); if(err) { return callback(err); } var data; if(options.encoding === 'utf8') { data = Encoding.decode(buffer); } else { data = buffer; } callback(null, data); }); }); }); } function write(fs, context, fd, buffer, offset, length, position, callback) { callback = arguments[arguments.length - 1]; offset = (undefined === offset) ? 0 : offset; length = (undefined === length) ? buffer.length - offset : length; var ofd = fs.openFiles[fd]; if(!ofd) { callback(new Errors.EBADF()); } else if(!_(ofd.flags).contains(O_WRITE)) { callback(new Errors.EBADF('descriptor does not permit writing')); } else if(buffer.length - offset < length) { callback(new Errors.EIO('intput buffer is too small')); } else { write_data(context, ofd, buffer, offset, length, position, callback); } } function writeFile(fs, context, path, data, options, callback) { callback = arguments[arguments.length - 1]; options = validate_file_options(options, 'utf8', 'w'); if(!pathCheck(path, callback)) return; var flags = validate_flags(options.flag || 'w'); if(!flags) { return callback(new Errors.EINVAL('flags is not valid', path)); } data = data || ''; if(typeof data === "number") { data = '' + data; } if(typeof data === "string" && options.encoding === 'utf8') { data = Encoding.encode(data); } open_file(context, path, flags, function(err, fileNode) { if(err) { return callback(err); } var ofd = new OpenFileDescription(path, fileNode.id, flags, 0); var fd = fs.allocDescriptor(ofd); replace_data(context, ofd, data, 0, data.length, function(err, nbytes) { fs.releaseDescriptor(fd); if(err) { return callback(err); } callback(null); }); }); } function appendFile(fs, context, path, data, options, callback) { callback = arguments[arguments.length - 1]; options = validate_file_options(options, 'utf8', 'a'); if(!pathCheck(path, callback)) return; var flags = validate_flags(options.flag || 'a'); if(!flags) { return callback(new Errors.EINVAL('flags is not valid', path)); } data = data || ''; if(typeof data === "number") { data = '' + data; } if(typeof data === "string" && options.encoding === 'utf8') { data = Encoding.encode(data); } open_file(context, path, flags, function(err, fileNode) { if(err) { return callback(err); } var ofd = new OpenFileDescription(path, fileNode.id, flags, fileNode.size); var fd = fs.allocDescriptor(ofd); write_data(context, ofd, data, 0, data.length, ofd.position, function(err, nbytes) { fs.releaseDescriptor(fd); if(err) { return callback(err); } callback(null); }); }); } function exists(fs, context, path, callback) { function cb(err, stats) { callback(err ? false : true); } stat(fs, context, path, cb); } function getxattr(fs, context, path, name, callback) { if (!pathCheck(path, callback)) return; getxattr_file(context, path, name, callback); } function fgetxattr(fs, context, fd, name, callback) { var ofd = fs.openFiles[fd]; if (!ofd) { callback(new Errors.EBADF()); } else { fgetxattr_file(context, ofd, name, callback); } } function setxattr(fs, context, path, name, value, flag, callback) { if(typeof flag === 'function') { callback = flag; flag = null; } if (!pathCheck(path, callback)) return; setxattr_file(context, path, name, value, flag, callback); } function fsetxattr(fs, context, fd, name, value, flag, callback) { if(typeof flag === 'function') { callback = flag; flag = null; } var ofd = fs.openFiles[fd]; if (!ofd) { callback(new Errors.EBADF()); } else if (!_(ofd.flags).contains(O_WRITE)) { callback(new Errors.EBADF('descriptor does not permit writing')); } else { fsetxattr_file(context, ofd, name, value, flag, callback); } } function removexattr(fs, context, path, name, callback) { if (!pathCheck(path, callback)) return; removexattr_file(context, path, name, callback); } function fremovexattr(fs, context, fd, name, callback) { var ofd = fs.openFiles[fd]; if (!ofd) { callback(new Errors.EBADF()); } else if (!_(ofd.flags).contains(O_WRITE)) { callback(new Errors.EBADF('descriptor does not permit writing')); } else { fremovexattr_file(context, ofd, name, callback); } } function lseek(fs, context, fd, offset, whence, callback) { function update_descriptor_position(error, stats) { if(error) { callback(error); } else { if(stats.size + offset < 0) { callback(new Errors.EINVAL('resulting file offset would be negative')); } else { ofd.position = stats.size + offset; callback(null, ofd.position); } } } var ofd = fs.openFiles[fd]; if(!ofd) { callback(new Errors.EBADF()); } if('SET' === whence) { if(offset < 0) { callback(new Errors.EINVAL('resulting file offset would be negative')); } else { ofd.position = offset; callback(null, ofd.position); } } else if('CUR' === whence) { if(ofd.position + offset < 0) { callback(new Errors.EINVAL('resulting file offset would be negative')); } else { ofd.position += offset; callback(null, ofd.position); } } else if('END' === whence) { fstat_file(context, ofd, update_descriptor_position); } else { callback(new Errors.EINVAL('whence argument is not a proper value')); } } function readdir(fs, context, path, callback) { if(!pathCheck(path, callback)) return; read_directory(context, path, callback); } function utimes(fs, context, path, atime, mtime, callback) { if(!pathCheck(path, callback)) return; var currentTime = Date.now(); atime = (atime) ? atime : currentTime; mtime = (mtime) ? mtime : currentTime; utimes_file(context, path, atime, mtime, callback); } function futimes(fs, context, fd, atime, mtime, callback) { var currentTime = Date.now(); atime = (atime) ? atime : currentTime; mtime = (mtime) ? mtime : currentTime; var ofd = fs.openFiles[fd]; if(!ofd) { callback(new Errors.EBADF()); } else if(!_(ofd.flags).contains(O_WRITE)) { callback(new Errors.EBADF('descriptor does not permit writing')); } else { futimes_file(context, ofd, atime, mtime, callback); } } function rename(fs, context, oldpath, newpath, callback) { if(!pathCheck(oldpath, callback)) return; if(!pathCheck(newpath, callback)) return; oldpath = normalize(oldpath); newpath = normalize(newpath); var oldParentPath = Path.dirname(oldpath); var newParentPath = Path.dirname(oldpath); var oldName = Path.basename(oldpath); var newName = Path.basename(newpath); var oldParentDirectory, oldParentData; var newParentDirectory, newParentData; var ctime = Date.now(); var fileNode; function update_times(error) { if(error) { callback(error); } else { update_node_times(context, newpath, fileNode, { ctime: ctime }, callback); } } function update_file_node(error, result) { if(error) { callback(error); } else { fileNode = Node.fromObject(result); // Don't think I need this here... // fileNode.nlinks += 1; fileNode.updatePathInfo(newpath, ctime); context.putObject(fileNode.id, fileNode, update_times); } } function read_new_directory(error) { if(error) { callback(error); } else { context.getObject(newParentData[newName].id, update_file_node); } } function update_old_parent_directory_data(error) { if(error) { callback(error); } else { if(oldParentDirectory.id === newParentDirectory.id) { oldParentData = newParentData; } delete oldParentData[oldName]; context.putObject(oldParentDirectory.data, oldParentData, read_new_directory); } } function update_new_parent_directory_data(error) { if(error) { callback(error); } else { newParentData[newName] = oldParentData[oldName]; context.putObject(newParentDirectory.data, newParentData, update_old_parent_directory_data); } } function check_if_new_directory_exists(error, result) { if(error) { callback(error); } else { newParentData = result; if(_(newParentData).has(newName)) { remove_directory(context, newpath, update_new_parent_directory_data); } else { update_new_parent_directory_data(); } } } function read_new_parent_directory_data(error, result) { if(error) { callback(error); } else { newParentDirectory = result; context.getObject(newParentDirectory.data, check_if_new_directory_exists); } } function get_new_parent_directory(error, result) { if(error) { callback(error); } else { oldParentData = result; find_node(context, newParentPath, read_new_parent_directory_data); } } function read_parent_directory_data(error, result) { if(error) { callback(error); } else { oldParentDirectory = result; context.getObject(result.data, get_new_parent_directory); } } function unlink_old_file(error) { if(error) { callback(error); } else { unlink_node(context, oldpath, callback); } } function check_node_type(error, node) { if(error) { callback(error); } else if(node.mode === NODE_TYPE_DIRECTORY) { find_node(context, oldParentPath, read_parent_directory_data); } else { link_node(context, oldpath, newpath, unlink_old_file); } } find_node(context, oldpath, check_node_type); } function symlink(fs, context, srcpath, dstpath, type, callback) { // NOTE: we support passing the `type` arg, but ignore it. callback = arguments[arguments.length - 1]; if(!pathCheck(srcpath, callback)) return; if(!pathCheck(dstpath, callback)) return; make_symbolic_link(context, srcpath, dstpath, callback); } function readlink(fs, context, path, callback) { if(!pathCheck(path, callback)) return; read_link(context, path, callback); } function lstat(fs, context, path, callback) { if(!pathCheck(path, callback)) return; function check_result(error, result) { if(error) { callback(error); } else { var stats = new Stats(result, fs.name); callback(null, stats); } } lstat_file(context, path, check_result); } function truncate(fs, context, path, length, callback) { // NOTE: length is optional callback = arguments[arguments.length - 1]; length = length || 0; if(!pathCheck(path, callback)) return; truncate_file(context, path, length, callback); } function ftruncate(fs, context, fd, length, callback) { // NOTE: length is optional callback = arguments[arguments.length - 1]; length = length || 0; var ofd = fs.openFiles[fd]; if(!ofd) { callback(new Errors.EBADF()); } else if(!_(ofd.flags).contains(O_WRITE)) { callback(new Errors.EBADF('descriptor does not permit writing')); } else { ftruncate_file(context, ofd, length, callback); } } module.exports = { ensureRootDirectory: ensure_root_directory, open: open, close: close, mknod: mknod, mkdir: mkdir, rmdir: rmdir, unlink: unlink, stat: stat, fstat: fstat, link: link, read: read, readFile: readFile, write: write, writeFile: writeFile, appendFile: appendFile, exists: exists, getxattr: getxattr, fgetxattr: fgetxattr, setxattr: setxattr, fsetxattr: fsetxattr, removexattr: removexattr, fremovexattr: fremovexattr, lseek: lseek, readdir: readdir, utimes: utimes, futimes: futimes, rename: rename, symlink: symlink, readlink: readlink, lstat: lstat, truncate: truncate, ftruncate: ftruncate };