Created
April 27, 2023 10:51
-
-
Save JohnnyJayJay/5505ddd9d25ec36c68cc11725dfd132b to your computer and use it in GitHub Desktop.
Babashka script for daily backups using borg, polkit and zenity
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bb | |
(ns borg-backup | |
(:require [babashka.process :as p :refer [shell process sh]] | |
[clojure.string :as str] | |
[clojure.java.io :as io])) | |
(def keep | |
{:daily 5 | |
:weekly 3 | |
:monthly 2}) | |
(def home (System/getenv "HOME")) | |
(def borg-rsh (str "ssh -i " home "/.ssh/id_backup")) | |
(def passphrase-file (str home "/.borg/passphrase.txt.gpg")) | |
(def repo-base "ssh://[email protected]/home/borg/laptop") | |
(def repos | |
{:suffix "/root" | |
:roots ["/"] | |
:exclusions | |
["tmp" "dev" "proc" "sys" "run" "var/tmp" "var/cache"]} | |
{:suffix "/home" | |
:roots ["/home"] | |
:exclusions | |
["home/*/.config/chromium/Default/Service Worker/CacheStorage" | |
"home/**/cache/" | |
"home/*/.m2/repository" | |
"home/*/.sdkman/tmp" | |
"home/*/.emacs.d/.local/straight" | |
"home/*/.config/discord/*Cache" | |
"home/**/.cache/" | |
"home/*/.npm" | |
"home/*/.steam" | |
"home/*/.local/share/Steam" | |
"home/**/node_modules"]}) | |
(defn cancel-backup? [] | |
(-> (shell | |
{:continue true} | |
"zenity --question --title Backup --text" | |
"Backup imminent. Perform full system backup now?") | |
:exit | |
(= 1))) | |
(defn create-backup [env exclusions roots] | |
(apply | |
process | |
{:out :inherit} | |
(concat | |
["pkexec" "env"] | |
(map (fn [[k v]] (str k \= v)) env) | |
["borg" "create" "::{hostname}-{now}"] | |
roots | |
["-v" "--list" "--filter" "AME" "-x" "--exclude-caches"] | |
(mapcat vector (repeat "--exclude") exclusions)))) | |
(defn prune-repo [env] | |
(apply | |
process | |
{:extra-env env | |
:out :inherit} | |
"borg prune --list -v" | |
(mapcat (fn [[k v]] [(str "--keep-" (name k)) (str v)]) keep))) | |
(defn compact-repo [env] | |
(process | |
{:extra-env env | |
:out :inherit} | |
"borg compact -v")) | |
(defn log [progress-in line] | |
(println line) | |
(.write progress-in (str \# line)) | |
(.newLine progress-in) | |
(.flush progress-in)) | |
(defn percentage-update [progress-in percentage] | |
(.write progress-in (str (int percentage))) | |
(.newLine progress-in) | |
(.flush progress-in)) | |
(def progress-step (double (/ 100 (* 3 (count repos))))) | |
;; Script | |
(def args *command-line-args*) | |
(def no-script (= (first args) "--")) | |
(when (and (not no-script) (cancel-backup?)) | |
(println "Backup cancelled.") | |
(System/exit 1)) | |
(println "Decrypting passphrase file") | |
(def borg-passphrase | |
(-> (shell {:out :string} "gpg --decrypt" passphrase-file) :out str/trim)) | |
(def env | |
{"BORG_RSH" borg-rsh | |
"BORG_PASSPHRASE" borg-passphrase}) | |
;; run regular borg command in context | |
(when no-script | |
(-> shell | |
(apply | |
{:extra-env env :continue true} | |
(map #(str/replace % "%repo%" repo-base) (rest args))) | |
:exit | |
System/exit)) | |
(let [repos (cond->> repos (seq args) (filter (comp (set args) :suffix))) | |
current-process (volatile! nil) | |
{:keys [in]} | |
(process | |
{:shutdown (fn [{:keys [exit]}] | |
(when-not (and exit (zero? exit)) | |
(p/destroy @current-process)))} | |
"zenity --title 'Creating backups' --progress --auto-close") | |
progress (volatile! 0) | |
exit-codes (volatile! [])] | |
(with-open [in (io/writer in)] | |
(doseq [{:keys [suffix exclusions roots]} repos | |
:let [env (assoc env "BORG_REPO" (str repo-base suffix))] | |
[task-name task] | |
(map vector | |
["Create Backup" "Prune Repo" "Compact Repo"] | |
[#(create-backup env exclusions roots) #(prune-repo env) #(compact-repo env)]) | |
:let [{:keys [err] :as proc} (task)]] | |
(vreset! current-process proc) | |
(println "Repo" suffix ":" task-name) | |
(with-open [out (io/reader err)] | |
(run! (partial log in) (line-seq out)) | |
(vswap! exit-codes conj {:repo suffix :task task-name :code (:exit @proc)}) | |
(vswap! progress + progress-step) | |
(percentage-update in @progress))) | |
(percentage-update in 100)) | |
(let [max-code (reduce max (map :code @exit-codes)) | |
status (case max-code | |
0 :ok | |
1 :warnings | |
:errors) | |
messages (->> @exit-codes | |
(remove (comp zero? :code)) | |
(map (fn [{:keys [task repo code]}] | |
(str "Task '" task "' for repo '" | |
repo "' finished with " | |
(if (= 1 code) "warnings" "errors") \.))) | |
(str/join "\n"))] | |
(shell | |
{:continue true} | |
"zenity" | |
(case status :ok "--info" :warnings "--warning" :errors "--error") | |
"--text" | |
(if (= status :ok) "Backup complete." messages)) | |
(System/exit max-code))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment