Ermöglicht HTML5 das Hochladen von Ordnern oder eines Ordnerbaums per Drag & Drop?

Antworten:

80

Dank Chrome> = 21 ist dies jetzt möglich.

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

Weitere Informationen: https://protonet.info/blog/html5-experiment-drag-drop-of-folders/

Christopher Blum
quelle
9
Selbst zwei Jahre später scheinen IE und Firefox nicht bereit zu sein, dies umzusetzen.
Nicolas Raoul
8
Jetzt auch für Firefox: stackoverflow.com/a/33431704/195216 Es zeigt das Hochladen von Ordnern über Drag'n'Drop und über den Dialog in Chrome und Firefox!
dforce
2
Edge unterstützt dies ebenfalls.
ZachB
7
Wichtige Warnung: Der Code in dieser Antwort ist auf 100 Dateien in einem bestimmten Verzeichnis beschränkt. Siehe hier: bugs.chromium.org/p/chromium/issues/detail?id=514087
johnozbay
4
@johnozbay Es ist bedauerlich, dass mehr Leute Ihre wichtige Warnung aufgegriffen haben, und es ist nicht unbedingt ein Chromium-Problem, da die Spezifikation besagt, readEntriesdass nicht alle Einträge in einem Verzeichnis zurückgegeben werden. Basierend auf dem von Ihnen bereitgestellten Fehlerlink
xlm
49

Leider ist keine der vorhandenen Antworten vollständig korrekt, da readEntriesnicht unbedingt ALLE (Datei- oder Verzeichnis-) Einträge für ein bestimmtes Verzeichnis zurückgegeben werden. Dies ist Teil der API-Spezifikation (siehe Abschnitt Dokumentation unten).

Um tatsächlich alle Dateien zu erhalten, müssen wir readEntrieswiederholt (für jedes Verzeichnis, auf das wir stoßen) aufrufen, bis ein leeres Array zurückgegeben wird. Wenn wir dies nicht tun, werden wir einige Dateien / Unterverzeichnisse in einem Verzeichnis vermissen, z. B. in Chrome.readEntries werden höchstens 100 Einträge gleichzeitig zurückgegeben.

Verwenden von Promises ( await/ async), um die korrekte Verwendung von readEntries(da es asynchron ist) und Breitensuche (BFS) zum Durchlaufen der Verzeichnisstruktur deutlicher zu demonstrieren :

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

Vollständiges Arbeitsbeispiel für Codepen: https://codepen.io/anon/pen/gBJrOP

FWIW Ich habe dies nur aufgegriffen, weil ich bei Verwendung der akzeptierten Antwort nicht alle erwarteten Dateien in einem Verzeichnis mit 40.000 Dateien (viele Verzeichnisse mit weit über 100 Dateien / Unterverzeichnissen) zurückerhalten habe.

Dokumentation:

Dieses Verhalten ist in FileSystemDirectoryReader dokumentiert . Auszug mit Hervorhebung hinzugefügt:

readEntries ()
Gibt ein Array zurück, das einige Einträge des Verzeichnisses enthält . Jedes Element im Array ist ein Objekt, das auf FileSystemEntry basiert - normalerweise entweder FileSystemFileEntry oder FileSystemDirectoryEntry.

Um fair zu sein, könnte die MDN-Dokumentation dies in anderen Abschnitten klarer machen. In der Dokumentation zu readEntries () wird lediglich Folgendes angegeben :

Die Methode readEntries () ruft die Verzeichniseinträge im zu lesenden Verzeichnis ab und liefert sie in einem Array an die bereitgestellte Rückruffunktion

Die einzige Erwähnung / Andeutung, dass mehrere Aufrufe erforderlich sind, ist die Beschreibung des Parameters successCallback :

Wenn in diesem FileSystemDirectoryReader keine Dateien mehr vorhanden sind oder Sie readEntries () bereits aufgerufen haben, ist das Array leer.

Möglicherweise könnte die API auch intuitiver sein, aber wie in der Dokumentation angegeben: Es handelt sich um eine nicht standardmäßige / experimentelle Funktion, die sich nicht auf einer Standardspur befindet und nicht für alle Browser geeignet ist.

Verbunden:

  • Johnozbay kommentiert, dass in Chrome readEntrieshöchstens 100 Einträge für ein Verzeichnis zurückgegeben werden (verifiziert als Chrome 64).
  • Xan erklärt die korrekte Verwendung von readEntriesziemlich gut in dieser Antwort (wenn auch ohne Code).
  • Die Antwort von Pablo Barría Urenda wirdreadEntries ohne BFS asynchron korrekt aufgerufen . Er stellt außerdem fest, dass Firefox alle Einträge in einem Verzeichnis zurückgibt (im Gegensatz zu Chrome), aber wir können uns angesichts der Spezifikation nicht darauf verlassen.
xlm
quelle
4
Vielen Dank für das Shout-Out und die Veröffentlichung dieses Inhalts. SOF braucht mehr fantastische Mitglieder wie Sie! ✌🏻
Johnozbay
6
Ich schätze, dass @johnozbay mir nur Sorgen macht, dass es den Anschein hat, dass viele Benutzer diese kleine, aber bedeutende Tatsache bezüglich Spezifikation / API übersehen, und dass dieser Randfall (über 100 Dateien in einem Verzeichnis) nicht so unwahrscheinlich ist. Ich habe es erst bemerkt, als ich nicht alle Dateien zurückbekommen habe, die ich erwartet hatte. Ihr Kommentar hätte beantwortet werden sollen.
xlm
Wie erhalte ich die Dateigröße?
Madeo
Um alle relevanten Metadaten zu erhalten (Größe, lastModified, MIME - Typ), müssen Sie alle konvertieren , FileSystemFileEntryum Fileüber das file(successCb, failureCb)Verfahren. Wenn Sie auch den vollständigen Pfad benötigen, sollten Sie diesen von übernehmen fileEntry.fullPath( file.webkitRelativePathwird nur der Name sein).
Iskren Ivov Chernev
Dies scheint die beste Antwort zu sein, funktioniert aber in Chromium 86 nicht. Scheint in Firefox einwandfrei zu funktionieren. In Chromium werden Auswahlen mit Dateien hochgeladen, für ein Verzeichnis wird jedoch nichts hochgeladen, da readEntriesPromise () ein leeres Array zurückgibt.
Happybeing
15

Diese Funktion gibt Ihnen ein Versprechen für das Array aller abgelegten Dateien, wie <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)
      })
  })
}

Verwendung:

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

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

npm-Paket

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

Anwendungsbeispiel: https://github.com/grabantot/datatransfer-files-promise/blob/master/index.html

Grabantot
quelle
4
Dies sollte die neu akzeptierte Antwort sein. Es ist besser als andere Antworten, weil es ein Versprechen zurückgibt, wenn es vollständig ist. Aber es gab ein paar Fehler: function getFilesWebkitDataTransferItems(dataTransfer)sollte sein function getFilesWebkitDataTransferItems(items)und for (entr of entries)sollte sein for (let entr of entries).
RoccoB
1
Es werden nicht alle Dateien in einem Verzeichnis abgerufen (für Chrome werden nur 100 Einträge in einem Verzeichnis zurückgegeben). Spec legt fest, dass readEntrieswiederholt aufgerufen werden muss, bis ein leeres Array zurückgegeben wird.
xlm
@xlm Aktualisiertes npm-Paket. Jetzt werden> 100 Einträge verarbeitet.
Grabantot
Sehr hilfreich! Danke für die Lösung. Bisher ist dies die präziseste und sauberste. Dies sollte eine neu akzeptierte Antwort sein, da stimme ich zu.
Siddhartha Chowdhury
13

In dieser Nachricht an die HTML 5-Mailingliste sagt Ian Hickson:

HTML5 muss jetzt viele Dateien gleichzeitig hochladen. Mit Browsern können Benutzer mehrere Dateien gleichzeitig auswählen, auch in mehreren Verzeichnissen. Das liegt etwas außerhalb des Bereichs der Spezifikation.

(Siehe auch den ursprünglichen Funktionsvorschlag .) Man kann also davon ausgehen, dass er das Hochladen von Ordnern per Drag & Drop auch außerhalb des Gültigkeitsbereichs in Betracht zieht. Anscheinend liegt es am Browser, einzelne Dateien bereitzustellen.

Das Hochladen von Ordnern hätte auch einige andere Schwierigkeiten, wie von Lars Gunther beschrieben :

Dieser […] Vorschlag muss zwei Prüfungen enthalten (sofern überhaupt möglich):

  1. Maximale Größe, um zu verhindern, dass jemand ein vollständiges Verzeichnis mit mehreren hundert unkomprimierten Rohbildern hochlädt ...

  2. Filtern, auch wenn das Attribut accept weggelassen wird. Mac OS-Metadaten und Windows-Miniaturansichten usw. sollten weggelassen werden. Alle versteckten Dateien und Verzeichnisse sollten standardmäßig ausgeschlossen werden.

Marcel Korpel
quelle
Hmmm, ich stimme Punkt 2 zu ... aber nur solange der Webentwickler feststellen kann, ob er das Hochladen versteckter Dateien aktivieren möchte - da immer das Potenzial besteht, dass eine versteckte Datei funktionsfähig ist die Verwendung des hochgeladenen Ordners. Insbesondere, wenn der Ordner ein volles Dokument ist, das in mehrere Teile aufgeteilt ist, wie es eine endgültige Schnittdatei sein könnte.
Charles John Thompson III
Nicht einverstanden mit außerhalb des Geltungsbereichs: Dies ist eine Ursache für Inkompatibilitäten für etwas, das viele Menschen tun möchten, daher sollte es angegeben werden.
Ciro Santilli 法轮功 冠状 病 六四 事件 23
10

