Bug 1653985: Implement IOUtils::stat method r=barret,Gijs
authorKeefer Rourke <krourke@mozilla.com>
Thu, 23 Jul 2020 18:15:30 +0000
changeset 541811 cf217b49d014efd5b82557b6bb9d85788852dcfd
parent 541810 486d9f1ad967a1e30a5fedeb35337459ec791542
child 541812 f25c770ccbf3cd6876f06f9b1940461064c1b64e
push id37633
push userccoroiu@mozilla.com
push dateFri, 24 Jul 2020 09:32:06 +0000
treeherdermozilla-central@141543043270 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbarret, Gijs
bugs1653985
milestone80.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 1653985: Implement IOUtils::stat method r=barret,Gijs Differential Revision: https://phabricator.services.mozilla.com/D84408
dom/chrome-webidl/IOUtils.webidl
dom/system/IOUtils.cpp
dom/system/IOUtils.h
dom/system/tests/file_ioutils_worker.js
dom/system/tests/test_ioutils.html
--- a/dom/chrome-webidl/IOUtils.webidl
+++ b/dom/chrome-webidl/IOUtils.webidl
@@ -47,16 +47,25 @@ namespace IOUtils {
    */
   Promise<void> remove(DOMString path, optional RemoveOptions options = {});
   /**
    * Creates a new directory at |path| according to |options|.
    *
    * @param path An absolute file path identifying the directory to create.
    */
   Promise<void> makeDirectory(DOMString path, optional MakeDirectoryOptions options = {});
+  /**
+   * Obtains information about a file, such as size, modification dates, etc.
+   *
+   * @param path An absolute file path identifying the file or directory to
+   *             inspect.
+   *
+   * @see FileInfo
+   */
+  Promise<FileInfo> stat(DOMString path);
 };
 
 /**
  * Options to be passed to the |IOUtils.writeAtomic| method.
  */
 dictionary WriteAtomicOptions {
   /**
    * If specified, backup the destination file to this path before writing.
@@ -114,8 +123,39 @@ dictionary MakeDirectoryOptions {
    */
   boolean createAncestors = true;
   /**
    * If true, succeed even if the directory already exists (default behavior).
    * Otherwise, fail if the directory already exists.
    */
   boolean ignoreExisting = true;
 };
+
+/**
+ * Types of files that are recognized by the |IOUtils.stat| method.
+ */
+enum FileType { "regular", "directory", "other" };
+
+/**
+ * Basic metadata about a file.
+ */
+dictionary FileInfo {
+  /**
+   * The absolute path to the file on disk, as known when this file info was
+   * obtained.
+   */
+  DOMString path;
+  /**
+   * Identifies if the file at |path| is a regular file, directory, or something
+   * something else.
+   */
+  FileType type;
+  /**
+   * If this represents a regular file, the size of the file in bytes.
+   * Otherwise, -1.
+   */
+  long long size;
+  /**
+   * The timestamp of the last file modification, represented in milliseconds
+   * since Epoch (1970-01-01T00:00:00.000Z).
+   */
+  long long lastModified;
+};
--- a/dom/system/IOUtils.cpp
+++ b/dom/system/IOUtils.cpp
@@ -568,16 +568,73 @@ already_AddRefed<Promise> IOUtils::MakeD
                 promise->MaybeRejectWithOperationError(
                     "Target exists and is not a directory");
                 break;
               default:
                 promise->MaybeRejectWithUnknownError(FormatErrorMessage(
                     aError, "Unexpected error creating directory"));
             }
           });
