Czy HTML5 umożliwia przesyłanie folderów lub drzewa folderów metodą przeciągnij i upuść?

82

Nie widziałem żadnych przykładów, które to robią. Czy to nie jest dozwolone w specyfikacji interfejsu API?

Szukam prostego rozwiązania typu „przeciągnij i upuść” do przesłania całego drzewa folderów ze zdjęciami.

Michael
źródło
To samo dotyczy input type=file: stackoverflow.com/questions/9518335/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

81

Teraz jest to możliwe dzięki 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);

Więcej informacji: https://protonet.info/blog/html5-experiment-drag-drop-of-folders/

Christopher Blum
źródło
9
Wydaje się, że nawet 2 lata później IE i Firefox nie chcą tego wdrożyć.
Nicolas Raoul,
8
Teraz także dla przeglądarki Firefox: stackoverflow.com/a/33431704/195216 Pokazuje przesyłanie folderów za pomocą funkcji drag'n'drop oraz okna dialogowego w przeglądarce Chrome i Firefox!
dforce
2
Edge również to obsługuje.
ZachB,
7
Ważne ostrzeżenie: kod w tej odpowiedzi jest ograniczony do 100 plików w danym katalogu. Zobacz tutaj: bugs.chromium.org/p/chromium/issues/detail?id=514087
johnozbay
4
@johnozbay to niefortunne, że więcej osób odebrało twoje ważne ostrzeżenie i niekoniecznie jest to problem z Chromium, ponieważ specyfikacja mówi, readEntriesże nie zwróci wszystkich wpisów w katalogu. Na podstawie podanego przez Ciebie linku do błędu napisałem pełną odpowiedź: stackoverflow.com/a/53058574/885922
xlm
49

Niestety żadna z istniejących odpowiedzi nie jest całkowicie poprawna, ponieważ readEntriesniekoniecznie zwróci WSZYSTKIE wpisy (plik lub katalog) dla danego katalogu. Jest to część specyfikacji API (patrz sekcja Dokumentacja poniżej).

Aby faktycznie pobrać wszystkie pliki, będziemy musieli wywoływać readEntrieswielokrotnie (dla każdego napotkanego katalogu), dopóki nie zwróci pustej tablicy. Jeśli tego nie zrobimy, pominiemy niektóre pliki / podkatalogi w katalogu, np. W przeglądarce Chrome, zwrócimy readEntriesmaksymalnie 100 wpisów na raz.

Używanie Promises ( await/ async) do wyraźniejszego zademonstrowania prawidłowego użycia readEntries(ponieważ jest asynchroniczne) i przeszukiwania wszerz (BFS) do przechodzenia po strukturze katalogów:

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

Kompletny przykład roboczy na Codepen: https://codepen.io/anon/pen/gBJrOP

FWIW Podniosłem to tylko dlatego, że nie odzyskałem wszystkich plików, których oczekiwałem w katalogu zawierającym 40 000 plików (wiele katalogów zawierających znacznie ponad 100 plików / podkatalogów), używając zaakceptowanej odpowiedzi.

Dokumentacja:

To zachowanie jest udokumentowane w FileSystemDirectoryReader . Fragment z dodanym naciskiem:

readEntries ()
Zwraca tablicę zawierającą pewną liczbę wpisów katalogu . Każdy element tablicy jest obiektem opartym na FileSystemEntry - zazwyczaj FileSystemFileEntry lub FileSystemDirectoryEntry.

Ale żeby być uczciwym, dokumentacja MDN mogłaby to wyjaśnić w innych sekcjach. Dokumentacja readEntries () po prostu zauważa:

Metoda readEntries () pobiera pozycje katalogu w czytanym katalogu i dostarcza je w tablicy do podanej funkcji zwrotnej

Jedyna wzmianka / wskazówka, że ​​potrzeba wielu wywołań, znajduje się w opisie parametru successCallback :

Jeśli nie ma żadnych plików lub już wywołałeś readEntries () w tym FileSystemDirectoryReader, tablica jest pusta.

Zapewne API mogłoby być również bardziej intuicyjne, ale jak zauważa dokumentacja: jest to funkcja niestandardowa / eksperymentalna, nie na ścieżce standardów i nie można oczekiwać, że będzie działać we wszystkich przeglądarkach.

Związane z:

  • johnozbay komentuje, że w przeglądarce Chrome readEntrieszwróci maksymalnie 100 wpisów dla katalogu (zweryfikowanego jako Chrome 64).
  • XanreadEntries dość dobrze wyjaśnia poprawne użycie w tej odpowiedzi (aczkolwiek bez kodu).
  • Odpowiedź Pabla Barríi Urendy poprawnie woła readEntriesw sposób asynchroniczny bez BFS. Zauważa również, że Firefox zwraca wszystkie wpisy w katalogu (w przeciwieństwie do Chrome), ale nie możemy na tym polegać, biorąc pod uwagę specyfikację.
xlm
źródło
4
Wielkie dzięki za okrzyk i upublicznienie tej zawartości. SOF potrzebuje więcej fantastycznych członków, takich jak Ty! ✌🏻
johnozbay
6
Doceniam to, że @johnozbay. Martwię się tylko, że wydaje się, że wielu użytkowników przeoczyło ten mały, ale istotny fakt dotyczący specyfikacji / API i tego skrajnego przypadku (ponad 100 plików w katalogu) nie jest tak nieprawdopodobne. Zrozumiałem to dopiero wtedy, gdy nie odzyskałem wszystkich plików, których się spodziewałem. Twój komentarz powinien być odpowiedzią.
XLM
Jak uzyskać rozmiar pliku?
Madeo
Aby uzyskać wszystkie istotne metadane (rozmiar, lastModified, typ MIME), musisz przekonwertować wszystkie FileSystemFileEntryna File, za pomocą file(successCb, failureCb)metody. Jeśli potrzebujesz również pełnej ścieżki, powinieneś ją pobrać z fileEntry.fullPath( file.webkitRelativePathbędzie tylko nazwa).
Iskren Ivov Chernev
Wydaje się, że to najlepsza odpowiedź, ale nie działa dla mnie w Chromium 86. Wydaje się, że działa dobrze w Firefoksie. W Chromium prześle zaznaczenia zawierające pliki, ale nic nie jest przesyłane do katalogu, ponieważ readEntriesPromise () zwraca pustą tablicę.
szczęśliwego bycia
15

Ta funkcja daje obietnicę dotyczącą tablicy wszystkich upuszczonych plików, na przykład <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)
      })
  })
}

Stosowanie:

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

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

pakiet npm

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

przykład użycia: https://github.com/grabantot/datatransfer-files-promise/blob/master/index.html

grabantot
źródło
4
To powinna być nowa zaakceptowana odpowiedź. Jest lepsza niż inne odpowiedzi, ponieważ po wypełnieniu zwraca obietnicę. Ale było kilka błędów: function getFilesWebkitDataTransferItems(dataTransfer)powinno być function getFilesWebkitDataTransferItems(items)i for (entr of entries)powinno być for (let entr of entries).
RoccoB,
1
W rzeczywistości nie pobierze wszystkich plików w katalogu (w przypadku Chrome zwróci tylko 100 wpisów w katalogu). Spec określa potrzebę readEntrieswielokrotnego wywoływania, dopóki nie zwróci pustej tablicy.
xlm
@xlm Zaktualizowany pakiet npm. Teraz obsługuje> 100 wpisów.
grabantot
Bardzo pomocny! Dzięki za rozwiązanie. Jak dotąd jest to najbardziej precyzyjne i czyste. To powinna być nowa zaakceptowana odpowiedź, zgadzam się.
Siddhartha Chowdhury,
13

W tej wiadomości do listy mailingowej HTML 5 Ian Hickson mówi:

HTML5 musi teraz przesyłać wiele plików naraz. Przeglądarki mogą pozwolić użytkownikom na wybieranie wielu plików jednocześnie, w tym w wielu katalogach; to trochę wykracza poza zakres specyfikacji.

(Zobacz także oryginalną propozycję funkcji ). Można więc bezpiecznie założyć, że rozważa przesyłanie folderów za pomocą metody przeciągania i upuszczania również poza zakresem. Najwyraźniej to przeglądarka musi obsługiwać poszczególne pliki.

Przesyłanie folderów wiązałoby się również z innymi trudnościami, które opisał Lars Gunther :

Ta […] propozycja musi mieć dwa sprawdzenia (jeśli w ogóle jest wykonalne):

  1. Maksymalny rozmiar, aby uniemożliwić komuś załadowanie pełnego katalogu zawierającego kilkaset nieskompresowanych surowych obrazów ...

  2. Filtrowanie, nawet jeśli pominięto atrybut accept. Należy pominąć metadane systemu Mac OS, miniatury systemu Windows itp. Wszystkie ukryte pliki i katalogi powinny być domyślnie wykluczone.

Marcel Korpel
źródło
Hmmm, zgadzam się z punktem 2 ... ale tylko pod warunkiem, że twórca stron internetowych może określić, czy chce włączyć przesyłanie ukrytych plików - ponieważ zawsze istnieje możliwość, że ukryty plik może działać korzystanie z przesłanego folderu. Zwłaszcza jeśli folder jest pełny w dokumencie podzielonym na wiele części, tak jak może to być ostateczny plik cięcia.
Charles John Thompson III
Nie zgadzam się z poza zakresem: jest to przyczyna niezgodności czegoś, co wiele osób chce zrobić, dlatego należy to określić.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
10

