Bug 1261693 - Implement HTMLInputElement::GetFiles, r=smaug
authorAndrea Marchesini <amarchesini@mozilla.com>
Tue, 31 May 2016 12:54:24 +0200
changeset 340698 b797131df3546a1b604e6da0f3283fdfdf1d5197
parent 340697 e33ed35dc9d98edf37f557fb24f9407522ab8a55
child 340699 d4a426b39dec87ff6ea6e4b11c2f5409ac54e1e7
push id1183
push userraliiev@mozilla.com
push dateMon, 05 Sep 2016 20:01:49 +0000
treeherdermozilla-release@3148731bed45 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1261693
milestone49.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 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,331 @@ 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.
+      rv = ExploreDirectory(domPath, currFile);
+      if (NS_WARN_IF(NS_FAILED(rv))) {
+        return rv;
+      }
+    }
+
+    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 +1362,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 +1439,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 +2885,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 +5407,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 +7956,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;
 };