LCOV - code coverage report
Current view: top level - dashql-app/src/storage/migration - migration_folder.ts Coverage Total Hit
Test: DashQL 915828b Lines: 98.3 % 121 119
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import { exists, join, readTextFile, remove, rename, writeTextFile } from '../../platform/electron_fs.js';
       2              : 
       3              : /// The top-level files of a session folder, as the session migrations see them.
       4              : ///
       5              : /// Session migrations only touch the top-level files of a session folder (the metadata and the
       6              : /// catalog files), never the scripts or the attachments. A folder that must stay untouched is
       7              : /// therefore migrated in memory with an `OverlayFolder`, while its scripts and attachments are read
       8              : /// as they are.
       9              : export interface MigrationFolder {
      10              :     /// Does the folder contain the file?
      11              :     exists(name: string): Promise<boolean>;
      12              :     /// Read a file, null if the folder does not contain it
      13              :     readText(name: string): Promise<string | null>;
      14              :     /// Write a file
      15              :     writeText(name: string, text: string): Promise<void>;
      16              :     /// Rename a file, an existing target is replaced
      17              :     rename(from: string, to: string): Promise<void>;
      18              :     /// Remove a file, a missing file is ignored
      19              :     remove(name: string): Promise<void>;
      20              : }
      21              : 
      22              : /// Rename a file while it exists and the target does not. A folder that has both files keeps both,
      23              : /// the target wins. Returns whether the file was renamed.
      24            1 : export async function renameFileIfPresent(folder: MigrationFolder, from: string, to: string): Promise<boolean> {
      25            1 :     if (!(await folder.exists(from)) || await folder.exists(to)) {
      26            1 :         return false;
      27              :     }
      28            1 :     await folder.rename(from, to);
      29            1 :     return true;
      30            1 : }
      31              : 
      32              : /// A folder in memory, e.g. the top-level files of an import
      33            1 : export class MemoryFolder implements MigrationFolder {
      34              :     /// The files by name
      35            1 :     readonly files: Map<string, string>;
      36              : 
      37            1 :     constructor(files: Iterable<[string, string]> = []) {
      38            1 :         this.files = new Map(files);
      39            1 :     }
      40              : 
      41            1 :     async exists(name: string): Promise<boolean> {
      42            1 :         return this.files.has(name);
      43            1 :     }
      44              : 
      45            1 :     async readText(name: string): Promise<string | null> {
      46            1 :         return this.files.get(name) ?? null;
      47            1 :     }
      48              : 
      49            1 :     async writeText(name: string, text: string): Promise<void> {
      50            1 :         this.files.set(name, text);
      51            1 :     }
      52              : 
      53            1 :     async rename(from: string, to: string): Promise<void> {
      54            1 :         const text = this.files.get(from);
      55            1 :         if (text === undefined) {
      56            0 :             throw new Error(`Cannot rename missing file ${from}`);
      57              :         }
      58            1 :         this.files.delete(from);
      59            1 :         this.files.set(to, text);
      60            1 :     }
      61              : 
      62            1 :     async remove(name: string): Promise<void> {
      63            1 :         this.files.delete(name);
      64            1 :     }
      65            1 : }
      66              : 
      67              : /// A folder whose changes stay in memory, the base folder is only read
      68            1 : export class OverlayFolder implements MigrationFolder {
      69              :     /// The folder that is read
      70            1 :     private readonly base: MigrationFolder;
      71              :     /// The changed files, null for removed ones
      72            1 :     private readonly changes: Map<string, string | null>;
      73              : 
      74            1 :     constructor(base: MigrationFolder) {
      75            1 :         this.base = base;
      76            1 :         this.changes = new Map();
      77            1 :     }
      78              : 
      79            1 :     async exists(name: string): Promise<boolean> {
      80            1 :         const change = this.changes.get(name);
      81            1 :         return change === undefined ? await this.base.exists(name) : change !== null;
      82            1 :     }
      83              : 
      84            1 :     async readText(name: string): Promise<string | null> {
      85            1 :         const change = this.changes.get(name);
      86            1 :         return change === undefined ? await this.base.readText(name) : change;
      87            1 :     }
      88              : 
      89            1 :     async writeText(name: string, text: string): Promise<void> {
      90            1 :         this.changes.set(name, text);
      91            1 :     }
      92              : 
      93            1 :     async rename(from: string, to: string): Promise<void> {
      94            1 :         const text = await this.readText(from);
      95            1 :         if (text === null) {
      96            0 :             throw new Error(`Cannot rename missing file ${from}`);
      97              :         }
      98            1 :         this.changes.set(to, text);
      99            1 :         this.changes.set(from, null);
     100            1 :     }
     101              : 
     102            1 :     async remove(name: string): Promise<void> {
     103            1 :         this.changes.set(name, null);
     104            1 :     }
     105            1 : }
     106              : 
     107              : /// A session folder in the Origin Private File System
     108            1 : export class OPFSFolder implements MigrationFolder {
     109              :     /// The directory of the session
     110            1 :     private readonly dir: FileSystemDirectoryHandle;
     111              : 
     112            1 :     constructor(dir: FileSystemDirectoryHandle) {
     113            1 :         this.dir = dir;
     114            1 :     }
     115              : 
     116            1 :     async exists(name: string): Promise<boolean> {
     117            1 :         try {
     118            1 :             await this.dir.getFileHandle(name, { create: false });
     119            1 :             return true;
     120            1 :         } catch (error) {
     121            1 :             if ((error as DOMException).name === 'NotFoundError' || (error as DOMException).name === 'TypeMismatchError') {
     122            1 :                 return false;
     123              :             }
     124            1 :             throw error;
     125            1 :         }
     126            1 :     }
     127              : 
     128            1 :     async readText(name: string): Promise<string | null> {
     129            1 :         if (!(await this.exists(name))) {
     130            1 :             return null;
     131              :         }
     132            1 :         return await (await (await this.dir.getFileHandle(name)).getFile()).text();
     133            1 :     }
     134              : 
     135            1 :     async writeText(name: string, text: string): Promise<void> {
     136            1 :         const writable = await (await this.dir.getFileHandle(name, { create: true })).createWritable();
     137            1 :         await writable.write(text);
     138            1 :         await writable.close();
     139            1 :     }
     140              : 
     141              :     // OPFS cannot rename files everywhere, the content is copied
     142            1 :     async rename(from: string, to: string): Promise<void> {
     143            1 :         const content = await (await (await this.dir.getFileHandle(from)).getFile()).arrayBuffer();
     144            1 :         const writable = await (await this.dir.getFileHandle(to, { create: true })).createWritable();
     145            1 :         await writable.write(content);
     146            1 :         await writable.close();
     147            1 :         await this.dir.removeEntry(from);
     148            1 :     }
     149              : 
     150            1 :     async remove(name: string): Promise<void> {
     151            1 :         if (await this.exists(name)) {
     152            1 :             await this.dir.removeEntry(name);
     153              :         }
     154            1 :     }
     155            1 : }
     156              : 
     157              : /// A session folder on the native file system
     158            1 : export class NativeFolder implements MigrationFolder {
     159              :     /// The absolute directory of the session
     160            1 :     private readonly dir: string;
     161              : 
     162            1 :     constructor(dir: string) {
     163            1 :         this.dir = dir;
     164            1 :     }
     165              : 
     166            1 :     async exists(name: string): Promise<boolean> {
     167            1 :         return await exists(await join(this.dir, name));
     168            1 :     }
     169              : 
     170            1 :     async readText(name: string): Promise<string | null> {
     171            1 :         const path = await join(this.dir, name);
     172            1 :         return await exists(path) ? await readTextFile(path) : null;
     173            1 :     }
     174              : 
     175            1 :     async writeText(name: string, text: string): Promise<void> {
     176            1 :         await writeTextFile(await join(this.dir, name), text);
     177            1 :     }
     178              : 
     179            1 :     async rename(from: string, to: string): Promise<void> {
     180            1 :         await rename(await join(this.dir, from), await join(this.dir, to));
     181            1 :     }
     182              : 
     183            1 :     async remove(name: string): Promise<void> {
     184            1 :         const path = await join(this.dir, name);
     185            1 :         if (await exists(path)) {
     186            1 :             await remove(path);
     187              :         }
     188            1 :     }
     189            1 : }
        

Generated by: LCOV version 2.0-1