fix(local-repo): automatically generate a subfolder for new local repos (#582)
Closes #562 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Improved repository setup with context-aware descriptions for new vs. existing repositories * Enhanced path handling: unique identifiers automatically appended for new repositories, exact paths preserved when importing existing ones * **Chores** * Updated documentation and build configuration <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
b1a0cdb195
commit
30f237dc1f
5 changed files with 45 additions and 5 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -38,3 +38,4 @@ playwright/temp
|
|||
openapi-ts-error-*.log
|
||||
.output
|
||||
tmp/
|
||||
qa-output
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
- Never create migration files manually. Always use the provided command to generate migrations
|
||||
- If you realize an automated migration is incorrect, make sure to remove all the associated entries from the `_journal.json` and the newly created files located in `app/drizzle/` before re-generating the migration
|
||||
- The dev server is running at http://localhost:3000. Username is `admin` and password is `password`
|
||||
- The repo is https://github.com/nicotsx/zerobyte
|
||||
|
||||
## Project Overview
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
import { useWatch } from "react-hook-form";
|
||||
import { Check, Pencil, X, AlertTriangle } from "lucide-react";
|
||||
import { Button } from "../../../../components/ui/button";
|
||||
import { FormItem, FormLabel, FormDescription, FormField, FormControl } from "../../../../components/ui/form";
|
||||
|
|
@ -33,6 +34,8 @@ export const LocalRepositoryForm = ({ form }: Props) => {
|
|||
queryFn: getConstants,
|
||||
});
|
||||
|
||||
const isExistingRepository = useWatch({ control: form.control, name: "isExistingRepository" });
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
|
|
@ -45,6 +48,7 @@ export const LocalRepositoryForm = ({ form }: Props) => {
|
|||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 text-sm font-mono bg-muted px-3 py-2 rounded-md border">
|
||||
{field.value || constants.REPOSITORY_BASE}
|
||||
{!isExistingRepository && <span className="text-muted-foreground">/{"{unique-id}"}</span>}
|
||||
</div>
|
||||
<Button type="button" variant="outline" onClick={() => setShowPathWarning(true)} size="sm">
|
||||
<Pencil className="h-4 w-4 mr-2" />
|
||||
|
|
@ -52,7 +56,11 @@ export const LocalRepositoryForm = ({ form }: Props) => {
|
|||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>The directory where the repository will be stored.</FormDescription>
|
||||
<FormDescription>
|
||||
{isExistingRepository
|
||||
? "The exact path to your existing repository."
|
||||
: "A unique subdirectory will be created inside this directory to store the repository."}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
|
||||
<AlertDialog open={showPathWarning} onOpenChange={setShowPathWarning}>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ describe("repositoriesService.createRepository", () => {
|
|||
expect(created.status).toBe("healthy");
|
||||
});
|
||||
|
||||
test("keeps an explicit local repository path unchanged", async () => {
|
||||
test("creates a shortId-scoped repository path when using a custom directory", async () => {
|
||||
// arrange
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const explicitPath = `${REPOSITORY_BASE}/custom-${randomUUID()}`;
|
||||
|
|
@ -98,6 +98,37 @@ describe("repositoriesService.createRepository", () => {
|
|||
throw new Error("Repository should be created");
|
||||
}
|
||||
|
||||
const savedConfig = created.config as Extract<RepositoryConfig, { backend: "local" }>;
|
||||
expect(savedConfig.path).toBe(`${explicitPath}/${created.shortId}`);
|
||||
expect(savedConfig.path).not.toBe(explicitPath);
|
||||
expect(created.status).toBe("healthy");
|
||||
});
|
||||
|
||||
test("keeps an explicit local repository path unchanged when importing existing repository", async () => {
|
||||
// arrange
|
||||
const { organizationId, user } = await createTestSession();
|
||||
const explicitPath = `${REPOSITORY_BASE}/custom-${randomUUID()}`;
|
||||
const config: RepositoryConfig = { backend: "local", path: explicitPath, isExistingRepository: true };
|
||||
|
||||
spyOn(restic, "snapshots").mockImplementation(() => Promise.resolve([]));
|
||||
|
||||
// act
|
||||
const result = await withContext({ organizationId, userId: user.id }, () =>
|
||||
repositoriesService.createRepository("existing repo", config),
|
||||
);
|
||||
|
||||
const created = await db.query.repositoriesTable.findFirst({
|
||||
where: {
|
||||
id: result.repository.id,
|
||||
},
|
||||
});
|
||||
|
||||
// assert
|
||||
expect(created).toBeTruthy();
|
||||
if (!created) {
|
||||
throw new Error("Repository should be created");
|
||||
}
|
||||
|
||||
const savedConfig = created.config as Extract<RepositoryConfig, { backend: "local" }>;
|
||||
expect(savedConfig.path).toBe(explicitPath);
|
||||
expect(created.status).toBe("healthy");
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import { addCommonArgs, buildEnv, buildRepoUrl, cleanupTemporaryKeys, restic } f
|
|||
import { safeSpawn } from "../../utils/spawn";
|
||||
import { backupsService } from "../backups/backups.service";
|
||||
import type { DumpPathKind, UpdateRepositoryBody } from "./repositories.dto";
|
||||
import { REPOSITORY_BASE } from "~/server/core/constants";
|
||||
import { findCommonAncestor } from "~/utils/common-ancestor";
|
||||
import { prepareSnapshotDump } from "./helpers/dump";
|
||||
import { executeDoctor } from "./helpers/doctor";
|
||||
|
|
@ -131,8 +130,8 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
|
|||
const id = crypto.randomUUID();
|
||||
const shortId = generateShortId();
|
||||
|
||||
if (config.backend === "local" && config.path === REPOSITORY_BASE) {
|
||||
config.path = `${REPOSITORY_BASE}/${shortId}`;
|
||||
if (config.backend === "local" && !config.isExistingRepository) {
|
||||
config.path = `${config.path}/${shortId}`;
|
||||
}
|
||||
|
||||
const encryptedConfig = await encryptConfig(config);
|
||||
|
|
|
|||
Loading…
Reference in a new issue