Line data Source code
1 : import type { Logger } from '../platform/logger/logger.js';
2 : import type { StorageBackend } from './storage_backend.js';
3 :
4 1 : const LOG_CTX = 'session_transfer';
5 :
6 : /// The result of copying one or more sessions between backends.
7 : export interface SessionTransferResult {
8 : /// The number of sessions copied
9 : sessionCount: number;
10 : /// The number of files written (session file, scripts and data)
11 : fileCount: number;
12 : }
13 :
14 : /// Copy a single session's files from `source` to `target`, keyed by the same UUID.
15 : ///
16 : /// The UUID is the authoritative identity and is preserved verbatim - nothing is re-prefixed. The
17 : /// session file, the flat scripts and the binary data are copied as
18 : /// they are. `source` is left untouched; the caller deletes the source copy only after the copy is
19 : /// verified.
20 1 : export async function copySession(
21 1 : sessionId: string,
22 1 : source: StorageBackend,
23 1 : target: StorageBackend,
24 1 : logger: Logger,
25 1 : ): Promise<SessionTransferResult> {
26 1 : let fileCount = 0;
27 :
28 : // Session manifest (copied verbatim; sessionId is preserved)
29 1 : const sessionData = await source.loadSession(sessionId);
30 1 : await target.saveSessionManifest(sessionId, sessionData);
31 1 : fileCount++;
32 :
33 1 : for (const script of await source.loadScripts(sessionId)) {
34 1 : await target.saveScript(sessionId, script.name, script.sql);
35 1 : fileCount++;
36 1 : }
37 1 : for (const file of await source.listDataFiles(sessionId)) {
38 1 : await target.saveDataFile(sessionId, file.name, await source.loadDataFile(sessionId, file.name));
39 1 : fileCount++;
40 1 : }
41 :
42 1 : logger.info('copied session', { sessionId, files: String(fileCount) }, LOG_CTX);
43 1 : return { sessionCount: 1, fileCount };
44 1 : }
45 :
46 : /// Clone a session into `target` under a new UUID (always used for OPFS duplicates).
47 : ///
48 : /// Rewrites `sessionId`, drops the display-only `sessionPath`, and suffixes a named session
49 : /// with " (copy)". The clone references the same connections. Query result cache is not copied. On any write failure the new session is
50 : /// deleted so a half-written clone cannot linger in the registry.
51 1 : export async function cloneSession(
52 1 : sourceSessionId: string,
53 1 : source: StorageBackend,
54 1 : target: StorageBackend,
55 1 : newSessionId: string,
56 1 : logger: Logger,
57 1 : ): Promise<SessionTransferResult> {
58 1 : let fileCount = 0;
59 1 : try {
60 : // The copy shares the connections of its source
61 1 : const sessionData = {
62 1 : ...(await source.loadSession(sourceSessionId)),
63 1 : sessionId: newSessionId,
64 1 : };
65 1 : delete sessionData.sessionPath;
66 1 : const trimmedName = sessionData.name?.trim();
67 1 : if (trimmedName) {
68 1 : sessionData.name = `${trimmedName} (copy)`;
69 : }
70 :
71 1 : await target.saveSessionManifest(newSessionId, sessionData);
72 1 : fileCount++;
73 :
74 1 : for (const script of await source.loadScripts(sourceSessionId)) {
75 1 : await target.saveScript(newSessionId, script.name, script.sql);
76 1 : fileCount++;
77 1 : }
78 1 : for (const file of await source.listDataFiles(sourceSessionId)) {
79 1 : await target.saveDataFile(newSessionId, file.name, await source.loadDataFile(sourceSessionId, file.name));
80 1 : fileCount++;
81 1 : }
82 :
83 1 : logger.info('cloned session', {
84 1 : sourceSessionId,
85 1 : sessionId: newSessionId,
86 1 : files: String(fileCount),
87 1 : }, LOG_CTX);
88 1 : return { sessionCount: 1, fileCount };
89 1 : } catch (error) {
90 1 : try {
91 1 : await target.deleteSession(newSessionId);
92 1 : } catch {
93 : // Preserve the clone error if rollback fails.
94 0 : }
95 1 : throw error;
96 1 : }
97 1 : }
98 :
99 : /// Verify a single session was copied completely by re-reading `target`.
100 : ///
101 : /// Checks that the target session parses with a sessionId and that its per-session script count
102 : /// matches the source. Returns false on any mismatch so the caller can abort and keep the source
103 : /// untouched.
104 1 : export async function verifySession(sessionId: string, source: StorageBackend, target: StorageBackend): Promise<boolean> {
105 1 : let targetData;
106 1 : try {
107 1 : targetData = await target.loadSession(sessionId);
108 1 : } catch {
109 0 : return false;
110 0 : }
111 1 : if (!targetData.sessionId) {
112 0 : return false;
113 : }
114 :
115 1 : if ((await source.loadScripts(sessionId)).length !== (await target.loadScripts(sessionId)).length) return false;
116 1 : try {
117 1 : const sourceFiles = await source.listDataFiles(sessionId);
118 1 : const targetFiles = await target.listDataFiles(sessionId);
119 1 : if (sourceFiles.length !== targetFiles.length) return false;
120 1 : for (const file of sourceFiles) {
121 1 : const sourceBytes = await source.loadDataFile(sessionId, file.name);
122 1 : const targetBytes = await target.loadDataFile(sessionId, file.name);
123 1 : if (sourceBytes.length !== targetBytes.length || !sourceBytes.every((byte, i) => byte === targetBytes[i])) return false;
124 1 : }
125 1 : return true;
126 1 : } catch {
127 0 : return false;
128 0 : }
129 1 : }
|