Matrix: harden legacy migration fallback

This commit is contained in:
Gustavo Madeira Santana
2026-03-12 21:17:08 +00:00
parent bba53b1855
commit 2b7c013918
6 changed files with 217 additions and 25 deletions

View File

@@ -150,6 +150,32 @@ describe("matrix client storage paths", () => {
expect(fs.existsSync(storagePaths.cryptoPath)).toBe(true);
});
it("continues migrating whichever legacy artifact is still missing", async () => {
const stateDir = setupStateDir();
const storagePaths = resolveMatrixStoragePaths({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "secret-token",
env: {},
});
const legacyRoot = path.join(stateDir, "matrix");
fs.mkdirSync(storagePaths.rootDir, { recursive: true });
fs.writeFileSync(storagePaths.storagePath, '{"new":true}');
fs.mkdirSync(path.join(legacyRoot, "crypto"), { recursive: true });
await maybeMigrateLegacyStorage({
storagePaths,
env: {},
});
expect(maybeCreateMatrixMigrationSnapshotMock).toHaveBeenCalledWith(
expect.objectContaining({ trigger: "matrix-client-fallback" }),
);
expect(fs.readFileSync(storagePaths.storagePath, "utf8")).toBe('{"new":true}');
expect(fs.existsSync(path.join(legacyRoot, "crypto"))).toBe(false);
expect(fs.existsSync(storagePaths.cryptoPath)).toBe(true);
});
it("refuses to migrate legacy storage when the snapshot step fails", async () => {
const stateDir = setupStateDir();
const storagePaths = resolveMatrixStoragePaths({

View File

@@ -185,18 +185,20 @@ export async function maybeMigrateLegacyStorage(params: {
storagePaths: MatrixStoragePaths;
env?: NodeJS.ProcessEnv;
}): Promise<void> {
const hasNewStorage =
fs.existsSync(params.storagePaths.storagePath) || fs.existsSync(params.storagePaths.cryptoPath);
if (hasNewStorage) {
return;
}
const legacy = resolveLegacyStoragePaths(params.env);
const hasLegacyStorage = fs.existsSync(legacy.storagePath);
const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath);
if (!hasLegacyStorage && !hasLegacyCrypto) {
return;
}
const hasTargetStorage = fs.existsSync(params.storagePaths.storagePath);
const hasTargetCrypto = fs.existsSync(params.storagePaths.cryptoPath);
// Continue partial migrations one artifact at a time; only skip items whose targets already exist.
const shouldMigrateStorage = hasLegacyStorage && !hasTargetStorage;
const shouldMigrateCrypto = hasLegacyCrypto && !hasTargetCrypto;
if (!shouldMigrateStorage && !shouldMigrateCrypto) {
return;
}
assertLegacyMigrationAccountSelection({
accountKey: params.storagePaths.accountKey,
@@ -210,22 +212,31 @@ export async function maybeMigrateLegacyStorage(params: {
});
fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
const moved: LegacyMoveRecord[] = [];
const skippedExistingTargets: string[] = [];
try {
if (hasLegacyStorage) {
if (shouldMigrateStorage) {
moveLegacyStoragePathOrThrow({
sourcePath: legacy.storagePath,
targetPath: params.storagePaths.storagePath,
label: "sync store",
moved,
});
} else if (hasLegacyStorage) {
skippedExistingTargets.push(
`- sync store remains at ${legacy.storagePath} because ${params.storagePaths.storagePath} already exists`,
);
}
if (hasLegacyCrypto) {
if (shouldMigrateCrypto) {
moveLegacyStoragePathOrThrow({
sourcePath: legacy.cryptoPath,
targetPath: params.storagePaths.cryptoPath,
label: "crypto store",
moved,
});
} else if (hasLegacyCrypto) {
skippedExistingTargets.push(
`- crypto store remains at ${legacy.cryptoPath} because ${params.storagePaths.cryptoPath} already exists`,
);
}
} catch (err) {
const rollbackError = rollbackLegacyMoves(moved);
@@ -242,6 +253,11 @@ export async function maybeMigrateLegacyStorage(params: {
.join("\n")}`,
);
}
if (skippedExistingTargets.length > 0) {
logger.warn?.(
`matrix: legacy client storage still exists in the flat path because some account-scoped targets already existed.\n${skippedExistingTargets.join("\n")}`,
);
}
}
function moveLegacyStoragePathOrThrow(params: {