【发布时间】:2011-04-05 03:21:44
【问题描述】:
我还没有看到任何这样做的例子。 API 规范中不允许这样做吗?
我正在寻找一种简单的拖放解决方案来上传整个文件夹树的照片。
【问题讨论】:
-
input type=file相同:stackoverflow.com/questions/9518335/…
标签: javascript html file-upload drag-and-drop
我还没有看到任何这样做的例子。 API 规范中不允许这样做吗?
我正在寻找一种简单的拖放解决方案来上传整个文件夹树的照片。
【问题讨论】:
input type=file 相同:stackoverflow.com/questions/9518335/…
标签: javascript html file-upload drag-and-drop
现在可以使用 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/
【讨论】:
readEntries 不会返回目录中的所有内容。根据您提供的错误链接,我写了一个完整的答案:stackoverflow.com/a/53058574/885922
不幸的是,没有一个现有答案是完全正确的,因为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 也可以更直观,但正如文档所述:它是非标准/实验性功能,不在标准轨道上,不能期望适用于所有浏览器。
相关:
readEntries 将最多返回 100 个目录条目(验证为 Chrome 64)。readEntries 的正确用法(尽管没有代码)。readEntries。他还指出,Firefox 会返回目录中的所有条目(与 Chrome 不同),但鉴于规范,我们不能依赖这一点。【讨论】:
file(successCb, failureCb) 方法将所有FileSystemFileEntry 转换为File。如果您还需要完整路径,则应从 fileEntry.fullPath 获取(file.webkitRelativePath 只是名称)。
File 对象无法让我们轻松处理我们有一个目录并希望获取其文件或子目录的情况。这就是我们为什么叫dataTransferItemList[i].webkitGetAsEntry()而不是dataTransferItemList[i].getAsFile()的原因
File 似乎并不密集,所以我不会担心那个操作。我已经在生产中得到了这段代码,它可以轻松处理数以万计的文件。事实上,我的测试工具是任意嵌套的 40,000 个文件。内容本身的上传当然取决于文件大小、磁盘、网络等。
此函数将为您提供所有已删除文件的数组的承诺,例如 <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
【讨论】:
function getFilesWebkitDataTransferItems(dataTransfer) 应该是function getFilesWebkitDataTransferItems(items),for (entr of entries) 应该是for (let entr of entries)。
readEntries,直到返回一个空数组。
在this message HTML 5 邮件列表中,Ian Hickson 说:
HTML5 现在必须在 一次。浏览器可以让用户 一次选择多个文件,包括 跨多个目录;那是一个 有点超出规范的范围。
(另见原文feature proposal。) 因此可以肯定地假设他认为使用拖放上传文件夹也超出了范围。显然,提供单个文件取决于浏览器。
上传文件夹也会有一些其他困难,如Lars Gunther所述:
这个 […] 提案必须有两个 检查(如果可行的话):
最大大小,阻止某人上传包含多个文件的完整目录 一百个未压缩的原始图像...
即使忽略了接受属性也会进行过滤。 Mac OS 元数据 和 Windows 缩略图等应该是 省略。所有隐藏文件和 目录应该默认为 排除在外。
【讨论】:
现在您可以通过拖放和输入来上传目录。
<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
【讨论】:
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。
【讨论】:
这里有一个完整的例子来说明如何使用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
【讨论】:
HTML5 是否允许拖放上传文件夹或文件夹树?
只有 Chrome 支持此功能。它没有任何牵引力,很可能会被移除。
参考:https://developer.mozilla.org/en/docs/Web/API/DirectoryReader#readEntries
【讨论】:
readEntries 如果另一个readEntries 的调用仍在运行,则无法调用。 DirectoryReader API 设计不是最好的
更新:自 2012 年以来发生了很多变化,请参阅上面的答案。为了考古,我把这个答案留在这里。
HTML5 规范并未规定在选择要上传的文件夹时,浏览器应递归上传所有包含的文件。
实际上,在Chrome/Chromium中,你可以上传一个文件夹,但是当你这样做的时候,它只是上传了一个无意义的4KB文件,它代表了目录。一些服务器端应用程序,如Alfresco 可以检测到这一点,并警告用户无法上传文件夹:
【讨论】:
最近偶然发现需要在我的两个项目中实现这一点,所以我创建了一堆实用函数来帮助解决这个问题。
创建一个表示所有文件夹、文件和它们之间关系的数据结构,就像这样?
{
folders: [
{
name: string,
folders: Array,
files: Array
},
/* ... */
],
files: Array
}
而另一个只返回所有文件的数组(在所有文件夹和子文件夹中)。
【讨论】:
在遇到 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;
}
【讨论】: