Чи дозволяє HTML5 перетягування папки або дерево папок?


82

Я не бачив жодного прикладу, що це робить. Це не дозволено в специфікації API?

Я шукаю просте рішення перетягування для завантаження цілого дерева папок фотографій.



Відповіді:


81

Тепер це можливо завдяки 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, схоже, не бажають це впроваджувати.
Ніколас Рауль

8
Тепер і для Firefox: stackoverflow.com/a/33431704/195216 Він показує завантаження папок за допомогою drag'n'drop та за допомогою діалогового вікна в chrome та firefox!
dforce

2
Edge це теж підтримує.
ZachB

7
Важливе застереження: Код у цій відповіді обмежений 100 файлами в даному каталозі. Дивіться тут: bugs.chromium.org/p/chromium/issues/detail?id=514087
johnozbay 02.03.18

4
@johnozbay прикро, що більше людей підхопило твоє важливе попередження, і це не обов’язково проблема Chromium, оскільки в специфікації сказано readEntries, що не буде повернуто всі записи в каталозі. Грунтуючись на засланні помилки вашого надані, я написав до повної відповіді: stackoverflow.com/a/53058574/885922
XLM

49

На жаль, жодна з існуючих відповідей не є абсолютно правильною, оскільки 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 :

Якщо файлів не залишилося, або ви вже викликали readEntries () на цьому FileSystemDirectoryReader, масив порожній.

Можливо, API також може бути більш інтуїтивно зрозумілим, але, як зазначається в документації: це нестандартна / експериментальна функція, не на стандартній доріжці, і не можна очікувати, що вона буде працювати для всіх браузерів.

Пов’язані:

  • johnozbay коментує, що в Chrome readEntriesповерне не більше 100 записів для каталогу (перевірено як Chrome 64).
  • readEntriesУ цій відповіді Ксан досить добре пояснює правильне вживання (хоча і без коду).
  • Відповідь Пабло Баррії Уренди правильно називає readEntriesасинхронно без BFS. Він також зазначає, що Firefox повертає всі записи в каталог (на відміну від Chrome), але ми не можемо на це покладатися, враховуючи специфікацію.

4
Велике спасибі за вигук та отримання цього вмісту. SOF потребує більше фантастичних членів, як ти! ✌🏻
johnozbay

6
Я вдячний, що @johnozbay мене просто турбує, що, здається, багато користувачів не помічають цей невеликий, але важливий факт re: specification / API та цей крайній випадок (більше 100 файлів у каталозі) - це не так малоймовірно. Я зрозумів це лише тоді, коли не отримав назад усі очікувані файли. Ваш коментар повинен був бути відповіддю.
xlm

Як отримати розмір файлу?
Madeo

Щоб отримати всі відповідні метадані (розмір, lastModified, тип mime), вам потрібно перетворити всі FileSystemFileEntryна File, за допомогою file(successCb, failureCb)методу. Якщо вам також потрібен повний шлях, ви повинні взяти це з fileEntry.fullPath( file.webkitRelativePathбуде лише ім'я).
Іскрен Івов Чернев

Це здається найкращою відповіддю, але в Chromium 86 для мене це не працює. Здається, у Firefox добре працює. У Chromium він буде завантажувати виділення, що містять файли, але для каталогу не завантажується нічого, оскільки readEntriesPromise () повертає порожній масив.
happybeing

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 записів у каталозі). Spec передбачає необхідність readEntriesповторного дзвінка, поки не поверне порожній масив.
xlm

@xlm Оновлений пакет npm. Тепер він обробляє> 100 записів.
grabantot

Дуже корисний! Дякую за рішення. Наразі це найточніший і найчистіший. Це повинна бути нова прийнята відповідь, я згоден.
Siddhartha Chowdhury

13

У цьому повідомленні до списку розсилки HTML 5 Ян Хіксон говорить:

HTML5 тепер повинен завантажувати багато файлів одночасно. Браузери можуть дозволити користувачам вибирати декілька файлів одночасно, включаючи різні каталоги; це трохи вийшло за межі специфікації.

(Також див. Оригінальну пропозицію функції .) Тож можна припустити, що він вважає, що завантаження папок за допомогою функції перетягування також виходить за рамки. Очевидно, що браузер повинен обслуговувати окремі файли.

Завантаження папок також матиме деякі інші труднощі, як описано Ларсом Гюнтером :

Ця […] пропозиція повинна мати дві перевірки (якщо це взагалі можливо):

  1. Максимальний розмір, щоб хтось не міг завантажувати повний каталог із декількох сотень нестиснених вихідних зображень ...

  2. Фільтрування, навіть якщо атрибут accept опущений. Метадані Mac OS, ескізи Windows тощо слід опустити. Усі приховані файли та каталоги за замовчуванням повинні бути виключені.


Мда, я погоджуюсь з пунктом 2 ... але лише до тих пір, поки веб-розробник може визначити, чи хоче він увімкнути завантаження прихованих файлів - оскільки завжди існує можливість, що прихований файл може працювати використання завантаженої папки. Особливо, якщо папка є повною в документі, розділеною на кілька частин, як це може бути файл остаточного вирізу.
Чарльз Джон Томпсон III

Не погоджуйтесь із непідтримкою: це є причиною несумісності того, що багато людей хочуть зробити, тому це слід вказати.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

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

8

Від 15 листопада 2016 року Firefox підтримує завантаження папок у версії 50: https://developer.mozilla.org/en-US/Firefox/Releases/50#Files_and_directories

Ви можете перетягувати папки у Firefox або переглядати та вибирати локальну папку для завантаження. Він також підтримує папки, вкладені в підпапки.

Це означає, що тепер ви можете використовувати Chrome, Firefox, Edge або Opera для завантаження папок. Зараз ви не можете користуватися Safari чи Internet Explorer.


3

Ось повний приклад того, як використовувати 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 підтримується Chrome 13+, Firefox 50+ та Edge.

Джерело: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry


1
Чудово працює. Портоване на Vue jsfiddle.net/KimNyholm/xua9kLny
Kim Nyholm

1

Чи дозволяє HTML5 перетягування папки або дерево папок?

Цю функцію підтримує лише Chrome. Він не мав жодної тяги і, ймовірно, буде знятий.

Посилання: https://developer.mozilla.org/en/docs/Web/API/DirectoryReader#readEntries


Ого. Розповідаючи з примітки W3C за цим посиланням, це справді не продовжується. На чому грунтується припущення, що воно не змогло натягнути жодної тяги?
беббі

@bebbi жоден інший постачальник браузерів не впровадив це
basarat

1
@ PabloBarríaUrenda коментар не відповідає дійсності; його проблема, швидше за все, посилається на його запитання: stackoverflow.com/questions/51850469/…, яке він вирішив / зрозумів, readEntriesне можна викликати, якщо ще один виклик readEntriesвсе ще виконується. Дизайн API DirectoryReader не найкращий
xlm

@xlm так, справді ти маєш рацію. Я опублікував це, хоча сам був здивований проблемою, але врешті-решт вирішив (і забув про цей коментар). Зараз я видалив заплутаний коментар.
Пабло Баррія Уренда,

1

ОНОВЛЕННЯ: З 2012 року багато чого змінилося, натомість див. Відповіді вище. Цю відповідь я залишаю тут заради археології.

Специфікація HTML5 НЕ говорить, що при виборі папки для завантаження браузер повинен рекурсивно завантажувати всі файли, що містяться.

Насправді в Chrome / Chromium ви можете завантажити папку, але коли ви це робите, вона просто завантажує безглуздий файл розміром 4 КБ, який представляє каталог. Деякі додатки на стороні сервера, такі як Alfresco, можуть це виявити та попереджати користувача про те, що папки завантажувати не можна:

Наступні файли не можна завантажувати, оскільки вони є або папками, або мають нульовий байт: undefined


@MoB: можливо, це справді якийсь вказівник. Але оскільки фактичний файл знаходиться на клієнтській машині, серверна машина, звичайно, не зможе нічого зробити з цим покажчиком.
Ніколас Рауль,

1

Нещодавно натрапив на необхідність реалізувати це у двох своїх проектах, тому я створив купу допоміжних функцій, які допоможуть у цьому.

Створюється структура даних, що представляє всі папки, файли та взаємозв'язок між ними, наприклад 👇

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

У той час як інший просто повертає масив усіх файлів (у всіх папках та підпапках).

Ось посилання на пакет: https://www.npmjs.com/package/file-system-utils

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.