summaryrefslogtreecommitdiff
path: root/tw/services/nextcloud.scm
diff options
context:
space:
mode:
authorTimo Wilken2023-10-10 00:24:00 +0200
committerTimo Wilken2023-10-28 20:09:41 +0200
commit9eae00f2a76dc1bf62c70080805ff2e1522c1a7a (patch)
treee135d60adb68fdf965a3795d65f81d9913e0ad39 /tw/services/nextcloud.scm
parentffefb628e35537c1414169bdd88c219530a03bf3 (diff)
Store Nextcloud backups in restic repo
Replace the Nextcloud backup shell script with a Guile program that writes to a restic repository instead.
Diffstat (limited to 'tw/services/nextcloud.scm')
-rw-r--r--tw/services/nextcloud.scm124
1 files changed, 109 insertions, 15 deletions
diff --git a/tw/services/nextcloud.scm b/tw/services/nextcloud.scm
index 6ede7005..e7952b49 100644
--- a/tw/services/nextcloud.scm
+++ b/tw/services/nextcloud.scm
@@ -1,6 +1,9 @@
(define-module (tw services nextcloud)
#:use-module (gnu)
+ #:use-module (gnu packages backup)
#:use-module (gnu packages certs)
+ #:use-module (gnu packages databases)
+ #:use-module (gnu packages linux)
#:use-module (gnu packages php)
#:use-module (gnu services certbot)
#:use-module (gnu services mcron)
@@ -39,6 +42,107 @@ opcache.save_comments=1
opcache.revalidate_freq=120
"))))))))
+(define nextcloud-backup-program
+ (program-file "nextcloud-backup-command"
+ #~(begin
+ (use-modules (srfi srfi-1)
+ (srfi srfi-26)
+ (ice-9 popen)
+ (ice-9 receive)
+ (ice-9 textual-ports))
+
+ (define nextcloud-dir "/var/www/nextcloud")
+ (define nextcloud-data-partition "/var/data") ; mountpoint of the partition containing Nextcloud data dir
+ (define nextcloud-data-path "nextcloud") ; relative to `nextcloud-data-partition'
+ (define snapshot (string-append nextcloud-data-partition "/tmp-nextcloud-backup"))
+ (define btrfs #$(file-append btrfs-progs "/bin/btrfs"))
+ (define restic #$(file-append restic "/bin/restic"))
+ (setenv "RESTIC_REPOSITORY" "/var/backups/nextcloud")
+ (setenv "RESTIC_PASSWORD_FILE" "/etc/restic/lud-nextcloud")
+
+ (define (nc-maintenance enable?)
+ (let ((child-pid (primitive-fork)))
+ (if (zero? child-pid)
+ (begin ; this is the child
+ (setgid (group:gid (getgr "httpd"))) ; while still root
+ (setuid (passwd:uid (getpw "httpd")))
+ (execl #$(file-append php "/bin/php")
+ "php" "-c" #$%nextcloud-php.ini
+ (string-append nextcloud-dir "/occ")
+ "maintenance:mode"
+ (if enable? "--on" "--off")))
+ (zero? (status:exit-val (cdr (waitpid child-pid)))))))
+
+ (define* (cleanup #:optional (recv-signal #f) #:key (rethrow #f))
+ (nc-maintenance #f)
+ (when (and (file-exists? snapshot)
+ (file-is-directory? snapshot))
+ (system* btrfs "subvolume" "delete" "-c" snapshot))
+ (cond
+ (recv-signal (exit (- recv-signal)))
+ (rethrow (raise-exception rethrow))))
+
+ (define (run-pipeline . commands)
+ (receive (from to pids) (pipeline commands)
+ (close to)
+ (do ((char (read-char from) (read-char from)))
+ ((eof-object? char))
+ (display char))
+ (close from)
+ (let ((failing-index
+ (list-index (compose not zero? status:exit-val cdr waitpid)
+ (reverse pids))))
+ (when failing-index
+ (apply error "Command exited with error status"
+ (list-ref commands failing-index))))))
+
+ (define (read-file name)
+ (string-trim-right (call-with-input-file name get-string-all) #\newline))
+
+ (define (main)
+ (unless (nc-maintenance #t)
+ (error "Could not enter maintenance mode"))
+
+ ;; Backup the database. This can only be done offline.
+ (run-pipeline
+ ;; `mysql-configuration' uses mariadb by default, so match it here.
+ (list #$(file-append mariadb "/bin/mysqldump")
+ "--single-transaction" "--quick" "--default-character-set=utf8mb4"
+ (string-append "-p" (read-file "/etc/default/nextcloud-database-password"))
+ "-u" "nextcloud" "nextcloud")
+ (list restic "backup" "--no-cache" "--stdin" "--stdin-filename=nextcloud.sql"))
+
+ ;; These shouldn't be copied while Nextcloud is online. They're also
+ ;; not in the data folder, so they won't be in the snapshot below.
+ (run-pipeline
+ (list restic "backup" "--no-cache"
+ (string-append nextcloud-dir "/config")
+ (string-append nextcloud-dir "/themes")))
+
+ ;; Make sure everything is synced to disk so it's in our snapshot.
+ (run-pipeline
+ (list btrfs "filesystem" "sync"
+ (string-append nextcloud-data-partition "/" nextcloud-data-path)))
+ (run-pipeline
+ (list btrfs "subvolume" "snapshot" "-r" nextcloud-data-partition snapshot))
+
+ ;; At this point, the data directory is in the snapshot, so Nextcloud
+ ;; can be turned on again.
+ (nc-maintenance #f)
+
+ ;; We don't need files under preview/, as those are thumbnails from
+ ;; the Previews "app" and can be regenerated using `php -f occ
+ ;; preview:pre-generate`.
+ (run-pipeline
+ (list restic "backup" "--no-cache"
+ "--exclude=appdata_*/preview"
+ "--exclude=appdata_*/passwords/*Cache"
+ (string-append snapshot "/" nextcloud-data-path))))
+
+ (for-each (cut sigaction <> cleanup) (list SIGHUP SIGINT SIGTERM))
+ (with-exception-handler (cut cleanup #:rethrow <>) main)
+ (cleanup))))
+
(define-public %nextcloud-services
(list (simple-service 'nextcloud-https-server httpd-service-type
;; The certbot service redirects everything on port 80 to
@@ -106,21 +210,11 @@ Header always set Strict-Transport-Security \"max-age=15552000\"
(list #~(job "*/5 * * * *"
#$(program-file "nextcloud-cron-command"
#~(begin
- ;; `setgid' first while we're still root
- (setgid (group:gid (getgr "httpd")))
- (setuid (passwd:uid (getpw "httpd")))
- (chdir "/var/www/nextcloud")
;; Nextcloud News needs this to fetch HTTPS feeds.
(setenv "SSL_CERT_DIR" #$(file-append nss-certs "/etc/ssl/certs"))
(execl #$(file-append php "/bin/php") "php"
- "-c" #$%nextcloud-php.ini "cron.php"))))
-
- ;; Nextcloud backups
- ;; Requires: sudo, php, btrfs, mysqldump, rsync
- (let ((backup-script (local-file "files/nextcloud-backup" #:recursive? #t)))
- #~(job "0 6 * * *"
- (lambda ()
- ;; Pass through the php.ini file that allows us to
- ;; use Nextcloud's occ script.
- (execl #$backup-script "nextcloud-backup" #$%nextcloud-php.ini))
- (string-append #$backup-script " " #$%nextcloud-php.ini)))))))
+ "-c" #$%nextcloud-php.ini "cron.php")))
+ #:user "httpd")
+
+ ;; TODO: try `with-mail-out' from `(mcron redirect)'?
+ #~(job "0 6 * * *" #$nextcloud-backup-program)))))