toolkit/components/osfile/modules/osfile_shared_front.jsm

Fri, 16 Jan 2015 18:13:44 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Fri, 16 Jan 2015 18:13:44 +0100
branch
TOR_BUG_9701
changeset 14
925c144e1f1f
permissions
-rw-r--r--

Integrate suggestion from review to improve consistency with existing code.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
     3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 /**
     6  * Code shared by OS.File front-ends.
     7  *
     8  * This code is meant to be included by another library. It is also meant to
     9  * be executed only on a worker thread.
    10  */
    12 if (typeof Components != "undefined") {
    13   throw new Error("osfile_shared_front.jsm cannot be used from the main thread");
    14 }
    15 (function(exports) {
    17 let SharedAll =
    18   require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
    19 let Path = require("resource://gre/modules/osfile/ospath.jsm");
    20 let Lz4 =
    21   require("resource://gre/modules/workers/lz4.js");
    22 let LOG = SharedAll.LOG.bind(SharedAll, "Shared front-end");
    23 let clone = SharedAll.clone;
    25 /**
    26  * Code shared by implementations of File.
    27  *
    28  * @param {*} fd An OS-specific file handle.
    29  * @param {string} path File path of the file handle, used for error-reporting.
    30  * @constructor
    31  */
    32 let AbstractFile = function AbstractFile(fd, path) {
    33   this._fd = fd;
    34   if (!path) {
    35     throw new TypeError("path is expected");
    36   }
    37   this._path = path;
    38 };
    40 AbstractFile.prototype = {
    41   /**
    42    * Return the file handle.
    43    *
    44    * @throw OS.File.Error if the file has been closed.
    45    */
    46   get fd() {
    47     if (this._fd) {
    48       return this._fd;
    49     }
    50     throw OS.File.Error.closed("accessing file", this._path);
    51   },
    52   /**
    53    * Read bytes from this file to a new buffer.
    54    *
    55    * @param {number=} bytes If unspecified, read all the remaining bytes from
    56    * this file. If specified, read |bytes| bytes, or less if the file does notclone
    57    * contain that many bytes.
    58    * @param {JSON} options
    59    * @return {Uint8Array} An array containing the bytes read.
    60    */
    61   read: function read(bytes, options = {}) {
    62     options = clone(options);
    63     options.bytes = bytes == null ? this.stat().size : bytes;
    64     let buffer = new Uint8Array(options.bytes);
    65     let size = this.readTo(buffer, options);
    66     if (size == options.bytes) {
    67       return buffer;
    68     } else {
    69       return buffer.subarray(0, size);
    70     }
    71   },
    73   /**
    74    * Read bytes from this file to an existing buffer.
    75    *
    76    * Note that, by default, this function may perform several I/O
    77    * operations to ensure that the buffer is as full as possible.
    78    *
    79    * @param {Typed Array | C pointer} buffer The buffer in which to
    80    * store the bytes. The buffer must be large enough to
    81    * accomodate |bytes| bytes.
    82    * @param {*=} options Optionally, an object that may contain the
    83    * following fields:
    84    * - {number} bytes The number of |bytes| to write from the buffer. If
    85    * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
    86    * if |buffer| is a C pointer.
    87    *
    88    * @return {number} The number of bytes actually read, which may be
    89    * less than |bytes| if the file did not contain that many bytes left.
    90    */
    91   readTo: function readTo(buffer, options = {}) {
    92     let {ptr, bytes} = SharedAll.normalizeToPointer(buffer, options.bytes);
    93     let pos = 0;
    94     while (pos < bytes) {
    95       let chunkSize = this._read(ptr, bytes - pos, options);
    96       if (chunkSize == 0) {
    97         break;
    98       }
    99       pos += chunkSize;
   100       ptr = SharedAll.offsetBy(ptr, chunkSize);
   101     }
   103     return pos;
   104   },
   106   /**
   107    * Write bytes from a buffer to this file.
   108    *
   109    * Note that, by default, this function may perform several I/O
   110    * operations to ensure that the buffer is fully written.
   111    *
   112    * @param {Typed array | C pointer} buffer The buffer in which the
   113    * the bytes are stored. The buffer must be large enough to
   114    * accomodate |bytes| bytes.
   115    * @param {*=} options Optionally, an object that may contain the
   116    * following fields:
   117    * - {number} bytes The number of |bytes| to write from the buffer. If
   118    * unspecified, this is |buffer.byteLength|. Note that |bytes| is required
   119    * if |buffer| is a C pointer.
   120    *
   121    * @return {number} The number of bytes actually written.
   122    */
   123   write: function write(buffer, options = {}) {
   125     let {ptr, bytes} =
   126       SharedAll.normalizeToPointer(buffer, options.bytes || undefined);
   128     let pos = 0;
   129     while (pos < bytes) {
   130       let chunkSize = this._write(ptr, bytes - pos, options);
   131       pos += chunkSize;
   132       ptr = SharedAll.offsetBy(ptr, chunkSize);
   133     }
   134     return pos;
   135   }
   136 };
   138 /**
   139  * 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.
   140  *
   141  * @param {string} path The path to the file.
   142  * @param {*=} options Additional options for file opening. This
   143  * implementation interprets the following fields:
   144  *
   145  * - {number} humanReadable If |true|, create a new filename appending a decimal number. ie: filename-1.ext, filename-2.ext.
   146  *  If |false| use HEX numbers ie: filename-A65BC0.ext
   147  * - {number} maxReadableNumber Used to limit the amount of tries after a failed
   148  *  file creation. Default is 20.
   149  *
   150  * @return {Object} contains A file object{file} and the path{path}.
   151  * @throws {OS.File.Error} If the file could not be opened.
   152  */
   153 AbstractFile.openUnique = function openUnique(path, options = {}) {
   154   let mode = {
   155     create : true
   156   };
   158   let dirName = Path.dirname(path);
   159   let leafName = Path.basename(path);
   160   let lastDotCharacter = leafName.lastIndexOf('.');
   161   let fileName = leafName.substring(0, lastDotCharacter != -1 ? lastDotCharacter : leafName.length);
   162   let suffix = (lastDotCharacter != -1 ? leafName.substring(lastDotCharacter) : "");
   163   let uniquePath = "";
   164   let maxAttempts = options.maxAttempts || 99;
   165   let humanReadable = !!options.humanReadable;
   166   const HEX_RADIX = 16;
   167   // We produce HEX numbers between 0 and 2^24 - 1.
   168   const MAX_HEX_NUMBER = 16777215;
   170   try {
   171     return {
   172       path: path,
   173       file: OS.File.open(path, mode)
   174     };
   175   } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
   176     for (let i = 0; i < maxAttempts; ++i) {
   177       try {
   178         if (humanReadable) {
   179           uniquePath = Path.join(dirName, fileName + "-" + (i + 1) + suffix);
   180         } else {
   181           let hexNumber = Math.floor(Math.random() * MAX_HEX_NUMBER).toString(HEX_RADIX);
   182           uniquePath = Path.join(dirName, fileName + "-" + hexNumber + suffix);
   183         }
   184         return {
   185           path: uniquePath,
   186           file: OS.File.open(uniquePath, mode)
   187         };
   188       } catch (ex if ex instanceof OS.File.Error && ex.becauseExists) {
   189         // keep trying ...
   190       }
   191     }
   192     throw OS.File.Error.exists("could not find an unused file name.", path);
   193   }
   194 };
   196 /**
   197  * Code shared by iterators.
   198  */
   199 AbstractFile.AbstractIterator = function AbstractIterator() {
   200 };
   201 AbstractFile.AbstractIterator.prototype = {
   202   /**
   203    * Allow iterating with |for|
   204    */
   205   __iterator__: function __iterator__() {
   206     return this;
   207   },
   208   /**
   209    * Apply a function to all elements of the directory sequentially.
   210    *
   211    * @param {Function} cb This function will be applied to all entries
   212    * of the directory. It receives as arguments
   213    *  - the OS.File.Entry corresponding to the entry;
   214    *  - the index of the entry in the enumeration;
   215    *  - the iterator itself - calling |close| on the iterator stops
   216    *   the loop.
   217    */
   218   forEach: function forEach(cb) {
   219     let index = 0;
   220     for (let entry in this) {
   221       cb(entry, index++, this);
   222     }
   223   },
   224   /**
   225    * Return several entries at once.
   226    *
   227    * Entries are returned in the same order as a walk with |forEach| or
   228    * |for(...)|.
   229    *
   230    * @param {number=} length If specified, the number of entries
   231    * to return. If unspecified, return all remaining entries.
   232    * @return {Array} An array containing the next |length| entries, or
   233    * less if the iteration contains less than |length| entries left.
   234    */
   235   nextBatch: function nextBatch(length) {
   236     let array = [];
   237     let i = 0;
   238     for (let entry in this) {
   239       array.push(entry);
   240       if (++i >= length) {
   241         return array;
   242       }
   243     }
   244     return array;
   245   }
   246 };
   248 /**
   249  * Utility function shared by implementations of |OS.File.open|:
   250  * extract read/write/trunc/create/existing flags from a |mode|
   251  * object.
   252  *
   253  * @param {*=} mode An object that may contain fields |read|,
   254  * |write|, |truncate|, |create|, |existing|. These fields
   255  * are interpreted only if true-ish.
   256  * @return {{read:bool, write:bool, trunc:bool, create:bool,
   257  * existing:bool}} an object recapitulating the options set
   258  * by |mode|.
   259  * @throws {TypeError} If |mode| contains other fields, or
   260  * if it contains both |create| and |truncate|, or |create|
   261  * and |existing|.
   262  */
   263 AbstractFile.normalizeOpenMode = function normalizeOpenMode(mode) {
   264   let result = {
   265     read: false,
   266     write: false,
   267     trunc: false,
   268     create: false,
   269     existing: false,
   270     append: true
   271   };
   272   for (let key in mode) {
   273     let val = !!mode[key]; // bool cast.
   274     switch (key) {
   275     case "read":
   276       result.read = val;
   277       break;
   278     case "write":
   279       result.write = val;
   280       break;
   281     case "truncate": // fallthrough
   282     case "trunc":
   283       result.trunc = val;
   284       result.write |= val;
   285       break;
   286     case "create":
   287       result.create = val;
   288       result.write |= val;
   289       break;
   290     case "existing": // fallthrough
   291     case "exist":
   292       result.existing = val;
   293       break;
   294     case "append":
   295       result.append = val;
   296       break;
   297     default:
   298       throw new TypeError("Mode " + key + " not understood");
   299     }
   300   }
   301   // Reject opposite modes
   302   if (result.existing && result.create) {
   303     throw new TypeError("Cannot specify both existing:true and create:true");
   304   }
   305   if (result.trunc && result.create) {
   306     throw new TypeError("Cannot specify both trunc:true and create:true");
   307   }
   308   // Handle read/write
   309   if (!result.write) {
   310     result.read = true;
   311   }
   312   return result;
   313 };
   315 /**
   316  * Return the contents of a file.
   317  *
   318  * @param {string} path The path to the file.
   319  * @param {number=} bytes Optionally, an upper bound to the number of bytes
   320  * to read. DEPRECATED - please use options.bytes instead.
   321  * @param {object=} options Optionally, an object with some of the following
   322  * fields:
   323  * - {number} bytes An upper bound to the number of bytes to read.
   324  * - {string} compression If "lz4" and if the file is compressed using the lz4
   325  *   compression algorithm, decompress the file contents on the fly.
   326  *
   327  * @return {Uint8Array} A buffer holding the bytes
   328  * and the number of bytes read from the file.
   329  */
   330 AbstractFile.read = function read(path, bytes, options = {}) {
   331   if (bytes && typeof bytes == "object") {
   332     options = bytes;
   333     bytes = options.bytes || null;
   334   }
   335   if ("encoding" in options && typeof options.encoding != "string") {
   336     throw new TypeError("Invalid type for option encoding");
   337   }
   338   if ("compression" in options && typeof options.compression != "string") {
   339     throw new TypeError("Invalid type for option compression: " + options.compression);
   340   }
   341   if ("bytes" in options && typeof options.bytes != "number") {
   342     throw new TypeError("Invalid type for option bytes");
   343   }
   344   let file = exports.OS.File.open(path);
   345   try {
   346     let buffer = file.read(bytes, options);
   347     if ("compression" in options) {
   348       if (options.compression == "lz4") {
   349         buffer = Lz4.decompressFileContent(buffer, options);
   350       } else {
   351         throw OS.File.Error.invalidArgument("Compression");
   352       }
   353     }
   354     if (!("encoding" in options)) {
   355       return buffer;
   356     }
   357     let decoder;
   358     try {
   359       decoder = new TextDecoder(options.encoding);
   360     } catch (ex if ex instanceof TypeError) {
   361       throw OS.File.Error.invalidArgument("Decode");
   362     }
   363     return decoder.decode(buffer);
   364   } finally {
   365     file.close();
   366   }
   367 };
   369 /**
   370  * Write a file, atomically.
   371  *
   372  * By opposition to a regular |write|, this operation ensures that,
   373  * until the contents are fully written, the destination file is
   374  * not modified.
   375  *
   376  * Limitation: In a few extreme cases (hardware failure during the
   377  * write, user unplugging disk during the write, etc.), data may be
   378  * corrupted. If your data is user-critical (e.g. preferences,
   379  * application data, etc.), you may wish to consider adding options
   380  * |tmpPath| and/or |flush| to reduce the likelihood of corruption, as
   381  * detailed below. Note that no combination of options can be
   382  * guaranteed to totally eliminate the risk of corruption.
   383  *
   384  * @param {string} path The path of the file to modify.
   385  * @param {Typed Array | C pointer} buffer A buffer containing the bytes to write.
   386  * @param {*=} options Optionally, an object determining the behavior
   387  * of this function. This object may contain the following fields:
   388  * - {number} bytes The number of bytes to write. If unspecified,
   389  * |buffer.byteLength|. Required if |buffer| is a C pointer.
   390  * - {string} tmpPath If |null| or unspecified, write all data directly
   391  * to |path|. If specified, write all data to a temporary file called
   392  * |tmpPath| and, once this write is complete, rename the file to
   393  * replace |path|. Performing this additional operation is a little
   394  * slower but also a little safer.
   395  * - {bool} noOverwrite - If set, this function will fail if a file already
   396  * exists at |path|.
   397  * - {bool} flush - If |false| or unspecified, return immediately once the
   398  * write is complete. If |true|, before writing, force the operating system
   399  * to write its internal disk buffers to the disk. This is considerably slower
   400  * (not just for the application but for the whole system) but also safer:
   401  * if the system shuts down improperly (typically due to a kernel freeze
   402  * or a power failure) or if the device is disconnected before the buffer
   403  * is flushed, the file has more chances of not being corrupted.
   404  * - {string} compression - If empty or unspecified, do not compress the file.
   405  * If "lz4", compress the contents of the file atomically using lz4. For the
   406  * time being, the container format is specific to Mozilla and cannot be read
   407  * by means other than OS.File.read(..., { compression: "lz4"})
   408  * - {string} backupTo - If specified, backup the destination file as |backupTo|.
   409  * Note that this function renames the destination file before overwriting it.
   410  * If the process or the operating system freezes or crashes
   411  * during the short window between these operations,
   412  * the destination file will have been moved to its backup.
   413  *
   414  * @return {number} The number of bytes actually written.
   415  */
   416 AbstractFile.writeAtomic =
   417      function writeAtomic(path, buffer, options = {}) {
   419   // Verify that path is defined and of the correct type
   420   if (typeof path != "string" || path == "") {
   421     throw new TypeError("File path should be a (non-empty) string");
   422   }
   423   let noOverwrite = options.noOverwrite;
   424   if (noOverwrite && OS.File.exists(path)) {
   425     throw OS.File.Error.exists("writeAtomic", path);
   426   }
   428   if (typeof buffer == "string") {
   429     // Normalize buffer to a C buffer by encoding it
   430     let encoding = options.encoding || "utf-8";
   431     buffer = new TextEncoder(encoding).encode(buffer);
   432   }
   434   if ("compression" in options && options.compression == "lz4") {
   435     buffer = Lz4.compressFileContent(buffer, options);
   436     options = Object.create(options);
   437     options.bytes = buffer.byteLength;
   438   }
   440   let bytesWritten = 0;
   442   if (!options.tmpPath) {
   443     if (options.backupTo) {
   444       try {
   445         OS.File.move(path, options.backupTo, {noCopy: true});
   446       } catch (ex if ex.becauseNoSuchFile) {
   447         // The file doesn't exist, nothing to backup.
   448       }
   449     }
   450     // Just write, without any renaming trick
   451     let dest = OS.File.open(path, {write: true, truncate: true});
   452     try {
   453       bytesWritten = dest.write(buffer, options);
   454       if (options.flush) {
   455         dest.flush();
   456       }
   457     } finally {
   458       dest.close();
   459     }
   460     return bytesWritten;
   461   }
   463   let tmpFile = OS.File.open(options.tmpPath, {write: true, truncate: true});
   464   try {
   465     bytesWritten = tmpFile.write(buffer, options);
   466     if (options.flush) {
   467       tmpFile.flush();
   468     }
   469   } catch (x) {
   470     OS.File.remove(options.tmpPath);
   471     throw x;
   472   } finally {
   473     tmpFile.close();
   474   }
   476   if (options.backupTo) {
   477     try {
   478       OS.File.move(path, options.backupTo, {noCopy: true});
   479     } catch (ex if ex.becauseNoSuchFile) {
   480       // The file doesn't exist, nothing to backup.
   481     }
   482   }
   484   OS.File.move(options.tmpPath, path, {noCopy: true});
   485   return bytesWritten;
   486 };
   488 /**
   489  * This function is used by removeDir to avoid calling lstat for each
   490  * files under the specified directory. External callers should not call
   491  * this function directly.
   492  */
   493 AbstractFile.removeRecursive = function(path, options = {}) {
   494   let iterator = new OS.File.DirectoryIterator(path);
   495   if (!iterator.exists()) {
   496     if (!("ignoreAbsent" in options) || options.ignoreAbsent) {
   497       return;
   498     }
   499   }
   501   try {
   502     for (let entry in iterator) {
   503       if (entry.isDir) {
   504         if (entry.isLink) {
   505           // Unlike Unix symlinks, NTFS junctions or NTFS symlinks to
   506           // directories are directories themselves. OS.File.remove()
   507           // will not work for them.
   508           OS.File.removeEmptyDir(entry.path, options);
   509         } else {
   510           // Normal directories.
   511           AbstractFile.removeRecursive(entry.path, options);
   512         }
   513       } else {
   514         // NTFS symlinks to files, Unix symlinks, or regular files.
   515         OS.File.remove(entry.path, options);
   516       }
   517     }
   518   } finally {
   519     iterator.close();
   520   }
   522   OS.File.removeEmptyDir(path);
   523 };
   525 /**
   526  * Create a directory and, optionally, its parent directories.
   527  *
   528  * @param {string} path The name of the directory.
   529  * @param {*=} options Additional options.
   530  *
   531  * - {string} from If specified, the call to |makeDir| creates all the
   532  * ancestors of |path| that are descendants of |from|. Note that |path|
   533  * must be a descendant of |from|, and that |from| and its existing
   534  * subdirectories present in |path|  must be user-writeable.
   535  * Example:
   536  *   makeDir(Path.join(profileDir, "foo", "bar"), { from: profileDir });
   537  *  creates directories profileDir/foo, profileDir/foo/bar
   538  * - {bool} ignoreExisting If |false|, throw an error if the directory
   539  * already exists. |true| by default. Ignored if |from| is specified.
   540  * - {number} unixMode Under Unix, if specified, a file creation mode,
   541  * as per libc function |mkdir|. If unspecified, dirs are
   542  * created with a default mode of 0700 (dir is private to
   543  * the user, the user can read, write and execute). Ignored under Windows
   544  * or if the file system does not support file creation modes.
   545  * - {C pointer} winSecurity Under Windows, if specified, security
   546  * attributes as per winapi function |CreateDirectory|. If
   547  * unspecified, use the default security descriptor, inherited from
   548  * the parent directory. Ignored under Unix or if the file system
   549  * does not support security descriptors.
   550  */
   551 AbstractFile.makeDir = function(path, options = {}) {
   552   if (!options.from) {
   553     return OS.File._makeDir(path, options);
   554   }
   555   if (!path.startsWith(options.from)) {
   556     throw new Error("Incorrect use of option |from|: " + path + " is not a descendant of " + options.from);
   557   }
   558   let innerOptions = Object.create(options, {
   559     ignoreExisting: {
   560       value: true
   561     }
   562   });
   563   // Compute the elements that appear in |path| but not in |options.from|.
   564   let items = Path.split(path).components.slice(Path.split(options.from).components.length);
   565   let current = options.from;
   566   for (let item of items) {
   567     current = Path.join(current, item);
   568     OS.File._makeDir(current, innerOptions);
   569   }
   570 };
   572 if (!exports.OS.Shared) {
   573   exports.OS.Shared = {};
   574 }
   575 exports.OS.Shared.AbstractFile = AbstractFile;
   576 })(this);

mercurial