Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,17 @@ policy = "basic"
match = "api.*"
policy = "token"
tokens = ["api-specific-token"]

# Backup configuration
[backup]
# Enable scheduled daily backups (default: false)
enabled = true

# Hour of day to run backups (0-23, default: 3 = 3am)
schedule_hour = 3

# Number of days to keep local backups (default: 7)
retain_days = 7

# Directory for local backup storage (default: /var/backups/trafic)
local_dir = "/var/backups/trafic"
131 changes: 130 additions & 1 deletion packages/trafic-agent/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { parseArgs } from "node:util";
import { loadConfig, validateConfig } from "./utils/config.js";
import { startServer } from "./server.js";
import { startIdleScheduler } from "./tasks/stop-idle.js";
import { startBackupScheduler } from "./tasks/backup-scheduler.js";
import { runBackup, listBackups, restoreProjectDb, findBackup, cleanOldBackups } from "./tasks/backup.js";
import { loadProjectList } from "./utils/ddev.js";
import { closeDb } from "./utils/db.js";
import { setup, audit } from "./setup/index.js";

Expand All @@ -17,6 +20,8 @@ Usage:

Commands:
start Start the agent server
backup Backup project databases
restore Restore a project database from backup
setup Setup a new server (Docker, DDEV, hardening)
audit Run security audit checks
version Show version
Expand All @@ -26,6 +31,18 @@ Start options:
-c, --config <path> Path to config file (default: /etc/trafic/config.toml)
-p, --port <port> Override port from config

Backup options:
-c, --config <path> Path to config file
--name <name> Backup a specific project (default: all)
--list List available backups
--clean Clean old backups beyond retention period

Restore options:
-c, --config <path> Path to config file
--name <name> Project name to restore (required)
--date <YYYY-MM-DD> Restore from a specific date (default: latest)
--file <path> Restore from a specific backup file

Setup options:
--tld <domain> TLD for DDEV projects (required)
--email <email> Email for Let's Encrypt certificates
Expand All @@ -45,6 +62,18 @@ Examples:
sudo trafic-agent setup --tld=previews.example.com --email=admin@example.com
sudo trafic-agent setup --tld=previews.example.com --no-hardening --dry-run

# Backup all projects
trafic-agent backup
trafic-agent backup --name my-app

# List and clean backups
trafic-agent backup --list
trafic-agent backup --clean

# Restore a project
trafic-agent restore --name my-app
trafic-agent restore --name my-app --date 2026-02-07

# Run security audit
trafic-agent audit
`;
Expand Down Expand Up @@ -86,9 +115,13 @@ async function runStart(values: Record<string, unknown>): Promise<void> {
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);

// Start server and scheduler
// Start server and schedulers
startServer(config);
startIdleScheduler(config);

if (config.backup.enabled) {
startBackupScheduler(config);
}
}

async function runSetup(values: Record<string, unknown>): Promise<void> {
Expand All @@ -113,6 +146,85 @@ async function runSetup(values: Record<string, unknown>): Promise<void> {
});
}

function runBackupCommand(values: Record<string, unknown>): void {
const config = loadConfig(values.config as string | undefined);

if (values.list) {
const entries = listBackups(config.backup);
if (entries.length === 0) {
console.log("No backups found.");
return;
}

// Group by date
let currentDate = "";
for (const entry of entries) {
if (entry.date !== currentDate) {
currentDate = entry.date;
console.log(`\n${currentDate}:`);
}
const sizeMb = (entry.sizeBytes / 1024 / 1024).toFixed(1);
console.log(` ${entry.project} (${sizeMb} MB)`);
}
return;
}

if (values.clean) {
const removed = cleanOldBackups(config.backup);
console.log(`Cleaned ${removed} old backup(s) (retention: ${config.backup.retainDays} days)`);
return;
}

const projectName = values.name as string | undefined;
const results = runBackup(config, {
projectName,
// Explicit CLI request: start stopped projects if targeting a specific one
forceStart: !!projectName,
});
const failed = results.filter((r) => !r.success && !r.error?.includes("skipped"));
if (failed.length > 0) {
process.exit(1);
}
}

function runRestoreCommand(values: Record<string, unknown>): void {
const config = loadConfig(values.config as string | undefined);
const name = values.name as string | undefined;

if (!name) {
console.error("Error: --name is required for restore");
process.exit(1);
}

// Find the backup file
let backupFile = values.file as string | undefined;

if (!backupFile) {
backupFile = findBackup(config.backup, name, values.date as string | undefined);
if (!backupFile) {
const dateHint = values.date ? ` on ${values.date}` : "";
console.error(`No backup found for ${name}${dateHint}`);
process.exit(1);
}
}

// Find project directory
const projects = loadProjectList(config.projectListPath);
const projectDir = projects.get(name);

if (!projectDir) {
console.error(`Project ${name} not found in project list`);
process.exit(1);
}

console.log(`Restoring ${name} from ${backupFile}`);
const success = restoreProjectDb(name, projectDir, backupFile);

if (!success) {
process.exit(1);
}
}

async function main(): Promise<void> {
const { values, positionals } = parseArgs({
allowPositionals: true,
Expand All @@ -132,6 +244,15 @@ async function main(): Promise<void> {
"no-ddev": { type: "boolean" },
"ssh-users": { type: "string" },
"dry-run": { type: "boolean" },

// Backup options
name: { type: "string" },
list: { type: "boolean" },
clean: { type: "boolean" },

// Restore options
date: { type: "string" },
file: { type: "string" },
},
});

Expand Down Expand Up @@ -159,6 +280,14 @@ async function main(): Promise<void> {
await runSetup(values);
break;

case "backup":
runBackupCommand(values);
break;

case "restore":
runRestoreCommand(values);
break;

case "audit":
await audit();
break;
Expand Down
14 changes: 14 additions & 0 deletions packages/trafic-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export type {
AuthRule,
AuthRequest,
AuthResult,
BackupConfig,
BackupResult,
BackupEntry,
DdevProject,
ProjectRecord,
AccessLog,
Expand Down Expand Up @@ -58,6 +61,17 @@ export type { ProjectConfig } from "./utils/project-config.js";

// Tasks
export { stopIdleProjects, startIdleScheduler } from "./tasks/stop-idle.js";
export {
runBackup,
backupProjectDb,
backupAgentData,
listBackups,
cleanOldBackups,
restoreProjectDb,
findBackup,
} from "./tasks/backup.js";
export type { RunBackupOptions } from "./tasks/backup.js";
export { startBackupScheduler } from "./tasks/backup-scheduler.js";

// Server
export { startServer } from "./server.js";
Expand Down
42 changes: 42 additions & 0 deletions packages/trafic-agent/src/tasks/backup-scheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { runBackup, cleanOldBackups } from "./backup.js";
import type { AgentConfig } from "../types.js";

/**
* Simple daily backup scheduler.
* Checks every 30 minutes if we've passed the scheduled hour and runs once per day.
*/
export function startBackupScheduler(config: AgentConfig): NodeJS.Timeout {
const { scheduleHour } = config.backup;
let lastRunDate = "";

console.log(
`Backup scheduler started: daily at ${String(scheduleHour).padStart(2, "0")}:00, ` +
`retention ${config.backup.retainDays} days, ` +
`dir ${config.backup.localDir}`,
);

const check = (): void => {
const now = new Date();
const todayDate = now.toISOString().slice(0, 10);
const currentHour = now.getHours();

// Run once per day, after the scheduled hour
if (currentHour >= scheduleHour && lastRunDate !== todayDate) {
lastRunDate = todayDate;

console.log(`[backup] Starting scheduled backup...`);
try {
runBackup(config);
cleanOldBackups(config.backup);
} catch (error) {
console.error("[backup] Scheduled backup failed:", error);
}
}
};

// Check immediately on start
check();

// Check every 30 minutes
return setInterval(check, 30 * 60 * 1000);
}
Loading