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

IT技术 javascript html file-upload drag-and-drop
2021-02-06 08:28:02

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

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

6个回答

现在可以了,多亏了 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 似乎也不愿意实现这一点。
2021-03-16 08:28:02
Edge 也支持这一点。
2021-03-20 08:28:02
现在,对于 Firefox 也是如此:stackoverflow.com/a/33431704/195216它显示了通过拖放和通过 chrome 和 firefox 中的对话框上传的文件夹!
2021-03-27 08:28:02
@johnozbay 不幸的是,更多的人收到了您的重要警告,这不一定是 Chromium 问题,因为规范说readEntries不会返回目录中的所有内容。根据您提供的错误链接,我写了一个完整的答案:stackoverflow.com/a/53058574/885922
2021-04-02 08:28:02
重要警告:此答案中的代码仅限于给定目录中的 100 个文件。请参阅此处: bugs.chromium.org/p/chromium/issues/detail?id=514087
2021-04-12 08:28:02

不幸的是,现有的答案都不是完全正确的,因为readEntries不一定会返回给定目录的所有(文件或目录)条目。这是 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 或 FileSystemDirectoryEntry。

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

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

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

如果没有剩余文件,或者您已经在此 FileSystemDirectoryReader 上调用了 readEntries(),则该数组为空。

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

有关的:

  • johnozbay 评论说,在 Chrome 上,readEntries一个目录最多返回 100 个条目(验证为 Chrome 64)。
  • XanreadEntries在这个答案中很好地解释了 的正确用法(尽管没有代码)。
  • Pablo Barría Urenda 的回答readEntries以异步方式正确调用,无需 BFS。他还指出,Firefox 会返回目录中的所有条目(与 Chrome 不同),但鉴于规范,我们不能依赖于此。
我很感激@johnozbay,我只是担心似乎许多用户都忽略了这个小而重要的事实:规范/API 并且这种边缘情况(目录中有 100 多个文件)并非不可能。当我没有取回我期望的所有文件时,我才意识到这一点。你的评论应该是回答。
2021-03-14 08:28:02
非常感谢您的大喊大叫,并在那里发布这些内容。SOF需要更多像你这样优秀的成员!✌🏻
2021-03-15 08:28:02
要获取所有相关元数据(大小、lastModified、mime 类型),您需要通过方法将 all 转换FileSystemFileEntry如果您还需要完整路径,则应该从(将只是名称) 中获取。Filefile(successCb, failureCb)fileEntry.fullPathfile.webkitRelativePath
2021-04-05 08:28:02
@Andrey 主要问题是该File对象无法让我们轻松处理我们拥有目录并希望获取其文件或子目录的情况。这就是我们调用dataTransferItemList[i].webkitGetAsEntry()而不是调用的原因dataTransferItemList[i].getAsFile()
2021-04-05 08:28:02
转换为File似乎并不密集,所以我不会担心那个操作。我已经在生产中使用了这段代码,它可以轻松处理数以万计的文件。事实上,我的测试工具是任意嵌套的 40,000 个文件。内容本身的上传当然取决于文件大小、磁盘、网络等。
2021-04-12 08:28:02

此函数将为您提供所有已删除文件的数组的Promise,例如<input type="file"/>.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

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

发送给 HTML 5 邮件列表的这条消息中,Ian Hickson 说:

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

(另请参阅原始功能提案。)因此可以安全地假设他考虑使用拖放上传文件夹也超出范围。显然,由浏览器来提供单个文件。

正如Lars Gunther所描述的,上传文件夹也会遇到一些其他困难

这个 [...] 提案必须有两个检查(如果它完全可行):

  1. 最大尺寸,以阻止某人上传包含数百个未压缩原始图像的完整目录...

  2. 即使省略了接受属性,也进行过滤。应省略 Mac OS 元数据和 Windows 缩略图等。默认情况下应排除所有隐藏文件和目录。

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

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

<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

是否可以在不使用压缩文件夹的情况下进行相同的下载?
2021-04-12 08:28:02