fix misfile

This commit is contained in:
Paul Wilde 2024-08-19 02:21:46 +01:00
parent 3a9294dcf1
commit 79a3dab734
19 changed files with 327 additions and 132 deletions

View file

@ -1,6 +1,6 @@
# Package # Package
version = "0.1.1" version = "0.1.5"
author = "Paul Wilde" author = "Paul Wilde"
description = "A Borg Backup Wrapper" description = "A Borg Backup Wrapper"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"

51
norg.toml.sample Normal file
View file

@ -0,0 +1,51 @@
# Source Directories you would like to back up in a TOML list format
source_directories = [
"/home/me/Music",
"/home/me/Pictures"
]
# Repositories to back up to, in separate TOML objects
[[repositories]]
label = "A Repository"
path = "/my/backup/location"
[[repositories]]
label = "Another Respository at BorgBase"
path = "ssh://1234abcd@1234abcd.repo.borgbase.com/./repo"
# Encryption Configuration
[encryption]
# `encryption_passphrase` gets set as the `BORG_PASSPHRASE` env var
encryption_passphrase = "MyReallySecurePassword"
# I hope to add more Borg encryption env vars here in time
# Actions that are called at various times during runtime
[actions]
# "everything" means before or after every possible option for all repositories
before_everything = ["echo before everything"]
after_everything = ["echo after everything"]
# "actions" means before any action, per repository
before_actions = ["echo before actions"]
after_actions = ["echo after actions"]
# before or after the backup process per repository
before_backup = ["echo before backup"]
after_backup = ["echo after backup"]
# before or after the extract process per repository
before_extract = ["echo before extract"]
after_extract = ["echo after extract"]
# before or after the prune process per repository
before_prune = ["echo before prune"]
after_prune = ["echo after prune"]
# before or after the compact process per respository
before_compact = ["echo before compact"]
after_compact = ["echo after compact"]
# before or after the check processs per repository
before_check = ["echo before check"]
after_check = ["echo after check"]
[uptimekuma]
# The base/push url of your Uptime Kuma monitor - without the query string.
# The quenry string will be generated at run time and will change dependant on the state of your backup.
base_url = "https://uptime.kuma.url/api/push/1234abcd"
# what backup states you wish to send an alert for, defaults to Success, Failure and Running
states = ["Success","Failure", "Running"]

View file

@ -1,118 +1,21 @@
import ../model/config_type import ../model/config_type
import ../model/state_type import ../model/encryption_type
import ../model/borg_type import ../model/borg_type
import ../notifier/notifier
import ../utils/actions import ../utils/actions
import strutils
import strformat
import sequtils
import osproc
import times
import os
import nativesockets
import execute
import prune
import create
import mount
import extract
proc genArchiveName(): string =
let hostname = getHostname()
let ts = getTime().format("yyyy-MM-dd'T'HH:mm:ss'.'ffffff")
return fmt"{hostname}-{ts}"
proc genCommand(cmd: string, repo: string, others: seq[string]): string =
let args = others.join(" ")
let cmd = fmt"{BORG_BIN} {cmd} {repo} {args}"
return cmd
proc run(cmd: string): int =
echo fmt"Trying to run : {cmd}"
try:
let res = execProcess(cmd)
echo res
except:
echo getCurrentExceptionMsg()
proc runDiscard(cmd: string): int =
echo fmt"Trying to run : {cmd}"
try:
let res = execCmd(cmd)
return res
except:
echo getCurrentExceptionMsg()
return 1
proc initRepo(nc: NorgConfig, repo: Repository): int = proc initRepo(nc: NorgConfig, repo: Repository): int =
return runDiscard genCommand(cmd = "init", repo = repo.path, others = nc.args.others) return runDiscard genCommand(cmd = "init", repo = repo.path, others = nc.args.others)
proc createArchive(nc: NorgConfig, repo: Repository, archivename: string, retry: int = 0): int =
let others = concat(nc.source_directories, nc.args.others)
let res = run genCommand(cmd = "create", repo = archivename, others = others)
if res == 1:
sleep 15 * 1000 # 15 seconds
if retry == nc.retries:
return 1
else:
return createArchive(nc, repo, archivename, retry + 1)
return res
proc backupSources(nc: NorgConfig, repo: Repository): int =
let start_time = now()
notify(nc.notifiers, state=Running)
let end_time = now()
let archivename = repo.path & "::" & genArchiveName()
let res = createArchive(nc, repo, archivename)
let total = (end_time - start_time).inMilliSeconds()
case res
of 0:
notify(nc.notifiers, state=Success, runtime=total)
of 1:
notify(nc.notifiers, state=Failure, runtime=total)
else:
notify(nc.notifiers, state=Failure, runtime=total)
return res
proc listArchives(nc: NorgConfig, repo: Repository): int = proc listArchives(nc: NorgConfig, repo: Repository): int =
return run genCommand(cmd = "list", repo = repo.path, others = nc.args.others) return run genCommand(cmd = "list", repo = repo.path, others = nc.args.others)
proc mountArchive(nc: NorgConfig, repo: Repository): int =
let archive = repo.path & "::" & nc.args.others[0]
let others = nc.args.others[1..^1]
let ok = runDiscard genCommand(cmd = "mount", repo = archive, others = others)
if ok == 0:
echo fmt"Mounted {archive} at {others[0]}"
else:
echo "Failed to mount ", archive
proc unmountArchive(nc: NorgConfig): int =
let ok = runDiscard genCommand(cmd = "umount", repo = "", others = nc.args.others)
if ok == 0:
echo "Unmounted ", nc.args.others[0]
else:
echo "Failed to unmount ", nc.args.others[0]
proc isEmpty(dir: string): bool =
var count = 0
for idx, f in walkDir(dir): return false
#count += 1
return count == 0
proc extractArchive(nc: NorgConfig, repo: Repository): int =
let archive = fmt"{repo.path}::{nc.args.others[0]}"
var others = nc.args.others[1..^1]
if nc.args.extract_destination != "":
discard existsOrCreateDir(nc.args.extract_destination)
setCurrentDir(nc.args.extract_destination)
let dir = getCurrentDir()
if dir.isEmpty():
echo "Restoring..."
let ok = run(genCommand(cmd = "extract", repo = archive, others = others))
return ok
else:
echo "Not restoring to non-empty destination\r\nPlease use the --destination flag"
proc pruneRepo(nc: NorgConfig, repo: Repository): int =
echo "Not Yet Implemented."
discard
proc compactRepo(nc: NorgConfig, repo: Repository): int = proc compactRepo(nc: NorgConfig, repo: Repository): int =
echo "Not Yet Implemented." echo "Not Yet Implemented."
discard discard
@ -130,7 +33,7 @@ proc execute*(nc: NorgConfig) =
discard initRepo(nc, repo) discard initRepo(nc, repo)
of CREATE: of CREATE:
run_actions(norg_config.actions.before_backup) run_actions(norg_config.actions.before_backup)
discard backupSources(nc, repo) discard createBackup(nc, repo)
run_actions(norg_config.actions.after_backup) run_actions(norg_config.actions.after_backup)
of LIST: of LIST:
discard listArchives(nc, repo) discard listArchives(nc, repo)

45
norg/borg/create.nim Normal file
View file

@ -0,0 +1,45 @@
import ../model/config_type
import ../model/state_type
import ../notifier/notifier
import execute
import prune
import os
import times
import sequtils
import strformat
import nativesockets
proc genArchiveName(): string =
let hostname = getHostname()
let ts = getTime().format("yyyy-MM-dd'T'HH:mm:ss'.'ffffff")
return fmt"{hostname}-{ts}"
proc createArchive(nc: NorgConfig, repo: Repository, archivename: string, retry: int = 0): int =
let others = concat(nc.source_directories, nc.args.others)
let res = run genCommand(cmd = "create", repo = archivename, others = others)
if res != 0:
sleep 15 * 1000 # 15 seconds
if retry == nc.retries:
return 1
else:
return createArchive(nc, repo, archivename, retry + 1)
return res
proc createBackup*(nc: NorgConfig, repo: Repository): int =
let start_time = now()
notify(nc.notifiers, state=Running)
let end_time = now()
let archivename = repo.path & "::" & genArchiveName()
let res = createArchive(nc, repo, archivename)
let total = (end_time - start_time).inMilliSeconds()
case res
of 0:
discard pruneRepo(nc, repo)
notify(nc.notifiers, state=Success, runtime=total)
of 1:
notify(nc.notifiers, state=Failure, runtime=total)
else:
notify(nc.notifiers, state=Failure, runtime=total, msg = $res)
return res

28
norg/borg/execute.nim Normal file
View file

@ -0,0 +1,28 @@
import strutils
import strformat
import osproc
import ../model/borg_type
proc genCommand*(cmd: string, repo: string, others: seq[string]): string =
let args = others.join(" ")
let cmd = fmt"{BORG_BIN} {cmd} {repo} {args}"
return cmd
proc run*(cmd: string): int =
echo fmt"Trying to run : {cmd}"
try:
let res = execCmd(cmd)
return res
except:
echo getCurrentExceptionMsg()
return 1
proc runDiscard*(cmd: string): int =
echo fmt"Trying to run : {cmd}"
try:
let res = execCmd(cmd)
return res
except:
echo getCurrentExceptionMsg()
return 1

24
norg/borg/extract.nim Normal file
View file

@ -0,0 +1,24 @@
import ../model/config_type
import execute
import strformat
import os
proc isEmpty(dir: string): bool =
var count = 0
for idx, f in walkDir(dir): return false
#count += 1
return count == 0
proc extractArchive*(nc: NorgConfig, repo: Repository): int =
let archive = fmt"{repo.path}::{nc.args.others[0]}"
var others = nc.args.others[1..^1]
if nc.args.extract_destination != "":
discard existsOrCreateDir(nc.args.extract_destination)
setCurrentDir(nc.args.extract_destination)
let dir = getCurrentDir()
if dir.isEmpty():
echo "Restoring..."
let ok = run genCommand(cmd = "extract", repo = archive, others = others)
return ok
else:
echo "Not restoring to non-empty destination\r\nPlease use the --destination flag"

19
norg/borg/mount.nim Normal file
View file

@ -0,0 +1,19 @@
import ../model/config_type
import execute
import strformat
proc mountArchive*(nc: NorgConfig, repo: Repository): int =
let archive = repo.path & "::" & nc.args.others[0]
let others = nc.args.others[1..^1]
let ok = runDiscard genCommand(cmd = "mount", repo = archive, others = others)
if ok == 0:
echo fmt"Mounted {archive} at {others[0]}"
else:
echo "Failed to mount ", archive
proc unmountArchive*(nc: NorgConfig): int =
let ok = runDiscard genCommand(cmd = "umount", repo = "", others = nc.args.others)
if ok == 0:
echo "Unmounted ", nc.args.others[0]
else:
echo "Failed to unmount ", nc.args.others[0]

19
norg/borg/prune.nim Normal file
View file

@ -0,0 +1,19 @@
import ../model/config_type
import strformat
import execute
proc addPruneOptions(cmd: var string, maintenance: Maintenance) =
cmd = fmt"""{cmd} \
--keep-hourly {maintenance.keep_hourly} \
--keep-daily {maintenance.keep_daily} \
--keep-weekly {maintenance.keep_weekly} \
--keep-monthly {maintenance.keep_monthly} \
--keep-yearly {maintenance.keep_yearly} \
"""
proc pruneRepo*(nc: NorgConfig, repo: Repository): int =
var cmd = genCommand(cmd = "prune", repo = repo.path, others = nc.args.others)
cmd.addPruneOptions(nc.maintenance)
return run cmd

View file

@ -0,0 +1,44 @@
import ../model/actions_type
import parsetoml
proc parseActions*(conf: TomlValueRef): Actions =
var actions: Actions = Actions()
# Oh I hate this bit..
# Everything
for action in conf{"before_everything"}.getElems():
actions.before_everything.add(action.getStr())
for action in conf{"after_everything"}.getElems():
actions.after_everything.add(action.getStr())
# Actions
for action in conf{"before_actions"}.getElems():
actions.before_actions.add(action.getStr())
for action in conf{"after_actions"}.getElems():
actions.after_actions.add(action.getStr())
# Backup
for action in conf{"before_backup"}.getElems():
actions.before_backup.add(action.getStr())
for action in conf{"after_backup"}.getElems():
actions.after_backup.add(action.getStr())
# Extract
for action in conf{"before_extract"}.getElems():
actions.before_extract.add(action.getStr())
for action in conf{"after_extract"}.getElems():
actions.after_extract.add(action.getStr())
# Prune
for action in conf{"before_prune"}.getElems():
actions.before_prune.add(action.getStr())
for action in conf{"after_prune"}.getElems():
actions.after_prune.add(action.getStr())
# Compact
for action in conf{"before_compact"}.getElems():
actions.before_compact.add(action.getStr())
for action in conf{"after_compact"}.getElems():
actions.after_compact.add(action.getStr())
# Check
for action in conf{"before_check"}.getElems():
actions.before_check.add(action.getStr())
for action in conf{"after_check"}.getElems():
actions.after_check.add(action.getStr())
return actions

View file

@ -1,7 +1,10 @@
import parsetoml import parsetoml
import ../model/config_type import ../model/config_type
import ../model/encryption_type
import notifier_config import notifier_config
import actions_config
import maintenance_config
export config_type export config_type
proc parseSourceDirectories*(in_conf: TomlValueRef): seq[string] = proc parseSourceDirectories*(in_conf: TomlValueRef): seq[string] =
@ -14,7 +17,9 @@ proc parseSourceDirectories*(in_conf: TomlValueRef): seq[string] =
return src_dirs return src_dirs
proc parseEncryption*(enc_conf: TomlValueRef) = proc parseEncryption*(enc_conf: TomlValueRef) =
norg_config.setEncryptionPassphrase(enc_conf{"encryption_passphrase"}.getStr("")) setEncryptionPassphrase(enc_conf{"encryption_passphrase"}.getStr(""))
setEncryptionPassphraseFD(enc_conf{"encryption_passphrase_fd"}.getStr(""))
setEncryptionPassCommand(enc_conf{"encryption_passcommand"}.getStr(""))
proc parseRepositories*(rep_conf: TomlValueRef): seq[Repository] = proc parseRepositories*(rep_conf: TomlValueRef): seq[Repository] =
var repos: seq[Repository] = @[] var repos: seq[Repository] = @[]
@ -33,6 +38,8 @@ proc parseConfigFile*(file: string): NorgConfig =
parseEncryption(in_conf{"encryption"}) parseEncryption(in_conf{"encryption"})
norg_config.repositories = parseRepositories(in_conf{"repositories"}) norg_config.repositories = parseRepositories(in_conf{"repositories"})
norg_config.notifiers = parseNotifiers(in_conf) norg_config.notifiers = parseNotifiers(in_conf)
norg_config.actions = parseActions(in_conf{"actions"})
norg_config.maintenance = parseMaintenance(in_conf{"maintenance"})
return norg_config return norg_config

View file

@ -0,0 +1,11 @@
import ../model/maintenance_type
import parsetoml
proc parseMaintenance*(conf: TomlValueRef): Maintenance =
var maintenance = newMaintenance()
maintenance.keep_hourly = conf{"keep_hourly"}.getInt(maintenance.keep_hourly)
maintenance.keep_daily = conf{"keep_daily"}.getInt(maintenance.keep_daily)
maintenance.keep_weekly = conf{"keep_weekly"}.getInt(maintenance.keep_weekly)
maintenance.keep_monthly = conf{"keep_monthly"}.getInt(maintenance.keep_monthly)
maintenance.keep_yearly = conf{"keep_yearly"}.getInt(maintenance.keep_yearly)
return maintenance

View file

