简体   繁体   English

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

[英]Does HTML5 allow drag-drop upload of folders or a folder tree?

I haven't seen any examples that do this.我还没有看到任何这样做的例子。 Is this not allowed in the API spec? API规范中不允许这样做吗?

I am searching for an easy drag-drop solution for uploading an entire folder tree of photos.我正在寻找一种简单的拖放解决方案来上传整个照片文件夹树。

It's now possible, thanks to Chrome >= 21.由于 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);

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

Unfortunately none of the existing answers are completely correct because readEntries will not necessarily return ALL the (file or directory) entries for a given directory.不幸的是,现有的答案都不是完全正确的,因为readEntries不一定会返回给定目录的所有(文件或目录)条目。 This is part of the API specification (see Documentation section below).这是 API 规范的一部分(请参阅下面的文档部分)。

To actually get all the files, we'll need to call readEntries repeatedly (for each directory we encounter) until it returns an empty array.为了实际获取所有文件,我们需要重复调​​用readEntries (对于我们遇到的每个目录),直到它返回一个空数组。 If we don't, we will miss some files/sub-directories in a directory eg in Chrome, readEntries will only return at most 100 entries at a time.如果我们不这样做,我们将错过目录中的一些文件/子目录,例如在 Chrome 中, readEntries一次最多只能返回 100 个条目。

Using Promises ( await / async ) to more clearly demonstrate the correct usage of readEntries (since it's asynchronous), and breadth-first search (BFS) to traverse the directory structure:使用 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);
  }
}

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

FWIW I only picked this up because I wasn't getting back all the files I expected in a directory containing 40,000 files (many directories containing well over 100 files/sub-directories) when using the accepted answer. FWIW 我只是选择了这个,因为在使用接受的答案时,我没有在包含 40,000 个文件的目录(许多目录包含超过 100 个文件/子目录)中取回我期望的所有文件。

Documentation:文档:

This behaviour is documented in FileSystemDirectoryReader .此行为记录在FileSystemDirectoryReader 中 Excerpt with emphasis added:摘录并强调:

readEntries()读取条目()
Returns a an array containing some number of the directory's entries .返回一个包含一定数量目录条目的数组。 Each item in the array is an object based on FileSystemEntry—typically either FileSystemFileEntry or FileSystemDirectoryEntry.数组中的每一项都是基于 FileSystemEntry 的对象——通常是 FileSystemFileEntry 或 FileSystemDirectoryEntry。

But to be fair, the MDN documentation could make this clearer in other sections.但公平地说,MDN 文档可以在其他部分更清楚地说明这一点。 The readEntries() documentation simply notes: readEntries()文档简单地指出:

readEntries() method retrieves the directory entries within the directory being read and delivers them in an array to the provided callback function readEntries()方法检索正在读取的目录中的目录条目,并将它们以数组形式传递给提供的回调函数

And the only mention/hint that multiple calls are needed is in the description of successCallback parameter:唯一需要多次调用的提及/提示是在successCallback参数的描述中:

If there are no files left, or you've already called readEntries() on this FileSystemDirectoryReader, the array is empty.如果没有剩余文件,或者您已经在此 FileSystemDirectoryReader 上调用了 readEntries(),则该数组为空。

Arguably the API could be more intuitive as well, but as the documentation notes: it is a non-standard/experimental feature, not on a standards track, and can't be expected to work for all browsers.可以说 API 也可以更直观,但正如文档所指出的那样:它是一个非标准/实验性功能,不在标准轨道上,并且不能期望适用于所有浏览器。

Related:有关的:

  • johnozbay comments that on Chrome, readEntries will return at most 100 entries for a directory (verified as Chrome 64). johnozbay 评论说,在 Chrome 上, readEntries最多会为一个目录返回 100 个条目(经验证为 Chrome 64)。
  • Xan explains the correct usage of readEntries quite well in this answer (albeit without code). Xan在这个答案中很好地解释了readEntries的正确用法(尽管没有代码)。
  • Pablo Barría Urenda's answer correctly calls readEntries in a asynchronous manner without BFS. Pablo Barría Urenda 的回答正确地以异步方式调用readEntries ,无需 BFS。 He also notes that Firefox returns all the entries in a directory (unlike Chrome) but we can't rely on this given the specification.他还指出,Firefox 会返回目录中的所有条目(与 Chrome 不同),但鉴于规范,我们不能依赖于此。

This function will give you a promise for array of all dropped files, like <input type="file"/>.files :此函数将为您提供所有已删除文件的数组的承诺,例如<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)
      })
  })
}

Usage:用法:

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

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

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

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

In this message to the HTML 5 mailing list Ian Hickson says:在给 HTML 5 邮件列表的这条消息中,Ian Hickson 说:

HTML5 now has to upload many files at once. HTML5 现在必须一次上传许多文件。 Browsers could allow users to pick multiple files at once, including across multiple directories;浏览器可以允许用户一次选择多个文件,包括跨多个目录; that's a bit out of scope of the spec.这有点超出规范的范围。

(Also see the original feature proposal .) So it's safe to assume he considers uploading folders using drag-and-drop also out of scope. (另请参阅原始功能提案。)因此可以安全地假设他考虑使用拖放上传文件夹也超出范围。 Apparently it's up to the browser to serve individual files.显然,由浏览器来提供单个文件。

Uploading folders would also have some other difficulties, as described by Lars Gunther :正如Lars Gunther所描述的,上传文件夹也会遇到一些其他困难:

This […] proposal must have two checks (if it is doable at all):这个 […] 提案必须有两个检查(如果它完全可行):

  1. Max size, to stop someone from uploading a full directory of several hundred uncompressed raw images...最大尺寸,以阻止某人上传包含数百个未压缩原始图像的完整目录...

  2. Filtering even if the accept attribute is omitted.即使省略了 accept 属性也进行过滤。 Mac OS metadata and Windows thumbnails, etc should be omitted.应省略 Mac OS 元数据和 Windows 缩略图等。 All hidden files and directories should default to be excluded.应默认排除所有隐藏文件和目录。

Now you can upload directories with both drag and drop and input.现在您可以通过拖放和输入上传目录。

<input type='file' webkitdirectory >

and for drag and drop(For webkit browsers).和拖放(对于 webkit 浏览器)。

Handling drag and drop folders.处理拖放文件夹。

<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>

Resources:资源:

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

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

You can drag and drop folders into Firefox or you can browse and select a local folder to upload.您可以将文件夹拖放到 Firefox 中,也可以浏览并选择要上传的本地文件夹。 It also supports folders nested in subfolders.它还支持嵌套在子文件夹中的文件夹。

That means you can now use either Chrome, Firefox, Edge or Opera to upload folders.这意味着您现在可以使用 Chrome、Firefox、Edge 或 Opera 上传文件夹。 You can't use Safari or Internet Explorer at present.您目前无法使用 Safari 或 Internet Explorer。

Here's a complete example of how to use the file and directory entries API :以下是如何使用文件和目录条目 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 is supported by Chrome 13+, Firefox 50+ and Edge. webkitGetAsEntry受 Chrome 13+、Firefox 50+ 和 Edge 支持。

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

UPDATE: Since 2012 a lot has changed, see answers above instead.更新:自 2012 年以来发生了很多变化,请参阅上面的答案。 I leave this answer here for the sake of archeology.为了考古,我把这个答案留在这里。

The HTML5 spec does NOT say that when selecting a folder for upload, the browser should upload all contained files recursively. HTML5 规范没有说在选择要上传的文件夹时,浏览器应该递归上传所有包含的文件。

Actually, in Chrome/Chromium, you can upload a folder, but when you do it, it just uploads a meaningless 4KB file, which represents the directory.其实在 Chrome/Chromium 中,你可以上传一个文件夹,但是当你上传时,它只是上传一个无意义的 4KB 文件,代表目录。 Some servers-side applications like Alfresco can detect this, and warn the user that folders can not be uploaded:一些服务器端应用程序如Alfresco可以检测到这一点,并警告用户文件夹无法上传:

无法上传以下内容,因为它们是文件夹或大小为零字节:未定义

Does HTML5 allow drag-drop upload of folders or a folder tree? HTML5 是否允许拖放上传文件夹或文件夹树?

Only Chrome supports this feature.只有 Chrome 支持此功能。 It has failed to have any traction and is likely to be removed.它没有任何牵引力,很可能会被移除。

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

Recently stumbled upon the need to implement this in two of my projects so I created a bunch of utility functions to help with this.最近偶然发现需要在我的两个项目中实现这一点,所以我创建了一堆实用函数来帮助解决这个问题。

One creates a data-structure representing all the folders, files and relationship between them, like so 👇创建一个数据结构来表示所有文件夹、文件和它们之间的关系,就像这样👇

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

While the other just returns an Array of all the files (in all folders and sub-folders).而另一个只返回所有文件的数组(在所有文件夹和子文件夹中)。

Here's the link to the package: https://www.npmjs.com/package/file-system-utils这是包的链接: https : //www.npmjs.com/package/file-system-utils

I had been happy copy/pasting @grabantot 's solution until I met the 100 file limit issue.在遇到 100 个文件限制问题之前,我一直很高兴复制/粘贴 @grabantot 的解决方案。

@xlm 's solution overcomes the 100-file-limit, and it returns an array of FileEntry objects. @xlm 的解决方案克服了 100 个文件的限制,它返回一个 FileEntry 对象数组。

However in my project I need to extract the file paths from fileEntry objects.但是在我的项目中,我需要从 fileEntry 对象中提取文件路径。

This works if you have access to the ChromeFileSystem api:如果您有权访问 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;

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM