feat: add deep secret resolution for all config fields
- Add resolveSecretsDeep() to recursively resolve secret placeholders (env://, file://, encv1:) in any config field - Refactor restic operations to resolve secrets once via resolveAndBuild() - Refactor volume backend dispatcher to resolve secrets at mount time - Remove redundant per-field resolveSecret() calls from SMB, WebDAV, SFTP backends - Simplify notifications service by replacing switch-based decryption - Update backend-compatibility to use deep resolution for credential comparison This allows users to use secret references in any configuration field, not just the designated sensitive fields.
This commit is contained in:
parent
21040d7d4c
commit
92ba5f9706
8 changed files with 150 additions and 160 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import type { BackendStatus } from "~/schemas/volumes";
|
||||
import type { BackendConfig, BackendStatus } from "~/schemas/volumes";
|
||||
import type { Volume } from "../../db/schema";
|
||||
import { cryptoUtils } from "../../utils/crypto";
|
||||
import { getVolumePath } from "../volumes/helpers";
|
||||
import { makeDirectoryBackend } from "./directory/directory-backend";
|
||||
import { makeNfsBackend } from "./nfs/nfs-backend";
|
||||
|
|
@ -19,27 +20,33 @@ export type VolumeBackend = {
|
|||
checkHealth: () => Promise<OperationResult>;
|
||||
};
|
||||
|
||||
export const createVolumeBackend = (volume: Volume): VolumeBackend => {
|
||||
const path = getVolumePath(volume);
|
||||
|
||||
switch (volume.config.backend) {
|
||||
case "nfs": {
|
||||
return makeNfsBackend(volume.config, path);
|
||||
}
|
||||
case "smb": {
|
||||
return makeSmbBackend(volume.config, path);
|
||||
}
|
||||
case "directory": {
|
||||
return makeDirectoryBackend(volume.config, path);
|
||||
}
|
||||
case "webdav": {
|
||||
return makeWebdavBackend(volume.config, path);
|
||||
}
|
||||
case "rclone": {
|
||||
return makeRcloneBackend(volume.config, path);
|
||||
}
|
||||
case "sftp": {
|
||||
return makeSftpBackend(volume.config, path);
|
||||
}
|
||||
const getBackendFactory = (backendType: BackendConfig["backend"]) => {
|
||||
switch (backendType) {
|
||||
case "nfs":
|
||||
return makeNfsBackend;
|
||||
case "smb":
|
||||
return makeSmbBackend;
|
||||
case "directory":
|
||||
return makeDirectoryBackend;
|
||||
case "webdav":
|
||||
return makeWebdavBackend;
|
||||
case "rclone":
|
||||
return makeRcloneBackend;
|
||||
case "sftp":
|
||||
return makeSftpBackend;
|
||||
}
|
||||
};
|
||||
|
||||
export const createVolumeBackend = (volume: Volume): VolumeBackend => {
|
||||
const path = getVolumePath(volume);
|
||||
const makeBackend = getBackendFactory(volume.config.backend);
|
||||
|
||||
return {
|
||||
mount: async () => {
|
||||
const resolvedConfig = await cryptoUtils.resolveSecretsDeep(volume.config);
|
||||
return makeBackend(resolvedConfig, path).mount();
|
||||
},
|
||||
unmount: () => makeBackend(volume.config, path).unmount(),
|
||||
checkHealth: () => makeBackend(volume.config, path).checkHealth(),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import * as os from "node:os";
|
|||
import * as path from "node:path";
|
||||
import { $ } from "bun";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
|
|
@ -79,8 +78,7 @@ const mount = async (config: BackendConfig, mountPath: string) => {
|
|||
|
||||
const keyPath = getPrivateKeyPath(mountPath);
|
||||
if (config.privateKey) {
|
||||
const decryptedKey = await cryptoUtils.resolveSecret(config.privateKey);
|
||||
let normalizedKey = decryptedKey.replace(/\r\n/g, "\n");
|
||||
let normalizedKey = config.privateKey.replace(/\r\n/g, "\n");
|
||||
if (!normalizedKey.endsWith("\n")) {
|
||||
normalizedKey += "\n";
|
||||
}
|
||||
|
|
@ -95,10 +93,9 @@ const mount = async (config: BackendConfig, mountPath: string) => {
|
|||
|
||||
let result: $.ShellOutput;
|
||||
if (config.password) {
|
||||
const password = await cryptoUtils.resolveSecret(config.password);
|
||||
args.push("-o", "password_stdin");
|
||||
logger.info(`Executing sshfs: echo "******" | sshfs ${args.join(" ")}`);
|
||||
result = await $`echo ${password} | sshfs ${args}`.nothrow();
|
||||
result = await $`echo ${config.password} | sshfs ${args}`.nothrow();
|
||||
} else {
|
||||
logger.info(`Executing sshfs: sshfs ${args.join(" ")}`);
|
||||
result = await $`sshfs ${args}`.nothrow();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
|
|
@ -36,13 +35,11 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
const run = async () => {
|
||||
await fs.mkdir(path, { recursive: true });
|
||||
|
||||
const password = await cryptoUtils.resolveSecret(config.password);
|
||||
|
||||
const source = `//${config.server}/${config.share}`;
|
||||
const { uid, gid } = os.userInfo();
|
||||
const options = [
|
||||
`user=${config.username}`,
|
||||
`pass=${password}`,
|
||||
`pass=${config.password}`,
|
||||
`vers=${config.vers}`,
|
||||
`port=${config.port}`,
|
||||
`uid=${uid}`,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import { OPERATION_TIMEOUT } from "../../../core/constants";
|
||||
import { cryptoUtils } from "../../../utils/crypto";
|
||||
import { toMessage } from "../../../utils/errors";
|
||||
import { logger } from "../../../utils/logger";
|
||||
import { getMountForPath } from "../../../utils/mountinfo";
|
||||
|
|
@ -49,9 +48,8 @@ const mount = async (config: BackendConfig, path: string) => {
|
|||
: [`uid=${uid}`, `gid=${gid}`, "file_mode=0664", "dir_mode=0775"];
|
||||
|
||||
if (config.username && config.password) {
|
||||
const password = await cryptoUtils.resolveSecret(config.password);
|
||||
const secretsFile = "/etc/davfs2/secrets";
|
||||
const secretsContent = `${source} ${config.username} ${password}\n`;
|
||||
const secretsContent = `${source} ${config.username} ${config.password}\n`;
|
||||
await fs.appendFile(secretsFile, secretsContent, { mode: 0o600 });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -83,55 +83,6 @@ async function encryptSensitiveFields(config: NotificationConfig): Promise<Notif
|
|||
}
|
||||
}
|
||||
|
||||
async function decryptSensitiveFields(config: NotificationConfig): Promise<NotificationConfig> {
|
||||
switch (config.type) {
|
||||
case "email":
|
||||
return {
|
||||
...config,
|
||||
password: config.password ? await cryptoUtils.resolveSecret(config.password) : undefined,
|
||||
};
|
||||
case "slack":
|
||||
return {
|
||||
...config,
|
||||
webhookUrl: await cryptoUtils.resolveSecret(config.webhookUrl),
|
||||
};
|
||||
case "discord":
|
||||
return {
|
||||
...config,
|
||||
webhookUrl: await cryptoUtils.resolveSecret(config.webhookUrl),
|
||||
};
|
||||
case "gotify":
|
||||
return {
|
||||
...config,
|
||||
token: await cryptoUtils.resolveSecret(config.token),
|
||||
};
|
||||
case "ntfy":
|
||||
return {
|
||||
...config,
|
||||
password: config.password ? await cryptoUtils.resolveSecret(config.password) : undefined,
|
||||
};
|
||||
case "pushover":
|
||||
return {
|
||||
...config,
|
||||
apiToken: await cryptoUtils.resolveSecret(config.apiToken),
|
||||
};
|
||||
case "telegram":
|
||||
return {
|
||||
...config,
|
||||
botToken: await cryptoUtils.resolveSecret(config.botToken),
|
||||
};
|
||||
case "generic":
|
||||
return config;
|
||||
case "custom":
|
||||
return {
|
||||
...config,
|
||||
shoutrrrUrl: await cryptoUtils.resolveSecret(config.shoutrrrUrl),
|
||||
};
|
||||
default:
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
const createDestination = async (name: string, config: NotificationConfig) => {
|
||||
const slug = slugify(name, { lower: true, strict: true });
|
||||
|
||||
|
|
@ -225,9 +176,9 @@ const testDestination = async (id: number) => {
|
|||
throw new ConflictError("Cannot test disabled notification destination");
|
||||
}
|
||||
|
||||
const decryptedConfig = await decryptSensitiveFields(destination.config);
|
||||
const resolvedConfig = await cryptoUtils.resolveSecretsDeep(destination.config);
|
||||
|
||||
const shoutrrrUrl = buildShoutrrrUrl(decryptedConfig);
|
||||
const shoutrrrUrl = buildShoutrrrUrl(resolvedConfig);
|
||||
|
||||
console.log("Testing notification with Shoutrrr URL:", shoutrrrUrl);
|
||||
|
||||
|
|
@ -327,8 +278,8 @@ const sendBackupNotification = async (
|
|||
|
||||
for (const assignment of relevantAssignments) {
|
||||
try {
|
||||
const decryptedConfig = await decryptSensitiveFields(assignment.destination.config);
|
||||
const shoutrrrUrl = buildShoutrrrUrl(decryptedConfig);
|
||||
const resolvedConfig = await cryptoUtils.resolveSecretsDeep(assignment.destination.config);
|
||||
const shoutrrrUrl = buildShoutrrrUrl(resolvedConfig);
|
||||
|
||||
const result = await sendNotification({
|
||||
shoutrrrUrl,
|
||||
|
|
|
|||
|
|
@ -35,52 +35,54 @@ export const hasCompatibleCredentials = async (
|
|||
return true;
|
||||
}
|
||||
|
||||
// Resolve secrets in both configs for comparison
|
||||
const resolvedConfig1 = await cryptoUtils.resolveSecretsDeep(config1);
|
||||
const resolvedConfig2 = await cryptoUtils.resolveSecretsDeep(config2);
|
||||
|
||||
switch (group1) {
|
||||
case "s3": {
|
||||
if (
|
||||
(config1.backend === "s3" || config1.backend === "r2") &&
|
||||
(config2.backend === "s3" || config2.backend === "r2")
|
||||
(resolvedConfig1.backend === "s3" || resolvedConfig1.backend === "r2") &&
|
||||
(resolvedConfig2.backend === "s3" || resolvedConfig2.backend === "r2")
|
||||
) {
|
||||
const accessKey1 = await cryptoUtils.resolveSecret(config1.accessKeyId);
|
||||
const secretKey1 = await cryptoUtils.resolveSecret(config1.secretAccessKey);
|
||||
|
||||
const accessKey2 = await cryptoUtils.resolveSecret(config2.accessKeyId);
|
||||
const secretKey2 = await cryptoUtils.resolveSecret(config2.secretAccessKey);
|
||||
|
||||
return accessKey1 === accessKey2 && secretKey1 === secretKey2;
|
||||
return (
|
||||
resolvedConfig1.accessKeyId === resolvedConfig2.accessKeyId &&
|
||||
resolvedConfig1.secretAccessKey === resolvedConfig2.secretAccessKey
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "gcs": {
|
||||
if (config1.backend === "gcs" && config2.backend === "gcs") {
|
||||
const credentials1 = await cryptoUtils.resolveSecret(config1.credentialsJson);
|
||||
const credentials2 = await cryptoUtils.resolveSecret(config2.credentialsJson);
|
||||
|
||||
return credentials1 === credentials2 && config1.projectId === config2.projectId;
|
||||
if (resolvedConfig1.backend === "gcs" && resolvedConfig2.backend === "gcs") {
|
||||
return (
|
||||
resolvedConfig1.credentialsJson === resolvedConfig2.credentialsJson &&
|
||||
resolvedConfig1.projectId === resolvedConfig2.projectId
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "azure": {
|
||||
if (config1.backend === "azure" && config2.backend === "azure") {
|
||||
const config1Accountkey = await cryptoUtils.resolveSecret(config1.accountKey);
|
||||
const config2Accountkey = await cryptoUtils.resolveSecret(config2.accountKey);
|
||||
|
||||
return config1.accountName === config2.accountName && config1Accountkey === config2Accountkey;
|
||||
if (resolvedConfig1.backend === "azure" && resolvedConfig2.backend === "azure") {
|
||||
return (
|
||||
resolvedConfig1.accountName === resolvedConfig2.accountName &&
|
||||
resolvedConfig1.accountKey === resolvedConfig2.accountKey
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case "rest": {
|
||||
if (config1.backend === "rest" && config2.backend === "rest") {
|
||||
if (!config1.username && !config2.username && !config1.password && !config2.password) {
|
||||
if (resolvedConfig1.backend === "rest" && resolvedConfig2.backend === "rest") {
|
||||
if (
|
||||
!resolvedConfig1.username &&
|
||||
!resolvedConfig2.username &&
|
||||
!resolvedConfig1.password &&
|
||||
!resolvedConfig2.password
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const config1Username = await cryptoUtils.resolveSecret(config1.username || "");
|
||||
const config1Password = await cryptoUtils.resolveSecret(config1.password || "");
|
||||
const config2Username = await cryptoUtils.resolveSecret(config2.username || "");
|
||||
const config2Password = await cryptoUtils.resolveSecret(config2.password || "");
|
||||
|
||||
return config1Username === config2Username && config1Password === config2Password;
|
||||
return (
|
||||
resolvedConfig1.username === resolvedConfig2.username && resolvedConfig1.password === resolvedConfig2.password
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -183,7 +183,53 @@ const sealSecret = async (value: string): Promise<string> => {
|
|||
return encrypt(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to check if a value is a plain object (not null, array, Date, etc.)
|
||||
*/
|
||||
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) && value.constructor === Object;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recursively resolves all secret references in an object or array.
|
||||
* Handles nested objects, arrays, and circular references.
|
||||
*/
|
||||
const resolveSecretsDeep = async <T>(input: T): Promise<T> => {
|
||||
const seen = new WeakMap<object, unknown>();
|
||||
|
||||
const resolve = async (value: unknown): Promise<unknown> => {
|
||||
if (typeof value === "string") {
|
||||
return resolveSecret(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (seen.has(value)) return seen.get(value);
|
||||
const result: unknown[] = [];
|
||||
seen.set(value, result);
|
||||
for (const item of value) {
|
||||
result.push(await resolve(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
if (seen.has(value)) return seen.get(value);
|
||||
const result: Record<string, unknown> = {};
|
||||
seen.set(value, result);
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
result[key] = await resolve(val);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
return resolve(input) as Promise<T>;
|
||||
};
|
||||
|
||||
export const cryptoUtils = {
|
||||
resolveSecret,
|
||||
sealSecret,
|
||||
resolveSecretsDeep,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -112,10 +112,9 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
|||
};
|
||||
|
||||
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 });
|
||||
await fs.writeFile(passwordFilePath, config.customPassword, { mode: 0o600 });
|
||||
env.RESTIC_PASSWORD_FILE = passwordFilePath;
|
||||
} else {
|
||||
env.RESTIC_PASSWORD_FILE = RESTIC_PASS_FILE;
|
||||
|
|
@ -123,26 +122,25 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
|||
|
||||
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);
|
||||
env.AWS_ACCESS_KEY_ID = config.accessKeyId;
|
||||
env.AWS_SECRET_ACCESS_KEY = config.secretAccessKey;
|
||||
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_ACCESS_KEY_ID = config.accessKeyId;
|
||||
env.AWS_SECRET_ACCESS_KEY = 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 });
|
||||
await fs.writeFile(credentialsPath, config.credentialsJson, { 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);
|
||||
env.AZURE_ACCOUNT_KEY = config.accountKey;
|
||||
if (config.endpointSuffix) {
|
||||
env.AZURE_ENDPOINT_SUFFIX = config.endpointSuffix;
|
||||
}
|
||||
|
|
@ -150,18 +148,17 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
|||
}
|
||||
case "rest": {
|
||||
if (config.username) {
|
||||
env.RESTIC_REST_USERNAME = await cryptoUtils.resolveSecret(config.username);
|
||||
env.RESTIC_REST_USERNAME = config.username;
|
||||
}
|
||||
if (config.password) {
|
||||
env.RESTIC_REST_PASSWORD = await cryptoUtils.resolveSecret(config.password);
|
||||
env.RESTIC_REST_PASSWORD = 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");
|
||||
let normalizedKey = config.privateKey.replace(/\r\n/g, "\n");
|
||||
if (!normalizedKey.endsWith("\n")) {
|
||||
normalizedKey += "\n";
|
||||
}
|
||||
|
|
@ -206,9 +203,8 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
|||
}
|
||||
|
||||
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 });
|
||||
await fs.writeFile(certPath, config.cacert, { mode: 0o600 });
|
||||
env.RESTIC_CACERT = certPath;
|
||||
}
|
||||
|
||||
|
|
@ -219,15 +215,24 @@ export const buildEnv = async (config: RepositoryConfig) => {
|
|||
return env;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves all secret placeholders in config and builds repo URL + env.
|
||||
* Call this at the start of each restic operation.
|
||||
*/
|
||||
const resolveAndBuild = async (config: RepositoryConfig) => {
|
||||
const resolved = await cryptoUtils.resolveSecretsDeep(config);
|
||||
const repoUrl = buildRepoUrl(resolved);
|
||||
const env = await buildEnv(resolved);
|
||||
return { resolved, repoUrl, env };
|
||||
};
|
||||
|
||||
const init = async (config: RepositoryConfig) => {
|
||||
await ensurePassfile();
|
||||
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
logger.info(`Initializing restic repository at ${repoUrl}...`);
|
||||
|
||||
const env = await buildEnv(config);
|
||||
|
||||
const args = ["init", "--repo", repoUrl];
|
||||
addCommonArgs(args, env);
|
||||
|
||||
|
|
@ -270,8 +275,7 @@ const backup = async (
|
|||
onProgress?: (progress: BackupProgress) => void;
|
||||
},
|
||||
) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "backup", "--compression", options?.compressionMode ?? "auto"];
|
||||
|
||||
|
|
@ -422,8 +426,7 @@ const restore = async (
|
|||
overwrite?: OverwriteMode;
|
||||
},
|
||||
) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "restore", snapshotId, "--target", target];
|
||||
|
||||
|
|
@ -505,8 +508,7 @@ const restore = async (
|
|||
const snapshots = async (config: RepositoryConfig, options: { tags?: string[] } = {}) => {
|
||||
const { tags } = options;
|
||||
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args = ["--repo", repoUrl, "snapshots"];
|
||||
|
||||
|
|
@ -537,8 +539,7 @@ const snapshots = async (config: RepositoryConfig, options: { tags?: string[] }
|
|||
};
|
||||
|
||||
const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra: { tag: string }) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "forget", "--group-by", "tags", "--tag", extra.tag];
|
||||
|
||||
|
|
@ -579,8 +580,7 @@ const forget = async (config: RepositoryConfig, options: RetentionPolicy, extra:
|
|||
};
|
||||
|
||||
const deleteSnapshots = async (config: RepositoryConfig, snapshotIds: string[]) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
if (snapshotIds.length === 0) {
|
||||
throw new Error("No snapshot IDs provided for deletion.");
|
||||
|
|
@ -609,8 +609,7 @@ const tagSnapshots = async (
|
|||
snapshotIds: string[],
|
||||
tags: { add?: string[]; remove?: string[]; set?: string[] },
|
||||
) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
if (snapshotIds.length === 0) {
|
||||
throw new Error("No snapshot IDs provided for tagging.");
|
||||
|
|
@ -677,8 +676,7 @@ const lsSnapshotInfoSchema = type({
|
|||
});
|
||||
|
||||
const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "ls", snapshotId, "--long"];
|
||||
|
||||
|
|
@ -733,8 +731,7 @@ const ls = async (config: RepositoryConfig, snapshotId: string, path?: string) =
|
|||
};
|
||||
|
||||
const unlock = async (config: RepositoryConfig) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args = ["unlock", "--repo", repoUrl, "--remove-all"];
|
||||
addCommonArgs(args, env);
|
||||
|
|
@ -752,8 +749,7 @@ const unlock = async (config: RepositoryConfig) => {
|
|||
};
|
||||
|
||||
const check = async (config: RepositoryConfig, options?: { readData?: boolean }) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args: string[] = ["--repo", repoUrl, "check"];
|
||||
|
||||
|
|
@ -790,8 +786,7 @@ const check = async (config: RepositoryConfig, options?: { readData?: boolean })
|
|||
};
|
||||
|
||||
const repairIndex = async (config: RepositoryConfig) => {
|
||||
const repoUrl = buildRepoUrl(config);
|
||||
const env = await buildEnv(config);
|
||||
const { repoUrl, env } = await resolveAndBuild(config);
|
||||
|
||||
const args = ["repair", "index", "--repo", repoUrl];
|
||||
addCommonArgs(args, env);
|
||||
|
|
@ -822,11 +817,8 @@ const copy = async (
|
|||
snapshotId?: string;
|
||||
},
|
||||
) => {
|
||||
const sourceRepoUrl = buildRepoUrl(sourceConfig);
|
||||
const destRepoUrl = buildRepoUrl(destConfig);
|
||||
|
||||
const sourceEnv = await buildEnv(sourceConfig);
|
||||
const destEnv = await buildEnv(destConfig);
|
||||
const { resolved: resolvedSource, repoUrl: sourceRepoUrl, env: sourceEnv } = await resolveAndBuild(sourceConfig);
|
||||
const { repoUrl: destRepoUrl, env: destEnv } = await resolveAndBuild(destConfig);
|
||||
|
||||
const env: Record<string, string> = {
|
||||
...sourceEnv,
|
||||
|
|
@ -848,7 +840,7 @@ const copy = async (
|
|||
|
||||
addCommonArgs(args, env);
|
||||
|
||||
if (sourceConfig.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) {
|
||||
if (resolvedSource.backend === "sftp" && sourceEnv._SFTP_SSH_ARGS) {
|
||||
args.push("-o", `sftp.args=${sourceEnv._SFTP_SSH_ARGS}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue