【问题标题】:Does HTML5 allow drag-drop upload of folders or a folder tree?HTML5 是否允许拖放上传文件夹或文件夹树?
【发布时间】:2011-04-05 03:21:44
【问题描述】:

我还没有看到任何这样做的例子。 API 规范中不允许这样做吗?

我正在寻找一种简单的拖放解决方案来上传整个文件夹树的照片。

【问题讨论】:

标签: javascript html file-upload drag-and-drop


【解决方案1】:

现在可以使用 Chrome >= 21。

function traverseFileTree(item, path) {
  path = path || "";
  if (item.isFile) {
    // Get file
    item.file(function(file) {
      console.log("File:", path + file.name);
    });
  } else if (item.isDirectory) {
    // Get folder contents
    var dirReader = item.createReader();
    dirReader.readEntries(function(entries) {
      for (var i=0; i<entries.length; i++) {
        traverseFileTree(entries[i], path + item.name + "/");
      }
    });
  }
}

dropArea.addEventListener("drop", function(event) {
  event.preventDefault();

  var items = event.dataTransfer.items;
  for (var i=0; i<items.length; i++) {
    // webkitGetAsEntry is where the magic happens
    var item = items[i].webkitGetAsEntry();
    if (item) {
      traverseFileTree(item);
    }
  }
}, false);

更多信息:https://protonet.info/blog/html5-experiment-drag-drop-of-folders/

【讨论】:

  • 即使在 2 年后,IE 和 Firefox 似乎也不愿意实现这一点。
  • 现在,对于 Firefox 也是如此:stackoverflow.com/a/33431704/195216 它显示了通过拖放以及通过 chrome 和 Firefox 中的对话框上传文件夹!
  • Edge 也支持这个。
  • 重要警告:此答案中的代码仅限于给定目录中的 100 个文件。见这里:bugs.chromium.org/p/chromium/issues/detail?id=514087
  • @johnozbay 不幸的是,更多人收到了您的重要警告,这不一定是 Chromium 问题,因为规范说 readEntries 不会返回目录中的所有内容。根据您提供的错误链接,我写了一个完整的答案:stackoverflow.com/a/53058574/885922
【解决方案2】:

不幸的是,没有一个现有答案是完全正确的,因为readEntries 不一定会返回 ALL 给定目录的(文件或目录)条目。这是 API 规范的一部分(请参阅下面的文档部分)。

要真正获取所有文件,我们需要反复调用readEntries(对于我们遇到的每个目录),直到它返回一个空数组。如果我们不这样做,我们将错过目录中的一些文件/子目录,例如在 Chrome 中,readEntries 一次最多只能返回 100 个条目。

使用 Promises (await/ async) 更清楚地展示readEntries 的正确用法(因为它是异步的),以及广度优先搜索 (BFS) 遍历目录结构:

// Drop handler function to get all files
async function getAllFileEntries(dataTransferItemList) {
  let fileEntries = [];
  // Use BFS to traverse entire directory/file structure
  let queue = [];
  // Unfortunately dataTransferItemList is not iterable i.e. no forEach
  for (let i = 0; i < dataTransferItemList.length; i++) {
    queue.push(dataTransferItemList[i].webkitGetAsEntry());
  }
  while (queue.length > 0) {
    let entry = queue.shift();
    if (entry.isFile) {
      fileEntries.push(entry);
    } else if (entry.isDirectory) {
      queue.push(...await readAllDirectoryEntries(entry.createReader()));
    }
  }
  return fileEntries;
}

// Get all the entries (files or sub-directories) in a directory 
// by calling readEntries until it returns empty array
async function readAllDirectoryEntries(directoryReader) {
  let entries = [];
  let readEntries = await readEntriesPromise(directoryReader);
  while (readEntries.length > 0) {
    entries.push(...readEntries);
    readEntries = await readEntriesPromise(directoryReader);
  }
  return entries;
}

// Wrap readEntries in a promise to make working with readEntries easier
// readEntries will return only some of the entries in a directory
// e.g. Chrome returns at most 100 entries at a time
async function readEntriesPromise(directoryReader) {
  try {
    return await new Promise((resolve, reject) => {
      directoryReader.readEntries(resolve, reject);
    });
  } catch (err) {
    console.log(err);
  }
}

Codepen 上的完整工作示例:https://codepen.io/anon/pen/gBJrOP

FWIW 我之所以选择这个,是因为在使用接受的答案时,我没有取回包含 40,000 个文件(许多目录包含超过 100 个文件/子目录)的目录中的所有文件。

文档:

此行为记录在 FileSystemDirectoryReader 中。重点摘录:

readEntries()
返回一个包含一些的数组 目录条目。数组中的每一项都是一个基于 FileSystemEntry——通常是 FileSystemFileEntry 或 文件系统目录条目。

但公平地说,MDN 文档可以在其他部分更清楚地说明这一点。 readEntries() 文档简单说明:

readEntries() 方法检索正在读取的目录中的目录条目,并将它们以数组的形式传递给提供的回调函数

并且需要多次调用的唯一提及/提示是在 successCallback 参数的描述中:

如果没有文件了,或者你已经调用了 readEntries() 此 FileSystemDirectoryReader,数组为空。

可以说 API 也可以更直观,但正如文档所述:它是非标准/实验性功能,不在标准轨道上,不能期望适用于所有浏览器。

相关:

  • johnozbay comments 在 Chrome 上,readEntries 将最多返回 100 个目录条目(验证为 Chrome 64)。
  • Xan 在这个answer 中很好地解释了readEntries 的正确用法(尽管没有代码)。
  • Pablo Barría Urenda's answer 在没有 BFS 的情况下以异步方式正确调用 readEntries。他还指出,Firefox 会返回目录中的所有条目(与 Chrome 不同),但鉴于规范,我们不能依赖这一点。

【讨论】:

  • 非常感谢您的呼声,并将此内容发布在那里。 SOF需要更多像你一样优秀的成员! ✌?
  • 我很欣赏@johnozbay 我只是担心许多用户似乎忽略了这个小而重要的事实:规范/API 和这个边缘案例(一个目录中的 100 多个文件)不是不太可能。当我没有取回我期望的所有文件时,我才意识到这一点。你的评论应该是答案。
  • 要获取所有相关元数据(大小、lastModified、mime 类型),您需要通过file(successCb, failureCb) 方法将所有FileSystemFileEntry 转换为File。如果您还需要完整路径,则应从 fileEntry.fullPath 获取(file.webkitRelativePath 只是名称)。
  • @Andrey 的主要问题是File 对象无法让我们轻松处理我们有一个目录并希望获取其文件或子目录的情况。这就是我们为什么叫dataTransferItemList[i].webkitGetAsEntry()而不是dataTransferItemList[i].getAsFile()的原因
  • 转换为File 似乎并不密集,所以我不会担心那个操作。我已经在生产中得到了这段代码,它可以轻松处理数以万计的文件。事实上,我的测试工具是任意嵌套的 40,000 个文件。内容本身的上传当然取决于文件大小、磁盘、网络等。
【解决方案3】:

此函数将为您提供所有已删除文件的数组的承诺,例如 &lt;input type="file"/&gt;.files:

function getFilesWebkitDataTransferItems(dataTransferItems) {
  function traverseFileTreePromise(item, path='') {
    return new Promise( resolve => {
      if (item.isFile) {
        item.file(file => {
          file.filepath = path + file.name //save full path
          files.push(file)
          resolve(file)
        })
      } else if (item.isDirectory) {
        let dirReader = item.createReader()
        dirReader.readEntries(entries => {
          let entriesPromises = []
          for (let entr of entries)
            entriesPromises.push(traverseFileTreePromise(entr, path + item.name + "/"))
          resolve(Promise.all(entriesPromises))
        })
      }
    })
  }

  let files = []
  return new Promise((resolve, reject) => {
    let entriesPromises = []
    for (let it of dataTransferItems)
      entriesPromises.push(traverseFileTreePromise(it.webkitGetAsEntry()))
    Promise.all(entriesPromises)
      .then(entries => {
        //console.log(entries)
        resolve(files)
      })
  })
}

用法:

dropArea.addEventListener("drop", function(event) {
  event.preventDefault();

  var items = event.dataTransfer.items;
  getFilesFromWebkitDataTransferItems(items)
    .then(files => {
      ...
    })
}, false);

NPM 包: https://www.npmjs.com/package/datatransfer-files-promise

使用示例: https://github.com/grabantot/datatransfer-files-promise/blob/master/index.html

【讨论】:

  • 这应该是新接受的答案。它比其他答案更好,因为它在完成时返回一个承诺。但是有一些错误:function getFilesWebkitDataTransferItems(dataTransfer) 应该是function getFilesWebkitDataTransferItems(items)for (entr of entries) 应该是for (let entr of entries)
  • 实际上不会获取目录中的所有文件(对于 Chrome,它只会返回目录中的 100 个条目)。 Spec规定需要反复调用readEntries,直到返回一个空数组。
  • @xlm 更新了 npm 包。现在它可以处理超过 100 个条目。
  • 非常有帮助!感谢您的解决方案。到目前为止,这是最精确和最干净的一个。这应该是新接受的答案,我同意。
  • 这太完美了!谢谢谢谢!
【解决方案4】:

this message HTML 5 邮件列表中,Ian Hickson 说:

HTML5 现在必须在 一次。浏览器可以让用户 一次选择多个文件,包括 跨多个目录;那是一个 有点超出规范的范围。

(另见原文feature proposal。) 因此可以肯定地假设他认为使用拖放上传文件夹也超出了范围。显然,提供单个文件取决于浏览器。

上传文件夹也会有一些其他困难,如Lars Gunther所述:

这个 […] 提案必须有两个 检查(如果可行的话):

  1. 最大大小,阻止某人上传包含多个文件的完整目录 一百个未压缩的原始图像...

  2. 即使忽略了接受属性也会进行过滤。 Mac OS 元数据 和 Windows 缩略图等应该是 省略。所有隐藏文件和 目录应该默认为 排除在外。

【讨论】:

  • 嗯,我同意第 2 点...但前提是 Web 开发人员有办法确定他们是否要启用隐藏文件的上传 - 因为总是有可能隐藏文件可以用于使用上传的文件夹。特别是如果文件夹是一个完整的文档,它被分成多个部分,就像最终剪切的文件一样。
  • 不同意超出范围:这是许多人想做的事情不兼容的原因,因此应该指定。
【解决方案5】:

现在您可以通过拖放和输入来上传目录。

<input type='file' webkitdirectory >

用于拖放(适用于 webkit 浏览器)。

处理拖放文件夹。

<div id="dropzone"></div>
<script>
var dropzone = document.getElementById('dropzone');
dropzone.ondrop = function(e) {
  var length = e.dataTransfer.items.length;
  for (var i = 0; i < length; i++) {
    var entry = e.dataTransfer.items[i].webkitGetAsEntry();
    if (entry.isFile) {
      ... // do whatever you want
    } else if (entry.isDirectory) {
      ... // do whatever you want
    }
  }
};
</script>

资源:

http://updates.html5rocks.com/2012/07/Drag-and-drop-a-folder-onto-Chrome-now-available

【讨论】:

  • 是否可以在不使用压缩文件夹的情况下进行同样的下载?
【解决方案6】:

Firefox 现在支持文件夹上传,截至 2016 年 11 月 15 日,v50.0:https://developer.mozilla.org/en-US/Firefox/Releases/50#Files_and_directories

您可以将文件夹拖放到 Firefox 中,也可以浏览并选择要上传的本地文件夹。它还支持嵌套在子文件夹中的文件夹。

这意味着您现在可以使用 Chrome、Firefox、Edge 或 Opera 来上传文件夹。您目前无法使用 Safari 或 Internet Explorer。

