API Truy cập hệ thống tệp: đơn giản hoá quyền truy cập vào các tệp cục bộ

File System Access API cho phép các ứng dụng web đọc hoặc lưu trực tiếp các thay đổi vào tệp và thư mục trên thiết bị của người dùng.

Xuất bản: ngày 19 tháng 8 năm 2024

File System Access API cho phép nhà phát triển tạo các ứng dụng web mạnh mẽ tương tác với các tệp trên thiết bị cục bộ của người dùng, chẳng hạn như IDE, trình chỉnh sửa ảnh và video, trình chỉnh sửa văn bản, v.v. Sau khi người dùng cấp quyền truy cập cho một ứng dụng web, API này cho phép họ đọc hoặc lưu các thay đổi trực tiếp vào tệp và thư mục trên thiết bị của người dùng. Ngoài việc đọc và ghi tệp, File System Access API còn cung cấp khả năng mở một thư mục và liệt kê nội dung của thư mục đó.

Nếu đã từng làm việc với các tệp đọc và ghi, thì bạn sẽ thấy nhiều nội dung tôi sắp chia sẻ quen thuộc. Tuy nhiên, bạn vẫn nên đọc vì không phải hệ thống nào cũng giống nhau.

File System Access API được hỗ trợ trên hầu hết các trình duyệt Chromium trên Windows, macOS, ChromeOS, Linux và Android. Một trường hợp ngoại lệ đáng chú ý là Brave, nơi hiện chỉ có sẵn đằng sau một cờ.

Sử dụng API Truy cập hệ thống tệp

Để thể hiện sức mạnh và tính hữu ích của File System Access API, tôi đã viết một trình chỉnh sửa văn bản cho một tệp duy nhất. Ứng dụng này cho phép bạn mở một tệp văn bản, chỉnh sửa tệp đó, lưu các thay đổi trở lại đĩa hoặc bắt đầu một tệp mới và lưu các thay đổi vào đĩa. Không có gì đặc biệt, nhưng đủ để giúp bạn hiểu các khái niệm.

Hỗ trợ trình duyệt

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: not supported.
  • Safari: not supported.

Source

Phát hiện đối tượng

Để biết File System Access API có được hỗ trợ hay không, hãy kiểm tra xem phương thức chọn mà bạn quan tâm có tồn tại hay không.

if ('showOpenFilePicker' in self) {
  // The `showOpenFilePicker()` method of the File System Access API is supported.
}

Dùng thử

Xem API Truy cập hệ thống tệp hoạt động trong bản minh hoạ trình chỉnh sửa văn bản.

Đọc một tệp từ hệ thống tệp cục bộ

Trường hợp sử dụng đầu tiên mà tôi muốn giải quyết là yêu cầu người dùng chọn một tệp, sau đó mở và đọc tệp đó từ đĩa.

Yêu cầu người dùng chọn một tệp để đọc

Điểm truy cập vào API Truy cập hệ thống tệp là window.showOpenFilePicker(). Khi được gọi, phương thức này sẽ hiện một hộp thoại bộ chọn tệp và nhắc người dùng chọn một tệp. Sau khi người dùng chọn một tệp, API sẽ trả về một mảng gồm các mã nhận dạng tệp. Tham số options (không bắt buộc) cho phép bạn tác động đến hành vi của bộ chọn tệp, chẳng hạn như cho phép người dùng chọn nhiều tệp, thư mục hoặc các loại tệp khác nhau. Nếu không có lựa chọn nào được chỉ định, trình chọn tệp sẽ cho phép người dùng chọn một tệp. Điều này rất phù hợp với trình chỉnh sửa văn bản.

Giống như nhiều API mạnh mẽ khác, bạn phải gọi showOpenFilePicker() trong một bối cảnh an toàn và phải gọi từ trong cử chỉ của người dùng.

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  // Destructure the one-element array.
  [fileHandle] = await window.showOpenFilePicker();
  // Do something with the file handle.
});

Sau khi người dùng chọn một tệp, showOpenFilePicker() sẽ trả về một mảng gồm các giá trị nhận dạng, trong trường hợp này là một mảng gồm một phần tử có một FileSystemFileHandle chứa các thuộc tính và phương thức cần thiết để tương tác với tệp.