+  return promise.forget();
+}
+
+already_AddRefed<Promise> IOUtils::Stat(GlobalObject& aGlobal,
+                                        const nsAString& aPath) {
+  RefPtr<Promise> promise = CreateJSPromise(aGlobal);
+  REJECT_IF_SHUTTING_DOWN(promise);
+
+  // Do the IO on a background thread and return the result to this thread.
+  RefPtr<nsISerialEventTarget> bg = GetBackgroundEventTarget();
+  REJECT_IF_NULL_EVENT_TARGET(bg, promise);
+
+  // Process arguments.
+  if (!IsAbsolutePath(aPath)) {
+    promise->MaybeRejectWithOperationError(
+        "Only absolute file paths are permitted");
+    return promise.forget();
+  }
+
+  InvokeAsync(
+      bg, __func__,
+      [path = nsAutoString(aPath)]() {
+        MOZ_ASSERT(!NS_IsMainThread());
+
+        auto rv = StatSync(path);
+        if (rv.isErr()) {
+          return IOStatMozPromise::CreateAndReject(rv.propagateErr(), __func__);
+        }
+        return IOStatMozPromise::CreateAndResolve(rv.unwrap(), __func__);
+      })
+      ->Then(
+          GetCurrentSerialEventTarget(), __func__,
+          [promise = RefPtr(promise)](const InternalFileInfo& aInfo) {
+            AutoJSAPI jsapi;
+            if (NS_WARN_IF(!jsapi.Init(promise->GetGlobalObject()))) {
+              promise->MaybeReject(NS_ERROR_UNEXPECTED);
+              return;
+            }
+            FileInfo jsResult;
+            jsResult.mPath.Construct(aInfo.mPath);
+            jsResult.mType.Construct(aInfo.mType);
+            jsResult.mSize.Construct(aInfo.mSize);
+            jsResult.mLastModified.Construct(aInfo.mLastModified);
+            promise->MaybeResolve(jsResult);
+          },
+          [promise = RefPtr(promise)](const nsresult& aError) {
+            switch (aError) {
+              case NS_ERROR_FILE_TARGET_DOES_NOT_EXIST:
+              case NS_ERROR_FILE_NOT_FOUND:
+                promise->MaybeRejectWithNotFoundError(
+                    "Target file does not exist");
+                break;
+              default:
+                promise->MaybeRejectWithUnknownError(FormatErrorMessage(
+                    aError, "Unexpected error accessing file"));
+            }
+          });
 
   return promise.forget();
 }
 
 /* static */
 already_AddRefed<nsISerialEventTarget> IOUtils::GetBackgroundEventTarget() {
   if (sShutdownStarted) {
     return nullptr;
@@ -863,16 +920,51 @@ nsresult IOUtils::CreateDirectorySync(co
     }
     if (aIgnoreExisting) {
       return NS_OK;
     }
   }
   return rv;
 }
 
