HTML5 có cho phép kéo-thả tải lên các thư mục hoặc một cây thư mục không?


81

Tôi chưa thấy bất kỳ ví dụ nào làm điều này. Điều này không được phép trong thông số API?

Tôi đang tìm kiếm một giải pháp kéo thả dễ dàng để tải lên toàn bộ cây thư mục ảnh.



Câu trả lời:


80

Bây giờ có thể, nhờ 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);

Thông tin thêm: https://protonet.info/blog/html5-experiment-drag-drop-of-folders/


9
Thậm chí 2 năm sau, IE và Firefox dường như không sẵn sàng thực hiện điều này.
Nicolas Raoul

8
Bây giờ, đối với Firefox cũng vậy: stackoverflow.com/a/33431704/195216 Nó hiển thị tải lên thư mục qua drag'n'drop và qua hộp thoại trong chrome và firefox!
bắt đầu từ

2
Edge cũng hỗ trợ điều này.
ZachB

7
Cảnh báo quan trọng: Mã trong câu trả lời này được giới hạn trong 100 tệp trong một thư mục nhất định. Xem tại đây: bug.chromium.org/p/chromium/issues/detail?id=514087
johnozbay

4
@johnozbay thật không may khi có nhiều người nhận được cảnh báo quan trọng của bạn và đó không nhất thiết là vấn đề Chromium vì thông số kỹ thuật cho biết readEntriessẽ không trả lại tất cả các yêu cầu trong một thư mục. Dựa vào liên kết lỗi mà bạn cung cấp, tôi đã viết lên một câu trả lời hoàn chỉnh: stackoverflow.com/a/53058574/885922
XLM

46

Thật không may, không có câu trả lời hiện có nào là hoàn toàn chính xác vì readEntriessẽ không nhất thiết trả về TẤT CẢ các mục nhập (tệp hoặc thư mục) cho một thư mục nhất định. Đây là một phần của đặc tả API (xem phần Tài liệu bên dưới).

Để thực sự nhận được tất cả các tệp, chúng tôi sẽ cần gọi readEntriesnhiều lần (đối với từng thư mục mà chúng tôi gặp phải) cho đến khi nó trả về một mảng trống. Nếu không, chúng tôi sẽ bỏ lỡ một số tệp / thư mục con trong một thư mục, ví dụ như trong Chrome, readEntriessẽ chỉ trả về tối đa 100 mục nhập cùng một lúc.

Sử dụng Promises ( await/ async) để chứng minh rõ ràng hơn cách sử dụng chính xác readEntries(vì nó không đồng bộ) và tìm kiếm theo chiều rộng (BFS) để duyệt qua cấu trúc thư mục:

// 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);
  }
}

Toàn bộ ví dụ làm việc trên Codepen: https://codepen.io/anon/pen/gBJrOP

FWIW Tôi chỉ chọn điều này vì tôi không lấy lại được tất cả các tệp mà tôi mong đợi trong thư mục chứa 40.000 tệp (nhiều thư mục chứa hơn 100 tệp / thư mục con) khi sử dụng câu trả lời được chấp nhận.

Tài liệu:

Hành vi này được ghi lại trong FileSystemDirectoryReader . Đoạn trích có nhấn mạnh thêm:

readEntries ()
Trả về một mảng có chứa một số mục nhập của thư mục . Mỗi mục trong mảng là một đối tượng dựa trên FileSystemEntry — thường là FileSystemFileEntry hoặc FileSystemDirectoryEntry.

Nhưng công bằng mà nói, tài liệu MDN có thể làm rõ hơn điều này trong các phần khác. Tài liệu readEntries () chỉ ghi chú:

Phương thức readEntries () truy xuất các mục nhập thư mục trong thư mục đang được đọc và phân phối chúng trong một mảng tới hàm gọi lại được cung cấp

Và đề cập / gợi ý duy nhất rằng cần có nhiều cuộc gọi nằm trong mô tả của tham số SuccessCallback :

Nếu không còn tệp nào hoặc bạn đã gọi readEntries () trên FileSystemDirectoryReader này, mảng sẽ trống.

Có thể cho rằng API cũng có thể trực quan hơn, nhưng như tài liệu lưu ý: nó là một tính năng không tiêu chuẩn / thử nghiệm, không theo tiêu chuẩn và không thể hoạt động cho tất cả các trình duyệt.

Có liên quan:

  • johnozbay nhận xét rằng trên Chrome, readEntriessẽ trả về tối đa 100 mục nhập cho một thư mục (được xác minh là Chrome 64).
  • Xan giải thích cách sử dụng chính xác của readEntrieskhá tốt trong câu trả lời này (mặc dù không có mã).
  • Câu trả lời của Pablo Barría Urenda gọi chính xác readEntriestheo cách không đồng bộ mà không có BFS. Ông cũng lưu ý rằng Firefox trả về tất cả các mục nhập trong một thư mục (không giống như Chrome) nhưng chúng tôi không thể dựa vào điều này với đặc điểm kỹ thuật.

4
Cảm ơn rất nhiều cho lời cảm ơn và đưa nội dung này ra khỏi đó. SOF cần thêm những thành viên tuyệt vời như chính bạn! ✌🏻
johnozbay

6
Tôi đánh giá cao rằng @johnozbay Tôi chỉ lo ngại rằng có vẻ như nhiều người dùng đang bỏ qua thực tế nhỏ nhưng quan trọng này về re: đặc tả / API và trường hợp biên này (hơn 100 tệp trong một thư mục) không phải là điều khó xảy ra. Tôi chỉ nhận ra điều đó khi tôi không nhận lại được tất cả các tệp mà tôi mong đợi. Bình luận của bạn lẽ ra phải có câu trả lời.
xlm

Làm thế nào để có được kích thước tệp?
Madeo

Để nhận tất cả siêu dữ liệu có liên quan (kích thước, lastModified, loại mime), bạn cần chuyển đổi tất cả FileSystemFileEntrythành File, thông qua file(successCb, failureCb)phương thức. Nếu bạn cũng cần đường dẫn đầy đủ, bạn nên lấy từ đó fileEntry.fullPath( file.webkitRelativePathsẽ chỉ là tên).
Iskren Ivov Chernev

Đây có vẻ là câu trả lời tốt nhất, nhưng không hoạt động với tôi trong Chromium 86. Có vẻ như hoạt động tốt trong Firefox. Trong Chromium, nó sẽ tải lên các lựa chọn có chứa tệp, nhưng không có gì được tải lên cho một thư mục vì readEntriesPromise () trả về một mảng trống.
chúc mừng

15

Hàm này sẽ cung cấp cho bạn một lời hứa cho mảng tất cả các tệp bị loại bỏ, như <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)
      })
  })
}

Sử dụng:

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

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

gói npm

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

ví dụ sử dụng: https://github.com/grabantot/datatransfer-files-promise/blob/master/index.html


4
Đây phải là câu trả lời mới được chấp nhận. Nó tốt hơn các câu trả lời khác vì nó trả về một lời hứa khi hoàn thành. Nhưng có một vài sai lầm: function getFilesWebkitDataTransferItems(dataTransfer)nên function getFilesWebkitDataTransferItems(items)for (entr of entries)nên làm for (let entr of entries).
RoccoB

1
Sẽ không thực sự nhận được tất cả các tệp trong một thư mục (đối với Chrome, nó sẽ chỉ trả về 100 mục nhập trong một thư mục). Spec quy định sự cần thiết phải gọi readEntriesnhiều lần cho đến khi nó trả về một mảng trống.
xlm,

@xlm Đã cập nhật gói npm. Bây giờ nó xử lý> 100 mục nhập.
grabantot

Rất hữu ích! Cảm ơn vì giải pháp. Cho đến nay đây là một trong những chính xác và sạch sẽ nhất. Đây phải là câu trả lời mới được chấp nhận, tôi đồng ý.
Siddhartha Chowdhury

13

Trong thông báo gửi đến danh sách gửi thư HTML 5 này, Ian Hickson nói:

HTML5 hiện phải tải lên nhiều tệp cùng một lúc. Trình duyệt có thể cho phép người dùng chọn nhiều tệp cùng một lúc, bao gồm trên nhiều thư mục; đó là một chút ngoài phạm vi của thông số kỹ thuật.

(Cũng xem đề xuất tính năng ban đầu .) Vì vậy, có thể an toàn khi cho rằng anh ấy cân nhắc việc tải lên các thư mục bằng cách sử dụng kéo và thả cũng nằm ngoài phạm vi. Rõ ràng là tùy thuộc vào trình duyệt để phân phát các tệp riêng lẻ.

Việc tải lên các thư mục cũng sẽ gặp một số khó khăn khác, như được mô tả bởi Lars Gunther :

Đề xuất […] này phải có hai lần kiểm tra (nếu nó có thể thực hiện được):

  1. Kích thước tối đa, để ngăn ai đó tải lên một thư mục đầy đủ gồm hàng trăm hình ảnh thô không nén ...

  2. Lọc ngay cả khi thuộc tính chấp nhận bị bỏ qua. Siêu dữ liệu Mac OS và hình thu nhỏ Windows, v.v. nên được bỏ qua. Tất cả các tệp và thư mục ẩn sẽ được mặc định loại trừ.


Rất tiếc, tôi đồng ý ở điểm 2 ... nhưng chỉ miễn là có cách để nhà phát triển web xác định xem họ có muốn cho phép tải lên các tệp ẩn hay không - vì luôn có khả năng một tệp ẩn có thể hoạt động việc sử dụng thư mục đã tải lên. Đặc biệt nếu thư mục là một tài liệu đầy đủ được chia thành nhiều phần giống như một tệp cắt cuối cùng có thể được.
Charles John Thompson III

Không đồng ý với ngoài phạm vi: đây là một nguyên nhân gây ra sự không tương thích đối với một thứ mà nhiều người muốn làm, vì vậy nó cần được chỉ rõ.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

10

Giờ đây, bạn có thể tải lên các thư mục bằng cả thao tác kéo thả và nhập liệu.

<input type='file' webkitdirectory >

và kéo và thả (Đối với trình duyệt webkit).

Xử lý các thư mục kéo và thả.

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

Tài nguyên:

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


1
Có thể thực hiện tương tự để tải xuống mà không sử dụng thư mục nén không?
user2284570

8

Firefox hiện hỗ trợ tải lên thư mục, kể từ ngày 15 tháng 11 năm 2016, trong v50.0: https://developer.mozilla.org/en-US/Firefox/Releases/50#Files_and_directories

Bạn có thể kéo và thả các thư mục vào Firefox hoặc bạn có thể duyệt và chọn một thư mục cục bộ để tải lên. Nó cũng hỗ trợ các thư mục được lồng trong các thư mục con.

Điều đó có nghĩa là bây giờ bạn có thể sử dụng Chrome, Firefox, Edge hoặc Opera để tải lên các thư mục. Bạn không thể sử dụng Safari hoặc Internet Explorer hiện tại.


3

Dưới đây là một ví dụ đầy đủ về cách sử dụng API mục nhập tệp và thư mục :

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 được hỗ trợ bởi Chrome 13+, Firefox 50+ và Edge.

Nguồn: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry


Làm việc tuyệt vời. Được chuyển đến Vue jsfiddle.net/KimNyholm/xua9kLny
Kim Nyholm

1

HTML5 có cho phép kéo-thả tải lên các thư mục hoặc một cây thư mục không?

Chỉ Chrome mới hỗ trợ tính năng này. Nó đã không có bất kỳ lực kéo nào và có khả năng bị loại bỏ.

Tham khảo: https://developer.mozilla.org/en/docs/Web/API/DirectoryReader#readEntries


Chà. Kể từ W3C Note tại liên kết đó, điều này thực sự không được tiếp tục. Cơ sở của giả định rằng nó đã không nhận được bất kỳ lực kéo nào?
bebbi

@bebbi không có nhà cung cấp trình duyệt nào khác triển khai nó
basarat 29/02/16

1
@ PabloBarríaUrenda nhận xét không đúng sự thật; vấn đề của anh ấy có thể đề cập đến câu hỏi của anh ấy: stackoverflow.com/questions/51850469/… mà anh ấy đã giải quyết / nhận ra readEntrieskhông thể được gọi nếu một cuộc gọi khác readEntriesvẫn đang được chạy. Thiết kế DirectoryReader API không phải là tốt nhất
XLM

@xlm vâng, thực sự là bạn đã đúng. Tôi đã đăng điều này trong khi bản thân tôi đang phân vân về vấn đề này, nhưng cuối cùng tôi đã giải quyết được nó (và quên mất bình luận này). Bây giờ tôi đã xóa bình luận khó hiểu.
Pablo Barría Urenda,

1

CẬP NHẬT: Kể từ năm 2012 đã có nhiều thay đổi, thay vào đó hãy xem câu trả lời ở trên. Tôi để câu trả lời này ở đây vì lợi ích của khảo cổ học.

Thông số HTML5 KHÔNG nói rằng khi chọn một thư mục để tải lên, trình duyệt phải tải lên một cách đệ quy tất cả các tệp chứa trong đó.

Trên thực tế, trong Chrome / Chromium, bạn có thể tải lên một thư mục, nhưng khi bạn làm điều đó, nó chỉ tải lên một tệp 4KB vô nghĩa, đại diện cho thư mục. Một số ứng dụng phía máy chủ như Alfresco có thể phát hiện điều này và cảnh báo người dùng rằng không thể tải lên các thư mục:

Không thể tải lên phần sau vì chúng là thư mục hoặc có kích thước bằng không byte: undefined


@MoB: có lẽ nó thực sự là một loại con trỏ. Nhưng vì tệp thực sự nằm trên máy khách, tất nhiên máy chủ sẽ không thể làm gì với con trỏ này.
Nicolas Raoul

1

Gần đây, tình cờ nhận thấy nhu cầu thực hiện điều này trong hai dự án của tôi, vì vậy tôi đã tạo một loạt các chức năng tiện ích để trợ giúp việc này.

Người ta tạo ra một cấu trúc dữ liệu đại diện cho tất cả các thư mục, tệp và mối quan hệ giữa chúng, giống như vậy

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

Trong khi cái kia chỉ trả về một Mảng của tất cả các tệp (trong tất cả các thư mục và thư mục con).

Đây là liên kết đến gói: https://www.npmjs.com/package/file-system-utils

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.