@ -1,12 +1,13 @@
import repository_type import repository_type
import notifier_types import notifier_types
import actions_type import actions_type
import maintenance_type
import ../config/args import ../config/args
import os
export repository_type export repository_type
export notifier_types export notifier_types
export actions_type export actions_type
export maintenance_type
type type
@ -19,20 +20,9 @@ type
notifiers*: Notifiers notifiers*: Notifiers
actions*: Actions actions*: Actions
args*: NorgArgs args*: NorgArgs
Maintenance* = object
keep_daily*: int
keep_weekly*: int
keep_monthly*: int
var norg_config*: NorgConfig var norg_config*: NorgConfig
proc newMaintenance*(): Maintenance =
var m = Maintenance()
m.keep_daily = 7
m.keep_weekly = 4
m.keep_monthly = 6
return m
proc newNorgConfig*(): NorgConfig = proc newNorgConfig*(): NorgConfig =
var nc = NorgConfig() var nc = NorgConfig()
nc.maintenance = newMaintenance() nc.maintenance = newMaintenance()
@ -44,11 +34,6 @@ proc newNorgConfig*(): NorgConfig =
# msg &= "Notifiers: " & $c.notifiers & "\r\n" # msg &= "Notifiers: " & $c.notifiers & "\r\n"
# return msg # return msg
proc setEncryptionPassphrase*(nc: var NorgConfig, pw: string) =
putEnv("BORG_PASSPHRASE", pw)
proc delEncryptionPassphraseInfo*() =
delEnv("BORG_PASSPHRASE")

View file

@ -0,0 +1,18 @@
import os
proc setEncryptionPassphrase*(pw: string) =
if pw != "":
putEnv("BORG_PASSPHRASE", pw)
proc setEncryptionPassphraseFD*(pw: string) =
if pw != "":
putEnv("BORG_PASSPHRASE_FD", pw)
proc setEncryptionPassCommand*(pw: string) =
if pw != "":
putEnv("BORG_PASSCOMMAND", pw)
proc delEncryptionPassphraseInfo*() =
delEnv("BORG_PASSPHRASE")
delEnv("BORG_PASSPHRASE_FD")
delEnv("BORG_PASSCOMMAND")

View file

@ -0,0 +1,17 @@
type
Maintenance* = object
keep_hourly*: int
keep_daily*: int
keep_weekly*: int
keep_monthly*: int
keep_yearly*: int
proc newMaintenance*(): Maintenance =
var m = Maintenance()
m.keep_hourly = 24
m.keep_daily = 7
m.keep_weekly = 4
m.keep_monthly = 6
m.keep_yearly = 1
return m

View file

@ -7,14 +7,15 @@ import ../utils/httprequest
type type
UptimeKuma* = object of Notifier UptimeKuma* = object of Notifier
proc send_notify*(uk: UptimeKuma, state: State, runtime: int = 0): int {.discardable.} = proc send_notify*(uk: UptimeKuma, state: State, runtime: int = 0, msg: string = ""): int {.discardable.} =
var status: string var status: string
case state case state
of Success, Running: of Success, Running:
status = "up" status = "up"
else: else:
status = "down" status = "down"
let url = fmt"{uk.base_url}?status={status}&msg={state}&ping={runtime}" let message = fmt"{status}\r\n{msg}"
let url = fmt"{uk.base_url}?status={status}&msg={message}&ping={runtime}"
echo "Sending notification to " & url echo "Sending notification to " & url
let res = sendHttpRequest(HttpGet, url) let res = sendHttpRequest(HttpGet, url)

View file

@ -1,7 +1,7 @@
import ../model/state_type import ../model/state_type
import ../model/notifier_types import ../model/notifier_types
proc notify*(notifiers: Notifiers, state: State, runtime: int = 0): int {.discardable.} = proc notify*(notifiers: Notifiers, state: State, runtime: int = 0, msg: string = ""): int {.discardable.} =
if notifiers.uptimekuma.base_url != "" and if notifiers.uptimekuma.base_url != "" and
state in notifiers.uptimekuma.states: state in notifiers.uptimekuma.states:
notifiers.uptimekuma.send_notify(state, runtime) notifiers.uptimekuma.send_notify(state, runtime, msg)

View file

@ -2,5 +2,5 @@ import osproc
proc run_actions*(actions: seq[string]): int {.discardable.} = proc run_actions*(actions: seq[string]): int {.discardable.} =
for action in actions: for action in actions:
echo execCmd(action) discard execCmd(action)

View file

@ -1,16 +1,16 @@
# Norg # Norg
A simple, portable, wrapper for the [borg backup utility](https://www.borgbackup.org) written in Nim A simple, portable, wrapper for the [borg backup utility](https://www.borgbackup.org) written in Nim
<!--more-->
Inspired by [Borgmatic](https://torsion.org/borgmatic) Inspired by [Borgmatic](https://torsion.org/borgmatic)
## Usage ## Usage
Norg uses a `toml` based config file for configuration. An example configuration would look like this: Norg uses a `toml` based config file for configuration. An example configuration would look like this:
```toml ```toml
source_dirs = [ source_directories = [
"/home/me/Music", "/home/me/Music",
"/home/me/Pictures" "/home/me/Pictures"
] ]
encryption_password = "MyReallySecurePassword"
[[repositories]] [[repositories]]
label = "A Repository" label = "A Repository"
path = "/my/backup/location" path = "/my/backup/location"
@ -19,6 +19,15 @@ path = "/my/backup/location"
label = "Another Respository at BorgBase" label = "Another Respository at BorgBase"
path = "ssh://1234abcd@1234abcd.repo.borgbase.com/./repo" path = "ssh://1234abcd@1234abcd.repo.borgbase.com/./repo"
[encryption]
encryption_passphrase = "MyReallySecurePassword"
[actions]
before_actions = ["echo before actions"]
after_actions = ["echo after actions", "echo actions completed"]
before_backup = ["echo before backup", "date"]
after_backup = ["echo after backup","echo backup completed"]
[uptimekuma] [uptimekuma]
base_url = "https://uptime.kuma.url/api/push/1234abcd" base_url = "https://uptime.kuma.url/api/push/1234abcd"
states = ["Success","Failure", "Running"] states = ["Success","Failure", "Running"]
@ -49,6 +58,20 @@ norg -c myconfig.toml extract pcname-2024-08-18T15:20:17773204
norg -c myconfig.toml extract pcname-2024-08-18T15:20:17773204 --destination /tmp/my_extracted_archive norg -c myconfig.toml extract pcname-2024-08-18T15:20:17773204 --destination /tmp/my_extracted_archive
``` ```
# Build from Source
Download and build from source
```sh
git clone https://codeberg.org/pswilde/norgbackup
cd norgbackup
nimble install
```
or just install directly with `nimble`
```sh
nimble install https://codeberg.org/pswilde/norgbackup
```
## Naming. Why "Norg"? ## Naming. Why "Norg"?
Well, I don't know. I'm a Star Trek fan so obviously I wanted to keep something Well, I don't know. I'm a Star Trek fan so obviously I wanted to keep something

View file

@ -1,7 +1,7 @@
# A list of things I'd like to include in Norg # A list of things I'd like to include in Norg
- [ ] Backup maintenance i.e. Keep Daily/Weekly/Monthy - [ ] Backup maintenance i.e. Keep Daily/Weekly/Monthy
- [ ] Pre and post run scripts i.e. to backup a database - [x] Pre and post run scripts i.e. to backup a database
- [ ] change encryption_password to encryption_passphrase in config to be more in line with borgbackup - [x] change encryption_password to encryption_passphrase in config to be more in line with borgbackup
- [ ] More notifiers (ntfy, healthchecks, etc.) - [ ] More notifiers (ntfy, healthchecks, etc.)
- [ ] Generate config command parameter - [ ] Generate config command parameter
- [ ] Allow to specify direct repository so mount/extract doesn't fail if multiple are available - [ ] Allow to specify direct repository so mount/extract doesn't fail if multiple are available