+Result<IOUtils::InternalFileInfo, nsresult> IOUtils::StatSync(
+    const nsAString& aPath) {
+  MOZ_ASSERT(!NS_IsMainThread());
+
+  RefPtr<nsLocalFile> file = new nsLocalFile();
+  MOZ_TRY(file->InitWithPath(aPath));
+
+  InternalFileInfo info;
+  info.mPath = nsString(aPath);
+
+  info.mType = FileType::Regular;
+  bool isRegular;
+  MOZ_TRY(file->IsFile(&isRegular));
+  if (!isRegular) {
+    bool isDir;
+    MOZ_TRY(file->IsDirectory(&isDir));
+    if (isDir) {
+      info.mType = FileType::Directory;
+    } else {
+      info.mType = FileType::Other;
+    }
+  }
+
+  int64_t size = -1;
+  if (info.mType == FileType::Regular) {
+    MOZ_TRY(file->GetFileSize(&size));
+  }
+  info.mSize = size;
+  PRTime lastModified = 0;
+  MOZ_TRY(file->GetLastModifiedTime(&lastModified));
+  info.mLastModified = static_cast<int64_t>(lastModified);
+
+  return info;
+}
+
 NS_IMPL_ISUPPORTS(IOUtilsShutdownBlocker, nsIAsyncShutdownBlocker);
 
 NS_IMETHODIMP IOUtilsShutdownBlocker::GetName(nsAString& aName) {
   aName = u"IOUtils Blocker"_ns;
   return NS_OK;
 }
 
 NS_IMETHODIMP IOUtilsShutdownBlocker::BlockShutdown(
--- a/dom/system/IOUtils.h
+++ b/dom/system/IOUtils.h
@@ -60,22 +60,26 @@ class IOUtils final {
   static already_AddRefed<Promise> Remove(GlobalObject& aGlobal,
                                           const nsAString& aPath,
                                           const RemoveOptions& aOptions);
 
   static already_AddRefed<Promise> MakeDirectory(
       GlobalObject& aGlobal, const nsAString& aPath,
       const MakeDirectoryOptions& aOptions);
 
+  static already_AddRefed<Promise> Stat(GlobalObject& aGlobal,
+                                        const nsAString& aPath);
+
   static bool IsAbsolutePath(const nsAString& aPath);
 
  private:
   ~IOUtils() = default;
 
   friend class IOUtilsShutdownBlocker;
+  struct InternalFileInfo;
 
   static StaticDataMutex<StaticRefPtr<nsISerialEventTarget>>
       sBackgroundEventTarget;
   static StaticRefPtr<nsIAsyncShutdownClient> sBarrier;
   static Atomic<bool> sShutdownStarted;
 
   static already_AddRefed<nsIAsyncShutdownClient> GetShutdownBarrier();
 
@@ -133,27 +137,49 @@ class IOUtils final {
    *                          system umask to compute the best mode for the new
    *                          directory.
    */
   static nsresult CreateDirectorySync(const nsAString& aPath,
                                       bool aCreateAncestors,
                                       bool aIgnoreExisting,
                                       int32_t aMode = 0777);
 
+  static Result<IOUtils::InternalFileInfo, nsresult> StatSync(
+      const nsAString& aPath);
+
   using IOReadMozPromise =
       mozilla::MozPromise<nsTArray<uint8_t>, const nsCString,
                           /* IsExclusive */ true>;
 
   using IOWriteMozPromise =
       mozilla::MozPromise<uint32_t, const nsCString, /* IsExclusive */ true>;
 
+  using IOStatMozPromise =
+      mozilla::MozPromise<struct InternalFileInfo, const nsresult,
+                          /* IsExclusive */ true>;
+
   using IOMozPromise = mozilla::MozPromise<bool /* ignored */, const nsresult,
                                            /* IsExclusive */ true>;
 };
 
+/**
+ * This is an easier to work with representation of a |mozilla::dom::FileInfo|
+ * for private use in the IOUtils implementation.
+ *
+ * Because web IDL dictionaries are not easily copy/moveable, this class is
+ * used instead, until converted to the proper |mozilla::dom::FileInfo| before
+ * returning any results to JavaScript.
+ */
+struct IOUtils::InternalFileInfo {
+  nsString mPath;
+  FileType mType;
+  uint64_t mSize;
+  uint64_t mLastModified;
+};
+
 class IOUtilsShutdownBlocker : public nsIAsyncShutdownBlocker {
  public:
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSIASYNCSHUTDOWNBLOCKER
 
  private:
   virtual ~IOUtilsShutdownBlocker() = default;
 };
--- a/dom/system/tests/file_ioutils_worker.js
+++ b/dom/system/tests/file_ioutils_worker.js
@@ -64,17 +64,17 @@ self.onmessage = async function(msg) {
   async function test_move_file() {
     const src = OS.Path.join(tmpDir, "test_move_file_src.tmp");
     const dest = OS.Path.join(tmpDir, "test_move_file_dest.tmp");
     const bytes = Uint8Array.of(...new Array(50).keys());
     await self.IOUtils.writeAtomic(src, bytes);
 
     await self.IOUtils.move(src, dest);
     ok(
-      !OS.File.exists(src) && OS.File.exists(dest),
+      !(await fileExists(src)) && (await fileExists(dest)),
       "IOUtils::move can move files from a worker"
     );
 
     await cleanup(dest);
   }
 
   async function test_make_directory() {
     const dir = OS.Path.join(tmpDir, "test_make_dir.tmp.d");
@@ -85,13 +85,31 @@ self.onmessage = async function(msg) {
     );
 
     await cleanup(dir);
   }
 
   async function cleanup(...files) {
     for (const file of files) {
       await self.IOUtils.remove(file, { ignoreAbsent: true, recursive: true });
-      const exists = OS.File.exists(file);
+      const exists = await fileOrDirExists(file);
       ok(!exists, `Removed temporary file: ${file}`);
     }
   }
+
+  async function fileOrDirExists(location) {
+    try {
+      await self.IOUtils.stat(location);
+      return true;
+    } catch (ex) {
+      return false;
+    }
+  }
+
+  async function fileExists(location) {
+    try {
+      let { type } = await self.IOUtils.stat(location);
+      return type === "regular";
+    } catch (ex) {
+      return false;
+    }
+  }
 };
--- a/dom/system/tests/test_ioutils.html
+++ b/dom/system/tests/test_ioutils.html
@@ -110,20 +110,20 @@
       ok(await fileExists(tmpFileName), `Expected file ${tmpFileName} to exist`);
 
       await window.IOUtils.remove(tmpFileName);
       ok(!await fileExists(tmpFileName), "IOUtils::remove can remove files");
 
       info("Test creating and removing an empty directory");
       const tmpDirName = OS.Path.join(tmpDir, "test_ioutils_create_and_remove.tmp.d");
       await window.IOUtils.makeDirectory(tmpDirName);
-      ok(await OS.File.exists(tmpDirName), `Expected directory ${tmpDirName} to exist`);
+      ok(await dirExists(tmpDirName), `Expected directory ${tmpDirName} to exist`);
 
       await window.IOUtils.remove(tmpDirName);
-      ok(!await OS.File.exists(tmpDirName), "IOUtils::remove can remove empty directories");
+      ok(!await dirExists(tmpDirName), "IOUtils::remove can remove empty directories");
     });
 
     add_task(async function test_remove_non_existing() {
       const tmpFileName = OS.Path.join(tmpDir, "test_ioutil_remove_non_existing.tmp");
       ok(!await fileExists(tmpFileName), `Expected file ${tmpFileName} not to exist`);
 
       await window.IOUtils.remove(tmpFileName, { ignoreAbsent: true });
       ok(!await fileExists(tmpFileName), "IOUtils::remove can ignore missing files without error");
@@ -157,17 +157,17 @@
       await Assert.rejects(
         window.IOUtils.remove(tmpParentDir, { recursive: false }),
         /Could not remove non-empty directory.*/,
         "IOUtils::remove fails if non-recursively removing directory with contents"
       );
 
       await window.IOUtils.remove(tmpParentDir, { recursive: true });
       ok(
-        !await OS.File.exists(tmpParentDir),
+        !await dirExists(tmpParentDir),
         "IOUtils::remove can recursively remove a directory"
       );
     });
 
     add_task(async function test_write_no_overwrite() {
       // Make a new file, and try to write to it with overwrites disabled.
       const tmpFileName = OS.Path.join(tmpDir, "test_ioutils_overwrite.tmp");
       const untouchableContents = new TextEncoder().encode("Can't touch this!\n");
@@ -469,41 +469,41 @@
     add_task(async function test_move_to_dir() {
       // Set up.
       info("Test move and rename to non-existing directory");
       const tmpFileName = OS.Path.join(tmpDir, "test_move_to_dir.tmp");
       const destDir = OS.Path.join(tmpDir, "test_move_to_dir.tmp.d");
       const dest = OS.Path.join(destDir, "dest.tmp");
       await createFile(tmpFileName);
       // Test.
-      ok(!await OS.File.exists(destDir), "Expected path not to exist");
+      ok(!await fileOrDirExists(destDir), "Expected path not to exist");
       await window.IOUtils.move(tmpFileName, dest);
       ok(
         !await fileExists(tmpFileName) && await fileExists(dest),
         "IOUtils::move creates non-existing parents if needed"
       );
 
       // Set up.
       info("Test move and rename to existing directory.")
       await createFile(tmpFileName);
       // Test.
-      ok(await OS.File.exists(destDir), "Expected path to exist");
+      ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
       await window.IOUtils.move(tmpFileName, dest);
       ok(
         !await fileExists(tmpFileName)
         && await fileExists(dest),
         "IOUtils::move can move/rename a file into an existing dir"
       );
 
       // Set up.
       info("Test move to existing directory without specifying leaf name.")
       await createFile(tmpFileName);
       // Test.
       await window.IOUtils.move(tmpFileName, destDir);
-      ok(await OS.File.exists(destDir), "Expected path to exist");
+      ok(await dirExists(destDir), `Expected ${destDir} to be a directory`);
       ok(
         !await fileExists(tmpFileName)
         && await fileExists(OS.Path.join(destDir, OS.Path.basename(tmpFileName))),
         "IOUtils::move can move a file into an existing dir"
       );
 
       // Clean up.
       await cleanup(destDir);
@@ -513,31 +513,31 @@
       // Set up.
       info("Test rename an empty directory");
       const srcDir = OS.Path.join(tmpDir, "test_move_dir.tmp.d");
       const destDir = OS.Path.join(tmpDir, "test_move_dir_dest.tmp.d");
       await createDir(srcDir);
       // Test.
       await window.IOUtils.move(srcDir, destDir);
       ok(
-        !await OS.File.exists(srcDir) && await OS.File.exists(destDir),
+        !await fileOrDirExists(srcDir) && await dirExists(destDir),
         "IOUtils::move can rename directories"
       );
 
       // Set up.
       info("Test move directory and its content into another directory");
       await createDir(srcDir);
       await createFile(OS.Path.join(srcDir, "file.tmp"), "foo");
       // Test.
       await window.IOUtils.move(srcDir, destDir);
       const destFile = OS.Path.join(destDir, OS.Path.basename(srcDir), "file.tmp");
       ok(
-        !await OS.File.exists(srcDir)
-        && await OS.File.exists(destDir)
-        && await OS.File.exists(OS.Path.join(destDir, OS.Path.basename(srcDir)))
+        !await fileOrDirExists(srcDir)
+        && await dirExists(destDir)
+        && await dirExists(OS.Path.join(destDir, OS.Path.basename(srcDir)))
         && await fileHasTextContents(destFile, "foo"),
         "IOUtils::move can move a directory and its contents into another one"
       )
 
       // Clean up.
       await cleanup(destDir);
     });
 
@@ -569,16 +569,74 @@
         /Source is a directory but destination is not/,
         "IOUtils::move throws if try to move dir into an existing file"
       );
 
       // Clean up.
       await cleanup(destFile, srcDir);
     });
 
+    add_task(async function test_stat() {
+      info("Test attempt to stat a regular empty file");
+      const emptyFileName = OS.Path.join(tmpDir, "test_stat_empty.tmp");
+      await createFile(emptyFileName);
+
+      const emptyFileInfo = await window.IOUtils.stat(emptyFileName);
+      is(emptyFileInfo.size, 0, "IOUtils::stat can get correct (empty) file size");
+      is(emptyFileInfo.path, emptyFileName, "IOUtils::stat result contains the path");
+      is(emptyFileInfo.type, "regular", "IOUtils::stat can stat regular (empty) files");
+      Assert.less(
+        (emptyFileInfo.lastModified - new Date().valueOf()),
+        1000, // Allow for 1 second deviation in case of slow tests.
+        "IOUtils::stat can get the last modification date for a regular file"
+      );
+
+      info("Test attempt to stat a regular binary file");
+      const tempFileName = OS.Path.join(tmpDir, "test_stat_binary.tmp");
+      const bytes = Uint8Array.of(...new Array(50).keys());
+      await createFile(tempFileName, bytes);
+
+      const fileInfo = await window.IOUtils.stat(tempFileName);
+      is(fileInfo.size, 50, "IOUtils::stat can get correct file size");
+      is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path");
+      is(fileInfo.type, "regular", "IOUtils::stat can stat regular files");
+      Assert.less(
+        (fileInfo.lastModified - new Date().valueOf()),
+        1000, // Allow for 1 second deviation in case of slow tests.
+        "IOUtils::stat can get the last modification date for a regular file"
+      );
+
+      info("Test attempt to stat a directory");
+      const tempDirName = OS.Path.join(tmpDir, "test_stat_dir.tmp.d");
+      await OS.File.makeDir(tempDirName);
+
+      const dirInfo = await window.IOUtils.stat(tempDirName);
+      is(dirInfo.size, -1, "IOUtils::stat reports -1 size for directories")
+      is(fileInfo.path, tempFileName, "IOUtils::stat result contains the path");
+      is(fileInfo.type, "regular", "IOUtils::stat can stat directories");
+      Assert.less(
+        (fileInfo.lastModified - new Date().valueOf()),
+        1000, // Allow for 1 second deviation in case of slow tests.
+        "IOUtils::stat can get the last modification date for a regular file"
+      );
+
+      await cleanup(emptyFileName, tempFileName, tempFileName)
+    });
+
+    add_task(async function test_stat_failures() {
+      info("Test attempt to stat a non-existing file");
+      const notExistsFile = OS.Path.join(tmpDir, "test_stat_not_exists.tmp");
+
+      await Assert.rejects(
+        window.IOUtils.stat(notExistsFile),
+        /Target file does not exist/,
+        "IOUtils::stat throws if the target file does not exist"
+      );
+    });
+
 
     // Utility functions.
 
     Uint8Array.prototype.equals = function equals(other) {
       if (this.byteLength !== other.byteLength) return false;
       return this.every((val, i) => val === other[i]);
     }
 
@@ -613,27 +671,45 @@
       info(`Opening ${location} for reading`);
       const bytes = await window.IOUtils.read(location);
       const contents = new TextDecoder().decode(bytes);
       return contents === expectedContents;
     }
 
     async function fileExists(file) {
       try {
-        await window.IOUtils.read(file);
+        let { type } = await window.IOUtils.stat(file);
+        return type === "regular";
       } catch (ex) {
         return false;
       }
-      return true;
+    }
+
+    async function dirExists(dir) {
+      try {
+        let { type } = await window.IOUtils.stat(dir);
+        return type === "directory";
+      } catch (ex) {
+        return false;
+      }
+    }
+
+    async function fileOrDirExists(location) {
+      try {
+        await window.IOUtils.stat(location);
+        return true;
+      } catch (ex) {
+        return false;
+      }
     }
 
     async function cleanup(...files) {
       for (const file of files) {
         await window.IOUtils.remove(file, { ignoreAbsent: true, recursive: true });
-        const exists = await fileExists(file);
+        const exists = await fileOrDirExists(file);
         ok(!exists, `Removed temporary file: ${file}`);
       }
     }
 
   </script>
 </head>
 
 <body>