From 9eae00f2a76dc1bf62c70080805ff2e1522c1a7a Mon Sep 17 00:00:00 2001 From: Timo Wilken Date: Tue, 10 Oct 2023 00:24:00 +0200 Subject: Store Nextcloud backups in restic repo Replace the Nextcloud backup shell script with a Guile program that writes to a restic repository instead. --- tw/services/nextcloud.scm | 124 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 109 insertions(+), 15 deletions(-) (limited to 'tw/services/nextcloud.scm') 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))))) -- cgit v1.2.3