Teraz możesz przesyłać katalogi za pomocą przeciągania i upuszczania oraz wprowadzania.

<input type='file' webkitdirectory >

oraz do przeciągania i upuszczania (dla przeglądarek webkit).

Obsługa folderów typu „przeciągnij i upuść”.

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

Zasoby:

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

Konga Raju
źródło
1
Czy można zrobić to samo w przypadku pobierania bez używania skompresowanych folderów?
user2284570
8

Firefox obsługuje teraz przesyłanie folderów od 15 listopada 2016 r. W wersji 50.0: https://developer.mozilla.org/en-US/Firefox/Releases/50#Files_and_directories

Możesz przeciągać i upuszczać foldery do przeglądarki Firefox lub przeglądać i wybierać folder lokalny do przesłania. Obsługuje również foldery zagnieżdżone w podfolderach.

Oznacza to, że możesz teraz przesyłać foldery za pomocą przeglądarki Chrome, Firefox, Edge lub Opera. Obecnie nie możesz używać Safari ani Internet Explorera.

Dan Roberts
źródło
3

Oto pełny przykład korzystania z interfejsu API plików i katalogów :

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 jest obsługiwany przez Chrome 13+, Firefox 50+ i Edge.

Źródło: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry

Paolo Moretti
źródło
1
Działa świetnie. Przeniesiono na Vue jsfiddle.net/KimNyholm/xua9kLny
Kim Nyholm,
1

Czy HTML5 umożliwia przesyłanie folderów lub drzewa folderów metodą przeciągnij i upuść?

Tylko Chrome obsługuje tę funkcję. Nie miał żadnej przyczepności i prawdopodobnie zostanie usunięty.

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

basarat
źródło
Łał. Mówiąc z notatki W3C pod tym linkiem, to rzeczywiście nie jest kontynuowane. Jaka jest podstawa przypuszczenia, że ​​nie uzyskał żadnej przyczepności?
bebbi
@bebbi żaden inny producent przeglądarek go nie wdrożył
basarat
1
@ Komentarz PabloBarríaUrenda nie jest prawdziwy; jego problem prawdopodobnie odnosi się do jego pytania: stackoverflow.com/questions/51850469/, które rozwiązał / zrealizował, readEntriesnie może zostać wywołane, jeśli readEntriesnadal trwa inne wywołanie . Projekt interfejsu DirectoryReader API nie jest najlepszy
xlm
@xlm tak, rzeczywiście masz rację. Opublikowałem to, gdy sam byłem zdziwiony problemem, ale w końcu go rozwiązałem (i zapomniałem o tym komentarzu). Teraz usunąłem mylący komentarz.
Pablo Barría Urenda
1

AKTUALIZACJA: Od 2012 roku wiele się zmieniło, zamiast tego zobacz odpowiedzi powyżej. Zostawiam tę odpowiedź tutaj ze względu na archeologię.

Specyfikacja HTML5 NIE mówi, że wybierając folder do przesłania, przeglądarka powinna przesyłać wszystkie zawarte w nim pliki rekurencyjnie.

Właściwie w Chrome / Chromium możesz przesłać folder, ale kiedy to zrobisz, po prostu przesyła bezsensowny plik 4KB, który reprezentuje katalog. Niektóre aplikacje po stronie serwera, takie jak Alfresco, mogą to wykryć i ostrzec użytkownika, że ​​foldery nie mogą zostać przesłane:

Nie można przesłać następujących plików, ponieważ są to foldery lub mają zerowy rozmiar: niezdefiniowane

Nicolas Raoul
źródło
@MoB: może rzeczywiście jest to jakiś wskaźnik. Ale ponieważ rzeczywisty plik znajduje się na komputerze klienta, serwer oczywiście nie będzie w stanie nic zrobić z tym wskaźnikiem.
Nicolas Raoul
1

Niedawno natknąłem się na potrzebę zaimplementowania tego w dwóch moich projektach, więc stworzyłem kilka funkcji narzędziowych, które pomogą w tym.

Tworzy się strukturę danych reprezentującą wszystkie foldery, pliki i relacje między nimi, w ten sposób 👇

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

Podczas gdy drugi po prostu zwraca tablicę wszystkich plików (we wszystkich folderach i podfolderach).

Oto link do pakietu: https://www.npmjs.com/package/file-system-utils

Pava
źródło