Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> JavaScript

Upload từ ổ cục bộ lên filesystem local trong HTML với Filesystem API


Tổng quan các API liên quan

APIMục đíchTrạ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-dropTấ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+
IndexedDBLưu trữ blob/file lớn, query, indexTấ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 CaseAPI 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ớnOPFS (trong Worker) + IndexedDB
Image editor, video editorOPFS + File System Access API
Drag & drop uploadDrag & 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 AbortError khi user cancel picker