Bug 866571 - [OS.File] Add createUnique method. r=yoric
authorMarcos Aruj <marcos@appcoast.com>
Wed, 16 Oct 2013 08:21:39 -0400
changeset 164795 ae104231ede7d3c54b867b2a39238c7370ce1455
parent 164794 cea994e07f34d6acd53396faffd4c56f31e8bffd
child 164796 3e103997950b87db99481a54b3bbe942ca6b0b9f
push id3066
push userakeybl@mozilla.com
push dateMon, 09 Dec 2013 19:58:46 +0000
treeherdermozilla-beta@a31a0dce83aa [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersyoric
bugs866571
milestone27.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 866571 - [OS.File] Add createUnique method. r=yoric
toolkit/components/osfile/modules/osfile_async_front.jsm
toolkit/components/osfile/modules/osfile_async_worker.js
toolkit/components/osfile/modules/osfile_shared_front.jsm
toolkit/components/osfile/modules/osfile_unix_front.jsm
toolkit/components/osfile/modules/osfile_win_front.jsm
toolkit/components/osfile/tests/xpcshell/test_unique.js
toolkit/components/osfile/tests/xpcshell/xpcshell.ini
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -503,16 +503,45 @@ File.open = function open(path, mode, op
   ).then(
     function onSuccess(msg) {
       return new File(msg);
     }
   );
 };
 
 /**
+ * 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.
+ */
+File.openUnique = function openUnique(path, options) {
+  return Scheduler.post(
+      "openUnique", [Type.path.toMsg(path), options],
+      path
+    ).then(
+    function onSuccess(msg) {
+      return {
+        path: msg.path,
+        file: new File(msg.file)
+      };
+    }
+  );
+};
+
+/**
  * Get the information on the file.
  *
  * @return {promise}
  * @resolves {OS.File.Info}
  * @rejects {OS.Error}
  */
 File.stat = function stat(path) {
   return Scheduler.post(
--- a/toolkit/components/osfile/modules/osfile_async_worker.js
+++ b/toolkit/components/osfile/modules/osfile_async_worker.js
@@ -265,16 +265,30 @@ if (this.Components) {
      let filePath = Type.path.fromMsg(path);
      let file = File.open(filePath, mode, options);
      return OpenedFiles.add(file, {
        // Adding path information to keep track of opened files
        // to report leaks when debugging.
        path: filePath
      });
    },
+   openUnique: function openUnique(path, options) {
+     let filePath = Type.path.fromMsg(path);
+     let openedFile = OS.Shared.AbstractFile.openUnique(filePath, options);
+     let resourceId = OpenedFiles.add(openedFile.file, {
+       // Adding path information to keep track of opened files
+       // to report leaks when debugging.
+       path: openedFile.path
+     });
+
+     return {
+       path: openedFile.path,
+       file: resourceId
+     };
+   },
    read: function read(path, bytes, options) {
      let data = File.read(Type.path.fromMsg(path), bytes, options);
      return new Transfer({buffer: data.buffer, byteOffset: data.byteOffset, byteLength: data.byteLength}, [data.buffer]);
    },
    exists: function exists(path) {
      return File.exists(Type.path.fromMsg(path));
    },
    writeAtomic: function writeAtomic(path, buffer, options) {
--- a/toolkit/components/osfile/modules/osfile_shared_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_shared_front.jsm
@@ -123,16 +123,74 @@ AbstractFile.prototype = {
       pos += chunkSize;
       ptr = exports.OS.Shared.offsetBy(ptr, 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 = OS.Path.dirname(path);
+  let leafName = OS.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 = OS.Path.join(dirName, fileName + "-" + (i + 1) + suffix);
+        } else {
+          let hexNumber = Math.floor(Math.random() * MAX_HEX_NUMBER).toString(HEX_RADIX);
+          uniquePath = OS.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.");
+  }
+};
+
+/**
  * Utility function used to normalize a Typed Array or C
  * pointer into a uint8_t C pointer.
  *
  * Future versions might extend this to other data structures.
  *
  * @param {Typed array | C pointer} candidate The buffer. If
  * a C pointer, it must be non-null.
  * @param {number} bytes The number of bytes that |candidate| should contain.
--- a/toolkit/components/osfile/modules/osfile_unix_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_unix_front.jsm
@@ -819,16 +819,17 @@
        } else {
          throw_on_negative("stat", UnixFile.stat(path, gStatDataPtr));
        }
        return new File.Info(gStatData);
      };
 
      File.read = exports.OS.Shared.AbstractFile.read;
      File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic;
+     File.openUnique = exports.OS.Shared.AbstractFile.openUnique;
      File.removeDir = exports.OS.Shared.AbstractFile.removeDir;
 
      /**
       * Get the current directory by getCurrentDirectory.
       */
      File.getCurrentDirectory = function getCurrentDirectory() {
        let path = UnixFile.get_current_dir_name?UnixFile.get_current_dir_name():
          UnixFile.getwd_auto(null);
--- a/toolkit/components/osfile/modules/osfile_win_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_win_front.jsm
@@ -751,16 +751,17 @@
        winAccess: 0,
        // Directories can only be opened with backup semantics(!)
        winFlags: OS.Constants.Win.FILE_FLAG_BACKUP_SEMANTICS,
        winDisposition: OS.Constants.Win.OPEN_EXISTING
      };
 
      File.read = exports.OS.Shared.AbstractFile.read;
      File.writeAtomic = exports.OS.Shared.AbstractFile.writeAtomic;
+     File.openUnique = exports.OS.Shared.AbstractFile.openUnique;
      File.removeDir = exports.OS.Shared.AbstractFile.removeDir;
 
      /**
       * Get the current directory by getCurrentDirectory.
       */
      File.getCurrentDirectory = function getCurrentDirectory() {
            // This function is more complicated than one could hope.
            //
new file mode 100644
--- /dev/null
+++ b/toolkit/components/osfile/tests/xpcshell/test_unique.js
@@ -0,0 +1,90 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+function run_test() {
+  run_next_test();
+}
+
+function testFiles(filename) {
+  return Task.spawn(function() {
+    const MAX_TRIES = 10;
+    let currentDir = yield OS.File.getCurrentDirectory();
+    let path = OS.Path.join(currentDir, filename);
+    let exists = yield OS.File.exists(path);
+    // Check a file with the same name doesn't exist already
+    do_check_false(exists);
+
+    // Ensure that openUnique() uses the file name if there is no file with that name already.
+    let openedFile = yield OS.File.openUnique(path);
+    do_print("\nCreate new file: " + openedFile.path);
+    yield openedFile.file.close();
+    exists = yield OS.File.exists(openedFile.path);
+    do_check_true(exists);
+    do_check_eq(path, openedFile.path);
+    let fileInfo = yield OS.File.stat(openedFile.path);
+    do_check_true(fileInfo.size == 0);
+
+    // Ensure that openUnique() creates a new file name using a HEX number, as the original name is already taken.
+    openedFile = yield OS.File.openUnique(path);
+    do_print("\nCreate unique HEX file: " + openedFile.path);
+    yield openedFile.file.close();
+    exists = yield OS.File.exists(openedFile.path);
+    do_check_true(exists);
+    let fileInfo = yield OS.File.stat(openedFile.path);
+    do_check_true(fileInfo.size == 0);
+
+    // Ensure that openUnique() generates different file names each time, using the HEX number algorithm
+    let filenames = new Set();
+    for (let i=0; i < MAX_TRIES; i++) {
+      openedFile = yield OS.File.openUnique(path);
+      yield openedFile.file.close();
+      filenames.add(openedFile.path);
+    }
+
+    do_check_eq(filenames.size, MAX_TRIES);
+
+    // Ensure that openUnique() creates a new human readable file name using, as the original name is already taken.
+    openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+    do_print("\nCreate unique Human Readable file: " + openedFile.path);
+    yield openedFile.file.close();
+    exists = yield OS.File.exists(openedFile.path);
+    do_check_true(exists);
+    let fileInfo = yield OS.File.stat(openedFile.path);
+    do_check_true(fileInfo.size == 0);
+
+    // Ensure that openUnique() generates different human readable file names each time
+    filenames = new Set();
+    for (let i=0; i < MAX_TRIES; i++) {
+      openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+      yield openedFile.file.close();
+      filenames.add(openedFile.path);
+    }
+
+    do_check_eq(filenames.size, MAX_TRIES);
+
+    let exn;
+    try {
+      for (let i=0; i < 100; i++) {
+        openedFile = yield OS.File.openUnique(path, {humanReadable : true});
+        yield openedFile.file.close();
+      }
+    } catch (ex) {
+      exn = ex;
+    }
+
+    do_print("Ensure that this raises the correct error");
+    do_check_true(!!exn);
+    do_check_true(exn instanceof OS.File.Error);
+    do_check_true(exn.becauseExists);
+  });
+}
+
+add_task(function test_unique() {
+  OS.Shared.DEBUG = true;
+  // Tests files with extension
+  yield testFiles("dummy_unique_file.txt");
+  // Tests files with no extension
+  yield testFiles("dummy_unique_file_no_ext");
+});
--- a/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/osfile/tests/xpcshell/xpcshell.ini
@@ -7,8 +7,9 @@ tail =
 [test_osfile_async.js]
 [test_osfile_async_bytes.js]
 [test_profiledir.js]
 [test_logging.js]
 [test_creationDate.js]
 [test_exception.js]
 [test_path_constants.js]
 [test_removeDir.js]
+[test_unique.js]