Bug 1261693 - Implement HTMLInputElement::GetFiles, r=smaug
☠☠ backed out by e94aa4bc6f1a ☠ ☠
authorAndrea Marchesini <amarchesini@mozilla.com>
Mon, 23 May 2016 17:00:45 +0200
changeset 323022 77c60a79313d1f2733e4b71a223e085f3086476f
parent 323021 14e6d851e527c191e7bd03bf4417f86634dc4558
child 323023 17e20404362d916a9034c6f67895748878e599dd
push id9671
push userraliiev@mozilla.com
push dateMon, 06 Jun 2016 20:27:52 +0000
treeherdermozilla-aurora@cea65ca3d0bd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1261693
milestone49.0a1
Bug 1261693 - Implement HTMLInputElement::GetFiles, r=smaug
dom/filesystem/tests/test_basic.html
dom/html/HTMLInputElement.cpp
dom/html/HTMLInputElement.h
dom/webidl/HTMLInputElement.webidl
--- a/dom/filesystem/tests/test_basic.html
+++ b/dom/filesystem/tests/test_basic.html
@@ -72,16 +72,65 @@ function test_duplicateGetFilesAndDirect
     script.destroy();
     next();
   }
 
   script.addMessageListener("dir.opened", onOpened);
   script.sendAsyncMessage("dir.open", { path: 'test' });
 }
 
