8.6 KiB
8.6 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| web-file-system-access | File System Access API — browser 가 file 직접 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
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 열기
const [handle] = await window.showOpenFilePicker({
types: [{ description: 'Text', accept: { 'text/plain': ['.txt'] } }],
});
const file = await handle.getFile();
const text = await file.text();
File 저장
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 열기
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)
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 저장)
// 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)
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)
// 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)
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
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
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
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
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
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)
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:
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
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.