Integrate consistent Nextcloud backups and recovery

This commit is contained in:
Fabio Scotto di Santolo
2026-10-04 17:00:48 +02:00
parent def3dbf313
commit 9f95e68190
20 changed files with 772 additions and 18 deletions

View File

@@ -50,6 +50,8 @@ atlas_zfs_dataset_photobook: media/photobook
atlas_mount_root: /zpool
atlas_manage_storage: true
atlas_manage_nextcloud: true
# Two local consistent bundles; long-term history stays in Borg/USB and ZFS.
atlas_nextcloud_backup_keep: 2
atlas_nextcloud_domain: cloud.fscotto.co
atlas_onlyoffice_domain: office.fscotto.co
# Resolved official amd64 images on 2026-10-03; updates are deliberate.
@@ -64,6 +66,17 @@ atlas_nextcloud_users:
- username: chiara
display_name: Chiara
password: "{{ vault_nextcloud_chiara_password }}"
atlas_nextcloud_external_mounts:
- name: Documenti
user: fabio
source: /zpool/archive/Documents
target: /mnt/archive-documents
readonly: false
- name: Foto iCloud
user: fabio
source: /zpool/archive/Pictures/iCloudPD
target: /mnt/archive-icloud
readonly: true
atlas_nextcloud_apps:
- id: groupfolders
version: 21.0.9

View File

@@ -16,9 +16,13 @@ atlas_nextcloud_image: ""
atlas_nextcloud_postgres_image: ""
atlas_nextcloud_redis_image: ""
atlas_onlyoffice_image: ""
atlas_nextcloud_backup_root: "{{ atlas_mount_root }}/backup/nextcloud"
atlas_nextcloud_backup_keep: 2
atlas_nextcloud_admin: admin
atlas_nextcloud_users: []
atlas_nextcloud_apps: []
# Existing Archive directories; never import into the internal data namespace.
atlas_nextcloud_external_mounts: []
atlas_nextcloud_services:
- atlas-nextcloud-db.service
- atlas-nextcloud-redis.service
@@ -147,7 +151,9 @@ atlas_monitor_effective_timers: >-
atlas_monitor_effective_failure_units: >-
{{ atlas_monitor_failure_units
+ (['atlas-prometheus-pull.service']
if atlas_manage_prometheus_backup_pull | bool else []) }}
if atlas_manage_prometheus_backup_pull | bool else [])
+ (['atlas-nextcloud-backup.service', 'atlas-nextcloud-backup-recovery.service']
if atlas_manage_nextcloud | bool else []) }}
atlas_monitor_remote_capacity: {}
atlas_monitor_pool_warning_percent: 80
atlas_monitor_pool_critical_percent: 90

View File

@@ -35,6 +35,9 @@
- name: Import Atlas offline USB backup tasks
ansible.builtin.import_tasks: usb_backup.yml
- name: Import recurring Nextcloud backup preparation
ansible.builtin.import_tasks: nextcloud_backup.yml
- name: Import Atlas Prometheus backup pull identity tasks
ansible.builtin.import_tasks: prometheus_pull_identity.yml

View File

@@ -39,6 +39,10 @@
(atlas_nextcloud_users | map(attribute='password') | list) }}
no_log: true
- name: Prepare access to declared existing Archive directories
ansible.builtin.include_tasks: nextcloud_external_access.yml
when: atlas_nextcloud_external_mounts | length > 0
- name: Verify the existing application-data parent is mounted
community.general.zfs_facts:
name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_app_data }}"
@@ -197,7 +201,8 @@
owner: "{{ atlas_admin_username }}"
group: "{{ atlas_admin_group }}"
mode: "0644"
loop: [atlas-nextcloud-cron.service, atlas-nextcloud-cron.timer]
loop: [atlas-nextcloud-cron.service, atlas-nextcloud-cron.timer,
atlas-nextcloud-external-scan.service, atlas-nextcloud-external-scan.timer]
register: atlas_nextcloud_cron_units
- name: Manage and verify rootless Nextcloud services
@@ -227,7 +232,13 @@
scope: user
name: "{{ item }}"
state: >-
{{ 'restarted' if (atlas_nextcloud_quadlets is changed or
{{ 'restarted' if (
atlas_nextcloud_quadlets.results |
selectattr('item', 'equalto', item | replace('.service', '.container')) |
selectattr('changed') | list | length > 0 or
atlas_nextcloud_quadlets.results |
selectattr('item', 'equalto', 'atlas-nextcloud.network') |
selectattr('changed') | list | length > 0 or
atlas_nextcloud_private_configuration is changed or
atlas_nextcloud_secret_files is changed) else 'started' }}
loop: "{{ atlas_nextcloud_services }}"
@@ -299,6 +310,14 @@
state: "{{ 'restarted' if atlas_nextcloud_cron_units is changed else 'started' }}"
enabled: true
- name: Enable periodic targeted Archive discovery
ansible.builtin.systemd:
scope: user
name: atlas-nextcloud-external-scan.timer
state: "{{ 'restarted' if atlas_nextcloud_cron_units is changed else 'started' }}"
enabled: true
when: atlas_nextcloud_external_mounts | length > 0
- name: Verify ONLYOFFICE local health without publishing the domain
ansible.builtin.uri:
url: "http://127.0.0.1:{{ atlas_onlyoffice_http_port }}/healthcheck"

View File

@@ -169,3 +169,18 @@
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, background:cron]
when: atlas_nextcloud_background_mode.stdout | trim != 'cron'
changed_when: true
- name: Enable shipped external storage support when required
ansible.builtin.command:
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, app:enable, files_external]
when:
- atlas_nextcloud_external_mounts | length > 0
- "'files_external' not in (atlas_nextcloud_current_apps.stdout | from_json).enabled"
changed_when: true
- name: Maintain only the declared Archive mounts
ansible.builtin.include_tasks: nextcloud_external_mount.yml
loop: "{{ atlas_nextcloud_external_mounts }}"
loop_control:
loop_var: atlas_nextcloud_mount
label: "{{ atlas_nextcloud_mount.name }}"

View File

@@ -0,0 +1,55 @@
---
- name: Manage recurring consistent Nextcloud backup preparation
tags: [atlas, nextcloud_backup]
when: atlas_manage_nextcloud | bool
block:
- name: Validate private backup scope and local bundle retention
ansible.builtin.assert:
that:
- atlas_nextcloud_backup_root == atlas_mount_root ~ '/backup/nextcloud'
- atlas_nextcloud_backup_keep | int >= 2
- atlas_manage_borg_backup | bool
- atlas_manage_usb_backup | bool
- name: Install recurring backup helper with shell syntax validation
ansible.builtin.template:
src: atlas-nextcloud-backup.sh.j2
dest: /usr/local/sbin/atlas-nextcloud-backup
owner: root
group: root
mode: "0750"
validate: /bin/bash -n %s
- name: Install Nextcloud backup preparation and boot recovery units
ansible.builtin.template:
src: "{{ item }}.j2"
dest: "/etc/systemd/system/{{ item }}"
owner: root
group: root
mode: "0644"
loop: [atlas-nextcloud-backup.service, atlas-nextcloud-backup-recovery.service]
- name: Create backup dependency drop-in directories
ansible.builtin.file:
path: "/etc/systemd/system/{{ item }}.d"
state: directory
owner: root
group: root
mode: "0755"
loop: [atlas-borg-backup.service, atlas-usb-backup.service]
- name: Require a fresh consistent bundle before offsite and manual USB backups
ansible.builtin.template:
src: atlas-nextcloud-backup-dependency.conf.j2
dest: "/etc/systemd/system/{{ item }}.d/nextcloud.conf"
owner: root
group: root
mode: "0644"
loop: [atlas-borg-backup.service, atlas-usb-backup.service]
- name: Reload systemd and enable interruption recovery without running a backup
ansible.builtin.systemd:
daemon_reload: true
name: atlas-nextcloud-backup-recovery.service
enabled: true
when: not ansible_check_mode

View File

@@ -0,0 +1,100 @@
---
- name: Restrict external storage to explicit Archive directories
ansible.builtin.assert:
that:
- item.source in [atlas_archive_mountpoint ~ '/Documents', atlas_icloudpd_photos_dir]
- item.target is match('^/mnt/archive-[a-z]+$')
- item.name is match('^[A-Za-z][A-Za-z ]+$')
- item.readonly is boolean
- item.user in (atlas_nextcloud_users | map(attribute='username') | list)
- item.source != atlas_icloudpd_photos_dir or item.readonly
loop: "{{ atlas_nextcloud_external_mounts }}"
- name: Inspect existing sources without creating or moving data
ansible.builtin.stat:
path: "{{ item.source }}"
follow: false
loop: "{{ atlas_nextcloud_external_mounts }}"
register: atlas_nextcloud_external_sources
- name: Refuse missing sources and symlinks
ansible.builtin.assert:
that:
- item.stat.isdir | default(false)
- not (item.stat.islnk | default(false))
loop: "{{ atlas_nextcloud_external_sources.results }}"
loop_control:
label: "{{ item.item.source }}"
- name: Verify the Archive dataset before modifying its ACL capability
community.general.zfs_facts:
name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
properties: name,mounted,mountpoint
register: atlas_nextcloud_external_dataset
- name: Refuse an absent or unmounted Archive dataset
ansible.builtin.assert:
that:
- atlas_nextcloud_external_dataset.ansible_facts.ansible_zfs_datasets | length == 1
- atlas_nextcloud_external_dataset.ansible_facts.ansible_zfs_datasets[0].mounted == 'yes'
- atlas_nextcloud_external_dataset.ansible_facts.ansible_zfs_datasets[0].mountpoint == atlas_archive_mountpoint
- name: Enable persistent POSIX ACL support on the verified Archive dataset
community.general.zfs:
name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_archive }}"
state: present
extra_zfs_properties:
acltype: posix
- name: Derive actual rootless web UID for narrowly scoped Archive ACLs
become_user: "{{ atlas_admin_username }}"
environment:
XDG_RUNTIME_DIR: "/run/user/{{ atlas_admin_uid }}"
ansible.builtin.command:
argv:
- podman
- unshare
- python3
- -c
- >-
print(next(int(b)+33-int(a) for a,b,n in
(l.split() for l in open('/proc/self/uid_map')) if int(a)<=33<int(a)+int(n)))
register: atlas_nextcloud_external_uid
changed_when: false
check_mode: false
- name: Grant web user access only inside the declared sources
ansible.posix.acl:
path: "{{ item.source }}"
entity: "{{ atlas_nextcloud_external_uid.stdout | trim }}"
etype: user
permissions: "{{ 'rX' if item.readonly else 'rwX' }}"
recursive: true
follow: false
state: present
loop: "{{ atlas_nextcloud_external_mounts }}"
- name: Inherit web access on new files and directories
ansible.posix.acl:
path: "{{ item.source }}"
entity: "{{ atlas_nextcloud_external_uid.stdout | trim }}"
etype: user
permissions: "{{ 'rX' if item.readonly else 'rwX' }}"
default: true
recursive: true
follow: false
state: present
loop: "{{ atlas_nextcloud_external_mounts }}"
- name: Preserve administrator access to documents created through Nextcloud
ansible.posix.acl:
path: "{{ item.source }}"
entity: "{{ atlas_admin_uid }}"
etype: user
permissions: rwX
default: true
recursive: true
follow: false
state: present
loop: "{{ atlas_nextcloud_external_mounts }}"
when: not item.readonly

