This commit is contained in:
Fenris Wolf 2025-02-20 09:12:08 +01:00
parent 09b657ae5c
commit 6e0ff08234
9 changed files with 4637 additions and 1980 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

218
misc/backup.js Normal file
View file

@ -0,0 +1,218 @@
#!/usr/bin/env nodejs
function string_coin(
template,
arguments_
)
{
let result = template;
Object.entries(arguments_).forEach(
([key, value]) => {
result = result.replace(new RegExp("{{" + key + "}}", "g"), value);
}
);
return result;
}
function borg_init(
repository_directory,
{
"encryption": encryption = "none",
} = {
}
)
{
return string_coin(
"borg init --encryption={{encryption}} {{repository_directory}}\n",
{
"repository_directory": repository_directory,
"encryption": encryption,
}
);
}
function borg_create(
repository_directory,
archive_name,
directories,
{
"compression": compression = "none",
} = {
}
)
{
return string_coin(
"borg create --compression={{compression}} {{repository_directory}}::{{archive_name}} {{directories}}\n",
{
"repository_directory": repository_directory,
"archive_name": archive_name,
"compression": compression,
"directories": directories.join(" "),
}
)
}
function borg_prune(
repository_directory,
age,
{
"keep_weekly": keep_weekly = null,
"keep_yearly": keep_yearly = null,
} = {
}
)
{
return string_coin(
"borg prune --keep-within=2w{{macro_keep_weekly}}{{macro_keep_yearly}} {{repository_directory}}\n",
{
"repository_directory": repository_directory,
"keep_within": age,
"macro_keep_weekly": ((keep_weekly === null) ? "" : string_coin(" --keep-weekly={{x}}", {"x": keep_weekly.toFixed(0)})),
"macro_keep_yearly": ((keep_yearly === null) ? "" : string_coin(" --keep-yearly={{x}}", {"x": keep_yearly.toFixed(0)})),
}
)
}
function get_conf(
)
{
const _fs = require("fs");
const conf = JSON.parse(_fs.readFileSync("conf.json"));
return conf;
}
function get_stamp(
)
{
const date = (new Date(Date.now()));
return string_coin(
"{{year}}{{month}}{{day}}",
{
"year": date.getFullYear().toFixed(0).padStart(4, "0"),
"month": (date.getMonth()+1).toFixed(0).padStart(2, "0"),
"day": date.getDate().toFixed(0).padStart(2, "0"),
}
);
}
function get_repository_directory(
conf
)
{
return conf.target.data.repository;
}
function init(
conf
)
{
const repository_directory = get_repository_directory(conf);
if (false) {
process.stdout.write(
string_coin(
"mkdir --parents {{repository_directory}}\n",
{
"repository_directory": repository_directory,
}
)
);
}
process.stdout.write(
borg_init(
repository_directory,
{
"encryption": "none",
}
)
);
}
function run(
conf,
stamp
)
{
const repository_directory = get_repository_directory(conf);
conf.concerns.forEach(
concern => {
process.stdout.write(
string_coin(
"## {{name}}\n",
{
"name": concern.name,
}
)
);
process.stdout.write(
borg_create(
repository_directory,
string_coin(
"{{concern_name}}-{{stamp}}",
{
"concern_name": concern.name,
"stamp": stamp,
}
),
[concern.source_directory],
{
"compression": conf.target.data.compression,
}
)
);
process.stdout.write(
borg_prune(
repository_directory,
"2w",
{
"keep_weekly": 7,
"keep_yearly": 2,
}
)
);
process.stdout.write(
"\n"
);
}
);
}
function main(
args
)
{
// args
const action = (args.shift() ?? "run");
// exec
switch (action) {
case "init": {
const conf = get_conf();
init(conf);
break;
}
case "run": {
const conf = get_conf();
const stamp = get_stamp();
run(conf, stamp);
break;
}
default: {
throw (new Error("unhandled action: " + action));
break;
}
}
}
main(process.argv.slice(2));

View file

@ -0,0 +1,34 @@
{
"version": "1",
"target": {
"kind": "plain",
"parameters": {
"directory": "/tmp/backup"
}
},
"concerns": [
{
"active": true,
"name": "fehuz",
"kind": "files",
"parameters": {
"path": "/var/fehuz",
"name": "fehuz"
}
},
{
"active": true,
"name": "uruz",
"kind": "postgresql_dump",
"parameters": {
"credentials": {
"host": "postgresql.example.org",
"username": "username",
"password": "password",
"schema": "example"
},
"name": "uruz"
}
}
]
}

48
misc/conf.json Normal file
View file

@ -0,0 +1,48 @@
{
"target": {
"kind": "borg",
"data": {
"repository": "ssh://pv-fensalir-kvasir///home/kvasir/repos/ramsch.sx",
"compression": "lz4"
}
},
"concerns": [
{
"name": "espe-database",
"source_directory": "/tmp/backup/espe-database"
},
{
"name": "forgejo-database",
"source_directory": "/tmp/backup/forgejo-database"
},
{
"name": "forgejo-files",
"source_directory": "/tmp/backup/forgejo-files"
},
{
"name": "hedgedoc-database",
"source_directory": "/tmp/backup/hedgedoc-database"
},
{
"name": "owncloud-files",
"source_directory": "/tmp/backup/owncloud-files"
},
{
"name": "synapse-database",
"source_directory": "/tmp/backup/synapse-database"
},
{
"name": "vikunja-database",
"source_directory": "/tmp/backup/vikunja-database"
},
{
"name": "wiki_js-database",
"source_directory": "/tmp/backup/wiki_js-database"
},
{
"name": "zeitbild-files",
"source_directory": "/tmp/backup/zeitbild-files"
}
]
}

9
misc/setup-client.sh Normal file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env sh
## exec
mkdir --parents ~/.ssh/keypairs
ssh-keygen -t ed25519 -f ~/.ssh/keypairs/fensalir-kvasir
# todo: add entry to ~/.ssh/config

17
misc/setup-server.sh Normal file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env sh
## const
username=kvasir
repodir=repos
## exec
apt update && apt install borgbackup
useradd ${username} --create-home --home-dir=/home/${username}/${repodir} --groups sudo
# todo: add ssh key to ~/.ssh/authorized_keys
sudo --other-user=${username} mkdir --parents /home/${username}/${repodir}

View file

@ -1,3 +1,228 @@
const _conf_schema : lib_plankton.conf.type_schema = {
"nullable": false,
"type": "object",
"properties": {
"version": {
"nullable": false,
"type": "string"
},
"target": {
"anyOf": [
{
"nullable": false,
"type": "object",
"properties": {
"kind": {
"nullable": false,
"type": "string",
"enum": ["plain"]
},
"parameters": {
"nullable": false,
"type": "object",
"properties": {
"directory": {
"nullable": false,
"type": "string"
},
},
"additionalProperties": false,
"required": [
"directory",
]
},
},
"additionalProperties": false,
"required": [
"kind",
"parameters",
]
},
{
"nullable": false,
"type": "object",
"properties": {
"kind": {
"nullable": false,
"type": "string",
"enum": ["borg"]
},
"parameters": {
"nullable": false,
"type": "object",
"properties": {
"repository": {
"nullable": false,
"type": "string"
},
"compression": {
"nullable": false,
"type": "string",
"enum": [
"none",
"lz4",
"zlib",
"lzma",
],
"default": "lz4"
},
},
"required": [
"repository",
]
},
},
"additionalProperties": false,
"required": [
"kind",
"parameters",
]
}
]
},
/*
"defaults": {
},
*/
"concerns": {
"nullable": false,
"type": "array",
"items": {
"anyOf": [
{
"nullable": false,
"type": "object",
"properties": {
"active": {
"nullable": false,
"type": "boolean",
"default": true
},
"name": {
"nullable": false,
"type": "string"
},
"kind": {
"nullable": false,
"type": "string",
"enum": ["files"]
},
"parameters": {
"nullable": false,
"type": "object",
"properties": {
"path": {
"nullable": false,
"type": "string",
},
"name": {
"nullable": false,
"type": "string",
},
},
"additionalProperties": false,
"required": [
"path",
"name",
]
}
},
"additionalProperties": false,
"required": [
"name",
"kind",
"parameters",
]
},
{
"nullable": false,
"type": "object",
"properties": {
"active": {
"nullable": false,
"type": "boolean",
"default": true
},
"name": {
"nullable": false,
"type": "string"
},
"kind": {
"nullable": false,
"type": "string",
"enum": ["postgresql_dump"]
},
"parameters": {
"nullable": false,
"type": "object",
"properties": {
"credentials": {
"nullable": false,
"type": "object",
"properties": {
"host": {
"nullable": false,
"type": "string",
},
"port": {
"nullable": false,
"type": "integer",
"default": 5432
},
"username": {
"nullable": false,
"type": "string",
},
"password": {
"nullable": false,
"type": "string",
},
"schema": {
"nullable": false,
"type": "string",
},
},
"additionalProperties": false,
"required": [
"host",
"username",
"password",
"schema",
]
},
"name": {
"nullable": false,
"type": "string",
},
},
"additionalProperties": false,
"required": [
"credentials",
"name",
]
}
},
"additionalProperties": false,
"required": [
"name",
"kind",
"parameters",
]
},
]
}
},
},
"additionalProperties": false,
"required": [
"version",
"target",
"concerns",
]
};
/** /**
*/ */
function get_stamp(): string function get_stamp(): string
@ -22,134 +247,176 @@ function get_stamp(): string
*/ */
async function main(): Promise<void> async function main(): Promise<void>
{ {
const conf = lib_plankton.json.decode(await lib_plankton.file.read("conf.json")); // const conf = lib_plankton.json.decode(await lib_plankton.file.read("conf.json"));
const conf : any = await lib_plankton.conf.load(
_conf_schema,
"conf.json"
);
const stamp: string = get_stamp(); const stamp: string = get_stamp();
const target_directory = (conf.target.directory + "/" + stamp);
let commands: Array<string> = []; switch (conf.target.kind) {
const commands_add : (command: string) => void = (command) => { case "plain": {
commands.push(command); const target_directory = (conf.target.parameters.directory/* + "/" + stamp*/);
};
const commands_apply : () => void = () => { let commands: Array<string> = [];
// TODO const commands_add : (command: string) => void = (command) => {
process.stdout.write(commands.join("\n") + "\n"); commands.push(command);
}; };
const commands_apply : () => void = () => {
commands_add( // TODO
lib_plankton.string.coin( process.stdout.write(commands.join("\n") + "\n");
"mkdir --parents {{directory}}", };
{
"directory": target_directory,
}
)
);
commands_add(
""
);
for await (const concern of conf.concerns) {
if (! concern.active) {
// do nothing
}
else {
commands_add( commands_add(
lib_plankton.string.coin( lib_plankton.string.coin(
"# {{name}}", "mkdir --parents {{directory}}",
{ {
"name": concern.name, "directory": target_directory,
"kind": concern.kind,
} }
) )
); );
commands_add(
lib_plankton.string.coin(
"echo '-- {{name}}' > /dev/stderr",
{
"name": concern.name,
"kind": concern.kind,
}
)
);
switch (concern.kind) {
case "postgresql_dump": {
const password_file_path: string = "${HOME}/.pgpass";
commands_add(
lib_plankton.string.coin(
"echo '{{host}}:{{port}}:{{schema}}:{{username}}:{{password}}' > {{path}} && chmod 0600 {{path}}",
{
"path": password_file_path,
"host": concern.parameters.credentials.host,
"port": concern.parameters.credentials.port.toFixed(0),
"username": concern.parameters.credentials.username,
"password": concern.parameters.credentials.password,
"schema": concern.parameters.credentials.schema,
}
)
);
commands_add(
lib_plankton.string.coin(
"pg_dump --host={{host}} --port={{port}} --username={{username}} {{schema}} > {{target_path}}",
{
"host": concern.parameters.credentials.host,
"port": concern.parameters.credentials.port.toFixed(0),
"username": concern.parameters.credentials.username,
"schema": concern.parameters.credentials.schema,
"target_path": lib_plankton.string.coin(
"{{directory}}/{{name}}.sql",
{
"directory": target_directory,
"name": concern.parameters.name,
}
),
}
)
);
commands_add(
lib_plankton.string.coin(
"rm {{path}}",
{
"path": password_file_path,
}
)
);
break;
}
case "files": {
commands_add(
lib_plankton.string.coin(
"tar --create --directory={{path}} . > {{target_path}}",
{
"path": concern.parameters.path,
"target_path": lib_plankton.string.coin(
"{{directory}}/{{name}}.tar",
{
"directory": target_directory,
"name": concern.parameters.name,
}
),
}
)
);
break;
}
default: {
throw (new Error("unhandled kind: " + concern.kind));
break;
}
}
commands_add( commands_add(
"" ""
); );
for await (const concern of conf.concerns) {
if (! concern.active) {
// do nothing
}
else {
commands_add(
lib_plankton.string.coin(
"# {{name}}",
{
"name": concern.name,
"kind": concern.kind,
}
)
);
commands_add(
lib_plankton.string.coin(
"echo '-- {{name}}' > /dev/stderr",
{
"name": concern.name,
"kind": concern.kind,
}
)
);
switch (concern.kind) {
case "files": {
commands_add(
lib_plankton.string.coin(
"mkdir --parents {{directory}}",
{
"directory": lib_plankton.string.coin(
"{{directory}}/{{name}}",
{
"directory": target_directory,
"name": concern.parameters.name,
}
),
}
)
);
commands_add(
lib_plankton.string.coin(
"tar --create --directory={{path}} . > {{target_path}}",
{
"path": concern.parameters.path,
"target_path": lib_plankton.string.coin(
"{{directory}}/{{name}}/data.tar",
{
"directory": target_directory,
"name": concern.parameters.name,
}
),
}
)
);
break;
}
case "postgresql_dump": {
const password_file_path: string = "${HOME}/.pgpass";
commands_add(
lib_plankton.string.coin(
"echo '{{host}}:{{port}}:{{schema}}:{{username}}:{{password}}' > {{path}} && chmod 0600 {{path}}",
{
"path": password_file_path,
"host": concern.parameters.credentials.host,
"port": concern.parameters.credentials.port.toFixed(0),
"username": concern.parameters.credentials.username,
"password": concern.parameters.credentials.password,
"schema": concern.parameters.credentials.schema,
}
)
);
commands_add(
lib_plankton.string.coin(
"mkdir --parents {{directory}}",
{
"directory": lib_plankton.string.coin(
"{{directory}}/{{name}}",
{
"directory": target_directory,
"name": concern.parameters.name,
}
),
}
)
);
commands_add(
lib_plankton.string.coin(
"pg_dump --host={{host}} --port={{port}} --username={{username}} {{schema}} > {{target_path}}",
{
"host": concern.parameters.credentials.host,
"port": concern.parameters.credentials.port.toFixed(0),
"username": concern.parameters.credentials.username,
"schema": concern.parameters.credentials.schema,
"target_path": lib_plankton.string.coin(
"{{directory}}/{{name}}/data.sql",
{
"directory": target_directory,
"name": concern.parameters.name,
}
),
}
)
);
commands_add(
lib_plankton.string.coin(
"rm {{path}}",
{
"path": password_file_path,
}
)
);
break;
}
default: {
throw (new Error("unhandled kind: " + concern.kind));
break;
}
}
commands_add(
""
);
}
}
commands_add(
lib_plankton.string.coin(
"echo '{{directory}}'",
{
"directory": target_directory,
}
)
);
commands_apply();
break;
}
default: {
throw (new Error("unhandled target kind: " + conf.target.kind));
break;
} }
} }
commands_add(
lib_plankton.string.coin(
"echo '{{directory}}'",
{
"directory": target_directory,
}
)
);
commands_apply();
return Promise.resolve<void>(undefined); return Promise.resolve<void>(undefined);
} }

View file

@ -6,6 +6,7 @@ dir=lib/plankton
modules="" modules=""
modules="${modules} base" modules="${modules} base"
modules="${modules} conf"
modules="${modules} file" modules="${modules} file"
modules="${modules} call" modules="${modules} call"
modules="${modules} json" modules="${modules} json"