Bạn nên giữ lại một bản tham chiếu đến mã nhận dạng tệp để có thể sử dụng sau này. Bạn sẽ cần có quyền này để lưu các thay đổi vào tệp hoặc thực hiện bất kỳ thao tác nào khác trên tệp.

Đọc một tệp từ hệ thống tệp

Giờ đây, bạn đã có một hàm xử lý cho tệp, bạn có thể lấy các thuộc tính của tệp hoặc truy cập vào chính tệp đó. Bây giờ, tôi sẽ đọc nội dung của thư. Khi gọi handle.getFile(), một đối tượng File sẽ được trả về. Đối tượng này chứa một blob. Để lấy dữ liệu từ blob, hãy gọi một trong các phương thức của blob (slice(), stream(), text() hoặc arrayBuffer()).

const file = await fileHandle.getFile();
const contents = await file.text();

Đối tượng File do FileSystemFileHandle.getFile() trả về chỉ có thể đọc được miễn là tệp cơ bản trên ổ đĩa chưa thay đổi. Nếu tệp trên đĩa bị sửa đổi, đối tượng File sẽ không đọc được và bạn cần gọi lại getFile() để lấy một đối tượng File mới nhằm đọc dữ liệu đã thay đổi.

Kết hợp kiến thức đã học

Khi người dùng nhấp vào nút Open (Mở), trình duyệt sẽ hiện một trình chọn tệp. Sau khi người dùng chọn một tệp, ứng dụng sẽ đọc nội dung và đưa nội dung đó vào một <textarea>.

let fileHandle;
butOpenFile.addEventListener('click', async () => {
  [fileHandle] = await window.showOpenFilePicker();
  const file = await fileHandle.getFile();
  const contents = await file.text();
  textArea.value = contents;
});

Ghi tệp vào hệ thống tệp cục bộ

Trong trình chỉnh sửa văn bản, có hai cách để lưu tệp: LưuLưu dưới dạng. Lưu sẽ ghi các thay đổi trở lại tệp gốc bằng cách sử dụng mã nhận dạng tệp đã truy xuất trước đó. Nhưng Lưu dưới dạng sẽ tạo một tệp mới, do đó cần có một mã nhận dạng tệp mới.

Tạo tệp mới

Để lưu một tệp, hãy gọi showSaveFilePicker(). Thao tác này sẽ hiện trình chọn tệp ở chế độ "lưu", cho phép người dùng chọn một tệp mới mà họ muốn dùng để lưu. Đối với trình chỉnh sửa văn bản, tôi cũng muốn trình chỉnh sửa này tự động thêm tiện ích .txt, vì vậy, tôi đã cung cấp một số tham số bổ sung.

async function getNewFileHandle() {
  const options = {
    types: [
      {
        description: 'Text Files',
        accept: {
          'text/plain': ['.txt'],
        },
      },
    ],
  };
  const handle = await window.showSaveFilePicker(options);
  return handle;
}

Lưu các thay đổi vào ổ đĩa

Bạn có thể tìm thấy tất cả mã để lưu các thay đổi vào một tệp trong bản minh hoạ trình chỉnh sửa văn bản của tôi trên GitHub. Các hoạt động tương tác cốt lõi của hệ thống tệp nằm trong fs-helpers.js. Ở dạng đơn giản nhất, quy trình này sẽ có dạng như mã sau. Tôi sẽ hướng dẫn bạn từng bước và giải thích cho bạn.

// fileHandle is an instance of FileSystemFileHandle..
async function writeFile(fileHandle, contents) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Write the contents of the file to the stream.
  await writable.write(contents);
  // Close the file and write the contents to disk.
  await writable.close();
}

