All files / src/fsa-to-crud FsaCrud.ts

94.33% Statements 100/106
100% Branches 20/20
90.9% Functions 10/11
93.25% Lines 83/89

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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172    4x 4x 4x   4x   156x             299x 299x 299x 299x 385x 345x 345x   259x   40x                 69x 49x 49x 27x   22x         156x           190x 186x 182x   182x   8x 8x 4x   8x 4x   4x     8x 8x   4x     4x     166x     174x 174x 174x     156x 35x 31x 27x 14x 14x 14x     156x 32x 28x 24x 24x 6x   18x       156x 42x 38x 22x 18x 7x 7x             16x 12x             156x 20x 16x 16x 8x 4x   4x 28x     8x       156x     24x 16x 56x 24x 16x       8x 8x               156x 12x 36x 4x     156x            
import type * as crud from '../crud/types';
import type * as fsa from '../fsa/types';
import { assertName } from '../node-to-fsa/util';
import { assertType } from '../crud/util';
import { newExistsError, newFile404Error, newFolder404Error, newMissingError } from './util';
 
export class FsaCrud implements crud.CrudApi {
  public constructor(
    protected readonly root: fsa.IFileSystemDirectoryHandle | Promise<fsa.IFileSystemDirectoryHandle>,
  ) {}
 
  protected async getDir(
    collection: crud.CrudCollection,
    create: boolean,
  ): Promise<[dir: fsa.IFileSystemDirectoryHandle, parent: fsa.IFileSystemDirectoryHandle | undefined]> {
    let parent: undefined | fsa.IFileSystemDirectoryHandle = undefined;
    let dir = await this.root;
    try {
      for (const name of collection) {
        const child = await dir.getDirectoryHandle(name, { create });
        parent = dir;
        dir = child;
      }
      return [dir, parent];
    } catch (error) {
      if (error.name === 'NotFoundError') throw newFolder404Error(collection);
      throw error;
    }
  }
 
  protected async getFile(
    collection: crud.CrudCollection,
    id: string,
  ): Promise<[dir: fsa.IFileSystemDirectoryHandle, file: fsa.IFileSystemFileHandle]> {
    const [dir] = await this.getDir(collection, false);
    try {
      const file = await dir.getFileHandle(id, { create: false });
      return [dir, file];
    } catch (error) {
      if (error.name === 'NotFoundError') throw newFile404Error(collection, id);
      throw error;
    }
  }
 
  public readonly put = async (
    collection: crud.CrudCollection,
    id: string,
    data: Uint8Array,
    options?: crud.CrudPutOptions,
  ): Promise<void> => {
    assertType(collection, 'put', 'crudfs');
    assertName(id, 'put', 'crudfs');
    const [dir] = await this.getDir(collection, true);
    let file: fsa.IFileSystemFileHandle | undefined;
    switch (options?.throwIf) {
      case 'exists': {
        try {
          file = await dir.getFileHandle(id, { create: false });
          throw newExistsError();
        } catch (e) {
          if (e.name !== 'NotFoundError') throw e;
          file = await dir.getFileHandle(id, { create: true });
        }
        break;
      }
      case 'missing': {
        try {
          file = await dir.getFileHandle(id, { create: false });
        } catch (e) {
          if (e.name === 'NotFoundError') throw newMissingError();
          throw e;
        }
        break;
      }
      default: {
        file = await dir.getFileHandle(id, { create: true });
      }
    }
    const writable = await file!.createWritable();
    await writable.write(data);
    await writable.close();
  };
 
  public readonly get = async (collection: crud.CrudCollection, id: string): Promise<Uint8Array> => {
    assertType(collection, 'get', 'crudfs');
    assertName(id, 'get', 'crudfs');
    const [, file] = await this.getFile(collection, id);
    const blob = await file.getFile();
    const buffer = await blob.arrayBuffer();
    return new Uint8Array(buffer);
  };
 
  public readonly del = async (collection: crud.CrudCollection, id: string, silent?: boolean): Promise<void> => {
    assertType(collection, 'del', 'crudfs');
    assertName(id, 'del', 'crudfs');
    try {
      const [dir] = await this.getFile(collection, id);
      await dir.removeEntry(id, { recursive: false });
    } catch (error) {
      if (!silent) throw error;
    }
  };
 
  public readonly info = async (collection: crud.CrudCollection, id?: string): Promise<crud.CrudResourceInfo> => {
    assertType(collection, 'info', 'crudfs');
    if (id) {
      assertName(id, 'info', 'crudfs');
      const [, file] = await this.getFile(collection, id);
      const blob = await file.getFile();
      return {
        type: 'resource',
        id,
        size: blob.size,
        modified: blob.lastModified,
      };
    } else {
      await this.getDir(collection, false);
      return {
        type: 'collection',
        id: '',
      };
    }
  };
 
  public readonly drop = async (collection: crud.CrudCollection, silent?: boolean): Promise<void> => {
    assertType(collection, 'drop', 'crudfs');
    try {
      const [dir, parent] = await this.getDir(collection, false);
      if (parent) {
        await parent.removeEntry(dir.name, { recursive: true });
      } else {
        const root = await this.root;
        for await (const name of root.keys()) await root.removeEntry(name, { recursive: true });
      }
    } catch (error) {
      if (!silent) throw error;
    }
  };
 
  public readonly scan = async function* (
    collection: crud.CrudCollection,
  ): AsyncIterableIterator<crud.CrudCollectionEntry> {
    assertType(collection, 'scan', 'crudfs');
    const [dir] = await this.getDir(collection, false);
    for await (const [id, handle] of dir.entries()) {
      if (handle.kind === 'file') {
        yield {
          type: 'resource',
          id,
        };
      } else if (handle.kind === 'directory') {
        yield {
          type: 'collection',
          id,
        };
      }
    }
  };
 
  public readonly list = async (collection: crud.CrudCollection): Promise<crud.CrudCollectionEntry[]> => {
    const entries: crud.CrudCollectionEntry[] = [];
    for await (const entry of this.scan(collection)) entries.push(entry);
    return entries;
  };
 
  public readonly from = async (collection: crud.CrudCollection): Promise<crud.CrudApi> => {
    assertType(collection, 'from', 'crudfs');
    const [dir] = await this.getDir(collection, true);
    return new FsaCrud(dir);
  };
}