[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
---
|
||||
id: web-file-system-access
|
||||
title: File System Access API — browser 가 file 직접
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [web, file-system, vibe-coding]
|
||||
tech_stack: { language: "TS", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [File System Access API, showOpenFilePicker, showSaveFilePicker, FileSystemHandle, OPFS]
|
||||
---
|
||||
|
||||
# File System Access API
|
||||
|
||||
> Browser 가 user 의 file 을 read/write. **`showOpenFilePicker` / `showSaveFilePicker`**. Native app 비슷한 UX. Chrome / Edge (Safari / FF 부분).
|
||||
|
||||
## 📖 핵심 개념
|
||||
- File handle: 영구 reference (permission 유지).
|
||||
- OPFS: Origin Private File System (sandboxed).
|
||||
- 사용자 명시 grant.
|
||||
- Streaming 가능.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### File 열기
|
||||
```ts
|
||||
const [handle] = await window.showOpenFilePicker({
|
||||
types: [{ description: 'Text', accept: { 'text/plain': ['.txt'] } }],
|
||||
});
|
||||
const file = await handle.getFile();
|
||||
const text = await file.text();
|
||||
```
|
||||
|
||||
### File 저장
|
||||
```ts
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: 'export.json',
|
||||
types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
|
||||
});
|
||||
|
||||
const writable = await handle.createWritable();
|
||||
await writable.write(JSON.stringify(data));
|
||||
await writable.close();
|
||||
```
|
||||
|
||||
→ User 가 location / name 선택.
|
||||
|
||||
### Directory 열기
|
||||
```ts
|
||||
const dirHandle = await window.showDirectoryPicker();
|
||||
|
||||
for await (const [name, handle] of dirHandle.entries()) {
|
||||
if (handle.kind === 'file') {
|
||||
const file = await handle.getFile();
|
||||
console.log(name, file.size);
|
||||
} else {
|
||||
// recursive
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### File 변경 모니터링 (poll)
|
||||
```ts
|
||||
let lastModified = 0;
|
||||
setInterval(async () => {
|
||||
const file = await handle.getFile();
|
||||
if (file.lastModified !== lastModified) {
|
||||
lastModified = file.lastModified;
|
||||
const newContent = await file.text();
|
||||
onChange(newContent);
|
||||
}
|
||||
}, 1000);
|
||||
```
|
||||
|
||||
→ True file watch X — poll.
|
||||
|
||||
### Persistent handle (IndexedDB 저장)
|
||||
```ts
|
||||
// Save
|
||||
import { openDB } from 'idb';
|
||||
const db = await openDB('fileHandles', 1);
|
||||
await db.put('handles', handle, 'lastFile');
|
||||
|
||||
// Restore (next session)
|
||||
const handle = await db.get('handles', 'lastFile');
|
||||
const perm = await handle.queryPermission({ mode: 'readwrite' });
|
||||
if (perm !== 'granted') {
|
||||
await handle.requestPermission({ mode: 'readwrite' });
|
||||
}
|
||||
```
|
||||
|
||||
→ User 가 같은 file 다시 open 안 함.
|
||||
|
||||
### OPFS (Origin Private File System)
|
||||
```ts
|
||||
const opfs = await navigator.storage.getDirectory();
|
||||
|
||||
// Write
|
||||
const fileHandle = await opfs.getFileHandle('app.db', { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(new Uint8Array([...]));
|
||||
await writable.close();
|
||||
|
||||
// Read
|
||||
const file = await fileHandle.getFile();
|
||||
const buf = await file.arrayBuffer();
|
||||
```
|
||||
|
||||
→ Sandboxed (사용자 X). 큰 storage. SQLite WASM 가 사용.
|
||||
|
||||
### Sync access (worker only)
|
||||
```ts
|
||||
// In Web Worker
|
||||
const opfs = await navigator.storage.getDirectory();
|
||||
const handle = await opfs.getFileHandle('db.sqlite');
|
||||
const sync = await handle.createSyncAccessHandle();
|
||||
|
||||
// Sync read / write
|
||||
const buf = new Uint8Array(1024);
|
||||
sync.read(buf, { at: 0 });
|
||||
sync.write(new Uint8Array([1,2,3]), { at: 0 });
|
||||
sync.flush();
|
||||
sync.close();
|
||||
```
|
||||
|
||||
→ Async overhead 없음. SQLite 식.
|
||||
|
||||
### Streaming write (큰 file)
|
||||
```ts
|
||||
const writable = await handle.createWritable();
|
||||
const ws = writable.getWriter();
|
||||
|
||||
for (const chunk of bigData) {
|
||||
await ws.ready;
|
||||
await ws.write(chunk);
|
||||
}
|
||||
await ws.close();
|
||||
```
|
||||
|
||||
→ Memory efficient.
|
||||
|
||||
### File picker option
|
||||
```ts
|
||||
await window.showOpenFilePicker({
|
||||
multiple: true,
|
||||
types: [
|
||||
{ description: 'Images', accept: { 'image/*': ['.png', '.jpg'] } },
|
||||
{ description: 'PDF', accept: { 'application/pdf': ['.pdf'] } },
|
||||
],
|
||||
excludeAcceptAllOption: true,
|
||||
startIn: 'documents', // 'desktop' / 'downloads' / 'pictures' / 'music' / 'videos'
|
||||
});
|
||||
```
|
||||
|
||||
### Permission
|
||||
```ts
|
||||
async function ensureWrite(handle) {
|
||||
const opts = { mode: 'readwrite' };
|
||||
|
||||
if ((await handle.queryPermission(opts)) === 'granted') return true;
|
||||
if ((await handle.requestPermission(opts)) === 'granted') return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
→ User 가 매번 prompt. Persistent 가능.
|
||||
|
||||
### Drag & drop + handle
|
||||
```ts
|
||||
window.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
for (const item of e.dataTransfer!.items) {
|
||||
const handle = await item.getAsFileSystemHandle();
|
||||
if (handle.kind === 'file') {
|
||||
const file = await handle.getFile();
|
||||
} else {
|
||||
// directory
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Editor 가 file save
|
||||
```tsx
|
||||
function Editor() {
|
||||
const [handle, setHandle] = useState<FileSystemFileHandle>();
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const open = async () => {
|
||||
const [h] = await window.showOpenFilePicker();
|
||||
setHandle(h);
|
||||
setContent(await (await h.getFile()).text());
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
let h = handle;
|
||||
if (!h) h = await window.showSaveFilePicker();
|
||||
const w = await h.createWritable();
|
||||
await w.write(content);
|
||||
await w.close();
|
||||
setHandle(h);
|
||||
};
|
||||
|
||||
// Cmd+S
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.metaKey && e.key === 's') { e.preventDefault(); save(); }
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [save]);
|
||||
|
||||
return <textarea value={content} onChange={(e) => setContent(e.target.value)} />;
|
||||
}
|
||||
```
|
||||
|
||||
### SQLite WASM + OPFS
|
||||
```ts
|
||||
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
const sqlite3 = await sqlite3InitModule();
|
||||
|
||||
const db = new sqlite3.oo1.OpfsDb('app.db');
|
||||
db.exec('CREATE TABLE IF NOT EXISTS users (id INT, name TEXT)');
|
||||
db.exec('INSERT INTO users VALUES (1, "Alice")');
|
||||
const r = db.exec('SELECT * FROM users', { rowMode: 'object' });
|
||||
```
|
||||
|
||||
→ Local DB. Browser 만 (no server).
|
||||
|
||||
### 큰 file (Photo editor)
|
||||
```ts
|
||||
const [handle] = await window.showOpenFilePicker({
|
||||
types: [{ accept: { 'image/*': ['.png', '.jpg'] } }],
|
||||
});
|
||||
const file = await handle.getFile(); // 100MB
|
||||
const arr = await file.arrayBuffer(); // memory load — careful
|
||||
```
|
||||
|
||||
→ 큰 file 가 stream:
|
||||
```ts
|
||||
const stream = file.stream();
|
||||
// process chunks
|
||||
```
|
||||
|
||||
### Browser support
|
||||
```
|
||||
File System Access API:
|
||||
- Chrome 86+
|
||||
- Edge 86+
|
||||
- Opera 72+
|
||||
- Safari: showSaveFilePicker, showOpenFilePicker (16.4+, partial)
|
||||
- Firefox: 안 (no plan)
|
||||
|
||||
OPFS:
|
||||
- 모든 modern (Safari 16, FF 111, Chrome 102)
|
||||
```
|
||||
|
||||
→ Fallback = `<input type="file">` + `URL.createObjectURL`.
|
||||
|
||||
### Fallback
|
||||
```ts
|
||||
async function openFile(): Promise<File> {
|
||||
if ('showOpenFilePicker' in window) {
|
||||
const [h] = await window.showOpenFilePicker();
|
||||
return h.getFile();
|
||||
}
|
||||
|
||||
// Fallback
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.click();
|
||||
return new Promise((res) => input.onchange = () => res(input.files![0]));
|
||||
}
|
||||
|
||||
async function saveFile(blob: Blob, name: string) {
|
||||
if ('showSaveFilePicker' in window) {
|
||||
const h = await window.showSaveFilePicker({ suggestedName: name });
|
||||
const w = await h.createWritable();
|
||||
await w.write(blob);
|
||||
await w.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = name;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
```
|
||||
|
||||
### Use case
|
||||
- VS Code Web (Microsoft)
|
||||
- Photoshop Web (Adobe)
|
||||
- Figma (web)
|
||||
- Google Docs (limited)
|
||||
- IDE / editor (StackBlitz, CodeSandbox)
|
||||
- DAW / audio editor (BandLab)
|
||||
|
||||
### Security
|
||||
```
|
||||
- HTTPS only
|
||||
- User gesture 필수 (click 안 = call 안 됨)
|
||||
- Domain-scoped (handle 가 다른 site 에서 사용 X)
|
||||
- System dir block (e.g. /System on macOS)
|
||||
```
|
||||
|
||||
### Permission persistence
|
||||
```
|
||||
Chrome: 같은 origin, 같은 file = persisted (refresh OK).
|
||||
Browser close = revoked.
|
||||
|
||||
→ IndexedDB 가 handle 저장 → re-prompt.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 작업 | 추천 |
|
||||
|---|---|
|
||||
| Native-like editor | File System Access API |
|
||||
| 큰 local DB | OPFS + SQLite |
|
||||
| 일반 download | Fallback (input + a.click) |
|
||||
| Photo edit | File handle + stream |
|
||||
| Cross-browser (FF) | Fallback |
|
||||
| Cloud sync | Server upload |
|
||||
| Drag & drop | DataTransferItem |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **User gesture 없이 호출**: error.
|
||||
- **Permission 매번 prompt**: persist.
|
||||
- **큰 file 가 arrayBuffer**: OOM.
|
||||
- **Safari / FF 가정**: fallback 없음.
|
||||
- **OPFS 가 user-visible 가정**: sandboxed.
|
||||
- **Sync access in main thread**: blocked.
|
||||
- **Origin 변경 + handle**: invalid.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- File System Access API 가 Chrome / Edge 만 — fallback.
|
||||
- OPFS = 큰 sandboxed storage (SQLite WASM 친화).
|
||||
- Handle = persistent reference (IndexedDB).
|
||||
- Streaming = 큰 file.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Frontend_Streams_API]]
|
||||
- [[Web_PWA_Service_Worker]]
|
||||
- [[DB_SQLite_Patterns]]
|
||||
Reference in New Issue
Block a user