mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
feat: restic pass file generation
This commit is contained in:
@@ -2,7 +2,7 @@ ARG BUN_VERSION="1.3.0"
|
|||||||
|
|
||||||
FROM oven/bun:${BUN_VERSION}-alpine AS runner_base
|
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
|
# DEVELOPMENT
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export const OPERATION_TIMEOUT = 5000;
|
export const OPERATION_TIMEOUT = 5000;
|
||||||
export const VOLUME_MOUNT_BASE = "/var/lib/docker/volumes/ironmount";
|
export const VOLUME_MOUNT_BASE = "/var/lib/docker/volumes/ironmount";
|
||||||
export const DATABASE_URL = "/data/ironmount.db";
|
export const DATABASE_URL = "/data/ironmount.db";
|
||||||
|
export const RESTIC_PASS_FILE = "/data/secrets/restic.pass";
|
||||||
|
|||||||
@@ -38,3 +38,7 @@ export const sessionsTable = sqliteTable("sessions_table", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type Session = typeof sessionsTable.$inferSelect;
|
export type Session = typeof sessionsTable.$inferSelect;
|
||||||
|
|
||||||
|
export const repositoriesTable = sqliteTable("repositories_table", {
|
||||||
|
id: text().primaryKey(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { db } from "../../db/db";
|
|||||||
import { volumesTable } from "../../db/schema";
|
import { volumesTable } from "../../db/schema";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { volumeService } from "../volumes/volume.service";
|
import { volumeService } from "../volumes/volume.service";
|
||||||
|
import { restic } from "../../utils/restic";
|
||||||
|
|
||||||
export const startup = async () => {
|
export const startup = async () => {
|
||||||
|
await restic.ensurePassfile();
|
||||||
|
|
||||||
const volumes = await db.query.volumesTable.findMany({
|
const volumes = await db.query.volumesTable.findMany({
|
||||||
where: or(
|
where: or(
|
||||||
eq(volumesTable.status, "mounted"),
|
eq(volumesTable.status, "mounted"),
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
65
apps/server/src/utils/restic.ts
Normal file
65
apps/server/src/utils/restic.ts
Normal 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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user