Jetzt können Sie Verzeichnisse per Drag & Drop und Eingabe hochladen.

<input type='file' webkitdirectory >

und für Drag & Drop (für Webkit-Browser).

Umgang mit Drag & Drop-Ordnern.

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

Ressourcen:

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

Konga Raju
quelle
1
Ist es möglich, dasselbe für das Herunterladen zu tun, ohne komprimierte Ordner zu verwenden?
user2284570
8

Firefox unterstützt jetzt das Hochladen von Ordnern ab dem 15. November 2016 in Version 50.0: https://developer.mozilla.org/en-US/Firefox/Releases/50#Files_and_directories

Sie können Ordner in Firefox ziehen und ablegen oder einen lokalen Ordner zum Hochladen durchsuchen und auswählen. Es werden auch Ordner unterstützt, die in Unterordnern verschachtelt sind.

Das bedeutet, dass Sie jetzt entweder Chrome, Firefox, Edge oder Opera verwenden können, um Ordner hochzuladen. Sie können derzeit weder Safari noch Internet Explorer verwenden.

Dan Roberts
quelle
3

Hier ist ein vollständiges Beispiel für die Verwendung der API für Datei- und Verzeichniseinträge :

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 wird von Chrome 13+, Firefox 50+ und Edge unterstützt.

Quelle: https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry

Paolo Moretti
quelle
1
Arbeitet großartig. Portiert auf Vue jsfiddle.net/KimNyholm/xua9kLny
Kim Nyholm
1

Ermöglicht HTML5 das Hochladen von Ordnern oder eines Ordnerbaums per Drag & Drop?

Nur Chrome unterstützt diese Funktion. Es hat keine Traktion und wird wahrscheinlich entfernt.

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

Basarat
quelle
Beeindruckend. Aus dem W3C-Hinweis unter diesem Link geht hervor, dass dies in der Tat nicht fortgesetzt wird. Was ist die Grundlage für die Annahme, dass es keine Traktion bekommen hat?
Bebbi
@bebbi kein anderer Browser-Anbieter hat es implementiert
Basarat
1
@ PabloBarríaUrenda Kommentar ist nicht wahr; Sein Problem bezieht sich wahrscheinlich auf seine Frage: stackoverflow.com/questions/51850469/…, die er gelöst / realisiert hat, readEntrieskann nicht aufgerufen werden, wenn noch ein weiterer Aufruf von ausgeführt readEntrieswird. Das DirectoryReader-API-Design ist nicht das beste
xlm
@xlm ja, in der Tat bist du richtig. Ich hatte dies gepostet, während ich selbst von dem Problem verwirrt war, aber ich habe es schließlich gelöst (und diesen Kommentar vergessen). Ich habe jetzt den verwirrenden Kommentar gelöscht.
Pablo Barría Urenda
1

UPDATE: Seit 2012 hat sich viel geändert, siehe stattdessen die obigen Antworten. Ich lasse diese Antwort hier der Archäologie zuliebe.

Die HTML5-Spezifikation besagt NICHT, dass der Browser bei der Auswahl eines Ordners zum Hochladen alle enthaltenen Dateien rekursiv hochladen sollte.

In Chrome / Chromium können Sie zwar einen Ordner hochladen, aber wenn Sie dies tun, wird nur eine bedeutungslose 4-KB-Datei hochgeladen, die das Verzeichnis darstellt. Einige serverseitige Anwendungen wie Alfresco können dies erkennen und den Benutzer warnen, dass Ordner nicht hochgeladen werden können:

Folgendes kann nicht hochgeladen werden, da es sich entweder um Ordner oder um eine Größe von null Byte handelt: undefiniert

Nicolas Raoul
quelle
@MoB: Vielleicht ist es tatsächlich eine Art Zeiger. Da sich die eigentliche Datei jedoch auf dem Client-Computer befindet, kann der Server-Computer mit diesem Zeiger natürlich nichts tun.
Nicolas Raoul
1

Ich bin kürzlich auf die Notwendigkeit gestoßen, dies in zwei meiner Projekte zu implementieren, und habe daher eine Reihe von Dienstprogrammfunktionen erstellt, um dies zu unterstützen.

Man erstellt eine Datenstruktur, die alle Ordner, Dateien und Beziehungen zwischen ihnen darstellt, wie so 👇

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

Während der andere nur ein Array aller Dateien zurückgibt (in allen Ordnern und Unterordnern).

Hier ist der Link zum Paket: https://www.npmjs.com/package/file-system-utils

Pava
quelle