【讨论】:

    【解决方案7】:

    这里有一个完整的例子来说明如何使用file and directory entries API

    var dropzone = document.getElementById("dropzone");
    var listing = document.getElementById("listing");
    
    function scanAndLogFiles(item, container) {
      var elem = document.createElement("li");
      elem.innerHTML = item.name;
      container.appendChild(elem);
    
      if (item.isDirectory) {
        var directoryReader = item.createReader();
        var directoryContainer = document.createElement("ul");
        container.appendChild(directoryContainer);
    
        directoryReader.readEntries(function(entries) {
          entries.forEach(function(entry) {
            scanAndLogFiles(entry, directoryContainer);
          });
        });
      }
    }
    
    dropzone.addEventListener(
      "dragover",
      function(event) {
        event.preventDefault();
      },
      false
    );
    
    dropzone.addEventListener(
      "drop",
      function(event) {
        var items = event.dataTransfer.items;
    
        event.preventDefault();
        listing.innerHTML = "";
    
        for (var i = 0; i < items.length; i++) {
          var item = items[i].webkitGetAsEntry();
    
          if (item) {
            scanAndLogFiles(item, listing);
          }
        }
      },
      false
    );
    body {
      font: 14px "Arial", sans-serif;
    }
    
    #dropzone {
      text-align: center;
      width: 300px;
      height: 100px;
      margin: 10px;
      padding: 10px;
      border: 4px dashed red;
      border-radius: 10px;
    }
    
    #boxtitle {
      display: table-cell;
      vertical-align: middle;
      text-align: center;
      color: black;
      font: bold 2em "Arial", sans-serif;
      width: 300px;
      height: 100px;
    }
    <p>Drag files and/or directories to the box below!</p>
    
    <div id="dropzone">
      <div id="boxtitle">
        Drop Files Here
      </div>
    </div>
    
    <h2>Directory tree:</h2>
    
    <ul id="listing"></ul>

    Chrome 13+、Firefox 50+ 和 Edge 支持webkitGetAsEntry

    来源:https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry

    【讨论】:

    【解决方案8】:

    HTML5 是否允许拖放上传文件夹或文件夹树?

    只有 Chrome 支持此功能。它没有任何牵引力,很可能会被移除。

    参考:https://developer.mozilla.org/en/docs/Web/API/DirectoryReader#readEntries

    【讨论】:

    • 哇。从那个链接的 W3C 说明来看,这确实没有继续。假设它未能获得任何牵引力的依据是什么?
    • @bebbi 没有其他浏览器供应商实现它
    • @PabloBarriaUrenda 评论不正确;他的问题很可能与他的问题有关:stackoverflow.com/questions/51850469/… 他解决/实现了readEntries 如果另一个readEntries 的调用仍在运行,则无法调用。 DirectoryReader API 设计不是最好的
    • @xlm 是的,您确实是正确的。当我自己对这个问题感到困惑时,我发布了这个,但我最终解决了它(并忘记了这个评论)。我现在已经删除了令人困惑的评论。
    【解决方案9】:

    更新:自 2012 年以来发生了很多变化,请参阅上面的答案。为了考古,我把这个答案留在这里。

    HTML5 规范并未规定在选择要上传的文件夹时,浏览器应递归上传所有包含的文件。

    实际上,在Chrome/Chromium中,你可以上传一个文件夹,但是当你这样做的时候,它只是上传了一个无意义的4KB文件,它代表了目录。一些服务器端应用程序,如Alfresco 可以检测到这一点,并警告用户无法上传文件夹:

    【讨论】:

    • @MoB:也许它确实是某种指针。但是由于实际文件在客户端机器上,服务器机器当然不能用这个指针做任何事情。
    【解决方案10】:

    最近偶然发现需要在我的两个项目中实现这一点,所以我创建了一堆实用函数来帮助解决这个问题。

    创建一个表示所有文件夹、文件和它们之间关系的数据结构,就像这样?

    {
      folders: [
        {
          name: string,
          folders: Array,
          files: Array
        },
        /* ... */
      ],
      files: Array
    }
    

    而另一个只返回所有文件的数组(在所有文件夹和子文件夹中)。

    这是包的链接:https://www.npmjs.com/package/file-system-utils

    【讨论】:

      【解决方案11】:

      在遇到 100 个文件限制问题之前,我一直很乐意复制/粘贴 @grabantot 的解决方案。

      @xlm 的解决方案克服了 100 个文件的限制,它返回一个 FileEntry 对象数组。

      但是在我的项目中,我需要从 fileEntry 对象中提取文件路径。

      如果您有权访问 ChromeFileSystem api,则此方法有效:

      
      const getAllPaths = async (dataTransferItems) =>{
      
          async function getAllFileEntries(dataTransferItemList) {
              let fileEntries = [];
              // Use BFS to traverse entire directory/file structure
              let queue = [];
             
              for (let i = 0; i < dataTransferItemList.length; i++) {
      
                queue.push(dataTransferItemList[i].webkitGetAsEntry());
      
              }
              while (queue.length > 0) {
                let entry = queue.shift();
                if (entry.isFile) {
                  fileEntries.push(entry);
                } else if (entry.isDirectory) {
                  queue.push(...await readAllDirectoryEntries(entry.createReader()));
                }
              }
              return fileEntries;
            }
            
            // Get all the entries (files or sub-directories) in a directory 
            // by calling readEntries until it returns empty array
            async function readAllDirectoryEntries(directoryReader) {
              let entries = [];
              let readEntries = await readEntriesPromise(directoryReader);
              while (readEntries.length > 0) {
                entries.push(...readEntries);
                readEntries = await readEntriesPromise(directoryReader);
              }
              return entries;
            }
            
            // Wrap readEntries in a promise to make working with readEntries easier
            // readEntries will return only some of the entries in a directory
            // e.g. Chrome returns at most 100 entries at a time
            async function readEntriesPromise(directoryReader) {
              try {
                return await new Promise((resolve, reject) => {
                  directoryReader.readEntries(resolve, reject);
                });
              } catch (err) {
                console.log(err);
              }
            }
      
      
           const getDisplayPath = (entry)=>{
              return new Promise((resolve, reject) =>{
                  chrome.fileSystem.getDisplayPath(entry, (path)=>{
                      if(chrome.runtime.lastError) {
                          reject(chrome.runtime.lastError)
                      }else {
                          resolve(path);
                      }
                  })
              })
          }
      
      
          
          const fileEnties = await getAllFileEntries(dataTransferItems);
      
          const files = await Promise.all(fileEnties.map(async(x)=>{
              return (await getDisplayPath(x))
          }))
      
          return files;
      
      }
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-02-10
        • 2011-09-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-03-20
        相关资源
        最近更新 更多