¿HTML5 permite la carga mediante arrastrar y soltar de carpetas o un árbol de carpetas?

Respuestas:

80

Ahora es posible gracias a 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);

Más información: https://protonet.info/blog/html5-experiment-drag-drop-of-folders/

Christopher Blum
fuente
9
Incluso 2 años después, IE y Firefox no parecen estar dispuestos a implementar esto.
Nicolas Raoul
8
Ahora, también para Firefox: stackoverflow.com/a/33431704/195216 ¡Muestra la carga de carpetas a través de arrastrar y soltar y a través del diálogo en Chrome y Firefox!
dforce
2
Edge también admite esto.
ZachB
7
Advertencia importante: el código de esta respuesta está limitado a 100 archivos en un directorio determinado. Ver aquí: bugs.chromium.org/p/chromium/issues/detail?id=514087
johnozbay
4
@johnozbay es lamentable que más personas hayan captado tu advertencia importante, y no es necesariamente un problema de Chromium ya que la especificación dice readEntriesque no devolverá todos los enteros en un directorio. Según el enlace de error que proporcionó, escribí una respuesta completa: stackoverflow.com/a/53058574/885922
xlm
46

Desafortunadamente, ninguna de las respuestas existentes es completamente correcta porque readEntriesno necesariamente devolverá TODAS las entradas (archivo o directorio) para un directorio determinado. Esto es parte de la especificación API (consulte la sección de documentación a continuación).

Para obtener todos los archivos, necesitaremos llamar readEntriesrepetidamente (para cada directorio que encontremos) hasta que devuelva una matriz vacía. Si no lo hacemos, perderemos algunos archivos / subdirectorios en un directorio, por ejemplo, en Chrome, readEntriessolo devolverá un máximo de 100 entradas a la vez.

Usando Promises ( await/ async) para demostrar más claramente el uso correcto de readEntries(ya que es asincrónico) y la búsqueda en amplitud (BFS) para recorrer la estructura del directorio:

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

Ejemplo de trabajo completo en Codepen: https://codepen.io/anon/pen/gBJrOP

FWIW Solo recogí esto porque no estaba recuperando todos los archivos que esperaba en un directorio que contiene 40,000 archivos (muchos directorios que contienen más de 100 archivos / subdirectorios) cuando usé la respuesta aceptada.

Documentación:

Este comportamiento está documentado en FileSystemDirectoryReader . Extracto con énfasis agregado:

readEntries ()
Devuelve una matriz que contiene algunas entradas del directorio . Cada elemento de la matriz es un objeto basado en FileSystemEntry, generalmente FileSystemFileEntry o FileSystemDirectoryEntry.

Pero para ser justos, la documentación de MDN podría aclarar esto en otras secciones. La documentación readEntries () simplemente señala:

El método readEntries () recupera las entradas del directorio dentro del directorio que se está leyendo y las entrega en una matriz a la función de devolución de llamada proporcionada

Y la única mención / sugerencia de que se necesitan varias llamadas está en la descripción del parámetro successCallback :

Si no quedan archivos, o si ya ha llamado readEntries () en este FileSystemDirectoryReader, la matriz está vacía.

Podría decirse que la API también podría ser más intuitiva, pero como señala la documentación: es una característica no estándar / experimental, no está en una pista de estándares y no se puede esperar que funcione para todos los navegadores.

Relacionado:

  • johnozbay comenta que en Chrome, readEntriesdevolverá como máximo 100 entradas para un directorio (verificado como Chrome 64).
  • Xan explica el uso correcto de readEntriesbastante bien en esta respuesta (aunque sin código).
  • La respuesta de Pablo Barría Urenda llama correctamente readEntriesde forma asincrónica sin BFS. También señala que Firefox devuelve todas las entradas en un directorio (a diferencia de Chrome), pero no podemos confiar en esto dada la especificación.
xlm
fuente
4
Muchas gracias por el reconocimiento y difundir este contenido. ¡SOF necesita más miembros fantásticos como tú! ✌🏻
johnozbay
6
Aprecio que @johnozbay, solo me preocupa que parece que muchos usuarios están pasando por alto este hecho pequeño pero significativo sobre la especificación / API y este caso de borde (más de 100 archivos en un directorio) no es tan improbable. Solo me di cuenta cuando no estaba recuperando todos los archivos que esperaba. Tu comentario debería haber sido respuesta.
xlm
¿Cómo obtener el tamaño del archivo?
Madeo
Para obtener todos los metadatos relevantes (tamaño, lastModified, tipo mime), debe convertir todos FileSystemFileEntrya File, a través del file(successCb, failureCb)método. Si también necesita la ruta completa, debe tomarla de fileEntry.fullPath( file.webkitRelativePathserá solo el nombre).
Iskren Ivov Chernev
Esta parece ser la mejor respuesta, pero no me funciona en Chromium 86. Parece que funciona bien en Firefox. En Chromium, cargará selecciones que contienen archivos, pero no se carga nada para un directorio porque readEntriesPromise () devuelve una matriz vacía.
happybeing
15

Esta función le dará una promesa para una matriz de todos los archivos caídos, como <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)
      })
  })
}

Uso:

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

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

paquete npm

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

ejemplo de uso: https://github.com/grabantot/datatransfer-files-promise/blob/master/index.html

grabantot
fuente
4
Esta debería ser la nueva respuesta aceptada. Es mejor que otras respuestas porque devuelve una promesa cuando se completa. Pero hubo algunos errores: function getFilesWebkitDataTransferItems(dataTransfer)debería ser function getFilesWebkitDataTransferItems(items)y for (entr of entries)debería ser for (let entr of entries).
RoccoB
1
En realidad, no obtendrá todos los archivos en un directorio (para Chrome, solo devolverá 100 entradas en un directorio). Spec estipula la necesidad de llamar readEntriesrepetidamente hasta que devuelve una matriz vacía.
xlm
@xlm Paquete npm actualizado. Ahora maneja> 100 entradas.
grabantot
¡Muy útil! Gracias por la solucion Hasta ahora este es el más preciso y limpio. Esta debería ser una nueva respuesta aceptada, estoy de acuerdo.
Siddhartha Chowdhury
13

En este mensaje a la lista de correo HTML 5, Ian Hickson dice:

HTML5 ahora tiene que cargar muchos archivos a la vez. Los navegadores podrían permitir a los usuarios seleccionar varios archivos a la vez, incluso en varios directorios; eso está un poco fuera del alcance de la especificación.

(Consulte también la propuesta de función original ). Por lo tanto, es seguro asumir que él considera que cargar carpetas usando arrastrar y soltar también está fuera de alcance. Aparentemente, depende del navegador entregar archivos individuales.

La carga de carpetas también tendría algunas otras dificultades, como lo describe Lars Gunther :

Esta propuesta […] debe tener dos comprobaciones (si es factible):

  1. Tamaño máximo, para evitar que alguien cargue un directorio completo de varios cientos de imágenes en bruto sin comprimir ...

  2. Filtrado incluso si se omite el atributo de aceptación. Se deben omitir los metadatos de Mac OS y las miniaturas de Windows, etc. Todos los archivos y directorios ocultos deben excluirse de forma predeterminada.

Marcel Korpel
fuente
Hmmm, estoy de acuerdo con el punto 2 ... pero solo mientras haya una forma para que el desarrollador web determine si desea habilitar la carga de archivos ocultos, ya que siempre existe la posibilidad de que un archivo oculto pueda estar operativo el uso de la carpeta cargada. Especialmente si la carpeta es un documento completo dividido en varias partes, como podría ser un archivo de corte final.
Charles John Thompson III
No estoy de acuerdo con fuera de alcance: esta es una causa de incompatibilidades para algo que muchas personas quieren hacer, por lo que debe especificarse.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
10