Việc ghi dữ liệu vào ổ đĩa sẽ sử dụng một đối tượng FileSystemWritableFileStream, một lớp con của WritableStream. Tạo luồng bằng cách gọi createWritable() trên đối tượng xử lý tệp. Khi createWritable() được gọi, trước tiên, trình duyệt sẽ kiểm tra xem người dùng đã cấp quyền ghi cho tệp hay chưa. Nếu chưa được cấp quyền ghi, trình duyệt sẽ nhắc người dùng cấp quyền. Nếu không được cấp quyền, createWritable() sẽ gửi DOMException và ứng dụng sẽ không thể ghi vào tệp. Trong trình chỉnh sửa văn bản, các đối tượng DOMException sẽ được xử lý trong phương thức saveFile().

Phương thức write() lấy một chuỗi, đây là những gì cần thiết cho một trình chỉnh sửa văn bản. Nhưng nó cũng có thể lấy BufferSource hoặc Blob. Ví dụ: bạn có thể truyền trực tiếp một luồng đến nó:

async function writeURLToFile(fileHandle, url) {
  // Create a FileSystemWritableFileStream to write to.
  const writable = await fileHandle.createWritable();
  // Make an HTTP request for the contents.
  const response = await fetch(url);
  // Stream the response into the file.
  await response.body.pipeTo(writable);
  // pipeTo() closes the destination pipe by default, no need to close it.
}

Bạn cũng có thể seek() hoặc truncate() trong luồng để cập nhật tệp ở một vị trí cụ thể hoặc đổi kích thước tệp.

Chỉ định tên tệp và thư mục bắt đầu được đề xuất

Trong nhiều trường hợp, bạn có thể muốn ứng dụng của mình đề xuất tên tệp hoặc vị trí mặc định. Ví dụ: một trình chỉnh sửa văn bản có thể muốn đề xuất tên tệp mặc định là Untitled Text.txt thay vì Untitled. Bạn có thể thực hiện việc này bằng cách truyền một thuộc tính suggestedName trong số các lựa chọn showSaveFilePicker.

const fileHandle = await self.showSaveFilePicker({
  suggestedName: 'Untitled Text.txt',
  types: [{
    description: 'Text documents',
    accept: {
      'text/plain': ['.txt'],
    },
  }],
});

Điều tương tự cũng áp dụng cho thư mục bắt đầu mặc định. Nếu đang tạo một trình chỉnh sửa văn bản, bạn có thể muốn bắt đầu hộp thoại lưu tệp hoặc mở tệp trong thư mục documents mặc định, trong khi đối với trình chỉnh sửa hình ảnh, bạn có thể muốn bắt đầu trong thư mục pictures mặc định. Bạn có thể đề xuất một thư mục khởi động mặc định bằng cách truyền một thuộc tính startIn đến các phương thức showSaveFilePicker, showDirectoryPicker() hoặc showOpenFilePicker như sau.

const fileHandle = await self.showOpenFilePicker({
  startIn: 'pictures'
});

Dưới đây là danh sách các thư mục hệ thống phổ biến:

  • desktop: Thư mục trên máy tính của người dùng (nếu có).
  • documents: Thư mục thường dùng để lưu trữ các tài liệu do người dùng tạo.
  • downloads: Thư mục thường dùng để lưu trữ các tệp đã tải xuống.
  • music: Thư mục thường dùng để lưu trữ tệp âm thanh.
  • pictures: Thư mục thường dùng để lưu trữ ảnh và các hình ảnh tĩnh khác.
  • videos: Thư mục thường dùng để lưu trữ video hoặc phim.

Ngoài các thư mục hệ thống đã biết, bạn cũng có thể truyền một tệp hoặc thư mục hiện có dưới dạng giá trị cho startIn. Sau đó, hộp thoại sẽ mở ra trong cùng thư mục.

// Assume `directoryHandle` is a handle to a previously opened directory.
const fileHandle = await self.showOpenFilePicker({
  startIn: directoryHandle
});

Chỉ định mục đích của các bộ chọn tệp khác nhau

Đôi khi, các ứng dụng có nhiều bộ chọn cho nhiều mục đích. Ví dụ: trình chỉnh sửa văn bản đa dạng thức có thể cho phép người dùng mở tệp văn bản, nhưng cũng cho phép nhập hình ảnh. Theo mặc định, mỗi trình chọn tệp sẽ mở ở vị trí được ghi nhớ gần đây nhất. Bạn có thể khắc phục điều này bằng cách lưu trữ các giá trị id cho từng loại bộ chọn. Nếu bạn chỉ định một id, thì quá trình triển khai bộ chọn tệp sẽ ghi nhớ một thư mục riêng biệt được dùng gần đây nhất cho id đó.

