michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: /** michael@0: * Synchronous front-end for the JavaScript OS.File library. michael@0: * Unix implementation. michael@0: * michael@0: * This front-end is meant to be imported by a worker thread. michael@0: */ michael@0: michael@0: { michael@0: if (typeof Components != "undefined") { michael@0: // We do not wish osfile_unix_front.jsm to be used directly as a main thread michael@0: // module yet. michael@0: michael@0: throw new Error("osfile_unix_front.jsm cannot be used from the main thread yet"); michael@0: } michael@0: (function(exports) { michael@0: "use strict"; michael@0: michael@0: // exports.OS.Unix is created by osfile_unix_back.jsm michael@0: if (exports.OS && exports.OS.File) { michael@0: return; // Avoid double-initialization michael@0: } michael@0: michael@0: let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); michael@0: let Path = require("resource://gre/modules/osfile/ospath.jsm"); michael@0: let SysAll = require("resource://gre/modules/osfile/osfile_unix_allthreads.jsm"); michael@0: exports.OS.Unix.File._init(); michael@0: let LOG = SharedAll.LOG.bind(SharedAll, "Unix front-end"); michael@0: let Const = SharedAll.Constants.libc; michael@0: let UnixFile = exports.OS.Unix.File; michael@0: let Type = UnixFile.Type; michael@0: michael@0: /** michael@0: * Representation of a file. michael@0: * michael@0: * You generally do not need to call this constructor yourself. Rather, michael@0: * to open a file, use function |OS.File.open|. michael@0: * michael@0: * @param fd A OS-specific file descriptor. michael@0: * @param {string} path File path of the file handle, used for error-reporting. michael@0: * @constructor michael@0: */ michael@0: let File = function File(fd, path) { michael@0: exports.OS.Shared.AbstractFile.call(this, fd, path); michael@0: this._closeResult = null; michael@0: }; michael@0: File.prototype = Object.create(exports.OS.Shared.AbstractFile.prototype); michael@0: michael@0: /** michael@0: * Close the file. michael@0: * michael@0: * This method has no effect if the file is already closed. However, michael@0: * if the first call to |close| has thrown an error, further calls michael@0: * will throw the same error. michael@0: * michael@0: * @throws File.Error If closing the file revealed an error that could michael@0: * not be reported earlier. michael@0: */ michael@0: File.prototype.close = function close() { michael@0: if (this._fd) { michael@0: let fd = this._fd; michael@0: this._fd = null; michael@0: // Call |close(fd)|, detach finalizer if any michael@0: // (|fd| may not be a CDataFinalizer if it has been michael@0: // instantiated from a controller thread). michael@0: let result = UnixFile._close(fd); michael@0: if (typeof fd == "object" && "forget" in fd) { michael@0: fd.forget(); michael@0: } michael@0: if (result == -1) { michael@0: this._closeResult = new File.Error("close", ctypes.errno, this._path); michael@0: } michael@0: } michael@0: if (this._closeResult) { michael@0: throw this._closeResult; michael@0: } michael@0: return; michael@0: }; michael@0: michael@0: /** michael@0: * Read some bytes from a file. michael@0: * michael@0: * @param {C pointer} buffer A buffer for holding the data michael@0: * once it is read. michael@0: * @param {number} nbytes The number of bytes to read. It must not michael@0: * exceed the size of |buffer| in bytes but it may exceed the number michael@0: * of bytes unread in the file. michael@0: * @param {*=} options Additional options for reading. Ignored in michael@0: * this implementation. michael@0: * michael@0: * @return {number} The number of bytes effectively read. If zero, michael@0: * the end of the file has been reached. michael@0: * @throws {OS.File.Error} In case of I/O error. michael@0: */ michael@0: File.prototype._read = function _read(buffer, nbytes, options = {}) { michael@0: // Populate the page cache with data from a file so the subsequent reads michael@0: // from that file will not block on disk I/O. michael@0: if (typeof(UnixFile.posix_fadvise) === 'function' && michael@0: (options.sequential || !("sequential" in options))) { michael@0: UnixFile.posix_fadvise(this.fd, 0, nbytes, michael@0: OS.Constants.libc.POSIX_FADV_SEQUENTIAL); michael@0: } michael@0: return throw_on_negative("read", michael@0: UnixFile.read(this.fd, buffer, nbytes), michael@0: this._path michael@0: ); michael@0: }; michael@0: michael@0: /** michael@0: * Write some bytes to a file. michael@0: * michael@0: * @param {C pointer} buffer A buffer holding the data that must be michael@0: * written. michael@0: * @param {number} nbytes The number of bytes to write. It must not michael@0: * exceed the size of |buffer| in bytes. michael@0: * @param {*=} options Additional options for writing. Ignored in michael@0: * this implementation. michael@0: * michael@0: * @return {number} The number of bytes effectively written. michael@0: * @throws {OS.File.Error} In case of I/O error. michael@0: */ michael@0: File.prototype._write = function _write(buffer, nbytes, options = {}) { michael@0: return throw_on_negative("write", michael@0: UnixFile.write(this.fd, buffer, nbytes), michael@0: this._path michael@0: ); michael@0: }; michael@0: michael@0: /** michael@0: * Return the current position in the file. michael@0: */ michael@0: File.prototype.getPosition = function getPosition(pos) { michael@0: return this.setPosition(0, File.POS_CURRENT); michael@0: }; michael@0: michael@0: /** michael@0: * Change the current position in the file. michael@0: * michael@0: * @param {number} pos The new position. Whether this position michael@0: * is considered from the current position, from the start of michael@0: * the file or from the end of the file is determined by michael@0: * argument |whence|. Note that |pos| may exceed the length of michael@0: * the file. michael@0: * @param {number=} whence The reference position. If omitted michael@0: * or |OS.File.POS_START|, |pos| is relative to the start of the michael@0: * file. If |OS.File.POS_CURRENT|, |pos| is relative to the michael@0: * current position in the file. If |OS.File.POS_END|, |pos| is michael@0: * relative to the end of the file. michael@0: * michael@0: * @return The new position in the file. michael@0: */ michael@0: File.prototype.setPosition = function setPosition(pos, whence) { michael@0: if (whence === undefined) { michael@0: whence = Const.SEEK_SET; michael@0: } michael@0: return throw_on_negative("setPosition", michael@0: UnixFile.lseek(this.fd, pos, whence), michael@0: this._path michael@0: ); michael@0: }; michael@0: michael@0: /** michael@0: * Fetch the information on the file. michael@0: * michael@0: * @return File.Info The information on |this| file. michael@0: */ michael@0: File.prototype.stat = function stat() { michael@0: throw_on_negative("stat", UnixFile.fstat(this.fd, gStatDataPtr), michael@0: this._path); michael@0: return new File.Info(gStatData, this._path); michael@0: }; michael@0: michael@0: /** michael@0: * Set the file's access permissions. Without any options, the michael@0: * permissions are set to an approximation of what they would michael@0: * have been if the file had been created in its current michael@0: * directory in the "most typical" fashion for the operating michael@0: * system. In the current implementation, this means we set michael@0: * the POSIX file mode to (0666 & ~umask). michael@0: * michael@0: * This operation is likely to fail if applied to a file that was michael@0: * not created by the currently running program (more precisely, michael@0: * if it was created by a program running under a different OS-level michael@0: * user account). It may also fail, or silently do nothing, if the michael@0: * filesystem containing the file does not support access permissions. michael@0: * michael@0: * @param {*=} options michael@0: * - {number} unixMode If present, the POSIX file mode is set to michael@0: * exactly this value, unless |unixHonorUmask| is michael@0: * also present. michael@0: * - {bool} unixHonorUmask If true, any |unixMode| value is modified by michael@0: * the process umask, as open() would have done. michael@0: */ michael@0: File.prototype.setPermissions = function setPermissions(options = {}) { michael@0: throw_on_negative("setPermissions", michael@0: UnixFile.fchmod(this.fd, unixMode(options)), michael@0: this._path); michael@0: }; michael@0: michael@0: /** michael@0: * Set the last access and modification date of the file. michael@0: * The time stamp resolution is 1 second at best, but might be worse michael@0: * depending on the platform. michael@0: * michael@0: * @param {Date,number=} accessDate The last access date. If numeric, michael@0: * milliseconds since epoch. If omitted or null, then the current date michael@0: * will be used. michael@0: * @param {Date,number=} modificationDate The last modification date. If michael@0: * numeric, milliseconds since epoch. If omitted or null, then the current michael@0: * date will be used. michael@0: * michael@0: * @throws {TypeError} In case of invalid parameters. michael@0: * @throws {OS.File.Error} In case of I/O error. michael@0: */ michael@0: File.prototype.setDates = function setDates(accessDate, modificationDate) { michael@0: accessDate = normalizeDate("File.prototype.setDates", accessDate); michael@0: modificationDate = normalizeDate("File.prototype.setDates", michael@0: modificationDate); michael@0: gTimevals[0].tv_sec = (accessDate / 1000) | 0; michael@0: gTimevals[0].tv_usec = 0; michael@0: gTimevals[1].tv_sec = (modificationDate / 1000) | 0; michael@0: gTimevals[1].tv_usec = 0; michael@0: throw_on_negative("setDates", michael@0: UnixFile.futimes(this.fd, gTimevalsPtr), michael@0: this._path); michael@0: }; michael@0: michael@0: /** michael@0: * Flushes the file's buffers and causes all buffered data michael@0: * to be written. michael@0: * Disk flushes are very expensive and therefore should be used carefully, michael@0: * sparingly and only in scenarios where it is vital that data survives michael@0: * system crashes. Even though the function will be executed off the michael@0: * main-thread, it might still affect the overall performance of any michael@0: * running application. michael@0: * michael@0: * @throws {OS.File.Error} In case of I/O error. michael@0: */ michael@0: File.prototype.flush = function flush() { michael@0: throw_on_negative("flush", UnixFile.fsync(this.fd), this._path); michael@0: }; michael@0: michael@0: // The default unix mode for opening (0600) michael@0: const DEFAULT_UNIX_MODE = 384; michael@0: michael@0: /** michael@0: * Open a file michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * @param {*=} mode The opening mode for the file, as michael@0: * an object that may contain the following fields: michael@0: * michael@0: * - {bool} truncate If |true|, the file will be opened michael@0: * for writing. If the file does not exist, it will be michael@0: * created. If the file exists, its contents will be michael@0: * erased. Cannot be specified with |create|. michael@0: * - {bool} create If |true|, the file will be opened michael@0: * for writing. If the file exists, this function fails. michael@0: * If the file does not exist, it will be created. Cannot michael@0: * be specified with |truncate| or |existing|. michael@0: * - {bool} existing. If the file does not exist, this function michael@0: * fails. Cannot be specified with |create|. michael@0: * - {bool} read If |true|, the file will be opened for michael@0: * reading. The file may also be opened for writing, depending michael@0: * on the other fields of |mode|. michael@0: * - {bool} write If |true|, the file will be opened for michael@0: * writing. The file may also be opened for reading, depending michael@0: * on the other fields of |mode|. michael@0: * - {bool} append If |true|, the file will be opened for appending, michael@0: * meaning the equivalent of |.setPosition(0, POS_END)| is executed michael@0: * before each write. The default is |true|, i.e. opening a file for michael@0: * appending. Specify |append: false| to open the file in regular mode. michael@0: * michael@0: * If neither |truncate|, |create| or |write| is specified, the file michael@0: * is opened for reading. michael@0: * michael@0: * Note that |false|, |null| or |undefined| flags are simply ignored. michael@0: * michael@0: * @param {*=} options Additional options for file opening. This michael@0: * implementation interprets the following fields: michael@0: * michael@0: * - {number} unixFlags If specified, file opening flags, as michael@0: * per libc function |open|. Replaces |mode|. michael@0: * - {number} unixMode If specified, a file creation mode, michael@0: * as per libc function |open|. If unspecified, files are michael@0: * created with a default mode of 0600 (file is private to the michael@0: * user, the user can read and write). michael@0: * michael@0: * @return {File} A file object. michael@0: * @throws {OS.File.Error} If the file could not be opened. michael@0: */ michael@0: File.open = function Unix_open(path, mode, options = {}) { michael@0: let omode = options.unixMode !== undefined ? michael@0: options.unixMode : DEFAULT_UNIX_MODE; michael@0: let flags; michael@0: if (options.unixFlags !== undefined) { michael@0: flags = options.unixFlags; michael@0: } else { michael@0: mode = OS.Shared.AbstractFile.normalizeOpenMode(mode); michael@0: // Handle read/write michael@0: if (!mode.write) { michael@0: flags = Const.O_RDONLY; michael@0: } else if (mode.read) { michael@0: flags = Const.O_RDWR; michael@0: } else { michael@0: flags = Const.O_WRONLY; michael@0: } michael@0: // Finally, handle create/existing/trunc michael@0: if (mode.trunc) { michael@0: if (mode.existing) { michael@0: flags |= Const.O_TRUNC; michael@0: } else { michael@0: flags |= Const.O_CREAT | Const.O_TRUNC; michael@0: } michael@0: } else if (mode.create) { michael@0: flags |= Const.O_CREAT | Const.O_EXCL; michael@0: } else if (mode.read && !mode.write) { michael@0: // flags are sufficient michael@0: } else if (!mode.existing) { michael@0: flags |= Const.O_CREAT; michael@0: } michael@0: if (mode.append) { michael@0: flags |= Const.O_APPEND; michael@0: } michael@0: } michael@0: return error_or_file(UnixFile.open(path, flags, omode), path); michael@0: }; michael@0: michael@0: /** michael@0: * Checks if a file exists michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * michael@0: * @return {bool} true if the file exists, false otherwise. michael@0: */ michael@0: File.exists = function Unix_exists(path) { michael@0: if (UnixFile.access(path, Const.F_OK) == -1) { michael@0: return false; michael@0: } else { michael@0: return true; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Remove an existing file. michael@0: * michael@0: * @param {string} path The name of the file. michael@0: * @param {*=} options Additional options. michael@0: * - {bool} ignoreAbsent If |false|, throw an error if the file does michael@0: * not exist. |true| by default. michael@0: * michael@0: * @throws {OS.File.Error} In case of I/O error. michael@0: */ michael@0: File.remove = function remove(path, options = {}) { michael@0: let result = UnixFile.unlink(path); michael@0: if (result == -1) { michael@0: if ((!("ignoreAbsent" in options) || options.ignoreAbsent) && michael@0: ctypes.errno == Const.ENOENT) { michael@0: return; michael@0: } michael@0: throw new File.Error("remove", ctypes.errno, path); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Remove an empty directory. michael@0: * michael@0: * @param {string} path The name of the directory to remove. michael@0: * @param {*=} options Additional options. michael@0: * - {bool} ignoreAbsent If |false|, throw an error if the directory michael@0: * does not exist. |true| by default michael@0: */ michael@0: File.removeEmptyDir = function removeEmptyDir(path, options = {}) { michael@0: let result = UnixFile.rmdir(path); michael@0: if (result == -1) { michael@0: if ((!("ignoreAbsent" in options) || options.ignoreAbsent) && michael@0: ctypes.errno == Const.ENOENT) { michael@0: return; michael@0: } michael@0: throw new File.Error("removeEmptyDir", ctypes.errno, path); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Gets the number of bytes available on disk to the current user. michael@0: * michael@0: * @param {string} sourcePath Platform-specific path to a directory on michael@0: * the disk to query for free available bytes. michael@0: * michael@0: * @return {number} The number of bytes available for the current user. michael@0: * @throws {OS.File.Error} In case of any error. michael@0: */ michael@0: File.getAvailableFreeSpace = function Unix_getAvailableFreeSpace(sourcePath) { michael@0: let fileSystemInfo = new Type.statvfs.implementation(); michael@0: let fileSystemInfoPtr = fileSystemInfo.address(); michael@0: michael@0: throw_on_negative("statvfs", UnixFile.statvfs(sourcePath, fileSystemInfoPtr)); michael@0: michael@0: let bytes = new Type.uint64_t.implementation( michael@0: fileSystemInfo.f_bsize * fileSystemInfo.f_bavail); michael@0: michael@0: return bytes.value; michael@0: }; michael@0: michael@0: /** michael@0: * Default mode for opening directories. michael@0: */ michael@0: const DEFAULT_UNIX_MODE_DIR = Const.S_IRWXU; michael@0: michael@0: /** michael@0: * Create a directory. michael@0: * michael@0: * @param {string} path The name of the directory. michael@0: * @param {*=} options Additional options. This michael@0: * implementation interprets the following fields: michael@0: * michael@0: * - {number} unixMode If specified, a file creation mode, michael@0: * as per libc function |mkdir|. If unspecified, dirs are michael@0: * created with a default mode of 0700 (dir is private to michael@0: * the user, the user can read, write and execute). michael@0: * - {bool} ignoreExisting If |false|, throw error if the directory michael@0: * already exists. |true| by default michael@0: * - {string} from If specified, the call to |makeDir| creates all the michael@0: * ancestors of |path| that are descendants of |from|. Note that |from| michael@0: * and its existing descendants must be user-writeable and that |path| michael@0: * must be a descendant of |from|. michael@0: * Example: michael@0: * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); michael@0: * creates directories profileDir/foo, profileDir/foo/bar michael@0: */ michael@0: File._makeDir = function makeDir(path, options = {}) { michael@0: let omode = options.unixMode !== undefined ? options.unixMode : DEFAULT_UNIX_MODE_DIR; michael@0: let result = UnixFile.mkdir(path, omode); michael@0: if (result == -1) { michael@0: if ((!("ignoreExisting" in options) || options.ignoreExisting) && michael@0: (ctypes.errno == Const.EEXIST || ctypes.errno == Const.EISDIR)) { michael@0: return; michael@0: } michael@0: throw new File.Error("makeDir", ctypes.errno, path); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Copy a file to a destination. michael@0: * michael@0: * @param {string} sourcePath The platform-specific path at which michael@0: * the file may currently be found. michael@0: * @param {string} destPath The platform-specific path at which the michael@0: * file should be copied. michael@0: * @param {*=} options An object which may contain the following fields: michael@0: * michael@0: * @option {bool} noOverwrite - If set, this function will fail if michael@0: * a file already exists at |destPath|. Otherwise, if this file exists, michael@0: * it will be erased silently. michael@0: * michael@0: * @throws {OS.File.Error} In case of any error. michael@0: * michael@0: * General note: The behavior of this function is defined only when michael@0: * it is called on a single file. If it is called on a directory, the michael@0: * behavior is undefined and may not be the same across all platforms. michael@0: * michael@0: * General note: The behavior of this function with respect to metadata michael@0: * is unspecified. Metadata may or may not be copied with the file. The michael@0: * behavior may not be the same across all platforms. michael@0: */ michael@0: File.copy = null; michael@0: michael@0: /** michael@0: * Move a file to a destination. michael@0: * michael@0: * @param {string} sourcePath The platform-specific path at which michael@0: * the file may currently be found. michael@0: * @param {string} destPath The platform-specific path at which the michael@0: * file should be moved. michael@0: * @param {*=} options An object which may contain the following fields: michael@0: * michael@0: * @option {bool} noOverwrite - If set, this function will fail if michael@0: * a file already exists at |destPath|. Otherwise, if this file exists, michael@0: * it will be erased silently. michael@0: * @option {bool} noCopy - If set, this function will fail if the michael@0: * operation is more sophisticated than a simple renaming, i.e. if michael@0: * |sourcePath| and |destPath| are not situated on the same device. michael@0: * michael@0: * @throws {OS.File.Error} In case of any error. michael@0: * michael@0: * General note: The behavior of this function is defined only when michael@0: * it is called on a single file. If it is called on a directory, the michael@0: * behavior is undefined and may not be the same across all platforms. michael@0: * michael@0: * General note: The behavior of this function with respect to metadata michael@0: * is unspecified. Metadata may or may not be moved with the file. The michael@0: * behavior may not be the same across all platforms. michael@0: */ michael@0: File.move = null; michael@0: michael@0: if (UnixFile.copyfile) { michael@0: // This implementation uses |copyfile(3)|, from the BSD library. michael@0: // Adding copying of hierarchies and/or attributes is just a flag michael@0: // away. michael@0: File.copy = function copyfile(sourcePath, destPath, options = {}) { michael@0: let flags = Const.COPYFILE_DATA; michael@0: if (options.noOverwrite) { michael@0: flags |= Const.COPYFILE_EXCL; michael@0: } michael@0: throw_on_negative("copy", michael@0: UnixFile.copyfile(sourcePath, destPath, null, flags), michael@0: sourcePath michael@0: ); michael@0: }; michael@0: } else { michael@0: // If the OS does not implement file copying for us, we need to michael@0: // implement it ourselves. For this purpose, we need to define michael@0: // a pumping function. michael@0: michael@0: /** michael@0: * Copy bytes from one file to another one. michael@0: * michael@0: * @param {File} source The file containing the data to be copied. It michael@0: * should be opened for reading. michael@0: * @param {File} dest The file to which the data should be written. It michael@0: * should be opened for writing. michael@0: * @param {*=} options An object which may contain the following fields: michael@0: * michael@0: * @option {number} nbytes The maximal number of bytes to michael@0: * copy. If unspecified, copy everything from the current michael@0: * position. michael@0: * @option {number} bufSize A hint regarding the size of the michael@0: * buffer to use for copying. The implementation may decide to michael@0: * ignore this hint. michael@0: * @option {bool} unixUserland Will force the copy operation to be michael@0: * caried out in user land, instead of using optimized syscalls such michael@0: * as splice(2). michael@0: * michael@0: * @throws {OS.File.Error} In case of error. michael@0: */ michael@0: let pump; michael@0: michael@0: // A buffer used by |pump_userland| michael@0: let pump_buffer = null; michael@0: michael@0: // An implementation of |pump| using |read|/|write| michael@0: let pump_userland = function pump_userland(source, dest, options = {}) { michael@0: let bufSize = options.bufSize > 0 ? options.bufSize : 4096; michael@0: let nbytes = options.nbytes > 0 ? options.nbytes : Infinity; michael@0: if (!pump_buffer || pump_buffer.length < bufSize) { michael@0: pump_buffer = new (ctypes.ArrayType(ctypes.char))(bufSize); michael@0: } michael@0: let read = source._read.bind(source); michael@0: let write = dest._write.bind(dest); michael@0: // Perform actual copy michael@0: let total_read = 0; michael@0: while (true) { michael@0: let chunk_size = Math.min(nbytes, bufSize); michael@0: let bytes_just_read = read(pump_buffer, bufSize); michael@0: if (bytes_just_read == 0) { michael@0: return total_read; michael@0: } michael@0: total_read += bytes_just_read; michael@0: let bytes_written = 0; michael@0: do { michael@0: bytes_written += write( michael@0: pump_buffer.addressOfElement(bytes_written), michael@0: bytes_just_read - bytes_written michael@0: ); michael@0: } while (bytes_written < bytes_just_read); michael@0: nbytes -= bytes_written; michael@0: if (nbytes <= 0) { michael@0: return total_read; michael@0: } michael@0: } michael@0: }; michael@0: michael@0: // Fortunately, under Linux, that pumping function can be optimized. michael@0: if (UnixFile.splice) { michael@0: const BUFSIZE = 1 << 17; michael@0: michael@0: // An implementation of |pump| using |splice| (for Linux/Android) michael@0: pump = function pump_splice(source, dest, options = {}) { michael@0: let nbytes = options.nbytes > 0 ? options.nbytes : Infinity; michael@0: let pipe = []; michael@0: throw_on_negative("pump", UnixFile.pipe(pipe)); michael@0: let pipe_read = pipe[0]; michael@0: let pipe_write = pipe[1]; michael@0: let source_fd = source.fd; michael@0: let dest_fd = dest.fd; michael@0: let total_read = 0; michael@0: let total_written = 0; michael@0: try { michael@0: while (true) { michael@0: let chunk_size = Math.min(nbytes, BUFSIZE); michael@0: let bytes_read = throw_on_negative("pump", michael@0: UnixFile.splice(source_fd, null, michael@0: pipe_write, null, chunk_size, 0) michael@0: ); michael@0: if (!bytes_read) { michael@0: break; michael@0: } michael@0: total_read += bytes_read; michael@0: let bytes_written = throw_on_negative( michael@0: "pump", michael@0: UnixFile.splice(pipe_read, null, michael@0: dest_fd, null, bytes_read, michael@0: (bytes_read == chunk_size)?Const.SPLICE_F_MORE:0 michael@0: )); michael@0: if (!bytes_written) { michael@0: // This should never happen michael@0: throw new Error("Internal error: pipe disconnected"); michael@0: } michael@0: total_written += bytes_written; michael@0: nbytes -= bytes_read; michael@0: if (!nbytes) { michael@0: break; michael@0: } michael@0: } michael@0: return total_written; michael@0: } catch (x) { michael@0: if (x.unixErrno == Const.EINVAL) { michael@0: // We *might* be on a file system that does not support splice. michael@0: // Try again with a fallback pump. michael@0: if (total_read) { michael@0: source.setPosition(-total_read, File.POS_CURRENT); michael@0: } michael@0: if (total_written) { michael@0: dest.setPosition(-total_written, File.POS_CURRENT); michael@0: } michael@0: return pump_userland(source, dest, options); michael@0: } michael@0: throw x; michael@0: } finally { michael@0: pipe_read.dispose(); michael@0: pipe_write.dispose(); michael@0: } michael@0: }; michael@0: } else { michael@0: // Fallback implementation of pump for other Unix platforms. michael@0: pump = pump_userland; michael@0: } michael@0: michael@0: // Implement |copy| using |pump|. michael@0: // This implementation would require some work before being able to michael@0: // copy directories michael@0: File.copy = function copy(sourcePath, destPath, options = {}) { michael@0: let source, dest; michael@0: let result; michael@0: try { michael@0: source = File.open(sourcePath); michael@0: // Need to open the output file with |append:false|, or else |splice| michael@0: // won't work. michael@0: if (options.noOverwrite) { michael@0: dest = File.open(destPath, {create:true, append:false}); michael@0: } else { michael@0: dest = File.open(destPath, {trunc:true, append:false}); michael@0: } michael@0: if (options.unixUserland) { michael@0: result = pump_userland(source, dest, options); michael@0: } else { michael@0: result = pump(source, dest, options); michael@0: } michael@0: } catch (x) { michael@0: if (dest) { michael@0: dest.close(); michael@0: } michael@0: if (source) { michael@0: source.close(); michael@0: } michael@0: throw x; michael@0: } michael@0: }; michael@0: } // End of definition of copy michael@0: michael@0: // Implement |move| using |rename| (wherever possible) or |copy| michael@0: // (if files are on distinct devices). michael@0: File.move = function move(sourcePath, destPath, options = {}) { michael@0: // An implementation using |rename| whenever possible or michael@0: // |File.pump| when required, for other Unices. michael@0: // It can move directories on one file system, not michael@0: // across file systems michael@0: michael@0: // If necessary, fail if the destination file exists michael@0: if (options.noOverwrite) { michael@0: let fd = UnixFile.open(destPath, Const.O_RDONLY, 0); michael@0: if (fd != -1) { michael@0: fd.dispose(); michael@0: // The file exists and we have access michael@0: throw new File.Error("move", Const.EEXIST, sourcePath); michael@0: } else if (ctypes.errno == Const.EACCESS) { michael@0: // The file exists and we don't have access michael@0: throw new File.Error("move", Const.EEXIST, sourcePath); michael@0: } michael@0: } michael@0: michael@0: // If we can, rename the file michael@0: let result = UnixFile.rename(sourcePath, destPath); michael@0: if (result != -1) michael@0: return; michael@0: michael@0: // If the error is not EXDEV ("not on the same device"), michael@0: // or if the error is EXDEV and we have passed an option michael@0: // that prevents us from crossing devices, throw the michael@0: // error. michael@0: if (ctypes.errno != Const.EXDEV || options.noCopy) { michael@0: throw new File.Error("move", ctypes.errno, sourcePath); michael@0: } michael@0: michael@0: // Otherwise, copy and remove. michael@0: File.copy(sourcePath, destPath, options); michael@0: // FIXME: Clean-up in case of copy error? michael@0: File.remove(sourcePath); michael@0: }; michael@0: michael@0: File.unixSymLink = function unixSymLink(sourcePath, destPath) { michael@0: throw_on_negative("symlink", UnixFile.symlink(sourcePath, destPath), michael@0: sourcePath); michael@0: }; michael@0: michael@0: /** michael@0: * Iterate on one directory. michael@0: * michael@0: * This iterator will not enter subdirectories. michael@0: * michael@0: * @param {string} path The directory upon which to iterate. michael@0: * @param {*=} options Ignored in this implementation. michael@0: * michael@0: * @throws {File.Error} If |path| does not represent a directory or michael@0: * if the directory cannot be iterated. michael@0: * @constructor michael@0: */ michael@0: File.DirectoryIterator = function DirectoryIterator(path, options) { michael@0: exports.OS.Shared.AbstractFile.AbstractIterator.call(this); michael@0: this._path = path; michael@0: this._dir = UnixFile.opendir(this._path); michael@0: if (this._dir == null) { michael@0: let error = ctypes.errno; michael@0: if (error != Const.ENOENT) { michael@0: throw new File.Error("DirectoryIterator", error, path); michael@0: } michael@0: this._exists = false; michael@0: this._closed = true; michael@0: } else { michael@0: this._exists = true; michael@0: this._closed = false; michael@0: } michael@0: }; michael@0: File.DirectoryIterator.prototype = Object.create(exports.OS.Shared.AbstractFile.AbstractIterator.prototype); michael@0: michael@0: /** michael@0: * Return the next entry in the directory, if any such entry is michael@0: * available. michael@0: * michael@0: * Skip special directories "." and "..". michael@0: * michael@0: * @return {File.Entry} The next entry in the directory. michael@0: * @throws {StopIteration} Once all files in the directory have been michael@0: * encountered. michael@0: */ michael@0: File.DirectoryIterator.prototype.next = function next() { michael@0: if (!this._exists) { michael@0: throw File.Error.noSuchFile("DirectoryIterator.prototype.next", this._path); michael@0: } michael@0: if (this._closed) { michael@0: throw StopIteration; michael@0: } michael@0: for (let entry = UnixFile.readdir(this._dir); michael@0: entry != null && !entry.isNull(); michael@0: entry = UnixFile.readdir(this._dir)) { michael@0: let contents = entry.contents; michael@0: let name = contents.d_name.readString(); michael@0: if (name == "." || name == "..") { michael@0: continue; michael@0: } michael@0: michael@0: let isDir, isSymLink; michael@0: if (!("d_type" in contents)) { michael@0: // |dirent| doesn't have d_type on some platforms (e.g. Solaris). michael@0: let path = Path.join(this._path, name); michael@0: throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr), this._path); michael@0: isDir = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFDIR; michael@0: isSymLink = (gStatData.st_mode & Const.S_IFMT) == Const.S_IFLNK; michael@0: } else { michael@0: isDir = contents.d_type == Const.DT_DIR; michael@0: isSymLink = contents.d_type == Const.DT_LNK; michael@0: } michael@0: michael@0: return new File.DirectoryIterator.Entry(isDir, isSymLink, name, this._path); michael@0: } michael@0: this.close(); michael@0: throw StopIteration; michael@0: }; michael@0: michael@0: /** michael@0: * Close the iterator and recover all resources. michael@0: * You should call this once you have finished iterating on a directory. michael@0: */ michael@0: File.DirectoryIterator.prototype.close = function close() { michael@0: if (this._closed) return; michael@0: this._closed = true; michael@0: UnixFile.closedir(this._dir); michael@0: this._dir = null; michael@0: }; michael@0: michael@0: /** michael@0: * Determine whether the directory exists. michael@0: * michael@0: * @return {boolean} michael@0: */ michael@0: File.DirectoryIterator.prototype.exists = function exists() { michael@0: return this._exists; michael@0: }; michael@0: michael@0: /** michael@0: * Return directory as |File| michael@0: */ michael@0: File.DirectoryIterator.prototype.unixAsFile = function unixAsFile() { michael@0: if (!this._dir) throw File.Error.closed("unixAsFile", this._path); michael@0: return error_or_file(UnixFile.dirfd(this._dir), this._path); michael@0: }; michael@0: michael@0: /** michael@0: * An entry in a directory. michael@0: */ michael@0: File.DirectoryIterator.Entry = function Entry(isDir, isSymLink, name, parent) { michael@0: // Copy the relevant part of |unix_entry| to ensure that michael@0: // our data is not overwritten prematurely. michael@0: this._parent = parent; michael@0: let path = Path.join(this._parent, name); michael@0: michael@0: SysAll.AbstractEntry.call(this, isDir, isSymLink, name, path); michael@0: }; michael@0: File.DirectoryIterator.Entry.prototype = Object.create(SysAll.AbstractEntry.prototype); michael@0: michael@0: /** michael@0: * Return a version of an instance of michael@0: * File.DirectoryIterator.Entry that can be sent from a worker michael@0: * thread to the main thread. Note that deserialization is michael@0: * asymmetric and returns an object with a different michael@0: * implementation. michael@0: */ michael@0: File.DirectoryIterator.Entry.toMsg = function toMsg(value) { michael@0: if (!value instanceof File.DirectoryIterator.Entry) { michael@0: throw new TypeError("parameter of " + michael@0: "File.DirectoryIterator.Entry.toMsg must be a " + michael@0: "File.DirectoryIterator.Entry"); michael@0: } michael@0: let serialized = {}; michael@0: for (let key in File.DirectoryIterator.Entry.prototype) { michael@0: serialized[key] = value[key]; michael@0: } michael@0: return serialized; michael@0: }; michael@0: michael@0: let gStatData = new Type.stat.implementation(); michael@0: let gStatDataPtr = gStatData.address(); michael@0: let gTimevals = new Type.timevals.implementation(); michael@0: let gTimevalsPtr = gTimevals.address(); michael@0: let MODE_MASK = 4095 /*= 07777*/; michael@0: File.Info = function Info(stat, path) { michael@0: let isDir = (stat.st_mode & Const.S_IFMT) == Const.S_IFDIR; michael@0: let isSymLink = (stat.st_mode & Const.S_IFMT) == Const.S_IFLNK; michael@0: let size = Type.off_t.importFromC(stat.st_size); michael@0: michael@0: let lastAccessDate = new Date(stat.st_atime * 1000); michael@0: let lastModificationDate = new Date(stat.st_mtime * 1000); michael@0: let unixLastStatusChangeDate = new Date(stat.st_ctime * 1000); michael@0: michael@0: let unixOwner = Type.uid_t.importFromC(stat.st_uid); michael@0: let unixGroup = Type.gid_t.importFromC(stat.st_gid); michael@0: let unixMode = Type.mode_t.importFromC(stat.st_mode & MODE_MASK); michael@0: michael@0: SysAll.AbstractInfo.call(this, path, isDir, isSymLink, size, michael@0: lastAccessDate, lastModificationDate, unixLastStatusChangeDate, michael@0: unixOwner, unixGroup, unixMode); michael@0: michael@0: // Some platforms (e.g. MacOS X, some BSDs) store a file creation date michael@0: if ("OSFILE_OFFSETOF_STAT_ST_BIRTHTIME" in Const) { michael@0: let date = new Date(stat.st_birthtime * 1000); michael@0: michael@0: /** michael@0: * The date of creation of this file. michael@0: * michael@0: * Note that the date returned by this method is not always michael@0: * reliable. Not all file systems are able to provide this michael@0: * information. michael@0: * michael@0: * @type {Date} michael@0: */ michael@0: this.macBirthDate = date; michael@0: } michael@0: }; michael@0: File.Info.prototype = Object.create(SysAll.AbstractInfo.prototype); michael@0: michael@0: // Deprecated, use macBirthDate/winBirthDate instead michael@0: Object.defineProperty(File.Info.prototype, "creationDate", { michael@0: get: function creationDate() { michael@0: // On the Macintosh, returns the birth date if available. michael@0: // On other Unix, as the birth date is not available, michael@0: // returns the epoch. michael@0: return this.macBirthDate || new Date(0); michael@0: } michael@0: }); michael@0: michael@0: /** michael@0: * Return a version of an instance of File.Info that can be sent michael@0: * from a worker thread to the main thread. Note that deserialization michael@0: * is asymmetric and returns an object with a different implementation. michael@0: */ michael@0: File.Info.toMsg = function toMsg(stat) { michael@0: if (!stat instanceof File.Info) { michael@0: throw new TypeError("parameter of File.Info.toMsg must be a File.Info"); michael@0: } michael@0: let serialized = {}; michael@0: for (let key in File.Info.prototype) { michael@0: serialized[key] = stat[key]; michael@0: } michael@0: return serialized; michael@0: }; michael@0: michael@0: /** michael@0: * Fetch the information on a file. michael@0: * michael@0: * @param {string} path The full name of the file to open. michael@0: * @param {*=} options Additional options. In this implementation: michael@0: * michael@0: * - {bool} unixNoFollowingLinks If set and |true|, if |path| michael@0: * represents a symbolic link, the call will return the information michael@0: * of the link itself, rather than that of the target file. michael@0: * michael@0: * @return {File.Information} michael@0: */ michael@0: File.stat = function stat(path, options = {}) { michael@0: if (options.unixNoFollowingLinks) { michael@0: throw_on_negative("stat", UnixFile.lstat(path, gStatDataPtr), path); michael@0: } else { michael@0: throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr), path); michael@0: } michael@0: return new File.Info(gStatData, path); michael@0: }; michael@0: michael@0: /** michael@0: * Set the file's access permissions. Without any options, the michael@0: * permissions are set to an approximation of what they would michael@0: * have been if the file had been created in its current michael@0: * directory in the "most typical" fashion for the operating michael@0: * system. In the current implementation, this means we set michael@0: * the POSIX file mode to (0666 & ~umask). michael@0: * michael@0: * This operation is likely to fail if applied to a file that was michael@0: * not created by the currently running program (more precisely, michael@0: * if it was created by a program running under a different OS-level michael@0: * user account). It may also fail, or silently do nothing, if the michael@0: * filesystem containing the file does not support access permissions. michael@0: * michael@0: * @param {string} path The name of the file to reset the permissions of. michael@0: * @param {*=} options michael@0: * - {number} unixMode If present, the POSIX file mode is set to michael@0: * exactly this value, unless |unixHonorUmask| is michael@0: * also present. michael@0: * - {bool} unixHonorUmask If true, any |unixMode| value is modified by michael@0: * the process umask, as open() would have done. michael@0: */ michael@0: File.setPermissions = function setPermissions(path, options = {}) { michael@0: throw_on_negative("setPermissions", michael@0: UnixFile.chmod(path, unixMode(options)), michael@0: path); michael@0: }; michael@0: michael@0: /** michael@0: * Set the last access and modification date of the file. michael@0: * The time stamp resolution is 1 second at best, but might be worse michael@0: * depending on the platform. michael@0: * michael@0: * @param {string} path The full name of the file to set the dates for. michael@0: * @param {Date,number=} accessDate The last access date. If numeric, michael@0: * milliseconds since epoch. If omitted or null, then the current date michael@0: * will be used. michael@0: * @param {Date,number=} modificationDate The last modification date. If michael@0: * numeric, milliseconds since epoch. If omitted or null, then the current michael@0: * date will be used. michael@0: * michael@0: * @throws {TypeError} In case of invalid paramters. michael@0: * @throws {OS.File.Error} In case of I/O error. michael@0: */ michael@0: File.setDates = function setDates(path, accessDate, modificationDate) { michael@0: accessDate = normalizeDate("File.setDates", accessDate); michael@0: modificationDate = normalizeDate("File.setDates", modificationDate); michael@0: gTimevals[0].tv_sec = (accessDate / 1000) | 0; michael@0: gTimevals[0].tv_usec = 0; michael@0: gTimevals[1].tv_sec = (modificationDate / 1000) | 0; michael@0: gTimevals[1].tv_usec = 0; michael@0: throw_on_negative("setDates", michael@0: UnixFile.utimes(path, gTimevalsPtr), michael@0: path); michael@0: }; michael@0: michael@0: File.read = exports.OS.Shared.AbstractFile.read; michael@0: File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic; michael@0: File.openUnique = exports.OS.Shared.AbstractFile.openUnique; michael@0: File.makeDir = exports.OS.Shared.AbstractFile.makeDir; michael@0: michael@0: /** michael@0: * Remove an existing directory and its contents. michael@0: * michael@0: * @param {string} path The name of the directory. michael@0: * @param {*=} options Additional options. michael@0: * - {bool} ignoreAbsent If |false|, throw an error if the directory doesn't michael@0: * exist. |true| by default. michael@0: * - {boolean} ignorePermissions If |true|, remove the file even when lacking write michael@0: * permission. michael@0: * michael@0: * @throws {OS.File.Error} In case of I/O error, in particular if |path| is michael@0: * not a directory. michael@0: * michael@0: * Note: This function will remove a symlink even if it points a directory. michael@0: */ michael@0: File.removeDir = function(path, options) { michael@0: let isSymLink; michael@0: try { michael@0: let info = File.stat(path, {unixNoFollowingLinks: true}); michael@0: isSymLink = info.isSymLink; michael@0: } catch (e) { michael@0: if ((!("ignoreAbsent" in options) || options.ignoreAbsent) && michael@0: ctypes.errno == Const.ENOENT) { michael@0: return; michael@0: } michael@0: throw e; michael@0: } michael@0: if (isSymLink) { michael@0: // A Unix symlink itself is not a directory even if it points michael@0: // a directory. michael@0: File.remove(path, options); michael@0: return; michael@0: } michael@0: exports.OS.Shared.AbstractFile.removeRecursive(path, options); michael@0: }; michael@0: michael@0: /** michael@0: * Get the current directory by getCurrentDirectory. michael@0: */ michael@0: File.getCurrentDirectory = function getCurrentDirectory() { michael@0: let path = UnixFile.get_current_dir_name?UnixFile.get_current_dir_name(): michael@0: UnixFile.getwd_auto(null); michael@0: throw_on_null("getCurrentDirectory", path); michael@0: return path.readString(); michael@0: }; michael@0: michael@0: /** michael@0: * Set the current directory by setCurrentDirectory. michael@0: */ michael@0: File.setCurrentDirectory = function setCurrentDirectory(path) { michael@0: throw_on_negative("setCurrentDirectory", michael@0: UnixFile.chdir(path), michael@0: path michael@0: ); michael@0: }; michael@0: michael@0: /** michael@0: * Get/set the current directory. michael@0: */ michael@0: Object.defineProperty(File, "curDir", { michael@0: set: function(path) { michael@0: this.setCurrentDirectory(path); michael@0: }, michael@0: get: function() { michael@0: return this.getCurrentDirectory(); michael@0: } michael@0: } michael@0: ); michael@0: michael@0: // Utility functions michael@0: michael@0: /** michael@0: * Turn the result of |open| into an Error or a File michael@0: * @param {number} maybe The result of the |open| operation that may michael@0: * represent either an error or a success. If -1, this function raises michael@0: * an error holding ctypes.errno, otherwise it returns the opened file. michael@0: * @param {string=} path The path of the file. michael@0: */ michael@0: function error_or_file(maybe, path) { michael@0: if (maybe == -1) { michael@0: throw new File.Error("open", ctypes.errno, path); michael@0: } michael@0: return new File(maybe, path); michael@0: } michael@0: michael@0: /** michael@0: * Utility function to sort errors represented as "-1" from successes. michael@0: * michael@0: * @param {string=} operation The name of the operation. If unspecified, michael@0: * the name of the caller function. michael@0: * @param {number} result The result of the operation that may michael@0: * represent either an error or a success. If -1, this function raises michael@0: * an error holding ctypes.errno, otherwise it returns |result|. michael@0: * @param {string=} path The path of the file. michael@0: */ michael@0: function throw_on_negative(operation, result, path) { michael@0: if (result < 0) { michael@0: throw new File.Error(operation, ctypes.errno, path); michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: /** michael@0: * Utility function to sort errors represented as |null| from successes. michael@0: * michael@0: * @param {string=} operation The name of the operation. If unspecified, michael@0: * the name of the caller function. michael@0: * @param {pointer} result The result of the operation that may michael@0: * represent either an error or a success. If |null|, this function raises michael@0: * an error holding ctypes.errno, otherwise it returns |result|. michael@0: * @param {string=} path The path of the file. michael@0: */ michael@0: function throw_on_null(operation, result, path) { michael@0: if (result == null || (result.isNull && result.isNull())) { michael@0: throw new File.Error(operation, ctypes.errno, path); michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: /** michael@0: * Normalize and verify a Date or numeric date value. michael@0: * michael@0: * @param {string} fn Function name of the calling function. michael@0: * @param {Date,number} date The date to normalize. If omitted or null, michael@0: * then the current date will be used. michael@0: * michael@0: * @throws {TypeError} Invalid date provided. michael@0: * michael@0: * @return {number} Sanitized, numeric date in milliseconds since epoch. michael@0: */ michael@0: function normalizeDate(fn, date) { michael@0: if (typeof date !== "number" && !date) { michael@0: // |date| was Omitted or null. michael@0: date = Date.now(); michael@0: } else if (typeof date.getTime === "function") { michael@0: // Input might be a date or date-like object. michael@0: date = date.getTime(); michael@0: } michael@0: michael@0: if (isNaN(date)) { michael@0: throw new TypeError("|date| parameter of " + fn + " must be a " + michael@0: "|Date| instance or number"); michael@0: } michael@0: return date; michael@0: }; michael@0: michael@0: /** michael@0: * Helper used by both versions of setPermissions. michael@0: */ michael@0: function unixMode(options) { michael@0: let mode = 438; /* 0666 */ michael@0: let unixHonorUmask = true; michael@0: if ("unixMode" in options) { michael@0: unixHonorUmask = false; michael@0: mode = options.unixMode; michael@0: } michael@0: if ("unixHonorUmask" in options) { michael@0: unixHonorUmask = options.unixHonorUmask; michael@0: } michael@0: if (unixHonorUmask) { michael@0: mode &= ~SharedAll.Constants.Sys.umask; michael@0: } michael@0: return mode; michael@0: } michael@0: michael@0: File.Unix = exports.OS.Unix.File; michael@0: File.Error = SysAll.Error; michael@0: exports.OS.File = File; michael@0: exports.OS.Shared.Type = Type; michael@0: michael@0: Object.defineProperty(File, "POS_START", { value: SysAll.POS_START }); michael@0: Object.defineProperty(File, "POS_CURRENT", { value: SysAll.POS_CURRENT }); michael@0: Object.defineProperty(File, "POS_END", { value: SysAll.POS_END }); michael@0: })(this); michael@0: }