Ahora puede cargar directorios con arrastrar y soltar e ingresar.

<input type='file' webkitdirectory >

y para arrastrar y soltar (para navegadores webkit).

Manejo de carpetas de arrastrar y soltar.

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

Recursos:

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

Konga Raju
fuente
1
¿Es posible hacer lo mismo para descargar sin usar carpetas comprimidas?
user2284570
8

Firefox ahora admite la carga de carpetas, a partir del 15 de noviembre de 2016, en la versión 50.0: https://developer.mozilla.org/en-US/Firefox/Releases/50#Files_and_directories

Puede arrastrar y soltar carpetas en Firefox o puede buscar y seleccionar una carpeta local para cargar. También admite carpetas anidadas en subcarpetas.

Eso significa que ahora puede usar Chrome, Firefox, Edge u Opera para cargar carpetas. No puede utilizar Safari o Internet Explorer en este momento.

Dan Roberts
fuente
3

A continuación, se muestra un ejemplo completo de cómo utilizar la API de entradas de directorio y archivo :

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 es compatible con Chrome 13+, Firefox 50+ y Edge.

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

Paolo Moretti
fuente
1
Funciona muy bien. Portado a Vue jsfiddle.net/KimNyholm/xua9kLny
Kim Nyholm
1

¿HTML5 permite la carga mediante arrastrar y soltar de carpetas o un árbol de carpetas?

Solo Chrome admite esta función. No ha tenido tracción y es probable que se retire.

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

basarat
fuente
Guau. Según la nota del W3C en ese enlace, esto de hecho no continúa. ¿Cuál es la base de la suposición de que no ha conseguido tracción?
bebbi
@bebbi ningún otro proveedor de navegadores lo implementó
basarat
1
El comentario de @ PabloBarríaUrenda no es cierto; Es probable que su problema se refiera a su pregunta: stackoverflow.com/questions/51850469/… que resolvió / se dio cuenta de readEntriesque no se puede llamar si readEntriesaún se está ejecutando otra llamada de . El diseño de la API de DirectoryReader no es el mejor
xlm
@xlm sí, de hecho tienes razón. Había publicado esto mientras yo mismo estaba desconcertado por el problema, pero finalmente lo resolví (y me olvidé de este comentario). Ahora he eliminado el comentario confuso.
Pablo Barría Urenda
1

ACTUALIZACIÓN: Desde 2012 ha cambiado mucho, consulte las respuestas anteriores. Dejo esta respuesta aquí por el bien de la arqueología.

La especificación HTML5 NO dice que al seleccionar una carpeta para cargar, el navegador debe cargar todos los archivos contenidos de forma recursiva.

En realidad, en Chrome / Chromium, puede cargar una carpeta, pero cuando lo hace, simplemente carga un archivo de 4KB sin sentido, que representa el directorio. Algunas aplicaciones del lado del servidor como Alfresco pueden detectar esto y advertir al usuario que las carpetas no se pueden cargar:

Lo siguiente no se puede cargar porque son carpetas o tienen un tamaño de cero bytes: indefinido

Nicolas Raoul
fuente
@MoB: tal vez sea una especie de puntero. Pero dado que el archivo real está en la máquina cliente, la máquina servidor no podrá hacer nada con este puntero, por supuesto.
Nicolas Raoul
1

Recientemente me encontré con la necesidad de implementar esto en dos de mis proyectos, así que creé un montón de funciones de utilidad para ayudar con esto.

Uno crea una estructura de datos que representa todas las carpetas, archivos y la relación entre ellos, así 👇

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

Mientras que el otro solo devuelve una matriz de todos los archivos (en todas las carpetas y subcarpetas).

Aquí está el enlace al paquete: https://www.npmjs.com/package/file-system-utils

Pava
fuente