+function test_inputGetFiles() {
+  var url = SimpleTest.getTestFileURL("script_fileList.js");
+  var script = SpecialPowers.loadChromeScript(url);
+
+  function onOpened(message) {
+    var fileList = document.getElementById('fileList');
+    SpecialPowers.wrap(fileList).mozSetDirectory(message.dir);
+
+    fileList.getFilesAndDirectories()
+    .then(function(result) {
+       is(result.length, 1, "getFilesAndDirectories should return 1 element");
+       ok(result[0] instanceof Directory, "getFilesAndDirectories should return 1 directory");
+
+      return fileList.getFiles(false);
+    })
+    .then(function(result) {
+      is(result.length, 1, "getFiles should return 1 element");
+      ok(result[0] instanceof File, "getFile should return 1 file");
+      is(result[0].name, 'foo.txt', "getFiles()[0].name should be 'foo.txt'");
+      is(result[0].webkitRelativePath, '/foo.txt', "getFiles()[0].webkitRelativePath should be '/foo.txt'");
+
+      return fileList.getFiles(true);
+    })
+    .then(function(result) {
+      is(result.length, 2, "getFiles should return 2 elements");
+
+      function checkFile(file) {
+        ok(file instanceof File, "getFile[x] should return a file");
+        if (file.name == 'foo.txt') {
+          is(file.webkitRelativePath, '/foo.txt', "getFiles()[x].webkitRelativePath should be '/foo.txt'");
+        } else {
+          is(file.name, 'bar.txt', "getFiles()[x].name should be 'bar.txt'");
+          is(file.webkitRelativePath, '/subdir/bar.txt', "getFiles()[x].webkitRelativePath should be '/subdir/bar.txt'");
+        }
+      }
+
+      checkFile(result[0]);
+      checkFile(result[1]);
+    })
+    .then(function() {
+      script.destroy();
+      next();
+    });
+  }
+
+  script.addMessageListener("dir.opened", onOpened);
+  script.sendAsyncMessage("dir.open", { path: 'test' });
+}
+
 var tests = [
   function() { setup_tests(next); },
 
   function() { create_fileList('ProfD') },
   function() { test_basic(directory, next); },
   function() { test_getFilesAndDirectories(directory, true, next); },
   function() { test_getFiles(directory, false, next); },
   function() { test_getFiles(directory, true, next); },
@@ -90,16 +139,17 @@ var tests = [
   function() { test_getFiles_recursiveComparison(directory, next); },
 
   function() { create_fileList('root'); },
   function() { test_basic(directory, next); },
   function() { test_getFilesAndDirectories(directory, false, next); },
   function() { test_getFiles(directory, false, next); },
 
   test_duplicateGetFilesAndDirectories,
+  test_inputGetFiles,
   test_simpleFilePicker
 ];
 
 function next() {
   if (!tests.length) {
     SimpleTest.finish();
     return;
   }
--- a/dom/html/HTMLInputElement.cpp
+++ b/dom/html/HTMLInputElement.cpp
@@ -213,16 +213,328 @@ const Decimal HTMLInputElement::kStepAny
   0x23e2,                                          \
   0x4479,                                          \
   {0xb5, 0x13, 0x7b, 0x36, 0x93, 0x43, 0xe3, 0xa0} \
 }
 
 #define PROGRESS_STR "progress"
 static const uint32_t kProgressEventInterval = 50; // ms
 
+// Retrieving the list of files can be very time/IO consuming. We use this
+// helper class to do it just once.
+class GetFilesHelper final : public Runnable
+{
+public:
+  static already_AddRefed<GetFilesHelper>
+  Create(nsIGlobalObject* aGlobal,
+         const nsTArray<OwningFileOrDirectory>& aFilesOrDirectory,
+         bool aRecursiveFlag, ErrorResult& aRv)
+  {
+    MOZ_ASSERT(aGlobal);
+
+    RefPtr<GetFilesHelper> helper = new GetFilesHelper(aGlobal, aRecursiveFlag);
+
+    nsAutoString directoryPath;
+
+    for (uint32_t i = 0; i < aFilesOrDirectory.Length(); ++i) {
+      const OwningFileOrDirectory& data = aFilesOrDirectory[i];
+      if (data.IsFile()) {
+        if (!helper->mFiles.AppendElement(data.GetAsFile(), fallible)) {
+          aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+          return nullptr;
+        }
+      } else {
+        MOZ_ASSERT(data.IsDirectory());
+
+        // We support the upload of only 1 top-level directory from our
+        // directory picker. This means that we cannot have more than 1
+        // Directory object in aFilesOrDirectory array.
+        MOZ_ASSERT(directoryPath.IsEmpty());
+
+        RefPtr<Directory> directory = data.GetAsDirectory();
+        MOZ_ASSERT(directory);
+
+        aRv = directory->GetFullRealPath(directoryPath);
+        if (NS_WARN_IF(aRv.Failed())) {
+          return nullptr;
+        }
+      }
+    }
+
+    // No directories to explore.
+    if (directoryPath.IsEmpty()) {
+      helper->mListingCompleted = true;
+      return helper.forget();
+    }
+
+    MOZ_ASSERT(helper->mFiles.IsEmpty());
+    helper->SetDirectoryPath(directoryPath);
+
+    nsCOMPtr<nsIEventTarget> target =
+      do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+    MOZ_ASSERT(target);
+
+    aRv = target->Dispatch(helper, NS_DISPATCH_NORMAL);
+    if (NS_WARN_IF(aRv.Failed())) {
+      return nullptr;
+    }
+
+    return helper.forget();
+  }
+
+  void
+  AddPromise(Promise* aPromise)
+  {
+    MOZ_ASSERT(aPromise);
+
+    // Still working.
+    if (!mListingCompleted) {
+      mPromises.AppendElement(aPromise);
+      return;
+    }
+
+    MOZ_ASSERT(mPromises.IsEmpty());
+    ResolveOrRejectPromise(aPromise);
+  }
+
+  // CC methods
+  void Unlink()
+  {
+    mGlobal = nullptr;
+    mFiles.Clear();
+    mPromises.Clear();
+  }
+
+  void Traverse(nsCycleCollectionTraversalCallback &cb)
+  {
+    GetFilesHelper* tmp = this;
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGlobal);
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFiles);
+    NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises);
+  }
+
+private:
+  GetFilesHelper(nsIGlobalObject* aGlobal, bool aRecursiveFlag)
+    : mGlobal(aGlobal)
+    , mRecursiveFlag(aRecursiveFlag)
+    , mListingCompleted(false)
+    , mErrorResult(NS_OK)
+  {
+    MOZ_ASSERT(aGlobal);
+  }
+
+  void
+  SetDirectoryPath(const nsAString& aDirectoryPath)
+  {
+    mDirectoryPath = aDirectoryPath;
+  }
+
+  NS_IMETHOD
+  Run() override
+  {
+    MOZ_ASSERT(!mDirectoryPath.IsEmpty());
+    MOZ_ASSERT(!mListingCompleted);
+
+    // First step is to retrieve the list of file paths.
+    // This happens in the I/O thread.
+    if (!NS_IsMainThread()) {
+      RunIO();
+      return NS_DispatchToMainThread(this);
+    }
+
+    RunMainThread();
+
+    // We mark the operation as completed here.
+    mListingCompleted = true;
+
+    // Let's process the pending promises.
+    nsTArray<RefPtr<Promise>> promises;
+    promises.SwapElements(mPromises);
+
+    for (uint32_t i = 0; i < promises.Length(); ++i) {
+      ResolveOrRejectPromise(promises[i]);
+    }
+
+    return NS_OK;
+  }
+
+  void
+  RunIO()
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+    MOZ_ASSERT(!mDirectoryPath.IsEmpty());
+    MOZ_ASSERT(!mListingCompleted);
+
+    nsCOMPtr<nsIFile> file;
+    mErrorResult = NS_NewNativeLocalFile(NS_ConvertUTF16toUTF8(mDirectoryPath), true,
+                                         getter_AddRefs(file));
+    if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
+      return;
+    }
+
+    nsAutoString path;
+    path.AssignLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+
+    mErrorResult = ExploreDirectory(path, file);
+  }
+
+  void
+  RunMainThread()
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(!mDirectoryPath.IsEmpty());
+    MOZ_ASSERT(!mListingCompleted);
+
+    // If there is an error, do nothing.
+    if (NS_FAILED(mErrorResult)) {
+      return;
+    }
+
+    // Create the sequence of Files.
+    for (uint32_t i = 0; i < mTargetPathArray.Length(); ++i) {
+      nsCOMPtr<nsIFile> file;
+      mErrorResult =
+        NS_NewNativeLocalFile(NS_ConvertUTF16toUTF8(mTargetPathArray[i].mRealPath),
+                              true, getter_AddRefs(file));
+      if (NS_WARN_IF(NS_FAILED(mErrorResult))) {
+        mFiles.Clear();
+        return;
+      }
+
+      RefPtr<File> domFile =
+        File::CreateFromFile(mGlobal, file);
+      MOZ_ASSERT(domFile);
+
+      domFile->SetPath(mTargetPathArray[i].mDomPath);
+
+      if (!mFiles.AppendElement(domFile, fallible)) {
+        mErrorResult = NS_ERROR_OUT_OF_MEMORY;
+        mFiles.Clear();
+        return;
+      }
+    }
+  }
+
+  nsresult
+  ExploreDirectory(const nsAString& aDOMPath, nsIFile* aFile)
+  {
+    MOZ_ASSERT(!NS_IsMainThread());
+    MOZ_ASSERT(aFile);
+
+    nsCOMPtr<nsISimpleEnumerator> entries;
+    nsresult rv = aFile->GetDirectoryEntries(getter_AddRefs(entries));
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+
+    for (;;) {
+      bool hasMore = false;
+      if (NS_WARN_IF(NS_FAILED(entries->HasMoreElements(&hasMore))) || !hasMore) {
+        break;
+      }
+
+      nsCOMPtr<nsISupports> supp;
+      if (NS_WARN_IF(NS_FAILED(entries->GetNext(getter_AddRefs(supp))))) {
+        break;
+      }
+
+      nsCOMPtr<nsIFile> currFile = do_QueryInterface(supp);
+      MOZ_ASSERT(currFile);
+
+      bool isLink, isSpecial, isFile, isDir;
+      if (NS_WARN_IF(NS_FAILED(currFile->IsSymlink(&isLink)) ||
+                     NS_FAILED(currFile->IsSpecial(&isSpecial))) ||
+          isLink || isSpecial) {
+        continue;
+      }
+
+      if (NS_WARN_IF(NS_FAILED(currFile->IsFile(&isFile)) ||
+                     NS_FAILED(currFile->IsDirectory(&isDir))) ||
+          !(isFile || isDir)) {
+        continue;
+      }
+
+      // The new domPath
+      nsAutoString domPath;
+      domPath.Assign(aDOMPath);
+      if (!aDOMPath.EqualsLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL)) {
+        domPath.AppendLiteral(FILESYSTEM_DOM_PATH_SEPARATOR_LITERAL);
+      }
+
+      nsAutoString leafName;
+      if (NS_WARN_IF(NS_FAILED(currFile->GetLeafName(leafName)))) {
+        continue;
+      }
+      domPath.Append(leafName);
+
+      if (isFile) {
+        FileData* data = mTargetPathArray.AppendElement(fallible);
+        if (!data) {
+          return NS_ERROR_OUT_OF_MEMORY;
+        }
+
+        if (NS_WARN_IF(NS_FAILED(currFile->GetPath(data->mRealPath)))) {
+          continue;
+        }
+
+        data->mDomPath = domPath;
+        continue;
+      }
+
+      MOZ_ASSERT(isDir);
+      if (!mRecursiveFlag) {
+        continue;
+      }
+
+      // Recursive.
+      return ExploreDirectory(domPath, currFile);
+    }
+
+    return NS_OK;
+  }
+
+  void
+  ResolveOrRejectPromise(Promise* aPromise)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    MOZ_ASSERT(mListingCompleted);
+    MOZ_ASSERT(aPromise);
+
+    // Error propagation.
+    if (NS_FAILED(mErrorResult)) {
+      aPromise->MaybeReject(mErrorResult);
+      return;
+    }
+
+    aPromise->MaybeResolve(mFiles);
+  }
+
+  nsCOMPtr<nsIGlobalObject> mGlobal;
+
+  bool mRecursiveFlag;
+  bool mListingCompleted;
+  nsString mDirectoryPath;
+
+  // We populate this array in the I/O thread with the paths of the Files that
+  // we want to send as result to the promise objects.
+  struct FileData {
+    nsString mDomPath;
+    nsString mRealPath;
+  };
+  FallibleTArray<FileData> mTargetPathArray;
+
+  // This is the real File sequence that we expose via Promises.
+  Sequence<RefPtr<File>> mFiles;
+
+  // Error code to propagate.
+  nsresult mErrorResult;
+
+  nsTArray<RefPtr<Promise>> mPromises;
+};
+
 class HTMLInputElementState final : public nsISupports
 {
   public:
     NS_DECLARE_STATIC_IID_ACCESSOR(NS_INPUT_ELEMENT_STATE_IID)
     NS_DECL_ISUPPORTS
 
     bool IsCheckedSet()
     {
@@ -1047,28 +1359,40 @@ NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLInput
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLInputElement,
                                                   nsGenericHTMLFormElementWithState)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers)
   if (tmp->IsSingleLineTextControl(false)) {
     tmp->mInputData.mState->Traverse(cb);
   }
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFilesOrDirectories)
+
+  if (tmp->mGetFilesRecursiveHelper) {
+    tmp->mGetFilesRecursiveHelper->Traverse(cb);
+  }
+
+  if (tmp->mGetFilesNonRecursiveHelper) {
+    tmp->mGetFilesNonRecursiveHelper->Traverse(cb);
+  }
+
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileList)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLInputElement,
                                                 nsGenericHTMLFormElementWithState)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFilesOrDirectories)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mFileList)
   if (tmp->IsSingleLineTextControl(false)) {
     tmp->mInputData.mState->Unlink();
   }
+
+  tmp->ClearGetFilesHelpers();
+
   //XXX should unlink more?
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_ADDREF_INHERITED(HTMLInputElement, Element)
 NS_IMPL_RELEASE_INHERITED(HTMLInputElement, Element)
 
 // QueryInterface implementation for HTMLInputElement
 NS_INTERFACE_TABLE_HEAD_CYCLE_COLLECTION_INHERITED(HTMLInputElement)
@@ -1112,16 +1436,17 @@ HTMLInputElement::Clone(mozilla::dom::No
       }
       break;
     case VALUE_MODE_FILENAME:
       if (it->OwnerDoc()->IsStaticDocument()) {
         // We're going to be used in print preview.  Since the doc is static
         // we can just grab the pretty string and use it as wallpaper
         GetDisplayFileName(it->mStaticDocFileList);
       } else {
+        it->ClearGetFilesHelpers();
         it->mFilesOrDirectories.Clear();
         it->mFilesOrDirectories.AppendElements(mFilesOrDirectories);
       }
       break;
     case VALUE_MODE_DEFAULT_ON:
       if (mCheckedChanged) {
         // We no longer have our original checked state.  Set our
         // checked state on the clone.
@@ -2557,28 +2882,31 @@ HTMLInputElement::GetDisplayFileName(nsA
 
   aValue = value;
 }
 
 void
 HTMLInputElement::SetFilesOrDirectories(const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories,
                                         bool aSetValueChanged)
 {
+  ClearGetFilesHelpers();
+
   mFilesOrDirectories.Clear();
   mFilesOrDirectories.AppendElements(aFilesOrDirectories);
 
   AfterSetFilesOrDirectories(aSetValueChanged);
 }
 
 void
 HTMLInputElement::SetFiles(nsIDOMFileList* aFiles,
                            bool aSetValueChanged)
 {
   RefPtr<FileList> files = static_cast<FileList*>(aFiles);
   mFilesOrDirectories.Clear();
+  ClearGetFilesHelpers();
 
   if (aFiles) {
     uint32_t listLength;
     aFiles->GetLength(&listLength);
     for (uint32_t i = 0; i < listLength; i++) {
       OwningFileOrDirectory* element = mFilesOrDirectories.AppendElement();
       element->SetAsFile() = files->Item(i);
     }
@@ -5076,16 +5404,68 @@ HTMLInputElement::GetFilesAndDirectories
       filesAndDirsSeq[i].SetAsFile() = filesAndDirs[i].GetAsFile();
     }
   }
 
   p->MaybeResolve(filesAndDirsSeq);
   return p.forget();
 }
 
