mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2026-03-14 06:23:01 +03:00
fix(nix): ensure absolute bundle path in pre-sync backup (#204)
* fix(nix): ensure absolute bundle path in pre-sync backup (#203) Use path.resolve() instead of conditional path.isAbsolute() check to guarantee bundlePath is always absolute before passing to git -C. On NixOS, relative paths were interpreted relative to the temp mirror clone directory, causing "No such file or directory" errors. Closes #203 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(nix): ensure absolute bundle path in pre-sync backup (#203) Use path.resolve() instead of conditional path.isAbsolute() check to guarantee bundlePath is always absolute before passing to git -C. On NixOS, relative paths were interpreted relative to the temp mirror clone directory, causing "No such file or directory" errors. Extract resolveBackupPaths() for testability. Bump version to 3.10.1. Closes #203 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: drop macos matrix and only run nix build on main/tags - Remove macos-latest from Nix CI matrix (ubuntu-only) - Only run `nix build` on main branch and version tags, skip on PRs - `nix flake check` still runs on all PRs for validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
6
.github/workflows/nix-build.yml
vendored
6
.github/workflows/nix-build.yml
vendored
@@ -13,10 +13,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
strategy:
|
runs-on: ubuntu-latest
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -34,4 +31,5 @@ jobs:
|
|||||||
run: nix flake show
|
run: nix flake show
|
||||||
|
|
||||||
- name: Build package
|
- name: Build package
|
||||||
|
if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
|
||||||
run: nix build --print-build-logs
|
run: nix build --print-build-logs
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "gitea-mirror",
|
"name": "gitea-mirror",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.10.0",
|
"version": "3.10.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.2.9"
|
"bun": ">=1.2.9"
|
||||||
},
|
},
|
||||||
|
|||||||
115
src/lib/repo-backup.test.ts
Normal file
115
src/lib/repo-backup.test.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||||
|
import type { Config } from "@/types/config";
|
||||||
|
import { resolveBackupPaths } from "@/lib/repo-backup";
|
||||||
|
|
||||||
|
describe("resolveBackupPaths", () => {
|
||||||
|
let originalBackupDirEnv: string | undefined;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalBackupDirEnv = process.env.PRE_SYNC_BACKUP_DIR;
|
||||||
|
delete process.env.PRE_SYNC_BACKUP_DIR;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalBackupDirEnv === undefined) {
|
||||||
|
delete process.env.PRE_SYNC_BACKUP_DIR;
|
||||||
|
} else {
|
||||||
|
process.env.PRE_SYNC_BACKUP_DIR = originalBackupDirEnv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns absolute paths when backupDirectory is relative", () => {
|
||||||
|
const config: Partial<Config> = {
|
||||||
|
userId: "user-123",
|
||||||
|
giteaConfig: {
|
||||||
|
backupDirectory: "data/repo-backups",
|
||||||
|
} as Config["giteaConfig"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { backupRoot, repoBackupDir } = resolveBackupPaths({
|
||||||
|
config,
|
||||||
|
owner: "RayLabsHQ",
|
||||||
|
repoName: "gitea-mirror",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(path.isAbsolute(backupRoot)).toBe(true);
|
||||||
|
expect(path.isAbsolute(repoBackupDir)).toBe(true);
|
||||||
|
expect(repoBackupDir).toBe(
|
||||||
|
path.join(backupRoot, "user-123", "RayLabsHQ", "gitea-mirror")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("returns absolute paths when backupDirectory is already absolute", () => {
|
||||||
|
const config: Partial<Config> = {
|
||||||
|
userId: "user-123",
|
||||||
|
giteaConfig: {
|
||||||
|
backupDirectory: "/data/repo-backups",
|
||||||
|
} as Config["giteaConfig"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { backupRoot, repoBackupDir } = resolveBackupPaths({
|
||||||
|
config,
|
||||||
|
owner: "owner",
|
||||||
|
repoName: "repo",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(backupRoot).toBe("/data/repo-backups");
|
||||||
|
expect(path.isAbsolute(repoBackupDir)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("falls back to cwd-based path when no backupDirectory is set", () => {
|
||||||
|
const config: Partial<Config> = {
|
||||||
|
userId: "user-123",
|
||||||
|
giteaConfig: {} as Config["giteaConfig"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { backupRoot } = resolveBackupPaths({
|
||||||
|
config,
|
||||||
|
owner: "owner",
|
||||||
|
repoName: "repo",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(path.isAbsolute(backupRoot)).toBe(true);
|
||||||
|
expect(backupRoot).toBe(
|
||||||
|
path.resolve(process.cwd(), "data", "repo-backups")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("uses PRE_SYNC_BACKUP_DIR env var when config has no backupDirectory", () => {
|
||||||
|
process.env.PRE_SYNC_BACKUP_DIR = "custom/backup/path";
|
||||||
|
|
||||||
|
const config: Partial<Config> = {
|
||||||
|
userId: "user-123",
|
||||||
|
giteaConfig: {} as Config["giteaConfig"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { backupRoot } = resolveBackupPaths({
|
||||||
|
config,
|
||||||
|
owner: "owner",
|
||||||
|
repoName: "repo",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(path.isAbsolute(backupRoot)).toBe(true);
|
||||||
|
expect(backupRoot).toBe(path.resolve("custom/backup/path"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sanitizes owner and repoName in path segments", () => {
|
||||||
|
const config: Partial<Config> = {
|
||||||
|
userId: "user-123",
|
||||||
|
giteaConfig: {
|
||||||
|
backupDirectory: "/backups",
|
||||||
|
} as Config["giteaConfig"],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { repoBackupDir } = resolveBackupPaths({
|
||||||
|
config,
|
||||||
|
owner: "org/with-slash",
|
||||||
|
repoName: "repo name!",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(repoBackupDir).toBe(
|
||||||
|
path.join("/backups", "user-123", "org_with-slash", "repo_name_")
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -101,6 +101,36 @@ export function shouldBlockSyncOnBackupFailure(config: Partial<Config>): boolean
|
|||||||
return configSetting === undefined ? true : Boolean(configSetting);
|
return configSetting === undefined ? true : Boolean(configSetting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveBackupPaths({
|
||||||
|
config,
|
||||||
|
owner,
|
||||||
|
repoName,
|
||||||
|
}: {
|
||||||
|
config: Partial<Config>;
|
||||||
|
owner: string;
|
||||||
|
repoName: string;
|
||||||
|
}): { backupRoot: string; repoBackupDir: string } {
|
||||||
|
let backupRoot =
|
||||||
|
config.giteaConfig?.backupDirectory?.trim() ||
|
||||||
|
process.env.PRE_SYNC_BACKUP_DIR?.trim() ||
|
||||||
|
path.join(process.cwd(), "data", "repo-backups");
|
||||||
|
|
||||||
|
// Ensure backupRoot is absolute - relative paths break git bundle creation
|
||||||
|
// because git runs with -C mirrorClonePath and interprets relative paths from there.
|
||||||
|
// Always use path.resolve() which guarantees an absolute path, rather than a
|
||||||
|
// conditional check that can miss edge cases (e.g., NixOS systemd services).
|
||||||
|
backupRoot = path.resolve(backupRoot);
|
||||||
|
|
||||||
|
const repoBackupDir = path.join(
|
||||||
|
backupRoot,
|
||||||
|
sanitizePathSegment(config.userId || "unknown-user"),
|
||||||
|
sanitizePathSegment(owner),
|
||||||
|
sanitizePathSegment(repoName)
|
||||||
|
);
|
||||||
|
|
||||||
|
return { backupRoot, repoBackupDir };
|
||||||
|
}
|
||||||
|
|
||||||
export async function createPreSyncBundleBackup({
|
export async function createPreSyncBundleBackup({
|
||||||
config,
|
config,
|
||||||
owner,
|
owner,
|
||||||
@@ -126,16 +156,7 @@ export async function createPreSyncBundleBackup({
|
|||||||
throw new Error("Decrypted Gitea token is required for pre-sync backup.");
|
throw new Error("Decrypted Gitea token is required for pre-sync backup.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let backupRoot =
|
const { repoBackupDir } = resolveBackupPaths({ config, owner, repoName });
|
||||||
config.giteaConfig?.backupDirectory?.trim() ||
|
|
||||||
process.env.PRE_SYNC_BACKUP_DIR?.trim() ||
|
|
||||||
path.join(process.cwd(), "data", "repo-backups");
|
|
||||||
|
|
||||||
// Ensure backupRoot is absolute - relative paths break git bundle creation
|
|
||||||
// because git runs with -C mirrorClonePath and interprets relative paths from there
|
|
||||||
if (!path.isAbsolute(backupRoot)) {
|
|
||||||
backupRoot = path.resolve(process.cwd(), backupRoot);
|
|
||||||
}
|
|
||||||
const retention = Math.max(
|
const retention = Math.max(
|
||||||
1,
|
1,
|
||||||
Number.isFinite(config.giteaConfig?.backupRetentionCount)
|
Number.isFinite(config.giteaConfig?.backupRetentionCount)
|
||||||
@@ -143,18 +164,13 @@ export async function createPreSyncBundleBackup({
|
|||||||
: parsePositiveInt(process.env.PRE_SYNC_BACKUP_KEEP_COUNT, 20)
|
: parsePositiveInt(process.env.PRE_SYNC_BACKUP_KEEP_COUNT, 20)
|
||||||
);
|
);
|
||||||
|
|
||||||
const repoBackupDir = path.join(
|
|
||||||
backupRoot,
|
|
||||||
sanitizePathSegment(config.userId || "unknown-user"),
|
|
||||||
sanitizePathSegment(owner),
|
|
||||||
sanitizePathSegment(repoName)
|
|
||||||
);
|
|
||||||
|
|
||||||
await mkdir(repoBackupDir, { recursive: true });
|
await mkdir(repoBackupDir, { recursive: true });
|
||||||
|
|
||||||
const tmpDir = await mkdtemp(path.join(os.tmpdir(), "gitea-mirror-backup-"));
|
const tmpDir = await mkdtemp(path.join(os.tmpdir(), "gitea-mirror-backup-"));
|
||||||
const mirrorClonePath = path.join(tmpDir, "repo.git");
|
const mirrorClonePath = path.join(tmpDir, "repo.git");
|
||||||
const bundlePath = path.join(repoBackupDir, `${buildTimestamp()}.bundle`);
|
// path.resolve guarantees an absolute path, critical because git -C changes
|
||||||
|
// the working directory and would misinterpret a relative bundlePath
|
||||||
|
const bundlePath = path.resolve(repoBackupDir, `${buildTimestamp()}.bundle`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authCloneUrl = buildAuthenticatedCloneUrl(cloneUrl, giteaToken);
|
const authCloneUrl = buildAuthenticatedCloneUrl(cloneUrl, giteaToken);
|
||||||
|
|||||||
Reference in New Issue
Block a user