Tiêu chuẩn Hệ thống tệp giới thiệu một hệ thống tệp riêng tư gốc (OPFS) làm điểm cuối lưu trữ riêng tư cho nguồn gốc của trang và người dùng không nhìn thấy. Hệ thống này 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.
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 Nhóm công tác về công nghệ ứng dụng siêu văn bản trên web (WHATWG) chuẩn hoá trong Tiêu chuẩn hiện hành về hệ thống tệp.
Động lực
Khi nghĩ đến các tệp trên máy tính, có lẽ bạn sẽ nghĩ đến một hệ thống 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 tên là Tom, danh sách Việc cần làm của họ có thể nằm trong C:\Users\Tom\Documents\ToDo.txt
. Trong ví dụ này, ToDo.txt
là tên tệp, còn Users
, Tom
và Documents
là tên thư mục. "C:" trên Windows đại diện cho thư mục gốc của ổ đĩa.
Cách truyền thống để làm việc với 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:
- Người dùng tải tệp lên một máy chủ hoặc mở tệp đó trên máy khách bằng
<input type="file">
. - Người dùng thực hiện các thay đổi rồi tải tệp kết quả xuống cùng với một
<a download="ToDo.txt>
được chèn mà bạnclick()
theo phương thức lập trình thông qua JavaScript. - Để mở thư mục, bạn sử dụng một thuộc tính đặc biệt trong
<input type="file" webkitdirectory>
. Mặc dù có tên độc quyền, nhưng thuộc tính này được hầu hết các trình duyệt hỗ trợ.
Cách thức hiện đại để làm việc với tệp trên web
Luồng này không thể hiện cách người dùng nghĩ về việc chỉnh sửa tệp, tức là người dùng sẽ có bản sao đã tải xuống của tệp đầu vào. Do đó, File System Access API đã giới thiệu 3 phương thức chọn – showOpenFilePicker()
, showSaveFilePicker()
và showDirectoryPicker()
– thực hiện đúng như tên gọi của chúng. Chúng cho phép một quy trình như sau:
- Mở
ToDo.txt
bằngshowOpenFilePicker()
và nhận đối tượngFileSystemFileHandle
. - Từ đối tượng
FileSystemFileHandle
, hãy lấyFile
bằng cách gọi phương thứcgetFile()
của trình xử lý tệp. - Sửa đổi tệp, sau đó gọi
requestPermission({mode: 'readwrite'})
trên đối tượng xử lý. - Nếu người dùng chấp nhận yêu cầu cấp quyền, hãy lưu các thay đổi vào tệp gốc.
- Hoặc 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 lần lưu lặp lại, bạn có thể giữ lại mã nhận dạng tệp để không phải hiện lại hộp thoại lưu tệp.
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 thông qua những phương thức này nằm trong hệ thống tệp hiển thị cho người dùng. Các tệp được lưu từ web (đặc biệt là tệp thực thi) sẽ được đánh dấu bằng dấu hiệu của web, vì vậy, hệ điều hành có thể hiện thêm một cảnh báo trước khi một tệp có khả năng gây 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 tính năng Duyệt web an toàn. Để đơn giản và trong bối cảnh của bài viết này, bạn có thể coi đây là một quy trình quét vi-rút dựa trên đám mây. Khi bạn ghi dữ liệu vào một tệp bằng File System Access API, các thao tác ghi sẽ không diễn ra tại chỗ mà sử dụng một tệp tạm thời. Bản thân tệp sẽ không bị sửa đổi trừ phi vượt qua tất cả các quy trình kiểm tra bảo mật này. Như bạn có thể hình dung, công việc này khiến các thao tác với tệp tương đối chậm, mặc dù đã áp dụng các điểm cải tiến nếu có thể, chẳng hạn như trên macOS. Tuy nhiên, mọi lệnh gọi write()
đều độc lập, vì vậy, ở chế độ nền, lệnh gọi này sẽ mở tệp, tìm đến độ lệch đã cho và cuối cùng là ghi dữ liệu.
Tệp là nền tảng của quá trình xử lý
Đồng thời, tệp là một cách tuyệt vời để 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 dùng trong quá trình xử lý hình ảnh. Mipmap là các chuỗi hình ảnh được tối ưu hoá và tính toán trước, mỗi chuỗi là một phiên bản có độ phân giải thấp hơn dần của phiên bản trước. Điều này giúp nhiều thao tác như thu phóng diễn ra nhanh hơn. Vậy làm cách nào để các ứng dụng web có thể tận dụng lợi ích của tệp mà không phải trả giá về hiệu suất khi xử lý tệp trên web? Câu trả lời là hệ thống tệp riêng tư gốc.
Người dùng có thể thấy hệ thống tệp riêng tư so với nguồn gốc
Không giống như hệ thống tệp mà người dùng có thể thấy khi duyệt 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 không dành cho người dùng. Như tên gọi cho thấy, các tệp và thư mục trong hệ thống tệp riêng tư gốc là riêng tư và cụ thể hơn là riêng tư đối với nguồn gốc của một trang web. Khám phá 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/
là https://developer.chrome.com
(tức là phần /articles
không thuộc nguồn gốc). Bạn có thể đọc thêm về lý thuyết nguồn gốc trong bài viết 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 cùng một dữ liệu trong hệ thống tệp riêng tư của nguồn gốc, vì vậy https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/
có thể xem cùng một thông tin chi tiết như ví dụ trước. Mỗi nguồn gốc có hệ thống tệp riêng tư độc lập của riêng mình, tức là hệ thống tệp riêng tư của nguồn gốc https://developer.chrome.com
hoàn toàn khác biệt với hệ thống tệp riêng tư của, 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 nhìn 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 ban đầu trống theo nguồn đượ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 nhìn thấy và hệ thống tệp riêng tư gốc, hãy xem sơ đồ sau. Sơ đồ cho thấy ngoài thư mục gốc, mọi thứ khác đều giống nhau về mặt khái niệm, với một hệ thống phân cấp các tệp và thư mục để sắp xếp và bố trí khi cần cho nhu cầu về dữ liệu và bộ nhớ của bạn.
Thông tin cụ thể về hệ thống tệp riêng tư gốc
Cũng 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ư của nguồn gốc phải tuân theo các hạn chế về hạn ngạch 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 kết quả, hãy xem mục usage
để biết mức bộ nhớ mà ứng dụng của bạn đã sử dụng, được chia nhỏ theo cơ chế lưu trữ trong đối tượng usageDetails
, trong đó 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ị cho người dùng nên không có lời nhắc về quyền và không có các bước kiểm tra của tính năng Duyệt web an toàn.
Truy cập vào thư mục gốc
Để truy cập vào thư mục gốc, hãy chạy lệnh sau. Bạn sẽ có một mã nhận dạng 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 Web Worker
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 Web Worker. Web Worker không thể chặn luồng chính, tức là trong ngữ cảnh này, các API có thể đồng bộ, một mẫu thường không được phép trên luồng chính. Các API đồng bộ có thể nhanh hơn vì chúng không phải xử lý các promise và các thao tác với tệp thường đồng bộ trong 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 thực hiện các thao tác nhanh nhất có thể với tệp hoặc bạn xử lý WebAssembly, hãy chuyển đến 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
Sau khi có thư mục gốc, hãy tạo tệp và thư mục bằng cách sử dụng phương thức getFileHandle()
và 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 bằng một thư mục mới tạo làm điểm bắt đầu.
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});
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 tệp hoặc 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 đó.
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 một mã nhận dạng 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()
và XMLHttpRequest.send()
chấp nhận cả Blobs
và Files
. Nếu bạn muốn, việc lấy một File
từ FileSystemFileHandle
sẽ "giải phóng" dữ liệu, nhờ đó 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 có thể 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 dữ liệu vào một tệp bằng cách gọi createWritable()
. Thao tác này sẽ tạo ra một FileSystemWritableFileStream
mà sau đó bạn sẽ write()
nội dung. Cuối cùng, bạn cần close()
luồng.
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 tệp hoặc thư mục đó. Để xoá một thư mục, bao gồm cả tất cả thư mục con, hãy truyền lựa 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 cũng như thư mục
Đổi tên và di chuyển tệp và 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 riêng 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 một tệp hoặc thư mục
Để biết vị trí của một tệp hoặc thư mục nhất định so với một thư mục tham chiếu, hãy sử dụng phương thức resolve()
, truyền cho phương thức này 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 thu được thông qua navigator.storage.getDirectory()
.
const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.
Kiểm tra xem 2 đối tượng xử lý 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ó hai đối tượng xử lý 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 một 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ộ, trình lặp này cũng hỗ trợ entries()
, values()
và keys()
. Bạn có thể chọn một trong các phương thức này tuỳ 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ê đệ quy nội dung của một thư mục và tất cả thư mục con
Việc xử lý các vòng lặp và hàm không đồng bộ kết hợp với đệ quy rất dễ bị sai. Hàm dưới đây có thể đóng vai trò là điểm bắt đầu để liệt kê nội dung của một thư mục và tất cả thư mục con của thư mục đó, bao gồm cả mọi tệp và kích thước của tệp. Bạn có thể đơn giản hoá hàm nếu không cần kích thước tệp bằng cách, ở chỗ có directoryEntryPromises.push
, không đẩy lời hứa handle.getFile()
mà đẩy trực tiế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 đó, Web Worker không thể chặn luồng chính. Đó là lý do trong ngữ cảnh này, các phương thức đồng bộ được cho phép.
Nhận một mã nhận dạng truy cập đồng bộ
Điểm truy cập vào các thao tác tệp nhanh nhất có thể là FileSystemSyncAccessHandle
, được 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 đồng bộ hoá tệp tại chỗ
Sau khi có một mã nhận 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 nhanh tại chỗ, 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 một 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ố byte đã ghi. Việc kiểm tra số lượng byte đã ghi được trả về cho phép người gọi phát hiện và xử lý lỗi cũng như các thao tác ghi một phần.read()
: Đọc nội dung của tệp vào một vùng đệm, không bắt buộc ở một độ lệch nhất định.truncate()
: Đổi kích thước tệp thành kích thước đã cho.flush()
: Đảm bảo rằng 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 quawrite()
.close()
: Đóng mã nhận dạng truy cập.
Dưới đâ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 sang hệ thống tệp mà người dùng có thể 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ư ban đầu sang hệ thống tệp mà người dùng có thể thấy, nhưng bạn có thể sao chép tệp. Vì showSaveFilePicker()
chỉ được hiển thị trên luồng chính chứ không phải trong luồng Worker, nên 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 tính năng hỗ trợ Công cụ cho nhà phát triển tích hợp được thêm vào (xem crbug/1284595), hãy sử dụng tiện ích OPFS Explorer trên Chrome để gỡ lỗi hệ thống tệp riêng tư gốc. Ảnh chụp màn hình ở trên trong phần Tạo tệp và thư mục mới được chụp trực tiếp từ tiện ích.
Sau khi cài đặt tiện ích này, 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 mà người dùng có thể thấy bằng cách nhấp vào tên tệp và xoá tệp cũng như 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ư gốc hoạt động (nếu bạn cài đặt tiện ích OPFS Explorer) trong một 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 ý rằng 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ư gốc (vì iframe là đa nguồn), nhưng khi bạn mở bản minh hoạ trong một thẻ riêng biệt, thì phiên bản này sẽ sử dụng.
Kết luận
Hệ thống tệp riêng tư gốc, theo chỉ định của WHATWG, đã đị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. Nó đã cho phép các trường hợp sử dụng mới mà không thể đạt được với hệ thống tệp mà người dùng nhìn thấy. 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 một tầm nhìn. Việc phát triển hệ thống tệp riêng tư gốc là một nỗ lực hợp tác rất lớn và ý kiến phản hồi của nhà phát triển và người dùng là yếu tố cần thiết để hệ thống này tiến bộ. Khi chúng tôi tiếp tục tinh chỉnh và cải thiệ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 hợp nhất.
Đường liên kết có liên quan
- Quy cách tiêu chuẩn của hệ thống tệp
- Kho lưu trữ Tiêu chuẩn hệ thống tệp
- File System API với bài đăng Origin Private File System WebKit
- Tiện ích OPFS Explorer
Lời cảm ơn
Bài viết này được Austin Sully, Etienne Noël và Rachel Andrew xem xét. Hình ảnh chính của Christina Rumpf trên Unsplash.