zerobyte/app/server/utils/restic/helpers/build-env.ts
Nico 4a812a884b
refactor(restic): split each command into its own file (#584)
refactor(restic): split each command into its own file

refactor: add missing await before promise expects

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

## Release Notes

* **Tests**
  * Improved async assertion handling across test suites for enhanced test reliability
  * Expanded test coverage for backup, restore, and other core operations

* **Refactor**
  * Reorganized internal utility structure for improved code maintainability

* **Chores**
  * Updated linting configuration and TypeScript dependencies

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-26 19:51:24 +01:00

167 lines
5.3 KiB
TypeScript

import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import type { RepositoryConfig } from "~/schemas/restic";
import { RESTIC_CACHE_DIR } from "~/server/core/constants";
import { db } from "~/server/db/db";
import { cryptoUtils } from "~/server/utils/crypto";
import { logger } from "~/server/utils/logger";
import type { ResticEnv } from "../types";
export const buildEnv = async (config: RepositoryConfig, organizationId: string): Promise<ResticEnv> => {
const env: ResticEnv = {
RESTIC_CACHE_DIR,
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
};
if (config.isExistingRepository && config.customPassword) {
const decryptedPassword = await cryptoUtils.resolveSecret(config.customPassword);
const passwordFilePath = path.join("/tmp", `zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
await fs.writeFile(passwordFilePath, decryptedPassword, {
mode: 0o600,
});
env.RESTIC_PASSWORD_FILE = passwordFilePath;
} else {
const org = await db.query.organization.findFirst({
where: { id: organizationId },
});
if (!org) {
throw new Error(`Organization ${organizationId} not found`);
}
const metadata = org.metadata;
if (!metadata?.resticPassword) {
throw new Error(`Restic password not configured for organization ${organizationId}`);
}
const decryptedPassword = await cryptoUtils.resolveSecret(metadata.resticPassword);
const passwordFilePath = path.join("/tmp", `zerobyte-pass-${crypto.randomBytes(8).toString("hex")}.txt`);
await fs.writeFile(passwordFilePath, decryptedPassword, {
mode: 0o600,
});
env.RESTIC_PASSWORD_FILE = passwordFilePath;
}
switch (config.backend) {
case "s3":
env.AWS_ACCESS_KEY_ID = await cryptoUtils.resolveSecret(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.resolveSecret(config.secretAccessKey);
if (config.endpoint.includes("myhuaweicloud")) {
env.AWS_S3_BUCKET_LOOKUP = "dns";
}
break;
case "r2":
env.AWS_ACCESS_KEY_ID = await cryptoUtils.resolveSecret(config.accessKeyId);
env.AWS_SECRET_ACCESS_KEY = await cryptoUtils.resolveSecret(config.secretAccessKey);
env.AWS_REGION = "auto";
env.AWS_S3_FORCE_PATH_STYLE = "true";
break;
case "gcs": {
const decryptedCredentials = await cryptoUtils.resolveSecret(config.credentialsJson);
const credentialsPath = path.join("/tmp", `zerobyte-gcs-${crypto.randomBytes(8).toString("hex")}.json`);
await fs.writeFile(credentialsPath, decryptedCredentials, {
mode: 0o600,
});
env.GOOGLE_PROJECT_ID = config.projectId;
env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
break;
}
case "azure": {
env.AZURE_ACCOUNT_NAME = config.accountName;
env.AZURE_ACCOUNT_KEY = await cryptoUtils.resolveSecret(config.accountKey);
if (config.endpointSuffix) {
env.AZURE_ENDPOINT_SUFFIX = config.endpointSuffix;
}
break;
}
case "rest": {
if (config.username) {
env.RESTIC_REST_USERNAME = await cryptoUtils.resolveSecret(config.username);
}
if (config.password) {
env.RESTIC_REST_PASSWORD = await cryptoUtils.resolveSecret(config.password);
}
break;
}
case "sftp": {
const decryptedKey = await cryptoUtils.resolveSecret(config.privateKey);
const keyPath = path.join("/tmp", `zerobyte-ssh-${crypto.randomBytes(8).toString("hex")}`);
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
if (!normalizedKey.endsWith("\n")) {
normalizedKey += "\n";
}
if (normalizedKey.includes("ENCRYPTED")) {
logger.error("SFTP: Private key appears to be passphrase-protected. Please use an unencrypted key.");
throw new Error("Passphrase-protected SSH keys are not supported. Please provide an unencrypted private key.");
}
await fs.writeFile(keyPath, normalizedKey, { mode: 0o600 });
env._SFTP_KEY_PATH = keyPath;
const sshArgs = [
"-o",
"LogLevel=ERROR",
"-o",
"BatchMode=yes",
"-o",
"NumberOfPasswordPrompts=0",
"-o",
"PreferredAuthentications=publickey",
"-o",
"PasswordAuthentication=no",
"-o",
"KbdInteractiveAuthentication=no",
"-o",
"IdentitiesOnly=yes",
"-o",
"ConnectTimeout=10",
"-o",
"ConnectionAttempts=1",
"-o",
"ServerAliveInterval=60",
"-o",
"ServerAliveCountMax=240",
"-i",
keyPath,
];
if (config.skipHostKeyCheck || !config.knownHosts) {
sshArgs.push("-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null");
} else if (config.knownHosts) {
const knownHostsPath = path.join("/tmp", `zerobyte-known-hosts-${crypto.randomBytes(8).toString("hex")}`);
await fs.writeFile(knownHostsPath, config.knownHosts, {
mode: 0o600,
});
env._SFTP_KNOWN_HOSTS_PATH = knownHostsPath;
sshArgs.push("-o", "StrictHostKeyChecking=yes", "-o", `UserKnownHostsFile=${knownHostsPath}`);
}
if (config.port && config.port !== 22) {
sshArgs.push("-p", String(config.port));
}
env._SFTP_SSH_ARGS = sshArgs.join(" ");
logger.info(`SFTP: SSH args: ${env._SFTP_SSH_ARGS}`);
break;
}
}
if (config.cacert) {
const decryptedCert = await cryptoUtils.resolveSecret(config.cacert);
const certPath = path.join("/tmp", `zerobyte-cacert-${crypto.randomBytes(8).toString("hex")}.pem`);
await fs.writeFile(certPath, decryptedCert, { mode: 0o600 });
env.RESTIC_CACERT = certPath;
}
if (config.insecureTls) {
env._INSECURE_TLS = "true";
}
return env;
};