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 : }
|