diff --git a/.gitignore b/.gitignore index f5ebc411..1e5f011a 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,4 @@ playwright/temp openapi-ts-error-*.log .output tmp/ +qa-output diff --git a/AGENTS.md b/AGENTS.md index a1ce055b..4b4cc050 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx b/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx index 3a788cc5..21ae1c05 100644 --- a/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx +++ b/app/client/modules/repositories/components/repository-forms/local-repository-form.tsx @@ -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 ( {
{field.value || constants.REPOSITORY_BASE} + {!isExistingRepository && /{"{unique-id}"}}
- The directory where the repository will be stored. + + {isExistingRepository + ? "The exact path to your existing repository." + : "A unique subdirectory will be created inside this directory to store the repository."} + diff --git a/app/server/modules/repositories/__tests__/repositories.service.test.ts b/app/server/modules/repositories/__tests__/repositories.service.test.ts index c7cdbbe3..9f88e72e 100644 --- a/app/server/modules/repositories/__tests__/repositories.service.test.ts +++ b/app/server/modules/repositories/__tests__/repositories.service.test.ts @@ -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; + 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; expect(savedConfig.path).toBe(explicitPath); expect(created.status).toBe("healthy"); diff --git a/app/server/modules/repositories/repositories.service.ts b/app/server/modules/repositories/repositories.service.ts index 9d0e7642..c63caf40 100644 --- a/app/server/modules/repositories/repositories.service.ts +++ b/app/server/modules/repositories/repositories.service.ts @@ -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);