檔案系統標準採用原始私人檔案系統 (OPFS) 做為網頁來源的私人儲存端點,不會讓使用者看見,而使用者會提供選擇性的存取某類檔案,以實現效能極佳的最佳化。
瀏覽器支援
新式瀏覽器支援來源私人檔案系統,並且根據「檔案系統運作標準」的 Web Hypertext Application Technology Working Group (WHATWG) 標準化。
動機
回想電腦上的檔案時,您可能會想到檔案階層:透過作業系統的檔案瀏覽器,您可以瀏覽資料夾內的檔案。以 Windows 裝置為例,當使用者名叫阿湯時,他們的「待辦事項」清單可能就位於 C:\Users\Tom\Documents\ToDo.txt
。在此範例中,ToDo.txt
是檔案名稱,而 Users
、Tom
和 Documents
是資料夾名稱。Windows 上的 `C:` 代表磁碟機的根目錄。
在網路上處理檔案的傳統方法
如要在網頁應用程式中編輯「待辦事項」清單,可按照以下流程進行:
- 使用者將檔案上傳至伺服器,或使用
<input type="file">
在用戶端開啟檔案。 - 使用者做出變更,然後利用插入的
<a download="ToDo.txt>
來下載產生的檔案,而您可以透過 JavaScript 以程式輔助方式插入click()
。 - 開啟資料夾時,您可在
<input type="file" webkitdirectory>
中使用特殊屬性,儘管其專屬名稱雖然其專屬名稱卻幾乎適用於通用的瀏覽器。
在網路上處理檔案的新方式
這個流程並不代表使用者編輯檔案的想法,也就是說,使用者最終會下載輸入檔案的副本。因此,File System Access API 引入了三種選擇器方法:showOpenFilePicker()
、showSaveFilePicker()
和 showDirectoryPicker()
,可完全按照名稱所說的意向。可以如下啟用流程:
- 使用
showOpenFilePicker()
開啟ToDo.txt
,並取得FileSystemFileHandle
物件。 - 從
FileSystemFileHandle
物件呼叫檔案控制代碼的getFile()
方法,即可取得File
。 - 修改檔案,然後在控點上呼叫
requestPermission({mode: 'readwrite'})
。 - 如果使用者接受權限要求,請將變更儲存回原始檔案。
- 或者,您也可以呼叫
showSaveFilePicker()
讓使用者選擇新檔案。(如果使用者選取先前開啟的檔案,內容將會被覆寫)。如要重複儲存,您可以保留檔案控制代碼,這樣就不必再次顯示檔案儲存對話方塊。
在網路上使用檔案的限制
可透過這些方法存取的檔案和資料夾,位於稱為「使用者可見」的檔案系統中。從網路上儲存的檔案和特別的可執行檔案都會標上網路標記,因此作業系統可能在執行有安全疑慮的檔案前會顯示額外的警告。做為額外安全性功能,從網路取得的檔案也會受到安全瀏覽的保護。為了簡單起見,本文提供雲端病毒掃描功能。使用 File System Access API 將資料寫入檔案時,寫入功能無法原封不動,而是使用暫存檔案。除非檔案通過所有這些安全檢查,否則檔案本身不會遭到修改。可以想像成,雖然此工作盡可能地改善檔案作業速度 (例如 macOS 上)。儘管所有的 write()
呼叫仍然都是獨立的,因此在開啟檔案後,會尋求指定的位移,最後寫入資料。
檔案做為處理基礎
同時,檔案也是記錄資料的絕佳方式。例如,SQLite 會將整個資料庫儲存在單一檔案中。另一個例子是處理圖片時使用的 mipmap。mipmap 是預先計算並經過最佳化處理的圖像序列,每個序列解析度都是前次會議的解析度較低,因此許多作業 (例如放大) 的速度較快。那麼,網頁應用程式該如何享受檔案帶來的優勢,卻又不甚成本處理網頁式檔案處理效能?答案是來源私人檔案系統。
使用者可以看見與來源私人檔案系統的差異
不同於透過作業系統的檔案瀏覽器瀏覽的檔案系統,使用者可以讀取、寫入、移動、重新命名檔案和資料夾,而不會看到來源私人檔案系統。原始私人檔案系統中的檔案和資料夾顧名思義,就是私人檔案,並且更明確地呈現網站來源的資料。在開發人員工具控制台中輸入 location.origin
,即可找出頁面的來源。舉例來說,網頁 https://developer.chrome.com/articles/
的來源為 https://developer.chrome.com
(也就是說 /articles
部分「不是」來源的一部分)。如要進一步瞭解來源理論,請參閱瞭解「相同網站」和「same-origin」。凡是來源相同的網頁,都能看到相同的來源私人檔案系統資料,因此 https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/
可以查看與上一個範例相同的詳細資料。每個來源都有自己的獨立來源私人檔案系統,這表示 https://developer.chrome.com
的來源私人檔案系統與各來源 (例如 https://web.dev
) 完全不同。在 Windows 上,使用者可以查看的檔案系統根目錄為 C:\\
。
原始私人檔案系統的同等項目是每個來源的初始空白根目錄,其可透過呼叫非同步方法存取
navigator.storage.getDirectory()
。
如需使用者可見的檔案系統與來源私人檔案系統的比較,請參閱下方圖表。這張圖表顯示除了根目錄以外,其他所有項目的概念都相同,即依照資料和儲存空間需求,採用階層式檔案和資料夾階層來整理和排列。
來源私人檔案系統注意事項
就像瀏覽器中的其他儲存機制 (例如 localStorage 或 IndexedDB) 一樣,來源私人檔案系統受到瀏覽器配額限制。當使用者清除所有瀏覽資料或所有網站資料時,來源的私人檔案系統也會一併刪除。呼叫 navigator.storage.estimate()
,並在產生的回應物件中查看 usage
項目,查看應用程式目前已耗用多少儲存空間;您可以透過 usageDetails
物件中的儲存機制,特別查看 fileSystem
項目中的特定內容。由於使用者看不到來源私人檔案系統,因此系統不會要求任何權限提示,也不會進行安全瀏覽檢查。
取得根目錄的存取權
如要存取根目錄,請執行下列指令。最終會產生空白的目錄控制代碼,更具體來說,也就是 FileSystemDirectoryHandle
。
const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);
主執行緒或網路工作站
使用來源私人檔案系統的方式有兩種:在主執行緒或 Web Worker 中。Web Worker 無法封鎖主執行緒,這表示在這種結構定義中,API 可以同步,但主執行緒一般會禁止這種模式。同步 API 可加快其避免處理承諾的速度,且檔案作業通常在可編譯至 WebAssembly 的 C 等語言中同步。
// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);
如果您需要盡快執行檔案作業,或者處理 WebAssembly 的相關事務,請跳至「在網路工作處理程序中使用原始私人檔案系統」。否則,請繼續閱讀。
在主執行緒上使用來源私人檔案系統
建立新的檔案和資料夾
取得根資料夾後,請分別使用 getFileHandle()
和 getDirectoryHandle()
方法建立檔案和資料夾。如果傳遞「{create: true}
」,如果檔案或資料夾不存在,系統會自動建立該檔案或資料夾。使用新建立的目錄做為起點來呼叫這些函式,藉此建立檔案階層。
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});
存取現有的檔案和資料夾
如果您知道資料夾的名稱,請呼叫 getFileHandle()
或 getDirectoryHandle()
方法,並傳入檔案或資料夾的名稱,即可存取先前建立的檔案和資料夾。
const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
.getDirectoryHandle('my first folder');
取得與檔案控制代碼相關聯的檔案以便讀取
FileSystemFileHandle
代表檔案系統中的檔案。如要取得相關聯的 File
,請使用 getFile()
方法。File
物件是特定類型的 Blob
,可用於 Blob
的任何環境。請特別注意,FileReader
、URL.createObjectURL()
、createImageBitmap()
和 XMLHttpRequest.send()
同時接受 Blobs
和 Files
。如果你確定要這麼做,請向 FileSystemFileHandle
「免費」取得 File
讓您存取這些資料,並提供給使用者可檢視的檔案系統。
const file = await fileHandle.getFile();
console.log(await file.text());
透過串流寫入檔案
呼叫 createWritable()
即可建立 FileSystemWritableFileStream
,然後write()
將資料串流至檔案。最後,您會需要close()
訊息串。
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();
刪除檔案和資料夾
呼叫這些檔案或目錄控制代碼的特定 remove()
方法,即可刪除檔案和資料夾。如要刪除包含所有子資料夾的資料夾,請傳送 {recursive: true}
選項。
await fileHandle.remove();
await directoryHandle.remove({recursive: true});
或者,如果您知道要刪除目錄中要刪除檔案或資料夾的名稱,請使用 removeEntry()
方法。
directoryHandle.removeEntry('my first nested file');
移動及重新命名檔案和資料夾
使用 move()
方法重新命名及移動檔案和資料夾。變更和重新命名作業可以同時進行,也可以單獨進行。
// 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');
解析檔案或資料夾的路徑
如要瞭解特定檔案或資料夾位於參照目錄的位置,請使用 resolve()
方法,將 FileSystemHandle
做為引數傳遞。如要取得原始私人檔案系統中檔案或資料夾的完整路徑,請使用根目錄做為透過 navigator.storage.getDirectory()
取得的參照目錄。
const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.
檢查兩個檔案或資料夾的控點是否指向同一個檔案或資料夾
有時會有兩個帳號代碼,但不確定是否指向同一個檔案或資料夾。如要檢查此情況是否屬於這種情況,請使用 isSameEntry()
方法。
fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.
列出資料夾內容
FileSystemDirectoryHandle
是非同步疊代器,可使用 for await…of
迴圈疊代。做為非同步疊代器,也支援 entries()
、values()
和 keys()
方法,您可以根據需要的資訊選擇:
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()) {}
以遞迴方式列出資料夾和所有子資料夾的內容
處理非同步迴圈和搭配遞迴的函式很容易出錯。您可以從下列函式著手,其中列出資料夾及其所有子資料夾的內容 (包括所有檔案及其大小)。不需要檔案大小時,您可以簡化函式,也就是 directoryEntryPromises.push
,不要直接推送 handle.getFile()
承諾,而是直接推送 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;
};
在網路工作站中使用來源私人檔案系統
如前所述,Web Worker 無法封鎖主執行緒,所以在此情境同步方法中可以使用。
取得同步存取控制代碼
盡可能加快檔案作業的進入點是 FileSystemSyncAccessHandle
,透過呼叫 createSyncAccessHandle()
,從一般 FileSystemFileHandle
取得。
const fileHandle = await opfsRoot
.getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();
就地同步檔案方法
設定同步存取處理常式後,您就可以採用立即可用的所有同步檔案方法。
getSize()
:傳回檔案大小,以位元組為單位。write()
:將緩衝區內容寫入檔案 (可選擇在指定偏移處),然後傳回寫入的位元組數。檢查傳回的位元組數可讓呼叫端偵測並處理錯誤和部分寫入作業。read()
:將檔案內容讀入緩衝區中,可選擇在指定偏移處讀取。truncate()
:將檔案調整為指定大小。flush()
:確保檔案內容包含透過write()
完成的所有修改。close()
:關閉存取控點。
以下示例說明如何使用上述所有方法。
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);
將檔案從來源私人檔案系統複製到使用者可見的檔案系統
如上所述,雖然無法將檔案從來源私人檔案系統移至使用者可見的檔案系統,但您可以複製檔案。由於 showSaveFilePicker()
只會在主執行緒上公開,但不在背景工作執行緒中,因此請務必在該執行緒中執行程式碼。
// 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);
}
對來源私人檔案系統進行偵錯
在新增開發人員工具支援功能之前 (請參閱 crbug/1284595),使用 OPFS Explorer Chrome 擴充功能對來源私人檔案系統進行偵錯。「建立新的檔案和資料夾」一節的螢幕截圖,都是從擴充功能直接取得。
安裝擴充功能後,請開啟 Chrome 開發人員工具,選取「OPFS Explorer」分頁標籤,接著就能檢查檔案階層。按一下檔案名稱,然後按一下垃圾桶圖示刪除檔案和資料夾,即可將來源私人檔案系統中的檔案儲存至使用者可檢視的檔案系統。
示範
在示範中,查看來源私人檔案系統的實際操作情形 (如果您安裝 OPFS Explorer 擴充功能),可將該系統做為編譯為 WebAssembly 的 SQLite 資料庫後端。請務必查看 Glitch 上的原始碼。請注意,下方的嵌入版本不會使用來源私人檔案系統後端 (因為 iframe 是跨來源),而當您另用分頁開啟示範內容時,就會發生這種狀況。
結論
WHATWG 所指定的原始私人檔案系統,改變了我們在網路上使用檔案及與檔案互動的方式。它開創了全新用途,為使用者可見的檔案系統根本無法實現。所有主要的瀏覽器廠商 (Apple、Mozilla 和 Google) 都積極參與,共同實現共同願景。開發原始私人檔案系統相當仰賴大家合作,而開發人員和使用者的意見對這項發展至關重要。在我們持續修正及改善標準的同時,歡迎您透過「問題」或「提取要求」形式對 whatwg/fs 存放區提供意見。
相關連結
特別銘謝
本文評論者為 Austin Sully、Etienne Noël 和 Rachel Andrew。主頁橫幅由 Christina Rumpf 針對 Unsplash 提供。