Tổng quan các API liên quan
| API | Mục đích | Trạng thái |
|---|---|---|
<input type="file" webkitdirectory> | Chọn thư mục (folder picker) | Chrome, Edge, Opera (webkit prefix) |
| File System Access API | Đọc/ghi file/thư mục thực trên ổ cứng (cần quyền user) | Chrome 86+, Edge 86+ (Standard) |
| File API / FileReader | Đọc file từ input/drag-drop | Tất cả trình duyệt hiện đại |
| Origin Private File System (OPFS) | Filesystem sandboxed trong origin (không cần quyền user) | Chrome 102+, Firefox 111+, Safari 15.2+ |
| IndexedDB | Lưu trữ blob/file lớn, query, index | Tất cả trình duyệt hiện đại |
1. Chọn thư mục với webkitdirectory (Legacy nhưng rộng rãi)
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<title>Folder Upload Demo</title>
<style>
body { font-family: sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
.file-tree { font-family: monospace; font-size: 0.875rem; background: #f8f9fa; padding: 1rem; border-radius: 8px; max-height: 400px; overflow-y: auto; }
.file-item { padding: 0.25rem 0; }
.file-item.directory { font-weight: 600; color: #3498db; }
.file-item.file { color: #2c3e50; padding-left: 2rem; }
.stats { margin: 1rem 0; padding: 1rem; background: #e8f5e9; border-radius: 8px; }
button { padding: 0.75rem 1.5rem; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 0.5rem; }
button:disabled { background: #bdc3c7; cursor: not-allowed; }
</style>
</head>
<body>
<h2>Chọn thư mục để upload (webkitdirectory)</h2>
<input type="file" id="folderInput" webkitdirectory multiple>
<button id="processBtn" disabled onclick="processFiles()">Xử lý file đã chọn</button>
<button onclick="clearSelection()">Xóa lựa chọn</button>
<div class="stats" id="stats">Chưa chọn thư mục</div>
<div class="file-tree" id="fileTree"></div>
<script>
const input = document.getElementById('folderInput');
const processBtn = document.getElementById('processBtn');
const statsEl = document.getElementById('stats');
const treeEl = document.getElementById('fileTree');
let selectedFiles = [];
input.addEventListener('change', (e) => {
selectedFiles = Array.from(e.target.files);
processBtn.disabled = selectedFiles.length === 0;
renderFileTree();
updateStats();
});
function renderFileTree() {
// Group by directory
const tree = {};
selectedFiles.forEach(file => {
// webkitRelativePath: "folder/subfolder/file.txt"
const path = file.webkitRelativePath || file.name;
const parts = path.split('/');
let current = tree;
parts.forEach((part, i) => {
if (i === parts.length - 1) {
current.__files = current.__files || [];
current.__files.push({ name: part, file });
} else {
current[part] = current[part] || {};
current = current[part];
}
});
});
treeEl.innerHTML = renderTreeNode(tree, 0);
}
function renderTreeNode(node, depth) {
let html = '';
for (const [key, value] of Object.entries(node)) {
if (key === '__files') continue;
const isDir = typeof value === 'object' && value !== null;
html += `${key}${isDir ? '/' : ''}`;
if (isDir) {
html += renderTreeNode(value, depth + 1);
}
}
if (node.__files) {
node.__files.forEach(f => {
html += `${f.name} (${formatBytes(f.file.size)})`;
});
}
return html;
}
function updateStats() {
const totalSize = selectedFiles.reduce((sum, f) => sum + f.size, 0);
const dirs = new Set(selectedFiles.map(f => f.webkitRelativePath.split('/')[0])).size;
statsEl.innerHTML = `
${selectedFiles.length} file(s) trong ${dirs} thư mục |
Tổng dung lượng: ${formatBytes(totalSize)}
`;
}
function formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function clearSelection() {
input.value = '';
selectedFiles = [];
processBtn.disabled = true;
treeEl.innerHTML = '';
statsEl.textContent = 'Chưa chọn thư mục';
}
async function processFiles() {
statsEl.innerHTML += '
Đang đọc file...';
for (const file of selectedFiles) {
if (file.type.startsWith('text/') || file.name.endsWith('.json')) {
const text = await file.text();
console.log(`${file.webkitRelativePath}:`, text.substring(0, 100));
}
}
statsEl.innerHTML += '
✅ Đã xử lý xong (xem console)';
}
</script>
</body>
</html>
2. File System Access API (Modern - Chrome/Edge)
Cho phép đọc/ghi file thực sự trên ổ cứng sau khi user cấp quyền.
// Mở file
async function openFile() {
try {
const [handle] = await window.showOpenFilePicker({
types: [{
description: 'Text Files',
accept: { 'text/plain': ['.txt', '.md', '.js', '.json'] }
}],
multiple: false
});
const file = await handle.getFile();
const content = await file.text();
return { handle, file, content };
} catch (e) {
if (e.name !== 'AbortError') console.error(e);
return null;
}
}
// Lưu file (ghi đè)
async function saveFile(handle, content) {
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
// Lưu file mới (Save As)
async function saveAsFile(content, suggestedName = 'untitled.txt') {
try {
const handle = await window.showSaveFilePicker({
suggestedName,
types: [{ description: 'Text', accept: { 'text/plain': ['.txt'] } }]
});
await saveFile(handle, content);
return handle;
} catch (e) {
if (e.name !== 'AbortError') console.error(e);
}
}
// Mở thư mục
async function openDirectory() {
try {
const handle = await window.showDirectoryPicker();
return handle;
} catch (e) {
if (e.name !== 'AbortError') console.error(e);
}
}
// Duyệt thư mục đệ quy
async function listFiles(dirHandle, path = '') {
const files = [];
for await (const [name, entry] of dirHandle.entries()) {
const fullPath = path + '/' + name;
if (entry.kind === 'file') {
const file = await entry.getFile();
files.push({ path: fullPath, file, handle: entry });
} else {
files.push(...await listFiles(entry, fullPath));
}
}
return files;
}
3. Origin Private File System (OPFS) - Sandboxed Storage
Lưu trữ file trong "sandbox" của origin, không cần quyền user, hiệu năng cao (sync access trong worker).
// Trong main thread (async)
const root = await navigator.storage.getDirectory();
// Tạo/ghi file
const fileHandle = await root.getFileHandle('data.json', { create: true });
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify({ users: [...] }));
await writable.close();
// Đọc file
const file = await fileHandle.getFile();
const text = await file.text();
const data = JSON.parse(text);
// Trong Web Worker (Sync API - hiệu năng cao)
const worker = new Worker('worker.js');
worker.postMessage({ type: 'init' });
// worker.js
self.onmessage = async (e) => {
if (e.data.type === 'init') {
const root = await navigator.storage.getDirectory();
// Sync access - chỉ trong worker
const syncHandle = await root.getFileHandle('large-data.bin', { create: true });
const syncAccessHandle = await syncHandle.createSyncAccessHandle();
// Ghi đọc đồng bộ, nhanh hơn nhiều
const buffer = new ArrayBuffer(1024);
syncAccessHandle.write(buffer);
syncAccessHandle.read(buffer);
syncAccessHandle.flush();
syncAccessHandle.close();
}
};
4. IndexedDB cho file lớn (Blobs)
// Lưu file lớn vào IndexedDB
async function saveBlobToIDB(blob, key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('FileStore', 1);
request.onupgradeneeded = (e) => {
e.target.result.createObjectStore('files');
};
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('files', 'readwrite');
tx.objectStore('files').put(blob, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
};
request.onerror = () => reject(request.error);
});
}
// Đọc
async function getBlobFromIDB(key) {
return new Promise((resolve, reject) => {
const request = indexedDB.open('FileStore', 1);
request.onsuccess = (e) => {
const db = e.target.result;
const tx = db.transaction('files', 'readonly');
const req = tx.objectStore('files').get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
};
});
}
So sánh & Khuyến nghị
| Use Case | API khuyên dùng |
|---|---|
| User chọn file/upload lên server | <input type="file"> + FileReader |
| User chọn thư mục (nhiều file) | <input webkitdirectory> |
| Web editor (mở/sửa/lưu file code) | File System Access API |
| App offline-first, cache lớn | OPFS (trong Worker) + IndexedDB |
| Image editor, video editor | OPFS + File System Access API |
| Drag & drop upload | Drag & Drop API + FileReader |
Lưu ý bảo mật
- File System Access API yêu cầu **HTTPS** và **user gesture** (click)
- Permission bị thu hồi khi tab đóng hoặc user revoke trong site settings
- OPFS bị xóa khi clear site data
- Luôn handle
AbortErrorkhi user cancel picker