summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xtw/services/files/nextcloud-backup68
-rw-r--r--tw/services/nextcloud.scm124
-rw-r--r--tw/system/lud.scm1
3 files changed, 109 insertions, 84 deletions
diff --git a/tw/services/files/nextcloud-backup b/tw/services/files/nextcloud-backup
deleted file mode 100755
index 4c533758..00000000
--- a/tw/services/files/nextcloud-backup
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/bin/sh -e
-# Nextcloud backup script, to run nightly.
-# Documentation on backups:
-# https://docs.nextcloud.com/server/latest/admin_manual/maintenance/backup.html
-# https://docs.nextcloud.com/server/latest/admin_manual/maintenance/restore.html
-# https://git.mdns.eu/nextcloud/passwords/-/wikis/Administrators/Backups
-
-. /etc/default/nextcloud-backup
-: "${DATABASE_PASSWORD:?You must pass the MySQL database password as DATABASE_PASSWORD}"
-
-php_ini=$1
-backup_dir=/var/backups/nextcloud/$(date -u '+%Y-%m-%d')
-nextcloud_dir=/var/www/nextcloud
-nextcloud_data_partition=/var/data # mountpoint of the partition containing Nextcloud data dir
-nextcloud_data_path=nextcloud # relative to $nextcloud_data_partition
-snapshot=$nextcloud_data_partition/tmp-nextcloud-backup
-
-nc_maintenance () {
- # Enable (--on) or disable (--off) Nextcloud's maintenance mode.
- sudo -nu httpd php ${php_ini:+-c "$php_ini"} "$nextcloud_dir/occ" maintenance:mode "$@"
-}
-
-# If there is a previous backup, compare against it later (so we don't have to
-# transfer every file).
-last_backup_dir=$(ls -1d "$(dirname "$backup_dir")"/????-??-?? | LC_ALL=C sort | tail -1)
-[ -d "$last_backup_dir" ] || unset last_backup_dir
-
-# Don't overwrite existing backups. mkdir will fail if $backup_dir exists.
-mkdir -m 750 "$backup_dir"
-
-# Always turn off maintenance mode and clean up the temporary snapshot on exit,
-# whether or not the backup succeeded.
-cleanup () {
- nc_maintenance --off || :
- if [ -d "$snapshot" ]; then
- btrfs subvolume delete -c "$snapshot" || :
- fi
-}
-trap cleanup EXIT HUP INT TERM # can't trap KILL
-
-# Turn Nextcloud off temporarily so the data doesn't change during the backup.
-nc_maintenance --on
-
-# Backup the database. This can only be done offline.
-mysqldump --single-transaction --default-character-set=utf8mb4 \
- -u nextcloud -p"$DATABASE_PASSWORD" nextcloud > "$backup_dir/nextcloud.sql"
-
-# These shouldn't be copied while Nextcloud is online.
-rsync -AUXHavx ${last_backup_dir+--link-dest="$last_backup_dir"} \
- "$nextcloud_dir/config" "$nextcloud_dir/themes" "$backup_dir"
-
-# Make sure everything is synced to disk so it's in our snapshot.
-btrfs filesystem sync "$nextcloud_data_partition/$nextcloud_data_path"
-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 --off
-
-# --link-dest is brittle (it only hardlinks to the old file if no metadata has
-# changed). Reflinks would be better, but rsync doesn't seem to support them.
-# 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`.
-rsync -AUXHavx --exclude='appdata_*/preview' --exclude='appdata_*/passwords/*Cache' \
- ${last_backup_dir+--link-dest="$last_backup_dir/data"} \
- "$snapshot/$nextcloud_data_path/" "$backup_dir/data"
-# Make sure everything is written out to the backup disk before we exit.
-btrfs filesystem sync "$backup_dir"
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)))))
diff --git a/tw/system/lud.scm b/tw/system/lud.scm
index 433219a4..11f7f3da 100644
--- a/tw/system/lud.scm
+++ b/tw/system/lud.scm
@@ -64,7 +64,6 @@ SSLSessionCacheTimeout 1200
(packages
(cons*
ffmpeg tor ; for video downloader
- mariadb ; for Nextcloud backup script
;; For Nextcloud. PHP modules must be installed in system
;; profile, as that's referred to in Nextcloud's php.ini.
php php-apcu php-imagick openssl ; curl is in `%base-system-packages'