refactor: make lock errors cleaner and show unlock button (#493)
* refactor: make lock errors cleaner and show unlock button * chore: pr feedbacks
This commit is contained in:
parent
cb7988b8ed
commit
b45d36e06a
13 changed files with 226 additions and 30 deletions
|
|
@ -62,6 +62,7 @@ import {
|
||||||
tagSnapshots,
|
tagSnapshots,
|
||||||
testConnection,
|
testConnection,
|
||||||
testNotificationDestination,
|
testNotificationDestination,
|
||||||
|
unlockRepository,
|
||||||
unmountVolume,
|
unmountVolume,
|
||||||
updateBackupSchedule,
|
updateBackupSchedule,
|
||||||
updateNotificationDestination,
|
updateNotificationDestination,
|
||||||
|
|
@ -171,6 +172,8 @@ import type {
|
||||||
TestConnectionResponse,
|
TestConnectionResponse,
|
||||||
TestNotificationDestinationData,
|
TestNotificationDestinationData,
|
||||||
TestNotificationDestinationResponse,
|
TestNotificationDestinationResponse,
|
||||||
|
UnlockRepositoryData,
|
||||||
|
UnlockRepositoryResponse,
|
||||||
UnmountVolumeData,
|
UnmountVolumeData,
|
||||||
UnmountVolumeResponse,
|
UnmountVolumeResponse,
|
||||||
UpdateBackupScheduleData,
|
UpdateBackupScheduleData,
|
||||||
|
|
@ -905,6 +908,25 @@ export const startDoctorMutation = (
|
||||||
return mutationOptions;
|
return mutationOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock a repository by removing all stale locks
|
||||||
|
*/
|
||||||
|
export const unlockRepositoryMutation = (
|
||||||
|
options?: Partial<Options<UnlockRepositoryData>>,
|
||||||
|
): UseMutationOptions<UnlockRepositoryResponse, DefaultError, Options<UnlockRepositoryData>> => {
|
||||||
|
const mutationOptions: UseMutationOptions<UnlockRepositoryResponse, DefaultError, Options<UnlockRepositoryData>> = {
|
||||||
|
mutationFn: async (fnOptions) => {
|
||||||
|
const { data } = await unlockRepository({
|
||||||
|
...options,
|
||||||
|
...fnOptions,
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return mutationOptions;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag multiple snapshots in a repository
|
* Tag multiple snapshots in a repository
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ export {
|
||||||
tagSnapshots,
|
tagSnapshots,
|
||||||
testConnection,
|
testConnection,
|
||||||
testNotificationDestination,
|
testNotificationDestination,
|
||||||
|
unlockRepository,
|
||||||
unmountVolume,
|
unmountVolume,
|
||||||
updateBackupSchedule,
|
updateBackupSchedule,
|
||||||
updateNotificationDestination,
|
updateNotificationDestination,
|
||||||
|
|
@ -222,6 +223,9 @@ export type {
|
||||||
TestNotificationDestinationErrors,
|
TestNotificationDestinationErrors,
|
||||||
TestNotificationDestinationResponse,
|
TestNotificationDestinationResponse,
|
||||||
TestNotificationDestinationResponses,
|
TestNotificationDestinationResponses,
|
||||||
|
UnlockRepositoryData,
|
||||||
|
UnlockRepositoryResponse,
|
||||||
|
UnlockRepositoryResponses,
|
||||||
UnmountVolumeData,
|
UnmountVolumeData,
|
||||||
UnmountVolumeResponse,
|
UnmountVolumeResponse,
|
||||||
UnmountVolumeResponses,
|
UnmountVolumeResponses,
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,8 @@ import type {
|
||||||
TestNotificationDestinationData,
|
TestNotificationDestinationData,
|
||||||
TestNotificationDestinationErrors,
|
TestNotificationDestinationErrors,
|
||||||
TestNotificationDestinationResponses,
|
TestNotificationDestinationResponses,
|
||||||
|
UnlockRepositoryData,
|
||||||
|
UnlockRepositoryResponses,
|
||||||
UnmountVolumeData,
|
UnmountVolumeData,
|
||||||
UnmountVolumeResponses,
|
UnmountVolumeResponses,
|
||||||
UpdateBackupScheduleData,
|
UpdateBackupScheduleData,
|
||||||
|
|
@ -462,6 +464,17 @@ export const startDoctor = <ThrowOnError extends boolean = false>(options: Optio
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock a repository by removing all stale locks
|
||||||
|
*/
|
||||||
|
export const unlockRepository = <ThrowOnError extends boolean = false>(
|
||||||
|
options: Options<UnlockRepositoryData, ThrowOnError>,
|
||||||
|
) =>
|
||||||
|
(options.client ?? client).post<UnlockRepositoryResponses, unknown, ThrowOnError>({
|
||||||
|
url: "/api/v1/repositories/{id}/unlock",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tag multiple snapshots in a repository
|
* Tag multiple snapshots in a repository
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1856,6 +1856,27 @@ export type StartDoctorResponses = {
|
||||||
|
|
||||||
export type StartDoctorResponse = StartDoctorResponses[keyof StartDoctorResponses];
|
export type StartDoctorResponse = StartDoctorResponses[keyof StartDoctorResponses];
|
||||||
|
|
||||||
|
export type UnlockRepositoryData = {
|
||||||
|
body?: never;
|
||||||
|
path: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: "/api/v1/repositories/{id}/unlock";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnlockRepositoryResponses = {
|
||||||
|
/**
|
||||||
|
* Repository unlocked successfully
|
||||||
|
*/
|
||||||
|
200: {
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnlockRepositoryResponse = UnlockRepositoryResponses[keyof UnlockRepositoryResponses];
|
||||||
|
|
||||||
export type TagSnapshotsData = {
|
export type TagSnapshotsData = {
|
||||||
body?: {
|
body?: {
|
||||||
snapshotIds: Array<string>;
|
snapshotIds: Array<string>;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { listSnapshotFilesOptions, restoreSnapshotMutation } from "~/client/api-
|
||||||
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
import { useFileBrowser } from "~/client/hooks/use-file-browser";
|
||||||
import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic";
|
import { OVERWRITE_MODES, type OverwriteMode } from "~/schemas/restic";
|
||||||
import type { Repository, Snapshot } from "~/client/lib/types";
|
import type { Repository, Snapshot } from "~/client/lib/types";
|
||||||
|
import { handleRepositoryError } from "~/client/lib/errors";
|
||||||
|
|
||||||
type RestoreLocation = "original" | "custom";
|
type RestoreLocation = "original" | "custom";
|
||||||
|
|
||||||
|
|
@ -103,7 +104,7 @@ export function RestoreForm({ snapshot, repository, snapshotId, returnPath }: Re
|
||||||
void navigate(returnPath);
|
void navigate(returnPath);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error("Restore failed", { description: error.message || "Failed to restore snapshot" });
|
handleRepositoryError("Restore failed", error, repository.id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
export const parseError = (error?: unknown) => {
|
|
||||||
if (error && typeof error === "object" && "message" in error) {
|
|
||||||
return { message: error.message as string };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === "string") {
|
|
||||||
return { message: error };
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
71
app/client/lib/errors.tsx
Normal file
71
app/client/lib/errors.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { unlockRepository } from "~/client/api-client/sdk.gen";
|
||||||
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
import { Unlock } from "lucide-react";
|
||||||
|
|
||||||
|
export const isLockError = (error: unknown): boolean => {
|
||||||
|
const errorMessage = parseError(error)?.message || "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
errorMessage.toLowerCase().includes("unable to create lock") ||
|
||||||
|
errorMessage.toLowerCase().includes("repository is already locked") ||
|
||||||
|
errorMessage.toLowerCase().includes("failed to lock repository")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseError = (error?: unknown) => {
|
||||||
|
if (error && typeof error === "object" && "message" in error) {
|
||||||
|
return { message: error.message as string };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === "string") {
|
||||||
|
return { message: error };
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const showLockErrorToast = (repositoryId: string, title: string) => {
|
||||||
|
toast.error(title, {
|
||||||
|
description:
|
||||||
|
"The repository is currently locked by another operation. This can happen when a previous operation didn't complete properly.",
|
||||||
|
duration: 5000,
|
||||||
|
action: (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
toast.dismiss();
|
||||||
|
toast.promise(
|
||||||
|
async () => {
|
||||||
|
const result = await unlockRepository({
|
||||||
|
path: { id: repositoryId },
|
||||||
|
throwOnError: true,
|
||||||
|
});
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loading: "Unlocking repository...",
|
||||||
|
success: "Repository unlocked successfully! You can now retry your operation.",
|
||||||
|
error: (err) => parseError(err)?.message || "Failed to unlock repository",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Unlock className="h-3 w-3 mr-1" />
|
||||||
|
Unlock
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleRepositoryError = (title: string, error: unknown, repositoryId: string) => {
|
||||||
|
if (isLockError(error)) {
|
||||||
|
showLockErrorToast(repositoryId, title);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.error(parseError(error)?.message || "An unexpected error occurred");
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
@ -17,7 +17,7 @@ import { BackupProgressCard } from "./backup-progress-card";
|
||||||
import { runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
import { runForgetMutation } from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { handleRepositoryError } from "~/client/lib/errors";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
import { formatShortDateTime, formatTimeAgo } from "~/client/lib/datetime";
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ export const ScheduleSummary = (props: Props) => {
|
||||||
toast.success("Retention policy applied successfully");
|
toast.success("Retention policy applied successfully");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to apply retention policy", { description: parseError(error)?.message });
|
handleRepositoryError("Failed to apply retention policy", error, schedule.repository.shortId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import {
|
||||||
stopBackupMutation,
|
stopBackupMutation,
|
||||||
deleteSnapshotMutation,
|
deleteSnapshotMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import { parseError } from "~/client/lib/errors";
|
import { parseError, handleRepositoryError } from "~/client/lib/errors";
|
||||||
import { getCronExpression } from "~/utils/utils";
|
import { getCronExpression } from "~/utils/utils";
|
||||||
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
|
import { CreateScheduleForm, type BackupScheduleFormValues } from "../components/create-schedule-form";
|
||||||
import { ScheduleSummary } from "../components/schedule-summary";
|
import { ScheduleSummary } from "../components/schedule-summary";
|
||||||
|
|
@ -118,7 +118,7 @@ export default function ScheduleDetailsPage({ params, loaderData }: Route.Compon
|
||||||
toast.success("Backup started successfully");
|
toast.success("Backup started successfully");
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error("Failed to start backup", { description: parseError(error)?.message });
|
handleRepositoryError("Failed to start backup", error, schedule.repository.shortId);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Check, Save, Square, Stethoscope, Trash2 } from "lucide-react";
|
import { Check, Save, Square, Stethoscope, Trash2, Unlock } from "lucide-react";
|
||||||
import { Card } from "~/client/components/ui/card";
|
import { Card } from "~/client/components/ui/card";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
import { Input } from "~/client/components/ui/input";
|
import { Input } from "~/client/components/ui/input";
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
cancelDoctorMutation,
|
cancelDoctorMutation,
|
||||||
deleteRepositoryMutation,
|
deleteRepositoryMutation,
|
||||||
startDoctorMutation,
|
startDoctorMutation,
|
||||||
|
unlockRepositoryMutation,
|
||||||
updateRepositoryMutation,
|
updateRepositoryMutation,
|
||||||
} from "~/client/api-client/@tanstack/react-query.gen";
|
} from "~/client/api-client/@tanstack/react-query.gen";
|
||||||
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
import type { CompressionMode, RepositoryConfig } from "~/schemas/restic";
|
||||||
|
|
@ -102,6 +103,18 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unlockRepo = useMutation({
|
||||||
|
...unlockRepositoryMutation(),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Repository unlocked successfully");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error("Failed to unlock repository", {
|
||||||
|
description: parseError(error)?.message,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setShowConfirmDialog(true);
|
setShowConfirmDialog(true);
|
||||||
|
|
@ -132,7 +145,7 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
||||||
<div>
|
<div>
|
||||||
<span className="text-lg font-semibold mb-4">Repository Settings</span>
|
<span className="text-lg font-semibold mb-4">Repository Settings</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex flex-wrap justify-end gap-2 sm:gap-4">
|
||||||
{repository.status === "doctor" ? (
|
{repository.status === "doctor" ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -144,21 +157,30 @@ export const RepositoryInfoTabContent = ({ repository }: Props) => {
|
||||||
<span>Cancel doctor</span>
|
<span>Cancel doctor</span>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => startDoctor.mutate({ path: { id: repository.id } })}
|
|
||||||
disabled={startDoctor.isPending}
|
|
||||||
>
|
|
||||||
<Stethoscope className="h-4 w-4 mr-2" />
|
|
||||||
Run doctor
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
onClick={() => startDoctor.mutate({ path: { id: repository.id } })}
|
||||||
onClick={() => setShowDeleteConfirm(true)}
|
disabled={startDoctor.isPending}
|
||||||
disabled={deleteRepo.isPending}
|
|
||||||
>
|
>
|
||||||
|
<Stethoscope className="h-4 w-4 mr-2" />
|
||||||
|
Run doctor
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => unlockRepo.mutate({ path: { id: repository.id } })}
|
||||||
|
loading={unlockRepo.isPending}
|
||||||
|
>
|
||||||
|
<Unlock className="h-4 w-4 mr-2" />
|
||||||
|
Unlock
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={() => setShowDeleteConfirm(true)}
|
||||||
|
disabled={deleteRepo.isPending}
|
||||||
|
>
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import {
|
||||||
updateRepositoryDto,
|
updateRepositoryDto,
|
||||||
devPanelExecBody,
|
devPanelExecBody,
|
||||||
devPanelExecDto,
|
devPanelExecDto,
|
||||||
|
unlockRepositoryDto,
|
||||||
type DeleteRepositoryDto,
|
type DeleteRepositoryDto,
|
||||||
type DeleteSnapshotDto,
|
type DeleteSnapshotDto,
|
||||||
type DeleteSnapshotsResponseDto,
|
type DeleteSnapshotsResponseDto,
|
||||||
|
|
@ -41,6 +42,7 @@ import {
|
||||||
type RestoreSnapshotDto,
|
type RestoreSnapshotDto,
|
||||||
type TagSnapshotsResponseDto,
|
type TagSnapshotsResponseDto,
|
||||||
type UpdateRepositoryDto,
|
type UpdateRepositoryDto,
|
||||||
|
type UnlockRepositoryDto,
|
||||||
} from "./repositories.dto";
|
} from "./repositories.dto";
|
||||||
import { repositoriesService } from "./repositories.service";
|
import { repositoriesService } from "./repositories.service";
|
||||||
import { getRcloneRemoteInfo, listRcloneRemotes } from "../../utils/rclone";
|
import { getRcloneRemoteInfo, listRcloneRemotes } from "../../utils/rclone";
|
||||||
|
|
@ -189,6 +191,13 @@ export const repositoriesController = new Hono()
|
||||||
|
|
||||||
return c.json<CancelDoctorDto>(result, 200);
|
return c.json<CancelDoctorDto>(result, 200);
|
||||||
})
|
})
|
||||||
|
.post("/:id/unlock", unlockRepositoryDto, async (c) => {
|
||||||
|
const { id } = c.req.param();
|
||||||
|
|
||||||
|
const result = await repositoriesService.unlockRepository(id);
|
||||||
|
|
||||||
|
return c.json<UnlockRepositoryDto>(result, 200);
|
||||||
|
})
|
||||||
.delete("/:id/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
|
.delete("/:id/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
|
||||||
const { id, snapshotId } = c.req.param();
|
const { id, snapshotId } = c.req.param();
|
||||||
await repositoriesService.deleteSnapshot(id, snapshotId);
|
await repositoriesService.deleteSnapshot(id, snapshotId);
|
||||||
|
|
|
||||||
|
|
@ -546,3 +546,29 @@ export const devPanelExecDto = describeRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlock repository
|
||||||
|
*/
|
||||||
|
export const unlockRepositoryResponse = type({
|
||||||
|
success: "boolean",
|
||||||
|
message: "string",
|
||||||
|
});
|
||||||
|
|
||||||
|
export type UnlockRepositoryDto = typeof unlockRepositoryResponse.infer;
|
||||||
|
|
||||||
|
export const unlockRepositoryDto = describeRoute({
|
||||||
|
description: "Unlock a repository by removing all stale locks",
|
||||||
|
tags: ["Repositories"],
|
||||||
|
operationId: "unlockRepository",
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: "Repository unlocked successfully",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: resolver(unlockRepositoryResponse),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -563,6 +563,23 @@ const updateRepository = async (id: string, updates: { name?: string; compressio
|
||||||
return { repository: updated };
|
return { repository: updated };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unlockRepository = async (id: string) => {
|
||||||
|
const organizationId = getOrganizationId();
|
||||||
|
const repository = await findRepository(id);
|
||||||
|
|
||||||
|
if (!repository) {
|
||||||
|
throw new NotFoundError("Repository not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseLock = await repoMutex.acquireExclusive(repository.id, "unlock");
|
||||||
|
try {
|
||||||
|
const result = await restic.unlock(repository.config, { organizationId });
|
||||||
|
return result;
|
||||||
|
} finally {
|
||||||
|
releaseLock();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const execResticCommand = async (
|
const execResticCommand = async (
|
||||||
id: string,
|
id: string,
|
||||||
command: string,
|
command: string,
|
||||||
|
|
@ -657,4 +674,5 @@ export const repositoriesService = {
|
||||||
refreshSnapshots,
|
refreshSnapshots,
|
||||||
execResticCommand,
|
execResticCommand,
|
||||||
getRetentionCategories,
|
getRetentionCategories,
|
||||||
|
unlockRepository,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue