This commit is contained in:
Esa Kataja
2025-05-14 11:32:22 +03:00
parent 929685d16e
commit 21aa75e170
6 changed files with 346 additions and 0 deletions
+2
View File
@@ -8,3 +8,5 @@ wheels/
# Virtual environments # Virtual environments
.venv .venv
.vscode
+23
View File
@@ -0,0 +1,23 @@
import typer
import os
from helpers import get_targets, run_backup, ask_repokey
app = typer.Typer()
@app.command()
def backup():
if not os.environ.get("BORG_PASSPHRASE"):
ask_repokey()
targets = get_targets()
for target in targets:
if not target.enabled:
print(f"Skipping disabled target: {target.path}")
continue
result_info = run_backup(target)
print(result_info.model_dump_json(indent=2))
if __name__ == "__main__":
app()
+36
View File
@@ -0,0 +1,36 @@
.Trash-1000
.Trash*
.Trashes
Thumbs.db
desktop.ini
.DS_Store
._*
*.tmp
*.temp
~*
*~
*.swp
.*.swp
.cache/
__pycache__/
node_modules/
.npm/
.yarn/
dist/
build/
out/
*.o
*.obj
*.exe
*.dll
*.py[cod]
*.log
logs/
*.db
*.sqlite
*.sqlite3
.git/
.svn/
.hg/
.idea/
.vscode/
+149
View File
@@ -0,0 +1,149 @@
from pathlib import Path
import json
from models import BackupItem, ResultInfo
from datetime import datetime
from subprocess import run
import os
import typer
# Default exclude patterns for backups
default_excludes = [
".Trash-1000",
".Trash*",
".Trashes", # Trash directories
"Thumbs.db",
"desktop.ini", # Windows system files
".DS_Store",
"._*", # macOS system files
"*.tmp",
"*.temp",
"~*",
"*~", # Temporary files
"*.swp",
".*.swp", # Vim swap files
".cache/",
"__pycache__/", # Cache directories
"node_modules/",
".npm/",
".yarn/", # Node.js directories
"dist/",
"build/",
"out/", # Build directories
"*.o",
"*.obj",
"*.exe",
"*.dll", # Compiled binaries
"*.py[cod]", # Python compiled files
"*.log",
"logs/", # Log files
"*.db",
"*.sqlite",
"*.sqlite3", # Database files
".git/",
".svn/",
".hg/", # Version control directories
".idea/",
".vscode/", # IDE directories
]
def ask_repokey() -> None:
repokey = typer.prompt("Enter repository key: ", hide_input=True)
os.environ["BORG_PASSPHRASE"] = repokey
def get_targets() -> list[BackupItem]:
JSON_TARGETS = Path(__file__).parent / "targets.json"
with open(JSON_TARGETS, "r") as f:
targets = json.load(f)
return [BackupItem(**target) for target in targets]
def add_target(target: BackupItem) -> None:
# Get current targets
targets = get_targets()
# Add new target
targets.append(target)
# Save targets
write_targets(targets)
def write_targets(targets: list[BackupItem]) -> None:
"""Save targets to JSON file"""
JSON_TARGETS = Path(__file__).parent / "targets.json"
with open(JSON_TARGETS, "w") as f:
json.dump(
[target.json_serializable for target in targets],
f,
ensure_ascii=False,
indent=2,
)
def set_targets(targets: list[BackupItem]) -> None:
JSON_TARGETS = Path(__file__).parent / "targets.json"
with open(JSON_TARGETS, "w") as f:
json.dump(
[target.json_serializable for target in targets],
f,
ensure_ascii=False,
indent=2,
)
def prune_backups(targets: list[BackupItem]) -> None:
for target in targets:
if not target.enabled:
continue
cmd = [
"borg",
"prune",
"--keep-daily",
"1",
"--keep-weekly",
"2",
"--keep-monthly",
"3",
"--keep-yearly",
"1",
target.target,
]
print("Pruning target:", target.path)
run(cmd, capture_output=True)
print("Pruned target:", target.path)
print("Compacting cache")
run(["borg", "compact", target.target], capture_output=True)
print("Compacted cache")
def run_backup(target: BackupItem) -> None:
date_str = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# Build borg command with base options
cmd = [
"borg",
"create",
"-C",
f"{target.compression.value},{target.comp_ratio}",
"--exclude-from",
"/home/esa/bin/backupper/src/excludes.txt",
"--stats",
"--json",
f"{target.target}::{date_str}",
target.path,
]
# Add exclude option only if there are excludes defined
if target.excludes: # This is true if the list is not empty
cmd.insert(3, "--exclude")
cmd.insert(4, ",".join(target.excludes))
print("Running target:", target.path)
result = run(cmd, capture_output=True)
if result.returncode != 0:
print("Result:", result.stderr.decode("utf-8"))
raise Exception(result.stderr.decode("utf-8"))
result_info = ResultInfo(**json.loads(result.stdout.decode("utf-8")))
prune_backups([target])
return result_info
+110
View File
@@ -0,0 +1,110 @@
from pydantic import BaseModel
from enum import Enum
from pathlib import Path
from datetime import datetime
class Compression(Enum):
GZIP = "gzip"
ZSTD = "zstd"
class BackupItem(BaseModel):
path: Path
compression: Compression = Compression.ZSTD
comp_ratio: int = 19
target: Path
excludes: list[str] = []
enabled: bool = True
@property
def json_serializable(self):
return {
"path": str(self.path),
"compression": self.compression.value,
"comp_ratio": self.comp_ratio,
"target": str(self.target),
"excludes": self.excludes,
"enabled": self.enabled,
}
{
"archive": {
"command_line": [
"/usr/bin/borg",
"create",
"-C",
"zstd,19",
"--exclude-from",
"/home/esa/bin/backupper/src/excludes.txt",
"--stats",
"--json",
"/home/esa/pCloudDrive/Backups/borgBackup/documents::2025-05-14_10-54-45",
"/home/esa/Documents",
],
"duration": 57.788986,
"end": "2025-05-14T10:55:43.000000",
"id": "9119a981b8e3b2a860f9fd5ed4543f9ec126d39bccfe510fb4e36b725331983b",
"limits": {"max_archive_size": 3.2854144431110464e-05},
"name": "2025-05-14_10-54-45",
"start": "2025-05-14T10:54:46.000000",
"stats": {
"compressed_size": 1285729209,
"deduplicated_size": 1285495760,
"nfiles": 1744,
"original_size": 1321765294,
},
},
"cache": {
"path": "/home/esa/.cache/borg/bf32695b5fde1b8eb6a1a278e8be9a5dde89c46b19ebe8831ccae3e329e6c09c",
"stats": {
"total_chunks": 2138,
"total_csize": 1285728520,
"total_size": 1321764559,
"total_unique_chunks": 2104,
"unique_csize": 1285644534,
"unique_size": 1320800483,
},
},
"encryption": {"mode": "repokey-blake2"},
"repository": {
"id": "bf32695b5fde1b8eb6a1a278e8be9a5dde89c46b19ebe8831ccae3e329e6c09c",
"last_modified": "2025-05-14T10:55:43.000000",
"location": "/home/esa/pCloudDrive/Backups/borgBackup/documents",
},
}
class _Result_Archive_Stats(BaseModel):
compressed_size: int
deduplicated_size: int
nfiles: int
original_size: int
class _Result_Archive(BaseModel):
command_line: list[str]
duration: float
end: datetime
id: str
limits: dict
name: str
start: datetime
stats: _Result_Archive_Stats
class _Result_Encryption(BaseModel):
mode: str
class _Result_Repository(BaseModel):
id: str
last_modified: str
location: str
class ResultInfo(BaseModel):
archive: _Result_Archive
encryption: _Result_Encryption
repository: _Result_Repository
+26
View File
@@ -0,0 +1,26 @@
[
{
"path": "/home/esa/Pictures",
"compression": "zstd",
"comp_ratio": 19,
"target": "/home/esa/pCloudDrive/Backups/borgBackup/pictures",
"excludes": [],
"enabled": true
},
{
"path": "/home/esa/Music",
"compression": "zstd",
"comp_ratio": 19,
"target": "/home/esa/pCloudDrive/Backups/borgBackup/music",
"excludes": [],
"enabled": true
},
{
"path": "/home/esa/Documents",
"compression": "zstd",
"comp_ratio": 19,
"target": "/home/esa/pCloudDrive/Backups/borgBackup/documents",
"excludes": [],
"enabled": true
}
]