Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 13 additions & 53 deletions lib/storage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { createHash } from "node:crypto";
import { existsSync, promises as fs } from "node:fs";
import { basename, dirname, isAbsolute, join, relative } from "node:path";
import { basename, dirname, join } from "node:path";
import { ACCOUNT_LIMITS } from "./constants.js";
import { createLogger } from "./logger.js";
import {
Expand Down Expand Up @@ -62,6 +62,7 @@ import {
resolvePath,
resolveProjectStorageIdentityRoot,
} from "./storage/paths.js";
import { restoreAccountsFromBackupFile } from "./storage/restore.js";

export type {
CooldownReason,
Expand Down Expand Up @@ -769,58 +770,17 @@ export async function restoreAccountsFromBackup(
path: string,
options?: { persist?: boolean },
): Promise<AccountStorageV3> {
const backupRoot = getNamedBackupRoot(getStoragePath());
let resolvedBackupRoot: string;
try {
resolvedBackupRoot = await fs.realpath(backupRoot);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new Error(`Backup root does not exist: ${backupRoot}`);
}
throw error;
}
let resolvedBackupPath: string;
try {
resolvedBackupPath = await fs.realpath(path);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new Error(`Backup file no longer exists: ${path}`);
}
throw error;
}
const relativePath = relative(resolvedBackupRoot, resolvedBackupPath);
const isInsideBackupRoot =
relativePath.length > 0 &&
!relativePath.startsWith("..") &&
!isAbsolute(relativePath);
if (!isInsideBackupRoot) {
throw new Error(
`Backup path must stay inside ${resolvedBackupRoot}: ${path}`,
);
}

const { normalized } = await (async () => {
try {
return await loadAccountsFromPath(resolvedBackupPath);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new Error(`Backup file no longer exists: ${path}`);
}
throw error;
}
})();
if (!normalized || normalized.accounts.length === 0) {
throw new Error(
`Backup does not contain any accounts: ${resolvedBackupPath}`,
);
}
if (options?.persist !== false) {
await saveAccounts(normalized);
}
return normalized;
return restoreAccountsFromBackupFile(
path,
{
realpath: fs.realpath,
getNamedBackupRoot,
getStoragePath,
loadAccountsFromPath,
saveAccounts,
},
options,
);
}

export async function exportNamedBackup(
Expand Down
71 changes: 71 additions & 0 deletions lib/storage/restore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { isAbsolute, relative } from "node:path";
import type { AccountStorageV3 } from "../storage.js";

export interface RestoreAccountsFromBackupDeps {
realpath: typeof import("node:fs").promises.realpath;
getNamedBackupRoot: (storagePath: string) => string;
getStoragePath: () => string;
loadAccountsFromPath: (
path: string,
) => Promise<{ normalized: AccountStorageV3 | null }>;
saveAccounts: (storage: AccountStorageV3) => Promise<void>;
}

export async function restoreAccountsFromBackupFile(
path: string,
deps: RestoreAccountsFromBackupDeps,
options?: { persist?: boolean },
): Promise<AccountStorageV3> {
const backupRoot = deps.getNamedBackupRoot(deps.getStoragePath());
let resolvedBackupRoot: string;
try {
resolvedBackupRoot = await deps.realpath(backupRoot);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new Error(`Backup root does not exist: ${backupRoot}`);
}
throw error;
}
let resolvedBackupPath: string;
try {
resolvedBackupPath = await deps.realpath(path);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new Error(`Backup file no longer exists: ${path}`);
}
throw error;
}
const relativePath = relative(resolvedBackupRoot, resolvedBackupPath);
const isInsideBackupRoot =
relativePath.length > 0 &&
!relativePath.startsWith("..") &&
!isAbsolute(relativePath);
if (!isInsideBackupRoot) {
throw new Error(
`Backup path must stay inside ${resolvedBackupRoot}: ${path}`,
);
}

const { normalized } = await (async () => {
try {
return await deps.loadAccountsFromPath(resolvedBackupPath);
} catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ENOENT") {
throw new Error(`Backup file no longer exists: ${path}`);
}
throw error;
}
})();
if (!normalized || normalized.accounts.length === 0) {
throw new Error(
`Backup does not contain any accounts: ${resolvedBackupPath}`,
);
}
if (options?.persist !== false) {
await deps.saveAccounts(normalized);
}
return normalized;
}