Files
2nd/10_Wiki/Topics/Coding/Web_File_System_Access.md
T
2026-05-10 22:08:15 +09:00

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
web
file-system
vibe-coding
language applicable_to
TS
Frontend
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 열기

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.

🔗 관련 문서