Repository navigation
Expand file tree
/
Copy pathinitializeV2Database.ts
More file actions
89 lines (83 loc) · 3.21 KB
/
Copy pathinitializeV2Database.ts
File metadata and controls
89 lines (83 loc) · 3.21 KB
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
import * as NodeSqlite from "node:sqlite";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
export class V2DatabaseImportError extends Schema.TaggedError<V2DatabaseImportError>()(
"V2DatabaseImportError",
{ sourcePath: Schema.String, destinationPath: Schema.String, cause: Schema.Defect() },
) {
override get message() {
return `Could not copy the V1 database at ${this.sourcePath} to ${this.destinationPath}. The V1 database has not been migrated.`;
}
}
const isV2DatabaseImportError = Schema.is(V2DatabaseImportError);
const isProcessAlive = (pid: number): boolean => {
if (!Number.isInteger(pid) || pid <= 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (error) {
return error instanceof Error && "code" in error && error.code === "EPERM";
}
};
// Legacy names have no PID; newer copies stay while their process is alive.
const removeAbandonedCopies = Effect.fn("removeAbandonedCopies")(function* (directory: string) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const entries = yield* fs.readDirectory(directory).pipe(
Effect.catchIf(
(error) => error.reason._tag === "NotFound",
() => Effect.succeed([]),
),
);
for (const entry of entries) {
const copy = /^\.v2-import-(?:(\d+)-)?[A-Za-z0-9]{6}$/.exec(entry);
if (copy === null || (copy[1] !== undefined && isProcessAlive(Number(copy[1])))) continue;
yield* fs.remove(path.join(directory, entry), { recursive: true });
}
});
/** Its copied legacy tables remain the source for lazy transcript import. */
export const initializeV2Database = Effect.fn("initializeV2Database")(function* (
destinationPath: string,
) {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const directory = path.dirname(destinationPath);
const sourcePath = path.join(directory, "state.sqlite");
yield* removeAbandonedCopies(directory).pipe(Effect.ignore({ log: true }));
yield* Effect.gen(function* () {
if (yield* fs.exists(destinationPath)) return;
if (!(yield* fs.exists(sourcePath))) return;
const temporaryDirectory = yield* fs.makeTempDirectoryScoped({
directory,
prefix: `.v2-import-${process.pid}-`,
});
const snapshotPath = path.join(temporaryDirectory, "snapshot.sqlite");
yield* Effect.tryPromise({
try: async () => {
const database = new NodeSqlite.DatabaseSync(sourcePath, { readOnly: true });
try {
await NodeSqlite.backup(database, snapshotPath);
} finally {
database.close();
}
},
catch: (cause) => new V2DatabaseImportError({ sourcePath, destinationPath, cause }),
});
// Publish only a complete snapshot, without replacing an existing V2 database.
yield* fs.link(snapshotPath, destinationPath).pipe(
Effect.catchIf(
(error) => error.reason._tag === "AlreadyExists",
(_error) => Effect.void,
),
);
}).pipe(
Effect.scoped,
Effect.mapError((cause) =>
isV2DatabaseImportError(cause)
? cause
: new V2DatabaseImportError({ sourcePath, destinationPath, cause }),
),
);
});