+already_AddRefed<Promise>
+HTMLInputElement::GetFiles(bool aRecursiveFlag, ErrorResult& aRv)
+{
+  if (mType != NS_FORM_INPUT_FILE) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
+  MOZ_ASSERT(global);
+  if (!global) {
+    return nullptr;
+  }
+
+  RefPtr<GetFilesHelper> helper;
+  if (aRecursiveFlag) {
+    if (!mGetFilesRecursiveHelper) {
+      mGetFilesRecursiveHelper =
+       GetFilesHelper::Create(global,
+                              GetFilesOrDirectoriesInternal(),
+                              aRecursiveFlag, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return nullptr;
+      }
+    }
+
+    helper = mGetFilesRecursiveHelper;
+  } else {
+    if (!mGetFilesNonRecursiveHelper) {
+      mGetFilesNonRecursiveHelper =
+       GetFilesHelper::Create(global,
+                              GetFilesOrDirectoriesInternal(),
+                              aRecursiveFlag, aRv);
+      if (NS_WARN_IF(aRv.Failed())) {
+        return nullptr;
+      }
+    }
+
+    helper = mGetFilesNonRecursiveHelper;
+  }
+
+  MOZ_ASSERT(helper);
+
+  RefPtr<Promise> p = Promise::Create(global, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  helper->AddPromise(p);
+  return p.forget();
+}
+
 
 // Controllers Methods
 
 nsIControllers*
 HTMLInputElement::GetControllers(ErrorResult& aRv)
 {
   //XXX: what about type "file"?
   if (IsSingleLineTextControl(false))
@@ -7573,12 +7953,26 @@ HTMLInputElement::PickerClosed()
 }
 
 JSObject*
 HTMLInputElement::WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return HTMLInputElementBinding::Wrap(aCx, this, aGivenProto);
 }
 
