Added systemd services for backups

This commit is contained in:
2025-06-13 12:16:29 +02:00
parent 004f1092f6
commit 2ab58a1a4e
17 changed files with 176 additions and 30 deletions

View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -0,0 +1,15 @@
# homeassistant-backup
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.15. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,57 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "homeassistant-backup",
"dependencies": {
"moment": "^2.30.1",
"pino": "^9.7.0",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="],
"@types/node": ["@types/node@22.15.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA=="],
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
"bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="],
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
"on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="],
"pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="],
"pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
"pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="],
"process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="],
"quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
}
}

View File

@@ -0,0 +1,86 @@
import { $ } from "bun";
import moment from "moment";
import logger from "pino";
import { parseArgs } from "util";
const { values } = parseArgs({
args: Bun.argv,
options: {
mode: {
type: "string",
required: true,
},
sourceFolder: {
type: "string",
required: true,
},
targetFolder: {
type: "string",
required: true,
},
backupName: {
type: "string",
required: true,
},
},
strict: true,
allowPositionals: true,
});
const log = logger();
switch (values.mode) {
case "hot":
await hotCopy(
values.sourceFolder!,
values.targetFolder!,
values.backupName!,
);
break;
case "cold":
await coldCopy(
values.sourceFolder!,
values.targetFolder!,
values.backupName!,
);
break;
}
// 3 cold copies are stored
async function hotCopy(
sourceFolder: string,
targetFolder: string,
backupName: string,
) {
const timestamp = moment().format("YYYY-MM-DD_HH-mm-ss"); // Return today's date time
log.info("Creating new copy");
await $`mkdir -p ${targetFolder}`;
// Sync new copy
await $`rsync -a ${sourceFolder} ${targetFolder}/backup_${backupName}_${timestamp}`;
log.info("Only keep last 3 copies");
// Only keep last 3 copies
await $`cd ${targetFolder} && ls | grep ${backupName} | sort -r | tail -n +4 | xargs rm -rf`;
}
// Only 1 cold copy is retained
async function coldCopy(
sourceFolder: string,
targetFolder: string,
backupName: string,
) {
const timestamp = moment().format("YYYY-MM-DD_HH-mm-ss"); // Return today's date time
log.info("Creating new copy");
// find latest backup
const sourceFolderToBackup =
await $`cd ${sourceFolder} && ls | grep ${backupName} | sort | tail -n 1`.text().then(resp => resp.replace('\n',''));
log.info(`Folder to back up: ${sourceFolderToBackup}`);
await $`mkdir -p ${targetFolder}`;
await $`zip -r ${targetFolder}/${backupName}_${timestamp}.zip ${sourceFolder}/${sourceFolderToBackup}`;
log.info("Only keep last copy");
// Only keep last copy
await $`cd ${targetFolder} && ls | grep ${backupName} | sort -r | tail -n +2 | xargs rm -rf`;
}

View File

@@ -0,0 +1,16 @@
{
"name": "homeassistant-backup",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"moment": "^2.30.1",
"pino": "^9.7.0"
}
}

View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Backup Home Assistant - Cold
After=network.target
[Service]
ExecStart=homeassistant-backup --mode cold --sourceFolder /mnt/usb/homeassistant/hot --targetFolder /mnt/usb/homeassistant/cold --backupName homeassistant
WorkingDirectory=/etc/homeassistant
Restart=off
User=willem
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Run Home assistant cold backup Service every day
[Timer]
OnBootSec=1min
OnUnitActiveSec=1day
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Backup Home Assistant - Hot
After=network.target
[Service]
ExecStart=homeassistant-backup --mode hot --sourceFolder /etc/homeassistant --targetFolder /mnt/usb/homeassistant/hot --backupName homeassistant
WorkingDirectory=/etc/homeassistant
Restart=off
User=willem
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Run Home assistant hot backup Service every hour
[Timer]
OnBootSec=1min
OnUnitActiveSec=1hour
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Pull zettelkasten
After=network.target
[Service]
ExecStart=git pull
WorkingDirectory=/home/willem/repos/zettelkasten
Restart=no
User=willem
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Pull zettelkasten every hour
[Timer]
OnBootSec=1min
OnUnitActiveSec=1min
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=Send todo mail
After=network.target
[Service]
ExecStart=sh -c "rg '\- \[ \]' . | mail -s todos willemserruys@proton.me"
WorkingDirectory=/home/willem/repos/zettelkasten
Restart=no
User=willem
Environment=PATH=/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Send ToDo mail
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
Persistent=true
[Install]
WantedBy=timers.target