1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
4import * as PlatformError from "effect/PlatformError";
5import * as Schema from "effect/Schema";
6
7export class DesktopUserDataInitializationError extends Schema.TaggedError<DesktopUserDataInitializationError>()(
8 "DesktopUserDataInitializationError",
9 {
10 operation: Schema.Literals(["inspect", "read", "create-directory", "write"]),
11 resourcePath: Schema.String,
12 category: Schema.String,
13 cause: Schema.Defect(),
14 },
15) {
16 override get message() {
17 return `Could not initialize Electron user data during ${this.operation} at ${this.resourcePath} (${this.category}).`;
18 }
19
20 static fromFileSystem(
21 cause: PlatformError.PlatformError,
22 operation: DesktopUserDataInitializationError["operation"],
23 resourcePath: string,
24 ) {
25 return new DesktopUserDataInitializationError({
26 operation,
27 resourcePath,
28 category: cause.reason._tag,
29 cause,
30 });
31 }
32}
33
34/** Select Electron's profile independently of the server's T3 home. */
35export const resolveUserDataPath = Effect.fn("desktop.userData.resolveUserDataPath")(
36 function* (input: {
37 readonly appDataDirectory: string;
38 readonly isDevelopment: boolean;
39 readonly platform: NodeJS.Platform;
40 }) {
41 const fs = yield* FileSystem.FileSystem;
42 const path = yield* Path.Path;
43 const names = input.isDevelopment
44 ? { current: "t3code-dev", legacy: "T3 Code (Dev)" }
45 : { current: "t3code-v2", legacy: "T3 Code (Alpha)" };
46 const destinationPath = path.join(input.appDataDirectory, names.current);
47 const legacyPath = path.join(input.appDataDirectory, names.legacy);
48 const inspect = (resourcePath: string) =>
49 fs
50 .exists(resourcePath)
51 .pipe(
52 Effect.mapError((cause) =>
53 DesktopUserDataInitializationError.fromFileSystem(cause, "inspect", resourcePath),
54 ),
55 );
56 if (input.isDevelopment) {
57 return (yield* inspect(legacyPath)) ? legacyPath : destinationPath;
58 }
59 // Chromium databases require their own profile for each running version.
60 if (input.platform !== "win32") return destinationPath;
61 const destinationState = path.join(destinationPath, "Local State");
62 if (yield* inspect(destinationState)) return destinationPath;
63 const legacyState = path.join(legacyPath, "Local State");
64 const sourceState = (yield* inspect(legacyState))
65 ? legacyState
66 : path.join(input.appDataDirectory, "t3code", "Local State");
67 if (!(yield* inspect(sourceState))) return destinationPath;
68 // Windows safeStorage keys live here. Copy only these preferences, never locked databases.
69 const state = yield* fs
70 .readFileString(sourceState)
71 .pipe(
72 Effect.mapError((cause) =>
73 DesktopUserDataInitializationError.fromFileSystem(cause, "read", sourceState),
74 ),
75 );
76 yield* fs
77 .makeDirectory(destinationPath, { recursive: true })
78 .pipe(
79 Effect.mapError((cause) =>
80 DesktopUserDataInitializationError.fromFileSystem(
81 cause,
82 "create-directory",
83 destinationPath,
84 ),
85 ),
86 );
87 yield* fs.writeFileString(destinationState, state, { flag: "wx" }).pipe(
88 Effect.catchIf(
89 (error) => error.reason._tag === "AlreadyExists",
90 () => Effect.void,
91 ),
92 Effect.mapError((cause) =>
93 DesktopUserDataInitializationError.fromFileSystem(cause, "write", destinationState),
94 ),
95 );
96 return destinationPath;
97 },
98);