Hệ thống tệp riêng tư gốc

Tiêu chuẩn hệ thống tệp giới thiệu một hệ thống tệp riêng tư có nguồn gốc (OPFS) dưới dạng một điểm cuối lưu trữ riêng tư đối với nguồn gốc của trang và người dùng không thể nhìn thấy, cung cấp quyền truy cập tùy chọn vào một loại tệp đặc biệt được tối ưu hóa cao cho hiệu suất.

Hỗ trợ trình duyệt

Hệ thống tệp riêng tư gốc được các trình duyệt hiện đại hỗ trợ và được chuẩn hoá theo Nhóm hoạt động công nghệ ứng dụng siêu văn bản trên web (whatWG) trong File System Living Standard.

Hỗ trợ trình duyệt

  • 86
  • 86
  • 111
  • 15,2

Nguồn

Động lực

Khi nghĩ về tệp trên máy tính, bạn có thể nghĩ về hệ phân cấp tệp: các tệp được sắp xếp trong các thư mục mà bạn có thể khám phá bằng trình khám phá tệp của hệ điều hành. Ví dụ: trên Windows, đối với người dùng có tên là Tom, danh sách Việc cần làm của họ có thể nằm ở C:\Users\Tom\Documents\ToDo.txt. Trong ví dụ này, ToDo.txt là tên tệp, Users, TomDocuments là tên thư mục. "C:" trên Windows biểu thị thư mục gốc của ổ đĩa.

Cách làm việc truyền thống với các tệp trên web

Để chỉnh sửa danh sách Việc cần làm trong một ứng dụng web, đây là quy trình thông thường:

  1. Người dùng tải tệp lên máy chủ hoặc mở tệp trên ứng dụng bằng <input type="file">.
  2. Người dùng thực hiện thay đổi rồi tải xuống tệp kết quả có <a download="ToDo.txt> được chèn mà bạn click() theo phương thức lập trình thông qua JavaScript.
  3. Để mở thư mục, bạn sử dụng một thuộc tính đặc biệt trong <input type="file" webkitdirectory>. Thuộc tính này thực tế có hỗ trợ trình duyệt phổ biến, mặc dù có tên thuộc quyền sở hữu riêng.

Cách làm việc hiện đại với các tệp trên web

Quy trình này không thể hiện suy nghĩ của người dùng về việc chỉnh sửa tệp và có nghĩa là người dùng sẽ tải bản sao của tệp đầu vào xuống. Do đó, API Truy cập hệ thống tệp đã giới thiệu 3 phương thức bộ chọn (showOpenFilePicker(), showSaveFilePicker()showDirectoryPicker()) hoạt động đúng như tên gọi của chúng. Chúng bật một quy trình như sau:

  1. Mở ToDo.txt bằng showOpenFilePicker() và lấy đối tượng FileSystemFileHandle.
  2. Từ đối tượng FileSystemFileHandle, hãy lấy File bằng cách gọi phương thức getFile() của tên người dùng tệp.
  3. Sửa đổi tệp, rồi gọi requestPermission({mode: 'readwrite'}) trên tên người dùng.
  4. Nếu người dùng chấp nhận yêu cầu cấp quyền, hãy lưu nội dung thay đổi vào tệp gốc.
  5. Ngoài ra, hãy gọi showSaveFilePicker() và cho phép người dùng chọn một tệp mới. (Nếu người dùng chọn một tệp đã mở trước đó, nội dung của tệp đó sẽ bị ghi đè.) Đối với các thao tác lưu nhiều lần, bạn có thể giữ tên người dùng xử lý tệp để không phải hiện lại hộp thoại lưu tệp.

Các hạn chế khi làm việc với tệp trên web

