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


Answers:


80

多亏了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/


9
即使在2年后,IE和Firefox似乎也不愿意实现这一目标。
Nicolas Raoul

8
现在,对于Firefox也是如此:stackoverflow.com/a/33431704/195216它显示通过拖放以及chrome和firefox中的对话框上传文件夹!
dforce 2015年

2
Edge也支持这一点。
ZachB '16

7
重要警告:此答案中的代码限于给定目录中的100个文件。看到这里: bugs.chromium.org/p/chromium/issues/detail?id=514087
johnozbay

4
@johnozbay不幸的是,更多的人收到了您的重要警告,这不一定是Chromium的问题,因为该规范说readEntries不会将所有内容返回目录中。根据您提供的错误链接,我写了一个完整的答案:stackoverflow.com/a/53058574/885922
xlm

46

不幸的是,现有答案中没有一个是完全正确的,因为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一个目录(验证为Chrome 64)最多返回100个条目。
  • XanreadEntries很好地解释了此答案的正确用法(尽管没有代码)。
  • PabloBarríaUrenda的答案可以readEntries在没有BFS的情况下以异步方式正确调用。他还指出,Firefox返回目录中的所有条目(与Chrome不同),但是给定规范,我们不能依靠它。

4
非常感谢您的大喊大叫,并将这些内容发布到这里。SOF需要像您一样的出色成员!✌🏻–
johnozbay

6
我很欣赏@johnozbay,我只是担心,似乎很多用户都忽略了这个小而重要的事实:规范/ API和这种极端情况(目录中有100多个文件)并不是没有可能。我只有在不找回我期望的所有文件时才意识到这一点。您的评论应该是答案。
xlm

如何获取文件大小?
马多

要获取所有相关的元数据(大小,lastModified,mime类型),您需要通过方法将全部转换FileSystemFileEntry为。如果您还需要完整的路径,则应使用(将仅是名称)。Filefile(successCb, failureCb)fileEntry.fullPathfile.webkitRelativePath
Iskren Ivov Chernev

这似乎是最好的答案,但在Chromium 86中对我不起作用。似乎在Firefox中可以正常工作。在Chromium中,它将上载包含文件的选择,但不会为目录上载任何内容,因为readEntriesPromise()返回一个空数组。
快乐

15

此功能将为您提供所有删除文件的数组的保证,例如<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


4
这应该是新接受的答案。它比其他答案更好,因为完成后它会返回一个承诺。但是有一些错误:function getFilesWebkitDataTransferItems(dataTransfer)应该是function getFilesWebkitDataTransferItems(items)for (entr of entries)应该是for (let entr of entries)
RoccoB

1
实际上不会获得目录中的所有文件(对于Chrome,它只会在目录中返回100个条目)。规范规定需要readEntries重复调用,直到返回一个空数组。
xlm

@xlm更新了npm软件包。现在,它可以处理> 100个条目。
汉普顿

很有帮助!感谢您的解决方案。到目前为止,这是最精确,最干净的一种。我同意,这应该是新的接受的答案。
Siddhartha Chowdhury

13

在发给HTML 5邮件列表的消息中,伊恩·希克森(Ian Hickson)说:

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

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

Lars Gunther所述,上传文件夹还存在其他一些困难:

此[…]提议必须进行两项检查(如果完全可行):

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

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


嗯,我同意第2点...,但前提是网络开发人员可以确定他们是否要启用隐藏文件的上载-因为总有可能隐藏文件可用于使用上载的文件夹。特别是如果该文件夹是一个完整的文档,则它可能像最终剪切文件一样分成多个部分。
查尔斯·约翰·汤普森三世

不同意超出范围:这是许多人想要做的事情不兼容的原因,因此应加以说明。
西罗Santilli郝海东冠状病六四事件法轮功2014年

10

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

<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


1
是否可以在不使用压缩文件夹的情况下进行相同的下载?
user2284570


3

这是有关如何使用文件和目录条目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>

webkitGetAsEntry Chrome 13 +,Firefox 50+和Edge支持。

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


工作很棒。移植到Vue jsfiddle.net/KimNyholm/xua9kLny
Kim Nyholm

1

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

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

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


哇。从该链接的W3C注释中得知,这确实没有继续。它没有获得任何牵引力的假设的基础是什么?
bebbi '16

@bebbi没有其他浏览器供应商实施它
basarat

1
@PabloBarríaUrenda评论不正确;他的问题很可能与他的问题有关:stackoverflow.com/questions/51850469/…readEntries如果readEntries仍在运行另一个调用,则无法调用他已解决/实现的问题。DirectoryReader API设计不是最好的
-xlm

@xlm是的,的确是正确的。当我自己对此问题感到困惑时,我已经发布了此内容,但最终我解决了它(并忘记了此评论)。我现在已经删除了令人困惑的评论。
PabloBarríaUrenda '18 -10-31

1

更新:自2012年以来,发生了很多变化,请改用上述答案。为了考古的缘故,我在这里留下这个答案。

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

实际上,在Chrome / Chromium中,您可以上传文件夹,但是在执行该操作时,它只是上传了一个无意义的4KB文件,该文件代表了目录。某些服务器端应用程序(例如Alfresco)可以检测到此情况,并警告用户无法上传文件夹:

以下内容不能上传,因为它们是文件夹或大小为零字节:undefined


@MoB:也许确实是某种指针。但是,由于实际文件在客户端计算机上,因此服务器计算机当然无法使用此指针执行任何操作。
尼古拉斯·拉乌尔

1

最近偶然发现需要在我的两个项目中实现此功能,因此我创建了许多实用程序函数来帮助实现此目的。

创建一个表示所有文件夹,文件及其之间关系的数据结构,如下所示:

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

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.