File System Access API 可讓網頁應用程式直接讀取或儲存使用者裝置上的檔案和資料夾變更。
什麼是 File System Access API?
File System Access API 可讓開發人員打造功能強大的網路應用程式, 使用者本機裝置上的檔案,例如 IDE、相片和影片編輯器、文字編輯器等。更新後 使用者授予網頁應用程式存取權時,這個 API 可讓使用者直接讀取或儲存檔案變更,以及 使用者裝置上的資料夾File System Access API 不僅提供讀取和寫入檔案,還提供 能夠開啟目錄並列舉其內容
如果您先前曾讀取及寫入檔案,關於以下部分 由於並非所有系統都與系統相似,建議您繼續閱讀。
適用於 iOS 的大多數 Chromium 瀏覽器都支援 File System Access API Windows、macOS、ChromeOS 和 Linux。值得一提的例外是「勇敢」 目前僅適用於標記之後。我們已在 crbug.com/1011535 環境中持續支援 Android。
使用 File System Access API
為了展現 File System Access API 的強大效能和實用性,我編寫了單一檔案文字 編輯器。開啟及編輯文字檔、將變更儲存回磁碟,或是直接啟動 一個新檔案,並將變更儲存到磁碟。雖說如此,但足以為你提供協助 瞭解相關概念
瀏覽器支援
特徵偵測
如要確認系統是否支援 File System Access API,請檢查挑選器方法 感興趣的主題
if ('showOpenFilePicker' in self) {
// The `showOpenFilePicker()` method of the File System Access API is supported.
}
立即試用
如要查看 File System Access API 的實際運作情形,請參閱 文字編輯器示範。
讀取本機檔案系統中的檔案
第一個用途是請使用者選擇檔案,然後開啟並閱讀 或是從磁碟擷取檔案
請使用者選取要讀取的檔案
File System Access API 的進入點是
window.showOpenFilePicker()
。呼叫這個方法時,系統會顯示檔案選擇器對話方塊
並提示使用者選取檔案選取檔案後,API 會傳回檔案陣列
控制代碼選用的 options
參數可影響檔案選擇器的行為,
例如允許使用者選取多個檔案、目錄或不同類型的檔案。
如未指定任何選項,檔案選擇器可讓使用者選取單一檔案。這是
非常適合文字編輯器
與許多其他強大的 API 一樣,呼叫 showOpenFilePicker()
時必須在安全機制中
情境),且必須在使用者手勢內呼叫。
let fileHandle;
butOpenFile.addEventListener('click', async () => {
// Destructure the one-element array.
[fileHandle] = await window.showOpenFilePicker();
// Do something with the file handle.
});
使用者選取檔案後,showOpenFilePicker()
會傳回控制代碼陣列。在本例中,
一個元素陣列,包含一個 FileSystemFileHandle
,其中包含
互動所需的方法。
建議您保留檔案控制代碼的參照,以便日後使用。將會 以便儲存檔案變更,或執行其他檔案作業。
從檔案系統讀取檔案
您現已擁有檔案的控制代碼,現在可以取得檔案屬性或存取檔案本身。
我會先唸出內容的內容呼叫 handle.getFile()
會傳回 File
物件,其中含有一個 blob如要取得 blob 中的資料,可以呼叫 blob 的
方法、(slice()
、
stream()
,
text()
,或
arrayBuffer()
。
const file = await fileHandle.getFile();
const contents = await file.text();
只有 FileSystemFileHandle.getFile()
傳回的 File
物件能夠讀取,
磁碟上的基礎檔案並未變更如果修改磁碟中的檔案,File
物件就會變為
無法讀取,您需要再次呼叫 getFile()
以取得新的 File
物件來讀取變更的內容
資料。
全面整合使用
當使用者按一下「Open」按鈕時,瀏覽器會顯示檔案選擇器。選取檔案後,
應用程式會讀取內容,並將其放入 <textarea>
中。
let fileHandle;
butOpenFile.addEventListener('click', async () => {
[fileHandle] = await window.showOpenFilePicker();
const file = await fileHandle.getFile();
const contents = await file.text();
textArea.value = contents;
});
將檔案寫入本機檔案系統
在文字編輯器中,您可以透過兩種方式儲存檔案:「儲存」和「另存新檔」。儲存 使用先前擷取的檔案控制代碼,將變更寫回原始檔案。但儲存 As 會建立新的檔案,因此需要新的檔案控制代碼。
建立新檔案
如要儲存檔案,請呼叫 showSaveFilePicker()
,畫面上會顯示檔案選擇器
「儲存」中模式,可讓使用者選擇要用來儲存的新檔案。關於文字
編輯器,我也希望這項工具能自動新增 .txt
擴充功能,所以另外還要加入一些
參數。
async function getNewFileHandle() {
const options = {
types: [
{
description: 'Text Files',
accept: {
'text/plain': ['.txt'],
},
},
],
};
const handle = await window.showSaveFilePicker(options);
return handle;
}
將變更儲存到磁碟
您可以在我的文字編輯器示範中,找到所有用於儲存檔案變更的程式碼:
GitHub檔案系統的核心互動資源
fs-helpers.js
。最簡單的程序如下所示。
我會逐步說明每個步驟,並說明各個步驟。
// 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();
}
將資料寫入磁碟會使用一個子類別 FileSystemWritableFileStream
物件
(共 WritableStream
個)。在檔案中呼叫 createWritable()
以建立串流
處理常式物件。呼叫 createWritable()
時,瀏覽器會先檢查使用者是否已授予
寫入檔案的權限。如果未授予寫入權限,瀏覽器會提示
要求授予權限的使用者如果沒有授予權限,createWritable()
會擲回
DOMException
,應用程式將無法寫入檔案。在文字編輯器中
DOMException
物件是在 saveFile()
方法中處理。
write()
方法採用字串,這是文字編輯器的必要操作。但也要
BufferSource 或 Blob。例如,您可以讓串流直接管道
它:
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.
}
你也可以在直播中 seek()
或 truncate()
更新
或是調整檔案大小
指定建議的檔案名稱和 start 目錄
在多數情況下,你可能會希望應用程式建議預設的檔案名稱或位置。例如
編輯器可能會建議將預設檔案名稱設為 Untitled Text.txt
,而非 Untitled
。個人中心
只要在 showSaveFilePicker
選項中傳遞 suggestedName
屬性即可。
const fileHandle = await self.showSaveFilePicker({
suggestedName: 'Untitled Text.txt',
types: [{
description: 'Text documents',
accept: {
'text/plain': ['.txt'],
},
}],
});
預設 start 目錄也是如此。建構文字編輯器時,建議您
在預設的 documents
資料夾中啟動「儲存檔案」或「開啟檔案」對話方塊,圖片則會啟動
編輯器,最好在預設的 pictures
資料夾中。您可以建議預設開始
將 startIn
屬性傳遞至 showSaveFilePicker
、showDirectoryPicker()
,或
showOpenFilePicker
方法,例如:
const fileHandle = await self.showOpenFilePicker({
startIn: 'pictures'
});
以下為已知的系統目錄:
desktop
:使用者的桌面目錄 (如果有的話)。documents
:通常儲存使用者建立文件的目錄。downloads
:通常儲存下載檔案的目錄。music
:通常用來儲存音訊檔案的目錄。pictures
:相片和其他靜態圖片的儲存目錄。videos
:通常儲存影片或電影的目錄。
除了知名的系統目錄之外,您也可以將現有的檔案或目錄控制代碼傳遞給
設為 startIn
的值。對話方塊會在同一個目錄中開啟。
// Assume `directoryHandle` is a handle to a previously opened directory.
const fileHandle = await self.showOpenFilePicker({
startIn: directoryHandle
});
指定不同檔案選擇器的用途
有時候,應用程式會有不同的挑選器,用於不同的用途。例如 RTF 格式
編輯者可以允許使用者開啟文字檔案,但也能匯入圖片。根據預設,每個檔案
挑選器會在最後記住的位置開啟。如要規避這項設定,請儲存 id
值
每種挑選器類型如果指定 id
,檔案選擇器的實作方式會記住
單獨的「id
」目錄。
const fileHandle1 = await self.showSaveFilePicker({
id: 'openText',
});
const fileHandle2 = await self.showSaveFilePicker({
id: 'importImage',
});
在 IndexedDB 中儲存檔案控制代碼或目錄控點
檔案控制代碼和目錄控制代碼可序列化,也就是說,您可以儲存檔案
向 IndexedDB 傳送目錄控制代碼,或是呼叫 postMessage()
,在相同的頂層
來源。
如果將檔案或目錄控制點儲存至 IndexedDB,就能儲存狀態或記住 使用者先前處理的檔案或目錄如此一來,您就可以保留最近開啟的地點清單 編輯檔案,建議在應用程式開啟時重新開啟上次開啟的檔案,以及還原先前編輯過的檔案 目錄等我在文字編輯器中儲存了使用者最近使用的五個檔案 開啟檔案,方便你再次存取這些檔案。
以下程式碼範例顯示如何儲存及擷取檔案控制代碼和目錄控制代碼。你可以 查看 Glitch 上的實際運作方式。(我使用的是 為求簡潔,idb-keyval 程式庫)。
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);
}
});
儲存的檔案或目錄控制代碼和權限
由於權限不一定會在工作階段之間保留,因此您應確認使用者是否
已使用 queryPermission()
授予檔案或目錄的權限。如果對方還沒有,請致電:
requestPermission()
即可 (重新) 要求。這也適用於檔案和目錄控制代碼。個人中心
,則需要執行 fileOrDirectoryHandle.requestPermission(descriptor)
或
fileOrDirectoryHandle.queryPermission(descriptor)
。
我在文字編輯器中建立了 verifyPermission()
方法,用來檢查使用者是否已訂閱
並視情況發出要求
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;
}
透過要求讀取權限要求寫入權限,我減少了權限提示的數量。 使用者開啟檔案時會看到一則提示,並授予讀取和寫入檔案的權限。
開啟目錄並列舉其中的內容
如要列舉目錄中的所有檔案,請呼叫 showDirectoryPicker()
。該使用者
會從挑選器中選取目錄,然後 FileSystemDirectoryHandle
顯示為
,可讓您列舉及存取目錄的檔案。預設情況下
能存取目錄中的檔案,但如果需要寫入權限,只要
{ mode: 'readwrite' }
加入方法。
butDir.addEventListener('click', async () => {
const dirHandle = await window.showDirectoryPicker();
for await (const entry of dirHandle.values()) {
console.log(entry.kind, entry.name);
}
});
舉例來說,如果您還需要使用 getFile()
存取每個檔案,例如取得
檔案大小,請勿依序使用 await
,而是處理
例如使用 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));
});
在目錄中建立或存取檔案和資料夾
您可以在目錄中建立或存取檔案和資料夾
getFileHandle()
或個別 getDirectoryHandle()
方法。透過傳入選用的 options
物件,該物件的索引鍵為 create
和布林值
true
或 false
,可以判斷是否要建立沒有的新檔案或資料夾。
// 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 });
解析目錄中項目的路徑
解析目錄中的檔案或資料夾時,可能有助於解析項目的路徑
一些問題。方法是使用適當命名的 resolve()
方法。解決方式時,
項目可以是目錄的直接或間接子項。
// 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"]
刪除目錄中的檔案和資料夾
如果您已經取得目錄存取權,便可刪除其中包含
removeEntry()
方法,增加圍繞地圖邊緣的邊框間距。針對資料夾,您可以選擇以遞迴方式刪除資料,包括
所有子資料夾和其中包含的檔案
// Delete a file.
await directoryHandle.removeEntry('Abandoned Projects.txt');
// Recursively delete a folder.
await directoryHandle.removeEntry('Old Stuff', { recursive: true });
直接刪除檔案或資料夾
如果您可以存取檔案或目錄控制代碼,請在 FileSystemFileHandle
上呼叫 remove()
,或
FileSystemDirectoryHandle
將其移除。
// Delete a file.
await fileHandle.remove();
// Delete a directory.
await directoryHandle.remove();
重新命名及移動檔案和資料夾
如要將檔案和資料夾重新命名或移至新位置,請在該位置呼叫 move()
FileSystemHandle
介面。FileSystemHandle
具有子項介面 FileSystemFileHandle
和
FileSystemDirectoryHandle
。move()
方法使用一或兩個參數。第一種可以
可以是包含新名稱的字串,或目的地資料夾的 FileSystemDirectoryHandle
。在
後者,選用的第二個參數是有新名稱的字串,因此移動和重新命名後
一次就能完成
// 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');
拖曳整合
HTML 拖曳介面
讓網頁應用程式接受
拖曳和捨棄的檔案
網頁在拖曳作業期間,拖曳的檔案和目錄項目會建立關聯
以及檔案項目和目錄項目DataTransferItem.getAsFileSystemHandle()
如果拖曳的項目是檔案,此方法會傳回包含 FileSystemFileHandle
物件的保證,且
如果拖曳的項目是目錄,則會傳回 FileSystemDirectoryHandle
物件。下列商家資訊
示範實際操作方式請注意,在「拖曳」介面中
DataTransferItem.kind
是
適用於「和」目錄的 "file"
,File System Access API 的 FileSystemHandle.kind
則是
檔案為 "file"
,目錄為 "directory"
。
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}`);
}
}
});
存取來源私人檔案系統
來源私人檔案系統是儲存空間端點,顧名思義,僅供
網頁來源。雖然瀏覽器通常會保留
將私人檔案系統來源轉移至磁碟,但「並非」以使用者的身分存取內容
方便存取同樣地,系統不會預期名稱相符的檔案或目錄
存在來源私人檔案系統的子項名稱。雖然瀏覽器可能會
由於是來源私人檔案系統,因此內部有檔案,因此瀏覽器可能會儲存
這些「檔案」資料庫或任何其他資料結構基本上,如果使用這個 API
「不會」預期在硬碟上尋找重複的檔案。您可以在
存取根私人檔案系統後,您就會取得根私人檔案系統的存取權 FileSystemDirectoryHandle
。
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 });
從來源私人檔案系統存取經過最佳化效能的檔案
來源私人檔案系統能讓使用者選擇是否存取具有高度需求的特殊檔案
以提升效能,例如,為應用程式的
內容。在 Chromium 102 以上版本中,還有另一個方法是
簡化檔案存取方式:createSyncAccessHandle()
(用於同步讀寫作業)。
這個檔案會顯示在 FileSystemFileHandle
上,但只會在
網路工作處理序。
// (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 });
聚合
您無法完全填滿 File System Access API 方法。
showOpenFilePicker()
方法可使用<input type="file">
元素進行近似值。- 您可使用
<a download="file_name">
元素模擬showSaveFilePicker()
方法。 儘管這會觸發程式輔助下載,並不允許覆寫現有檔案。 showDirectoryPicker()
方法可以稍微模擬非標準<input type="file" webkitdirectory>
元素。
我們開發了一個名為 browser-fs-access 的程式庫,該程式庫會使用 System Access API 可盡量沿用其他最佳選項 用途
安全性和權限
Chrome 團隊根據核心原則設計並實作 File System Access API 定義,包括控管強大的 Web Platform 功能存取權,包括使用者 資訊公開、管理、資訊公開,以及使用者人體工學
開啟檔案或儲存新檔案
開啟檔案時,使用者會提供使用檔案選擇器讀取檔案或目錄的權限。
必須透過使用者手勢,才能透過安全連線功能顯示開啟的檔案選擇器
背景資訊。如果使用者改變心意,可以取消檔案中的選取項目
挑選器,而網站無法存取任何項目。這和
<input type="file">
元素。
同樣地,當網頁應用程式要儲存新檔案時,瀏覽器會顯示檔案選擇器。 讓使用者指定新檔案的名稱和位置。使用者儲存新檔案 檔案存取目標裝置 (而不是覆寫現有檔案),檔案選擇器會將以下權限授予應用程式: 寫入檔案。
受限制的資料夾
為了保護使用者及其資料,瀏覽器可能會限制使用者將資料儲存功能 資料夾,例如 Windows 等核心作業系統資料夾、macOS 程式庫資料夾。 此時,瀏覽器會顯示提示,並要求使用者選擇其他選項 資料夾。
修改現有的檔案或目錄
網頁應用程式必須取得使用者明確授權,才能修改磁碟中的檔案。
權限提示
如果使用者想儲存先前授予檔案讀取的權限,瀏覽器 會顯示權限提示,要求網站寫入磁碟變更。 只有使用者手勢 (例如按一下「儲存」) 才能觸發權限要求 按鈕。
或者,編輯多個檔案的網頁應用程式 (例如 IDE) 也可以要求儲存權限 進行變更。
如果使用者選擇「取消」,但未授予寫入權限,網頁應用程式就無法將變更儲存到 本機檔案。應用程式應提供另一種方式,讓使用者儲存資料, 開始「下載」檔案或將資料儲存到雲端。
透明度
使用者授權網頁應用程式儲存本機檔案後,瀏覽器會顯示圖示 。按一下圖示即可開啟彈出式視窗,顯示使用者授予的檔案清單 資源存取權使用者隨時可以撤銷存取權。
權限持續性
網頁應用程式可以繼續儲存檔案變更,不提示其所有分頁 已打烊分頁關閉後,網站就會失去所有存取權。當使用者下次使用 系統就會再次提示使用者存取檔案
意見回饋
歡迎與我們分享您使用 File System Access API 的體驗。
請與我們分享 API 設計
您覺得這個 API 有什麼不如預期的運作方式?或者缺少某些方法 需要實現什麼構想?對安全性有任何疑問或意見 以及模型
- 前往 WICG File System Access GitHub 存放區提交規格問題,或是新增你的想法 現有問題。
無法導入嗎?
您發現 Chrome 實作錯誤嗎?還是採用與規格不同?
- 前往 https://new.crbug.com 回報錯誤。請務必盡量提供詳細資料
重現操作說明,並將「Components」(元件) 設為
Blink>Storage>FileSystem
。 Glitch 為快速分享成果的絕佳方法,
想要使用這個 API 嗎?
想要在網站上使用 File System Access API 嗎?你的公開支援服務有助於我們優先處理 功能,並向其他瀏覽器廠商說明支援這些功能的重要性。
- 歡迎前往 WICG Discourse 討論串,說明這項工具的運用方式。
- 使用主題標記將推文傳送至 @ChromiumDev
#FileSystemAccess
和 請告訴我們你使用應用程式的位置和方式。
實用連結
- 公開說明
- 檔案系統存取權規格和檔案規格
- 追蹤錯誤
- ChromeStatus.com 項目
- TypeScript 定義
- File System Access API - Chromium 安全性模型
- 閃爍元件:
Blink>Storage>FileSystem
特別銘謝
File System Access API 規格的編寫者為 Marijn Kruisselbrink。