View File

@@ -0,0 +1,94 @@
---
- name: Inspect current system mounts without exposing credentials
ansible.builtin.command:
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, files_external:list, --output=json]
register: atlas_nextcloud_mount_list
changed_when: false
no_log: true
- name: Select only the matching mount name
ansible.builtin.set_fact:
atlas_nextcloud_matching_mounts: >-
{{ atlas_nextcloud_mount_list.stdout | from_json |
selectattr('mount_point', 'equalto', '/' ~ atlas_nextcloud_mount.name) | list }}
no_log: true
- name: Refuse duplicates or repurposing of existing unrelated storage
ansible.builtin.assert:
that:
- atlas_nextcloud_matching_mounts | length <= 1
- >-
atlas_nextcloud_matching_mounts | length == 0 or
(atlas_nextcloud_matching_mounts[0].configuration.datadir | default('') == atlas_nextcloud_mount.target
and atlas_nextcloud_matching_mounts[0].storage == '\\OC\\Files\\Storage\\Local')
fail_msg: Existing storage conflicts with the declared Archive mount; refusing an implicit replacement.
- name: Create an absent local mount restricted to its declared user
ansible.builtin.command:
argv:
- podman
- exec
- --user
- '33'
- atlas-nextcloud
- php
- occ
- files_external:create
- "{{ atlas_nextcloud_mount.name }}"
- local
- null::null
- --config
- "datadir={{ atlas_nextcloud_mount.target }}"
- --applicable-user
- "{{ atlas_nextcloud_mount.user }}"
- --output=json
when: atlas_nextcloud_matching_mounts | length == 0
register: atlas_nextcloud_mount_created
changed_when: true
- name: Record the managed mount ID and options
ansible.builtin.set_fact:
atlas_nextcloud_mount_id: >-
{{ atlas_nextcloud_mount_created.stdout | trim if atlas_nextcloud_matching_mounts | length == 0
else atlas_nextcloud_matching_mounts[0].mount_id }}
atlas_nextcloud_mount_options: >-
{{ {} if atlas_nextcloud_matching_mounts | length == 0 else atlas_nextcloud_matching_mounts[0].options }}
- name: Restrict the managed mount to exactly its declared user
ansible.builtin.command:
argv: >-
{{ ['podman', 'exec', '--user', '33', 'atlas-nextcloud', 'php', 'occ',
'files_external:applicable', atlas_nextcloud_mount_id | string,
'--add-user=' ~ atlas_nextcloud_mount.user] +
(atlas_nextcloud_matching_mounts[0].applicable_groups |
map('regex_replace', '^', '--remove-group=') | list) +
(atlas_nextcloud_matching_mounts[0].applicable_users |
reject('equalto', atlas_nextcloud_mount.user) |
map('regex_replace', '^', '--remove-user=') | list) }}
when:
- atlas_nextcloud_matching_mounts | length > 0
- >-
atlas_nextcloud_matching_mounts[0].applicable_groups | length > 0 or
atlas_nextcloud_matching_mounts[0].applicable_users != [atlas_nextcloud_mount.user]
changed_when: true
- name: Maintain read-only photos and external change detection
ansible.builtin.command:
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, files_external:option,
"{{ atlas_nextcloud_mount_id }}", "{{ item.key }}", "{{ item.value | to_json }}"]
loop:
- {key: readonly, value: "{{ atlas_nextcloud_mount.readonly }}"}
- {key: filesystem_check_changes, value: 1}
- {key: enable_sharing, value: false}
# Nextcloud persists option values as strings ("1" / "" for booleans).
when: >-
item.key not in atlas_nextcloud_mount_options or
atlas_nextcloud_mount_options[item.key] | string !=
(('1' if item.value else '') if item.value is boolean else item.value | string)
changed_when: true
- name: Verify the managed local storage is accessible
ansible.builtin.command:
argv: [podman, exec, --user, '33', atlas-nextcloud, php, occ, files_external:verify,
"{{ atlas_nextcloud_mount_id }}"]
changed_when: false

View File

@@ -10,6 +10,7 @@
properties:
compression: zstd
mountpoint: "{{ atlas_archive_mountpoint }}"
acltype: posix
- name: "{{ atlas_zfs_pool }}/{{ atlas_zfs_dataset_services }}"
mountpoint: "{{ atlas_services_mountpoint }}"
owner: "{{ atlas_admin_username }}"

View File

@@ -0,0 +1,3 @@
[Unit]
Requires=atlas-nextcloud-backup.service
After=atlas-nextcloud-backup.service

View File

@@ -0,0 +1,19 @@
[Unit]
Description=Recover interrupted Nextcloud backup preparation after boot
Requires=zfs.target user@{{ atlas_admin_uid }}.service
After=zfs.target user@{{ atlas_admin_uid }}.service
{% if atlas_manage_monitoring | bool %}
OnFailure=atlas-monitor-failure@%n.service
{% endif %}
[Service]
Type=oneshot
User=root
UMask=0077
StateDirectory=atlas-nextcloud-backup
StateDirectoryMode=0700
ExecStart=/usr/local/sbin/atlas-nextcloud-backup --recover
TimeoutStartSec=5min
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,21 @@
[Unit]
Description=Prepare a consistent Nextcloud bundle before Atlas backups
Requires=zfs.target user@{{ atlas_admin_uid }}.service
After=zfs.target user@{{ atlas_admin_uid }}.service atlas-nextcloud-backup-recovery.service
{% if atlas_manage_monitoring | bool %}
OnFailure=atlas-monitor-failure@%n.service
{% endif %}
[Service]
Type=oneshot
User=root
UMask=0077
StateDirectory=atlas-nextcloud-backup
StateDirectoryMode=0700
ExecStart=/usr/local/sbin/atlas-nextcloud-backup
ExecStopPost=/usr/local/sbin/atlas-nextcloud-backup --recover
TimeoutStartSec=3h
TimeoutStopSec=5min
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -Eeuo pipefail
export PATH=/usr/sbin:/usr/bin:/sbin:/bin
umask 077
readonly dataset={{ atlas_nextcloud_dataset | quote }}
readonly source_root={{ atlas_nextcloud_root | quote }}
readonly backup_root={{ atlas_nextcloud_backup_root | quote }}
readonly state=/var/lib/atlas-nextcloud-backup
readonly keep={{ atlas_nextcloud_backup_keep | int }}
readonly owner={{ atlas_admin_username | quote }}
readonly uid={{ atlas_admin_uid | int }}
user_run() {
runuser -u "$owner" -- env XDG_RUNTIME_DIR="/run/user/$uid" \
DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$uid/bus" "$@"
}
occ() { user_run podman exec --user 33 atlas-nextcloud php occ "$@"; }
exec 8>/run/lock/atlas-nextcloud-backup.lock
flock 8
exec 9>/run/lock/atlas-zfs-snapshot.lock
mkdir -p "$state"
chmod 0700 "$state"
resume() {
[[ -e "$state/paused" ]] || return 0
user_run systemctl --user start atlas-nextcloud.service atlas-onlyoffice.service
local ready=false
for _ in {1..60}; do
if occ maintenance:mode --off >/dev/null 2>&1; then ready=true; break; fi
sleep 2
done
[[ "$ready" == true ]] || { echo 'Nextcloud resume failed; recovery marker retained' >&2; return 1; }
user_run systemctl --user start atlas-nextcloud-cron.timer
# Persist maintenance-off before clearing durable interruption ownership.
sync -f "$source_root/app"
rm "$state/paused"
sync -f "$state"
echo 'Nextcloud/Office resumed and cron timer restored'
}
recover() {
resume || return 1
[[ -e "$state/stamp" ]] || return 0
local stamp snapshot mount source
stamp=$(cat "$state/stamp")
[[ "$stamp" =~ ^[0-9]{8}T[0-9]{6}Z-[0-9]+$ ]] || return 65
snapshot="nc-backup-$stamp"
flock 9
for component in files app; do
mount="$source_root/$component/.zfs/snapshot/$snapshot"
source=$(findmnt -rn -M "$mount" -o SOURCE || true)
if [[ -n "$source" ]]; then
[[ "$source" == "$dataset/$component@$snapshot" ]] || return 65
umount "$mount" || return 1
fi
done
if zfs list -H -t snapshot "$dataset@$snapshot" >/dev/null 2>&1; then
zfs destroy -r "$dataset@$snapshot" || return 1
fi
flock -u 9
# Only this job's private, unpublished staging directory can be removed.
rm -rf -- "$backup_root/.partial-$stamp"
rm "$state/stamp"
}
if [[ "${1:-}" == --recover ]]; then recover; exit; fi
recover
[[ "$(zfs get -H -o value mounted "$dataset")" == yes ]]
[[ "$(zfs get -H -o value mountpoint "$dataset")" == "$source_root" ]]
[[ "$(zfs get -H -o value mounted {{ (atlas_zfs_pool ~ '/backup') | quote }})" == yes ]]
[[ "$(zfs get -H -o value mountpoint {{ (atlas_zfs_pool ~ '/backup') | quote }})" == {{ (atlas_mount_root ~ '/backup') | quote }} ]]
for component in app files; do
[[ "$(zfs get -H -o value mounted "$dataset/$component")" == yes ]]
[[ "$(zfs get -H -o value mountpoint "$dataset/$component")" == "$source_root/$component" ]]
done
for unit in atlas-nextcloud.service atlas-onlyoffice.service atlas-nextcloud-cron.timer; do
user_run systemctl --user is-active --quiet "$unit"
done
occ status --output=json | python3 -c 'import json,sys; s=json.load(sys.stdin); assert s["installed"] and not s["maintenance"] and not s["needsDbUpgrade"]'
mkdir -p "$backup_root/versions"
chmod 0700 "$backup_root" "$backup_root/versions"
stamp="$(date -u +%Y%m%dT%H%M%SZ)-$$"
snapshot="nc-backup-$stamp"
stage="$backup_root/.partial-$stamp"
mkdir "$stage"
printf '%s\n' "$stamp" > "$state/stamp"
sync -f "$state"
cleanup() {
local rc=$?
trap - EXIT
if ! recover; then rc=1; fi
exit "$rc"
}
trap cleanup EXIT
trap 'exit 143' HUP INT TERM
# Wait for snapshot serialization before interrupting application availability.
flock 9
touch "$state/paused"
sync -f "$state"
user_run systemctl --user stop atlas-nextcloud-cron.timer atlas-nextcloud-cron.service
occ maintenance:mode --on
user_run systemctl --user stop atlas-onlyoffice.service atlas-nextcloud.service
user_run podman exec atlas-nextcloud-db pg_dumpall -U nextcloud --globals-only > "$stage/postgres-globals.sql"
user_run podman exec atlas-nextcloud-db pg_dump -U nextcloud -d nextcloud --format=custom > "$stage/database.dump"
zfs snapshot -r "$dataset@$snapshot"
flock -u 9
resume
# Copy immutable snapshot views; hashing and transfer never extend the outage.
previous=$(readlink -f "$backup_root/latest" 2>/dev/null || true)
for component in app files; do
args=(-aHAX)
if [[ "$previous" == "$backup_root/versions/"* && -d "$previous/$component" ]]; then
args+=("--link-dest=$previous/$component")
fi
if [[ "$component" == app ]]; then args+=(--exclude=/data); fi
rsync "${args[@]}" "$source_root/$component/.zfs/snapshot/$snapshot/" "$stage/$component/"
done
user_run podman exec -i atlas-nextcloud-db pg_restore --list < "$stage/database.dump" > "$stage/database-toc.txt"
user_run podman inspect --format '{% raw %}{{.ImageName}}{% endraw %}' atlas-nextcloud atlas-nextcloud-db atlas-nextcloud-redis atlas-onlyoffice > "$stage/images.txt"
(cd "$stage"; find app files -type f -exec sha256sum '{}' +; sha256sum database.dump postgres-globals.sql images.txt) > "$stage/SHA256SUMS"
(cd "$stage"; sha256sum --quiet --check SHA256SUMS)
printf 'snapshot=%s@%s\ncreated_utc=%s\n' "$dataset" "$snapshot" "$stamp" > "$stage/manifest.txt"
mv "$stage" "$backup_root/versions/$stamp"
ln -s "versions/$stamp" "$backup_root/.latest-$stamp"
mv -Tf "$backup_root/.latest-$stamp" "$backup_root/latest"
sync -f "$backup_root"
# Prune only timestamped job-owned versions after verified atomic publication.
mapfile -t versions < <(find "$backup_root/versions" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | grep -E '^[0-9]{8}T[0-9]{6}Z-[0-9]+$' | sort -r)
{% raw %}
for ((index=keep; index<${#versions[@]}; index++)); do
{% endraw %}
rm -rf -- "$backup_root/versions/${versions[$index]}"
done
echo "Published verified consistent Nextcloud bundle $stamp; local retention=$keep"

View File

@@ -0,0 +1,12 @@
[Unit]
Description=Discover existing Archive documents and iCloud photos in Nextcloud
Requires=atlas-nextcloud.service
After=atlas-nextcloud.service
[Service]
Type=oneshot
{% for mount in atlas_nextcloud_external_mounts %}
ExecStart=/usr/bin/podman exec --user 33 atlas-nextcloud php occ files:scan "--path={{ mount.user }}/files/{{ mount.name }}" --quiet
{% endfor %}
TimeoutStartSec=90min
NoNewPrivileges=true

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Periodic discovery of Archive changes made outside Nextcloud
[Timer]
OnBootSec=15min
OnUnitInactiveSec=1h
Unit=atlas-nextcloud-external-scan.service
[Install]
WantedBy=timers.target

View File

@@ -2,7 +2,7 @@
Description=Atlas Nextcloud
Requires=atlas-nextcloud-db.service atlas-nextcloud-redis.service
After=atlas-nextcloud-db.service atlas-nextcloud-redis.service
RequiresMountsFor={{ atlas_nextcloud_root }}/app {{ atlas_nextcloud_root }}/files
RequiresMountsFor={{ atlas_nextcloud_root }}/app {{ atlas_nextcloud_root }}/files{% for mount in atlas_nextcloud_external_mounts %} {{ mount.source }}{% endfor %}
[Container]
ContainerName=atlas-nextcloud
@@ -26,6 +26,10 @@ Environment=PHP_UPLOAD_LIMIT=2G
Volume={{ atlas_nextcloud_root }}/app:/var/www/html:Z
Volume={{ atlas_nextcloud_root }}/files:/var/www/html/data:Z
Volume={{ atlas_nextcloud_app_cache }}:/mnt/atlas-apps:ro,z
{% for mount in atlas_nextcloud_external_mounts %}
# Shared Archive label, not the private :Z label used for internal state.
Volume={{ mount.source }}:{{ mount.target }}:{{ 'ro' if mount.readonly else 'rw' }},z
{% endfor %}
Volume={{ atlas_nextcloud_private_dir }}/postgres-password:/run/secrets/postgres-password:ro,z
Volume={{ atlas_nextcloud_private_dir }}/admin-password:/run/secrets/admin-password:ro,z
Volume={{ atlas_nextcloud_private_dir }}/redis-password:/run/secrets/redis-password:ro,z