No trabajarás con directorios a diario, pero a veces surge un caso de uso, como procesar todas las imágenes en un directorio. Con la API de File System Access, los usuarios ahora pueden abrir directorios en el navegador y decidir si necesitan acceso de escritura o no.
La forma moderna
Cómo usar el método showDirectoryPicker()
de la API de File System Access
Para abrir un directorio, llama a showDirectoryPicker()
, que muestra una promesa con el directorio elegido. Si necesitas acceso de escritura, puedes pasar { mode: 'readwrite' }
al método.
La forma clásica
Cómo usar el elemento <input type="file" webkitdirectory>
El elemento <input type="file" webkitdirectory>
de una página permite al usuario hacer clic en él y abrir un directorio. El truco ahora consiste en insertar el elemento de forma invisible en una página con JavaScript y hacer clic en él de manera programática.
Mejora progresiva
El siguiente método usa la API de File System Access cuando es compatible y, de lo contrario, recurre al enfoque clásico. En ambos casos, la función muestra un directorio, pero en los casos en que se admita la API de File System Access, cada objeto de archivo también tiene un FileSystemDirectoryHandle
almacenado en la propiedad directoryHandle
y un FileSystemFileHandle
almacenado en la propiedad handle
, por lo que, de manera opcional, puedes serializar los controladores en el disco.
const openDirectory = async (mode = "read") => {
// Feature detection. The API needs to be supported
// and the app not run in an iframe.
const supportsFileSystemAccess =
"showDirectoryPicker" in window &&
(() => {
try {
return window.self === window.top;
} catch {
return false;
}
})();
// If the File System Access API is supported…
if (supportsFileSystemAccess) {
let directoryStructure = undefined;
// Recursive function that walks the directory structure.
const getFiles = async (dirHandle, path = dirHandle.name) => {
const dirs = [];
const files = [];
for await (const entry of dirHandle.values()) {
const nestedPath = `${path}/${entry.name}`;
if (entry.kind === "file") {
files.push(
entry.getFile().then((file) => {
file.directoryHandle = dirHandle;
file.handle = entry;
return Object.defineProperty(file, "webkitRelativePath", {
configurable: true,
enumerable: true,
get: () => nestedPath,
});
})
);
} else if (entry.kind === "directory") {
dirs.push(getFiles(entry, nestedPath));
}
}
return [
...(await Promise.all(dirs)).flat(),
...(await Promise.all(files)),
];
};
try {
// Open the directory.
const handle = await showDirectoryPicker({
mode,
});
// Get the directory structure.
directoryStructure = getFiles(handle, undefined);
} catch (err) {
if (err.name !== "AbortError") {
console.error(err.name, err.message);
}
}
return directoryStructure;
}
// Fallback if the File System Access API is not supported.
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.webkitdirectory = true;
input.addEventListener('change', () => {
let files = Array.from(input.files);
resolve(files);
});
if ('showPicker' in HTMLInputElement.prototype) {
input.showPicker();
} else {
input.click();
}
});
};
Lecturas adicionales
Demostración
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📂</text></svg>"
/>
<title>How to open a directory</title>
</head>
<body>
<h1>How to open a directory</h1>
<button type="button">Open directory</button>
<pre></pre>
</body>
</html>
CSS
:root {
color-scheme: dark light;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 1rem;
font-family: system-ui, sans-serif;
}
JS
const button = document.querySelector('button');
const pre = document.querySelector('pre');
const openDirectory = async (mode = "read") => {
// Feature detection. The API needs to be supported
// and the app not run in an iframe.
const supportsFileSystemAccess =
"showDirectoryPicker" in window &&
(() => {
try {
return window.self === window.top;
} catch {
return false;
}
})();
// If the File System Access API is supported…
if (supportsFileSystemAccess) {
let directoryStructure = undefined;
const getFiles = async (dirHandle, path = dirHandle.name) => {
const dirs = [];
const files = [];
for await (const entry of dirHandle.values()) {
const nestedPath = `${path}/${entry.name}`;
if (entry.kind === "file") {
files.push(
entry.getFile().then((file) => {
file.directoryHandle = dirHandle;
file.handle = entry;
return Object.defineProperty(file, "webkitRelativePath", {
configurable: true,
enumerable: true,
get: () => nestedPath,
});
})
);
} else if (entry.kind === "directory") {
dirs.push(getFiles(entry, nestedPath));
}
}
return [
...(await Promise.all(dirs)).flat(),
...(await Promise.all(files)),
];
};
try {
const handle = await showDirectoryPicker({
mode,
});
directoryStructure = getFiles(handle, undefined);
} catch (err) {
if (err.name !== "AbortError") {
console.error(err.name, err.message);
}
}
return directoryStructure;
}
// Fallback if the File System Access API is not supported.
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.webkitdirectory = true;
input.addEventListener('change', () => {
let files = Array.from(input.files);
resolve(files);
});
if ('showPicker' in HTMLInputElement.prototype) {
input.showPicker();
} else {
input.click();
}
});
};
button.addEventListener('click', async () => {
const filesInDirectory = await openDirectory();
if (!filesInDirectory) {
return;
}
Array.from(filesInDirectory).forEach((file) => (pre.textContent += `${file.name}\n`));
});