+void
+HTMLInputElement::ClearGetFilesHelpers()
+{
+  if (mGetFilesRecursiveHelper) {
+    mGetFilesRecursiveHelper->Unlink();
+    mGetFilesRecursiveHelper = nullptr;
+  }
+
+  if (mGetFilesNonRecursiveHelper) {
+    mGetFilesNonRecursiveHelper->Unlink();
+    mGetFilesNonRecursiveHelper = nullptr;
+  }
+}
+
 } // namespace dom
 } // namespace mozilla
 
 #undef NS_ORIGINAL_CHECKED_VALUE
--- a/dom/html/HTMLInputElement.h
+++ b/dom/html/HTMLInputElement.h
@@ -35,16 +35,17 @@ namespace mozilla {
 class EventChainPostVisitor;
 class EventChainPreVisitor;
 
 namespace dom {
 
 class Date;
 class File;
 class FileList;
+class GetFilesHelper;
 
 /**
  * A class we use to create a singleton object that is used to keep track of
  * the last directory from which the user has picked files (via
  * <input type=file>) on a per-domain basis. The implementation uses
  * nsIContentPrefService2/NS_CONTENT_PREF_SERVICE_CONTRACTID to store the last
  * directory per-domain, and to ensure that whether the directories are
  * persistently saved (saved across sessions) or not honors whether or not the
@@ -698,16 +699,18 @@ public:
   {
     SetHTMLBoolAttr(nsGkAtoms::directory, aValue, aRv);
   }
 
   bool IsFilesAndDirectoriesSupported() const;
 
   already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv);
 
+  already_AddRefed<Promise> GetFiles(bool aRecursiveFlag, ErrorResult& aRv);
+
   void ChooseDirectory(ErrorResult& aRv);
 
   // XPCOM GetAlign() is OK
   void SetAlign(const nsAString& aValue, ErrorResult& aRv)
   {
     SetHTMLAttr(nsGkAtoms::align, aValue, aRv);
   }
 
@@ -1248,16 +1251,18 @@ protected:
    * Use this function before trying to open a picker.
    * It checks if the page is allowed to open a new pop-up.
    * If it returns true, you should not create the picker.
    *
    * @return true if popup should be blocked, false otherwise
    */
   bool IsPopupBlocked() const;
 
+  void ClearGetFilesHelpers();
+
   nsCOMPtr<nsIControllers> mControllers;
 
   /*
    * In mInputData, the mState field is used if IsSingleLineTextControl returns
    * true and mValue is used otherwise.  We have to be careful when handling it
    * on a type change.
    *
    * Accessing the mState member should be done using the GetEditorState function,
@@ -1281,16 +1286,19 @@ protected:
    * value from a text-input to a file-input. Additionally, the logic for this
    * value is kept as simple as possible to avoid accidental errors where the
    * wrong filename is used.  Therefor the list of filenames is always owned by
    * this member, never by the frame. Whenever the frame wants to change the
    * filename it has to call SetFilesOrDirectories to update this member.
    */
   nsTArray<OwningFileOrDirectory> mFilesOrDirectories;
 
+  RefPtr<GetFilesHelper> mGetFilesRecursiveHelper;
+  RefPtr<GetFilesHelper> mGetFilesNonRecursiveHelper;
+
 #ifndef MOZ_CHILD_PERMISSIONS
   /**
    * Hack for bug 1086684: Stash the .value when we're a file picker.
    */
   nsString mFirstFilePath;
 #endif
 
   RefPtr<FileList>  mFileList;
--- a/dom/webidl/HTMLInputElement.webidl
+++ b/dom/webidl/HTMLInputElement.webidl
@@ -200,16 +200,19 @@ partial interface HTMLInputElement {
 
   [Pref="dom.input.dirpicker"]
   readonly attribute boolean isFilesAndDirectoriesSupported;
 
   [Throws, Pref="dom.input.dirpicker"]
   Promise<sequence<(File or Directory)>> getFilesAndDirectories();
 
   [Throws, Pref="dom.input.dirpicker"]
+  Promise<sequence<File>> getFiles(optional boolean recursiveFlag = false);
+
+  [Throws, Pref="dom.input.dirpicker"]
   void chooseDirectory();
 };
 
 [NoInterfaceObject]
 interface MozPhonetic {
   [Pure, ChromeOnly]
   readonly attribute DOMString phonetic;
 };