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