feat: restic pass file generation

This commit is contained in:
Nicolas Meienberger
2025-10-17 13:15:24 +02:00
parent 41756e087a
commit 8af0bac63b
7 changed files with 75 additions and 1 deletions

View File

@@ -2,7 +2,7 @@ ARG BUN_VERSION="1.3.0"
FROM oven/bun:${BUN_VERSION}-alpine AS runner_base
RUN apk add --no-cache davfs2=1.6.1-r2
RUN apk add --no-cache davfs2 restic
# ------------------------------
# DEVELOPMENT

View File

@@ -1,3 +1,4 @@
export const OPERATION_TIMEOUT = 5000;
export const VOLUME_MOUNT_BASE = "/var/lib/docker/volumes/ironmount";
export const DATABASE_URL = "/data/ironmount.db";
export const RESTIC_PASS_FILE = "/data/secrets/restic.pass";

View File

@@ -38,3 +38,7 @@ export const sessionsTable = sqliteTable("sessions_table", {
});
export type Session = typeof sessionsTable.$inferSelect;
export const repositoriesTable = sqliteTable("repositories_table", {
id: text().primaryKey(),
});

View File

@@ -4,8 +4,11 @@ import { db } from "../../db/db";
import { volumesTable } from "../../db/schema";
import { logger } from "../../utils/logger";
import { volumeService } from "../volumes/volume.service";
import { restic } from "../../utils/restic";
export const startup = async () => {
await restic.ensurePassfile();
const volumes = await db.query.volumesTable.findMany({
where: or(
eq(volumesTable.status, "mounted"),

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,65 @@
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { type } from "arktype";
import { $ } from "bun";
import { RESTIC_PASS_FILE } from "../core/constants";
import { logger } from "./logger";
const backupOutputSchema = type({
message_type: "'summary'",
files_new: "number",
files_changed: "number",
files_unmodified: "number",
dirs_new: "number",
dirs_changed: "number",
dirs_unmodified: "number",
data_blobs: "number",
tree_blobs: "number",
data_added: "number",
total_files_processed: "number",
total_bytes_processed: "number",
total_duration: "number",
snapshot_id: "string",
});
const ensurePassfile = async () => {
await fs.mkdir(path.dirname(RESTIC_PASS_FILE), { recursive: true });
try {
await fs.access(RESTIC_PASS_FILE);
} catch {
logger.info("Restic passfile not found, creating a new one...");
await fs.writeFile(RESTIC_PASS_FILE, crypto.randomBytes(32).toString("hex"), { mode: 0o600 });
}
};
const init = async (name: string) => {
const res =
await $`restic init --repo /data/repositories/${name} --password-file /data/secrets/restic.pass --json`.nothrow();
};
const backup = async (repo: string, source: string) => {
const res =
await $`restic --repo /data/repositories/${repo} backup ${source} --password-file /data/secrets/restic.pass --json`.nothrow();
if (res.exitCode !== 0) {
logger.error(`Restic backup failed: ${res.stderr}`);
throw new Error(`Restic backup failed: ${res.stderr}`);
}
const result = backupOutputSchema(res.json());
if (result instanceof type.errors) {
logger.error(`Restic backup output validation failed: ${result}`);
throw new Error(`Restic backup output validation failed: ${result}`);
}
return result;
};
export const restic = {
ensurePassfile,
init,
backup,
};