Web API 提供的服務涵蓋很廣,File System API 已經推出一段時間,主要功能是讓瀏覽器能經過使用者授權後,與使用者本機的檔案系統做互動,這篇筆記就是將一些如何使用 File system API 記錄下來
本筆記將使用 Angular v19-next 作為練習環境
Typescript Types
首先,因為 typescript 還不認得 File System API. 所以必須要手動安裝設定對應的 types
1
| npm i -D @types/wicg-file-system-access
|
接下來在 tsconfig.json
內增修以下
1 2 3 4 5 6
| "compilerOptions": { ... "types": [ "@types/wicg-file-system-access" ] }
|
Directory
開啟 DirectoryPicker
1 2 3 4
| async broweFolders() { const dirHandler = await (<any>window).showDirectoryPicker(); console.log(dirHandler); }
|
1
| <button (click)="broweFolders()">Browe</button>
|
當按下按鈕時,會跳出挑選資料夾的 Dialog,選擇完要開啟的資料夾後,Console log 的地方應該會看到很簡單的資訊
-
回傳的型別為 FileSystemDirectoryHandle
-
顯示資料夾名稱
-
showDirectoryPicker
支援傳入參數 in Object
startIn
desktop
:使用者的桌面目錄 (如果有的話)。
documents
:通常儲存使用者建立文件的目錄。
downloads
:通常儲存下載檔案的目錄。
music
:通常用來儲存音訊檔案的目錄。
pictures
:相片和其他靜態圖片的儲存目錄。
videos
:通常儲存影片或電影的目錄。
id
: 指定不同檔案選擇器的用途識別用。為什麼會有指定 id 的情境,因為根據預設,每個檔案挑選器會在最後記住的位置開啟,為了避免此情形,就可以透過設定 id 的方式來區分
當有了這一個 directoryHandler
, 就可以做一些有趣的事情
列出資料夾下的檔案及資料夾
1 2 3 4 5 6 7
| async listFolderItems(entry: FileSystemDirectoryHandle) { const items = []; for await (const handle of entry.values()) { items.push(handle); } return items; }
|
因為 directoryHandler
的 values 回傳的是 AsyncIterableIterator
,可搭配 for await 的新語法取得所有的值,如果不知道 Iterator & Generator 的朋友,可以回去看一下相關的文件。
1 2 3 4 5 6
| async broweFolders() { const dirHandler = await (<any>window).showDirectoryPicker(); console.log(dirHandler); this.items = await this.listFolderItems(dirHandler); console.log(this.items); }
|
1 2 3 4 5 6 7
| <button (click)="broweFolders()">Browe</button> <hr /> <ul> @for (item of items; track item) { <li>{{ item.kind }} - {{ item.name }}</li> } </ul>
|
這樣輸出的結果如下
到這邊應該還算單純,當然如果想要取得所有檔案(包含子資料夾下),就會動到遞迴的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| getAllFilesFromDirectory(dirHandler: FileSystemDirectoryHandle){ for await (const fileHandle of this.getFilesRecursively(dirHandler)) { console.log(fileHandle); } }
async *getFilesRecursively(entry: FileSystemHandle): AsyncGenerator<any> { if (entry.kind === 'file') { const file = await (<FileSystemFileHandle>entry).getFile(); if (file !== null) { yield file; } } else if (entry.kind === 'directory') { for await (const handle of (<FileSystemDirectoryHandle>entry).values()) { yield* this.getFilesRecursively(handle); } } }
|
建立資料夾
1 2 3 4 5 6 7 8
| async createFolder() { if (this.dirHandler === undefined) return; const subHandler = await this.dirHandler.getDirectoryHandle( `Folder_${Math.floor(Math.random() * 10)}`, { create: true }, ); console.log(subHandler); }
|
透過 getDirectoryHandle
+ {create: true}
就可以在所選取的 root directory 下建立資料夾,如果遇到資料夾名稱一樣的,基本上就會回傳已存在的 directory,所以我們可以這樣理解,當要取得某個 `DirectoryHandle時,如果不存在就建立一個新的。
刪除資料夾
1 2 3
| await directoryHandle.removeEntry('Old Stuff', { recursive: true });
await directoryHandle.remove();
|
File
取得檔案相對路徑
1 2 3 4 5 6
| async getRelativePath(entry: FileSystemFileHandle) { if (this.dirHandler === undefined) return ''; const relativePaths = await this.dirHandler.resolve(entry); console.log(relativePaths); return relativePaths?.join('/'); }
|
刪除檔案
1 2 3
| await directoryHandle.removeEntry('Abandoned Projects.txt');
await fileHandle.remove();
|
讀取檔案
1 2 3 4 5 6 7
| async read(handler: FileSystemHandle) { if (handler.kind === 'file') { const fileHandler = handler as FileSystemFileHandle; const content = await fileHandler.getFile().then((file) => file.text()); console.log(content); } }
|
建立檔案
1 2 3 4 5 6 7 8
| async save() { if (this.dirHandler === undefined || this.fileContent.length === 0) return; const fileName = `notes_${new Date().toDateString()}`; const fileHandler = await this.dirHandler.getFileHandle(fileName, { create: true, }); ... }
|
回寫檔案
1 2 3 4 5 6 7 8 9 10 11 12
| async save() { if (this.dirHandler === undefined || this.fileContent.length === 0) return; const fileName = `notes_${new Date().toDateString()}`; const fileHandler = await this.dirHandler.getFileHandle(fileName, { create: true, }); const writeable = await fileHandler.createWritable(); await writeable.write(this.fileContent); await writeable.close(); this.fileContent = ''; }
|
Reference