mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: restore to custom location (#78)
* feat: restore to custom location * refactor: define overwrite mode in shared schema
This commit is contained in:
@@ -1224,6 +1224,8 @@ export type RestoreSnapshotData = {
|
|||||||
exclude?: Array<string>;
|
exclude?: Array<string>;
|
||||||
excludeXattr?: Array<string>;
|
excludeXattr?: Array<string>;
|
||||||
include?: Array<string>;
|
include?: Array<string>;
|
||||||
|
overwrite?: 'always' | 'if-changed' | 'if-newer' | 'never';
|
||||||
|
targetPath?: string;
|
||||||
};
|
};
|
||||||
path: {
|
path: {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { ChevronDown, FileIcon } from "lucide-react";
|
import { ChevronDown, FileIcon, FolderOpen, RotateCcw } from "lucide-react";
|
||||||
import { FileTree } from "~/client/components/file-tree";
|
import { FileTree } from "~/client/components/file-tree";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/client/components/ui/card";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Checkbox } from "~/client/components/ui/checkbox";
|
import { Checkbox } from "~/client/components/ui/checkbox";
|
||||||
import { Label } from "~/client/components/ui/label";
|
import { Label } from "~/client/components/ui/label";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -22,6 +23,9 @@ import type { Snapshot, Volume } from "~/client/lib/types";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||||
|
import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic";
|
||||||
|
|
||||||
|
type RestoreLocation = "original" | "custom";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
snapshot: Snapshot;
|
snapshot: Snapshot;
|
||||||
@@ -42,6 +46,9 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
const [deleteExtraFiles, setDeleteExtraFiles] = useState(false);
|
const [deleteExtraFiles, setDeleteExtraFiles] = useState(false);
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
const [excludeXattr, setExcludeXattr] = useState("");
|
const [excludeXattr, setExcludeXattr] = useState("");
|
||||||
|
const [restoreLocation, setRestoreLocation] = useState<RestoreLocation>("original");
|
||||||
|
const [customTargetPath, setCustomTargetPath] = useState("");
|
||||||
|
const [overwriteMode, setOverwriteMode] = useState<OverwriteMode>("always");
|
||||||
|
|
||||||
const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/";
|
const volumeBasePath = snapshot.paths[0]?.match(/^(.*?_data)(\/|$)/)?.[1] || "/";
|
||||||
|
|
||||||
@@ -127,6 +134,9 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
.map((s) => s.trim())
|
.map((s) => s.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const isCustomLocation = restoreLocation === "custom";
|
||||||
|
const targetPath = isCustomLocation && customTargetPath.trim() ? customTargetPath.trim() : undefined;
|
||||||
|
|
||||||
restoreSnapshot({
|
restoreSnapshot({
|
||||||
path: { name: repositoryName },
|
path: { name: repositoryName },
|
||||||
body: {
|
body: {
|
||||||
@@ -134,11 +144,24 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
include: includePaths,
|
include: includePaths,
|
||||||
delete: deleteExtraFiles,
|
delete: deleteExtraFiles,
|
||||||
excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,
|
excludeXattr: excludeXattrArray && excludeXattrArray.length > 0 ? excludeXattrArray : undefined,
|
||||||
|
targetPath,
|
||||||
|
overwrite: overwriteMode,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowRestoreDialog(false);
|
setShowRestoreDialog(false);
|
||||||
}, [selectedPaths, addBasePath, repositoryName, snapshot.short_id, restoreSnapshot, deleteExtraFiles, excludeXattr]);
|
}, [
|
||||||
|
selectedPaths,
|
||||||
|
addBasePath,
|
||||||
|
repositoryName,
|
||||||
|
snapshot.short_id,
|
||||||
|
restoreSnapshot,
|
||||||
|
deleteExtraFiles,
|
||||||
|
excludeXattr,
|
||||||
|
restoreLocation,
|
||||||
|
customTargetPath,
|
||||||
|
overwriteMode,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -221,17 +244,77 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<AlertDialog open={showRestoreDialog} onOpenChange={setShowRestoreDialog}>
|
<AlertDialog open={showRestoreDialog} onOpenChange={setShowRestoreDialog}>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent className="max-w-lg">
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Confirm Restore</AlertDialogTitle>
|
<AlertDialogTitle>Confirm Restore</AlertDialogTitle>
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
{selectedPaths.size > 0
|
{selectedPaths.size > 0
|
||||||
? `This will restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"} from the snapshot.`
|
? `This will restore ${selectedPaths.size} selected ${selectedPaths.size === 1 ? "item" : "items"} from the snapshot.`
|
||||||
: "This will restore everything from the snapshot."}{" "}
|
: "This will restore everything from the snapshot."}
|
||||||
Existing files will be overwritten by what's in the snapshot. This action cannot be undone.
|
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-sm font-medium">Restore Location</Label>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={restoreLocation === "original" ? "secondary" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="flex justify-start gap-2"
|
||||||
|
onClick={() => setRestoreLocation("original")}
|
||||||
|
>
|
||||||
|
<RotateCcw size={16} className="mr-1" />
|
||||||
|
Original location
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={restoreLocation === "custom" ? "secondary" : "outline"}
|
||||||
|
size="sm"
|
||||||
|
className="justify-start gap-2"
|
||||||
|
onClick={() => setRestoreLocation("custom")}
|
||||||
|
>
|
||||||
|
<FolderOpen size={16} className="mr-1" />
|
||||||
|
Custom location
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{restoreLocation === "custom" && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Input
|
||||||
|
placeholder="/path/to/restore"
|
||||||
|
value={customTargetPath}
|
||||||
|
onChange={(e) => setCustomTargetPath(e.target.value)}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">Files will be restored directly to this path</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-sm font-medium">Overwrite Mode</Label>
|
||||||
|
<Select value={overwriteMode} onValueChange={(value) => setOverwriteMode(value as OverwriteMode)}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select overwrite behavior" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={OVERWRITE_MODES.always}>Always overwrite</SelectItem>
|
||||||
|
<SelectItem value={OVERWRITE_MODES.ifChanged}>Only if content changed</SelectItem>
|
||||||
|
<SelectItem value={OVERWRITE_MODES.ifNewer}>Only if snapshot is newer</SelectItem>
|
||||||
|
<SelectItem value={OVERWRITE_MODES.never}>Never overwrite</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{overwriteMode === OVERWRITE_MODES.always &&
|
||||||
|
"Existing files will always be replaced with the snapshot version."}
|
||||||
|
{overwriteMode === OVERWRITE_MODES.ifChanged &&
|
||||||
|
"Files are only replaced if their content differs from the snapshot."}
|
||||||
|
{overwriteMode === OVERWRITE_MODES.ifNewer &&
|
||||||
|
"Files are only replaced if the snapshot version has a newer modification time."}
|
||||||
|
{overwriteMode === OVERWRITE_MODES.never &&
|
||||||
|
"Existing files will never be replaced, only missing files are restored."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -240,32 +323,34 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||||
className="h-auto p-0 text-sm font-normal"
|
className="h-auto p-0 text-sm font-normal"
|
||||||
>
|
>
|
||||||
Advanced
|
Advanced Options
|
||||||
<ChevronDown size={16} className={`ml-1 transition-transform ${showAdvanced ? "rotate-180" : ""}`} />
|
<ChevronDown size={16} className={`ml-1 transition-transform ${showAdvanced ? "rotate-180" : ""}`} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{showAdvanced && (
|
{showAdvanced && (
|
||||||
<div className="mt-4 space-y-2">
|
<div className="mt-4 space-y-3">
|
||||||
<Label htmlFor="exclude-xattr" className="text-sm">
|
<div className="space-y-2">
|
||||||
Exclude Extended Attributes (Optional)
|
<Label htmlFor="exclude-xattr" className="text-sm">
|
||||||
</Label>
|
Exclude Extended Attributes
|
||||||
<Input
|
</Label>
|
||||||
id="exclude-xattr"
|
<Input
|
||||||
placeholder="com.apple.metadata,user.*,nfs4.*"
|
id="exclude-xattr"
|
||||||
value={excludeXattr}
|
placeholder="com.apple.metadata,user.*,nfs4.*"
|
||||||
onChange={(e) => setExcludeXattr(e.target.value)}
|
value={excludeXattr}
|
||||||
/>
|
onChange={(e) => setExcludeXattr(e.target.value)}
|
||||||
<p className="text-xs text-muted-foreground">
|
/>
|
||||||
Exclude specific extended attributes during restore (comma-separated)
|
<p className="text-xs text-muted-foreground">
|
||||||
</p>
|
Exclude specific extended attributes during restore (comma-separated)
|
||||||
<div className="flex items-center space-x-2 mt-2">
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="delete-extra"
|
id="delete-extra"
|
||||||
checked={deleteExtraFiles}
|
checked={deleteExtraFiles}
|
||||||
onCheckedChange={(checked) => setDeleteExtraFiles(checked === true)}
|
onCheckedChange={(checked) => setDeleteExtraFiles(checked === true)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="delete-extra" className="text-sm font-normal cursor-pointer">
|
<Label htmlFor="delete-extra" className="text-sm font-normal cursor-pointer">
|
||||||
Delete files not present in the snapshot?
|
Delete files not present in the snapshot
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -274,7 +359,12 @@ export const SnapshotFileBrowser = (props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
<AlertDialogAction onClick={handleConfirmRestore}>Confirm</AlertDialogAction>
|
<AlertDialogAction
|
||||||
|
onClick={handleConfirmRestore}
|
||||||
|
disabled={restoreLocation === "custom" && !customTargetPath.trim()}
|
||||||
|
>
|
||||||
|
Confirm Restore
|
||||||
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ import type { Repository } from "~/client/lib/types";
|
|||||||
import { slugify } from "~/client/lib/utils";
|
import { slugify } from "~/client/lib/utils";
|
||||||
import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { updateRepositoryMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import type { UpdateRepositoryResponse } from "~/client/api-client/types.gen";
|
import type { UpdateRepositoryResponse } from "~/client/api-client/types.gen";
|
||||||
|
import type { CompressionMode } from "~/schemas/restic";
|
||||||
type CompressionMode = "off" | "auto" | "fastest" | "better" | "max";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
|
|||||||
@@ -105,3 +105,12 @@ export const REPOSITORY_STATUS = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type RepositoryStatus = keyof typeof REPOSITORY_STATUS;
|
export type RepositoryStatus = keyof typeof REPOSITORY_STATUS;
|
||||||
|
|
||||||
|
export const OVERWRITE_MODES = {
|
||||||
|
always: "always",
|
||||||
|
ifChanged: "if-changed",
|
||||||
|
ifNewer: "if-newer",
|
||||||
|
never: "never",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type OverwriteMode = (typeof OVERWRITE_MODES)[keyof typeof OVERWRITE_MODES];
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import { describeRoute, resolver } from "hono-openapi";
|
import { describeRoute, resolver } from "hono-openapi";
|
||||||
import { COMPRESSION_MODES, REPOSITORY_BACKENDS, REPOSITORY_STATUS, repositoryConfigSchema } from "~/schemas/restic";
|
import {
|
||||||
|
COMPRESSION_MODES,
|
||||||
|
OVERWRITE_MODES,
|
||||||
|
REPOSITORY_BACKENDS,
|
||||||
|
REPOSITORY_STATUS,
|
||||||
|
repositoryConfigSchema,
|
||||||
|
} from "~/schemas/restic";
|
||||||
|
|
||||||
export const repositorySchema = type({
|
export const repositorySchema = type({
|
||||||
id: "string",
|
id: "string",
|
||||||
@@ -269,12 +275,16 @@ export const listSnapshotFilesDto = describeRoute({
|
|||||||
/**
|
/**
|
||||||
* Restore a snapshot
|
* Restore a snapshot
|
||||||
*/
|
*/
|
||||||
|
export const overwriteModeSchema = type.valueOf(OVERWRITE_MODES);
|
||||||
|
|
||||||
export const restoreSnapshotBody = type({
|
export const restoreSnapshotBody = type({
|
||||||
snapshotId: "string",
|
snapshotId: "string",
|
||||||
include: "string[]?",
|
include: "string[]?",
|
||||||
exclude: "string[]?",
|
exclude: "string[]?",
|
||||||
excludeXattr: "string[]?",
|
excludeXattr: "string[]?",
|
||||||
delete: "boolean?",
|
delete: "boolean?",
|
||||||
|
targetPath: "string?",
|
||||||
|
overwrite: overwriteModeSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RestoreSnapshotBody = typeof restoreSnapshotBody.infer;
|
export type RestoreSnapshotBody = typeof restoreSnapshotBody.infer;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { toMessage } from "../../utils/errors";
|
|||||||
import { generateShortId } from "../../utils/id";
|
import { generateShortId } from "../../utils/id";
|
||||||
import { restic } from "../../utils/restic";
|
import { restic } from "../../utils/restic";
|
||||||
import { cryptoUtils } from "../../utils/crypto";
|
import { cryptoUtils } from "../../utils/crypto";
|
||||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
import type { CompressionMode, OverwriteMode, RepositoryConfig } from "~/schemas/restic";
|
||||||
|
|
||||||
const listRepositories = async () => {
|
const listRepositories = async () => {
|
||||||
const repositories = await db.query.repositoriesTable.findMany({});
|
const repositories = await db.query.repositoriesTable.findMany({});
|
||||||
@@ -201,7 +201,14 @@ const listSnapshotFiles = async (name: string, snapshotId: string, path?: string
|
|||||||
const restoreSnapshot = async (
|
const restoreSnapshot = async (
|
||||||
name: string,
|
name: string,
|
||||||
snapshotId: string,
|
snapshotId: string,
|
||||||
options?: { include?: string[]; exclude?: string[]; excludeXattr?: string[]; delete?: boolean },
|
options?: {
|
||||||
|
include?: string[];
|
||||||
|
exclude?: string[];
|
||||||
|
excludeXattr?: string[];
|
||||||
|
delete?: boolean;
|
||||||
|
targetPath?: string;
|
||||||
|
overwrite?: OverwriteMode;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const repository = await db.query.repositoriesTable.findFirst({
|
const repository = await db.query.repositoriesTable.findFirst({
|
||||||
where: eq(repositoriesTable.name, name),
|
where: eq(repositoriesTable.name, name),
|
||||||
@@ -211,7 +218,9 @@ const restoreSnapshot = async (
|
|||||||
throw new NotFoundError("Repository not found");
|
throw new NotFoundError("Repository not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await restic.restore(repository.config, snapshotId, "/", options);
|
const target = options?.targetPath || "/";
|
||||||
|
|
||||||
|
const result = await restic.restore(repository.config, snapshotId, target, options);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { logger } from "./logger";
|
|||||||
import { cryptoUtils } from "./crypto";
|
import { cryptoUtils } from "./crypto";
|
||||||
import type { RetentionPolicy } from "../modules/backups/backups.dto";
|
import type { RetentionPolicy } from "../modules/backups/backups.dto";
|
||||||
import { safeSpawn } from "./spawn";
|
import { safeSpawn } from "./spawn";
|
||||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
import type { CompressionMode, RepositoryConfig, OverwriteMode } from "~/schemas/restic";
|
||||||
import { ResticError } from "./errors";
|
import { ResticError } from "./errors";
|
||||||
|
|
||||||
const backupOutputSchema = type({
|
const backupOutputSchema = type({
|
||||||
@@ -353,7 +353,7 @@ const backup = async (
|
|||||||
|
|
||||||
const restoreOutputSchema = type({
|
const restoreOutputSchema = type({
|
||||||
message_type: "'summary'",
|
message_type: "'summary'",
|
||||||
total_files: "number",
|
total_files: "number?",
|
||||||
files_restored: "number",
|
files_restored: "number",
|
||||||
files_skipped: "number",
|
files_skipped: "number",
|
||||||
total_bytes: "number?",
|
total_bytes: "number?",
|
||||||
@@ -369,8 +369,8 @@ const restore = async (
|
|||||||
include?: string[];
|
include?: string[];
|
||||||
exclude?: string[];
|
exclude?: string[];
|
||||||
excludeXattr?: string[];
|
excludeXattr?: string[];
|
||||||
path?: string;
|
|
||||||
delete?: boolean;
|
delete?: boolean;
|
||||||
|
overwrite?: OverwriteMode;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const repoUrl = buildRepoUrl(config);
|
const repoUrl = buildRepoUrl(config);
|
||||||
@@ -378,8 +378,8 @@ const restore = async (
|
|||||||
|
|
||||||
const args: string[] = ["--repo", repoUrl, "restore", snapshotId, "--target", target];
|
const args: string[] = ["--repo", repoUrl, "restore", snapshotId, "--target", target];
|
||||||
|
|
||||||
if (options?.path) {
|
if (options?.overwrite) {
|
||||||
args[args.length - 4] = `${snapshotId}:${options.path}`;
|
args.push("--overwrite", options.overwrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.delete) {
|
if (options?.delete) {
|
||||||
@@ -407,6 +407,7 @@ const restore = async (
|
|||||||
addRepoSpecificArgs(args, config, env);
|
addRepoSpecificArgs(args, config, env);
|
||||||
args.push("--json");
|
args.push("--json");
|
||||||
|
|
||||||
|
logger.debug(`Executing: restic ${args.join(" ")}`);
|
||||||
const res = await $`restic ${args}`.env(env).nothrow();
|
const res = await $`restic ${args}`.env(env).nothrow();
|
||||||
await cleanupTemporaryKeys(config, env);
|
await cleanupTemporaryKeys(config, env);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
* This removes passwords and credentials from logs and error messages
|
* This removes passwords and credentials from logs and error messages
|
||||||
*/
|
*/
|
||||||
export const sanitizeSensitiveData = (text: string): string => {
|
export const sanitizeSensitiveData = (text: string): string => {
|
||||||
|
if (process.env.NODE_ENV === "development") {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
let sanitized = text.replace(/\b(pass|password)=([^\s,]+)/gi, "$1=***");
|
let sanitized = text.replace(/\b(pass|password)=([^\s,]+)/gi, "$1=***");
|
||||||
|
|
||||||
sanitized = sanitized.replace(/\/\/([^:@\s]+):([^@\s]+)@/g, "//$1:***@");
|
sanitized = sanitized.replace(/\/\/([^:@\s]+):([^@\s]+)@/g, "//$1:***@");
|
||||||
|
|||||||
Reference in New Issue
Block a user