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: * Code shared by OS.File front-ends. michael@0: * michael@0: * This code is meant to be included by another library. It is also meant to michael@0: * be executed only on a worker thread. michael@0: */ michael@0: michael@0: if (typeof Components != "undefined") { michael@0: throw new Error("osfile_shared_front.jsm cannot be used from the main thread"); michael@0: } michael@0: (function(exports) { michael@0: michael@0: let SharedAll = michael@0: require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm"); michael@0: let Path = require("resource://gre/modules/osfile/ospath.jsm"); michael@0: let Lz4 = michael@0: require("resource://gre/modules/workers/lz4.js"); michael@0: let LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end"); michael@0: let clone = SharedAll.clone; michael@0: michael@0: /** michael@0: * Code shared by implementations of File. michael@0: * michael@0: * @param {*} fd An OS-specific file handle. michael@0: * @param {string} path File path of the file handle, used for error-reporting. michael@0: * @constructor michael@0: */ michael@0: let AbstractFile = function AbstractFile(fd, path) { michael@0: this._fd = fd; michael@0: if (!path) { michael@0: throw new TypeError("path is expected"); michael@0: } michael@0: this._path = path; michael@0: }; michael@0: michael@0: AbstractFile.prototype = { michael@0: /** michael@0: * Return the file handle. michael@0: * michael@0: * @throw OS.File.Error if the file has been closed. michael@0: */ michael@0: get fd() { michael@0: if (this._fd) { michael@0: return this._fd; michael@0: } michael@0: throw OS.File.Error.closed("accessing file", this._path); michael@0: }, michael@0: /** michael@0: * Read bytes from this file to a new buffer. michael@0: * michael@0: * @param {number=} bytes If unspecified, read all the remaining bytes from michael@0: * this file. If specified, read |bytes| bytes, or less if the file does notclone michael@0: * contain that many bytes. michael@0: * @param {JSON} options michael@0: * @return {Uint8Array} An array containing the bytes read. michael@0: */ michael@0: read: function read(bytes, options = {}) { michael@0: options = clone(options); michael@0: options.bytes = bytes == null ? this.stat().size : bytes; michael@0: let buffer = new Uint8Array(options.bytes); michael@0: let size = this.readTo(buffer, options); michael@0: if (size == options.bytes) { michael@0: return buffer; michael@0: } else { michael@0: return buffer.subarray(0, size); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Read bytes from this file to an existing buffer. michael@0: * michael@0: * Note that, by default, this function may perform several I/O michael@0: * operations to ensure that the buffer is as full as possible. michael@0: * michael@0: * @param {Typed Array | C pointer} buffer The buffer in which to michael@0: * store the bytes. The buffer must be large enough to michael@0: * accomodate |bytes| bytes. michael@0: * @param {*=} options Optionally, an object that may contain the michael@0: * following fields: michael@0: * - {number} bytes The number of |bytes| to write from the buffer. If michael@0: * unspecified, this is |buffer.byteLength|. Note that |bytes| is required michael@0: * if |buffer| is a C pointer. michael@0: * michael@0: * @return {number} The number of bytes actually read, which may be michael@0: * less than |bytes| if the file did not contain that many bytes left. michael@0: */ michael@0: readTo: function readTo(buffer, options = {}) { michael@0: let {ptr, bytes} = SharedAll.normalizeToPointer(buffer, options.bytes); michael@0: let pos = 0; michael@0: while (pos < bytes) { michael@0: let chunkSize = this._read(ptr, bytes - pos, options); michael@0: if (chunkSize == 0) { michael@0: break; michael@0: } michael@0: pos += chunkSize; michael@0: ptr = SharedAll.offsetBy(ptr, chunkSize); michael@0: } michael@0: michael@0: return pos; michael@0: }, michael@0: michael@0: /** michael@0: * Write bytes from a buffer to this file. michael@0: * michael@0: * Note that, by default, this function may perform several I/O michael@0: * operations to ensure that the buffer is fully written. michael@0: * michael@0: * @param {Typed array | C pointer} buffer The buffer in which the michael@0: * the bytes are stored. The buffer must be large enough to michael@0: * accomodate |bytes| bytes. michael@0: * @param {*=} options Optionally, an object that may contain the michael@0: * following fields: michael@0: * - {number} bytes The number of |bytes| to write from the buffer. If michael@0: * unspecified, this is |buffer.byteLength|. Note that |bytes| is required michael@0: * if |buffer| is a C pointer. michael@0: * michael@0: * @return {number} The number of bytes actually written. michael@0: */ michael@0: write: function write(buffer, options = {}) { michael@0: michael@0: let {ptr, bytes} = michael@0: SharedAll.normalizeToPointer(buffer, options.bytes || undefined); michael@0: michael@0: let pos = 0; michael@0: while (pos < bytes) { michael@0: let chunkSize = this._write(ptr, bytes - pos, options); michael@0: pos += chunkSize; michael@0: ptr = SharedAll.offsetBy(ptr, chunkSize); michael@0: } michael@0: return pos; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Creates and opens a file with a unique name. By default, generate a random HEX number and use it to create a unique new file name. michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * @param {*=} options Additional options for file opening. This michael@0: * implementation interprets the following fields: michael@0: * michael@0: * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext. michael@0: * If |false| use HEX numbers ie: filename-A65BC0.ext michael@0: * - {number} maxReadableNumber Used to limit the amount of tries after a failed michael@0: * file creation. Default is 20. michael@0: * michael@0: * @return {Object} contains A file object{file} and the path{path}. michael@0: * @throws {OS.File.Error} If the file could not be opened. michael@0: */ michael@0: AbstractFile.openUnique = function openUnique(path, options = {}) { michael@0: let mode = { michael@0: create : true michael@0: }; michael@0: michael@0: let dirName = Path.dirname(path); michael@0: let leafName = Path.basename(path); michael@0: let lastDotCharacter = leafName.lastIndexOf('.'); michael@0: let fileName = leafName.substring(0, lastDotCharacter != -1 ? lastDotCharacter : leafName.length); michael@0: let suffix = (lastDotCharacter != -1 ? leafName.substring(lastDotCharacter) : ""); michael@0: let uniquePath = ""; michael@0: let maxAttempts = options.maxAttempts || 99; michael@0: let humanReadable = !!options.humanReadable; michael@0: const HEX_RADIX = 16; michael@0: // We produce HEX numbers between 0 and 2^24 - 1. michael@0: const MAX_HEX_NUMBER = 16777215; michael@0: michael@0: try { michael@0: return { michael@0: path: path, michael@0: file: OS.File.open(path, mode) michael@0: }; michael@0: } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { michael@0: for (let i = 0; i < maxAttempts; ++i) { michael@0: try { michael@0: if (humanReadable) { michael@0: uniquePath = Path.join(dirName, fileName + "-" + (i + 1) + suffix); michael@0: } else { michael@0: let hexNumber = Math.floor(Math.random() * MAX_HEX_NUMBER).toString(HEX_RADIX); michael@0: uniquePath = Path.join(dirName, fileName + "-" + hexNumber + suffix); michael@0: } michael@0: return { michael@0: path: uniquePath, michael@0: file: OS.File.open(uniquePath, mode) michael@0: }; michael@0: } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) { michael@0: // keep trying ... michael@0: } michael@0: } michael@0: throw OS.File.Error.exists("could not find an unused file name.", path); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Code shared by iterators. michael@0: */ michael@0: AbstractFile.AbstractIterator = function AbstractIterator() { michael@0: }; michael@0: AbstractFile.AbstractIterator.prototype = { michael@0: /** michael@0: * Allow iterating with |for| michael@0: */ michael@0: __iterator__: function __iterator__() { michael@0: return this; michael@0: }, michael@0: /** michael@0: * Apply a function to all elements of the directory sequentially. michael@0: * michael@0: * @param {Function} cb This function will be applied to all entries michael@0: * of the directory. It receives as arguments michael@0: * - the OS.File.Entry corresponding to the entry; michael@0: * - the index of the entry in the enumeration; michael@0: * - the iterator itself - calling |close| on the iterator stops michael@0: * the loop. michael@0: */ michael@0: forEach: function forEach(cb) { michael@0: let index = 0; michael@0: for (let entry in this) { michael@0: cb(entry, index++, this); michael@0: } michael@0: }, michael@0: /** michael@0: * Return several entries at once. michael@0: * michael@0: * Entries are returned in the same order as a walk with |forEach| or michael@0: * |for(...)|. michael@0: * michael@0: * @param {number=} length If specified, the number of entries michael@0: * to return. If unspecified, return all remaining entries. michael@0: * @return {Array} An array containing the next |length| entries, or michael@0: * less if the iteration contains less than |length| entries left. michael@0: */ michael@0: nextBatch: function nextBatch(length) { michael@0: let array = []; michael@0: let i = 0; michael@0: for (let entry in this) { michael@0: array.push(entry); michael@0: if (++i >= length) { michael@0: return array; michael@0: } michael@0: } michael@0: return array; michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Utility function shared by implementations of |OS.File.open|: michael@0: * extract read/write/trunc/create/existing flags from a |mode| michael@0: * object. michael@0: * michael@0: * @param {*=} mode An object that may contain fields |read|, michael@0: * |write|, |truncate|, |create|, |existing|. These fields michael@0: * are interpreted only if true-ish. michael@0: * @return {{read:bool, write:bool, trunc:bool, create:bool, michael@0: * existing:bool}} an object recapitulating the options set michael@0: * by |mode|. michael@0: * @throws {TypeError} If |mode| contains other fields, or michael@0: * if it contains both |create| and |truncate|, or |create| michael@0: * and |existing|. michael@0: */ michael@0: AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) { michael@0: let result = { michael@0: read: false, michael@0: write: false, michael@0: trunc: false, michael@0: create: false, michael@0: existing: false, michael@0: append: true michael@0: }; michael@0: for (let key in mode) { michael@0: let val = !!mode[key]; // bool cast. michael@0: switch (key) { michael@0: case "read": michael@0: result.read = val; michael@0: break; michael@0: case "write": michael@0: result.write = val; michael@0: break; michael@0: case "truncate": // fallthrough michael@0: case "trunc": michael@0: result.trunc = val; michael@0: result.write |= val; michael@0: break; michael@0: case "create": michael@0: result.create = val; michael@0: result.write |= val; michael@0: break; michael@0: case "existing": // fallthrough michael@0: case "exist": michael@0: result.existing = val; michael@0: break; michael@0: case "append": michael@0: result.append = val; michael@0: break; michael@0: default: michael@0: throw new TypeError("Mode " + key + " not understood"); michael@0: } michael@0: } michael@0: // Reject opposite modes michael@0: if (result.existing && result.create) { michael@0: throw new TypeError("Cannot specify both existing:true and create:true"); michael@0: } michael@0: if (result.trunc && result.create) { michael@0: throw new TypeError("Cannot specify both trunc:true and create:true"); michael@0: } michael@0: // Handle read/write michael@0: if (!result.write) { michael@0: result.read = true; michael@0: } michael@0: return result; michael@0: }; michael@0: michael@0: /** michael@0: * Return the contents of a file. michael@0: * michael@0: * @param {string} path The path to the file. michael@0: * @param {number=} bytes Optionally, an upper bound to the number of bytes michael@0: * to read. DEPRECATED - please use options.bytes instead. michael@0: * @param {object=} options Optionally, an object with some of the following michael@0: * fields: michael@0: * - {number} bytes An upper bound to the number of bytes to read. michael@0: * - {string} compression If "lz4" and if the file is compressed using the lz4 michael@0: * compression algorithm, decompress the file contents on the fly. michael@0: * michael@0: * @return {Uint8Array} A buffer holding the bytes michael@0: * and the number of bytes read from the file. michael@0: */ michael@0: AbstractFile.read = function read(path, bytes, options = {}) { michael@0: if (bytes && typeof bytes == "object") { michael@0: options = bytes; michael@0: bytes = options.bytes || null; michael@0: } michael@0: if ("encoding" in options && typeof options.encoding != "string") { michael@0: throw new TypeError("Invalid type for option encoding"); michael@0: } michael@0: if ("compression" in options && typeof options.compression != "string") { michael@0: throw new TypeError("Invalid type for option compression: " + options.compression); michael@0: } michael@0: if ("bytes" in options && typeof options.bytes != "number") { michael@0: throw new TypeError("Invalid type for option bytes"); michael@0: } michael@0: let file = exports.OS.File.open(path); michael@0: try { michael@0: let buffer = file.read(bytes, options); michael@0: if ("compression" in options) { michael@0: if (options.compression == "lz4") { michael@0: buffer = Lz4.decompressFileContent(buffer, options); michael@0: } else { michael@0: throw OS.File.Error.invalidArgument("Compression"); michael@0: } michael@0: } michael@0: if (!("encoding" in options)) { michael@0: return buffer; michael@0: } michael@0: let decoder; michael@0: try { michael@0: decoder = new TextDecoder(options.encoding); michael@0: } catch (ex if ex instanceof TypeError) { michael@0: throw OS.File.Error.invalidArgument("Decode"); michael@0: } michael@0: return decoder.decode(buffer); michael@0: } finally { michael@0: file.close(); michael@0: } michael@0: }; michael@0: michael@0: /** michael@0: * Write a file, atomically. michael@0: * michael@0: * By opposition to a regular |write|, this operation ensures that, michael@0: * until the contents are fully written, the destination file is michael@0: * not modified. michael@0: * michael@0: * Limitation: In a few extreme cases (hardware failure during the michael@0: * write, user unplugging disk during the write, etc.), data may be michael@0: * corrupted. If your data is user-critical (e.g. preferences, michael@0: * application data, etc.), you may wish to consider adding options michael@0: * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as michael@0: * detailed below. Note that no combination of options can be michael@0: * guaranteed to totally eliminate the risk of corruption. michael@0: * michael@0: * @param {string} path The path of the file to modify. michael@0: * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write. michael@0: * @param {*=} options Optionally, an object determining the behavior michael@0: * of this function. This object may contain the following fields: michael@0: * - {number} bytes The number of bytes to write. If unspecified, michael@0: * |buffer.byteLength|. Required if |buffer| is a C pointer. michael@0: * - {string} tmpPath If |null| or unspecified, write all data directly michael@0: * to |path|. If specified, write all data to a temporary file called michael@0: * |tmpPath| and, once this write is complete, rename the file to michael@0: * replace |path|. Performing this additional operation is a little michael@0: * slower but also a little safer. michael@0: * - {bool} noOverwrite - If set, this function will fail if a file already michael@0: * exists at |path|. michael@0: * - {bool} flush - If |false| or unspecified, return immediately once the michael@0: * write is complete. If |true|, before writing, force the operating system michael@0: * to write its internal disk buffers to the disk. This is considerably slower michael@0: * (not just for the application but for the whole system) but also safer: michael@0: * if the system shuts down improperly (typically due to a kernel freeze michael@0: * or a power failure) or if the device is disconnected before the buffer michael@0: * is flushed, the file has more chances of not being corrupted. michael@0: * - {string} compression - If empty or unspecified, do not compress the file. michael@0: * If "lz4", compress the contents of the file atomically using lz4. For the michael@0: * time being, the container format is specific to Mozilla and cannot be read michael@0: * by means other than OS.File.read(..., { compression: "lz4"}) michael@0: * - {string} backupTo - If specified, backup the destination file as |backupTo|. michael@0: * Note that this function renames the destination file before overwriting it. michael@0: * If the process or the operating system freezes or crashes michael@0: * during the short window between these operations, michael@0: * the destination file will have been moved to its backup. michael@0: * michael@0: * @return {number} The number of bytes actually written. michael@0: */ michael@0: AbstractFile.writeAtomic = michael@0: function writeAtomic(path, buffer, options = {}) { michael@0: michael@0: // Verify that path is defined and of the correct type michael@0: if (typeof path != "string" || path == "") { michael@0: throw new TypeError("File path should be a (non-empty) string"); michael@0: } michael@0: let noOverwrite = options.noOverwrite; michael@0: if (noOverwrite && OS.File.exists(path)) { michael@0: throw OS.File.Error.exists("writeAtomic", path); michael@0: } michael@0: michael@0: if (typeof buffer == "string") { michael@0: // Normalize buffer to a C buffer by encoding it michael@0: let encoding = options.encoding || "utf-8"; michael@0: buffer = new TextEncoder(encoding).encode(buffer); michael@0: } michael@0: michael@0: if ("compression" in options && options.compression == "lz4") { michael@0: buffer = Lz4.compressFileContent(buffer, options); michael@0: options = Object.create(options); michael@0: options.bytes = buffer.byteLength; michael@0: } michael@0: michael@0: let bytesWritten = 0; michael@0: michael@0: if (!options.tmpPath) { michael@0: if (options.backupTo) { michael@0: try { michael@0: OS.File.move(path, options.backupTo, {noCopy: true}); michael@0: } catch (ex if ex.becauseNoSuchFile) { michael@0: // The file doesn't exist, nothing to backup. michael@0: } michael@0: } michael@0: // Just write, without any renaming trick michael@0: let dest = OS.File.open(path, {write: true, truncate: true}); michael@0: try { michael@0: bytesWritten = dest.write(buffer, options); michael@0: if (options.flush) { michael@0: dest.flush(); michael@0: } michael@0: } finally { michael@0: dest.close(); michael@0: } michael@0: return bytesWritten; michael@0: } michael@0: michael@0: let tmpFile = OS.File.open(options.tmpPath, {write: true, truncate: true}); michael@0: try { michael@0: bytesWritten = tmpFile.write(buffer, options); michael@0: if (options.flush) { michael@0: tmpFile.flush(); michael@0: } michael@0: } catch (x) { michael@0: OS.File.remove(options.tmpPath); michael@0: throw x; michael@0: } finally { michael@0: tmpFile.close(); michael@0: } michael@0: michael@0: if (options.backupTo) { michael@0: try { michael@0: OS.File.move(path, options.backupTo, {noCopy: true}); michael@0: } catch (ex if ex.becauseNoSuchFile) { michael@0: // The file doesn't exist, nothing to backup. michael@0: } michael@0: } michael@0: michael@0: OS.File.move(options.tmpPath, path, {noCopy: true}); michael@0: return bytesWritten; michael@0: }; michael@0: michael@0: /** michael@0: * This function is used by removeDir to avoid calling lstat for each michael@0: * files under the specified directory. External callers should not call michael@0: * this function directly. michael@0: */ michael@0: AbstractFile.removeRecursive = function(path, options = {}) { michael@0: let iterator = new OS.File.DirectoryIterator(path); michael@0: if (!iterator.exists()) { michael@0: if (!("ignoreAbsent" in options) || options.ignoreAbsent) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: try { michael@0: for (let entry in iterator) { michael@0: if (entry.isDir) { michael@0: if (entry.isLink) { michael@0: // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to michael@0: // directories are directories themselves. OS.File.remove() michael@0: // will not work for them. michael@0: OS.File.removeEmptyDir(entry.path, options); michael@0: } else { michael@0: // Normal directories. michael@0: AbstractFile.removeRecursive(entry.path, options); michael@0: } michael@0: } else { michael@0: // NTFS symlinks to files, Unix symlinks, or regular files. michael@0: OS.File.remove(entry.path, options); michael@0: } michael@0: } michael@0: } finally { michael@0: iterator.close(); michael@0: } michael@0: michael@0: OS.File.removeEmptyDir(path); michael@0: }; michael@0: michael@0: /** michael@0: * Create a directory and, optionally, its parent directories. michael@0: * michael@0: * @param {string} path The name of the directory. michael@0: * @param {*=} options Additional options. michael@0: * 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 |path| michael@0: * must be a descendant of |from|, and that |from| and its existing michael@0: * subdirectories present in |path| must be user-writeable. michael@0: * Example: michael@0: * makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir }); michael@0: * creates directories profileDir/foo, profileDir/foo/bar michael@0: * - {bool} ignoreExisting If |false|, throw an error if the directory michael@0: * already exists. |true| by default. Ignored if |from| is specified. michael@0: * - {number} unixMode Under Unix, 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). Ignored under Windows michael@0: * or if the file system does not support file creation modes. michael@0: * - {C pointer} winSecurity Under Windows, if specified, security michael@0: * attributes as per winapi function |CreateDirectory|. If michael@0: * unspecified, use the default security descriptor, inherited from michael@0: * the parent directory. Ignored under Unix or if the file system michael@0: * does not support security descriptors. michael@0: */ michael@0: AbstractFile.makeDir = function(path, options = {}) { michael@0: if (!options.from) { michael@0: return OS.File._makeDir(path, options); michael@0: } michael@0: if (!path.startsWith(options.from)) { michael@0: throw new Error("Incorrect use of option |from|: " + path + " is not a descendant of " + options.from); michael@0: } michael@0: let innerOptions = Object.create(options, { michael@0: ignoreExisting: { michael@0: value: true michael@0: } michael@0: }); michael@0: // Compute the elements that appear in |path| but not in |options.from|. michael@0: let items = Path.split(path).components.slice(Path.split(options.from).components.length); michael@0: let current = options.from; michael@0: for (let item of items) { michael@0: current = Path.join(current, item); michael@0: OS.File._makeDir(current, innerOptions); michael@0: } michael@0: }; michael@0: michael@0: if (!exports.OS.Shared) { michael@0: exports.OS.Shared = {}; michael@0: } michael@0: exports.OS.Shared.AbstractFile = AbstractFile; michael@0: })(this);