const fileHandle1 = await self.showSaveFilePicker({
  id: 'openText',
});

const fileHandle2 = await self.showSaveFilePicker({
  id: 'importImage',
});

Lưu trữ các mã nhận dạng tệp hoặc mã nhận dạng thư mục trong IndexedDB

Xử lý tệp và xử lý thư mục có thể được chuyển đổi tuần tự, tức là bạn có thể lưu một tệp hoặc xử lý thư mục vào IndexedDB hoặc gọi postMessage() để gửi chúng giữa cùng một nguồn gốc cấp cao nhất.

Việc lưu tay cầm tệp hoặc thư mục vào IndexedDB có nghĩa là bạn có thể lưu trữ trạng thái hoặc ghi nhớ những tệp hoặc thư mục mà người dùng đang làm việc. Điều này giúp bạn có thể lưu giữ danh sách các tệp đã mở hoặc chỉnh sửa gần đây, đề xuất mở lại tệp gần đây nhất khi ứng dụng được mở, khôi phục thư mục làm việc trước đó, v.v. Trong trình chỉnh sửa văn bản, tôi lưu trữ danh sách 5 tệp gần đây nhất mà người dùng đã mở, nhờ đó họ có thể truy cập lại vào các tệp đó.

Ví dụ về mã sau đây cho thấy cách lưu trữ và truy xuất một mã nhận dạng tệp và một mã nhận dạng thư mục. Bạn có thể xem ví dụ thực tế trên Glitch. (Tôi sử dụng thư viện idb-keyval để cho ngắn gọn.)

import { get, set } from 'https://unpkg.com/idb-keyval@5.0.2/dist/esm/index.js';

const pre1 = document.querySelector('pre.file');
const pre2 = document.querySelector('pre.directory');
const button1 = document.querySelector('button.file');
const button2 = document.querySelector('button.directory');

// File handle
button1.addEventListener('click', async () => {
  try {
    const fileHandleOrUndefined = await get('file');
    if (fileHandleOrUndefined) {
      pre1.textContent = `Retrieved file handle "${fileHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const [fileHandle] = await window.showOpenFilePicker();
    await set('file', fileHandle);
    pre1.textContent = `Stored file handle for "${fileHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

// Directory handle
button2.addEventListener('click', async () => {
  try {
    const directoryHandleOrUndefined = await get('directory');
    if (directoryHandleOrUndefined) {
      pre2.textContent = `Retrieved directroy handle "${directoryHandleOrUndefined.name}" from IndexedDB.`;
      return;
    }
    const directoryHandle = await window.showDirectoryPicker();
    await set('directory', directoryHandle);
    pre2.textContent = `Stored directory handle for "${directoryHandle.name}" in IndexedDB.`;
  } catch (error) {
    alert(error.name, error.message);
  }
});

Quyền và các mã nhận dạng tệp hoặc thư mục được lưu trữ

các quyền không phải lúc nào cũng được duy trì giữa các phiên, nên bạn phải xác minh xem người dùng đã cấp quyền cho tệp hoặc thư mục hay chưa bằng cách sử dụng queryPermission(). Nếu họ chưa gửi, hãy gọi requestPermission() để (yêu cầu lại) mã này. Điều này cũng áp dụng cho các thao tác xử lý tệp và thư mục. Bạn cần chạy fileOrDirectoryHandle.requestPermission(descriptor) hoặc fileOrDirectoryHandle.queryPermission(descriptor) tương ứng.

Trong trình chỉnh sửa văn bản, tôi đã tạo một phương thức verifyPermission() để kiểm tra xem người dùng đã cấp quyền hay chưa và đưa ra yêu cầu nếu cần.

async function verifyPermission(fileHandle, readWrite) {
  const options = {};
  if (readWrite) {
    options.mode = 'readwrite';
  }
  // Check if permission was already granted. If so, return true.
  if ((await fileHandle.queryPermission(options)) === 'granted') {
    return true;
  }
  // Request permission. If the user grants permission, return true.
  if ((await fileHandle.requestPermission(options)) === 'granted') {
    return true;
  }
  // The user didn't grant permission, so return false.
  return false;
}

Bằng cách yêu cầu quyền ghi cùng với yêu cầu đọc, tôi đã giảm số lượng lời nhắc cấp quyền; người dùng sẽ thấy một lời nhắc khi mở tệp và cấp quyền đọc cũng như ghi vào tệp đó.

Mở một thư mục và liệt kê nội dung của thư mục đó

Để liệt kê tất cả các tệp trong một thư mục, hãy gọi showDirectoryPicker(). Người dùng chọn một thư mục trong bộ chọn, sau đó FileSystemDirectoryHandle sẽ được trả về, cho phép bạn liệt kê và truy cập vào các tệp của thư mục. Theo mặc định, bạn sẽ có quyền đọc các tệp trong thư mục, nhưng nếu cần quyền ghi, bạn có thể truyền { mode: 'readwrite' } vào phương thức.

butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  for await (const entry of dirHandle.values()) {
    console.log(entry.kind, entry.name);
  }
});

Nếu bạn cần truy cập vào từng tệp bằng cách sử dụng getFile() để, chẳng hạn như, lấy kích thước của từng tệp, thì đừng sử dụng await trên từng kết quả theo trình tự, mà hãy xử lý tất cả các tệp song song, chẳng hạn như bằng cách sử dụng Promise.all().

butDir.addEventListener('click', async () => {
  const dirHandle = await window.showDirectoryPicker();
  const promises = [];
  for await (const entry of dirHandle.values()) {
    if (entry.kind !== 'file') {
      continue;
    }
    promises.push(entry.getFile().then((file) => `${file.name} (${file.size})`));
  }
  console.log(await Promise.all(promises));
});

Tạo hoặc truy cập vào tệp và thư mục trong một thư mục

Từ một thư mục, bạn có thể tạo hoặc truy cập vào các tệp và thư mục bằng cách sử dụng phương thức getFileHandle() hoặc getDirectoryHandle() tương ứng. Bằng cách truyền vào một đối tượng options không bắt buộc có khoá là create và giá trị boolean là true hoặc false, bạn có thể xác định xem có nên tạo một tệp hoặc thư mục mới nếu tệp hoặc thư mục đó không tồn tại hay không.

// In an existing directory, create a new directory named "My Documents".
const newDirectoryHandle = await existingDirectoryHandle.getDirectoryHandle('My Documents', {
  create: true,
});
// In this new directory, create a file named "My Notes.txt".
const newFileHandle = await newDirectoryHandle.getFileHandle('My Notes.txt', { create: true });

Giải quyết đường dẫn của một mục trong thư mục

Khi làm việc với các tệp hoặc thư mục trong một thư mục, bạn có thể giải quyết đường dẫn của mục trong câu hỏi. Bạn có thể thực hiện việc này bằng phương thức resolve(). Để phân giải, mục này có thể là mục con trực tiếp hoặc gián tiếp của thư mục.

// Resolve the path of the previously created file called "My Notes.txt".
const path = await newDirectoryHandle.resolve(newFileHandle);
// `path` is now ["My Documents", "My Notes.txt"]

Xoá tệp và thư mục trong một thư mục

Nếu đã có quyền truy cập vào một thư mục, bạn có thể xoá các tệp và thư mục có trong đó bằng phương thức removeEntry(). Đối với thư mục, bạn có thể xoá theo quy tắc đệ quy (không bắt buộc) và xoá tất cả thư mục con cũng như các tệp có trong đó.

// Delete a file.
await directoryHandle.removeEntry('Abandoned Projects.txt');
// Recursively delete a folder.
await directoryHandle.removeEntry('Old Stuff', { recursive: true });

Xoá trực tiếp một tệp hoặc thư mục

Nếu bạn có quyền truy cập vào một tệp hoặc thư mục, hãy gọi remove() trên FileSystemFileHandle hoặc FileSystemDirectoryHandle để xoá tệp hoặc thư mục đó.

// Delete a file.
await fileHandle.remove();
// Delete a directory.
await directoryHandle.remove();

Đổi tên và di chuyển tệp cũng như thư mục

Bạn có thể đổi tên hoặc di chuyển tệp và thư mục sang một vị trí mới bằng cách gọi move() trên giao diện FileSystemHandle. FileSystemHandle có các giao diện con FileSystemFileHandleFileSystemDirectoryHandle. Phương thức move() có một hoặc hai tham số. Tham số đầu tiên có thể là một chuỗi có tên mới hoặc một FileSystemDirectoryHandle đến thư mục đích. Trong trường hợp sau, tham số thứ hai không bắt buộc là một chuỗi có tên mới, vì vậy, bạn có thể di chuyển và đổi tên trong một bước.

// Rename the file.
await file.move('new_name');
// Move the file to a new directory.
await file.move(directory);
// Move the file to a new directory and rename it.
await file.move(directory, 'newer_name');

Tích hợp tính năng kéo và thả

Giao diện Kéo và thả HTML cho phép các ứng dụng web chấp nhận tệp được kéo và thả trên một trang web. Trong quá trình kéo và thả, các mục tệp và thư mục được kéo sẽ được liên kết với các mục tệp và mục thư mục tương ứng. Phương thức DataTransferItem.getAsFileSystemHandle() trả về một lời hứa với đối tượng FileSystemFileHandle nếu mục được kéo là một tệp và một lời hứa với đối tượng FileSystemDirectoryHandle nếu mục được kéo là một thư mục. Danh sách sau đây cho thấy điều này trong thực tế. Xin lưu ý rằng DataTransferItem.kind của giao diện Kéo và thả là "file" cho cả tệp thư mục, trong khi FileSystemHandle.kind của File System Access API là "file" cho tệp và "directory" cho thư mục.

elem.addEventListener('dragover', (e) => {
  // Prevent navigation.
  e.preventDefault();
});

elem.addEventListener('drop', async (e) => {
  e.preventDefault();

  const fileHandlesPromises = [...e.dataTransfer.items]
    .filter((item) => item.kind === 'file')
    .map((item) => item.getAsFileSystemHandle());

  for await (const handle of fileHandlesPromises) {
    if (handle.kind === 'directory') {
      console.log(`Directory: ${handle.name}`);
    } else {
      console.log(`File: ${handle.name}`);
    }
  }
});

Truy cập vào hệ thống tệp riêng tư của nguồn gốc

Hệ thống tệp riêng tư của nguồn gốc là một điểm cuối lưu trữ, như tên cho thấy, là riêng tư đối với nguồn gốc của trang. Mặc dù các trình duyệt thường triển khai việc này bằng cách duy trì nội dung của hệ thống tệp riêng tư gốc này vào đĩa ở một nơi nào đó, nhưng không có nghĩa là người dùng có thể truy cập vào nội dung. Tương tự, không có kỳ vọng rằng các tệp hoặc thư mục có tên trùng với tên của các tệp con trong hệ thống tệp riêng tư gốc sẽ tồn tại. Mặc dù trình duyệt có thể khiến bạn nghĩ rằng có các tệp, nhưng về nội bộ – vì đây là một hệ thống tệp riêng tư theo nguồn gốc – trình duyệt có thể lưu trữ những "tệp" này trong cơ sở dữ liệu hoặc bất kỳ cấu trúc dữ liệu nào khác. Về cơ bản, nếu bạn sử dụng API này, thì đừng mong đợi tìm thấy các tệp đã tạo khớp chính xác ở đâu đó trên ổ cứng. Bạn có thể hoạt động như bình thường trên hệ thống tệp riêng tư gốc sau khi có quyền truy cập vào FileSystemDirectoryHandle gốc.

const root = await navigator.storage.getDirectory();
// Create a new file handle.
const fileHandle = await root.getFileHandle('Untitled.txt', { create: true });
// Create a new directory handle.
const dirHandle = await root.getDirectoryHandle('New Folder', { create: true });
// Recursively remove a directory.
await root.removeEntry('Old Stuff', { recursive: true });

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

Truy cập vào các tệp được tối ưu hoá hiệu suất từ hệ thống tệp riêng tư của nguồn gốc

Hệ thống tệp riêng tư của nguồn gốc cung cấp quyền truy cập không bắt buộc vào một loại tệp đặc biệt được tối ưu hoá cao về hiệu suất, chẳng hạn như bằng cách cung cấp quyền ghi tại chỗ và độc quyền vào nội dung của tệp. Trong Chromium 102 trở lên, có một phương thức bổ sung trên hệ thống tệp riêng tư của nguồn gốc để đơn giản hoá quyền truy cập vào tệp: createSyncAccessHandle() (đối với các thao tác đọc và ghi đồng bộ). API này được hiển thị trên FileSystemFileHandle, nhưng chỉ trong Web Workers.

// (Read and write operations are synchronous,
// but obtaining the handle is asynchronous.)
// Synchronous access exclusively in Worker contexts.
const accessHandle = await fileHandle.createSyncAccessHandle();
const writtenBytes = accessHandle.write(buffer);
const readBytes = accessHandle.read(buffer, { at: 1 });

Polyfilling

Bạn không thể hoàn toàn polyfill các phương thức API Truy cập hệ thống tệp.

  • Phương thức showOpenFilePicker() có thể được ước chừng bằng phần tử <input type="file">.
  • Bạn có thể mô phỏng phương thức showSaveFilePicker() bằng phần tử <a download="file_name">, mặc dù phương thức này sẽ kích hoạt quá trình tải xuống theo chương trình và không cho phép ghi đè các tệp hiện có.
  • Bạn có thể mô phỏng phương thức showDirectoryPicker() bằng phần tử <input type="file" webkitdirectory> không chuẩn.

Chúng tôi đã phát triển một thư viện có tên là browser-fs-access. Thư viện này sử dụng File System Access API (API Truy cập hệ thống tệp) bất cứ khi nào có thể và chuyển sang những lựa chọn tốt nhất tiếp theo này trong mọi trường hợp khác.

Tính bảo mật và quyền truy cập

Nhóm Chrome đã thiết kế và triển khai File System Access API (API Truy cập hệ thống tệp) dựa trên các nguyên tắc cốt lõi được xác định trong Kiểm soát quyền truy cập vào các tính năng mạnh mẽ của nền tảng web, bao gồm cả quyền kiểm soát và tính minh bạch của người dùng, cũng như tính tiện dụng cho người dùng.

Mở tệp hoặc lưu tệp mới

Bộ chọn tệp để mở một tệp để đọc
Bộ chọn tệp dùng để mở một tệp hiện có để đọc.

Khi mở một tệp, người dùng cấp quyền đọc một tệp hoặc thư mục bằng trình chọn tệp. Bạn chỉ có thể hiện trình chọn tệp mở bằng cử chỉ của người dùng khi được phân phát từ một bối cảnh an toàn. Nếu đổi ý, người dùng có thể huỷ lựa chọn trong trình chọn tệp và trang web sẽ không truy cập được vào bất kỳ nội dung nào. Đây là hành vi tương tự như hành vi của phần tử <input type="file">.

Trình chọn tệp để lưu tệp vào ổ đĩa.
Trình chọn tệp dùng để lưu tệp vào ổ đĩa.

Tương tự, khi một ứng dụng web muốn lưu một tệp mới, trình duyệt sẽ hiện trình chọn tệp lưu, cho phép người dùng chỉ định tên và vị trí của tệp mới. Vì họ đang lưu một tệp mới vào thiết bị (thay vì ghi đè một tệp hiện có), nên bộ chọn tệp sẽ cấp cho ứng dụng quyền ghi vào tệp.

Thư mục bị hạn chế

Để giúp bảo vệ người dùng và dữ liệu của họ, trình duyệt có thể hạn chế khả năng lưu của người dùng vào một số thư mục nhất định, chẳng hạn như các thư mục hệ điều hành cốt lõi như Windows, thư mục Thư viện macOS. Khi điều này xảy ra, trình duyệt sẽ hiện một lời nhắc và yêu cầu người dùng chọn một thư mục khác.

Sửa đổi tệp hoặc thư mục hiện có

Ứng dụng web không thể sửa đổi tệp trên đĩa mà không có sự cho phép rõ ràng của người dùng.

Lời nhắc cấp quyền

Nếu một người muốn lưu các thay đổi vào một tệp mà trước đây họ đã được cấp quyền đọc, thì trình duyệt sẽ hiện một lời nhắc cấp quyền, yêu cầu quyền cho trang web ghi các thay đổi vào ổ đĩa. Bạn chỉ có thể kích hoạt yêu cầu cấp quyền bằng một cử chỉ của người dùng, chẳng hạn như bằng cách nhấp vào nút Lưu.

Lời nhắc cấp quyền xuất hiện trước khi lưu tệp.
Lời nhắc xuất hiện với người dùng trước khi trình duyệt được cấp quyền ghi trên một tệp hiện có.

Ngoài ra, một ứng dụng web chỉnh sửa nhiều tệp (chẳng hạn như IDE) cũng có thể yêu cầu cấp quyền lưu các thay đổi tại thời điểm mở.

Nếu người dùng chọn Huỷ và không cấp quyền ghi, thì ứng dụng web sẽ không thể lưu các thay đổi vào tệp cục bộ. Bạn nên cung cấp một phương thức thay thế để người dùng lưu dữ liệu của họ, chẳng hạn như bằng cách cung cấp một cách để "tải" tệp xuống hoặc lưu dữ liệu vào đám mây.

Sự minh bạch

Biểu tượng trên thanh địa chỉ
Biểu tượng trên thanh địa chỉ cho biết người dùng đã cấp cho trang web quyền lưu vào tệp cục bộ.

Sau khi người dùng cấp quyền cho một ứng dụng web để lưu tệp cục bộ, trình duyệt sẽ hiện một biểu tượng trong thanh địa chỉ. Khi nhấp vào biểu tượng này, một cửa sổ bật lên sẽ mở ra, cho thấy danh sách các tệp mà người dùng đã cấp quyền truy cập. Người dùng luôn có thể thu hồi quyền truy cập đó nếu muốn.

Quyền có tác dụng lâu dài

Ứng dụng web có thể tiếp tục lưu các thay đổi vào tệp mà không cần nhắc cho đến khi bạn đóng tất cả các thẻ cho nguồn gốc của tệp. Sau khi thẻ bị đóng, trang web sẽ mất toàn bộ quyền truy cập. Vào lần tiếp theo người dùng sử dụng ứng dụng web, họ sẽ được nhắc lại về quyền truy cập vào các tệp.

Phản hồi

Chúng tôi muốn biết ý kiến của bạn về trải nghiệm khi sử dụng File System Access API.

Hãy cho chúng tôi biết về thiết kế API

Có vấn đề gì về API mà bạn không mong muốn không? Hoặc có phương thức hay thuộc tính nào bị thiếu mà bạn cần triển khai ý tưởng của mình không? Bạn có câu hỏi hoặc bình luận về mô hình bảo mật?

Bạn gặp vấn đề khi triển khai?

Bạn có phát hiện thấy lỗi trong quá trình triển khai của Chrome không? Hoặc việc triển khai có khác với quy cách không?

  • Báo cáo lỗi tại https://new.crbug.com. Nhớ cung cấp càng nhiều thông tin chi tiết càng tốt, hướng dẫn tái tạo và đặt Components (Thành phần) thành Blink>Storage>FileSystem.

Bạn đang lên kế hoạch sử dụng API này?

Bạn có dự định sử dụng API Truy cập hệ thống tệp trên trang web của mình không? Sự ủng hộ công khai của bạn giúp chúng tôi ưu tiên các tính năng và cho các nhà cung cấp trình duyệt khác thấy tầm quan trọng của việc hỗ trợ các tính năng này.

Đường liên kết hữu ích

Lời cảm ơn

Quy cách API Truy cập hệ thống tệp được viết bởi Marijn Kruisselbrink.