All files / src/adapters fsa.ts

100% Statements 28/28
91.66% Branches 11/12
100% Functions 5/5
100% Lines 28/28

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 681x                                         1x 4x 4x                   1x   1x 3x 1x 1x 2x 2x 2x 3x     1x 6x 6x     1x 7x 7x 5x 7x           1x 4x 2x 4x 2x 4x 1x  
import { HandleAdapter } from './handle.js';
 
type PermissionMode = 'read' | 'readwrite';
 
interface PermissionCapableHandle {
  queryPermission?: (descriptor: { mode: PermissionMode }) => Promise<PermissionState>;
  requestPermission?: (descriptor: { mode: PermissionMode }) => Promise<PermissionState>;
}
 
interface DirectoryPickerOptions {
  id?: string;
  mode?: PermissionMode;
  startIn?: string | FileSystemHandle;
}
 
declare global {
  interface Window {
    showDirectoryPicker?: (options?: DirectoryPickerOptions) => Promise<FileSystemDirectoryHandle>;
  }
}
 
export function isFSAAvailable(): boolean {
  return typeof window !== 'undefined' && typeof window.showDirectoryPicker === 'function';
}
 
/**
 * File System Access API backend — a real folder on the user's disk.
 *
 * The important difference from OPFS is that permission is revocable and does
 * not survive a page reload: a stored handle has to be re-authorised with a
 * user gesture before it can be read again. Call {@link ensurePermission}
 * before a sync run and surface the `false` case in your UI.
 */
export class FSAAdapter extends HandleAdapter {
  /** Prompts for a folder. Must be called from a user gesture. */
  static async pick(options: DirectoryPickerOptions = {}): Promise<FSAAdapter> {
    if (!isFSAAvailable()) {
      throw new Error('The File System Access API is not available in this browser');
    }
    const picker = window.showDirectoryPicker as NonNullable<Window['showDirectoryPicker']>;
    const handle = await picker({ mode: 'readwrite', ...options });
    return new FSAAdapter(handle);
  }
 
  /** Wraps a handle restored from IndexedDB. */
  static fromHandle(handle: FileSystemDirectoryHandle, name?: string): FSAAdapter {
    return new FSAAdapter(handle, name);
  }
 
  /** True if already granted, without prompting. */
  async hasPermission(mode: PermissionMode = 'readwrite'): Promise<boolean> {
    const handle = this.root as FileSystemDirectoryHandle & PermissionCapableHandle;
    if (!handle.queryPermission) return true;
    return (await handle.queryPermission({ mode })) === 'granted';
  }
 
  /**
   * Re-requests permission if needed. The prompt requires a user gesture, so
   * call this from a click handler rather than from a background timer.
   */
  async ensurePermission(mode: PermissionMode = 'readwrite'): Promise<boolean> {
    if (await this.hasPermission(mode)) return true;
    const handle = this.root as FileSystemDirectoryHandle & PermissionCapableHandle;
    if (!handle.requestPermission) return true;
    return (await handle.requestPermission({ mode })) === 'granted';
  }
}