Các tệp và thư mục có thể truy cập được bằng các phương thức này nằm trong hệ thống tệp người dùng hiển thị. Các tệp lưu từ web và cụ thể là các tệp thực thi được đánh dấu bằng nhãn hiệu của web, do đó sẽ có thêm cảnh báo mà hệ điều hành có thể hiển thị trước khi một tệp tiềm ẩn nguy hiểm được thực thi. Là một tính năng bảo mật bổ sung, các tệp lấy từ web cũng được bảo vệ bằng công cụ Duyệt web an toàn. Để đơn giản và trong ngữ cảnh của bài viết này, bạn có thể coi đây là quá trình quét vi-rút trên đám mây. Khi bạn ghi dữ liệu vào một tệp bằng API Truy cập hệ thống tệp, hoạt động ghi không diễn ra, mà hãy sử dụng tệp tạm thời. Tệp sẽ không bị sửa đổi trừ phi tệp vượt qua tất cả các bước kiểm tra bảo mật này. Như bạn có thể hình dung, thao tác này khiến thao tác đối với tệp tương đối chậm, mặc dù có những điểm cải tiến được áp dụng khi có thể, chẳng hạn như trên macOS. Tuy nhiên, mọi lệnh gọi write() đều độc lập, nên về bản chất, lệnh gọi này sẽ mở tệp, tìm độ lệch đã cho và cuối cùng là ghi dữ liệu.

Tệp đóng vai trò nền tảng cho việc xử lý

Đồng thời, tệp cũng là một cách hiệu quả để ghi lại dữ liệu. Ví dụ: SQLite lưu trữ toàn bộ cơ sở dữ liệu trong một tệp duy nhất. Một ví dụ khác là mipmap được sử dụng trong xử lý hình ảnh. Mipmap là các chuỗi hình ảnh được tính toán trước và tối ưu hoá, mỗi trình tự là một bản trình bày có độ phân giải thấp hơn dần dần so với trước đây, giúp nhiều thao tác như thu phóng nhanh hơn. Vậy làm cách nào để các ứng dụng web có thể tận hưởng lợi ích của tệp mà không phải chịu chi phí hiệu suất của việc xử lý tệp dựa trên web? Câu trả lời chính là hệ thống tệp riêng tư gốc.

Hệ thống tệp riêng tư mà người dùng nhìn thấy so với hệ thống tệp riêng tư gốc

Không giống như hệ thống tệp mà người dùng nhìn thấy được khi duyệt xem bằng trình khám phá tệp của hệ điều hành, với các tệp và thư mục mà bạn có thể đọc, ghi, di chuyển và đổi tên, hệ thống tệp riêng tư gốc sẽ không cho người dùng xem. Các tệp và thư mục trong hệ thống tệp riêng tư gốc, đúng như tên gọi, là tệp riêng tư và cụ thể hơn là riêng tư đối với nguồn gốc của trang web. Tìm hiểu nguồn gốc của một trang bằng cách nhập location.origin vào Bảng điều khiển công cụ cho nhà phát triển. Ví dụ: nguồn gốc của trang https://developer.chrome.com/articles/https://developer.chrome.com (nghĩa là phần /articles không phải là phần nguồn gốc). Bạn có thể đọc thêm về lý thuyết nguồn gốc trong phần Tìm hiểu về "same-site" và "same-origin". Tất cả các trang có cùng nguồn gốc đều có thể xem dữ liệu hệ thống tệp riêng tư của cùng một nguồn gốc, vì vậy, https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/ có thể xem các chi tiết giống như ví dụ trước. Mỗi nguồn gốc có hệ thống tệp riêng tư gốc độc lập, nghĩa là hệ thống tệp riêng tư gốc của https://developer.chrome.com hoàn toàn khác với hệ thống tệp, chẳng hạn như https://web.dev. Trên Windows, thư mục gốc của hệ thống tệp mà người dùng thấy là C:\\. Tương đương với hệ thống tệp riêng tư gốc là một thư mục gốc trống ban đầu trên mỗi nguồn gốc được truy cập bằng cách gọi phương thức không đồng bộ navigator.storage.getDirectory(). Để so sánh hệ thống tệp mà người dùng hiển thị và hệ thống tệp riêng tư theo nguồn gốc, hãy xem sơ đồ dưới đây. Sơ đồ này cho thấy ngoài thư mục gốc, mọi thứ khác về mặt lý thuyết đều giống nhau, với hệ phân cấp tệp và thư mục để sắp xếp và sắp xếp theo nhu cầu của bạn về dữ liệu và lưu trữ.

Sơ đồ hệ thống tệp mà người dùng nhìn thấy và hệ thống tệp riêng tư gốc với hai hệ thống phân cấp tệp mẫu. Điểm truy cập cho hệ thống tệp mà người dùng nhìn thấy là một ổ đĩa cứng tượng trưng, điểm truy cập cho hệ thống tệp riêng tư gốc đang gọi phương thức &quot;navigation.storage.getDirectory&quot;.

Thông tin cụ thể về hệ thống tệp riêng tư gốc

Giống như các cơ chế lưu trữ khác trong trình duyệt (ví dụ: localStorage hoặc IndexedDB), hệ thống tệp riêng tư gốc phải tuân theo các hạn chế về hạn mức của trình duyệt. Khi người dùng xoá tất cả dữ liệu duyệt web hoặc tất cả dữ liệu trang web, hệ thống tệp riêng tư gốc cũng sẽ bị xoá. Gọi navigator.storage.estimate() và trong đối tượng phản hồi thu được, hãy xem mục usage để biết mức dung lượng lưu trữ mà ứng dụng của bạn đã sử dụng. Mức bộ nhớ này được chia nhỏ theo cơ chế lưu trữ trong đối tượng usageDetails, nơi bạn muốn xem cụ thể mục fileSystem. Vì hệ thống tệp riêng tư gốc không hiển thị với người dùng, nên không có lời nhắc cấp quyền và tính năng Duyệt web an toàn không kiểm tra.

Truy cập vào thư mục gốc

Để có quyền truy cập vào thư mục gốc, hãy chạy lệnh sau. Bạn sẽ kết thúc bằng một ô điều khiển thư mục trống, cụ thể hơn là FileSystemDirectoryHandle.

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

Luồng chính hoặc Trình chạy web

Có hai cách sử dụng hệ thống tệp riêng tư gốc: trên luồng chính hoặc trong Trình chạy web. Trình chạy web không thể chặn luồng chính, nghĩa là trong ngữ cảnh này, API có thể đồng bộ, một mẫu thường không được phép trên luồng chính. API đồng bộ có thể nhanh hơn vì chúng tránh được việc phải xử lý các lời hứa và hoạt động của tệp thường được đồng bộ hoá bằng các ngôn ngữ như C có thể được biên dịch thành WebAssembly.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

Nếu bạn cần thao tác tệp nhanh nhất có thể hoặc xử lý WebAssembly, hãy chuyển xuống phần Sử dụng hệ thống tệp riêng tư gốc trong Web Worker. Nếu không, bạn có thể đọc tiếp.

Sử dụng hệ thống tệp riêng tư gốc trên luồng chính

Tạo tệp và thư mục mới

Khi bạn có thư mục gốc, hãy tạo tệp và thư mục bằng phương thức getFileHandle()getDirectoryHandle() tương ứng. Bằng cách truyền {create: true}, tệp hoặc thư mục sẽ được tạo nếu chưa có. Xây dựng một hệ phân cấp tệp bằng cách gọi các hàm này thông qua thư mục mới tạo làm điểm xuất phát.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

Hệ phân cấp tệp thu được từ mã mẫu trước đó.

Truy cập vào các tệp và thư mục hiện có

Nếu bạn biết tên của các tệp và thư mục đó, hãy truy cập vào các tệp và thư mục đã tạo trước đó bằng cách gọi phương thức getFileHandle() hoặc getDirectoryHandle(), truyền tên của tệp hoặc thư mục vào.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

Lấy tệp được liên kết với ô điều khiển tệp để đọc

FileSystemFileHandle đại diện cho một tệp trên hệ thống tệp. Để lấy File được liên kết, hãy sử dụng phương thức getFile(). Đối tượng File là một loại Blob cụ thể và có thể được dùng trong mọi ngữ cảnh mà Blob có thể. Cụ thể, FileReader, URL.createObjectURL(), createImageBitmap()XMLHttpRequest.send() chấp nhận cả BlobsFiles. Nếu có, việc lấy File từ FileSystemFileHandle sẽ "giải phóng" dữ liệu để bạn có thể truy cập và cung cấp dữ liệu đó cho hệ thống tệp mà người dùng thấy.

const file = await fileHandle.getFile();
console.log(await file.text());

Ghi vào tệp bằng cách truyền trực tuyến

Truyền trực tuyến dữ liệu vào tệp bằng cách gọi createWritable() để tạo FileSystemWritableFileStream cho bạn, sau đó write() nội dung. Khi kết thúc, bạn cần close() phát trực tiếp.

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

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

Xoá tệp và thư mục bằng cách gọi phương thức remove() cụ thể của trình xử lý tệp hoặc thư mục. Để xoá một thư mục bao gồm tất cả thư mục con, hãy chuyển tuỳ chọn {recursive: true}.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

Ngoài ra, nếu bạn biết tên của tệp hoặc thư mục cần xoá trong một thư mục, hãy sử dụng phương thức removeEntry().

directoryHandle.removeEntry('my first nested file');

Di chuyển và đổi tên tệp và thư mục

Đổi tên và di chuyển tệp cũng như thư mục bằng phương thức move(). Bạn có thể di chuyển và đổi tên cùng lúc hoặc tách biệt.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

Phân giải đường dẫn của tệp hoặc thư mục

Để tìm hiểu vị trí của một tệp hoặc thư mục cụ thể so với một thư mục tham chiếu, hãy sử dụng phương thức resolve(), truyền vào đó một FileSystemHandle làm đối số. Để lấy đường dẫn đầy đủ của một tệp hoặc thư mục trong hệ thống tệp riêng tư gốc, hãy sử dụng thư mục gốc làm thư mục tham chiếu lấy được qua navigator.storage.getDirectory().

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

Kiểm tra xem hai tên người dùng của tệp hoặc thư mục có trỏ đến cùng một tệp hoặc thư mục hay không

Đôi khi, bạn có 2 tên người dùng và không biết liệu chúng có trỏ đến cùng một tệp hoặc thư mục hay không. Để kiểm tra xem đây có phải là trường hợp của bạn hay không, hãy sử dụng phương thức isSameEntry().

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

Liệt kê nội dung của thư mục

FileSystemDirectoryHandle là một trình lặp không đồng bộ mà bạn lặp lại bằng vòng lặp for await…of. Là một trình lặp không đồng bộ, nó cũng hỗ trợ các phương thức entries(), values()keys() mà từ đó bạn có thể chọn tùy thuộc vào thông tin bạn cần:

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

Liệt kê nội dung của thư mục và tất cả các thư mục con theo quy tắc đệ quy

Xử lý các vòng lặp và hàm không đồng bộ được ghép nối với đệ quy rất dễ gây ra lỗi. Hàm dưới đây có thể đóng vai trò là điểm bắt đầu để liệt kê nội dung của thư mục và tất cả các thư mục con trong đó, bao gồm tất cả tệp và kích thước của thư mục. Bạn có thể đơn giản hoá hàm này nếu không cần kích thước tệp, trong đó nêu directoryEntryPromises.push, không đẩy lời hứa handle.getFile() mà trực tiếp nhập handle.

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

Sử dụng hệ thống tệp riêng tư gốc trong Web Worker

Như đã trình bày trước đó, Trình chạy web không thể chặn luồng chính. Do đó, chúng tôi cho phép sử dụng các phương thức đồng bộ trong ngữ cảnh này.

Lấy tên người dùng truy cập đồng bộ

Điểm truy cập để thực hiện các thao tác nhanh nhất có thể trên tệp là FileSystemSyncAccessHandle, lấy từ FileSystemFileHandle thông thường bằng cách gọi createSyncAccessHandle().

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

Phương thức tệp đồng bộ tại chỗ

Sau khi có tên người dùng truy cập đồng bộ, bạn sẽ có quyền truy cập vào các phương thức tệp đồng bộ tại chỗ và tất cả đều đồng bộ.

  • getSize(): Trả về kích thước của tệp tính bằng byte.
  • write(): Ghi nội dung của vùng đệm vào tệp (không bắt buộc tại một độ lệch nhất định) và trả về số lượng byte đã ghi. Việc kiểm tra số byte được ghi được trả về cho phép phương thức gọi phát hiện và xử lý lỗi cũng như hoạt động ghi một phần.
  • read(): Đọc nội dung của tệp vào vùng đệm, nếu muốn tại một độ lệch nhất định.
  • truncate(): Đổi kích thước tệp theo kích thước đã cho.
  • flush(): Đảm bảo nội dung của tệp chứa tất cả các nội dung sửa đổi được thực hiện thông qua write().
  • close(): Đóng ô điều khiển truy cập.

Sau đây là ví dụ sử dụng tất cả các phương thức nêu trên.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

Sao chép một tệp từ hệ thống tệp riêng tư gốc vào hệ thống tệp mà người dùng nhìn thấy

Như đã đề cập ở trên, bạn không thể di chuyển tệp từ hệ thống tệp riêng tư gốc sang hệ thống tệp hiển thị cho người dùng, nhưng bạn có thể sao chép tệp. Vì showSaveFilePicker() chỉ hiển thị trên luồng chính, chứ không hiển thị trong luồng Worker, hãy nhớ chạy mã ở đó.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

Gỡ lỗi hệ thống tệp riêng tư gốc

Cho đến khi thêm tính năng hỗ trợ Công cụ cho nhà phát triển tích hợp sẵn (xem crbug/1284595), hãy sử dụng tiện ích OPFS Explorer của Chrome để gỡ lỗi hệ thống tệp riêng tư theo nguồn gốc. Ảnh chụp màn hình ở phần Tạo tệp và thư mục mới ở trên được chụp ngay từ tiện ích.

Tiện ích OPFS Explorer dành cho công cụ của Chrome cho nhà phát triển trong Cửa hàng Chrome trực tuyến.

Sau khi cài đặt tiện ích, hãy mở Công cụ của Chrome cho nhà phát triển, chọn thẻ OPFS Explorer (Trình khám phá OPFS), sau đó, bạn có thể kiểm tra hệ thống phân cấp tệp. Lưu tệp từ hệ thống tệp riêng tư gốc vào hệ thống tệp hiển thị cho người dùng bằng cách nhấp vào tên tệp, rồi xoá tệp và thư mục bằng cách nhấp vào biểu tượng thùng rác.

Bản minh hoạ

Xem hệ thống tệp riêng tư nguồn gốc hoạt động (nếu bạn cài đặt tiện ích OPFS Explorer) trong bản minh hoạ sử dụng hệ thống này làm phần phụ trợ cho cơ sở dữ liệu SQLite được biên dịch thành WebAssembly. Hãy nhớ xem mã nguồn trên Glitch. Lưu ý cách phiên bản được nhúng bên dưới không sử dụng phần phụ trợ hệ thống tệp riêng tư cho nguồn gốc (vì iframe này có hoạt động trên nhiều nguồn gốc), nhưng khi bạn mở bản minh hoạ trong một thẻ riêng thì điều này sẽ xảy ra.

Kết luận

Hệ thống tệp riêng tư theo nguồn gốc, như được chỉ định bởi trực tiếp, đã định hình cách chúng ta sử dụng và tương tác với các tệp trên web. Công cụ này hỗ trợ các trường hợp sử dụng mới mà hệ thống tệp mà người dùng không thể làm được. Tất cả các nhà cung cấp trình duyệt lớn – Apple, Mozilla và Google – đều đã tham gia và có chung tầm nhìn. Việc phát triển hệ thống tệp riêng tư theo nguồn gốc đòi hỏi rất nhiều nỗ lực hợp tác, và ý kiến phản hồi của các nhà phát triển cũng như người dùng là rất quan trọng đối với quá trình phát triển của hệ thống này. Trong quá trình chúng tôi tiếp tục tinh chỉnh và cải tiến tiêu chuẩn này, chúng tôi rất mong nhận được ý kiến phản hồi về kho lưu trữ whatwg/fs dưới dạng Vấn đề hoặc Yêu cầu lấy dữ liệu.

Xác nhận

Bài viết này đã được Austin Sully, Etienne NoëlRachel Andrew xem xét. Hình ảnh chính của Christina Rumpf trên Unsplash.