From 7fb0d2212a9ce117d394192faf8f9922ff2b48b8 Mon Sep 17 00:00:00 2001 From: julien Date: Mon, 17 Nov 2025 18:51:08 +0100 Subject: [PATCH] Updates --- nextcloud-aio/.gitattributes | 1 + nextcloud-aio/.gitignore | 15 + nextcloud-aio/Containers/alpine/Dockerfile | 7 + nextcloud-aio/Containers/apache/Caddyfile | 75 + nextcloud-aio/Containers/apache/Dockerfile | 91 + .../Containers/apache/healthcheck.sh | 5 + .../Containers/apache/nextcloud.conf | 54 + nextcloud-aio/Containers/apache/start.sh | 77 + .../Containers/apache/supervisord.conf | 23 + .../Containers/borgbackup/Dockerfile | 28 + .../Containers/borgbackup/backupscript.sh | 619 ++ .../Containers/borgbackup/borg_excludes | 11 + nextcloud-aio/Containers/borgbackup/start.sh | 67 + nextcloud-aio/Containers/clamav/Dockerfile | 29 + .../Containers/clamav/healthcheck.sh | 9 + nextcloud-aio/Containers/clamav/start.sh | 10 + .../Containers/clamav/supervisord.conf | 23 + nextcloud-aio/Containers/collabora/Dockerfile | 14 + .../Containers/collabora/healthcheck.sh | 7 + .../Containers/docker-socket-proxy/Dockerfile | 22 + .../docker-socket-proxy/haproxy.cfg | 68 + .../docker-socket-proxy/healthcheck.sh | 4 + .../Containers/docker-socket-proxy/start.sh | 23 + .../Containers/domaincheck/Dockerfile | 21 + .../Containers/domaincheck/lighttpd.conf | 20 + nextcloud-aio/Containers/domaincheck/start.sh | 23 + .../Containers/fulltextsearch/Dockerfile | 26 + .../Containers/fulltextsearch/healthcheck.sh | 3 + nextcloud-aio/Containers/imaginary/Dockerfile | 46 + .../Containers/imaginary/healthcheck.sh | 3 + nextcloud-aio/Containers/imaginary/start.sh | 8 + .../Containers/mastercontainer/Caddyfile | 37 + .../Containers/mastercontainer/Dockerfile | 132 + .../backup-time-file-watcher.sh | 30 + .../Containers/mastercontainer/cron.sh | 75 + .../mastercontainer/daily-backup.sh | 133 + .../Containers/mastercontainer/healthcheck.sh | 10 + .../mastercontainer/mastercontainer.conf | 62 + .../mastercontainer/session-deduplicator.sh | 22 + .../Containers/mastercontainer/start.sh | 390 + .../mastercontainer/supervisord.conf | 64 + nextcloud-aio/Containers/nextcloud/Dockerfile | 266 + .../nextcloud/config/aio.config.php | 5 + .../nextcloud/config/apcu.config.php | 4 + .../nextcloud/config/apps.config.php | 21 + .../nextcloud/config/postgres.config.php | 9 + .../nextcloud/config/proxy.config.php | 13 + .../nextcloud/config/redis.config.php | 25 + .../nextcloud/config/reverse-proxy.config.php | 20 + .../Containers/nextcloud/config/s3.config.php | 34 + .../nextcloud/config/smtp.config.php | 20 + .../nextcloud/config/swift.config.php | 31 + nextcloud-aio/Containers/nextcloud/cron.sh | 18 + .../Containers/nextcloud/entrypoint.sh | 952 ++ .../Containers/nextcloud/healthcheck.sh | 15 + .../Containers/nextcloud/notify-all.sh | 27 + nextcloud-aio/Containers/nextcloud/notify.sh | 35 + nextcloud-aio/Containers/nextcloud/root.motd | 4 + .../Containers/nextcloud/run-exec-commands.sh | 36 + nextcloud-aio/Containers/nextcloud/start.sh | 173 + .../Containers/nextcloud/supervisord.conf | 45 + .../Containers/nextcloud/upgrade.exclude | 5 + .../Containers/notify-push/Dockerfile | 25 + .../Containers/notify-push/healthcheck.sh | 7 + nextcloud-aio/Containers/notify-push/start.sh | 84 + .../Containers/onlyoffice/Dockerfile | 11 + .../Containers/onlyoffice/healthcheck.sh | 3 + .../Containers/postgresql/Dockerfile | 47 + .../Containers/postgresql/healthcheck.sh | 7 + .../Containers/postgresql/init-user-db.sh | 15 + nextcloud-aio/Containers/postgresql/start.sh | 198 + nextcloud-aio/Containers/redis/Dockerfile | 24 + nextcloud-aio/Containers/redis/healthcheck.sh | 3 + nextcloud-aio/Containers/redis/start.sh | 17 + .../Containers/talk-recording/Dockerfile | 61 + .../Containers/talk-recording/healthcheck.sh | 3 + .../Containers/talk-recording/recording.conf | 123 + .../Containers/talk-recording/start.sh | 64 + nextcloud-aio/Containers/talk/Dockerfile | 110 + nextcloud-aio/Containers/talk/healthcheck.sh | 7 + nextcloud-aio/Containers/talk/server.conf.in | 342 + nextcloud-aio/Containers/talk/start.sh | 116 + .../Containers/talk/supervisord.conf | 37 + .../Containers/watchtower/Dockerfile | 19 + nextcloud-aio/Containers/watchtower/start.sh | 26 + .../Containers/whiteboard/Dockerfile | 22 + .../Containers/whiteboard/healthcheck.sh | 4 + nextcloud-aio/Containers/whiteboard/start.sh | 17 + nextcloud-aio/LICENSE | 661 ++ nextcloud-aio/app/.editorconfig | 19 + nextcloud-aio/app/appinfo/info.xml | 23 + nextcloud-aio/app/composer/autoload.php | 7 + nextcloud-aio/app/composer/composer.json | 13 + nextcloud-aio/app/composer/composer.lock | 18 + .../app/composer/composer/ClassLoader.php | 481 + .../composer/composer/InstalledVersions.php | 337 + nextcloud-aio/app/composer/composer/LICENSE | 21 + .../composer/composer/autoload_classmap.php | 11 + .../composer/composer/autoload_namespaces.php | 9 + .../app/composer/composer/autoload_psr4.php | 10 + .../app/composer/composer/autoload_real.php | 46 + .../app/composer/composer/autoload_static.php | 37 + .../app/composer/composer/installed.json | 5 + .../app/composer/composer/installed.php | 23 + nextcloud-aio/app/lib/Settings/Admin.php | 83 + nextcloud-aio/app/readme.md | 7 + nextcloud-aio/app/templates/admin.php | 16 + .../borgbackup-viewer/borgbackup-viewer.json | 71 + .../borgbackup-viewer/readme.md | 17 + .../community-containers/caddy/caddy.json | 56 + .../community-containers/caddy/readme.md | 22 + .../calcardbackup/calcardbackup.json | 37 + .../calcardbackup/readme.md | 15 + .../container-management.json | 41 + .../container-management/readme.md | 15 + .../community-containers/dlna/dlna.json | 39 + .../community-containers/dlna/readme.md | 14 + .../facerecognition/facerecognition.json | 40 + .../facerecognition/readme.md | 34 + .../fail2ban/fail2ban.json | 42 + .../community-containers/fail2ban/readme.md | 14 + .../helloworld/helloworld.json | 12 + .../community-containers/helloworld/readme.md | 8 + .../jellyfin/jellyfin.json | 40 + .../community-containers/jellyfin/readme.md | 20 + .../jellyseerr/jellyseerr.json | 35 + .../community-containers/jellyseerr/readme.md | 16 + .../languagetool/languagetool.json | 16 + .../languagetool/readme.md | 13 + .../libretranslate/libretranslate.json | 34 + .../libretranslate/readme.md | 22 + .../community-containers/lldap/lldap.json | 47 + .../community-containers/lldap/readme.md | 91 + .../local-ai/local-ai.json | 46 + .../community-containers/local-ai/readme.md | 21 + .../community-containers/makemkv/makemkv.json | 59 + .../community-containers/makemkv/readme.md | 20 + .../memories/memories.json | 39 + .../community-containers/memories/readme.md | 12 + .../community-containers/minio/minio.json | 41 + .../community-containers/minio/readme.md | 18 + .../nextcloud-exporter.json | 35 + .../nextcloud-exporter/readme.md | 72 + .../community-containers/nocodb/nocodb.json | 44 + .../community-containers/nocodb/readme.md | 28 + .../community-containers/npmplus/npmplus.json | 32 + .../community-containers/npmplus/readme.md | 20 + .../community-containers/pi-hole/pi-hole.json | 57 + .../community-containers/pi-hole/readme.md | 18 + .../community-containers/plex/plex.json | 42 + .../community-containers/plex/readme.md | 17 + nextcloud-aio/community-containers/readme.md | 20 + .../community-containers/scrutiny/readme.md | 16 + .../scrutiny/scrutiny.json | 56 + .../community-containers/smbserver/readme.md | 15 + .../smbserver/smbserver.json | 60 + .../community-containers/stalwart/readme.md | 22 + .../stalwart/stalwart.json | 75 + .../vaultwarden/readme.md | 17 + .../vaultwarden/vaultwarden.json | 49 + nextcloud-aio/compose-example.yaml | 79 + nextcloud-aio/compose.yaml | 48 + nextcloud-aio/develop.md | 72 + nextcloud-aio/docker-ipv6-support.md | 44 + nextcloud-aio/docker-rootless.md | 39 + nextcloud-aio/local-instance.md | 31 + nextcloud-aio/manual-install/latest.yml | 512 ++ nextcloud-aio/manual-install/readme.md | 54 + nextcloud-aio/manual-install/sample.conf | 42 + nextcloud-aio/manual-install/update-yaml.sh | 163 + nextcloud-aio/manual-upgrade.md | 122 + nextcloud-aio/migration.md | 105 + nextcloud-aio/multiple-instances.md | 226 + .../nextcloud-aio-helm-chart/Chart.yaml | 13 + .../nextcloud-aio-helm-chart/readme.md | 45 + .../nextcloud-aio-apache-deployment.yaml | 107 + ...loud-aio-apache-persistentvolumeclaim.yaml | 16 + .../nextcloud-aio-apache-service.yaml | 23 + .../nextcloud-aio-clamav-deployment.yaml | 100 + ...loud-aio-clamav-persistentvolumeclaim.yaml | 18 + .../nextcloud-aio-clamav-service.yaml | 19 + .../nextcloud-aio-collabora-deployment.yaml | 67 + .../nextcloud-aio-collabora-service.yaml | 19 + .../nextcloud-aio-database-deployment.yaml | 108 + ...o-database-dump-persistentvolumeclaim.yaml | 16 + ...ud-aio-database-persistentvolumeclaim.yaml | 16 + .../nextcloud-aio-database-service.yaml | 17 + ...o-elasticsearch-persistentvolumeclaim.yaml | 18 + ...xtcloud-aio-fulltextsearch-deployment.yaml | 85 + .../nextcloud-aio-fulltextsearch-service.yaml | 19 + .../nextcloud-aio-imaginary-deployment.yaml | 69 + .../nextcloud-aio-imaginary-service.yaml | 19 + .../nextcloud-aio-namespace-namespace.yaml | 11 + .../nextcloud-aio-networkpolicy.yaml | 36 + ...-nextcloud-data-persistentvolumeclaim.yaml | 18 + .../nextcloud-aio-nextcloud-deployment.yaml | 241 + ...d-aio-nextcloud-persistentvolumeclaim.yaml | 16 + .../nextcloud-aio-nextcloud-service.yaml | 20 + ...trusted-cacerts-persistentvolumeclaim.yaml | 16 + .../nextcloud-aio-notify-push-deployment.yaml | 93 + .../nextcloud-aio-notify-push-service.yaml | 17 + .../nextcloud-aio-onlyoffice-deployment.yaml | 73 + ...-aio-onlyoffice-persistentvolumeclaim.yaml | 18 + .../nextcloud-aio-onlyoffice-service.yaml | 19 + .../nextcloud-aio-redis-deployment.yaml | 76 + ...cloud-aio-redis-persistentvolumeclaim.yaml | 16 + .../nextcloud-aio-redis-service.yaml | 17 + .../nextcloud-aio-talk-deployment.yaml | 87 + ...xtcloud-aio-talk-recording-deployment.yaml | 82 + ...-talk-recording-persistentvolumeclaim.yaml | 18 + .../nextcloud-aio-talk-recording-service.yaml | 19 + .../templates/nextcloud-aio-talk-service.yaml | 44 + .../nextcloud-aio-whiteboard-deployment.yaml | 79 + .../nextcloud-aio-whiteboard-service.yaml | 19 + .../nextcloud-aio-helm-chart/update-helm.sh | 531 ++ .../nextcloud-aio-helm-chart/values.yaml | 75 + nextcloud-aio/php/README.md | 65 + nextcloud-aio/php/composer.json | 38 + nextcloud-aio/php/composer.lock | 4801 ++++++++++ nextcloud-aio/php/containers-schema.json | 232 + nextcloud-aio/php/containers.json | 895 ++ nextcloud-aio/php/data/.gitkeep | 0 nextcloud-aio/php/domain-validator.php | 19 + nextcloud-aio/php/psalm-baseline.xml | 64 + nextcloud-aio/php/psalm.xml | 24 + nextcloud-aio/php/public/automatic_reload.js | 19 + nextcloud-aio/php/public/before-unload.js | 3 + .../php/public/containers-form-submit.js | 88 + nextcloud-aio/php/public/disable-clamav.js | 5 + nextcloud-aio/php/public/disable-collabora.js | 5 + .../php/public/disable-docker-socket-proxy.js | 7 + .../php/public/disable-fulltextsearch.js | 5 + nextcloud-aio/php/public/disable-imaginary.js | 5 + .../php/public/disable-onlyoffice.js | 7 + .../php/public/disable-talk-recording.js | 4 + nextcloud-aio/php/public/disable-talk.js | 5 + .../php/public/disable-whiteboard.js | 5 + nextcloud-aio/php/public/forms.js | 90 + nextcloud-aio/php/public/img/favicon.png | Bin 0 -> 90022 bytes .../public/img/jo-myoung-hee-fluid-dark.webp | Bin 0 -> 97010 bytes .../php/public/img/jo-myoung-hee-fluid.webp | Bin 0 -> 101012 bytes .../php/public/img/nextcloud-logo.svg | 4 + nextcloud-aio/php/public/index.php | 198 + nextcloud-aio/php/public/robots.txt | 2 + .../php/public/second-tab-warning.js | 12 + nextcloud-aio/php/public/style.css | 551 ++ nextcloud-aio/php/public/timezone.js | 7 + nextcloud-aio/php/public/toggle-dark-mode.js | 37 + nextcloud-aio/php/session/.gitkeep | 0 nextcloud-aio/php/src/Auth/AuthManager.php | 46 + .../php/src/Auth/PasswordGenerator.php | 7801 +++++++++++++++++ .../php/src/Container/AioVariables.php | 19 + nextcloud-aio/php/src/Container/Container.php | 157 + .../ContainerEnvironmentVariables.php | 19 + .../php/src/Container/ContainerPort.php | 12 + .../php/src/Container/ContainerPorts.php | 19 + .../php/src/Container/ContainerState.php | 12 + .../php/src/Container/ContainerVolume.php | 12 + .../php/src/Container/ContainerVolumes.php | 19 + .../php/src/Container/VersionState.php | 8 + .../php/src/ContainerDefinitionFetcher.php | 351 + .../Controller/ConfigurationController.php | 170 + .../php/src/Controller/DockerController.php | 313 + .../php/src/Controller/LoginController.php | 51 + .../php/src/Cron/BackupNotification.php | 33 + nextcloud-aio/php/src/Cron/CheckBackup.php | 17 + .../php/src/Cron/CheckFreeDiskSpace.php | 26 + nextcloud-aio/php/src/Cron/CreateBackup.php | 17 + .../php/src/Cron/OutdatedNotification.php | 26 + .../php/src/Cron/PullContainerImages.php | 20 + .../php/src/Cron/StartAndUpdateContainers.php | 20 + .../php/src/Cron/StartContainers.php | 20 + nextcloud-aio/php/src/Cron/StopContainers.php | 17 + .../php/src/Cron/UpdateMastercontainer.php | 17 + .../php/src/Cron/UpdateNotification.php | 30 + .../php/src/Data/ConfigurationManager.php | 1117 +++ nextcloud-aio/php/src/Data/DataConst.php | 61 + .../InvalidSettingConfigurationException.php | 6 + nextcloud-aio/php/src/Data/Setup.php | 27 + nextcloud-aio/php/src/DependencyInjection.php | 55 + .../php/src/Docker/DockerActionManager.php | 1015 +++ .../php/src/Docker/DockerHubManager.php | 61 + .../Docker/GitHubContainerRegistryManager.php | 62 + .../php/src/Middleware/AuthMiddleware.php | 39 + nextcloud-aio/php/src/Twig/ClassExtension.php | 25 + nextcloud-aio/php/src/Twig/CsrfExtension.php | 34 + .../php/templates/already-installed.twig | 13 + .../templates/components/container-state.twig | 27 + nextcloud-aio/php/templates/containers.twig | 625 ++ .../php/templates/includes/aio-config.twig | 44 + .../php/templates/includes/backup-dirs.twig | 6 + .../includes/community-containers.twig | 42 + .../includes/optional-containers.twig | 201 + nextcloud-aio/php/templates/layout.twig | 21 + nextcloud-aio/php/templates/login.twig | 25 + nextcloud-aio/php/templates/setup.twig | 16 + nextcloud-aio/php/tests/.gitignore | 7 + nextcloud-aio/php/tests/package-lock.json | 97 + nextcloud-aio/php/tests/package.json | 8 + nextcloud-aio/php/tests/playwright.config.js | 29 + .../php/tests/tests/initial-setup.spec.js | 96 + .../php/tests/tests/restore-instance.spec.js | 83 + nextcloud-aio/readme.md | 1111 +++ nextcloud-aio/reverse-proxy.md | 1075 +++ nextcloud-aio/tests/QA/001-initial-setup.md | 10 + nextcloud-aio/tests/QA/002-new-instance.md | 31 + nextcloud-aio/tests/QA/003-automatic-login.md | 8 + nextcloud-aio/tests/QA/004-initial-backup.md | 18 + .../tests/QA/010-restore-instance.md | 19 + .../tests/QA/020-backup-and-restore.md | 12 + .../tests/QA/030-aio-password-change.md | 12 + nextcloud-aio/tests/QA/040-login-behavior.md | 7 + nextcloud-aio/tests/QA/050-optional-addons.md | 15 + .../tests/QA/060-environmental-variables.md | 28 + nextcloud-aio/tests/QA/070-timezone-change.md | 10 + .../tests/QA/080-daily-backup-script.md | 5 + .../tests/QA/assets/backup-archive/readme.md | 4 + nextcloud-aio/tests/QA/readme.md | 7 + 318 files changed, 35761 insertions(+) create mode 100644 nextcloud-aio/.gitattributes create mode 100644 nextcloud-aio/.gitignore create mode 100644 nextcloud-aio/Containers/alpine/Dockerfile create mode 100644 nextcloud-aio/Containers/apache/Caddyfile create mode 100644 nextcloud-aio/Containers/apache/Dockerfile create mode 100644 nextcloud-aio/Containers/apache/healthcheck.sh create mode 100644 nextcloud-aio/Containers/apache/nextcloud.conf create mode 100644 nextcloud-aio/Containers/apache/start.sh create mode 100644 nextcloud-aio/Containers/apache/supervisord.conf create mode 100644 nextcloud-aio/Containers/borgbackup/Dockerfile create mode 100644 nextcloud-aio/Containers/borgbackup/backupscript.sh create mode 100644 nextcloud-aio/Containers/borgbackup/borg_excludes create mode 100644 nextcloud-aio/Containers/borgbackup/start.sh create mode 100644 nextcloud-aio/Containers/clamav/Dockerfile create mode 100644 nextcloud-aio/Containers/clamav/healthcheck.sh create mode 100644 nextcloud-aio/Containers/clamav/start.sh create mode 100644 nextcloud-aio/Containers/clamav/supervisord.conf create mode 100644 nextcloud-aio/Containers/collabora/Dockerfile create mode 100644 nextcloud-aio/Containers/collabora/healthcheck.sh create mode 100644 nextcloud-aio/Containers/docker-socket-proxy/Dockerfile create mode 100644 nextcloud-aio/Containers/docker-socket-proxy/haproxy.cfg create mode 100644 nextcloud-aio/Containers/docker-socket-proxy/healthcheck.sh create mode 100644 nextcloud-aio/Containers/docker-socket-proxy/start.sh create mode 100644 nextcloud-aio/Containers/domaincheck/Dockerfile create mode 100644 nextcloud-aio/Containers/domaincheck/lighttpd.conf create mode 100644 nextcloud-aio/Containers/domaincheck/start.sh create mode 100644 nextcloud-aio/Containers/fulltextsearch/Dockerfile create mode 100644 nextcloud-aio/Containers/fulltextsearch/healthcheck.sh create mode 100644 nextcloud-aio/Containers/imaginary/Dockerfile create mode 100644 nextcloud-aio/Containers/imaginary/healthcheck.sh create mode 100644 nextcloud-aio/Containers/imaginary/start.sh create mode 100644 nextcloud-aio/Containers/mastercontainer/Caddyfile create mode 100644 nextcloud-aio/Containers/mastercontainer/Dockerfile create mode 100644 nextcloud-aio/Containers/mastercontainer/backup-time-file-watcher.sh create mode 100644 nextcloud-aio/Containers/mastercontainer/cron.sh create mode 100644 nextcloud-aio/Containers/mastercontainer/daily-backup.sh create mode 100644 nextcloud-aio/Containers/mastercontainer/healthcheck.sh create mode 100644 nextcloud-aio/Containers/mastercontainer/mastercontainer.conf create mode 100644 nextcloud-aio/Containers/mastercontainer/session-deduplicator.sh create mode 100644 nextcloud-aio/Containers/mastercontainer/start.sh create mode 100644 nextcloud-aio/Containers/mastercontainer/supervisord.conf create mode 100644 nextcloud-aio/Containers/nextcloud/Dockerfile create mode 100644 nextcloud-aio/Containers/nextcloud/config/aio.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/apcu.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/apps.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/postgres.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/proxy.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/redis.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/reverse-proxy.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/s3.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/smtp.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/config/swift.config.php create mode 100644 nextcloud-aio/Containers/nextcloud/cron.sh create mode 100644 nextcloud-aio/Containers/nextcloud/entrypoint.sh create mode 100644 nextcloud-aio/Containers/nextcloud/healthcheck.sh create mode 100644 nextcloud-aio/Containers/nextcloud/notify-all.sh create mode 100644 nextcloud-aio/Containers/nextcloud/notify.sh create mode 100644 nextcloud-aio/Containers/nextcloud/root.motd create mode 100644 nextcloud-aio/Containers/nextcloud/run-exec-commands.sh create mode 100644 nextcloud-aio/Containers/nextcloud/start.sh create mode 100644 nextcloud-aio/Containers/nextcloud/supervisord.conf create mode 100644 nextcloud-aio/Containers/nextcloud/upgrade.exclude create mode 100644 nextcloud-aio/Containers/notify-push/Dockerfile create mode 100644 nextcloud-aio/Containers/notify-push/healthcheck.sh create mode 100644 nextcloud-aio/Containers/notify-push/start.sh create mode 100644 nextcloud-aio/Containers/onlyoffice/Dockerfile create mode 100644 nextcloud-aio/Containers/onlyoffice/healthcheck.sh create mode 100644 nextcloud-aio/Containers/postgresql/Dockerfile create mode 100644 nextcloud-aio/Containers/postgresql/healthcheck.sh create mode 100644 nextcloud-aio/Containers/postgresql/init-user-db.sh create mode 100644 nextcloud-aio/Containers/postgresql/start.sh create mode 100644 nextcloud-aio/Containers/redis/Dockerfile create mode 100644 nextcloud-aio/Containers/redis/healthcheck.sh create mode 100644 nextcloud-aio/Containers/redis/start.sh create mode 100644 nextcloud-aio/Containers/talk-recording/Dockerfile create mode 100644 nextcloud-aio/Containers/talk-recording/healthcheck.sh create mode 100644 nextcloud-aio/Containers/talk-recording/recording.conf create mode 100644 nextcloud-aio/Containers/talk-recording/start.sh create mode 100644 nextcloud-aio/Containers/talk/Dockerfile create mode 100644 nextcloud-aio/Containers/talk/healthcheck.sh create mode 100644 nextcloud-aio/Containers/talk/server.conf.in create mode 100644 nextcloud-aio/Containers/talk/start.sh create mode 100644 nextcloud-aio/Containers/talk/supervisord.conf create mode 100644 nextcloud-aio/Containers/watchtower/Dockerfile create mode 100644 nextcloud-aio/Containers/watchtower/start.sh create mode 100644 nextcloud-aio/Containers/whiteboard/Dockerfile create mode 100644 nextcloud-aio/Containers/whiteboard/healthcheck.sh create mode 100644 nextcloud-aio/Containers/whiteboard/start.sh create mode 100644 nextcloud-aio/LICENSE create mode 100644 nextcloud-aio/app/.editorconfig create mode 100644 nextcloud-aio/app/appinfo/info.xml create mode 100644 nextcloud-aio/app/composer/autoload.php create mode 100644 nextcloud-aio/app/composer/composer.json create mode 100644 nextcloud-aio/app/composer/composer.lock create mode 100644 nextcloud-aio/app/composer/composer/ClassLoader.php create mode 100644 nextcloud-aio/app/composer/composer/InstalledVersions.php create mode 100644 nextcloud-aio/app/composer/composer/LICENSE create mode 100644 nextcloud-aio/app/composer/composer/autoload_classmap.php create mode 100644 nextcloud-aio/app/composer/composer/autoload_namespaces.php create mode 100644 nextcloud-aio/app/composer/composer/autoload_psr4.php create mode 100644 nextcloud-aio/app/composer/composer/autoload_real.php create mode 100644 nextcloud-aio/app/composer/composer/autoload_static.php create mode 100644 nextcloud-aio/app/composer/composer/installed.json create mode 100644 nextcloud-aio/app/composer/composer/installed.php create mode 100644 nextcloud-aio/app/lib/Settings/Admin.php create mode 100644 nextcloud-aio/app/readme.md create mode 100644 nextcloud-aio/app/templates/admin.php create mode 100644 nextcloud-aio/community-containers/borgbackup-viewer/borgbackup-viewer.json create mode 100644 nextcloud-aio/community-containers/borgbackup-viewer/readme.md create mode 100644 nextcloud-aio/community-containers/caddy/caddy.json create mode 100644 nextcloud-aio/community-containers/caddy/readme.md create mode 100644 nextcloud-aio/community-containers/calcardbackup/calcardbackup.json create mode 100644 nextcloud-aio/community-containers/calcardbackup/readme.md create mode 100644 nextcloud-aio/community-containers/container-management/container-management.json create mode 100644 nextcloud-aio/community-containers/container-management/readme.md create mode 100644 nextcloud-aio/community-containers/dlna/dlna.json create mode 100644 nextcloud-aio/community-containers/dlna/readme.md create mode 100644 nextcloud-aio/community-containers/facerecognition/facerecognition.json create mode 100644 nextcloud-aio/community-containers/facerecognition/readme.md create mode 100644 nextcloud-aio/community-containers/fail2ban/fail2ban.json create mode 100644 nextcloud-aio/community-containers/fail2ban/readme.md create mode 100644 nextcloud-aio/community-containers/helloworld/helloworld.json create mode 100644 nextcloud-aio/community-containers/helloworld/readme.md create mode 100644 nextcloud-aio/community-containers/jellyfin/jellyfin.json create mode 100644 nextcloud-aio/community-containers/jellyfin/readme.md create mode 100644 nextcloud-aio/community-containers/jellyseerr/jellyseerr.json create mode 100644 nextcloud-aio/community-containers/jellyseerr/readme.md create mode 100644 nextcloud-aio/community-containers/languagetool/languagetool.json create mode 100644 nextcloud-aio/community-containers/languagetool/readme.md create mode 100644 nextcloud-aio/community-containers/libretranslate/libretranslate.json create mode 100644 nextcloud-aio/community-containers/libretranslate/readme.md create mode 100644 nextcloud-aio/community-containers/lldap/lldap.json create mode 100644 nextcloud-aio/community-containers/lldap/readme.md create mode 100644 nextcloud-aio/community-containers/local-ai/local-ai.json create mode 100644 nextcloud-aio/community-containers/local-ai/readme.md create mode 100644 nextcloud-aio/community-containers/makemkv/makemkv.json create mode 100644 nextcloud-aio/community-containers/makemkv/readme.md create mode 100644 nextcloud-aio/community-containers/memories/memories.json create mode 100644 nextcloud-aio/community-containers/memories/readme.md create mode 100644 nextcloud-aio/community-containers/minio/minio.json create mode 100644 nextcloud-aio/community-containers/minio/readme.md create mode 100644 nextcloud-aio/community-containers/nextcloud-exporter/nextcloud-exporter.json create mode 100644 nextcloud-aio/community-containers/nextcloud-exporter/readme.md create mode 100644 nextcloud-aio/community-containers/nocodb/nocodb.json create mode 100644 nextcloud-aio/community-containers/nocodb/readme.md create mode 100644 nextcloud-aio/community-containers/npmplus/npmplus.json create mode 100644 nextcloud-aio/community-containers/npmplus/readme.md create mode 100644 nextcloud-aio/community-containers/pi-hole/pi-hole.json create mode 100644 nextcloud-aio/community-containers/pi-hole/readme.md create mode 100644 nextcloud-aio/community-containers/plex/plex.json create mode 100644 nextcloud-aio/community-containers/plex/readme.md create mode 100644 nextcloud-aio/community-containers/readme.md create mode 100644 nextcloud-aio/community-containers/scrutiny/readme.md create mode 100644 nextcloud-aio/community-containers/scrutiny/scrutiny.json create mode 100644 nextcloud-aio/community-containers/smbserver/readme.md create mode 100644 nextcloud-aio/community-containers/smbserver/smbserver.json create mode 100644 nextcloud-aio/community-containers/stalwart/readme.md create mode 100644 nextcloud-aio/community-containers/stalwart/stalwart.json create mode 100644 nextcloud-aio/community-containers/vaultwarden/readme.md create mode 100644 nextcloud-aio/community-containers/vaultwarden/vaultwarden.json create mode 100644 nextcloud-aio/compose-example.yaml create mode 100644 nextcloud-aio/compose.yaml create mode 100644 nextcloud-aio/develop.md create mode 100644 nextcloud-aio/docker-ipv6-support.md create mode 100644 nextcloud-aio/docker-rootless.md create mode 100644 nextcloud-aio/local-instance.md create mode 100644 nextcloud-aio/manual-install/latest.yml create mode 100644 nextcloud-aio/manual-install/readme.md create mode 100644 nextcloud-aio/manual-install/sample.conf create mode 100644 nextcloud-aio/manual-install/update-yaml.sh create mode 100644 nextcloud-aio/manual-upgrade.md create mode 100644 nextcloud-aio/migration.md create mode 100644 nextcloud-aio/multiple-instances.md create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/Chart.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/readme.md create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-dump-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-namespace-namespace.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-networkpolicy.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-trusted-cacerts-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-persistentvolumeclaim.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-deployment.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-service.yaml create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/update-helm.sh create mode 100755 nextcloud-aio/nextcloud-aio-helm-chart/values.yaml create mode 100644 nextcloud-aio/php/README.md create mode 100644 nextcloud-aio/php/composer.json create mode 100644 nextcloud-aio/php/composer.lock create mode 100644 nextcloud-aio/php/containers-schema.json create mode 100644 nextcloud-aio/php/containers.json create mode 100644 nextcloud-aio/php/data/.gitkeep create mode 100644 nextcloud-aio/php/domain-validator.php create mode 100644 nextcloud-aio/php/psalm-baseline.xml create mode 100644 nextcloud-aio/php/psalm.xml create mode 100644 nextcloud-aio/php/public/automatic_reload.js create mode 100644 nextcloud-aio/php/public/before-unload.js create mode 100644 nextcloud-aio/php/public/containers-form-submit.js create mode 100644 nextcloud-aio/php/public/disable-clamav.js create mode 100644 nextcloud-aio/php/public/disable-collabora.js create mode 100644 nextcloud-aio/php/public/disable-docker-socket-proxy.js create mode 100644 nextcloud-aio/php/public/disable-fulltextsearch.js create mode 100644 nextcloud-aio/php/public/disable-imaginary.js create mode 100644 nextcloud-aio/php/public/disable-onlyoffice.js create mode 100644 nextcloud-aio/php/public/disable-talk-recording.js create mode 100644 nextcloud-aio/php/public/disable-talk.js create mode 100644 nextcloud-aio/php/public/disable-whiteboard.js create mode 100644 nextcloud-aio/php/public/forms.js create mode 100644 nextcloud-aio/php/public/img/favicon.png create mode 100644 nextcloud-aio/php/public/img/jo-myoung-hee-fluid-dark.webp create mode 100644 nextcloud-aio/php/public/img/jo-myoung-hee-fluid.webp create mode 100644 nextcloud-aio/php/public/img/nextcloud-logo.svg create mode 100644 nextcloud-aio/php/public/index.php create mode 100644 nextcloud-aio/php/public/robots.txt create mode 100644 nextcloud-aio/php/public/second-tab-warning.js create mode 100644 nextcloud-aio/php/public/style.css create mode 100644 nextcloud-aio/php/public/timezone.js create mode 100644 nextcloud-aio/php/public/toggle-dark-mode.js create mode 100644 nextcloud-aio/php/session/.gitkeep create mode 100644 nextcloud-aio/php/src/Auth/AuthManager.php create mode 100644 nextcloud-aio/php/src/Auth/PasswordGenerator.php create mode 100644 nextcloud-aio/php/src/Container/AioVariables.php create mode 100644 nextcloud-aio/php/src/Container/Container.php create mode 100644 nextcloud-aio/php/src/Container/ContainerEnvironmentVariables.php create mode 100644 nextcloud-aio/php/src/Container/ContainerPort.php create mode 100644 nextcloud-aio/php/src/Container/ContainerPorts.php create mode 100644 nextcloud-aio/php/src/Container/ContainerState.php create mode 100644 nextcloud-aio/php/src/Container/ContainerVolume.php create mode 100644 nextcloud-aio/php/src/Container/ContainerVolumes.php create mode 100644 nextcloud-aio/php/src/Container/VersionState.php create mode 100644 nextcloud-aio/php/src/ContainerDefinitionFetcher.php create mode 100644 nextcloud-aio/php/src/Controller/ConfigurationController.php create mode 100644 nextcloud-aio/php/src/Controller/DockerController.php create mode 100644 nextcloud-aio/php/src/Controller/LoginController.php create mode 100644 nextcloud-aio/php/src/Cron/BackupNotification.php create mode 100644 nextcloud-aio/php/src/Cron/CheckBackup.php create mode 100644 nextcloud-aio/php/src/Cron/CheckFreeDiskSpace.php create mode 100644 nextcloud-aio/php/src/Cron/CreateBackup.php create mode 100644 nextcloud-aio/php/src/Cron/OutdatedNotification.php create mode 100644 nextcloud-aio/php/src/Cron/PullContainerImages.php create mode 100644 nextcloud-aio/php/src/Cron/StartAndUpdateContainers.php create mode 100644 nextcloud-aio/php/src/Cron/StartContainers.php create mode 100644 nextcloud-aio/php/src/Cron/StopContainers.php create mode 100644 nextcloud-aio/php/src/Cron/UpdateMastercontainer.php create mode 100644 nextcloud-aio/php/src/Cron/UpdateNotification.php create mode 100644 nextcloud-aio/php/src/Data/ConfigurationManager.php create mode 100644 nextcloud-aio/php/src/Data/DataConst.php create mode 100644 nextcloud-aio/php/src/Data/InvalidSettingConfigurationException.php create mode 100644 nextcloud-aio/php/src/Data/Setup.php create mode 100644 nextcloud-aio/php/src/DependencyInjection.php create mode 100644 nextcloud-aio/php/src/Docker/DockerActionManager.php create mode 100644 nextcloud-aio/php/src/Docker/DockerHubManager.php create mode 100644 nextcloud-aio/php/src/Docker/GitHubContainerRegistryManager.php create mode 100644 nextcloud-aio/php/src/Middleware/AuthMiddleware.php create mode 100644 nextcloud-aio/php/src/Twig/ClassExtension.php create mode 100644 nextcloud-aio/php/src/Twig/CsrfExtension.php create mode 100644 nextcloud-aio/php/templates/already-installed.twig create mode 100644 nextcloud-aio/php/templates/components/container-state.twig create mode 100644 nextcloud-aio/php/templates/containers.twig create mode 100644 nextcloud-aio/php/templates/includes/aio-config.twig create mode 100644 nextcloud-aio/php/templates/includes/backup-dirs.twig create mode 100644 nextcloud-aio/php/templates/includes/community-containers.twig create mode 100644 nextcloud-aio/php/templates/includes/optional-containers.twig create mode 100644 nextcloud-aio/php/templates/layout.twig create mode 100644 nextcloud-aio/php/templates/login.twig create mode 100644 nextcloud-aio/php/templates/setup.twig create mode 100644 nextcloud-aio/php/tests/.gitignore create mode 100644 nextcloud-aio/php/tests/package-lock.json create mode 100644 nextcloud-aio/php/tests/package.json create mode 100644 nextcloud-aio/php/tests/playwright.config.js create mode 100644 nextcloud-aio/php/tests/tests/initial-setup.spec.js create mode 100644 nextcloud-aio/php/tests/tests/restore-instance.spec.js create mode 100644 nextcloud-aio/readme.md create mode 100644 nextcloud-aio/reverse-proxy.md create mode 100644 nextcloud-aio/tests/QA/001-initial-setup.md create mode 100644 nextcloud-aio/tests/QA/002-new-instance.md create mode 100644 nextcloud-aio/tests/QA/003-automatic-login.md create mode 100644 nextcloud-aio/tests/QA/004-initial-backup.md create mode 100644 nextcloud-aio/tests/QA/010-restore-instance.md create mode 100644 nextcloud-aio/tests/QA/020-backup-and-restore.md create mode 100644 nextcloud-aio/tests/QA/030-aio-password-change.md create mode 100644 nextcloud-aio/tests/QA/040-login-behavior.md create mode 100644 nextcloud-aio/tests/QA/050-optional-addons.md create mode 100644 nextcloud-aio/tests/QA/060-environmental-variables.md create mode 100644 nextcloud-aio/tests/QA/070-timezone-change.md create mode 100644 nextcloud-aio/tests/QA/080-daily-backup-script.md create mode 100644 nextcloud-aio/tests/QA/assets/backup-archive/readme.md create mode 100644 nextcloud-aio/tests/QA/readme.md diff --git a/nextcloud-aio/.gitattributes b/nextcloud-aio/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/nextcloud-aio/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/nextcloud-aio/.gitignore b/nextcloud-aio/.gitignore new file mode 100644 index 0000000..a9ac8d7 --- /dev/null +++ b/nextcloud-aio/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +.idea/ +*.iml + +/php/data/* +/php/session/* +!/php/data/.gitkeep +!/php/session/.gitkeep +/php/vendor + +/manual-install/*.conf +!/manual-install/sample.conf +/manual-install/docker-compose.yml +/manual-install/compose.yaml +/manual-install/.env diff --git a/nextcloud-aio/Containers/alpine/Dockerfile b/nextcloud-aio/Containers/alpine/Dockerfile new file mode 100644 index 0000000..429485b --- /dev/null +++ b/nextcloud-aio/Containers/alpine/Dockerfile @@ -0,0 +1,7 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.22.1 + +RUN set -ex; \ + apk upgrade --no-cache -a + +LABEL org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/apache/Caddyfile b/nextcloud-aio/Containers/apache/Caddyfile new file mode 100644 index 0000000..4b92d80 --- /dev/null +++ b/nextcloud-aio/Containers/apache/Caddyfile @@ -0,0 +1,75 @@ +{ + auto_https disable_redirects + + storage file_system { + root /mnt/data/caddy + } + + servers { + # trusted_proxies placeholder + } + + log { + level ERROR + } +} + +https://{$ADDITIONAL_TRUSTED_DOMAIN}:443, +http://{$APACHE_HOST}:23973, # For Collabora callback and WOPI requests, see containers.json +{$PROTOCOL}://{$NC_DOMAIN}:{$APACHE_PORT} { + header -Server + header -X-Powered-By + + # Collabora + route /browser/* { + reverse_proxy {$COLLABORA_HOST}:9980 + } + route /hosting/* { + reverse_proxy {$COLLABORA_HOST}:9980 + } + route /cool/* { + reverse_proxy {$COLLABORA_HOST}:9980 + } + + # Notify Push + route /push/* { + uri strip_prefix /push + reverse_proxy {$NOTIFY_PUSH_HOST}:7867 + } + + # Onlyoffice + route /onlyoffice/* { + uri strip_prefix /onlyoffice + reverse_proxy {$ONLYOFFICE_HOST}:80 { + header_up X-Forwarded-Host {http.request.hostport}/onlyoffice + header_up X-Forwarded-Proto https + } + } + + # Talk + route /standalone-signaling/* { + uri strip_prefix /standalone-signaling + reverse_proxy {$TALK_HOST}:8081 + } + + # Whiteboard + route /whiteboard/* { + uri strip_prefix /whiteboard + reverse_proxy {$WHITEBOARD_HOST}:3002 + } + + # Nextcloud + route { + header Strict-Transport-Security max-age=31536000; + reverse_proxy 127.0.0.1:8000 + } + redir /.well-known/carddav /remote.php/dav/ 301 + redir /.well-known/caldav /remote.php/dav/ 301 + + # TLS options + tls { + issuer acme { + disable_http_challenge + } + } +} diff --git a/nextcloud-aio/Containers/apache/Dockerfile b/nextcloud-aio/Containers/apache/Dockerfile new file mode 100644 index 0000000..d960286 --- /dev/null +++ b/nextcloud-aio/Containers/apache/Dockerfile @@ -0,0 +1,91 @@ +# syntax=docker/dockerfile:latest +FROM caddy:2.10.2-alpine AS caddy + +# From https://github.com/docker-library/httpd/blob/master/2.4/alpine/Dockerfile +FROM httpd:2.4.65-alpine3.22 + +COPY --from=caddy /usr/bin/caddy /usr/bin/caddy + +COPY --chown=33:33 Caddyfile /Caddyfile +COPY --chmod=664 nextcloud.conf /usr/local/apache2/conf/nextcloud.conf +COPY --chmod=664 supervisord.conf /supervisord.conf +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +VOLUME /mnt/data + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache shadow; \ + groupmod -g 33 www-data; \ + usermod -u 33 -g 33 www-data; \ + apk del --no-cache shadow; \ + \ + mkdir -p /mnt/data; \ + chown -R www-data:www-data /mnt/data; \ + chown -R 777 /tmp; \ + \ + apk add --no-cache \ + bash \ + supervisor \ + tzdata \ + ca-certificates \ + openssl \ + bind-tools \ + netcat-openbsd; \ + \ + sed -i \ + -e '/^Listen /d' \ + -e 's/^#\(LoadModule .*mod_rewrite.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_headers.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_proxy.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_proxy_fcgi.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_setenvif.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_env.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mime.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_dir.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_authz_core.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_alias.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mpm_event.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_brotli.so\)/\1/' \ + -e 's/\(LoadModule .*mod_mpm_worker.so\)/#\1/' \ + -e 's/\(LoadModule .*mod_mpm_prefork.so\)/#\1/' \ + -e 's/\(ScriptAlias \)/#\1/' \ + /usr/local/apache2/conf/httpd.conf; \ + echo "Include conf/nextcloud.conf" | tee -a /usr/local/apache2/conf/httpd.conf; \ + echo "ServerName localhost" | tee -a /usr/local/apache2/conf/httpd.conf; \ +# Sync this with max db connections and pm.max_children +# We don't actually expect so many workers but don't want to limit it artificially because people will report issues otherwise. + sed -i 's|MaxRequestWorkers.*|MaxRequestWorkers 5000|' /usr/local/apache2/conf/extra/httpd-mpm.conf; \ + grep -q '' /usr/local/apache2/conf/extra/httpd-mpm.conf; \ +# ServerLimit needs to be set to MaxRequestWorkers divided by ThreadsPerChild which is set to 25 by default + sed -i '//a\ \ \ \ ServerLimit 200' /usr/local/apache2/conf/extra/httpd-mpm.conf; \ + \ + rm -rf /usr/local/apache2/conf/original /var/www; \ + mkdir -p /var/www; \ + chown -R www-data:www-data /var/www; \ + \ + mkdir /var/log/supervisord; \ + mkdir /var/run/supervisord; \ + chown www-data:www-data /var/run/supervisord; \ + chown www-data:www-data /var/log/supervisord; \ + chmod 777 /var/run/supervisord; \ + chmod 777 /var/log/supervisord; \ + \ + chown -R www-data:www-data /usr/local/apache2; \ + chmod +r -R /usr/local/apache2; \ + mkdir -p /usr/local/apache2/logs; \ + chmod 777 -R /home/www-data; \ + chmod 777 -R /usr/local/apache2/logs; \ + rm -rf /usr/local/apache2/cgi-bin/; \ + \ + echo "root:$(openssl rand -base64 12)" | chpasswd + +USER 33 + +ENTRYPOINT ["/start.sh"] +CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/apache/healthcheck.sh b/nextcloud-aio/Containers/apache/healthcheck.sh new file mode 100644 index 0000000..e9c1fad --- /dev/null +++ b/nextcloud-aio/Containers/apache/healthcheck.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +nc -z "$NEXTCLOUD_HOST" 9000 || exit 0 +nc -z 127.0.0.1 8000 || exit 1 +nc -z 127.0.0.1 "$APACHE_PORT" || exit 1 diff --git a/nextcloud-aio/Containers/apache/nextcloud.conf b/nextcloud-aio/Containers/apache/nextcloud.conf new file mode 100644 index 0000000..728d19d --- /dev/null +++ b/nextcloud-aio/Containers/apache/nextcloud.conf @@ -0,0 +1,54 @@ +Listen 8000 + + ServerName localhost + + # Add error log + CustomLog /proc/self/fd/1 proxy + LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" proxy + ErrorLog /proc/self/fd/2 + ErrorLogFormat "[%t] [%l] [%E] [client: %{X-Forwarded-For}i] [%M] [%{User-Agent}i]" + LogLevel warn + + # PHP match + + SetHandler "proxy:fcgi://${NEXTCLOUD_HOST}:9000" + + + + + + # Enable Brotli compression for js, css and svg files - other plain files are compressed by Nextcloud by default + + AddOutputFilterByType BROTLI_COMPRESS text/javascript application/javascript application/x-javascript text/css image/svg+xml + BrotliCompressionQuality 0 + + + # Nextcloud dir + DocumentRoot /var/www/html/ + + Options Indexes FollowSymLinks + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + Satisfy Any + + Dav off + + + # Deny access to .ht files + + Require all denied + + + # See https://httpd.apache.org/docs/current/en/mod/core.html#limitrequestbody + LimitRequestBody ${APACHE_MAX_SIZE} + + # See https://httpd.apache.org/docs/current/mod/core.html#timeout + Timeout ${APACHE_MAX_TIME} + + # See https://httpd.apache.org/docs/current/mod/mod_proxy.html#proxytimeout + ProxyTimeout ${APACHE_MAX_TIME} + + # See https://httpd.apache.org/docs/trunk/mod/core.html#traceenable + TraceEnable Off + diff --git a/nextcloud-aio/Containers/apache/start.sh b/nextcloud-aio/Containers/apache/start.sh new file mode 100644 index 0000000..02a2f2a --- /dev/null +++ b/nextcloud-aio/Containers/apache/start.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +if [ -z "$NC_DOMAIN" ]; then + echo "NC_DOMAIN and NEXTCLOUD_HOST need to be provided. Exiting!" + exit 1 +fi + +# Need write access to /mnt/data +if ! [ -w /mnt/data ]; then + echo "Cannot write to /mnt/data" + exit 1 +fi + +# Only start container if nextcloud is accessible +while ! nc -z "$NEXTCLOUD_HOST" 9000; do + echo "Waiting for Nextcloud to start..." + sleep 5 +done + +# Get ipv4-address of Apache +# shellcheck disable=SC2153 +IPv4_ADDRESS="$(dig "$APACHE_HOST" A +short +search | head -1)" +# Bring it in CIDR notation +# shellcheck disable=SC2001 +IPv4_ADDRESS="$(echo "$IPv4_ADDRESS" | sed 's|[0-9]\+$|0/16|')" + +if [ -z "$APACHE_PORT" ]; then + export APACHE_PORT="443" +fi + +# Change variables in case of reverse proxies +if [ "$APACHE_PORT" != '443' ]; then + export PROTOCOL="http" + export NC_DOMAIN="" +else + export PROTOCOL="https" +fi + +# Change the auto_https in case of reverse proxies +if [ "$APACHE_PORT" != '443' ]; then + CADDYFILE="$(sed 's|auto_https.*|auto_https off|' /Caddyfile)" +else + CADDYFILE="$(sed 's|auto_https.*|auto_https disable_redirects|' /Caddyfile)" +fi +echo "$CADDYFILE" > /tmp/Caddyfile + +# Change the trusted_proxies in case of reverse proxies +if [ "$APACHE_PORT" != '443' ]; then + # Here the 100.64.0.0/10 range gets added which is the CGNAT range used by Tailscale nodes + # See https://github.com/nextcloud/all-in-one/pull/6703 for reference + CADDYFILE="$(sed 's|# trusted_proxies placeholder|trusted_proxies static private_ranges 100.64.0.0/10|' /tmp/Caddyfile)" +else + CADDYFILE="$(sed "s|# trusted_proxies placeholder|trusted_proxies static $IPv4_ADDRESS|" /tmp/Caddyfile)" +fi +echo "$CADDYFILE" > /tmp/Caddyfile + +# Remove additional domain if not given +if [ -z "$ADDITIONAL_TRUSTED_DOMAIN" ]; then + CADDYFILE="$(sed '/ADDITIONAL_TRUSTED_DOMAIN/d' /tmp/Caddyfile)" +fi +echo "$CADDYFILE" > /tmp/Caddyfile + +# Fix the Caddyfile format +caddy fmt --overwrite /tmp/Caddyfile + +# Add caddy path +mkdir -p /mnt/data/caddy/ + +# Fix caddy startup +if [ -d "/mnt/data/caddy/locks" ]; then + rm -rf /mnt/data/caddy/locks/* +fi + +# Fix apache startup +rm -f /usr/local/apache2/logs/httpd.pid + +exec "$@" diff --git a/nextcloud-aio/Containers/apache/supervisord.conf b/nextcloud-aio/Containers/apache/supervisord.conf new file mode 100644 index 0000000..d40bf78 --- /dev/null +++ b/nextcloud-aio/Containers/apache/supervisord.conf @@ -0,0 +1,23 @@ +[supervisord] +nodaemon=true +nodaemon=true +logfile=/var/log/supervisord/supervisord.log +pidfile=/var/run/supervisord/supervisord.pid +childlogdir=/var/log/supervisord/ +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=error + +[program:apache] +# Stdout logging is disabled as otherwise the logs are spammed +stdout_logfile=NONE +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=apachectl -DFOREGROUND + +[program:caddy] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/usr/bin/caddy run --config /tmp/Caddyfile diff --git a/nextcloud-aio/Containers/borgbackup/Dockerfile b/nextcloud-aio/Containers/borgbackup/Dockerfile new file mode 100644 index 0000000..74d87f4 --- /dev/null +++ b/nextcloud-aio/Containers/borgbackup/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.22.1 + +RUN set -ex; \ + \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + util-linux-misc \ + bash \ + borgbackup \ + rsync \ + fuse \ + py3-llfuse \ + jq \ + openssh-client + +VOLUME /root + +COPY --chmod=770 *.sh / +COPY borg_excludes / + +ENTRYPOINT ["/start.sh"] +# hadolint ignore=DL3002 +USER root + +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" +ENV BORG_RETENTION_POLICY="--keep-within=7d --keep-weekly=4 --keep-monthly=6" diff --git a/nextcloud-aio/Containers/borgbackup/backupscript.sh b/nextcloud-aio/Containers/borgbackup/backupscript.sh new file mode 100644 index 0000000..602ec58 --- /dev/null +++ b/nextcloud-aio/Containers/borgbackup/backupscript.sh @@ -0,0 +1,619 @@ +#!/bin/bash + +# Functions +get_start_time(){ + START_TIME=$(date +%s) + CURRENT_DATE=$(date --date @"$START_TIME" +"%Y%m%d_%H%M%S") +} +get_expiration_time() { + END_TIME=$(date +%s) + END_DATE_READABLE=$(date --date @"$END_TIME" +"%d.%m.%Y - %H:%M:%S") + DURATION=$((END_TIME-START_TIME)) + DURATION_SEC=$((DURATION % 60)) + DURATION_MIN=$(((DURATION / 60) % 60)) + DURATION_HOUR=$((DURATION / 3600)) + DURATION_READABLE=$(printf "%02d hours %02d minutes %02d seconds" $DURATION_HOUR $DURATION_MIN $DURATION_SEC) +} + +# Test if all volumes aren't empty +VOLUME_DIRS="$(find /nextcloud_aio_volumes -mindepth 1 -maxdepth 1 -type d)" +mapfile -t VOLUME_DIRS <<< "$VOLUME_DIRS" +for directory in "${VOLUME_DIRS[@]}"; do + if ! mountpoint -q "$directory"; then + echo "$directory is not a mountpoint which is not allowed." + exit 1 + fi +done +# Test if default volumes are there +DEFAULT_VOLUMES=(nextcloud_aio_apache nextcloud_aio_nextcloud nextcloud_aio_database nextcloud_aio_database_dump nextcloud_aio_elasticsearch nextcloud_aio_nextcloud_data nextcloud_aio_mastercontainer) +for volume in "${DEFAULT_VOLUMES[@]}"; do + if ! mountpoint -q "/nextcloud_aio_volumes/$volume"; then + echo "$volume is missing which is not intended." + exit 1 + fi +done + +# Check if target is mountpoint +if [ -z "$BORG_REMOTE_REPO" ] && ! mountpoint -q "$MOUNT_DIR"; then + echo "$MOUNT_DIR is not a mountpoint which is not allowed." + exit 1 +fi + +# Check if repo is uninitialized +if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != test ] && ! borg info > /dev/null; then + if [ -n "$BORG_REMOTE_REPO" ]; then + echo "The repository is uninitialized or cannot connect to remote. Cannot perform check or restore." + else + echo "The repository is uninitialized. Cannot perform check or restore." + fi + exit 1 +fi + +# Do not continue if this file exists (needed for simple external blocking) +if [ -z "$BORG_REMOTE_REPO" ] && [ -f "$BORG_BACKUP_DIRECTORY/aio-lockfile" ]; then + echo "Not continuing because aio-lockfile exists – it seems like a script is externally running which is locking the backup archive." + echo "If this should not be the case, you can fix this by deleting the 'aio-lockfile' file from the backup archive directory." + exit 1 +fi + +# Create lockfile +if [ "$BORG_MODE" = backup ] || [ "$BORG_MODE" = restore ]; then + touch "/nextcloud_aio_volumes/nextcloud_aio_database_dump/backup-is-running" +fi + +if [ -n "$BORG_REMOTE_REPO" ] && ! [ -f "$BORGBACKUP_KEY" ]; then + echo "First run, creating borg ssh key" + ssh-keygen -f "$BORGBACKUP_KEY" -N "" + echo "You should configure the remote to accept this public key" +fi +if [ -n "$BORG_REMOTE_REPO" ] && [ -f "$BORGBACKUP_KEY.pub" ]; then + echo "Your public ssh key for borgbackup is: $(cat "$BORGBACKUP_KEY.pub")" +fi + +# Do the backup +if [ "$BORG_MODE" = backup ]; then + + # Test if important files are present + if ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json" ]; then + echo "configuration.json not present. Cannot perform the backup!" + exit 1 + elif ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud/config/config.php" ]; then + echo "config.php is missing. Cannot perform backup!" + exit 1 + elif ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/database-dump.sql" ]; then + echo "database-dump is missing. Cannot perform backup!" + echo "Please check the database container logs!" + exit 1 + elif ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.ocdata" ] && ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.ncdata" ]; then + echo "The .ncdata or .ocdata file is missing in Nextcloud datadir which means it is invalid!" + echo "Is the drive where the datadir is located on still mounted?" + exit 1 + fi + + # Test that default volumes are not empty + for volume in "${DEFAULT_VOLUMES[@]}"; do + if [ -z "$(ls -A "/nextcloud_aio_volumes/$volume")" ] && [ "$volume" != "nextcloud_aio_elasticsearch" ]; then + echo "/nextcloud_aio_volumes/$volume is empty which should not happen!" + exit 1 + fi + done + + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/export.failed" ]; then + echo "Cannot create a backup now." + echo "Reason is that the database export failed the last time." + echo "Most likely was the database container not correctly shut down via the AIO interface." + echo "" + echo "You might want to try the database export again manually by running the three commands:" + echo "sudo docker start nextcloud-aio-database" + echo "sleep 10" + echo "sudo docker stop nextcloud-aio-database -t 1800" + echo "" + echo "Afterwards try to create a backup again and it should hopefully work." + echo "If it should still fail, feel free to report this to https://github.com/nextcloud/all-in-one/issues and post the database container logs and the borgbackup container logs into the thread. Thanks!" + exit 1 + fi + + if [ -z "$BORG_REMOTE_REPO" ]; then + # Create backup folder + mkdir -p "$BORG_BACKUP_DIRECTORY" + fi + + # Initialize the repository if can't get info from target + if ! borg info > /dev/null; then + # Don't initialize if already initialized + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" ]; then + if [ -n "$BORG_REMOTE_REPO" ]; then + echo "Borg could not get info from the remote repo." + echo "This might be a failure to connect to the remote server. See the above borg info output for details." + else + echo "Borg could not get info from the targeted directory." + echo "This might happen if the targeted directory is located on an external drive and the drive not connected anymore. You should check this." + fi + echo "If you instead want to initialize a new backup repository, you may delete the 'borg.config' file that is stored in the mastercontainer volume manually, which will allow you to initialize a new borg repository in the chosen directory:" + echo "sudo docker exec nextcloud-aio-mastercontainer rm /mnt/docker-aio-config/data/borg.config" + exit 1 + fi + + echo "Initializing repository..." + NEW_REPOSITORY=1 + if ! borg init --debug --encryption=repokey-blake2; then + echo "Could not initialize borg repository." + if [ -z "$BORG_REMOTE_REPO" ]; then + # Originally we checked for presence of the config file instead of calling `borg info`. Likely `borg info` + # will error on a partially initialized repo, so this line is probably no longer necessary + rm -f "$BORG_BACKUP_DIRECTORY/config" + fi + exit 1 + fi + + if [ -z "$BORG_REMOTE_REPO" ]; then + # borg config only works for local repos; it's up to the remote to ensure the disk isn't full + borg config :: additional_free_space 2G + + # Fix too large Borg cache + # https://borgbackup.readthedocs.io/en/stable/faq.html#the-borg-cache-eats-way-too-much-disk-space-what-can-i-do + BORG_ID="$(borg config :: id)" + rm -r "/root/.cache/borg/$BORG_ID/chunks.archive.d" + touch "/root/.cache/borg/$BORG_ID/chunks.archive.d" + fi + + if ! borg info > /dev/null; then + echo "Borg can't get info from the repo it created. Something is wrong." + exit 1 + fi + + rm -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" + if [ -n "$BORG_REMOTE_REPO" ]; then + # `borg config` does not support remote repos so instead create a dummy file and rely on the remote to avoid + # corruption of the config file (which contains the encryption key). We don't actually use the contents of + # this file anywhere, so a touch is all we need so we remember we already initialized the repo. + touch "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" + else + # Make a backup from the borg config file + if ! cp "$BORG_BACKUP_DIRECTORY/config" "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config"; then + echo "Could not copy config file to second place. Cannot perform backup." + exit 1 + fi + fi + + echo "Repository successfully initialized." + fi + + # Perform backup + echo "Performing backup..." + + # Borg options + # auto,zstd compression seems to has the best ratio based on: + # https://forum.level1techs.com/t/optimal-compression-for-borg-backups/145870/6 + BORG_OPTS=(-v --stats --compression "auto,zstd") + if [ "$NEW_REPOSITORY" = 1 ]; then + BORG_OPTS+=(--progress) + fi + + # Exclude the nextcloud log and audit log for GDPR reasons + BORG_EXCLUDE=(--exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log*" --exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log" --exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/lost+found") + BORG_INCLUDE=() + + # Exclude datadir if .noaiobackup file was found + # shellcheck disable=SC2144 + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.noaiobackup" ]; then + BORG_EXCLUDE+=(--exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/") + BORG_INCLUDE+=(--pattern="+/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.noaiobackup") + echo "⚠️⚠️⚠️ '.noaiobackup' file was found in Nextclouds data directory. Excluding the data directory from backup!" + # Exclude preview folder if .noaiobackup file was found + elif [ -f /nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/.noaiobackup ]; then + BORG_EXCLUDE+=(--exclude "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/") + BORG_INCLUDE+=(--pattern="+/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/.noaiobackup") + echo "⚠️⚠️⚠️ '.noaiobackup' file was found in the preview directory. Excluding the preview directory from backup!" + fi + + # Make sure that there is always a borg.config file before creating a new backup + if ! [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" ]; then + echo "Did not find borg.config file in the mastercontainer volume." + echo "Cannot create a backup as this is wrong." + exit 1 + fi + + # Create the backup + echo "Starting the backup..." + get_start_time + if ! borg create "${BORG_OPTS[@]}" "${BORG_INCLUDE[@]}" "${BORG_EXCLUDE[@]}" "::$CURRENT_DATE-nextcloud-aio" "/nextcloud_aio_volumes/" --exclude-from /borg_excludes; then + echo "Deleting the failed backup archive..." + borg delete --stats "::$CURRENT_DATE-nextcloud-aio" + echo "Backup failed!" + echo "You might want to check the backup integrity via the AIO interface." + if [ "$NEW_REPOSITORY" = 1 ]; then + echo "Deleting borg.config file so that you can choose a different location for the backup." + rm "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" + fi + exit 1 + fi + + # Remove the update skip file because the backup was successful + rm -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/skip.update" + + # Prune options + read -ra BORG_PRUNE_OPTS <<< "$BORG_RETENTION_POLICY" + echo "BORG_PRUNE_OPTS are ${BORG_PRUNE_OPTS[*]}" + + # Prune archives + echo "Pruning the archives..." + if ! borg prune --stats --glob-archives '*_*-nextcloud-aio' "${BORG_PRUNE_OPTS[@]}"; then + echo "Failed to prune archives!" + exit 1 + fi + + # Compact archives + echo "Compacting the archives..." + if ! borg compact; then + echo "Failed to compact archives!" + exit 1 + fi + + # Back up additional directories of the host + if [ "$ADDITIONAL_DIRECTORIES_BACKUP" = 'yes' ]; then + if [ -d "/docker_volumes/" ]; then + DOCKER_VOLUME_DIRS="$(find /docker_volumes -mindepth 1 -maxdepth 1 -type d)" + mapfile -t DOCKER_VOLUME_DIRS <<< "$DOCKER_VOLUME_DIRS" + for directory in "${DOCKER_VOLUME_DIRS[@]}"; do + if [ -z "$(ls -A "$directory")" ]; then + echo "$directory is empty which is not allowed." + exit 1 + fi + done + echo "Starting the backup for additional volumes..." + if ! borg create "${BORG_OPTS[@]}" "::$CURRENT_DATE-additional-docker-volumes" "/docker_volumes/"; then + echo "Deleting the failed backup archive..." + borg delete --stats "::$CURRENT_DATE-additional-docker-volumes" + echo "Backup of additional docker-volumes failed!" + exit 1 + fi + echo "Pruning additional volumes..." + if ! borg prune --stats --glob-archives '*_*-additional-docker-volumes' "${BORG_PRUNE_OPTS[@]}"; then + echo "Failed to prune additional docker-volumes archives!" + exit 1 + fi + echo "Compacting additional volumes..." + if ! borg compact; then + echo "Failed to compact additional docker-volume archives!" + exit 1 + fi + fi + if [ -d "/host_mounts/" ]; then + EXCLUDED_DIRECTORIES=(home/*/.cache root/.cache var/cache lost+found run var/run dev tmp sys proc) + # Exclude borg backup cache + EXCLUDED_DIRECTORIES+=(var/lib/docker/volumes/nextcloud_aio_backup_cache/_data) + # Exclude target directory + if [ -n "$BORGBACKUP_HOST_LOCATION" ] && [ "$BORGBACKUP_HOST_LOCATION" != "nextcloud_aio_backupdir" ]; then + EXCLUDED_DIRECTORIES+=("$BORGBACKUP_HOST_LOCATION") + fi + for directory in "${EXCLUDED_DIRECTORIES[@]}" + do + EXCLUDE_DIRS+=(--exclude "/host_mounts/$directory/") + done + echo "Starting the backup for additional host mounts..." + if ! borg create "${BORG_OPTS[@]}" "${EXCLUDE_DIRS[@]}" "::$CURRENT_DATE-additional-host-mounts" "/host_mounts/"; then + echo "Deleting the failed backup archive..." + borg delete --stats "::$CURRENT_DATE-additional-host-mounts" + echo "Backup of additional host-mounts failed!" + exit 1 + fi + echo "Pruning additional host mounts..." + if ! borg prune --stats --glob-archives '*_*-additional-host-mounts' "${BORG_PRUNE_OPTS[@]}"; then + echo "Failed to prune additional host-mount archives!" + exit 1 + fi + echo "Compacting additional host mounts..." + if ! borg compact; then + echo "Failed to compact additional host-mount archives!" + exit 1 + fi + fi + fi + + # Inform user + get_expiration_time + echo "Backup finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/update.failed" ]; then + echo "However a Nextcloud update failed. So reporting that the backup failed which will skip any update attempt the next time." + echo "Please restore a backup from before the failed Nextcloud update attempt." + exit 1 + fi + exit 0 +fi + +# Do the restore +if [ "$BORG_MODE" = restore ]; then + get_start_time + + # Pick archive to restore + if [ -n "$SELECTED_RESTORE_TIME" ]; then + SELECTED_ARCHIVE="$(borg list | grep "nextcloud-aio" | grep "$SELECTED_RESTORE_TIME" | awk -F " " '{print $1}' | head -1)" + else + SELECTED_ARCHIVE="$(borg list | grep "nextcloud-aio" | awk -F " " '{print $1}' | sort -r | head -1)" + fi + echo "Restoring '$SELECTED_ARCHIVE'..." + + ADDITIONAL_RSYNC_EXCLUDES=() + ADDITIONAL_BORG_EXCLUDES=() + ADDITIONAL_FIND_EXCLUDES=() + # Exclude datadir if .noaiobackup file was found + # shellcheck disable=SC2144 + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/.noaiobackup" ]; then + # Keep these 3 in sync. Beware, the pattern syntax and the paths differ + ADDITIONAL_RSYNC_EXCLUDES=(--exclude "nextcloud_aio_nextcloud_data/**") + ADDITIONAL_BORG_EXCLUDES=(--exclude "sh:nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/**") + ADDITIONAL_FIND_EXCLUDES=(-o -regex 'nextcloud_aio_volumes/nextcloud_aio_nextcloud_data\(/.*\)?') + echo "⚠️⚠️⚠️ '.noaiobackup' file was found in Nextclouds data directory. Excluding the data directory from restore!" + echo "You might run into problems due to this afterwards as potentially this makes the directory go out of sync with the database." + echo "You might be able to fix this by running 'occ files:scan --all' and 'occ maintenance:repair' and 'occ files:scan-app-data' after the restore." + echo "See https://github.com/nextcloud/all-in-one#how-to-run-occ-commands" + # Exclude previews from restore if selected to speed up process or exclude preview folder if .noaiobackup file was found + elif [ -n "$RESTORE_EXCLUDE_PREVIEWS" ] || [ -f /nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/.noaiobackup ]; then + # Keep these 3 in sync. Beware, the pattern syntax and the paths differ + ADDITIONAL_RSYNC_EXCLUDES=(--exclude "nextcloud_aio_nextcloud_data/appdata_*/preview/**") + ADDITIONAL_BORG_EXCLUDES=(--exclude "sh:nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_*/preview/**") + ADDITIONAL_FIND_EXCLUDES=(-o -regex 'nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/appdata_[^/]*/preview\(/.*\)?') + echo "⚠️⚠️⚠️ Excluding previews from restore!" + echo "You might run into problems due to this afterwards as potentially this makes the directory go out of sync with the database." + echo "You might be able to fix this by running 'occ files:scan-app-data preview' after the restore." + echo "See https://github.com/nextcloud/all-in-one#how-to-run-occ-commands" + fi + + # Save Additional Backup dirs + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" ]; then + ADDITIONAL_BACKUP_DIRECTORIES="$(cat /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories)" + fi + + # Save daily backup time + if [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_time" ]; then + DAILY_BACKUPTIME="$(cat /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_time)" + fi + + # Save current aio password + AIO_PASSWORD="$(jq '.password' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + + # Save current backup location vars + BORG_LOCATION="$(jq '.borg_backup_host_location' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + REMOTE_REPO="$(jq '.borg_remote_repo' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + + # Save current nextcloud datadir + if grep -q '"nextcloud_datadir":' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json; then + NEXTCLOUD_DATADIR="$(jq '.nextcloud_datadir' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + else + NEXTCLOUD_DATADIR='""' + fi + + if [ -z "$BORG_REMOTE_REPO" ]; then + mkdir -p /tmp/borg + if ! borg mount "::$SELECTED_ARCHIVE" /tmp/borg; then + echo "Could not mount the backup!" + exit 1 + fi + + # Restore everything except the configuration file + # + # These exclude patterns need to be kept in sync with the borg_excludes file and the find excludes in this file, + # which use a different syntax (patterns appear in 3 places in total) + if ! rsync --stats --archive --human-readable -vv --delete \ + --exclude "nextcloud_aio_apache/caddy/**" \ + --exclude "nextcloud_aio_mastercontainer/caddy/**" \ + --exclude "nextcloud_aio_nextcloud/data/nextcloud.log*" \ + --exclude "nextcloud_aio_nextcloud/data/audit.log" \ + --exclude "nextcloud_aio_mastercontainer/certs/**" \ + --exclude "nextcloud_aio_mastercontainer/data/configuration.json" \ + --exclude "nextcloud_aio_mastercontainer/data/daily_backup_running" \ + --exclude "nextcloud_aio_mastercontainer/data/session_date_file" \ + --exclude "nextcloud_aio_mastercontainer/session/**" \ + --exclude "nextcloud_aio_nextcloud_data/lost+found" \ + "${ADDITIONAL_RSYNC_EXCLUDES[@]}" \ + /tmp/borg/nextcloud_aio_volumes/ /nextcloud_aio_volumes/; then + RESTORE_FAILED=1 + echo "Something failed while restoring from backup." + fi + + # Restore the configuration file + if ! rsync --archive --human-readable -vv \ + /tmp/borg/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json \ + /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json; then + RESTORE_FAILED=1 + echo "Something failed while restoring the configuration.json." + fi + + if ! umount /tmp/borg; then + echo "Failed to unmount the borg archive but should still be able to restore successfully" + fi + else + # Restore nearly everything + # + # borg mount is really slow for remote repos (did not check whether it's slow for local repos too), + # using extract to /tmp would require temporarily storing a second copy of the data. + # So instead extract directly on top of the destination with exclude patterns for the config, but + # then we do still need to delete local files which are not present in the archive. + # + # Older backups may still contain files we've since excluded, so we have to exclude on extract as well. + cd / # borg extract has no destination arg and extracts to CWD + if ! borg extract "::$SELECTED_ARCHIVE" --progress --exclude-from /borg_excludes "${ADDITIONAL_BORG_EXCLUDES[@]}" --pattern '+nextcloud_aio_volumes/**' + then + RESTORE_FAILED=1 + echo "Failed to extract backup archive." + else + # Delete files/dirs present locally, but not in the backup archive, excluding conf files + # https://unix.stackexchange.com/a/759341 + # This comm does not support -z, but I doubt any file names would have \n in them + # + # These find patterns need to be kept in sync with the borg_excludes file and the rsync excludes in this + # file, which use a different syntax (patterns appear in 3 places in total) + echo "Deleting local files which do not exist in the backup" + if ! find nextcloud_aio_volumes \ + -not \( \ + -path nextcloud_aio_volumes/nextcloud_aio_apache/caddy \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_apache/caddy/*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy/*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs/*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session/*" \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log*" \ + -o -path nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_running \ + -o -path nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/session_date_file \ + -o -path "nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg*" \ + -o -path "nextcloud_aio_nextcloud_data/lost+found" \ + "${ADDITIONAL_FIND_EXCLUDES[@]}" \ + \) \ + | LC_ALL=C sort \ + | LC_ALL=C comm -23 - \ + <(borg list "::$SELECTED_ARCHIVE" --short --exclude-from /borg_excludes --pattern '+nextcloud_aio_volumes/**' | LC_ALL=C sort) \ + > /tmp/local_files_not_in_backup + then + RESTORE_FAILED=1 + echo "Failed to delete local files not in backup archive." + else + # More robust than e.g. xargs as I got a ~"args line too long" error while testing that, but it's slower + # https://stackoverflow.com/a/21848934 + while IFS= read -r file + do rm -vrf -- "$file" || DELETE_FAILED=1 + done < /tmp/local_files_not_in_backup + + if [ "$DELETE_FAILED" = 1 ]; then + RESTORE_FAILED=1 + echo "Failed to delete (some) local files not in backup archive." + fi + fi + fi + fi + + # Set backup-mode to restore since it was a restore + CONTENTS="$(jq '."backup-mode" = "restore"' /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json + + # Reset the backup location vars to the currently used one + CONTENTS="$(jq ".borg_backup_host_location = $BORG_LOCATION" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json + CONTENTS="$(jq ".borg_remote_repo = $REMOTE_REPO" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json + + # Reset the AIO password to the currently used one + CONTENTS="$(jq ".password = $AIO_PASSWORD" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json + + # Reset the datadir to the one that was used for the restore + CONTENTS="$(jq ".nextcloud_datadir = $NEXTCLOUD_DATADIR" /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json)" + echo -E "${CONTENTS}" > /nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/configuration.json + + # Reset the additional backup directories + if [ -n "$ADDITIONAL_BACKUP_DIRECTORIES" ]; then + echo "$ADDITIONAL_BACKUP_DIRECTORIES" > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" + chown 33:0 "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" + chmod 770 "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/additional_backup_directories" + fi + + # Reset the additional backup directories + if [ -n "$DAILY_BACKUPTIME" ]; then + echo "$DAILY_BACKUPTIME" > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_time" + chown 33:0 "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_time" + chmod 770 "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_time" + fi + + if [ "$RESTORE_FAILED" = 1 ]; then + exit 1 + fi + + # Inform user + get_expiration_time + echo "Restore finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." + + # Add file to Nextcloud container so that it skips any update the next time + touch "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/skip.update" + chmod 777 "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/skip.update" + + # Add file to Nextcloud container so that it performs a fingerprint update the next time + touch "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/fingerprint.update" + chmod 777 "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/fingerprint.update" + + # Add file to Netcloud container to trigger a preview scan the next time it starts + if [ -n "$RESTORE_EXCLUDE_PREVIEWS" ]; then + touch "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/trigger-preview.scan" + chmod 777 "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/trigger-preview.scan" + fi + + # Delete redis cache + rm -f "/mnt/redis/dump.rdb" +fi + +# Do the Backup check +if [ "$BORG_MODE" = check ]; then + get_start_time + echo "Checking the backup integrity..." + + # Perform the check + if ! borg check -v --verify-data; then + echo "Some errors were found while checking the backup integrity!" + echo "Check the AIO interface for advice on how to proceed now!" + exit 1 + fi + + # Inform user + get_expiration_time + echo "Check finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." + exit 0 +fi + +# Do the Backup check-repair +if [ "$BORG_MODE" = "check-repair" ]; then + get_start_time + echo "Checking the backup integrity and repairing it..." + + # Perform the check-repair + if ! echo YES | borg check -v --repair; then + echo "Some errors were found while checking and repairing the backup integrity!" + exit 1 + fi + + # Inform user + get_expiration_time + echo "Check finished successfully on $END_DATE_READABLE ($DURATION_READABLE)." + exit 0 +fi + +# Do the backup test +if [ "$BORG_MODE" = test ]; then + if [ -n "$BORG_REMOTE_REPO" ]; then + if ! borg info > /dev/null; then + echo "Borg could not get info from the remote repo." + echo "See the above borg info output for details." + exit 1 + fi + else + if ! [ -d "$BORG_BACKUP_DIRECTORY" ]; then + echo "No 'borg' directory in the given backup directory found!" + echo "Only the files/folders below have been found in the given directory." + ls -a "$MOUNT_DIR" + echo "Please adjust the directory so that the borg archive is positioned in a folder named 'borg' inside the given directory!" + exit 1 + elif ! [ -f "$BORG_BACKUP_DIRECTORY/config" ]; then + echo "A 'borg' directory was found but could not find the borg archive." + echo "Only the files/folders below have been found in the borg directory." + ls -a "$BORG_BACKUP_DIRECTORY" + echo "The archive and most importantly the config file must be positioned directly in the 'borg' subfolder." + exit 1 + fi + fi + + if ! borg list >/dev/null; then + echo "The entered path seems to be valid but could not open the backup archive." + echo "Most likely the entered password was wrong so please adjust it accordingly!" + exit 1 + else + if ! borg list | grep "nextcloud-aio"; then + echo "The backup archive does not contain a valid Nextcloud AIO backup." + echo "Most likely was the archive not created via Nextcloud AIO." + exit 1 + else + echo "Everything looks fine so feel free to continue!" + exit 0 + fi + fi +fi diff --git a/nextcloud-aio/Containers/borgbackup/borg_excludes b/nextcloud-aio/Containers/borgbackup/borg_excludes new file mode 100644 index 0000000..bbe6ada --- /dev/null +++ b/nextcloud-aio/Containers/borgbackup/borg_excludes @@ -0,0 +1,11 @@ +# These patterns need to be kept in sync with rsync and find excludes in backupscript.sh, +# which use a different syntax (patterns appear in 3 places in total) +nextcloud_aio_volumes/nextcloud_aio_apache/caddy/ +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/caddy/ +nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/nextcloud.log* +nextcloud_aio_volumes/nextcloud_aio_nextcloud/data/audit.log +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/certs/ +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/daily_backup_running +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/session_date_file +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/session/ +nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg* \ No newline at end of file diff --git a/nextcloud-aio/Containers/borgbackup/start.sh b/nextcloud-aio/Containers/borgbackup/start.sh new file mode 100644 index 0000000..9da0d84 --- /dev/null +++ b/nextcloud-aio/Containers/borgbackup/start.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Variables +export MOUNT_DIR="/mnt/borgbackup" +export BORG_BACKUP_DIRECTORY="$MOUNT_DIR/borg" # necessary even when remote to store the aio-lockfile + +# Validate BORG_PASSWORD +if [ -z "$BORG_PASSWORD" ] && [ -z "$BACKUP_RESTORE_PASSWORD" ]; then + echo "Neither BORG_PASSWORD nor BACKUP_RESTORE_PASSWORD are set." + exit 1 +fi + +# Export defaults +if [ -n "$BACKUP_RESTORE_PASSWORD" ]; then + export BORG_PASSPHRASE="$BACKUP_RESTORE_PASSWORD" +else + export BORG_PASSPHRASE="$BORG_PASSWORD" +fi +export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes +export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes +if [ -n "$BORG_REMOTE_REPO" ]; then + export BORG_REPO="$BORG_REMOTE_REPO" + + # Location to create the borg ssh pub/priv key + export BORGBACKUP_KEY="/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/id_borg" + + # Accept any host key the first time connecting to the remote. Strictly speaking should be provided by user but you'd + # have to be very unlucky to get MitM'ed on your first connection. + export BORG_RSH="ssh -o StrictHostKeyChecking=accept-new -i $BORGBACKUP_KEY" +else + export BORG_REPO="$BORG_BACKUP_DIRECTORY" +fi + +# Validate BORG_MODE +if [ "$BORG_MODE" != backup ] && [ "$BORG_MODE" != restore ] && [ "$BORG_MODE" != check ] && [ "$BORG_MODE" != "check-repair" ] && [ "$BORG_MODE" != test ]; then + echo "No correct BORG_MODE mode applied. Valid are 'backup', 'check', 'restore' and 'test'." + exit 1 +fi + +export BORG_MODE + +# Run the backup script +if ! bash /backupscript.sh; then + FAILED=1 +fi + +# Remove lockfile +rm -f "/nextcloud_aio_volumes/nextcloud_aio_database_dump/backup-is-running" + +# Get a list of all available borg archives +if borg list &>/dev/null; then + borg list | grep "nextcloud-aio" | awk -F " " '{print $1","$3,$4}' > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list" +else + echo "" > "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list" +fi +chmod +r "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/backup_archives.list" + +if [ -n "$FAILED" ]; then + if [ "$BORG_MODE" = backup ]; then + # Add file to Nextcloud container so that it skips any update the next time + touch "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/skip.update" + chmod 777 "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data/skip.update" + fi + exit 1 +fi + +exec "$@" \ No newline at end of file diff --git a/nextcloud-aio/Containers/clamav/Dockerfile b/nextcloud-aio/Containers/clamav/Dockerfile new file mode 100644 index 0000000..216ea1c --- /dev/null +++ b/nextcloud-aio/Containers/clamav/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.22.1 + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache tzdata clamav supervisor bash; \ + mkdir -p /var/lib/clamav /run/clamav /var/log/supervisord /var/run/supervisord; \ + chmod 777 -R /run/clamav /var/log/clamav /var/log/supervisord /var/run/supervisord; \ + chown -R 100:100 /var/lib/clamav; \ + sed -i "s|#\?MaxDirectoryRecursion.*|MaxDirectoryRecursion 30|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?MaxFileSize.*|MaxFileSize 2G|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?PCREMaxFileSize.*|PCREMaxFileSize aio-placeholder|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?StreamMaxLength.*|StreamMaxLength aio-placeholder|g" /etc/clamav/clamd.conf; \ + sed -i "s|#\?TCPSocket|TCPSocket|g" /etc/clamav/clamd.conf; \ + sed -i "s|^LocalSocket .*|LocalSocket /tmp/clamd.sock|g" /etc/clamav/clamd.conf + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh +COPY --chmod=664 supervisord.conf /supervisord.conf + +USER 100 +RUN set -ex; \ + freshclam --foreground --stdout +VOLUME /var/lib/clamav +ENTRYPOINT ["/start.sh"] +CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" +HEALTHCHECK --start-period=60s --retries=9 CMD /healthcheck.sh diff --git a/nextcloud-aio/Containers/clamav/healthcheck.sh b/nextcloud-aio/Containers/clamav/healthcheck.sh new file mode 100644 index 0000000..fe8b5da --- /dev/null +++ b/nextcloud-aio/Containers/clamav/healthcheck.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [ "$(echo "PING" | nc 127.0.0.1 3310)" != "PONG" ]; then + echo "ERROR: Unable to contact server" + exit 1 +fi + +echo "Clamd is up" +exit 0 diff --git a/nextcloud-aio/Containers/clamav/start.sh b/nextcloud-aio/Containers/clamav/start.sh new file mode 100644 index 0000000..bda4add --- /dev/null +++ b/nextcloud-aio/Containers/clamav/start.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +sed "s|aio-placeholder|$MAX_SIZE|" /etc/clamav/clamd.conf > /tmp/clamd.conf + +# Print out clamav version for compliance reasons +clamscan --version + +echo "Clamav started" + +exec "$@" diff --git a/nextcloud-aio/Containers/clamav/supervisord.conf b/nextcloud-aio/Containers/clamav/supervisord.conf new file mode 100644 index 0000000..8f53856 --- /dev/null +++ b/nextcloud-aio/Containers/clamav/supervisord.conf @@ -0,0 +1,23 @@ +[supervisord] +nodaemon=true +nodaemon=true +logfile=/var/log/supervisord/supervisord.log +pidfile=/var/run/supervisord/supervisord.pid +childlogdir=/var/log/supervisord/ +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=error + +[program:freshclam] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=freshclam --foreground --stdout --daemon --daemon-notify=/tmp/clamd.conf + +[program:clamd] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=clamd --foreground --config-file=/tmp/clamd.conf diff --git a/nextcloud-aio/Containers/collabora/Dockerfile b/nextcloud-aio/Containers/collabora/Dockerfile new file mode 100644 index 0000000..593c532 --- /dev/null +++ b/nextcloud-aio/Containers/collabora/Dockerfile @@ -0,0 +1,14 @@ +# syntax=docker/dockerfile:latest +# From a file located probably somewhere here: https://github.com/CollaboraOnline/online/blob/master/docker/from-packages/Dockerfile +FROM collabora/code:25.04.6.1.1 + +USER root +ARG DEBIAN_FRONTEND=noninteractive + +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +USER 1001 + +HEALTHCHECK --start-period=60s --retries=9 CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/collabora/healthcheck.sh b/nextcloud-aio/Containers/collabora/healthcheck.sh new file mode 100644 index 0000000..45e9278 --- /dev/null +++ b/nextcloud-aio/Containers/collabora/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Unfortunately, no curl and no nc is installed in the container +# and packages can also not be added as the package list is broken. +# So always exiting 0 for now. +# nc http://127.0.0.1:9980 || exit 1 +exit 0 diff --git a/nextcloud-aio/Containers/docker-socket-proxy/Dockerfile b/nextcloud-aio/Containers/docker-socket-proxy/Dockerfile new file mode 100644 index 0000000..72034ce --- /dev/null +++ b/nextcloud-aio/Containers/docker-socket-proxy/Dockerfile @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:latest +FROM haproxy:3.2.6-alpine + +# hadolint ignore=DL3002 +USER root +ENV NEXTCLOUD_HOST=nextcloud-aio-nextcloud +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + bash \ + bind-tools; \ + chmod -R 777 /tmp + +COPY --chmod=775 *.sh / +COPY --chmod=664 haproxy.cfg /haproxy.cfg + +ENTRYPOINT ["/start.sh"] +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/docker-socket-proxy/haproxy.cfg b/nextcloud-aio/Containers/docker-socket-proxy/haproxy.cfg new file mode 100644 index 0000000..632df43 --- /dev/null +++ b/nextcloud-aio/Containers/docker-socket-proxy/haproxy.cfg @@ -0,0 +1,68 @@ +# Inspiration: https://github.com/Tecnativa/docker-socket-proxy/blob/master/haproxy.cfg + +global + maxconn 10 + +defaults + timeout connect 30s + timeout client 30s + timeout server 1800s + +frontend http + mode http + bind :::2375 v4v6 + http-request deny unless { src 127.0.0.1 } || { src ::1 } || { src NC_IPV4_PLACEHOLDER } || { src NC_IPV6_PLACEHOLDER } + # docker system _ping + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/_ping$ } METH_GET + # docker inspect image: GET images/%s/json + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images/.*/json } METH_GET + # container inspect: GET containers/%s/json + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/json } METH_GET + # container inspect: GET containers/%s/logs + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/logs } METH_GET + # container start/stop: POST containers/%s/start containers/%s/stop + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/((start)|(stop)) } METH_POST + # container rm: DELETE containers/%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+ } METH_DELETE + # container update/exec: POST containers/%s/update containers/%s/exec + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/((update)|(exec)) } METH_POST + # container put: PUT containers/%s/archive + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/nc_app_[a-zA-Z0-9_.-]+/archive } METH_PUT + # run exec instance: POST exec/%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/exec/[a-zA-Z0-9_.-]+/start } METH_POST + + # container create: POST containers/create?name=%s + # ACL to restrict container name to nc_app_[a-zA-Z0-9_.-]+ + acl nc_app_container_name url_param(name) -m reg -i "^nc_app_[a-zA-Z0-9_.-]+" + + # ACL to restrict the number of Mounts to 1 + acl one_mount_volume req.body -m reg -i "\"Mounts\"\s*:\s*\[\s*(?:(?!\"Mounts\"\s*:\s*\[)[^}]*)}[^}]*\]" + # ACL to deny if there are any binds + acl binds_present req.body -m reg -i "\"HostConfig\"\s*:.*\"Binds\"\s*:" + # ACL to restrict the type of Mounts to volume + acl type_not_volume req.body -m reg -i "\"Mounts\"\s*:\s*\[[^\]]*(\"Type\"\s*:\s*\"(?!volume\b)\w+\"[^\]]*)+\]" + http-request deny if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/create } nc_app_container_name !one_mount_volume binds_present type_not_volume METH_POST + + # ACL to restrict container creation, that it has HostConfig.Privileged(by searching for "Privileged" word in all payload) + acl no_privileged_flag req.body -m reg -i "\"Privileged\"" + # ACL to allow mount volume with strict pattern for name: nc_app_[a-zA-Z0-9_.-]+_data + acl nc_app_volume_data_only req.body -m reg -i "\"Mounts\"\s*:\s*\[\s*{[^}]*\"Source\"\s*:\s*\"nc_app_[a-zA-Z0-9_.-]+_data\"" + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/containers/create } nc_app_container_name !no_privileged_flag nc_app_volume_data_only METH_POST + # end of container create + + # volume create: POST volumes/create + # restrict name + acl nc_app_volume_data req.body -m reg -i "\"Name\"\s*:\s*\"nc_app_[a-zA-Z0-9_.-]+_data\"" + # do not allow to use "device" word e.g., "--opt device=:/path/to/dir" + acl volume_no_device req.body -m reg -i "\"device\"" + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes/create } nc_app_volume_data !volume_no_device METH_POST + # volume rm: DELETE volumes/%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/volumes/nc_app_[a-zA-Z0-9_.-]+_data } METH_DELETE + # image pull: POST images/create?fromImage=%s + http-request allow if { path,url_dec -m reg -i ^(/v[\d\.]+)?/images/create } METH_POST + http-request deny + default_backend dockerbackend + +backend dockerbackend + mode http + server dockersocket /var/run/docker.sock diff --git a/nextcloud-aio/Containers/docker-socket-proxy/healthcheck.sh b/nextcloud-aio/Containers/docker-socket-proxy/healthcheck.sh new file mode 100644 index 0000000..d89deb6 --- /dev/null +++ b/nextcloud-aio/Containers/docker-socket-proxy/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +nc -z "$NEXTCLOUD_HOST" 9001 || exit 0 +nc -z 127.0.0.1 2375 || exit 1 diff --git a/nextcloud-aio/Containers/docker-socket-proxy/start.sh b/nextcloud-aio/Containers/docker-socket-proxy/start.sh new file mode 100644 index 0000000..657c914 --- /dev/null +++ b/nextcloud-aio/Containers/docker-socket-proxy/start.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +# Only start container if nextcloud is accessible +while ! nc -z "$NEXTCLOUD_HOST" 9001; do + echo "Waiting for Nextcloud to start..." + sleep 5 +done + +set -x +IPv4_ADDRESS_NC="$(dig nextcloud-aio-nextcloud IN A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" +HAPROXYFILE="$(sed "s|NC_IPV4_PLACEHOLDER|$IPv4_ADDRESS_NC|" /haproxy.cfg)" +echo "$HAPROXYFILE" > /tmp/haproxy.cfg + +IPv6_ADDRESS_NC="$(dig nextcloud-aio-nextcloud AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" +if [ -n "$IPv6_ADDRESS_NC" ]; then + HAPROXYFILE="$(sed "s|NC_IPV6_PLACEHOLDER|$IPv6_ADDRESS_NC|" /tmp/haproxy.cfg)" +else + HAPROXYFILE="$(sed "s# || { src NC_IPV6_PLACEHOLDER }##g" /tmp/haproxy.cfg)" +fi +echo "$HAPROXYFILE" > /tmp/haproxy.cfg +set +x + +haproxy -f /tmp/haproxy.cfg -db diff --git a/nextcloud-aio/Containers/domaincheck/Dockerfile b/nextcloud-aio/Containers/domaincheck/Dockerfile new file mode 100644 index 0000000..99ae118 --- /dev/null +++ b/nextcloud-aio/Containers/domaincheck/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.22.1 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache bash lighttpd netcat-openbsd; \ + adduser -S www-data -G www-data; \ + rm -rf /etc/lighttpd/lighttpd.conf; \ + chmod 777 -R /etc/lighttpd; \ + mkdir -p /var/www/domaincheck; \ + chown www-data:www-data -R /var/www; \ + chmod 777 -R /var/www/domaincheck +COPY --chown=www-data:www-data lighttpd.conf /lighttpd.conf + +COPY --chmod=775 start.sh /start.sh + +USER www-data +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD nc -z 127.0.0.1 $APACHE_PORT || exit 1 +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/domaincheck/lighttpd.conf b/nextcloud-aio/Containers/domaincheck/lighttpd.conf new file mode 100644 index 0000000..8b2bb84 --- /dev/null +++ b/nextcloud-aio/Containers/domaincheck/lighttpd.conf @@ -0,0 +1,20 @@ +server.document-root = "/var/www/domaincheck/" + +server.port = env.APACHE_PORT + +server.username = "www-data" +server.groupname = "www-data" + +mimetype.assign = ( + ".html" => "text/html", + ".txt" => "text/plain", + ".jpg" => "image/jpeg", + ".png" => "image/png" +) + +static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" ) +index-file.names = ( "index.html" ) + +$SERVER["socket"] == "ipv6-placeholder" { + server.document-root = "/var/www/domaincheck/" +} diff --git a/nextcloud-aio/Containers/domaincheck/start.sh b/nextcloud-aio/Containers/domaincheck/start.sh new file mode 100644 index 0000000..06c0aef --- /dev/null +++ b/nextcloud-aio/Containers/domaincheck/start.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +if [ -z "$INSTANCE_ID" ]; then + echo "You need to provide an instance id." + exit 1 +fi + +echo "$INSTANCE_ID" > /var/www/domaincheck/index.html + +if [ -z "$APACHE_PORT" ]; then + export APACHE_PORT="443" +fi + +CONF_FILE="$(sed "s|ipv6-placeholder|\[::\]:$APACHE_PORT|" /lighttpd.conf)" +echo "$CONF_FILE" > /etc/lighttpd/lighttpd.conf + +# Check config file +lighttpd -tt -f /etc/lighttpd/lighttpd.conf + +# Run server +lighttpd -D -f /etc/lighttpd/lighttpd.conf + +exec "$@" diff --git a/nextcloud-aio/Containers/fulltextsearch/Dockerfile b/nextcloud-aio/Containers/fulltextsearch/Dockerfile new file mode 100644 index 0000000..0651bf5 --- /dev/null +++ b/nextcloud-aio/Containers/fulltextsearch/Dockerfile @@ -0,0 +1,26 @@ +# syntax=docker/dockerfile:latest +# Probably from here https://github.com/elastic/elasticsearch/blob/main/distribution/docker/src/docker/Dockerfile +FROM elasticsearch:8.19.4 + +USER root + +ARG DEBIAN_FRONTEND=noninteractive + +# hadolint ignore=DL3008 +RUN set -ex; \ + \ + apt-get update; \ + apt-get upgrade -y; \ + apt-get install -y --no-install-recommends \ + tzdata \ + ; \ + rm -rf /var/lib/apt/lists/*; + +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +USER 1000:0 + +HEALTHCHECK --interval=10s --timeout=5s --start-period=1m --retries=5 CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" +ENV ES_JAVA_OPTS="-Xms512M -Xmx512M" diff --git a/nextcloud-aio/Containers/fulltextsearch/healthcheck.sh b/nextcloud-aio/Containers/fulltextsearch/healthcheck.sh new file mode 100644 index 0000000..5e888ea --- /dev/null +++ b/nextcloud-aio/Containers/fulltextsearch/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 9200 || exit 1 diff --git a/nextcloud-aio/Containers/imaginary/Dockerfile b/nextcloud-aio/Containers/imaginary/Dockerfile new file mode 100644 index 0000000..79298af --- /dev/null +++ b/nextcloud-aio/Containers/imaginary/Dockerfile @@ -0,0 +1,46 @@ +# syntax=docker/dockerfile:latest +FROM golang:1.25.1-alpine3.22 AS go + +ENV IMAGINARY_HASH=1d4e251cfcd58ea66f8361f8721d7b8cc85002a3 + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + vips-dev \ + vips-magick \ + vips-heif \ + vips-jxl \ + vips-poppler \ + build-base; \ + go install github.com/h2non/imaginary@"$IMAGINARY_HASH"; + +FROM alpine:3.22.1 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + tzdata \ + ca-certificates \ + netcat-openbsd \ + vips \ + vips-magick \ + vips-heif \ + vips-jxl \ + vips-poppler \ + ttf-dejavu \ + bash + +COPY --from=go /go/bin/imaginary /usr/local/bin/imaginary +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +ENV PORT=9000 + +USER 65534 + +# https://github.com/h2non/imaginary#memory-issues +ENV MALLOC_ARENA_MAX=2 +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/imaginary/healthcheck.sh b/nextcloud-aio/Containers/imaginary/healthcheck.sh new file mode 100644 index 0000000..46d700f --- /dev/null +++ b/nextcloud-aio/Containers/imaginary/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 "$PORT" || exit 1 diff --git a/nextcloud-aio/Containers/imaginary/start.sh b/nextcloud-aio/Containers/imaginary/start.sh new file mode 100644 index 0000000..2b93da8 --- /dev/null +++ b/nextcloud-aio/Containers/imaginary/start.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +echo "Imaginary has started" +if [ -z "$IMAGINARY_SECRET" ]; then + imaginary -return-size -max-allowed-resolution 222.2 "$@" +else + imaginary -return-size -max-allowed-resolution 222.2 -key "$IMAGINARY_SECRET" "$@" +fi diff --git a/nextcloud-aio/Containers/mastercontainer/Caddyfile b/nextcloud-aio/Containers/mastercontainer/Caddyfile new file mode 100644 index 0000000..da0e222 --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/Caddyfile @@ -0,0 +1,37 @@ +{ + # auto_https will create redirects for https://{host}:8443 instead of https://{host} + # https redirects are added manually in the http://:80 block + auto_https disable_redirects + + storage file_system { + root /mnt/docker-aio-config/caddy/ + } + + log { + level ERROR + } + + servers { + protocols h1 h2 h2c + } + + on_demand_tls { + ask http://127.0.0.1:9876/ + } +} + +http://:80 { + redir https://{host}{uri} permanent +} + +https://:8443 { + + reverse_proxy 127.0.0.1:8000 + + tls { + on_demand + issuer acme { + disable_tlsalpn_challenge + } + } +} diff --git a/nextcloud-aio/Containers/mastercontainer/Dockerfile b/nextcloud-aio/Containers/mastercontainer/Dockerfile new file mode 100644 index 0000000..2532ec1 --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/Dockerfile @@ -0,0 +1,132 @@ +# syntax=docker/dockerfile:latest +# Docker CLI is a requirement +FROM docker:28.5.0-cli AS docker + +# Caddy is a requirement +FROM caddy:2.10.2-alpine AS caddy + +# From https://github.com/docker-library/php/blob/master/8.4/alpine3.22/fpm/Dockerfile +FROM php:8.4.13-fpm-alpine3.22 + +EXPOSE 80 +EXPOSE 8080 +EXPOSE 8443 + +COPY --from=caddy /usr/bin/caddy /usr/bin/caddy +COPY --from=docker /usr/local/bin/docker /usr/local/bin/docker + +COPY community-containers /var/www/docker-aio/community-containers +COPY php /var/www/docker-aio/php +COPY --chmod=775 Containers/mastercontainer/*.sh / +COPY --chmod=664 Containers/mastercontainer/Caddyfile /Caddyfile +COPY --chmod=664 Containers/mastercontainer/supervisord.conf /supervisord.conf +COPY Containers/mastercontainer/mastercontainer.conf /etc/apache2/sites-available/mastercontainer.conf + +WORKDIR /var/www/docker-aio + +# hadolint ignore=SC2086,DL3047,DL3003,DL3004 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache shadow; \ + groupmod -g 33 www-data; \ + usermod -u 33 -g 33 www-data; \ + \ + apk add --no-cache \ + util-linux-misc \ + ca-certificates \ + wget \ + bash \ + apache2 \ + apache2-proxy \ + apache2-ssl \ + supervisor \ + openssl \ + sudo \ + netcat-openbsd \ + curl \ + grep; \ + \ + apk add --no-cache --virtual .build-deps \ + autoconf \ + build-base; \ + pecl install APCu-5.1.27; \ + docker-php-ext-enable apcu; \ + rm -r /tmp/pear; \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-cache --virtual .nextcloud-aio-rundeps $runDeps; \ + apk del .build-deps; \ + grep -q '^pm = dynamic' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's/^pm = dynamic/pm = ondemand/' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's/^pm.max_children =.*/pm.max_children = 80/' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's|access.log = /proc/self/fd/2|access.log = /proc/self/fd/1|' /usr/local/etc/php-fpm.d/docker.conf; \ + grep -q ';listen.allowed_clients' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's|;listen.allowed_clients.*|listen.allowed_clients = 127.0.0.1,::1|' /usr/local/etc/php-fpm.d/www.conf; \ + \ + apk add --no-cache git; \ + wget https://getcomposer.org/installer -O - | php -- --install-dir=/usr/local/bin --filename=composer; \ + chmod +x /usr/local/bin/composer; \ + cd /var/www/docker-aio; \ + rm -r ./php/tests; \ + chown www-data:www-data -R /var/www/docker-aio; \ + cd php; \ + sudo -u www-data composer install --no-dev; \ + sudo -u www-data composer clear-cache; \ + cd ..; \ + rm -f /usr/local/bin/composer; \ + chmod -R 770 /var/www/docker-aio; \ + chown -R www-data:www-data /var/www; \ + rm -r php/data; \ + rm -r php/session; \ + \ + mkdir -p /etc/apache2/certs; \ + cd /etc/apache2/certs; \ + openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj "/C=DE/ST=BE/L=Local/O=Dev/CN=nextcloud.local" -keyout /etc/apache2/certs/ssl.key -out /etc/apache2/certs/ssl.crt; \ + \ + sed -i \ + -e '/^Listen /d' \ + -e 's/^LogLevel .*/LogLevel error/' \ + -e 's|^ErrorLog .*|ErrorLog /proc/self/fd/2|' \ + -e 's/User apache/User www-data/g' \ + -e 's/Group apache/Group www-data/g' \ + -e 's/^#\(LoadModule .*mod_rewrite.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_headers.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_env.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mime.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_dir.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_authz_core.so\)/\1/' \ + -e 's/^#\(LoadModule .*mod_mpm_event.so\)/\1/' \ + -e 's/\(LoadModule .*mod_mpm_worker.so\)/#\1/' \ + -e 's/\(LoadModule .*mod_mpm_prefork.so\)/#\1/' \ + -e 's/\(ScriptAlias \)/#\1/' \ + /etc/apache2/httpd.conf; \ + mkdir -p /etc/apache2/logs; \ + rm /etc/apache2/conf.d/ssl.conf; \ + echo "ServerName localhost" | tee -a /etc/apache2/httpd.conf; \ + grep -q '^LoadModule lbmethod_heartbeat_module' /etc/apache2/conf.d/proxy.conf; \ + sed -i 's|^LoadModule lbmethod_heartbeat_module.*|#LoadModule lbmethod_heartbeat_module|' /etc/apache2/conf.d/proxy.conf; \ + echo "SSLSessionCache nonenotnull" | tee -a /etc/apache2/httpd.conf; \ + echo "LoadModule ssl_module modules/mod_ssl.so" | tee -a /etc/apache2/httpd.conf; \ + echo "LoadModule socache_shmcb_module modules/mod_socache_shmcb.so" | tee -a /etc/apache2/httpd.conf; \ + echo "Include /etc/apache2/sites-available/mastercontainer.conf" | tee -a /etc/apache2/httpd.conf; \ + \ + rm -f /etc/apache2/conf.d/default.conf \ + /etc/apache2/conf.d/userdir.conf \ + /etc/apache2/conf.d/info.conf; \ + \ + rm -rf /var/www/localhost/cgi-bin/; \ + mkdir /var/log/supervisord; \ + mkdir /var/run/supervisord; + +LABEL org.label-schema.vendor="Nextcloud" + +# hadolint ignore=DL3002 +USER root + +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh diff --git a/nextcloud-aio/Containers/mastercontainer/backup-time-file-watcher.sh b/nextcloud-aio/Containers/mastercontainer/backup-time-file-watcher.sh new file mode 100644 index 0000000..ec9154a --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/backup-time-file-watcher.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +restart_process() { + echo "Restarting cron.sh because daily backup time was set, changed or unset." + pkill cron.sh +} + +file_present() { + if [ -f "/mnt/docker-aio-config/data/daily_backup_time" ]; then + if [ "$FILE_PRESENT" = 0 ]; then + restart_process + else + if [ -n "$BACKUP_TIME" ] && [ "$(head -1 "/mnt/docker-aio-config/data/daily_backup_time")" != "$BACKUP_TIME" ]; then + restart_process + fi + fi + FILE_PRESENT=1 + BACKUP_TIME="$(head -1 "/mnt/docker-aio-config/data/daily_backup_time")" + else + if [ "$FILE_PRESENT" = 1 ]; then + restart_process + fi + FILE_PRESENT=0 + fi +} + +while true; do + file_present + sleep 2 +done diff --git a/nextcloud-aio/Containers/mastercontainer/cron.sh b/nextcloud-aio/Containers/mastercontainer/cron.sh new file mode 100644 index 0000000..fc8c408 --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/cron.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +while true; do + if [ -f "/mnt/docker-aio-config/data/daily_backup_time" ]; then + set -x + BACKUP_TIME="$(head -1 "/mnt/docker-aio-config/data/daily_backup_time")" + export BACKUP_TIME + export DAILY_BACKUP=1 + if [ "$(sed -n '2p' "/mnt/docker-aio-config/data/daily_backup_time")" != 'automaticUpdatesAreNotEnabled' ]; then + export AUTOMATIC_UPDATES=1 + else + export AUTOMATIC_UPDATES=0 + export START_CONTAINERS=1 + fi + if [ "$(sed -n '3p' "/mnt/docker-aio-config/data/daily_backup_time")" != 'successNotificationsAreNotEnabled' ]; then + export SEND_SUCCESS_NOTIFICATIONS=1 + else + export SEND_SUCCESS_NOTIFICATIONS=0 + fi + set +x + if [ -f "/mnt/docker-aio-config/data/daily_backup_running" ]; then + export LOCK_FILE_PRESENT=1 + else + export LOCK_FILE_PRESENT=0 + fi + else + export BACKUP_TIME="04:00" + export DAILY_BACKUP=0 + export LOCK_FILE_PRESENT=0 + fi + + # Allow to continue directly if e.g. the mastercontainer was updated. Otherwise wait for the next execution + if [ "$LOCK_FILE_PRESENT" = 0 ]; then + while [ "$(date +%H:%M)" != "$BACKUP_TIME" ]; do + sleep 30 + done + fi + + if [ "$DAILY_BACKUP" = 1 ]; then + bash /daily-backup.sh + fi + + # Make sure to delete the lock file always + rm -f "/mnt/docker-aio-config/data/daily_backup_running" + + # Check for updates and send notification if yes on saturdays + if [ "$(date +%u)" = 6 ]; then + sudo -u www-data php /var/www/docker-aio/php/src/Cron/UpdateNotification.php + fi + + # Check if AIO is outdated + sudo -u www-data php /var/www/docker-aio/php/src/Cron/OutdatedNotification.php + + # Remove sessions older than 24h + find "/mnt/docker-aio-config/session/" -mindepth 1 -mmin +1440 -delete + + # Remove nextcloud-aio-domaincheck container + if sudo -u www-data docker ps --format "{{.Names}}" --filter "status=exited" | grep -q "^nextcloud-aio-domaincheck$"; then + sudo -u www-data docker container remove nextcloud-aio-domaincheck + fi + + # Remove dangling images + sudo -u www-data docker image prune --force + + # Check for available free space + sudo -u www-data php /var/www/docker-aio/php/src/Cron/CheckFreeDiskSpace.php + + # Remove mastercontainer from default bridge network + if sudo -u www-data docker inspect nextcloud-aio-mastercontainer --format "{{.NetworkSettings.Networks}}" | grep -q "bridge"; then + sudo -u www-data docker network disconnect bridge nextcloud-aio-mastercontainer + fi + + # Wait 60s so that the whole loop will not be executed again + sleep 60 +done diff --git a/nextcloud-aio/Containers/mastercontainer/daily-backup.sh b/nextcloud-aio/Containers/mastercontainer/daily-backup.sh new file mode 100644 index 0000000..5c97c0c --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/daily-backup.sh @@ -0,0 +1,133 @@ +#!/bin/bash + +echo "Daily backup script has started" + +# Check if initial configuration has been done, otherwise this script should do nothing. +CONFIG_FILE=/mnt/docker-aio-config/data/configuration.json +if ! [ -f "$CONFIG_FILE" ] || ! grep -q "wasStartButtonClicked.*1" "$CONFIG_FILE"; then + echo "Initial configuration via AIO interface not done yet. Exiting..." + exit 0 +fi + +# Daily backup and backup check cannot be run at the same time +if [ "$DAILY_BACKUP" = 1 ] && [ "$CHECK_BACKUP" = 1 ]; then + echo "Daily backup and backup check cannot be run at the same time. Exiting..." + exit 1 +fi + +# Delete all active sessions and create a lock file +# But don't kick out the user if the mastercontainer was just updated since we block the interface either way with the lock file +if [ "$LOCK_FILE_PRESENT" = 0 ] || ! [ -f "/mnt/docker-aio-config/data/daily_backup_running" ]; then + find "/mnt/docker-aio-config/session/" -mindepth 1 -delete +fi +sudo -u www-data touch "/mnt/docker-aio-config/data/daily_backup_running" + +# Check if apache is running/stopped, watchtower is stopped and backupcontainer is stopped +APACHE_PORT="$(docker inspect nextcloud-aio-apache --format "{{.Config.Env}}" | grep -o 'APACHE_PORT=[0-9]\+' | grep -o '[0-9]\+' | head -1)" +if [ -z "$APACHE_PORT" ]; then + echo "APACHE_PORT is not set which is not expected..." +else + # Connect mastercontainer to nextcloud-aio network to make sure that nextcloud-aio-apache is reachable + # Prevent issues like https://github.com/nextcloud/all-in-one/discussions/5222 + docker network connect nextcloud-aio nextcloud-aio-mastercontainer &>/dev/null + + # Wait for apache to start + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-apache$" && ! nc -z nextcloud-aio-apache "$APACHE_PORT"; do + echo "Waiting for apache to become available" + sleep 30 + done +fi +while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; do + echo "Waiting for watchtower to stop" + sleep 30 +done +while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-borgbackup$"; do + echo "Waiting for borgbackup to stop" + sleep 30 +done + +# Update the mastercontainer +if [ "$AUTOMATIC_UPDATES" = 1 ]; then + echo "Starting mastercontainer update..." + echo "(The script might get exited due to that. In order to update all the other containers correctly, you need to run this script with the same settings a second time.)" + sudo -u www-data php /var/www/docker-aio/php/src/Cron/UpdateMastercontainer.php +fi + +# Wait for watchtower to stop +if [ "$AUTOMATIC_UPDATES" = 1 ]; then + if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; then + echo "Something seems to be wrong: Watchtower should be started at this step." + fi + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; do + echo "Waiting for watchtower to stop" + sleep 30 + done +fi + +# Update container images to reduce downtime later on +if [ "$AUTOMATIC_UPDATES" = 1 ]; then + echo "Updating container images..." + sudo -u www-data php /var/www/docker-aio/php/src/Cron/PullContainerImages.php +fi + +# Stop containers if required +# shellcheck disable=SC2235 +if [ "$CHECK_BACKUP" != 1 ] && ([ "$DAILY_BACKUP" != 1 ] || [ "$STOP_CONTAINERS" = 1 ]); then + echo "Stopping containers..." + sudo -u www-data php /var/www/docker-aio/php/src/Cron/StopContainers.php +fi + +# Execute the backup itself and some related tasks (also stops the containers) +if [ "$DAILY_BACKUP" = 1 ]; then + echo "Creating daily backup..." + sudo -u www-data php /var/www/docker-aio/php/src/Cron/CreateBackup.php + if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-borgbackup$"; then + echo "Something seems to be wrong: the borg container should be started at this step." + fi + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-borgbackup$"; do + echo "Waiting for backup container to stop" + sleep 30 + done +fi + +# Execute backup check +if [ "$CHECK_BACKUP" = 1 ]; then + echo "Starting backup check..." + sudo -u www-data php /var/www/docker-aio/php/src/Cron/CheckBackup.php +fi + +# Start and/or update containers +if [ "$AUTOMATIC_UPDATES" = 1 ]; then + echo "Starting and updating containers..." + sudo -u www-data php /var/www/docker-aio/php/src/Cron/StartAndUpdateContainers.php +else + if [ "$START_CONTAINERS" = 1 ]; then + echo "Starting containers without updating them..." + sudo -u www-data php /var/www/docker-aio/php/src/Cron/StartContainers.php + fi +fi + +# Delete the lock file +rm -f "/mnt/docker-aio-config/data/daily_backup_running" + +# Send backup notification +# shellcheck disable=SC2235 +if [ "$DAILY_BACKUP" = 1 ] && ([ "$AUTOMATIC_UPDATES" = 1 ] || [ "$START_CONTAINERS" = 1 ]); then + # Wait for the nextcloud container to start and send if the backup was successful + if ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-nextcloud$"; then + echo "Something seems to be wrong: Nextcloud should be started at this step." + else + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-nextcloud$" && ! nc -z nextcloud-aio-nextcloud 9000; do + echo "Waiting for the Nextcloud container to start" + sleep 30 + if [ "$(docker inspect nextcloud-aio-nextcloud --format "{{.State.Restarting}}")" = "true" ]; then + echo "Nextcloud container restarting. Skipping this check!" + break + fi + done + fi + echo "Sending backup notification..." + sudo -E -u www-data php /var/www/docker-aio/php/src/Cron/BackupNotification.php +fi + +echo "Daily backup script has finished" diff --git a/nextcloud-aio/Containers/mastercontainer/healthcheck.sh b/nextcloud-aio/Containers/mastercontainer/healthcheck.sh new file mode 100644 index 0000000..7218759 --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/healthcheck.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +if [ -f "/mnt/docker-aio-config/data/configuration.json" ]; then + nc -z 127.0.0.1 80 || exit 1 + nc -z 127.0.0.1 8000 || exit 1 + nc -z 127.0.0.1 8080 || exit 1 + nc -z 127.0.0.1 8443 || exit 1 + nc -z 127.0.0.1 9000 || exit 1 + nc -z 127.0.0.1 9876 || exit 1 +fi diff --git a/nextcloud-aio/Containers/mastercontainer/mastercontainer.conf b/nextcloud-aio/Containers/mastercontainer/mastercontainer.conf new file mode 100644 index 0000000..6a7d37d --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/mastercontainer.conf @@ -0,0 +1,62 @@ +Listen 8000 +Listen 8080 + +# Deny access to .ht files + + Require all denied + + +# Http host + + ServerName localhost + + # Add error log + CustomLog /proc/self/fd/1 proxy + LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" proxy + ErrorLog /proc/self/fd/2 + ErrorLogFormat "[%t] [%l] [%E] [client: %{X-Forwarded-For}i] [%M] [%{User-Agent}i]" + LogLevel warn + + # PHP match + + SetHandler "proxy:fcgi://127.0.0.1:9000" + + # Master dir + DocumentRoot /var/www/docker-aio/php/public/ + + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [QSA,L] + Options Indexes FollowSymLinks + Require all granted + AllowOverride All + Options FollowSymLinks MultiViews + Satisfy Any + + Dav off + + + + +# Https host + + # Proxy to https + ProxyPass / http://127.0.0.1:8000/ + ProxyPassReverse / http://127.0.0.1:8000/ + ProxyPreserveHost On + # SSL + SSLCertificateKeyFile /etc/apache2/certs/ssl.key + SSLCertificateFile /etc/apache2/certs/ssl.crt + SSLEngine on + SSLProtocol -all +TLSv1.2 +TLSv1.3 + SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 + SSLHonorCipherOrder off + SSLSessionTickets off + + +# Increase timeout in case e.g. the initial download takes a long time +Timeout 7200 +ProxyTimeout 7200 + +# See https://httpd.apache.org/docs/trunk/mod/core.html#traceenable +TraceEnable Off diff --git a/nextcloud-aio/Containers/mastercontainer/session-deduplicator.sh b/nextcloud-aio/Containers/mastercontainer/session-deduplicator.sh new file mode 100644 index 0000000..08ec0f9 --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/session-deduplicator.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +deduplicate_sessions() { + echo "Deleting duplicate sessions" + find "/mnt/docker-aio-config/session/" -mindepth 1 -exec grep -qv "$NEW_SESSION_TIME" {} \; -delete +} + +compare_times() { + if [ -f "/mnt/docker-aio-config/data/session_date_file" ]; then + unset NEW_SESSION_TIME + NEW_SESSION_TIME="$(cat "/mnt/docker-aio-config/data/session_date_file")" + if [ -n "$NEW_SESSION_TIME" ] && [ -n "$OLD_SESSION_TIME" ] && [ "$NEW_SESSION_TIME" != "$OLD_SESSION_TIME" ]; then + deduplicate_sessions + fi + OLD_SESSION_TIME="$NEW_SESSION_TIME" + fi +} + +while true; do + compare_times + sleep 2 +done diff --git a/nextcloud-aio/Containers/mastercontainer/start.sh b/nextcloud-aio/Containers/mastercontainer/start.sh new file mode 100644 index 0000000..616068f --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/start.sh @@ -0,0 +1,390 @@ +#!/bin/bash + +# Function to show text in green +print_green() { + local TEXT="$1" + printf "%b%s%b\n" "\e[0;92m" "$TEXT" "\e[0m" +} + +# Function to show text in red +print_red() { + local TEXT="$1" + printf "%b%s%b\n" "\e[0;31m" "$TEXT" "\e[0m" +} + +# Function to check if number was provided +check_if_number() { +case "${1}" in + ''|*[!0-9]*) return 1 ;; + *) return 0 ;; +esac +} + +# Check if running as root user +if [ "$EUID" != "0" ]; then + print_red "Container does not run as root user. This is not supported." + exit 1 +fi + +# Check that the CMD is not overwritten nor set +if [ "$*" != "" ]; then + print_red "Docker run command for AIO is incorrect as a CMD option was given which is not expected." + exit 1 +fi + +# Check if socket is available and readable +if ! [ -e "/var/run/docker.sock" ]; then + print_red "Docker socket is not available. Cannot continue." + echo "Please make sure to mount the docker socket into /var/run/docker.sock inside the container!" + echo "If you did this by purpose because you don't want the container to have access to the docker socket, see https://github.com/nextcloud/all-in-one/tree/main/manual-install." + echo "And https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" + exit 1 +elif ! mountpoint -q "/mnt/docker-aio-config"; then + print_red "/mnt/docker-aio-config is not a mountpoint. Cannot proceed!" + echo "Please make sure to mount the nextcloud_aio_mastercontainer docker volume into /mnt/docker-aio-config inside the container!" + echo "If you are on TrueNas SCALE, see https://github.com/nextcloud/all-in-one#can-i-run-aio-on-truenas-scale" + exit 1 +elif mountpoint -q /var/www/docker-aio/php/containers.json; then + print_red "/var/www/docker-aio/php/containers.json is a mountpoint. Cannot proceed!" + echo "This is a not-supported customization of the mastercontainer!" + echo "Please remove this bind-mount from the mastercontainer." + echo "If you need to customize things, feel free to use https://github.com/nextcloud/all-in-one/tree/main/manual-install" + echo "See https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" + exit 1 +elif ! sudo -u www-data test -r /var/run/docker.sock; then + echo "Trying to fix docker.sock permissions internally..." + DOCKER_GROUP=$(stat -c '%G' /var/run/docker.sock) + DOCKER_GROUP_ID=$(stat -c '%g' /var/run/docker.sock) + # Check if a group with the same group name of /var/run/docker.socket already exists in the container + if grep -q "^$DOCKER_GROUP:" /etc/group; then + # If yes, add www-data to that group + echo "Adding internal www-data to group $DOCKER_GROUP" + usermod -aG "$DOCKER_GROUP" www-data + else + # Delete the docker group for cases when the docker socket permissions changed between restarts + groupdel docker &>/dev/null + + # If the group doesn't exist, create it + echo "Creating docker group internally with id $DOCKER_GROUP_ID" + groupadd -g "$DOCKER_GROUP_ID" docker + usermod -aG docker www-data + fi + if ! sudo -u www-data test -r /var/run/docker.sock; then + print_red "Docker socket is not readable by the www-data user. Cannot continue." + exit 1 + fi +fi + +# Check if api version is supported +if ! sudo -u www-data docker info &>/dev/null; then + print_red "Cannot connect to the docker socket. Cannot proceed." + echo "Did you maybe remove group read permissions for the docker socket? AIO needs them in order to access the docker socket." + echo "If SELinux is enabled on your host, see https://github.com/nextcloud/all-in-one#are-there-known-problems-when-selinux-is-enabled" + echo "If you are on TrueNas SCALE, see https://github.com/nextcloud/all-in-one#can-i-run-aio-on-truenas-scale" + exit 1 +fi +API_VERSION_FILE="$(find ./ -name DockerActionManager.php | head -1)" +API_VERSION="$(grep -oP 'const string API_VERSION.*\;' "$API_VERSION_FILE" | grep -oP '[0-9]+.[0-9]+' | head -1)" +# shellcheck disable=SC2001 +API_VERSION_NUMB="$(echo "$API_VERSION" | sed 's/\.//')" +LOCAL_API_VERSION_NUMB="$(sudo -u www-data docker version | grep -i "api version" | grep -oP '[0-9]+.[0-9]+' | head -1 | sed 's/\.//')" +if [ -n "$LOCAL_API_VERSION_NUMB" ] && [ -n "$API_VERSION_NUMB" ]; then + if ! [ "$LOCAL_API_VERSION_NUMB" -ge "$API_VERSION_NUMB" ]; then + print_red "Docker API v$API_VERSION is not supported by your docker engine. Cannot proceed. Please upgrade your docker engine if you want to run Nextcloud AIO!" + exit 1 + fi +else + echo "LOCAL_API_VERSION_NUMB or API_VERSION_NUMB are not set correctly. Cannot check if the API version is supported." + sleep 10 +fi + +# Check Storage drivers +STORAGE_DRIVER="$(sudo -u www-data docker info | grep "Storage Driver")" +# Check if vfs is used: https://github.com/nextcloud/all-in-one/discussions/1467 +if echo "$STORAGE_DRIVER" | grep -q vfs; then + echo "$STORAGE_DRIVER" + print_red "Warning: It seems like the storage driver vfs is used. This will lead to problems with disk space and performance and is disrecommended!" +elif echo "$STORAGE_DRIVER" | grep -q fuse-overlayfs; then + echo "$STORAGE_DRIVER" + print_red "Warning: It seems like the storage driver fuse-overlayfs is used. Please check if you can switch to overlay2 instead." +fi + +# Check if snap install +if sudo -u www-data docker info | grep "Docker Root Dir" | grep "/var/snap/docker/"; then + print_red "Warning: It looks like your installation uses docker installed via snap." + print_red "This comes with some limitations and is disrecommended by the docker maintainers." + print_red "See for example https://github.com/nextcloud/all-in-one/discussions/4890#discussioncomment-10386752" +fi + +# Check if startup command was executed correctly +if ! sudo -u www-data docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-mastercontainer$"; then + print_red "It seems like you did not give the mastercontainer the correct name? (The 'nextcloud-aio-mastercontainer' container was not found.) +Using a different name is not supported since mastercontainer updates will not work in that case! +If you are on docker swarm and try to run AIO, see https://github.com/nextcloud/all-in-one#can-i-run-this-with-docker-swarm" + exit 1 +elif ! sudo -u www-data docker volume ls --format "{{.Name}}" | grep -q "^nextcloud_aio_mastercontainer$"; then + print_red "It seems like you did not give the mastercontainer volume the correct name? (The 'nextcloud_aio_mastercontainer' volume was not found.) +Using a different name is not supported since the built-in backup solution will not work in that case!" + exit 1 +elif ! sudo -u www-data docker inspect nextcloud-aio-mastercontainer | grep -q "nextcloud_aio_mastercontainer"; then + print_red "It seems like you did not attach the 'nextcloud_aio_mastercontainer' volume to the mastercontainer? +This is not supported since the built-in backup solution will not work in that case!" + exit 1 +fi + +# Check for other options +if [ -n "$NEXTCLOUD_DATADIR" ]; then + if [ "$NEXTCLOUD_DATADIR" = "nextcloud_aio_nextcloud_datadir" ]; then + sleep 1 + elif ! echo "$NEXTCLOUD_DATADIR" | grep -q "^/" || [ "$NEXTCLOUD_DATADIR" = "/" ]; then + print_red "You've set NEXTCLOUD_DATADIR but not to an allowed value. +The string must start with '/' and must not be equal to '/'. Also allowed is 'nextcloud_aio_nextcloud_datadir'. +It is set to '$NEXTCLOUD_DATADIR'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_MOUNT" ]; then + if ! echo "$NEXTCLOUD_MOUNT" | grep -q "^/" || [ "$NEXTCLOUD_MOUNT" = "/" ]; then + print_red "You've set NEXTCLOUD_MOUNT but not to an allowed value. +The string must start with '/' and must not be equal to '/'. +It is set to '$NEXTCLOUD_MOUNT'." + exit 1 + elif [ "$NEXTCLOUD_MOUNT" = "/mnt/ncdata" ] || echo "$NEXTCLOUD_MOUNT" | grep -q "^/mnt/ncdata/"; then + print_red "'/mnt/ncdata' and '/mnt/ncdata/' are not allowed as values for NEXTCLOUD_MOUNT." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_DATADIR" ] && [ -n "$NEXTCLOUD_MOUNT" ]; then + if [ "$NEXTCLOUD_DATADIR" = "$NEXTCLOUD_MOUNT" ]; then + print_red "NEXTCLOUD_DATADIR and NEXTCLOUD_MOUNT are not allowed to be equal." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_UPLOAD_LIMIT" ]; then + if ! echo "$NEXTCLOUD_UPLOAD_LIMIT" | grep -q '^[0-9]\+G$'; then + print_red "You've set NEXTCLOUD_UPLOAD_LIMIT but not to an allowed value. +The string must start with a number and end with 'G'. +It is set to '$NEXTCLOUD_UPLOAD_LIMIT'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_MAX_TIME" ]; then + if ! echo "$NEXTCLOUD_MAX_TIME" | grep -q '^[0-9]\+$'; then + print_red "You've set NEXTCLOUD_MAX_TIME but not to an allowed value. +The string must be a number. E.g. '3600'. +It is set to '$NEXTCLOUD_MAX_TIME'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_MEMORY_LIMIT" ]; then + if ! echo "$NEXTCLOUD_MEMORY_LIMIT" | grep -q '^[0-9]\+M$'; then + print_red "You've set NEXTCLOUD_MEMORY_LIMIT but not to an allowed value. +The string must start with a number and end with 'M'. +It is set to '$NEXTCLOUD_MEMORY_LIMIT'." + exit 1 + fi +fi +if [ -n "$APACHE_PORT" ]; then + if ! check_if_number "$APACHE_PORT"; then + print_red "You provided an Apache port but did not only use numbers. +It is set to '$APACHE_PORT'." + exit 1 + elif ! [ "$APACHE_PORT" -le 65535 ] || ! [ "$APACHE_PORT" -ge 1 ]; then + print_red "The provided Apache port is invalid. It must be between 1 and 65535" + exit 1 + fi +fi +if [ -n "$APACHE_IP_BINDING" ]; then + if ! echo "$APACHE_IP_BINDING" | grep -q '^[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+$\|^[0-9a-f:]\+$\|^@INTERNAL$'; then + print_red "You provided an ip-address for the apache container's ip-binding but it was not a valid ip-address. +It is set to '$APACHE_IP_BINDING'." + exit 1 + fi +fi +if [ -n "$APACHE_ADDITIONAL_NETWORK" ]; then + if ! echo "$APACHE_ADDITIONAL_NETWORK" | grep -q "^[a-zA-Z0-9._-]\+$"; then + print_red "You've set APACHE_ADDITIONAL_NETWORK but not to an allowed value. +It needs to be a string with letters, numbers, hyphens and underscores. +It is set to '$APACHE_ADDITIONAL_NETWORK'." + exit 1 + fi +fi +if [ -n "$TALK_PORT" ]; then + if ! check_if_number "$TALK_PORT"; then + print_red "You provided an Talk port but did not only use numbers. +It is set to '$TALK_PORT'." + exit 1 + elif ! [ "$TALK_PORT" -le 65535 ] || ! [ "$TALK_PORT" -ge 1 ]; then + print_red "The provided Talk port is invalid. It must be between 1 and 65535" + exit 1 + fi +fi +if [ -n "$APACHE_PORT" ] && [ -n "$TALK_PORT" ]; then + if [ "$APACHE_PORT" = "$TALK_PORT" ]; then + print_red "APACHE_PORT and TALK_PORT are not allowed to be equal." + exit 1 + fi +fi +if [ -n "$WATCHTOWER_DOCKER_SOCKET_PATH" ]; then + if ! echo "$WATCHTOWER_DOCKER_SOCKET_PATH" | grep -q "^/" || echo "$WATCHTOWER_DOCKER_SOCKET_PATH" | grep -q "/$"; then + print_red "You've set WATCHTOWER_DOCKER_SOCKET_PATH but not to an allowed value. +The string must start with '/' and must not end with '/'. +It is set to '$WATCHTOWER_DOCKER_SOCKET_PATH'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_TRUSTED_CACERTS_DIR" ]; then + if ! echo "$NEXTCLOUD_TRUSTED_CACERTS_DIR" | grep -q "^/" || echo "$NEXTCLOUD_TRUSTED_CACERTS_DIR" | grep -q "/$"; then + print_red "You've set NEXTCLOUD_TRUSTED_CACERTS_DIR but not to an allowed value. +It should be an absolute path to a directory that starts with '/' but not end with '/'. +It is set to '$NEXTCLOUD_TRUSTED_CACERTS_DIR '." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_STARTUP_APPS" ]; then + if ! echo "$NEXTCLOUD_STARTUP_APPS" | grep -q "^[a-z0-9 _-]\+$"; then + print_red "You've set NEXTCLOUD_STARTUP_APPS but not to an allowed value. +It needs to be a string. Allowed are small letters a-z, 0-9, spaces, hyphens and '_'. +It is set to '$NEXTCLOUD_STARTUP_APPS'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_ADDITIONAL_APKS" ]; then + if ! echo "$NEXTCLOUD_ADDITIONAL_APKS" | grep -q "^[a-z0-9 ._-]\+$"; then + print_red "You've set NEXTCLOUD_ADDITIONAL_APKS but not to an allowed value. +It needs to be a string. Allowed are small letters a-z, digits 0-9, spaces, hyphens, dots and '_'. +It is set to '$NEXTCLOUD_ADDITIONAL_APKS'." + exit 1 + fi +fi +if [ -n "$NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS" ]; then + if ! echo "$NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS" | grep -q "^[a-z0-9 ._-]\+$"; then + print_red "You've set NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS but not to an allowed value. +It needs to be a string. Allowed are small letters a-z, digits 0-9, spaces, hyphens, dots and '_'. +It is set to '$NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS'." + exit 1 + fi +fi +if [ -n "$AIO_COMMUNITY_CONTAINERS" ]; then + print_red "You've set AIO_COMMUNITY_CONTAINERS but the option was removed. +The community containers get managed via the AIO interface now." +fi + +# Check if ghcr.io is reachable +# Solves issues like https://github.com/nextcloud/all-in-one/discussions/5268 +if ! curl --no-progress-meter https://ghcr.io/v2/ >/dev/null; then + print_red "Could not reach https://ghcr.io." + echo "Most likely is something blocking access to it." + echo "You should be able to fix this by following https://dockerlabs.collabnix.com/intermediate/networking/Configuring_DNS.html" + echo "Another solution is using https://github.com/nextcloud/all-in-one/tree/main/manual-install" + echo "See https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" + exit 1 +fi + +# Check that no changes have been made to timezone settings since AIO only supports running in Etc/UTC timezone +if [ -n "$TZ" ]; then + print_red "The environmental variable TZ has been set which is not supported by AIO since it only supports running in the default Etc/UTC timezone!" + echo "The correct timezone can be set in the AIO interface later on!" + # Disable exit since it seems to be by default set on unraid and we dont want to break these instances + # exit 1 +fi +# Check that http proxy or no_proxy variable is not set which AIO does not support +if [ -n "$HTTP_PROXY" ] || [ -n "$http_proxy" ] || [ -n "$HTTPS_PROXY" ] || [ -n "$https_proxy" ] || [ -n "$NO_PROXY" ] || [ -n "$no_proxy" ]; then + print_red "The environmental variable HTTP_PROXY, http_proxy, HTTPS_PROXY, https_proxy, NO_PROXY or no_proxy has been set which is not supported by AIO." + echo "If you need this, then you should use https://github.com/nextcloud/all-in-one/tree/main/manual-install" + echo "See https://github.com/nextcloud/all-in-one/blob/main/manual-install/latest.yml" + exit 1 +fi +if mountpoint -q /etc/localtime; then + print_red "/etc/localtime has been mounted into the container which is not allowed because AIO only supports running in the default Etc/UTC timezone!" + echo "The correct timezone can be set in the AIO interface later on!" + exit 1 +fi +if mountpoint -q /etc/timezone; then + print_red "/etc/timezone has been mounted into the container which is not allowed because AIO only supports running in the default Etc/UTC timezone!" + echo "The correct timezone can be set in the AIO interface later on!" + exit 1 +fi + +# Check if unsupported env are set (but don't exit as it would break many instances) +if [ -n "$APACHE_DISABLE_REWRITE_IP" ]; then + print_red "The environmental variable APACHE_DISABLE_REWRITE_IP has been set which is not supported by AIO. Please remove it!" +fi +if [ -n "$NEXTCLOUD_TRUSTED_DOMAINS" ]; then + print_red "The environmental variable NEXTCLOUD_TRUSTED_DOMAINS has been set which is not supported by AIO. Please remove it!" +fi +if [ -n "$TRUSTED_PROXIES" ]; then + print_red "The environmental variable TRUSTED_PROXIES has been set which is not supported by AIO. Please remove it!" +fi + +# Add important folders +mkdir -p /mnt/docker-aio-config/data/ +mkdir -p /mnt/docker-aio-config/session/ +mkdir -p /mnt/docker-aio-config/caddy/ +mkdir -p /mnt/docker-aio-config/certs/ + +# Adjust permissions for all instances +chmod 770 -R /mnt/docker-aio-config +chmod 777 /mnt/docker-aio-config +chown www-data:www-data -R /mnt/docker-aio-config/data/ +chown www-data:www-data -R /mnt/docker-aio-config/session/ +chown www-data:www-data -R /mnt/docker-aio-config/caddy/ +chown root:root -R /mnt/docker-aio-config/certs/ + +# Don't allow access to the AIO interface from the Nextcloud container +# Probably more cosmetic than anything but at least an attempt +if ! grep -q '# nextcloud-aio-block' /etc/apache2/httpd.conf; then + cat << APACHE_CONF >> /etc/apache2/httpd.conf +# nextcloud-aio-block-start + +order allow,deny +deny from nextcloud-aio-nextcloud.nextcloud-aio +allow from all + +# nextcloud-aio-block-end +APACHE_CONF +fi + +# Adjust certs +GENERATED_CERTS="/mnt/docker-aio-config/certs" +TMP_CERTS="/etc/apache2/certs" +mkdir -p "$GENERATED_CERTS" +cd "$GENERATED_CERTS" || exit 1 +if ! [ -f ./ssl.crt ] && ! [ -f ./ssl.key ]; then + openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 -subj "/C=DE/ST=BE/L=Local/O=Dev/CN=nextcloud.local" -keyout ./ssl.key -out ./ssl.crt +fi +if [ -f ./ssl.crt ] && [ -f ./ssl.key ]; then + cd "$TMP_CERTS" || exit 1 + rm ./ssl.crt + rm ./ssl.key + cp "$GENERATED_CERTS/ssl.crt" ./ + cp "$GENERATED_CERTS/ssl.key" ./ +fi + +print_green "Initial startup of Nextcloud All-in-One complete! +You should be able to open the Nextcloud AIO Interface now on port 8080 of this server! +E.g. https://internal.ip.of.this.server:8080 +⚠️ Important: do always use an ip-address if you access this port and not a domain as HSTS might block access to it later! + +If your server has port 80 and 8443 open and you point a domain to your server, you can get a valid certificate automatically by opening the Nextcloud AIO Interface via: +https://your-domain-that-points-to-this-server.tld:8443" + +# Set the timezone to Etc/UTC +export TZ=Etc/UTC + +# Fix apache startup +rm -f /var/run/apache2/httpd.pid + +# Fix caddy startup +if [ -d "/mnt/docker-aio-config/caddy/locks" ]; then + rm -rf /mnt/docker-aio-config/caddy/locks/* +fi + +# Fix the Caddyfile format +caddy fmt --overwrite /Caddyfile + +# Fix caddy log +chmod 777 /root + +# Start supervisord +exec /usr/bin/supervisord -c /supervisord.conf diff --git a/nextcloud-aio/Containers/mastercontainer/supervisord.conf b/nextcloud-aio/Containers/mastercontainer/supervisord.conf new file mode 100644 index 0000000..fa5d084 --- /dev/null +++ b/nextcloud-aio/Containers/mastercontainer/supervisord.conf @@ -0,0 +1,64 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisord/supervisord.log +pidfile=/var/run/supervisord/supervisord.pid +childlogdir=/var/log/supervisord/ +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=error +user=root + +[program:php-fpm] +# Stdout logging is disabled as otherwise the logs are spammed +stdout_logfile=NONE +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=php-fpm +user=root + +[program:apache] +# Stdout logging is disabled as otherwise the logs are spammed +stdout_logfile=NONE +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=httpd -DFOREGROUND +user=root + +[program:caddy] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/usr/bin/caddy run --config /Caddyfile +user=www-data + +[program:cron] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/cron.sh +user=root + +[program:backup-time-file-watcher] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/backup-time-file-watcher.sh +user=root + +[program:session-deduplicator] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/session-deduplicator.sh +user=root + +[program:domain-validator] +# Logging is disabled as otherwise all attempts will be logged which spams the logs +stdout_logfile=NONE +stderr_logfile=NONE +command=php -S 127.0.0.1:9876 /var/www/docker-aio/php/domain-validator.php +user=www-data diff --git a/nextcloud-aio/Containers/nextcloud/Dockerfile b/nextcloud-aio/Containers/nextcloud/Dockerfile new file mode 100644 index 0000000..0ae91b6 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/Dockerfile @@ -0,0 +1,266 @@ +# syntax=docker/dockerfile:latest +FROM php:8.3.26-fpm-alpine3.22 + +ENV PHP_MEMORY_LIMIT=512M +ENV PHP_UPLOAD_LIMIT=16G +ENV PHP_MAX_TIME=3600 +ENV SOURCE_LOCATION=/usr/src/nextcloud +ENV REDIS_DB_INDEX=0 + +# AIO settings start # Do not remove or change this line! +ENV NEXTCLOUD_VERSION=31.0.9 +ENV AIO_TOKEN=123456 +ENV AIO_URL=localhost +# AIO settings end # Do not remove or change this line! + +COPY --chmod=775 Containers/nextcloud/*.sh / +COPY --chmod=774 Containers/nextcloud/upgrade.exclude /upgrade.exclude +COPY Containers/nextcloud/config/*.php / +COPY Containers/nextcloud/supervisord.conf /supervisord.conf + +# AIO cloning start # Do not remove or change this line! +COPY app /usr/src/nextcloud/apps/nextcloud-aio +COPY Containers/nextcloud/root.motd /root.motd +# AIO cloning end # Do not remove or change this line! + +VOLUME /mnt/ncdata +VOLUME /var/www/html + +# Custom: change id of www-data user as it needs to be the same like on old installations +# hadolint ignore=SC2086,DL3003 +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache shadow; \ + deluser www-data; \ + addgroup -g 33 -S www-data; \ + adduser -u 33 -D -S -G www-data www-data; \ + \ +# entrypoint.sh and cron.sh dependencies + apk add --no-cache \ + rsync \ + ; \ +# install the PHP extensions we need +# see https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + autoconf \ + freetype-dev \ + gmp-dev \ + icu-dev \ + imagemagick-dev \ + imagemagick-svg \ + imagemagick-heic \ + imagemagick-tiff \ + libevent-dev \ + libjpeg-turbo-dev \ + libmcrypt-dev \ + libmemcached-dev \ + libpng-dev \ + libwebp-dev \ + libxml2-dev \ + libzip-dev \ + openldap-dev \ + pcre-dev \ + postgresql-dev \ + ; \ + \ + docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \ + docker-php-ext-configure ftp --with-openssl-dir=/usr; \ + docker-php-ext-configure ldap; \ + docker-php-ext-install -j "$(nproc)" \ + bcmath \ + exif \ + gd \ + gmp \ + intl \ + ldap \ + opcache \ + pcntl \ + pdo_pgsql \ + sysvsem \ + zip \ + ; \ + \ +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install -o igbinary-3.2.16; \ + pecl install APCu-5.1.27; \ + pecl install -D 'enable-memcached-igbinary="yes"' memcached-3.3.0; \ + pecl install -oD 'enable-redis-igbinary="yes" enable-redis-zstd="yes" enable-redis-lz4="yes"' redis-6.2.0; \ + pecl install -o imagick-3.8.0; \ + \ + docker-php-ext-enable \ + igbinary \ + apcu \ + memcached \ + redis \ + ; \ + rm -r /tmp/pear; \ + \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-cache --virtual .nextcloud-phpext-rundeps $runDeps; \ + apk del .build-deps; \ + \ + { \ + echo 'apc.serializer=igbinary'; \ + echo 'session.serialize_handler=igbinary'; \ + } >> /usr/local/etc/php/conf.d/docker-php-ext-igbinary.ini; \ + \ +# set recommended PHP.ini settings +# see https://docs.nextcloud.com/server/stable/admin_manual/installation/server_tuning.html#enable-php-opcache and below + { \ + echo 'opcache.max_accelerated_files=10000'; \ + echo 'opcache.memory_consumption=256'; \ + echo 'opcache.interned_strings_buffer=64'; \ + echo 'opcache.save_comments=1'; \ + echo 'opcache.revalidate_freq=60'; \ + echo 'opcache.jit=1255'; \ + echo 'opcache.jit_buffer_size=8M'; \ + } > /usr/local/etc/php/conf.d/opcache-recommended.ini; \ + \ + { \ + echo 'apc.enable_cli=1'; \ + echo 'apc.shm_size=64M'; \ + } >> /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini; \ + \ + { \ + echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \ + echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \ + echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \ + echo 'max_execution_time=${PHP_MAX_TIME}'; \ + echo 'max_input_time=${PHP_MAX_TIME}'; \ + echo 'default_socket_timeout=${PHP_MAX_TIME}'; \ + } > /usr/local/etc/php/conf.d/nextcloud.ini; \ + \ + { \ + echo 'session.save_handler = redis'; \ + echo 'session.save_path = "tcp://${REDIS_HOST}:6379?database=${REDIS_DB_INDEX}${REDIS_USER_AUTH}&auth[]=${REDIS_HOST_PASSWORD}"'; \ + echo 'redis.session.locking_enabled = 1'; \ + echo 'redis.session.lock_retries = -1'; \ + echo 'redis.session.lock_wait_time = 10000'; \ + echo 'session.gc_maxlifetime = 86400'; \ + } > /usr/local/etc/php/conf.d/redis-session.ini; \ + \ + mkdir -p /var/www/data; \ + chown -R www-data:root /var/www; \ + chmod -R g=u /var/www; \ + \ +# Download Nextcloud archive start # Do not remove or change this line! + apk add --no-cache --virtual .fetch-deps \ + bzip2 \ + gnupg \ + ; \ + \ + curl -fsSL -o nextcloud.tar.bz2 \ + "https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2"; \ + curl -fsSL -o nextcloud.tar.bz2.asc \ + "https://download.nextcloud.com/server/releases/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2.asc"; \ + export GNUPGHOME="$(mktemp -d)"; \ +# gpg key from https://nextcloud.com/nextcloud.asc + gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 28806A878AE423A28372792ED75899B9A724937A; \ + gpg --batch --verify nextcloud.tar.bz2.asc nextcloud.tar.bz2; \ + tar -xjf nextcloud.tar.bz2 -C /usr/src/; \ + gpgconf --kill all; \ + rm nextcloud.tar.bz2.asc nextcloud.tar.bz2; \ + mkdir -p /usr/src/nextcloud/data; \ + mkdir -p /usr/src/nextcloud/custom_apps; \ + chmod +x /usr/src/nextcloud/occ; \ + mkdir -p /usr/src/nextcloud/config; \ + apk del .fetch-deps; \ +# Download Nextcloud archive end # Do not remove or change this line! + mv /*.php /usr/src/nextcloud/config/; \ + \ +# Template from https://github.com/nextcloud/docker/blob/master/.examples/dockerfiles/full/fpm-alpine/Dockerfile + apk add --no-cache \ + ffmpeg \ + procps \ + samba-client \ + supervisor \ +# libreoffice \ + ; \ + \ + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + imap-dev \ + krb5-dev \ + openssl-dev \ + samba-dev \ + bzip2-dev \ + libpq-dev \ + ; \ + \ + docker-php-ext-configure imap --with-kerberos --with-imap-ssl; \ + docker-php-ext-install \ + bz2 \ + imap \ + pgsql \ + ftp \ + ; \ + pecl install smbclient; \ + docker-php-ext-enable smbclient; \ + \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; \ + apk add --no-cache --virtual .nextcloud-phpext-rundeps $runDeps; \ + apk del .build-deps; \ + \ + mkdir -p \ + /var/log/supervisord \ + /var/run/supervisord \ + ; \ + chmod 777 -R /var/log/supervisord; \ + chmod 777 -R /var/run/supervisord; \ + \ + apk add --no-cache \ + bash \ + netcat-openbsd \ + openssl \ + gnupg \ + git \ + postgresql-client \ + tzdata \ + sudo \ + grep \ + nodejs \ + libreoffice \ + bind-tools \ + imagemagick \ + imagemagick-svg \ + imagemagick-heic \ + imagemagick-tiff \ + coreutils; \ + \ + grep -q '^pm = dynamic' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's/^pm = dynamic/pm = ondemand/' /usr/local/etc/php-fpm.d/www.conf; \ +# Sync this with max db connections and MaxRequestWorkers +# We don't actually expect so many children but don't want to limit it artificially because people will report issues otherwise. +# Also children will usually be terminated again after the process is done due to the ondemand setting + sed -i 's/^pm.max_children =.*/pm.max_children = 5000/' /usr/local/etc/php-fpm.d/www.conf; \ + sed -i 's|access.log = /proc/self/fd/2|access.log = /proc/self/fd/1|' /usr/local/etc/php-fpm.d/docker.conf; \ + \ + echo "[ -n \"\$TERM\" ] && [ -f /root.motd ] && cat /root.motd" >> /root/.bashrc; \ + \ + chown www-data:root -R /usr/src && \ + chmod 777 -R /usr/local/etc/php/conf.d && \ + chmod 777 -R /usr/local/etc/php-fpm.d && \ + chmod -R 777 /tmp; \ + \ + mkdir -p /nc-updater; \ + chmod -R 777 /nc-updater + +# hadolint ignore=DL3002 +USER root +ENTRYPOINT ["/start.sh"] +CMD ["/usr/bin/supervisord", "-c", "/supervisord.conf"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/nextcloud/config/aio.config.php b/nextcloud-aio/Containers/nextcloud/config/aio.config.php new file mode 100644 index 0000000..7c80b6b --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/aio.config.php @@ -0,0 +1,5 @@ + true, + 'one-click-instance.user-limit' => 100, +); diff --git a/nextcloud-aio/Containers/nextcloud/config/apcu.config.php b/nextcloud-aio/Containers/nextcloud/config/apcu.config.php new file mode 100644 index 0000000..69fed87 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/apcu.config.php @@ -0,0 +1,4 @@ + '\OC\Memcache\APCu', +); diff --git a/nextcloud-aio/Containers/nextcloud/config/apps.config.php b/nextcloud-aio/Containers/nextcloud/config/apps.config.php new file mode 100644 index 0000000..99bf5e4 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/apps.config.php @@ -0,0 +1,21 @@ + array ( + 0 => array ( + 'path' => '/var/www/html/apps', + 'url' => '/apps', + 'writable' => false, + ), + 1 => array ( + 'path' => '/var/www/html/custom_apps', + 'url' => '/custom_apps', + 'writable' => true, + ), + ), +); +if (getenv('APPS_ALLOWLIST')) { + $CONFIG['appsallowlist'] = explode(" ", getenv('APPS_ALLOWLIST')); +} +if (getenv('NEXTCLOUD_APP_STORE_URL')) { + $CONFIG['appstoreurl'] = getenv('NEXTCLOUD_APP_STORE_URL'); +} diff --git a/nextcloud-aio/Containers/nextcloud/config/postgres.config.php b/nextcloud-aio/Containers/nextcloud/config/postgres.config.php new file mode 100644 index 0000000..38f980f --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/postgres.config.php @@ -0,0 +1,9 @@ + array( + 'mode' => 'verify-ca', + 'rootcert' => '/var/www/html/data/certificates/POSTGRES', + ), + ); +} diff --git a/nextcloud-aio/Containers/nextcloud/config/proxy.config.php b/nextcloud-aio/Containers/nextcloud/config/proxy.config.php new file mode 100644 index 0000000..c283f86 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/proxy.config.php @@ -0,0 +1,13 @@ + '\OC\Memcache\Redis', + 'memcache.locking' => '\OC\Memcache\Redis', + 'redis' => array( + 'host' => getenv('REDIS_HOST'), + 'password' => (string) getenv('REDIS_HOST_PASSWORD'), + ), + ); + + if (getenv('REDIS_HOST_PORT')) { + $CONFIG['redis']['port'] = (int) getenv('REDIS_HOST_PORT'); + } elseif (getenv('REDIS_HOST')[0] != '/') { + $CONFIG['redis']['port'] = 6379; + } + + if (getenv('REDIS_DB_INDEX')) { + $CONFIG['redis']['dbindex'] = (int) getenv('REDIS_DB_INDEX'); + } + + if (getenv('REDIS_USER_AUTH') !== false) { + $CONFIG['redis']['user'] = str_replace("&auth[]=", "", getenv('REDIS_USER_AUTH')); + } +} diff --git a/nextcloud-aio/Containers/nextcloud/config/reverse-proxy.config.php b/nextcloud-aio/Containers/nextcloud/config/reverse-proxy.config.php new file mode 100644 index 0000000..c865091 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/reverse-proxy.config.php @@ -0,0 +1,20 @@ + array( + 'class' => '\OC\Files\ObjectStore\S3', + 'arguments' => array( + 'bucket' => getenv('OBJECTSTORE_S3_BUCKET'), + 'key' => getenv('OBJECTSTORE_S3_KEY') ?: '', + 'secret' => getenv('OBJECTSTORE_S3_SECRET') ?: '', + 'region' => getenv('OBJECTSTORE_S3_REGION') ?: '', + 'hostname' => getenv('OBJECTSTORE_S3_HOST') ?: '', + 'port' => getenv('OBJECTSTORE_S3_PORT') ?: '', + 'storageClass' => getenv('OBJECTSTORE_S3_STORAGE_CLASS') ?: '', + 'objectPrefix' => getenv("OBJECTSTORE_S3_OBJECT_PREFIX") ? getenv("OBJECTSTORE_S3_OBJECT_PREFIX") : "urn:oid:", + 'autocreate' => strtolower($autocreate) !== 'false', + 'use_ssl' => strtolower($use_ssl) !== 'false', + // required for some non Amazon S3 implementations + 'use_path_style' => strtolower($use_path) === 'true', + // required for older protocol versions + 'legacy_auth' => strtolower($use_legacyauth) === 'true' + ) + ) + ); + + $sse_c_key = getenv('OBJECTSTORE_S3_SSE_C_KEY'); + if ($sse_c_key) { + $CONFIG['objectstore']['arguments']['sse_c_key'] = $sse_c_key; + } +} diff --git a/nextcloud-aio/Containers/nextcloud/config/smtp.config.php b/nextcloud-aio/Containers/nextcloud/config/smtp.config.php new file mode 100644 index 0000000..40cfdf9 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/smtp.config.php @@ -0,0 +1,20 @@ + 'smtp', + 'mail_smtphost' => getenv('SMTP_HOST'), + 'mail_smtpport' => getenv('SMTP_PORT') ?: (getenv('SMTP_SECURE') ? 465 : 25), + 'mail_smtpsecure' => getenv('SMTP_SECURE') ?: '', + 'mail_smtpauth' => getenv('SMTP_NAME') && getenv('SMTP_PASSWORD'), + 'mail_smtpauthtype' => getenv('SMTP_AUTHTYPE') ?: 'LOGIN', + 'mail_smtpname' => getenv('SMTP_NAME') ?: '', + 'mail_from_address' => getenv('MAIL_FROM_ADDRESS'), + 'mail_domain' => getenv('MAIL_DOMAIN'), + ); + + if (getenv('SMTP_PASSWORD')) { + $CONFIG['mail_smtppassword'] = getenv('SMTP_PASSWORD'); + } else { + $CONFIG['mail_smtppassword'] = ''; + } +} diff --git a/nextcloud-aio/Containers/nextcloud/config/swift.config.php b/nextcloud-aio/Containers/nextcloud/config/swift.config.php new file mode 100644 index 0000000..47ada56 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/config/swift.config.php @@ -0,0 +1,31 @@ + [ + 'class' => 'OC\\Files\\ObjectStore\\Swift', + 'arguments' => [ + 'autocreate' => $autocreate == true && strtolower($autocreate) !== 'false', + 'user' => [ + 'name' => getenv('OBJECTSTORE_SWIFT_USER_NAME'), + 'password' => getenv('OBJECTSTORE_SWIFT_USER_PASSWORD'), + 'domain' => [ + 'name' => (getenv('OBJECTSTORE_SWIFT_USER_DOMAIN')) ?: 'Default', + ], + ], + 'scope' => [ + 'project' => [ + 'name' => getenv('OBJECTSTORE_SWIFT_PROJECT_NAME'), + 'domain' => [ + 'name' => (getenv('OBJECTSTORE_SWIFT_PROJECT_DOMAIN')) ?: 'Default', + ], + ], + ], + 'serviceName' => (getenv('OBJECTSTORE_SWIFT_SERVICE_NAME')) ?: 'swift', + 'region' => getenv('OBJECTSTORE_SWIFT_REGION'), + 'url' => getenv('OBJECTSTORE_SWIFT_URL'), + 'bucket' => getenv('OBJECTSTORE_SWIFT_CONTAINER_NAME'), + ] + ] + ); +} diff --git a/nextcloud-aio/Containers/nextcloud/cron.sh b/nextcloud-aio/Containers/nextcloud/cron.sh new file mode 100644 index 0000000..0b88827 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/cron.sh @@ -0,0 +1,18 @@ +#!/bin/bash +wait_for_cron() { + set -x + while [ -n "$(pgrep -f /var/www/html/cron.php)" ]; do + echo "Waiting for cron to stop..." + sleep 5 + done + echo "Cronjob successfully exited." + exit +} + +trap wait_for_cron SIGINT SIGTERM + +while true; do + php -f /var/www/html/cron.php & + sleep 5m & + wait $! +done diff --git a/nextcloud-aio/Containers/nextcloud/entrypoint.sh b/nextcloud-aio/Containers/nextcloud/entrypoint.sh new file mode 100644 index 0000000..4c50648 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/entrypoint.sh @@ -0,0 +1,952 @@ +#!/bin/bash + +# version_greater A B returns whether A > B +version_greater() { + [ "$(printf '%s\n' "$@" | sort -t '.' -n -k1,1 -k2,2 -k3,3 -k4,4 | head -n 1)" != "$1" ] +} + +# return true if specified directory is empty +directory_empty() { + [ -z "$(ls -A "$1/")" ] +} + +run_upgrade_if_needed_due_to_app_update() { + if php /var/www/html/occ status | grep maintenance | grep -q true; then + php /var/www/html/occ maintenance:mode --off + fi + if php /var/www/html/occ status | grep needsDbUpgrade | grep -q true; then + php /var/www/html/occ upgrade + php /var/www/html/occ app:enable nextcloud-aio --force + fi +} + +# Adjust DATABASE_TYPE to by Nextcloud supported value +if [ "$DATABASE_TYPE" = postgres ]; then + export DATABASE_TYPE=pgsql +fi + +# Only start container if redis is accessible +# shellcheck disable=SC2153 +while ! nc -z "$REDIS_HOST" "6379"; do + echo "Waiting for redis to start..." + sleep 5 +done + +# Check permissions in ncdata +touch "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" +if ! [ -f "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" ]; then + echo "The www-data user doesn't seem to have access rights in the datadir. +Most likely are the files located on a drive that does not follow linux permissions. +Please adjust the permissions like mentioned below. +The found permissions are: +$(stat -c "%u:%g %a" "$NEXTCLOUD_DATA_DIR") +(userID:groupID permissions) +but they should be: +33:0 750 +(userID:groupID permissions) +Also make sure that the parent directories on the host of the directory that you've chosen as datadir are publicly readable with e.g. 'sudo chmod +r /mnt' (adjust the command accordingly to your case) and the same for all subdirectories. +Additionally, if you want to use a Fuse-mount as datadir, set 'allow_other' as additional mount option. +For SMB/CIFS mounts as datadir, see https://github.com/nextcloud/all-in-one#can-i-use-a-cifssmb-share-as-nextclouds-datadir" + exit 1 +fi +rm "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" + +if [ -f /var/www/html/version.php ]; then + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" +else + installed_version="0.0.0.0" +fi +if [ -f "$SOURCE_LOCATION/version.php" ]; then + # shellcheck disable=SC2016 + image_version="$(php -r "require '$SOURCE_LOCATION/version.php'; echo implode('.', \$OC_Version);")" +else + image_version="$installed_version" +fi + +# unset admin password +if [ "$installed_version" != "0.0.0.0" ]; then + unset ADMIN_PASSWORD +fi + +# Don't start the container if Nextcloud is not compatible with the PHP version +if [ -f "/var/www/html/lib/versioncheck.php" ] && ! php /var/www/html/lib/versioncheck.php; then + echo "It seems like your installed Nextcloud is not compatible with the by the container provided PHP version." + echo "This most likely happened because you tried to restore an old Nextcloud version from backup that is not compatible with the PHP version that comes with the container." + echo "Please try to restore a more recent backup which contains a Nextcloud version that is compatible with the PHP version that comes with the container." + echo "If you do not have a more recent backup, feel free to have a look at this documentation: https://github.com/nextcloud/all-in-one/blob/main/manual-upgrade.md" + exit 1 +fi + +# Do not start the container if the last update failed +if [ -f "$NEXTCLOUD_DATA_DIR/update.failed" ]; then + echo "The last Nextcloud update failed." + echo "Please restore from backup and try again!" + echo "If you do not have a backup in place, you can simply delete the update.failed file in the datadir which will allow the container to start again." + exit 1 +fi + +# Do not start the container if the install failed +if [ -f "$NEXTCLOUD_DATA_DIR/install.failed" ]; then + echo "The initial Nextcloud installation failed." + echo "Please reset AIO properly and try again. For further clues what went wrong, check the logs above." + echo "See https://github.com/nextcloud/all-in-one#how-to-properly-reset-the-instance" + exit 1 +fi + +# Skip any update if Nextcloud was just restored +if ! [ -f "$NEXTCLOUD_DATA_DIR/skip.update" ]; then + if version_greater "$image_version" "$installed_version"; then + # Check if it skips a major version + INSTALLED_MAJOR="${installed_version%%.*}" + IMAGE_MAJOR="${image_version%%.*}" + + if [ "$installed_version" != "0.0.0.0" ]; then + # Write output to logfile. + exec > >(tee -i "/var/www/html/data/update.log") + exec 2>&1 + fi + + if [ "$installed_version" != "0.0.0.0" ] && [ "$((IMAGE_MAJOR - INSTALLED_MAJOR))" -gt 1 ]; then +# Do not skip major versions placeholder # Do not remove or change this line! +# Do not skip major versions start # Do not remove or change this line! + set -ex + NEXT_MAJOR="$((INSTALLED_MAJOR + 1))" + curl -fsSL -o nextcloud.tar.bz2 "https://download.nextcloud.com/server/releases/latest-${NEXT_MAJOR}.tar.bz2" + curl -fsSL -o nextcloud.tar.bz2.asc "https://download.nextcloud.com/server/releases/latest-${NEXT_MAJOR}.tar.bz2.asc" + GNUPGHOME="$(mktemp -d)" + export GNUPGHOME + # gpg key from https://nextcloud.com/nextcloud.asc + gpg --batch --keyserver keyserver.ubuntu.com --recv-keys 28806A878AE423A28372792ED75899B9A724937A + gpg --batch --verify nextcloud.tar.bz2.asc nextcloud.tar.bz2 + mkdir -p /usr/src/tmp + tar -xjf nextcloud.tar.bz2 -C /usr/src/tmp/ + gpgconf --kill all + rm nextcloud.tar.bz2.asc nextcloud.tar.bz2 + mkdir -p /usr/src/tmp/nextcloud/data + mkdir -p /usr/src/tmp/nextcloud/custom_apps + chmod +x /usr/src/tmp/nextcloud/occ + cp -r "$SOURCE_LOCATION"/config/* /usr/src/tmp/nextcloud/config/ + mkdir -p /usr/src/tmp/nextcloud/apps/nextcloud-aio + cp -r "$SOURCE_LOCATION"/apps/nextcloud-aio/* /usr/src/tmp/nextcloud/apps/nextcloud-aio/ + mv "$SOURCE_LOCATION" /usr/src/temp-nextcloud + mv /usr/src/tmp/nextcloud "$SOURCE_LOCATION" + rm -r /usr/src/tmp + rm -r /usr/src/temp-nextcloud + # shellcheck disable=SC2016 + image_version="$(php -r "require '$SOURCE_LOCATION/version.php'; echo implode('.', \$OC_Version);")" + IMAGE_MAJOR="${image_version%%.*}" + set +ex +# Do not skip major versions end # Do not remove or change this line! + fi + + if [ "$installed_version" != "0.0.0.0" ]; then +# Check connection to appstore start # Do not remove or change this line! + while true; do + echo -e "Checking connection to appstore" + APPSTORE_URL="https://apps.nextcloud.com/api/v1" + if grep -q appstoreurl /var/www/html/config/config.php; then + set -x + APPSTORE_URL="$(grep appstoreurl /var/www/html/config/config.php | grep -oP 'https://.*v[0-9]+')" + set +x + fi + # Default appstoreurl parameter in config.php defaults to 'https://apps.nextcloud.com/api/v1' so we check for the apps.json file stored in there + CURL_STATUS="$(curl -LI "$APPSTORE_URL"/apps.json -o /dev/null -w '%{http_code}\n' -s)" + if [[ "$CURL_STATUS" = "200" ]] + then + echo "Appstore is reachable" + break + else + echo "Curl didn't produce a 200 status, is appstore reachable?" + sleep 5 + fi + done +# Check connection to appstore end # Do not remove or change this line! + + run_upgrade_if_needed_due_to_app_update + + php /var/www/html/occ maintenance:mode --off + + echo "Getting and backing up the status of apps for later, this might take a while..." + NC_APPS="$(find /var/www/html/custom_apps/ -type d -maxdepth 1 -mindepth 1 | sed 's|/var/www/html/custom_apps/||g')" + if [ -z "$NC_APPS" ]; then + echo "No apps detected, aborting export of app status..." + APPSTORAGE="no-export-done" + else + mapfile -t NC_APPS_ARRAY <<< "$NC_APPS" + declare -Ag APPSTORAGE + echo "Disabling apps before the update in order to make the update procedure more safe. This can take a while..." + for app in "${NC_APPS_ARRAY[@]}"; do + if APPSTORAGE[$app]="$(php /var/www/html/occ config:app:get "$app" enabled)"; then + php /var/www/html/occ app:disable "$app" + else + APPSTORAGE[$app]="" + echo "Not disabling $app because the occ command to get the enabled state was failing." + fi + done + fi + + if [ "$((IMAGE_MAJOR - INSTALLED_MAJOR))" -eq 1 ]; then + php /var/www/html/occ config:system:delete app_install_overwrite + fi + + php /var/www/html/occ app:update --all + + run_upgrade_if_needed_due_to_app_update + fi + + echo "Initializing nextcloud $image_version ..." + rsync -rlD --delete --exclude-from=/upgrade.exclude "$SOURCE_LOCATION/" /var/www/html/ + + # Copy custom_apps from Nextcloud archive + if ! directory_empty "$SOURCE_LOCATION/custom_apps"; then + set -x + for app in "$SOURCE_LOCATION/custom_apps"/*; do + app_id="$(basename "$app")" + mkdir -p "/var/www/html/custom_apps/$app_id" + rsync -rlD --delete --include "/$app_id/" --exclude '/*' "$SOURCE_LOCATION/custom_apps/" /var/www/html/custom_apps/ + done + set +x + fi + + # Copy over initial data from Nextcloud archive + for dir in config data custom_apps themes; do + if [ ! -d "/var/www/html/$dir" ] || directory_empty "/var/www/html/$dir"; then + rsync -rlD --include "/$dir/" --exclude '/*' "$SOURCE_LOCATION/" /var/www/html/ + fi + done + rsync -rlD --delete --include '/config/' --exclude '/*' --exclude '/config/CAN_INSTALL' --exclude '/config/config.sample.php' --exclude '/config/config.php' "$SOURCE_LOCATION/" /var/www/html/ + rsync -rlD --include '/version.php' --exclude '/*' "$SOURCE_LOCATION/" /var/www/html/ + echo "Initializing finished" + + #install + if [ "$installed_version" = "0.0.0.0" ]; then + echo "New Nextcloud instance." + + # Write output to logfile. + mkdir -p /var/www/html/data + exec > >(tee -i "/var/www/html/data/install.log") + exec 2>&1 + + INSTALL_OPTIONS=(-n --admin-user "$ADMIN_USER" --admin-pass "$ADMIN_PASSWORD") + if [ -n "${NEXTCLOUD_DATA_DIR}" ]; then + INSTALL_OPTIONS+=(--data-dir "$NEXTCLOUD_DATA_DIR") + fi + + # We do our own permission check so the permission check is not needed + cat << DATADIR_PERMISSION_CONF > /var/www/html/config/datadir.permission.config.php + false +); +DATADIR_PERMISSION_CONF + + # Write out postgres root cert + if [ -n "$NEXTCLOUD_TRUSTED_CERTIFICATES_POSTGRES" ]; then + mkdir /var/www/html/data/certificates + echo "$NEXTCLOUD_TRUSTED_CERTIFICATES_POSTGRES" > "/var/www/html/data/certificates/POSTGRES" + fi + + echo "Installing with $DATABASE_TYPE database" + # Set a default value for POSTGRES_PORT + if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 + fi + # shellcheck disable=SC2153 + INSTALL_OPTIONS+=(--database "$DATABASE_TYPE" --database-name "$POSTGRES_DB" --database-user "$POSTGRES_USER" --database-pass "$POSTGRES_PASSWORD" --database-host "$POSTGRES_HOST" --database-port "$POSTGRES_PORT") + + echo "Starting Nextcloud installation..." + if ! php /var/www/html/occ maintenance:install "${INSTALL_OPTIONS[@]}"; then + echo "Installation of Nextcloud failed!" + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi + + # Try to force generation of appdata dir: + php /var/www/html/occ maintenance:repair + + if [ -z "$OBJECTSTORE_S3_BUCKET" ] && [ -z "$OBJECTSTORE_SWIFT_URL" ]; then + max_retries=10 + try=0 + while [ -z "$(find "$NEXTCLOUD_DATA_DIR/" -maxdepth 1 -mindepth 1 -type d -name "appdata_*")" ] && [ "$try" -lt "$max_retries" ]; do + echo "Waiting for appdata to become available..." + try=$((try+1)) + sleep 10s + done + + if [ "$try" -ge "$max_retries" ]; then + echo "Installation of Nextcloud failed!" + echo "Install errors: $(cat /var/www/html/data/nextcloud.log)" + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi + fi + + # This autoconfig is not needed anymore and should be able to be overwritten by the user + rm /var/www/html/config/datadir.permission.config.php + + # unset admin password + unset ADMIN_PASSWORD + + # Enable the updatenotification app but disable its UI and server update notifications + php /var/www/html/occ config:system:set updatechecker --type=bool --value=false + php /var/www/html/occ config:app:set updatenotification notify_groups --value="[]" + +# AIO update to latest start # Do not remove or change this line! + if [ "$INSTALL_LATEST_MAJOR" = yes ]; then + php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater" + INSTALLED_AT="$(php /var/www/html/occ config:app:get core installedat)" + if [ -n "${INSTALLED_AT}" ]; then + # Set the installdat to 00 which will allow to skip staging and install the next major directly + # shellcheck disable=SC2001 + INSTALLED_AT="$(echo "${INSTALLED_AT}" | sed "s|[0-9][0-9]$|00|")" + php /var/www/html/occ config:app:set core installedat --value="${INSTALLED_AT}" + fi + php /var/www/html/updater/updater.phar --no-interaction --no-backup + if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then + echo "Installation of Nextcloud failed!" + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" + INSTALLED_MAJOR="${installed_version%%.*}" + IMAGE_MAJOR="${image_version%%.*}" + if ! [ "$INSTALLED_MAJOR" -gt "$IMAGE_MAJOR" ]; then + php /var/www/html/updater/updater.phar --no-interaction --no-backup + if ! php /var/www/html/occ -V || php /var/www/html/occ status | grep maintenance | grep -q 'true'; then + echo "Installation of Nextcloud failed!" + touch "$NEXTCLOUD_DATA_DIR/install.failed" + exit 1 + fi + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" + fi + php /var/www/html/occ config:system:set updatechecker --type=bool --value=true + php /var/www/html/occ app:enable nextcloud-aio --force + php /var/www/html/occ db:add-missing-columns + php /var/www/html/occ db:add-missing-primary-keys + yes | php /var/www/html/occ db:convert-filecache-bigint + fi +# AIO update to latest end # Do not remove or change this line! + + # Apply log settings + echo "Applying default settings..." + mkdir -p /var/www/html/data + php /var/www/html/occ config:system:set loglevel --value="2" --type=integer + php /var/www/html/occ config:system:set log_type --value="file" + php /var/www/html/occ config:system:set logfile --value="/var/www/html/data/nextcloud.log" + php /var/www/html/occ config:system:set log_rotate_size --value="10485760" --type=integer + php /var/www/html/occ app:enable admin_audit + php /var/www/html/occ config:app:set admin_audit logfile --value="/var/www/html/data/audit.log" + php /var/www/html/occ config:system:set log.condition apps 0 --value="admin_audit" + + # Apply preview settings + echo "Applying preview settings..." + php /var/www/html/occ config:system:set preview_max_x --value="2048" --type=integer + php /var/www/html/occ config:system:set preview_max_y --value="2048" --type=integer + php /var/www/html/occ config:system:set jpeg_quality --value="60" --type=integer + php /var/www/html/occ config:app:set preview jpeg_quality --value="60" + php /var/www/html/occ config:system:delete enabledPreviewProviders + php /var/www/html/occ config:system:set enabledPreviewProviders 1 --value="OC\\Preview\\Image" + php /var/www/html/occ config:system:set enabledPreviewProviders 2 --value="OC\\Preview\\MarkDown" + php /var/www/html/occ config:system:set enabledPreviewProviders 3 --value="OC\\Preview\\MP3" + php /var/www/html/occ config:system:set enabledPreviewProviders 4 --value="OC\\Preview\\TXT" + php /var/www/html/occ config:system:set enabledPreviewProviders 5 --value="OC\\Preview\\OpenDocument" + php /var/www/html/occ config:system:set enabledPreviewProviders 6 --value="OC\\Preview\\Movie" + php /var/www/html/occ config:system:set enabledPreviewProviders 7 --value="OC\\Preview\\Krita" + php /var/www/html/occ config:system:set enable_previews --value=true --type=boolean + + # Apply other settings + echo "Applying other settings..." + # Add missing indices after new installation because they seem to be missing on new installation + php /var/www/html/occ db:add-missing-indices + php /var/www/html/occ config:system:set upgrade.disable-web --type=bool --value=true + php /var/www/html/occ config:system:set mail_smtpmode --value="smtp" + php /var/www/html/occ config:system:set trashbin_retention_obligation --value="auto, 30" + php /var/www/html/occ config:system:set versions_retention_obligation --value="auto, 30" + php /var/www/html/occ config:system:set activity_expire_days --value="30" --type=integer + php /var/www/html/occ config:system:set simpleSignUpLink.shown --type=bool --value=false + php /var/www/html/occ config:system:set share_folder --value="/Shared" + + # Install some apps by default + if [ -n "$STARTUP_APPS" ]; then + read -ra STARTUP_APPS_ARRAY <<< "$STARTUP_APPS" + for app in "${STARTUP_APPS_ARRAY[@]}"; do + if ! echo "$app" | grep -q '^-'; then + if [ -z "$(find /var/www/html/apps /var/www/html/custom_apps -type d -maxdepth 1 -mindepth 1 -name "$app" )" ]; then + # If not shipped, install and enable the app + php /var/www/html/occ app:install "$app" + else + # If shipped, enable the app + php /var/www/html/occ app:enable "$app" + fi + else + app="${app#-}" + # Disable the app if '-' was provided in front of the appid + php /var/www/html/occ app:disable "$app" + fi + done + fi + + #upgrade + else + touch "$NEXTCLOUD_DATA_DIR/update.failed" + echo "Upgrading nextcloud from $installed_version to $image_version..." + php /var/www/html/occ config:system:delete integrity.check.disabled + if ! php /var/www/html/occ upgrade || ! php /var/www/html/occ -V; then + echo "Upgrade failed. Please restore from backup." + bash /notify.sh "Nextcloud update to $image_version failed!" "Please restore from backup!" + exit 1 + fi + + # shellcheck disable=SC2016 + installed_version="$(php -r 'require "/var/www/html/version.php"; echo implode(".", $OC_Version);')" + + rm "$NEXTCLOUD_DATA_DIR/update.failed" + bash /notify.sh "Nextcloud update to $image_version successful!" "Feel free to inspect the Nextcloud container logs for more info." + + php /var/www/html/occ app:update --all + + run_upgrade_if_needed_due_to_app_update + + # Restore app status + if [ "${APPSTORAGE[0]}" != "no-export-done" ]; then + echo "Restoring the status of apps. This can take a while..." + for app in "${!APPSTORAGE[@]}"; do + if [ -n "${APPSTORAGE[$app]}" ]; then + if [ "${APPSTORAGE[$app]}" != "no" ]; then + echo "Enabling $app..." + if ! php /var/www/html/occ app:enable "$app" >/dev/null; then + php /var/www/html/occ app:disable "$app" >/dev/null + if ! php /var/www/html/occ -V &>/dev/null; then + rm -r "/var/www/html/custom_apps/$app" + php /var/www/html/occ maintenance:mode --off + fi + run_upgrade_if_needed_due_to_app_update + echo "The $app app could not get enabled. Probably because it is not compatible with the new Nextcloud version." + if [ "$app" = apporder ]; then + CUSTOM_HINT="The apporder app was deprecated. A possible replacement is the side_menu app, aka 'Custom menu'." + else + CUSTOM_HINT="Most likely because it is not compatible with the new Nextcloud version." + fi + bash /notify.sh "Could not enable the $app app after the Nextcloud update!" "$CUSTOM_HINT Feel free to look at the Nextcloud update logs and force-enable the app again from the app-store UI." + continue + fi + # Only restore the group settings, if the app was enabled (and is thus compatible with the new NC version) + if [ "${APPSTORAGE[$app]}" != "yes" ]; then + php /var/www/html/occ config:app:set "$app" enabled --value="${APPSTORAGE[$app]}" + fi + fi + fi + done + fi + + php /var/www/html/occ app:update --all + + run_upgrade_if_needed_due_to_app_update + + # Enable the updatenotification app but disable its UI and server update notifications + php /var/www/html/occ config:system:set updatechecker --type=bool --value=false + php /var/www/html/occ app:enable updatenotification + php /var/www/html/occ config:app:set updatenotification notify_groups --value="[]" + + # Apply optimization + echo "Doing some optimizations..." + if [ "$NEXTCLOUD_SKIP_DATABASE_OPTIMIZATION" != yes ]; then + php /var/www/html/occ maintenance:repair --include-expensive + php /var/www/html/occ db:add-missing-indices + php /var/www/html/occ db:add-missing-columns + php /var/www/html/occ db:add-missing-primary-keys + yes | php /var/www/html/occ db:convert-filecache-bigint + else + php /var/www/html/occ maintenance:repair + fi + fi + fi + + # Performing update of all apps if daily backups are enabled, running and successful and if it is saturday + if [ "$UPDATE_NEXTCLOUD_APPS" = 'yes' ] && [ "$(date +%u)" = 6 ]; then + UPDATED_APPS="$(php /var/www/html/occ app:update --all)" + run_upgrade_if_needed_due_to_app_update + if [ -n "$UPDATED_APPS" ]; then + bash /notify.sh "Your apps just got updated!" "$UPDATED_APPS" + fi + fi +else + SKIP_UPDATE=1 +fi + +run_upgrade_if_needed_due_to_app_update + +if [ -z "$OBJECTSTORE_S3_BUCKET" ] && [ -z "$OBJECTSTORE_SWIFT_URL" ]; then + # Check if appdata is present + # If not, something broke (e.g. changing ncdatadir after aio was first started) + if [ -z "$(find "$NEXTCLOUD_DATA_DIR/" -maxdepth 1 -mindepth 1 -type d -name "appdata_*")" ]; then + echo "Appdata is not present. Did you maybe change the datadir after the initial Nextcloud installation? This is not supported!" + echo "See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir" + echo "If you adjusted the datadir to be located on an external drive, make sure that the drive is still mounted!" + echo "In the datadir was found:" + ls -la "$NEXTCLOUD_DATA_DIR/" + exit 1 + fi + + # Delete formerly configured tempdirectory as the default is usually faster (if the datadir is on a HDD or network FS) + if [ "$(php /var/www/html/occ config:system:get tempdirectory)" = "$NEXTCLOUD_DATA_DIR/tmp/" ]; then + php /var/www/html/occ config:system:delete tempdirectory + if [ -d "$NEXTCLOUD_DATA_DIR/tmp/" ]; then + rm -r "$NEXTCLOUD_DATA_DIR/tmp/" + fi + fi + +fi + +# Perform fingerprint update if instance was restored +if [ -f "$NEXTCLOUD_DATA_DIR/fingerprint.update" ]; then + php /var/www/html/occ maintenance:data-fingerprint + rm "$NEXTCLOUD_DATA_DIR/fingerprint.update" +fi + +# Perform preview scan if previews were excluded from restore +if [ -f "$NEXTCLOUD_DATA_DIR/trigger-preview.scan" ]; then + php /var/www/html/occ files:scan-app-data preview -vvv + rm "$NEXTCLOUD_DATA_DIR/trigger-preview.scan" +fi + +# AIO one-click settings start # Do not remove or change this line! +# Apply one-click-instance settings +echo "Applying one-click-instance settings..." +php /var/www/html/occ config:system:set one-click-instance --value=true --type=bool +php /var/www/html/occ config:system:set one-click-instance.user-limit --value=100 --type=int +php /var/www/html/occ config:system:set one-click-instance.link --value="https://nextcloud.com/all-in-one/" +# AIO one-click settings end # Do not remove or change this line! +php /var/www/html/occ app:enable support +if [ -n "$SUBSCRIPTION_KEY" ] && [ -z "$(php /var/www/html/occ config:app:get support potential_subscription_key)" ]; then + php /var/www/html/occ config:app:set support potential_subscription_key --value="$SUBSCRIPTION_KEY" + php /var/www/html/occ config:app:delete support last_check +fi +if [ -n "$NEXTCLOUD_DEFAULT_QUOTA" ]; then + if [ "$NEXTCLOUD_DEFAULT_QUOTA" = "unlimited" ]; then + php /var/www/html/occ config:app:delete files default_quota + else + php /var/www/html/occ config:app:set files default_quota --value="$NEXTCLOUD_DEFAULT_QUOTA" + fi +fi + +# Adjusting log files to be stored on a volume +echo "Adjusting log files..." +php /var/www/html/occ config:system:set upgrade.cli-upgrade-link --value="https://github.com/nextcloud/all-in-one/discussions/2726" +php /var/www/html/occ config:system:set logfile --value="/var/www/html/data/nextcloud.log" +php /var/www/html/occ config:app:set admin_audit logfile --value="/var/www/html/data/audit.log" +php /var/www/html/occ config:system:set updatedirectory --value="/nc-updater" +if [ -n "$NEXTCLOUD_SKELETON_DIRECTORY" ]; then + if [ "$NEXTCLOUD_SKELETON_DIRECTORY" = "empty" ]; then + php /var/www/html/occ config:system:set skeletondirectory --value="" + else + php /var/www/html/occ config:system:set skeletondirectory --value="$NEXTCLOUD_SKELETON_DIRECTORY" + fi +fi +if [ -n "$SERVERINFO_TOKEN" ] && [ -z "$(php /var/www/html/occ config:app:get serverinfo token)" ]; then + php /var/www/html/occ config:app:set serverinfo token --value="$SERVERINFO_TOKEN" +fi +# Set maintenance window so that no warning is shown in the admin overview +if [ -z "$NEXTCLOUD_MAINTENANCE_WINDOW" ]; then + NEXTCLOUD_MAINTENANCE_WINDOW=100 +fi +php /var/www/html/occ config:system:set maintenance_window_start --type=int --value="$NEXTCLOUD_MAINTENANCE_WINDOW" + +# Apply network settings +echo "Applying network settings..." +php /var/www/html/occ config:system:set allow_local_remote_servers --type=bool --value=true +php /var/www/html/occ config:system:set davstorage.request_timeout --value="$PHP_MAX_TIME" --type=int +php /var/www/html/occ config:system:set trusted_domains 1 --value="$NC_DOMAIN" +php /var/www/html/occ config:system:set overwrite.cli.url --value="https://$NC_DOMAIN/" +php /var/www/html/occ config:system:set documentation_url.server_logs --value="https://github.com/nextcloud/all-in-one/discussions/5425" +php /var/www/html/occ config:system:set htaccess.RewriteBase --value="/" +php /var/www/html/occ maintenance:update:htaccess + +# Revert dbpersistent setting to check if it fixes too many db connections +php /var/www/html/occ config:system:set dbpersistent --value=false --type=bool + +if [ "$DISABLE_BRUTEFORCE_PROTECTION" = yes ]; then + php /var/www/html/occ config:system:set auth.bruteforce.protection.enabled --type=bool --value=false + php /var/www/html/occ config:system:set ratelimit.protection.enabled --type=bool --value=false +else + php /var/www/html/occ config:system:set auth.bruteforce.protection.enabled --type=bool --value=true + php /var/www/html/occ config:system:set ratelimit.protection.enabled --type=bool --value=true +fi + +# Disallow creating local external storages when nothing was mounted +if [ -z "$NEXTCLOUD_MOUNT" ]; then + php /var/www/html/occ config:system:set files_external_allow_create_new_local --type=bool --value=false +else + php /var/www/html/occ config:system:set files_external_allow_create_new_local --type=bool --value=true +fi + +# AIO app start # Do not remove or change this line! +# AIO app +if [ "$THIS_IS_AIO" = "true" ]; then + if [ "$(php /var/www/html/occ config:app:get nextcloud-aio enabled)" != "yes" ]; then + php /var/www/html/occ app:enable nextcloud-aio + fi +else + if [ "$(php /var/www/html/occ config:app:get nextcloud-aio enabled)" != "no" ]; then + php /var/www/html/occ app:disable nextcloud-aio + fi +fi +# AIO app end # Do not remove or change this line! + +# Allow to add custom certs to Nextcloud's trusted cert store +if env | grep -q NEXTCLOUD_TRUSTED_CERTIFICATES_; then + set -x + TRUSTED_CERTIFICATES="$(env | grep NEXTCLOUD_TRUSTED_CERTIFICATES_ | grep -oP '^[A-Z_a-z0-9]+')" + mapfile -t TRUSTED_CERTIFICATES <<< "$TRUSTED_CERTIFICATES" + CERTIFICATES_ROOT_DIR="/var/www/html/data/certificates" + mkdir -p "$CERTIFICATES_ROOT_DIR" + for certificate in "${TRUSTED_CERTIFICATES[@]}"; do + # shellcheck disable=SC2001 + CERTIFICATE_NAME="$(echo "$certificate" | sed 's|^NEXTCLOUD_TRUSTED_CERTIFICATES_||')" + if ! [ -f "$CERTIFICATES_ROOT_DIR/$CERTIFICATE_NAME" ]; then + echo "${!certificate}" > "$CERTIFICATES_ROOT_DIR/$CERTIFICATE_NAME" + php /var/www/html/occ security:certificates:import "$CERTIFICATES_ROOT_DIR/$CERTIFICATE_NAME" + fi + done + set +x +fi + +# Notify push +if ! [ -d "/var/www/html/custom_apps/notify_push" ]; then + php /var/www/html/occ app:install notify_push +elif [ "$(php /var/www/html/occ config:app:get notify_push enabled)" != "yes" ]; then + php /var/www/html/occ app:enable notify_push +elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update notify_push +fi +chmod 775 -R /var/www/html/custom_apps/notify_push/bin/ +php /var/www/html/occ config:system:set trusted_proxies 0 --value="127.0.0.1" +php /var/www/html/occ config:system:set trusted_proxies 1 --value="::1" +if [ -n "$ADDITIONAL_TRUSTED_PROXY" ]; then + php /var/www/html/occ config:system:set trusted_proxies 2 --value="$ADDITIONAL_TRUSTED_PROXY" +fi + +# Get ipv4-address of Nextcloud +if [ -z "$NEXTCLOUD_HOST" ]; then + export NEXTCLOUD_HOST="nextcloud-aio-nextcloud" +fi +IPv4_ADDRESS="$(dig "$NEXTCLOUD_HOST" A +short +search | head -1)" +# Bring it in CIDR notation +# shellcheck disable=SC2001 +IPv4_ADDRESS="$(echo "$IPv4_ADDRESS" | sed 's|[0-9]\+$|0/16|')" +if [ -n "$IPv4_ADDRESS" ]; then + php /var/www/html/occ config:system:set trusted_proxies 10 --value="$IPv4_ADDRESS" +fi + +if [ -n "$ADDITIONAL_TRUSTED_DOMAIN" ]; then + php /var/www/html/occ config:system:set trusted_domains 2 --value="$ADDITIONAL_TRUSTED_DOMAIN" +fi +php /var/www/html/occ config:app:set notify_push base_endpoint --value="https://$NC_DOMAIN/push" + +# Collabora +if [ "$COLLABORA_ENABLED" = 'yes' ]; then + set -x + if echo "$COLLABORA_HOST" | grep -q "nextcloud-.*-collabora"; then + COLLABORA_HOST="$NC_DOMAIN" + fi + set +x + # Remove richdcoumentscode if it should be incorrectly installed + if [ -d "/var/www/html/custom_apps/richdocumentscode" ]; then + php /var/www/html/occ app:remove richdocumentscode + fi + if ! [ -d "/var/www/html/custom_apps/richdocuments" ]; then + php /var/www/html/occ app:install richdocuments + elif [ "$(php /var/www/html/occ config:app:get richdocuments enabled)" != "yes" ]; then + php /var/www/html/occ app:enable richdocuments + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update richdocuments + fi + php /var/www/html/occ config:app:set richdocuments wopi_url --value="https://$COLLABORA_HOST/" + # Make collabora more save + COLLABORA_IPv4_ADDRESS="$(dig "$COLLABORA_HOST" A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" + COLLABORA_IPv6_ADDRESS="$(dig "$COLLABORA_HOST" AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" + COLLABORA_ALLOW_LIST="$(php /var/www/html/occ config:app:get richdocuments wopi_allowlist)" + if [ -n "$COLLABORA_IPv4_ADDRESS" ]; then + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$COLLABORA_IPv4_ADDRESS"; then + if [ -z "$COLLABORA_ALLOW_LIST" ]; then + COLLABORA_ALLOW_LIST="$COLLABORA_IPv4_ADDRESS" + else + COLLABORA_ALLOW_LIST+=",$COLLABORA_IPv4_ADDRESS" + fi + fi + else + echo "Warning: No ipv4-address found for $COLLABORA_HOST." + fi + if [ -n "$COLLABORA_IPv6_ADDRESS" ]; then + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$COLLABORA_IPv6_ADDRESS"; then + if [ -z "$COLLABORA_ALLOW_LIST" ]; then + COLLABORA_ALLOW_LIST="$COLLABORA_IPv6_ADDRESS" + else + COLLABORA_ALLOW_LIST+=",$COLLABORA_IPv6_ADDRESS" + fi + fi + else + echo "No ipv6-address found for $COLLABORA_HOST." + fi + if [ -n "$COLLABORA_ALLOW_LIST" ]; then + PRIVATE_IP_RANGES='127.0.0.1/8,192.168.0.0/16,172.16.0.0/12,10.0.0.0/8,fd00::/8,::1' + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$PRIVATE_IP_RANGES"; then + COLLABORA_ALLOW_LIST+=",$PRIVATE_IP_RANGES" + fi + if [ -n "$ADDITIONAL_TRUSTED_PROXY" ]; then + if ! echo "$COLLABORA_ALLOW_LIST" | grep -q "$ADDITIONAL_TRUSTED_PROXY"; then + COLLABORA_ALLOW_LIST+=",$ADDITIONAL_TRUSTED_PROXY" + fi + fi + php /var/www/html/occ config:app:set richdocuments wopi_allowlist --value="$COLLABORA_ALLOW_LIST" + else + echo "Warning: wopi_allowlist is empty which should not be the case!" + fi +else + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/richdocuments" ]; then + php /var/www/html/occ app:remove richdocuments + fi +fi + +# OnlyOffice +if [ "$ONLYOFFICE_ENABLED" = 'yes' ]; then + if echo "$ONLYOFFICE_HOST" | grep -q "nextcloud-.*-onlyoffice"; then + ONLYOFFICE_PORT=80 + else + ONLYOFFICE_PORT=443 + fi + while ! nc -z "$ONLYOFFICE_HOST" "$ONLYOFFICE_PORT"; do + echo "waiting for OnlyOffice to become available..." + sleep 5 + done + if ! [ -d "/var/www/html/custom_apps/onlyoffice" ]; then + php /var/www/html/occ app:install onlyoffice + elif [ "$(php /var/www/html/occ config:app:get onlyoffice enabled)" != "yes" ]; then + php /var/www/html/occ app:enable onlyoffice + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update onlyoffice + fi + php /var/www/html/occ config:system:set onlyoffice jwt_secret --value="$ONLYOFFICE_SECRET" + php /var/www/html/occ config:app:set onlyoffice jwt_secret --value="$ONLYOFFICE_SECRET" + php /var/www/html/occ config:system:set onlyoffice jwt_header --value="AuthorizationJwt" + if echo "$ONLYOFFICE_HOST" | grep -q "nextcloud-.*-onlyoffice"; then + ONLYOFFICE_HOST="$NC_DOMAIN/onlyoffice" + export ONLYOFFICE_HOST + fi + php /var/www/html/occ config:app:set onlyoffice DocumentServerUrl --value="https://$ONLYOFFICE_HOST" +else + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/onlyoffice" ] && [ -n "$ONLYOFFICE_SECRET" ] && [ "$(php /var/www/html/occ config:system:get onlyoffice jwt_secret)" = "$ONLYOFFICE_SECRET" ]; then + php /var/www/html/occ app:remove onlyoffice + fi +fi + +# Talk +if [ "$TALK_ENABLED" = 'yes' ]; then + set -x + if [ -z "$TALK_HOST" ] || echo "$TALK_HOST" | grep -q "nextcloud-.*-talk"; then + TALK_HOST="$NC_DOMAIN" + HPB_PATH="/standalone-signaling/" + fi + if [ -z "$TURN_DOMAIN" ]; then + TURN_DOMAIN="$TALK_HOST" + fi + set +x + if ! [ -d "/var/www/html/custom_apps/spreed" ]; then + php /var/www/html/occ app:install spreed + elif [ "$(php /var/www/html/occ config:app:get spreed enabled)" != "yes" ]; then + php /var/www/html/occ app:enable spreed + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update spreed + fi + # Based on https://github.com/nextcloud/spreed/issues/960#issuecomment-416993435 + if [ -z "$(php /var/www/html/occ talk:turn:list --output="plain")" ]; then + # shellcheck disable=SC2153 + php /var/www/html/occ talk:turn:add turn "$TURN_DOMAIN:$TALK_PORT" "udp,tcp" --secret="$TURN_SECRET" + fi + STUN_SERVER="$(php /var/www/html/occ talk:stun:list --output="plain")" + if [ -z "$STUN_SERVER" ] || echo "$STUN_SERVER" | grep -oP '[a-zA-Z.:0-9]+' | grep -q "^stun.nextcloud.com:443$"; then + php /var/www/html/occ talk:stun:add "$TURN_DOMAIN:$TALK_PORT" + php /var/www/html/occ talk:stun:delete "stun.nextcloud.com:443" + fi + if ! php /var/www/html/occ talk:signaling:list --output="plain" | grep -q "https://$TALK_HOST$HPB_PATH"; then + php /var/www/html/occ talk:signaling:add "https://$TALK_HOST$HPB_PATH" "$SIGNALING_SECRET" --verify + fi +else + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/spreed" ]; then + php /var/www/html/occ app:remove spreed + fi +fi + +# Talk recording +if [ -d "/var/www/html/custom_apps/spreed" ]; then + if [ "$TALK_RECORDING_ENABLED" = 'yes' ]; then + while ! nc -z "$TALK_RECORDING_HOST" 1234; do + echo "waiting for Talk Recording to become available..." + sleep 5 + done + # TODO: migrate to occ command if that becomes available + RECORDING_SERVERS_STRING="{\"servers\":[{\"server\":\"http://$TALK_RECORDING_HOST:1234/\",\"verify\":true}],\"secret\":\"$RECORDING_SECRET\"}" + php /var/www/html/occ config:app:set spreed recording_servers --value="$RECORDING_SERVERS_STRING" + else + php /var/www/html/occ config:app:delete spreed recording_servers + fi +fi + +# Clamav +if [ "$CLAMAV_ENABLED" = 'yes' ]; then + count=0 + while ! nc -z "$CLAMAV_HOST" 3310 && [ "$count" -lt 90 ]; do + echo "waiting for clamav to become available..." + count=$((count+5)) + sleep 5 + done + if [ "$count" -ge 90 ]; then + echo "Clamav did not start in time. Skipping initialization and disabling files_antivirus app." + php /var/www/html/occ app:disable files_antivirus + else + if ! [ -d "/var/www/html/custom_apps/files_antivirus" ]; then + php /var/www/html/occ app:install files_antivirus + elif [ "$(php /var/www/html/occ config:app:get files_antivirus enabled)" != "yes" ]; then + php /var/www/html/occ app:enable files_antivirus + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update files_antivirus + fi + php /var/www/html/occ config:app:set files_antivirus av_mode --value="daemon" + php /var/www/html/occ config:app:set files_antivirus av_port --value="3310" + php /var/www/html/occ config:app:set files_antivirus av_host --value="$CLAMAV_HOST" + php /var/www/html/occ config:app:set files_antivirus av_stream_max_length --value="$CLAMAV_MAX_SIZE" + php /var/www/html/occ config:app:set files_antivirus av_max_file_size --value="$CLAMAV_MAX_SIZE" + php /var/www/html/occ config:app:set files_antivirus av_infected_action --value="only_log" + if [ -n "$CLAMAV_BLOCKLISTED_DIRECTORIES" ]; then + php /var/www/html/occ config:app:set files_antivirus av_blocklisted_directories --value="$CLAMAV_BLOCKLISTED_DIRECTORIES" + fi + fi +else + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/files_antivirus" ]; then + php /var/www/html/occ app:remove files_antivirus + fi +fi + +# Imaginary +if [ "$IMAGINARY_ENABLED" = 'yes' ]; then + php /var/www/html/occ config:system:set enabledPreviewProviders 0 --value="OC\\Preview\\Imaginary" + php /var/www/html/occ config:system:set enabledPreviewProviders 23 --value="OC\\Preview\\ImaginaryPDF" + php /var/www/html/occ config:system:set preview_imaginary_url --value="http://$IMAGINARY_HOST:9000" + php /var/www/html/occ config:system:set preview_imaginary_key --value="$IMAGINARY_SECRET" +else + if [ -n "$(php /var/www/html/occ config:system:get preview_imaginary_url)" ]; then + php /var/www/html/occ config:system:delete enabledPreviewProviders 0 + php /var/www/html/occ config:system:delete preview_imaginary_url + php /var/www/html/occ config:system:delete enabledPreviewProviders 20 + php /var/www/html/occ config:system:delete enabledPreviewProviders 21 + php /var/www/html/occ config:system:delete enabledPreviewProviders 22 + php /var/www/html/occ config:system:delete enabledPreviewProviders 23 + fi +fi + +# Fulltextsearch +if [ "$FULLTEXTSEARCH_ENABLED" = 'yes' ]; then + count=0 + while ! nc -z "$FULLTEXTSEARCH_HOST" "$FULLTEXTSEARCH_PORT" && [ "$count" -lt 90 ]; do + echo "waiting for Fulltextsearch to become available..." + count=$((count+5)) + sleep 5 + done + if [ "$count" -ge 90 ]; then + echo "Fulltextsearch did not start in time. Skipping initialization and disabling fulltextsearch apps." + php /var/www/html/occ app:disable fulltextsearch + php /var/www/html/occ app:disable fulltextsearch_elasticsearch + php /var/www/html/occ app:disable files_fulltextsearch + else + if ! [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then + php /var/www/html/occ app:install fulltextsearch + elif [ "$(php /var/www/html/occ config:app:get fulltextsearch enabled)" != "yes" ]; then + php /var/www/html/occ app:enable fulltextsearch + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update fulltextsearch + fi + if ! [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then + php /var/www/html/occ app:install fulltextsearch_elasticsearch + elif [ "$(php /var/www/html/occ config:app:get fulltextsearch_elasticsearch enabled)" != "yes" ]; then + php /var/www/html/occ app:enable fulltextsearch_elasticsearch + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update fulltextsearch_elasticsearch + fi + if ! [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then + php /var/www/html/occ app:install files_fulltextsearch + elif [ "$(php /var/www/html/occ config:app:get files_fulltextsearch enabled)" != "yes" ]; then + php /var/www/html/occ app:enable files_fulltextsearch + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update files_fulltextsearch + fi + php /var/www/html/occ fulltextsearch:configure '{"search_platform":"OCA\\FullTextSearch_Elasticsearch\\Platform\\ElasticSearchPlatform"}' + php /var/www/html/occ fulltextsearch_elasticsearch:configure "{\"elastic_host\":\"http://$FULLTEXTSEARCH_USER:$FULLTEXTSEARCH_PASSWORD@$FULLTEXTSEARCH_HOST:$FULLTEXTSEARCH_PORT\",\"elastic_index\":\"$FULLTEXTSEARCH_INDEX\"}" + php /var/www/html/occ files_fulltextsearch:configure "{\"files_pdf\":\"1\",\"files_office\":\"1\"}" + + # Do the index + if ! [ -f "$NEXTCLOUD_DATA_DIR/fts-index.done" ]; then + echo "Waiting 10s before activating FTS..." + sleep 10 + echo "Activating fulltextsearch..." + if php /var/www/html/occ fulltextsearch:test && php /var/www/html/occ fulltextsearch:index "{\"errors\": \"reset\"}" --no-readline; then + touch "$NEXTCLOUD_DATA_DIR/fts-index.done" + else + echo "Fulltextsearch failed. Could not index." + echo "Feel free to follow https://github.com/nextcloud/all-in-one/discussions/1709 if you want to skip the indexing in the future." + fi + fi + fi +else + if [ "$REMOVE_DISABLED_APPS" = yes ]; then + if [ -d "/var/www/html/custom_apps/fulltextsearch" ]; then + php /var/www/html/occ app:remove fulltextsearch + fi + if [ -d "/var/www/html/custom_apps/fulltextsearch_elasticsearch" ]; then + php /var/www/html/occ app:remove fulltextsearch_elasticsearch + fi + if [ -d "/var/www/html/custom_apps/files_fulltextsearch" ]; then + php /var/www/html/occ app:remove files_fulltextsearch + fi + fi +fi + +# Docker socket proxy +# app_api is a shipped app +if [ -d "/var/www/html/custom_apps/app_api" ]; then + php /var/www/html/occ app:disable app_api + rm -r "/var/www/html/custom_apps/app_api" +fi +if [ "$DOCKER_SOCKET_PROXY_ENABLED" = 'yes' ]; then + if [ "$(php /var/www/html/occ config:app:get app_api enabled)" != "yes" ]; then + php /var/www/html/occ app:enable app_api + fi +else + if [ "$REMOVE_DISABLED_APPS" = yes ]; then + if [ "$(php /var/www/html/occ config:app:get app_api enabled)" != "no" ]; then + php /var/www/html/occ app:disable app_api + fi + fi +fi + +# Whiteboard app +if [ "$WHITEBOARD_ENABLED" = 'yes' ]; then + if ! [ -d "/var/www/html/custom_apps/whiteboard" ]; then + php /var/www/html/occ app:install whiteboard + elif [ "$(php /var/www/html/occ config:app:get whiteboard enabled)" != "yes" ]; then + php /var/www/html/occ app:enable whiteboard + elif [ "$SKIP_UPDATE" != 1 ]; then + php /var/www/html/occ app:update whiteboard + fi + php /var/www/html/occ config:app:set whiteboard collabBackendUrl --value="https://$NC_DOMAIN/whiteboard" + php /var/www/html/occ config:app:set whiteboard jwt_secret_key --value="$WHITEBOARD_SECRET" +else + if [ "$REMOVE_DISABLED_APPS" = yes ] && [ -d "/var/www/html/custom_apps/whiteboard" ]; then + php /var/www/html/occ app:remove whiteboard + fi +fi + +# Remove the update skip file always +rm -f "$NEXTCLOUD_DATA_DIR"/skip.update diff --git a/nextcloud-aio/Containers/nextcloud/healthcheck.sh b/nextcloud-aio/Containers/nextcloud/healthcheck.sh new file mode 100644 index 0000000..54c79dc --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/healthcheck.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +# Set a default value for POSTGRES_PORT +if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 +fi + + +# POSTGRES_HOST must be set in the containers env vars and POSTGRES_PORT has a default above +# shellcheck disable=SC2153 +nc -z "$POSTGRES_HOST" "$POSTGRES_PORT" || exit 0 + +if ! nc -z 127.0.0.1 9000; then + exit 1 +fi diff --git a/nextcloud-aio/Containers/nextcloud/notify-all.sh b/nextcloud-aio/Containers/nextcloud/notify-all.sh new file mode 100644 index 0000000..f4dfa0f --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/notify-all.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +if [[ "$EUID" = 0 ]]; then + COMMAND=(sudo -E -u www-data php /var/www/html/occ) +else + COMMAND=(php /var/www/html/occ) +fi + +SUBJECT="$1" +MESSAGE="$2" + +if [ "$("${COMMAND[@]}" config:app:get notifications enabled)" = "no" ]; then + echo "Cannot send notification as notification app is not enabled." + exit 1 +fi + +echo "Posting notifications to all users..." +NC_USERS=$("${COMMAND[@]}" user:list | sed 's|^ - ||g' | sed 's|:.*||') +mapfile -t NC_USERS <<< "$NC_USERS" +for user in "${NC_USERS[@]}" +do + echo "Posting '$SUBJECT' to: $user" + "${COMMAND[@]}" notification:generate "$user" "$NC_DOMAIN: $SUBJECT" -l "$MESSAGE" --object-type='update' --object-id="$SUBJECT" +done + +echo "Done!" +exit 0 \ No newline at end of file diff --git a/nextcloud-aio/Containers/nextcloud/notify.sh b/nextcloud-aio/Containers/nextcloud/notify.sh new file mode 100644 index 0000000..2ac4cea --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/notify.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +if [[ "$EUID" = 0 ]]; then + COMMAND=(sudo -E -u www-data php /var/www/html/occ) +else + COMMAND=(php /var/www/html/occ) +fi + +SUBJECT="$1" +MESSAGE="$2" + +if [ "$("${COMMAND[@]}" config:app:get notifications enabled)" = "no" ]; then + echo "Cannot send notification as notification app is not enabled." + exit 1 +fi + +echo "Posting notifications to users that are admins..." +NC_USERS=$("${COMMAND[@]}" user:list | sed 's|^ - ||g' | sed 's|:.*||') +mapfile -t NC_USERS <<< "$NC_USERS" +for user in "${NC_USERS[@]}" +do + if "${COMMAND[@]}" user:info "$user" | cut -d "-" -f2 | grep -x -q " admin" + then + NC_ADMIN_USER+=("$user") + fi +done + +for admin in "${NC_ADMIN_USER[@]}" +do + echo "Posting '$SUBJECT' to: $admin" + "${COMMAND[@]}" notification:generate "$admin" "$NC_DOMAIN: $SUBJECT" -l "$MESSAGE" --object-type='update' --object-id="$SUBJECT" +done + +echo "Done!" +exit 0 diff --git a/nextcloud-aio/Containers/nextcloud/root.motd b/nextcloud-aio/Containers/nextcloud/root.motd new file mode 100644 index 0000000..00cb480 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/root.motd @@ -0,0 +1,4 @@ +Warning: You have logged in into the Nextcloud container as root user. +See https://github.com/nextcloud/all-in-one#how-to-run-occ-commands if you want to run occ commands. +Apart from that, you can use 'sudo -E -u www-data php occ ' in order to run occ commands. +Of course needs to be substituted with the command that you want to use. diff --git a/nextcloud-aio/Containers/nextcloud/run-exec-commands.sh b/nextcloud-aio/Containers/nextcloud/run-exec-commands.sh new file mode 100644 index 0000000..9ef6ba6 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/run-exec-commands.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Wait until the apache container is ready +while ! nc -z "$APACHE_HOST" "$APACHE_PORT"; do + echo "Waiting for $APACHE_HOST to become available..." + sleep 15 +done + +if [ -n "$NEXTCLOUD_EXEC_COMMANDS" ]; then + echo "#!/bin/bash" > /tmp/nextcloud-exec-commands + echo "$NEXTCLOUD_EXEC_COMMANDS" >> /tmp/nextcloud-exec-commands + if ! grep "one-click-instance" /tmp/nextcloud-exec-commands; then + bash /tmp/nextcloud-exec-commands + rm /tmp/nextcloud-exec-commands + fi +else + # Collabora must work also if using manual-install + if [ "$COLLABORA_ENABLED" = yes ]; then + echo "Activating Collabora config..." + php /var/www/html/occ richdocuments:activate-config + fi + # OnlyOffice must work also if using manual-install + if [ "$ONLYOFFICE_ENABLED" = yes ]; then + echo "Activating OnlyOffice config..." + php /var/www/html/occ onlyoffice:documentserver --check + fi +fi + +signal_handler() { + exit 0 +} + +trap signal_handler SIGINT SIGTERM + +sleep inf & +wait $! diff --git a/nextcloud-aio/Containers/nextcloud/start.sh b/nextcloud-aio/Containers/nextcloud/start.sh new file mode 100644 index 0000000..37aa4d9 --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/start.sh @@ -0,0 +1,173 @@ +#!/bin/bash + +# Set a default value for POSTGRES_PORT +if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 +fi + +# Only start container if database is accessible +# POSTGRES_HOST must be set in the containers env vars and POSTGRES_PORT has a default above +# shellcheck disable=SC2153 +while ! sudo -u www-data nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do + echo "Waiting for database to start..." + sleep 5 +done + +# Use the correct Postgres username +POSTGRES_USER="oc_$POSTGRES_USER" +export POSTGRES_USER + +# Check that db type is not empty +if [ -z "$DATABASE_TYPE" ]; then + export DATABASE_TYPE=postgres +fi + +# Fix false database connection on old instances +if [ -f "/var/www/html/config/config.php" ]; then + sleep 2 + while ! sudo -u www-data psql -d "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB" -c "select now()"; do + echo "Waiting for the database to start..." + sleep 5 + done + if [ "$POSTGRES_USER" = "oc_nextcloud" ] && [ "$POSTGRES_DB" = "nextcloud_database" ] && echo "$POSTGRES_PASSWORD" | grep -q '^[a-z0-9]\+$'; then + # This was introduced with https://github.com/nextcloud/all-in-one/pull/218 + sed -i "s|'dbuser'.*=>.*$|'dbuser' => '$POSTGRES_USER',|" /var/www/html/config/config.php + sed -i "s|'dbpassword'.*=>.*$|'dbpassword' => '$POSTGRES_PASSWORD',|" /var/www/html/config/config.php + sed -i "s|'db_name'.*=>.*$|'db_name' => '$POSTGRES_DB',|" /var/www/html/config/config.php + fi +fi + +# Trust additional Cacerts, if the user provided $TRUSTED_CACERTS_DIR +if [ -n "$TRUSTED_CACERTS_DIR" ]; then + echo "User required to trust additional CA certificates, running 'update-ca-certificates.'" + update-ca-certificates +fi + +# Check if /dev/dri device is present and apply correct permissions +set -x +if ! [ -f "/dev-dri-group-was-added" ] && [ -n "$(find /dev -maxdepth 1 -mindepth 1 -name dri)" ] && [ -n "$(find /dev/dri -maxdepth 1 -mindepth 1 -name renderD128)" ]; then + # From https://memories.gallery/hw-transcoding/#docker-installations + GID="$(stat -c "%g" /dev/dri/renderD128)" + groupadd -g "$GID" render2 || true # sometimes this is needed + GROUP="$(getent group "$GID" | cut -d: -f1)" + usermod -aG "$GROUP" www-data + touch "/dev-dri-group-was-added" +fi +set +x + +# Check datadir permissions +sudo -u www-data touch "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" &>/dev/null +if ! [ -f "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" ]; then + chown -R www-data:root "$NEXTCLOUD_DATA_DIR" + chmod 750 -R "$NEXTCLOUD_DATA_DIR" +fi +sudo -u www-data rm -f "$NEXTCLOUD_DATA_DIR/this-is-a-test-file" + +# Install additional dependencies +if [ -n "$ADDITIONAL_APKS" ]; then + if ! [ -f "/additional-apks-are-installed" ]; then + # Allow to disable imagemagick without having to download it each time + if ! echo "$ADDITIONAL_APKS" | grep -q imagemagick; then + apk del imagemagick imagemagick-svg imagemagick-heic imagemagick-tiff; + fi + read -ra ADDITIONAL_APKS_ARRAY <<< "$ADDITIONAL_APKS" + for app in "${ADDITIONAL_APKS_ARRAY[@]}"; do + if [ "$app" != imagemagick ]; then + echo "Installing $app via apk..." + if ! apk add --no-cache "$app" >/dev/null; then + echo "The packet $app was not installed!" + fi + fi + done + fi + touch /additional-apks-are-installed +fi + +# Install additional php extensions +if [ -n "$ADDITIONAL_PHP_EXTENSIONS" ]; then + if ! [ -f "/additional-php-extensions-are-installed" ]; then + read -ra ADDITIONAL_PHP_EXTENSIONS_ARRAY <<< "$ADDITIONAL_PHP_EXTENSIONS" + for app in "${ADDITIONAL_PHP_EXTENSIONS_ARRAY[@]}"; do + if [ "$app" = imagick ]; then + echo "Enabling Imagick..." + if ! docker-php-ext-enable imagick >/dev/null; then + echo "Could not install PHP extension imagick!" + fi + continue + fi + # shellcheck disable=SC2086 + if [ "$PHP_DEPS_ARE_INSTALLED" != 1 ]; then + echo "Installing PHP build dependencies..." + if ! apk add --no-cache --virtual .build-deps \ + libxml2-dev \ + autoconf \ + $PHPIZE_DEPS >/dev/null; then + echo "Could not install build-deps!" + fi + PHP_DEPS_ARE_INSTALLED=1 + fi + if [ "$app" = inotify ]; then + echo "Installing $app via PECL..." + pecl install "$app" >/dev/null + if ! docker-php-ext-enable "$app" >/dev/null; then + echo "Could not install PHP extension $app!" + fi + elif [ "$app" = soap ]; then + echo "Installing $app from core..." + if ! docker-php-ext-install -j "$(nproc)" "$app" >/dev/null; then + echo "Could not install PHP extension $app!" + fi + else + echo "Installing PHP extension $app ..." + if ! docker-php-ext-install -j "$(nproc)" "$app" >/dev/null; then + echo "Could not install $app from core. Trying to install from PECL..." + pecl install "$app" >/dev/null + if ! docker-php-ext-enable "$app" >/dev/null; then + echo "Could also not install $app from PECL. The PHP extensions was not installed!" + fi + fi + fi + done + if [ "$PHP_DEPS_ARE_INSTALLED" = 1 ]; then + rm -rf /tmp/pear + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )"; + # shellcheck disable=SC2086 + apk add --no-cache --virtual .nextcloud-phpext-rundeps $runDeps >/dev/null + apk del .build-deps >/dev/null + fi + fi + touch /additional-php-extensions-are-installed +fi + +# Run original entrypoint +if ! sudo -E -u www-data bash /entrypoint.sh; then + exit 1 +fi + +while [ "$THIS_IS_AIO" = "true" ] && [ -z "$(dig nextcloud-aio-apache A +short +search)" ]; do + echo "Waiting for nextcloud-aio-apache to start..." + sleep 5 +done + +set -x +# shellcheck disable=SC2235 +if [ "$THIS_IS_AIO" = "true" ] && [ "$APACHE_PORT" = 443 ]; then + IPv4_ADDRESS_APACHE="$(dig nextcloud-aio-apache A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" + IPv6_ADDRESS_APACHE="$(dig nextcloud-aio-apache AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" + IPv4_ADDRESS_MASTERCONTAINER="$(dig nextcloud-aio-mastercontainer A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" + IPv6_ADDRESS_MASTERCONTAINER="$(dig nextcloud-aio-mastercontainer AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" + + sed -i "s|^;listen.allowed_clients|listen.allowed_clients|" /usr/local/etc/php-fpm.d/www.conf + sed -i "s|listen.allowed_clients.*|listen.allowed_clients = 127.0.0.1,::1,$IPv4_ADDRESS_APACHE,$IPv6_ADDRESS_APACHE,$IPv4_ADDRESS_MASTERCONTAINER,$IPv6_ADDRESS_MASTERCONTAINER|" /usr/local/etc/php-fpm.d/www.conf + sed -i "/^listen.allowed_clients/s/,,/,/g" /usr/local/etc/php-fpm.d/www.conf + sed -i "/^listen.allowed_clients/s/,$//" /usr/local/etc/php-fpm.d/www.conf + grep listen.allowed_clients /usr/local/etc/php-fpm.d/www.conf +fi +set +x + +exec "$@" diff --git a/nextcloud-aio/Containers/nextcloud/supervisord.conf b/nextcloud-aio/Containers/nextcloud/supervisord.conf new file mode 100644 index 0000000..1db885e --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/supervisord.conf @@ -0,0 +1,45 @@ +# From https://github.com/nextcloud/docker/blob/master/.examples/dockerfiles/full/fpm/supervisord.conf +[supervisord] +nodaemon=true +logfile=/var/log/supervisord/supervisord.log +pidfile=/var/run/supervisord/supervisord.pid +childlogdir=/var/log/supervisord/ +logfile_maxbytes=50MB ; maximum size of logfile before rotation +logfile_backups=10 ; number of backed up logfiles +loglevel=error +user=root + +[program:php-fpm] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=php-fpm +user=root + +[program:cron] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/cron.sh +user=www-data + +[program:run-exec-commands] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=/run-exec-commands.sh +user=www-data + +# This is a hack but no better solution is there +[program:is-nextcloud-online] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +# Restart the netcat command once a day to ensure that it stays reachable +# See https://github.com/nextcloud/all-in-one/issues/6334 +command=timeout 86400 nc -lk 9001 +user=www-data diff --git a/nextcloud-aio/Containers/nextcloud/upgrade.exclude b/nextcloud-aio/Containers/nextcloud/upgrade.exclude new file mode 100644 index 0000000..354864d --- /dev/null +++ b/nextcloud-aio/Containers/nextcloud/upgrade.exclude @@ -0,0 +1,5 @@ +/config/ +/data/ +/custom_apps/ +/themes/ +/version.php diff --git a/nextcloud-aio/Containers/notify-push/Dockerfile b/nextcloud-aio/Containers/notify-push/Dockerfile new file mode 100644 index 0000000..8138582 --- /dev/null +++ b/nextcloud-aio/Containers/notify-push/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:latest +FROM alpine:3.22.1 + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + netcat-openbsd \ + tzdata \ + bash \ + openssl; \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + apk del --no-cache \ + openssl; + +USER 33 +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/notify-push/healthcheck.sh b/nextcloud-aio/Containers/notify-push/healthcheck.sh new file mode 100644 index 0000000..e253943 --- /dev/null +++ b/nextcloud-aio/Containers/notify-push/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +if ! nc -z "$NEXTCLOUD_HOST" 9001; then + exit 0 +fi + +nc -z 127.0.0.1 7867 || exit 1 diff --git a/nextcloud-aio/Containers/notify-push/start.sh b/nextcloud-aio/Containers/notify-push/start.sh new file mode 100644 index 0000000..859c630 --- /dev/null +++ b/nextcloud-aio/Containers/notify-push/start.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +if [ -z "$NEXTCLOUD_HOST" ]; then + echo "NEXTCLOUD_HOST needs to be provided. Exiting!" + exit 1 +elif [ -z "$POSTGRES_HOST" ]; then + echo "POSTGRES_HOST needs to be provided. Exiting!" + exit 1 +elif [ -z "$REDIS_HOST" ]; then + echo "REDIS_HOST needs to be provided. Exiting!" + exit 1 +fi + +# Only start container if nextcloud is accessible +while ! nc -z "$NEXTCLOUD_HOST" 9001; do + echo "Waiting for Nextcloud to start..." + sleep 5 +done + +# Correctly set CPU_ARCH for notify_push +CPU_ARCH="$(uname -m)" +export CPU_ARCH +if [ -z "$CPU_ARCH" ]; then + echo "Could not get processor architecture. Exiting." + exit 1 +elif [ "$CPU_ARCH" != "x86_64" ]; then + export CPU_ARCH="aarch64" +fi + +# Add warning +if ! [ -f /nextcloud/custom_apps/notify_push/bin/"$CPU_ARCH"/notify_push ]; then + echo "The notify_push binary was not found." + echo "Most likely is DNS resolution not working correctly." + echo "You can try to fix this by configuring a DNS server globally in dockers daemon.json." + echo "See https://dockerlabs.collabnix.com/intermediate/networking/Configuring_DNS.html" + echo "Afterwards a restart of docker should automatically resolve this." + echo "Additionally, make sure to disable VPN software that might be running on your server" + echo "Also check your firewall if it blocks connections to github" + echo "If it should still not work afterwards, feel free to create a new thread at https://github.com/nextcloud/all-in-one/discussions/new?category=questions and post the Nextcloud container logs there." + echo "" + echo "" + exit 1 +fi + +echo "notify-push was started" + +# Set a default value for POSTGRES_PORT +if [ -z "$POSTGRES_PORT" ]; then + POSTGRES_PORT=5432 +fi +# Set a default for redis db index +if [ -z "$REDIS_DB_INDEX" ]; then + REDIS_DB_INDEX=0 +fi +# Set a default for db type +if [ -z "$DATABASE_TYPE" ]; then + DATABASE_TYPE=postgres +elif [ "$DATABASE_TYPE" != postgres ] && [ "$DATABASE_TYPE" != mysql ]; then + echo "DB type must be either postgres or mysql" + exit 1 +fi + +# Use the correct Postgres username +if [ "$POSTGRES_USER" = nextcloud ]; then + POSTGRES_USER="oc_$POSTGRES_USER" + export POSTGRES_USER +fi + +# Postgres root cert +if [ -f "/nextcloud/data/certificates/POSTGRES" ]; then + POSTGRES_CERT="?sslmode=verify-ca&sslrootcert=/nextcloud/data/certificates/POSTGRES" +fi + +# Set sensitive values as env +export DATABASE_URL="$DATABASE_TYPE://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB$POSTGRES_CERT" +export REDIS_URL="redis://$REDIS_USER:$REDIS_HOST_PASSWORD@$REDIS_HOST/$REDIS_DB_INDEX" + +# Run it +/nextcloud/custom_apps/notify_push/bin/"$CPU_ARCH"/notify_push \ + --database-prefix="oc_" \ + --nextcloud-url "https://$NC_DOMAIN" \ + --port 7867 + +exec "$@" diff --git a/nextcloud-aio/Containers/onlyoffice/Dockerfile b/nextcloud-aio/Containers/onlyoffice/Dockerfile new file mode 100644 index 0000000..42c72e5 --- /dev/null +++ b/nextcloud-aio/Containers/onlyoffice/Dockerfile @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:latest +# From https://github.com/ONLYOFFICE/Docker-DocumentServer/blob/master/Dockerfile +FROM onlyoffice/documentserver:9.0.4.1 + +# USER root is probably used + +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +HEALTHCHECK --start-period=60s --retries=9 CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/onlyoffice/healthcheck.sh b/nextcloud-aio/Containers/onlyoffice/healthcheck.sh new file mode 100644 index 0000000..7a9d79d --- /dev/null +++ b/nextcloud-aio/Containers/onlyoffice/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 80 || exit 1 diff --git a/nextcloud-aio/Containers/postgresql/Dockerfile b/nextcloud-aio/Containers/postgresql/Dockerfile new file mode 100644 index 0000000..2533a5d --- /dev/null +++ b/nextcloud-aio/Containers/postgresql/Dockerfile @@ -0,0 +1,47 @@ +# syntax=docker/dockerfile:latest +# From https://github.com/docker-library/postgres/blob/master/17/alpine3.22/Dockerfile +FROM postgres:17.6-alpine + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh +COPY --chmod=775 init-user-db.sh /docker-entrypoint-initdb.d/init-user-db.sh + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + bash \ + openssl \ + shadow \ + grep; \ + \ +# We need to use the same gid and uid as on old installations + deluser postgres; \ + groupmod -g 9999 ping; \ + addgroup -g 999 -S postgres; \ + adduser -u 999 -S -D -G postgres -H -h /var/lib/postgresql -s /bin/sh postgres; \ + apk del --no-cache shadow; \ + \ +# Fix default permissions + chown -R postgres:postgres /var/lib/postgresql; \ + chown -R postgres:postgres /var/run/postgresql; \ + chmod -R 777 /var/run/postgresql; \ + chown -R postgres:postgres "$PGDATA"; \ + \ + mkdir /mnt/data; \ + chown postgres:postgres /mnt/data; \ + \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + apk --no-cache del openssl; \ + \ +# Get rid of unused binaries + rm -f /usr/local/bin/gosu /usr/local/bin/su-exec; + +VOLUME /mnt/data + +USER 999 +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/postgresql/healthcheck.sh b/nextcloud-aio/Containers/postgresql/healthcheck.sh new file mode 100644 index 0000000..9f303a3 --- /dev/null +++ b/nextcloud-aio/Containers/postgresql/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +test -f "/mnt/data/backup-is-running" && exit 0 + +psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()" && exit 0 + +psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:5432/$POSTGRES_DB" -c "select now()" || exit 1 diff --git a/nextcloud-aio/Containers/postgresql/init-user-db.sh b/nextcloud-aio/Containers/postgresql/init-user-db.sh new file mode 100644 index 0000000..cfd3827 --- /dev/null +++ b/nextcloud-aio/Containers/postgresql/init-user-db.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -ex + +touch "$DUMP_DIR/initialization.failed" + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER "oc_$POSTGRES_USER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB; + ALTER DATABASE "$POSTGRES_DB" OWNER TO "oc_$POSTGRES_USER"; + GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "oc_$POSTGRES_USER"; + GRANT ALL PRIVILEGES ON SCHEMA public TO "oc_$POSTGRES_USER"; +EOSQL + +rm "$DUMP_DIR/initialization.failed" + +set +ex diff --git a/nextcloud-aio/Containers/postgresql/start.sh b/nextcloud-aio/Containers/postgresql/start.sh new file mode 100644 index 0000000..551bb10 --- /dev/null +++ b/nextcloud-aio/Containers/postgresql/start.sh @@ -0,0 +1,198 @@ +#!/bin/bash + +# Variables +DATADIR="/var/lib/postgresql/data" +export DUMP_DIR="/mnt/data" +DUMP_FILE="$DUMP_DIR/database-dump.sql" +export PGPASSWORD="$POSTGRES_PASSWORD" + +# Don't start database as long as backup is running +while [ -f "$DUMP_DIR/backup-is-running" ]; do + echo "Waiting for backup container to finish..." + echo "If this is incorrect because the backup container is not running anymore (because it was forcefully killed), you might delete the lock file:" + echo "sudo docker exec --user root nextcloud-aio-database rm /mnt/data/backup-is-running" + sleep 10 +done + +# Check if dump dir is writeable +if ! [ -w "$DUMP_DIR" ]; then + echo "DUMP dir is not writeable by postgres user." + exit 1 +fi + +# Don't start if import failed +if [ -f "$DUMP_DIR/import.failed" ]; then + echo "The database import failed. Please restore a backup and try again." + echo "For further clues on what went wrong, look at the logs above." + exit 1 +fi + +# Don't start if initialization failed +if [ -f "$DUMP_DIR/initialization.failed" ]; then + echo "The database initialization failed. Most likely was a wrong timezone selected." + echo "The selected timezone is '$TZ'." + echo "Please check if it is in the 'TZ identifier' column of the timezone list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List" + echo "For further clues on what went wrong, look at the logs above." + echo "You might start again from scratch by following https://github.com/nextcloud/all-in-one#how-to-properly-reset-the-instance and selecting a proper timezone." + exit 1 +fi + +# Delete the datadir once (needed for setting the correct credentials on old instances once) +if ! [ -f "$DUMP_DIR/export.failed" ] && ! [ -f "$DUMP_DIR/initial-cleanup-done" ]; then + set -ex + rm -rf "${DATADIR:?}/"* + touch "$DUMP_DIR/initial-cleanup-done" + set +ex +fi + +# Test if some things match +# shellcheck disable=SC2235 +if ( [ -f "$DATADIR/PG_VERSION" ] && [ "$PG_MAJOR" != "$(cat "$DATADIR/PG_VERSION")" ] ) \ +|| ( ! [ -f "$DATADIR/PG_VERSION" ] && ( [ -f "$DUMP_FILE" ] || [ -f "$DUMP_DIR/export.failed" ] ) ); then + # The DUMP_file must be provided + if ! [ -f "$DUMP_FILE" ]; then + echo "Unable to restore the database because the database dump is missing." + exit 1 + fi + + # If database export was unsuccessful, skip update + if [ -f "$DUMP_DIR/export.failed" ]; then + echo "Database export failed the last time. Most likely was the export time not high enough." + echo "Please report this to https://github.com/nextcloud/all-in-one/issues. Thanks!" + exit 1 + fi + + # Write output to logfile. + exec > >(tee -i "$DUMP_DIR/database-import.log") + exec 2>&1 + + # Inform + echo "Restoring from database dump." + + # Add import.failed file + touch "$DUMP_DIR/import.failed" + + # Exit if any command fails + set -ex + + # Remove old database files + rm -rf "${DATADIR:?}/"* + + # Change database port to a random port temporarily + export PGPORT=11000 + + # Create new database + exec docker-entrypoint.sh postgres & + + # Wait for creation + while ! psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()"; do + echo "Waiting for the database to start." + sleep 5 + done + + # Check if the line we grep for later on is there + GREP_STRING='Name: oc_appconfig; Type: TABLE; Schema: public; Owner:' + if ! grep -qa "$GREP_STRING" "$DUMP_FILE"; then + echo "The needed oc_appconfig line is not there which is unexpected." + echo "Please report this to https://github.com/nextcloud/all-in-one/issues. Thanks!" + exit 1 + fi + + # Get the Owner + DB_OWNER="$(grep -a "$GREP_STRING" "$DUMP_FILE" | head -1 | grep -oP 'Owner:.*$' | sed 's|Owner:||;s|[[:space:]]||g')" + if [ "$DB_OWNER" = "$POSTGRES_USER" ]; then + echo "Unfortunately was the found database owner of the dump file the same as the POSTGRES_USER $POSTGRES_USER" + echo "It is not possible to import a database dump from this database owner." + echo "However you might rename the owner in the dumpfile to something else." + exit 1 + elif [ "$DB_OWNER" != "oc_$POSTGRES_USER" ]; then + DIFFERENT_DB_OWNER=1 + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER "$DB_OWNER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB; + ALTER DATABASE "$POSTGRES_DB" OWNER TO "$DB_OWNER"; + GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "$DB_OWNER"; + GRANT ALL PRIVILEGES ON SCHEMA public TO "$DB_OWNER"; +EOSQL + fi + + # Restore database + echo "Restoring the database from database dump" + psql "$POSTGRES_DB" -U "$POSTGRES_USER" < "$DUMP_FILE" + + # Correct permissions + if [ -n "$DIFFERENT_DB_OWNER" ]; then + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + ALTER DATABASE "$POSTGRES_DB" OWNER TO "oc_$POSTGRES_USER"; + REASSIGN OWNED BY "$DB_OWNER" TO "oc_$POSTGRES_USER"; +EOSQL + fi + + # Shut down the database to be able to start it again + # The smart mode disallows new connections, then waits for all existing clients to disconnect and any online backup to finish + # Wait for 1800s to make sure that a checkpoint is completed successfully + pg_ctl stop -m smart -t 1800 + + # Change database port back to default + export PGPORT=5432 + + # Don't exit if command fails anymore + set +ex + + # Remove import failed file if everything went correctly + rm "$DUMP_DIR/import.failed" +fi + +# Cover the last case +if ! [ -f "$DATADIR/PG_VERSION" ] && ! [ -f "$DUMP_FILE" ]; then + # Remove old database files if somehow there should be some + rm -rf "${DATADIR:?}/"* +fi + +# Modify postgresql.conf +if [ -f "/var/lib/postgresql/data/postgresql.conf" ]; then + echo "Setting postgres values..." + + # Sync this with max pm.max_children and MaxRequestWorkers + # 5000 connections is apparently the highest possible value with postgres so set it to that so that we don't run into a limit here. + # We don't actually expect so many connections but don't want to limit it artificially because people will report issues otherwise + # Also connections should usually be closed again after the process is done + # If we should actually exceed this limit, it is definitely a bug in Nextcloud server or some of its apps that does not close connections correctly and not a bug in AIO + sed -i "s|^max_connections =.*|max_connections = 5000|" "/var/lib/postgresql/data/postgresql.conf" + + # Do not log checkpoints + if grep -q "#log_checkpoints" /var/lib/postgresql/data/postgresql.conf; then + sed -i 's|#log_checkpoints.*|log_checkpoints = off|' /var/lib/postgresql/data/postgresql.conf + fi + + # Closing idling connections automatically seems to break any logic so was reverted again to default where it is disabled + if grep -q "^idle_session_timeout" /var/lib/postgresql/data/postgresql.conf; then + sed -i 's|^idle_session_timeout.*|#idle_session_timeout|' /var/lib/postgresql/data/postgresql.conf + fi +fi + +do_database_dump() { + set -x + rm -f "$DUMP_FILE.temp" + touch "$DUMP_DIR/export.failed" + if pg_dump --username "$POSTGRES_USER" "$POSTGRES_DB" > "$DUMP_FILE.temp"; then + rm -f "$DUMP_FILE" + mv "$DUMP_FILE.temp" "$DUMP_FILE" + pg_ctl stop -m fast + rm "$DUMP_DIR/export.failed" + echo 'Database dump successful!' + set +x + exit 0 + else + pg_ctl stop -m fast + echo "Database dump unsuccessful!" + set +x + exit 1 + fi +} + +# Catch docker stop attempts +trap do_database_dump SIGINT SIGTERM + +# Start the database +exec docker-entrypoint.sh postgres & +wait $! diff --git a/nextcloud-aio/Containers/redis/Dockerfile b/nextcloud-aio/Containers/redis/Dockerfile new file mode 100644 index 0000000..8cb0f97 --- /dev/null +++ b/nextcloud-aio/Containers/redis/Dockerfile @@ -0,0 +1,24 @@ +# syntax=docker/dockerfile:latest +# From https://github.com/docker-library/redis/blob/master/7.2/alpine/Dockerfile +FROM redis:7.2.11-alpine + +COPY --chmod=775 start.sh /start.sh + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache openssl bash; \ + \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + \ +# Get rid of unused binaries + rm -f /usr/local/bin/gosu; + +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +USER 999 +ENTRYPOINT ["/start.sh"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/redis/healthcheck.sh b/nextcloud-aio/Containers/redis/healthcheck.sh new file mode 100644 index 0000000..6588229 --- /dev/null +++ b/nextcloud-aio/Containers/redis/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +redis-cli -a "$REDIS_HOST_PASSWORD" PING || exit 1 diff --git a/nextcloud-aio/Containers/redis/start.sh b/nextcloud-aio/Containers/redis/start.sh new file mode 100644 index 0000000..69764c1 --- /dev/null +++ b/nextcloud-aio/Containers/redis/start.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Show wiki if vm.overcommit is disabled +if [ "$(sysctl -n vm.overcommit_memory)" != "1" ]; then + echo "Memory overcommit is disabled but necessary for safe operation" + echo "See https://github.com/nextcloud/all-in-one/discussions/1731 how to enable overcommit" +fi + +# Run redis with a password if provided +echo "Redis has started" +if [ -n "$REDIS_HOST_PASSWORD" ]; then + exec redis-server --requirepass "$REDIS_HOST_PASSWORD" --loglevel warning +else + exec redis-server --loglevel warning +fi + +exec "$@" diff --git a/nextcloud-aio/Containers/talk-recording/Dockerfile b/nextcloud-aio/Containers/talk-recording/Dockerfile new file mode 100644 index 0000000..40b0147 --- /dev/null +++ b/nextcloud-aio/Containers/talk-recording/Dockerfile @@ -0,0 +1,61 @@ +# syntax=docker/dockerfile:latest +FROM python:3.13.7-alpine3.22 + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +ENV RECORDING_VERSION=v0.1 +ENV ALLOW_ALL=false +ENV HPB_PROTOCOL=https +ENV NC_PROTOCOL=https +ENV SKIP_VERIFY=false +ENV HPB_PATH=/standalone-signaling/ + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + bash \ + xvfb \ + ffmpeg \ + firefox \ + bind-tools \ + netcat-openbsd \ + git \ + wget \ + shadow \ + pulseaudio \ + openssl \ + build-base \ + linux-headers \ + geckodriver; \ + useradd -d /tmp --system recording -u 122; \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + git clone --recursive https://github.com/nextcloud/nextcloud-talk-recording --depth=1 --single-branch --branch "$RECORDING_VERSION" /src; \ + python3 -m pip install --no-cache-dir /src; \ + rm -rf /src; \ + touch /etc/recording.conf; \ + chown recording:recording -R \ + /tmp /etc/recording.conf; \ + mkdir -p /conf; \ + chmod 777 /conf; \ + chmod 777 /tmp; \ + apk del --no-cache \ + git \ + wget \ + shadow \ + openssl \ + build-base \ + linux-headers; + +VOLUME /tmp +WORKDIR /tmp +USER 122 +ENTRYPOINT ["/start.sh"] +CMD ["python", "-m", "nextcloud.talk.recording", "--config", "/conf/recording.conf"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/talk-recording/healthcheck.sh b/nextcloud-aio/Containers/talk-recording/healthcheck.sh new file mode 100644 index 0000000..8397ab3 --- /dev/null +++ b/nextcloud-aio/Containers/talk-recording/healthcheck.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +nc -z 127.0.0.1 1234 || exit 1 diff --git a/nextcloud-aio/Containers/talk-recording/recording.conf b/nextcloud-aio/Containers/talk-recording/recording.conf new file mode 100644 index 0000000..9951552 --- /dev/null +++ b/nextcloud-aio/Containers/talk-recording/recording.conf @@ -0,0 +1,123 @@ +[logs] +# Log level based on numeric values of Python logging levels: +# - Critical: 50 +# - Error: 40 +# - Warning: 30 +# - Info: 20 +# - Debug: 10 +# - Not set: 0 +#level = 20 + +[http] +# IP and port to listen on for HTTP requests. +#listen = 127.0.0.1:8000 + +[backend] +# Allow any hostname as backend endpoint. This is extremely insecure and should +# only be used during development. +#allowall = false + +# Common shared secret for requests from and to the backend servers if +# "allowall" is enabled. This must be the same value as configured in the +# Nextcloud admin ui. +#secret = the-shared-secret + +# Comma-separated list of backend ids allowed to connect. +#backends = backend-id, another-backend + +# If set to "true", certificate validation of backend endpoints will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +# Overridable by backend. +#skipverify = false + +# Maximum allowed size in bytes for messages sent by the backend. +# Overridable by backend. +#maxmessagesize = 1024 + +# Width for recorded videos. +# Overridable by backend. +#videowidth = 1920 + +# Height for recorded videos. +# Overridable by backend. +#videoheight = 1080 + +# Temporary directory used to store recordings until uploaded. It must be +# writable by the user running the recording server. +# Overridable by backend. +#directory = /tmp + +# Backend configurations as defined in the "[backend]" section above. The +# section names must match the ids used in "backends" above. +#[backend-id] +# URL of the Nextcloud instance +#url = https://cloud.domain.invalid + +# Shared secret for requests from and to the backend servers. This must be the +# same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +#[another-backend] +# URL of the Nextcloud instance +#url = https://cloud.otherdomain.invalid + +# Shared secret for requests from and to the backend servers. This must be the +# same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +[signaling] +# Common shared secret for authenticating as an internal client of signaling +# servers if a specific secret is not set for a signaling server. This must be +# the same value as configured in the signaling server configuration file. +#internalsecret = the-shared-secret-for-internal-clients + +# Comma-separated list of signaling servers with specific internal secrets. +#signalings = signaling-id, another-signaling + +# Signaling server configurations as defined in the "[signaling]" section above. +# The section names must match the ids used in "signalings" above. +#[signaling-id] +# URL of the signaling server +#url = https://signaling.domain.invalid + +# Shared secret for authenticating as an internal client of signaling servers. +# This must be the same value as configured in the signaling server +# configuration file. +#internalsecret = the-shared-secret-for-internal-clients + +#[another-signaling] +# URL of the signaling server +#url = https://signaling.otherdomain.invalid + +# Shared secret for authenticating as an internal client of signaling servers. +# This must be the same value as configured in the signaling server +# configuration file. +#internalsecret = the-shared-secret-for-internal-clients + +[ffmpeg] +# The ffmpeg executable (name or full path) and the global options given to +# ffmpeg. The options given here fully override the default global options. +#common = ffmpeg -loglevel level+warning -n + +# The options given to ffmpeg to encode the audio output. The options given here +# fully override the default options for the audio output. +#outputaudio = -c:a libopus + +# The options given to ffmpeg to encode the video output. The options given here +# fully override the default options for the video output. +#outputvideo = -c:v libvpx -deadline:v realtime -crf 10 -b:v 1M + +# The extension of the file for audio only recordings. +#extensionaudio = .ogg + +# The extension of the file for audio and video recordings. +#extensionvideo = .webm + +[recording] +# Browser to use for recordings. Please note that the "chrome" value does not +# refer to the web browser, but to the Selenium WebDriver. In practice, "chrome" +# will use Google Chrome, or Chromium if Google Chrome is not installed. +# Allowed values: firefox, chrome +# Defaults to firefox +# browser = firefox diff --git a/nextcloud-aio/Containers/talk-recording/start.sh b/nextcloud-aio/Containers/talk-recording/start.sh new file mode 100644 index 0000000..a03eed0 --- /dev/null +++ b/nextcloud-aio/Containers/talk-recording/start.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# Variables +if [ -z "$NC_DOMAIN" ]; then + echo "You need to provide the NC_DOMAIN." + exit 1 +elif [ -z "$RECORDING_SECRET" ]; then + echo "You need to provide the RECORDING_SECRET." + exit 1 +elif [ -z "$INTERNAL_SECRET" ]; then + echo "You need to provide the INTERNAL_SECRET." + exit 1 +fi + +if [ -z "$HPB_DOMAIN" ]; then + export HPB_DOMAIN="$NC_DOMAIN" +fi + +# Delete all contents on startup to start fresh +rm -fr /tmp/{*,.*} + +cat << RECORDING_CONF > "/conf/recording.conf" +[logs] +# 30 means Warning +level = 30 + +[http] +listen = 0.0.0.0:1234 + +[backend] +allowall = ${ALLOW_ALL} +# The secret below is still needed if allowall is set to true, also it doesn't hurt to be here +secret = ${RECORDING_SECRET} +backends = backend-1 +skipverify = ${SKIP_VERIFY} +maxmessagesize = 1024 +videowidth = 1920 +videoheight = 1080 +directory = /tmp + +[backend-1] +url = ${NC_PROTOCOL}://${NC_DOMAIN} +secret = ${RECORDING_SECRET} +skipverify = ${SKIP_VERIFY} + +[signaling] +signalings = signaling-1 + +[signaling-1] +url = ${HPB_PROTOCOL}://${HPB_DOMAIN}${HPB_PATH} +internalsecret = ${INTERNAL_SECRET} + +[ffmpeg] +# common = ffmpeg -loglevel level+warning -n +# outputaudio = -c:a libopus +# outputvideo = -c:v libvpx -deadline:v realtime -crf 10 -b:v 1M +extensionaudio = .ogg +extensionvideo = .webm + +[recording] +browser = firefox +RECORDING_CONF + +exec "$@" diff --git a/nextcloud-aio/Containers/talk/Dockerfile b/nextcloud-aio/Containers/talk/Dockerfile new file mode 100644 index 0000000..7067c72 --- /dev/null +++ b/nextcloud-aio/Containers/talk/Dockerfile @@ -0,0 +1,110 @@ +# syntax=docker/dockerfile:latest +FROM nats:2.12.0-scratch AS nats +FROM eturnal/eturnal:1.12.2-alpine AS eturnal +FROM strukturag/nextcloud-spreed-signaling:2.0.4 AS signaling +FROM alpine:3.22.1 AS janus + +ARG JANUS_VERSION=v1.3.2 +WORKDIR /src +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + git \ + autoconf \ + automake \ + build-base \ + pkgconfig \ + libtool \ + util-linux \ + glib-dev \ + zlib-dev \ + openssl-dev \ + jansson-dev \ + libnice-dev \ + libconfig-dev \ + libsrtp-dev \ + libusrsctp-dev \ + gengetopt-dev \ + libwebsockets-dev; \ + git clone --recursive https://github.com/meetecho/janus-gateway --depth=1 --single-branch --branch "$JANUS_VERSION" /src; \ + /src/autogen.sh; \ + /src/configure --disable-rabbitmq --disable-mqtt --disable-boringssl; \ + make; \ + make install; \ + make configs; \ + rename -v ".jcfg.sample" ".jcfg" /usr/local/etc/janus/*.jcfg.sample + +FROM alpine:3.22.1 +ENV ETURNAL_ETC_DIR="/conf" +ENV SKIP_CERT_VERIFY=false +COPY --from=janus --chmod=777 --chown=1000:1000 /usr/local /usr/local +COPY --from=eturnal --chmod=777 --chown=1000:1000 /opt/eturnal /opt/eturnal +COPY --from=nats --chmod=777 --chown=1000:1000 /nats-server /usr/local/bin/nats-server +COPY --from=signaling --chmod=777 --chown=1000:1000 /usr/bin/nextcloud-spreed-signaling /usr/local/bin/nextcloud-spreed-signaling + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh +COPY --chmod=664 supervisord.conf /supervisord.conf + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache \ + ca-certificates \ + tzdata \ + bash \ + openssl \ + supervisor \ + bind-tools \ + netcat-openbsd \ + \ + glib \ + zlib \ + libssl3 \ + libcrypto3 \ + jansson \ + libnice \ + libconfig \ + libsrtp \ + libusrsctp \ + libwebsockets \ + \ + shadow \ + grep; \ + useradd --system -u 1000 eturnal; \ + apk del --no-cache \ + shadow; \ + \ +# Give root a random password + echo "root:$(openssl rand -base64 12)" | chpasswd; \ + \ + touch \ + /etc/nats.conf \ + /etc/eturnal.yml; \ + echo "listen: 127.0.0.1:4222" | tee /etc/nats.conf; \ + mkdir -p \ + /var/tmp \ + /conf \ + /var/lib/turn \ + /var/log/supervisord \ + /var/run/supervisord \ + /usr/local/lib/janus/loggers; \ + chown eturnal:eturnal -R \ + /etc/nats.conf \ + /var/log/supervisord \ + /var/run/supervisord; \ + chmod 777 -R \ + /tmp \ + /conf \ + /var/run/supervisord \ + /var/log/supervisord; \ + ln -s /opt/eturnal/bin/stun /usr/local/bin/stun; \ + ln -s /opt/eturnal/bin/eturnalctl /usr/local/bin/eturnalctl + +USER 1000 +ENTRYPOINT ["/start.sh"] +CMD ["supervisord", "-c", "/supervisord.conf"] + +HEALTHCHECK CMD /healthcheck.sh +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/talk/healthcheck.sh b/nextcloud-aio/Containers/talk/healthcheck.sh new file mode 100644 index 0000000..e454476 --- /dev/null +++ b/nextcloud-aio/Containers/talk/healthcheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +nc -z 127.0.0.1 8081 || exit 1 +nc -z 127.0.0.1 8188 || exit 1 +nc -z 127.0.0.1 4222 || exit 1 +nc -z 127.0.0.1 "$TALK_PORT" || exit 1 +eturnalctl status || exit 1 diff --git a/nextcloud-aio/Containers/talk/server.conf.in b/nextcloud-aio/Containers/talk/server.conf.in new file mode 100644 index 0000000..8f437e3 --- /dev/null +++ b/nextcloud-aio/Containers/talk/server.conf.in @@ -0,0 +1,342 @@ +[http] +# IP and port to listen on for HTTP requests. +# Comment line to disable the listener. +#listen = 127.0.0.1:8080 + +# HTTP socket read timeout in seconds. +#readtimeout = 15 + +# HTTP socket write timeout in seconds. +#writetimeout = 30 + +[https] +# IP and port to listen on for HTTPS requests. +# Comment line to disable the listener. +#listen = 127.0.0.1:8443 + +# HTTPS socket read timeout in seconds. +#readtimeout = 15 + +# HTTPS socket write timeout in seconds. +#writetimeout = 30 + +# Certificate / private key to use for the HTTPS server. +certificate = /etc/nginx/ssl/server.crt +key = /etc/nginx/ssl/server.key + +[app] +# Set to "true" to install pprof debug handlers. +# See "https://golang.org/pkg/net/http/pprof/" for further information. +debug = false + +# Set to "true" to allow subscribing any streams. This is insecure and should +# only be enabled for testing. By default only streams of users in the same +# room and call can be subscribed. +#allowsubscribeany = false + +# Comma separated list of trusted proxies (IPs or CIDR networks) that may set +# the "X-Real-Ip" or "X-Forwarded-For" headers. If both are provided, the +# "X-Real-Ip" header will take precedence (if valid). +# Leave empty to allow loopback and local addresses. +#trustedproxies = + +[sessions] +# Secret value used to generate checksums of sessions. This should be a random +# string of 32 or 64 bytes. +hashkey = the-secret-for-session-checksums + +# Optional key for encrypting data in the sessions. Must be either 16, 24 or +# 32 bytes. +# If no key is specified, data will not be encrypted (not recommended). +blockkey = -encryption-key- + +[clients] +# Shared secret for connections from internal clients. This must be the same +# value as configured in the respective internal services. +internalsecret = the-shared-secret-for-internal-clients + +[federation] +# If set to "true", certificate validation of federation targets will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +#skipverify = false + +# Timeout in seconds for requests to federation targets. +#timeout = 10 + +[backend] +# Type of backend configuration. +# Defaults to "static". +# +# Possible values: +# - static: A comma-separated list of backends is given in the "backends" option. +# - etcd: Backends are retrieved from an etcd cluster. +#backendtype = static + +# For backend type "static": +# Comma-separated list of backend ids from which clients are allowed to connect +# from. Each backend will have isolated rooms, i.e. clients connecting to room +# "abc12345" on backend 1 will be in a different room than clients connected to +# a room with the same name on backend 2. Also sessions connected from different +# backends will not be able to communicate with each other. +#backends = backend-id, another-backend + +# For backend type "etcd": +# Key prefix of backend entries. All keys below will be watched and assumed to +# contain a JSON document with the following entries: +# - "urls": List of urls of the Nextcloud instance. +# - "url": Url of the Nextcloud instance (deprecated). +# - "secret": Shared secret for requests from and to the backend servers. +# +# Additional optional entries: +# - "maxstreambitrate": Maximum bitrate per publishing stream (in bits per second). +# - "maxscreenbitrate": Maximum bitrate per screensharing stream (in bits per second). +# - "sessionlimit": Number of sessions that are allowed to connect. +# +# Example: +# "/signaling/backend/one" -> {"urls": ["https://nextcloud.domain1.invalid"], ...} +# "/signaling/backend/two" -> {"urls": ["https://domain2.invalid/nextcloud"], ...} +#backendprefix = /signaling/backend + +# Allow any hostname as backend endpoint. This is extremely insecure and should +# only be used while running the benchmark client against the server. +allowall = false + +# Common shared secret for requests from and to the backend servers. Used if +# "allowall" is enabled or as fallback for individual backends that don't have +# their own secret set. +# This must be the same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret-for-allowall + +# Timeout in seconds for requests to the backend. +timeout = 10 + +# Maximum number of concurrent backend connections per host. +connectionsperhost = 8 + +# If set to "true", certificate validation of backend endpoints will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +#skipverify = false + +# For backendtype "static": +# Backend configurations as defined in the "[backend]" section above. The +# section names must match the ids used in "backends" above. +#[backend-id] +# Comma-separated list of urls of the Nextcloud instance +#urls = https://cloud.domain.invalid + +# Shared secret for requests from and to the backend servers. Leave empty to use +# the common shared secret from above. +# This must be the same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +# Limit the number of sessions that are allowed to connect to this backend. +# Omit or set to 0 to not limit the number of sessions. +#sessionlimit = 10 + +# The maximum bitrate per publishing stream (in bits per second). +# Defaults to the maximum bitrate configured for the proxy / MCU. +#maxstreambitrate = 1048576 + +# The maximum bitrate per screensharing stream (in bits per second). +# Defaults to the maximum bitrate configured for the proxy / MCU. +#maxscreenbitrate = 2097152 + +#[another-backend] +# Comma-separated list of urls of the Nextcloud instance +#urls = https://cloud.otherdomain.invalid + +# Shared secret for requests from and to the backend servers. Leave empty to use +# the common shared secret from above. +# This must be the same value as configured in the Nextcloud admin ui. +#secret = the-shared-secret + +[nats] +# Url of NATS backend to use. This can also be a list of URLs to connect to +# multiple backends. For local development, this can be set to "nats://loopback" +# to process NATS messages internally instead of sending them through an +# external NATS backend. +#url = nats://localhost:4222 + +[mcu] +# The type of the MCU to use. Currently only "janus" and "proxy" are supported. +# Leave empty to disable MCU functionality. +#type = + +# For type "janus": the URL to the websocket endpoint of the MCU server. +# For type "proxy": a space-separated list of proxy URLs to connect to. +#url = + +# The maximum bitrate per publishing stream (in bits per second). +# Defaults to 1 mbit/sec. +# For type "proxy": will be capped to the maximum bitrate configured at the +# proxy server that is used. +#maxstreambitrate = 1048576 + +# The maximum bitrate per screensharing stream (in bits per second). +# Default is 2 mbit/sec. +# For type "proxy": will be capped to the maximum bitrate configured at the +# proxy server that is used. +#maxscreenbitrate = 2097152 + +# List of IP addresses / subnets that are allowed to be used by clients in +# candidates. The allowed list has preference over the blocked list below. +#allowedcandidates = 10.0.0.0/8 + +# List of IP addresses / subnets to filter from candidates received by clients. +#blockedcandidates = 1.2.3.0/24 + +# For type "proxy": timeout in seconds for requests to the proxy server. +#proxytimeout = 2 + +# For type "proxy": type of URL configuration for proxy servers. +# Defaults to "static". +# +# Possible values: +# - static: A space-separated list of proxy URLs is given in the "url" option. +# - etcd: Proxy URLs are retrieved from an etcd cluster (see below). +#urltype = static + +# If set to "true", certificate validation of proxy servers will be skipped. +# This should only be enabled during development, e.g. to work with self-signed +# certificates. +#skipverify = false + +# For type "proxy": the id of the token to use when connecting to proxy servers. +#token_id = server1 + +# For type "proxy": the private key for the configured token id to use when +# connecting to proxy servers. +#token_key = privkey.pem + +# For url type "static": Enable DNS discovery on hostname of configured URL. +# If the hostname resolves to multiple IP addresses, a connection is established +# to each of them. +# Changes to the DNS are monitored regularly and proxy connections are created +# or deleted as necessary. +#dnsdiscovery = true + +# For url type "etcd": Key prefix of MCU proxy entries. All keys below will be +# watched and assumed to contain a JSON document. The entry "address" from this +# document will be used as proxy URL, other contents in the document will be +# ignored. +# +# Example: +# "/signaling/proxy/server/one" -> {"address": "https://proxy1.domain.invalid"} +# "/signaling/proxy/server/two" -> {"address": "https://proxy2.domain.invalid"} +#keyprefix = /signaling/proxy/server + +[turn] +# API key that the MCU will need to send when requesting TURN credentials. +#apikey = the-api-key-for-the-rest-service + +# The shared secret to use for generating TURN credentials. This must be the +# same as on the TURN server. +#secret = 6d1c17a7-c736-4e22-b02c-e2955b7ecc64 + +# A comma-separated list of TURN servers to use. Leave empty to disable the +# TURN REST API. +#servers = turn:1.2.3.4:9991?transport=udp,turn:1.2.3.4:9991?transport=tcp + +[geoip] +# License key to use when downloading the MaxMind GeoIP database. You can +# register an account at "https://www.maxmind.com/en/geolite2/signup" for +# free. See "https://dev.maxmind.com/geoip/geoip2/geolite2/" for further +# information. +# You can also get a free GeoIP database from https://db-ip.com/ without +# registration. Provide the URL below in this case. +# Leave empty to disable GeoIP lookups. +#license = + +# Optional URL to download a MaxMind GeoIP database from. Will be generated if +# "license" is provided above. Can be a "file://" url if a local file should +# be used. Please note that the database must provide a country field when +# looking up IP addresses. +#url = + +[geoip-overrides] +# Optional overrides for GeoIP lookups. The key is an IP address / range, the +# value the associated country code. +#127.0.0.1 = DE +#192.168.0.0/24 = DE + +[continent-overrides] +# Optional overrides for continent mappings. The key is a continent code, the +# value a comma-separated list of continent codes to map the continent to. +# Use European servers for clients in Africa. +#AF = EU +# Use servers in North Africa for clients in South America. +#SA = NA + +[stats] +# Comma-separated list of IP addresses that are allowed to access the stats +# endpoint. Leave empty (or commented) to only allow access from "127.0.0.1". +#allowed_ips = + +[etcd] +# Comma-separated list of static etcd endpoints to connect to. +#endpoints = 127.0.0.1:2379,127.0.0.1:22379,127.0.0.1:32379 + +# Options to perform endpoint discovery through DNS SRV. +# Only used if no endpoints are configured manually. +#discoverysrv = example.com +#discoveryservice = foo + +# Path to private key, client certificate and CA certificate if TLS +# authentication should be used. +#clientkey = /path/to/etcd-client.key +#clientcert = /path/to/etcd-client.crt +#cacert = /path/to/etcd-ca.crt + +[grpc] +# IP and port to listen on for GRPC requests. +# Comment line to disable the listener. +#listen = 0.0.0.0:9090 + +# Certificate / private key to use for the GRPC server. +# Omit to use unencrypted connections. +#servercertificate = /path/to/grpc-server.crt +#serverkey = /path/to/grpc-server.key + +# CA certificate that is allowed to issue certificates of GRPC servers. +# Omit to expect unencrypted connections. +#serverca = /path/to/grpc-ca.crt + +# Certificate / private key to use for the GRPC client. +# Omit if clients don't need to authenticate on the server. +#clientcertificate = /path/to/grpc-client.crt +#clientkey = /path/to/grpc-client.key + +# CA certificate that is allowed to issue certificates of GRPC clients. +# Omit to allow any clients to connect. +#clientca = /path/to/grpc-ca.crt + +# Type of GRPC target configuration. +# Defaults to "static". +# +# Possible values: +# - static: A comma-separated list of targets is given in the "targets" option. +# - etcd: Target URLs are retrieved from an etcd cluster. +#targettype = static + +# For target type "static": Comma-separated list of GRPC targets to connect to +# for clustering mode. +#targets = 192.168.0.1:9090, 192.168.0.2:9090 + +# For target type "static": Enable DNS discovery on hostnames of GRPC target. +# If a hostname resolves to multiple IP addresses, a connection is established +# to each of them. +# Changes to the DNS are monitored regularly and GRPC clients are created or +# deleted as necessary. +#dnsdiscovery = true + +# For target type "etcd": Key prefix of GRPC target entries. All keys below will +# be watched and assumed to contain a JSON document. The entry "address" from +# this document will be used as target URL, other contents in the document will +# be ignored. +# +# Example: +# "/signaling/cluster/grpc/one" -> {"address": "192.168.0.1:9090"} +# "/signaling/cluster/grpc/two" -> {"address": "192.168.0.2:9090"} +#targetprefix = /signaling/cluster/grpc diff --git a/nextcloud-aio/Containers/talk/start.sh b/nextcloud-aio/Containers/talk/start.sh new file mode 100644 index 0000000..f89949f --- /dev/null +++ b/nextcloud-aio/Containers/talk/start.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +# Variables +if [ -z "$NC_DOMAIN" ]; then + echo "You need to provide the NC_DOMAIN." + exit 1 +elif [ -z "$TALK_PORT" ]; then + echo "You need to provide the TALK_PORT." + exit 1 +elif [ -z "$TURN_SECRET" ]; then + echo "You need to provide the TURN_SECRET." + exit 1 +elif [ -z "$SIGNALING_SECRET" ]; then + echo "You need to provide the SIGNALING_SECRET." + exit 1 +elif [ -z "$INTERNAL_SECRET" ]; then + echo "You need to provide the INTERNAL_SECRET." + exit 1 +fi + +set -x +IPv4_ADDRESS_TALK_RELAY="$(hostname -i | grep -oP '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)" +# shellcheck disable=SC2153 +IPv4_ADDRESS_TALK="$(dig "$TALK_HOST" IN A +short +search | grep '^[0-9.]\+$' | sort | head -n1)" +# shellcheck disable=SC2153 +IPv6_ADDRESS_TALK="$(dig "$TALK_HOST" AAAA +short +search | grep '^[0-9a-f:]\+$' | sort | head -n1)" +set +x + +if [ -n "$IPv4_ADDRESS_TALK" ] && [ "$IPv4_ADDRESS_TALK_RELAY" = "$IPv4_ADDRESS_TALK" ]; then + IPv4_ADDRESS_TALK="" +fi + +set -x +IP_BINDING="::" +if grep -q "1" /sys/module/ipv6/parameters/disable \ +|| grep -q "1" /proc/sys/net/ipv6/conf/all/disable_ipv6 \ +|| grep -q "1" /proc/sys/net/ipv6/conf/default/disable_ipv6; then + IP_BINDING="0.0.0.0" +fi +set +x + +# Turn +cat << TURN_CONF > "/conf/eturnal.yml" +eturnal: + listen: + - ip: "$IP_BINDING" + port: $TALK_PORT + transport: udp + - ip: "$IP_BINDING" + port: $TALK_PORT + transport: tcp + log_dir: stdout + log_level: warning + secret: "$TURN_SECRET" + relay_ipv4_addr: "$IPv4_ADDRESS_TALK_RELAY" + relay_ipv6_addr: "$IPv6_ADDRESS_TALK" + blacklist_peers: + - recommended + whitelist_peers: + - 127.0.0.1 + - ::1 + - "$IPv4_ADDRESS_TALK_RELAY" + - "$IPv4_ADDRESS_TALK" + - "$IPv6_ADDRESS_TALK" +TURN_CONF + +# Remove empty lines so that the config is not invalid +sed -i '/""/d' /conf/eturnal.yml + +if [ -z "$TALK_MAX_STREAM_BITRATE" ]; then + TALK_MAX_STREAM_BITRATE=1048576 +fi + +if [ -z "$TALK_MAX_SCREEN_BITRATE" ]; then + TALK_MAX_SCREEN_BITRATE=2097152 +fi + +# Signling +cat << SIGNALING_CONF > "/conf/signaling.conf" +[http] +listen = 0.0.0.0:8081 + +[app] +debug = false + +[sessions] +hashkey = $(openssl rand -hex 16) +blockkey = $(openssl rand -hex 16) + +[clients] +internalsecret = ${INTERNAL_SECRET} + +[backend] +backends = backend-1 +allowall = false +timeout = 10 +connectionsperhost = 8 +skipverify = ${SKIP_CERT_VERIFY} + +[backend-1] +urls = https://${NC_DOMAIN} +secret = ${SIGNALING_SECRET} +maxstreambitrate = ${TALK_MAX_STREAM_BITRATE} +maxscreenbitrate = ${TALK_MAX_SCREEN_BITRATE} + +[nats] +url = nats://127.0.0.1:4222 + +[mcu] +type = janus +url = ws://127.0.0.1:8188 +maxstreambitrate = ${TALK_MAX_STREAM_BITRATE} +maxscreenbitrate = ${TALK_MAX_SCREEN_BITRATE} +SIGNALING_CONF + +exec "$@" diff --git a/nextcloud-aio/Containers/talk/supervisord.conf b/nextcloud-aio/Containers/talk/supervisord.conf new file mode 100644 index 0000000..89287db --- /dev/null +++ b/nextcloud-aio/Containers/talk/supervisord.conf @@ -0,0 +1,37 @@ +[supervisord] +nodaemon=true +logfile=/var/log/supervisord/supervisord.log +pidfile=/var/run/supervisord/supervisord.pid +childlogdir=/var/log/supervisord/ +logfile_maxbytes=50MB +logfile_backups=10 +loglevel=error + +[program:eturnal] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=eturnalctl foreground + +[program:nats-server] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=nats-server -c /etc/nats.conf + +[program:janus] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +# debug-level 3 means warning +command=janus --config=/usr/local/etc/janus/janus.jcfg --disable-colors --log-stdout --full-trickle --debug-level 3 + +[program:signaling] +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +command=nextcloud-spreed-signaling -config /conf/signaling.conf diff --git a/nextcloud-aio/Containers/watchtower/Dockerfile b/nextcloud-aio/Containers/watchtower/Dockerfile new file mode 100644 index 0000000..ec2c0d0 --- /dev/null +++ b/nextcloud-aio/Containers/watchtower/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:latest +FROM ghcr.io/nicholas-fedor/watchtower:1.12.1 AS watchtower + +FROM alpine:3.22.1 + +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache bash ca-certificates tzdata + +COPY --from=watchtower /watchtower /watchtower + +COPY --chmod=775 start.sh /start.sh + +# hadolint ignore=DL3002 +USER root + +ENTRYPOINT ["/start.sh"] +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/watchtower/start.sh b/nextcloud-aio/Containers/watchtower/start.sh new file mode 100644 index 0000000..cf16e7a --- /dev/null +++ b/nextcloud-aio/Containers/watchtower/start.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Check if socket is available and readable +if ! [ -e "/var/run/docker.sock" ]; then + echo "Docker socket is not available. Cannot continue." + exit 1 +elif ! test -r /var/run/docker.sock; then + echo "Docker socket is not readable by the root user. Cannot continue." + exit 1 +fi + +if [ -f /run/.containerenv ]; then + # If running under podman disable memory_swappiness setting in watchtower. + # It is a necessary workaround until https://github.com/containers/podman/issues/23824 gets fixed. + echo "Running under Podman. Setting WATCHTOWER_DISABLE_MEMORY_SWAPPINESS to 1." + export WATCHTOWER_DISABLE_MEMORY_SWAPPINESS=1 +fi + +if [ -n "$CONTAINER_TO_UPDATE" ]; then + exec /watchtower --cleanup --debug --run-once "$CONTAINER_TO_UPDATE" +else + echo "'CONTAINER_TO_UPDATE' is not set. Cannot update anything." + exit 1 +fi + +exec "$@" diff --git a/nextcloud-aio/Containers/whiteboard/Dockerfile b/nextcloud-aio/Containers/whiteboard/Dockerfile new file mode 100644 index 0000000..680a59d --- /dev/null +++ b/nextcloud-aio/Containers/whiteboard/Dockerfile @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:latest +# Probably from this file: https://github.com/nextcloud/whiteboard/blob/main/Dockerfile +FROM ghcr.io/nextcloud-releases/whiteboard:v1.2.1 + +USER root +RUN set -ex; \ + apk upgrade --no-cache -a; \ + apk add --no-cache bash; \ + chmod 777 -R /tmp +USER 65534 + +COPY --chmod=775 start.sh /start.sh +COPY --chmod=775 healthcheck.sh /healthcheck.sh + +HEALTHCHECK CMD /healthcheck.sh + +WORKDIR /tmp + +ENTRYPOINT ["/start.sh"] + +LABEL com.centurylinklabs.watchtower.enable="false" \ + org.label-schema.vendor="Nextcloud" diff --git a/nextcloud-aio/Containers/whiteboard/healthcheck.sh b/nextcloud-aio/Containers/whiteboard/healthcheck.sh new file mode 100644 index 0000000..4f53988 --- /dev/null +++ b/nextcloud-aio/Containers/whiteboard/healthcheck.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +nc -z "$REDIS_HOST" 6379 || exit 0 +nc -z 127.0.0.1 3002 || exit 1 diff --git a/nextcloud-aio/Containers/whiteboard/start.sh b/nextcloud-aio/Containers/whiteboard/start.sh new file mode 100644 index 0000000..962df9b --- /dev/null +++ b/nextcloud-aio/Containers/whiteboard/start.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Only start container if nextcloud is accessible +while ! nc -z "$REDIS_HOST" 6379; do + echo "Waiting for redis to start..." + sleep 5 +done + +# Set a default for redis db index +if [ -z "$REDIS_DB_INDEX" ]; then + REDIS_DB_INDEX=0 +fi + +export REDIS_URL="redis://$REDIS_USER:$REDIS_HOST_PASSWORD@$REDIS_HOST/$REDIS_DB_INDEX" + +# Run it +exec npm --prefix /app run server:start diff --git a/nextcloud-aio/LICENSE b/nextcloud-aio/LICENSE new file mode 100644 index 0000000..0ad25db --- /dev/null +++ b/nextcloud-aio/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/nextcloud-aio/app/.editorconfig b/nextcloud-aio/app/.editorconfig new file mode 100644 index 0000000..5ce6434 --- /dev/null +++ b/nextcloud-aio/app/.editorconfig @@ -0,0 +1,19 @@ +# https://editorconfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.feature] +indent_size = 2 +indent_style = space + +[*.yml] +indent_size = 2 +indent_style = space diff --git a/nextcloud-aio/app/appinfo/info.xml b/nextcloud-aio/app/appinfo/info.xml new file mode 100644 index 0000000..8b911c1 --- /dev/null +++ b/nextcloud-aio/app/appinfo/info.xml @@ -0,0 +1,23 @@ + + + nextcloud-aio + Nextcloud All-in-One + Provides a login link for admins. + Add a link to the admin settings that gives access to the Nextcloud All-in-One admin interface + 0.8.0 + agpl + Azul + AllInOne + + monitoring + https://github.com/nextcloud/all-in-one/issues + + + + + + OCA\AllInOne\Settings\Admin + + + diff --git a/nextcloud-aio/app/composer/autoload.php b/nextcloud-aio/app/composer/autoload.php new file mode 100644 index 0000000..fae752a --- /dev/null +++ b/nextcloud-aio/app/composer/autoload.php @@ -0,0 +1,7 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + private $vendorDir; + + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + private static $registeredLoaders = array(); + + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + } + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders indexed by their corresponding vendor directories. + * + * @return self[] + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/nextcloud-aio/app/composer/composer/InstalledVersions.php b/nextcloud-aio/app/composer/composer/InstalledVersions.php new file mode 100644 index 0000000..b3a4e16 --- /dev/null +++ b/nextcloud-aio/app/composer/composer/InstalledVersions.php @@ -0,0 +1,337 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require it's presence, you can require `composer-runtime-api ^2.0` + */ +class InstalledVersions +{ + private static $installed; + private static $canGetVendors; + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']); + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints($constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + + if (self::$canGetVendors) { + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php'; + if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { + self::$installed = $installed[count($installed) - 1]; + } + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = require __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + $installed[] = self::$installed; + + return $installed; + } +} diff --git a/nextcloud-aio/app/composer/composer/LICENSE b/nextcloud-aio/app/composer/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/nextcloud-aio/app/composer/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/nextcloud-aio/app/composer/composer/autoload_classmap.php b/nextcloud-aio/app/composer/composer/autoload_classmap.php new file mode 100644 index 0000000..7c3c438 --- /dev/null +++ b/nextcloud-aio/app/composer/composer/autoload_classmap.php @@ -0,0 +1,11 @@ + $vendorDir . '/composer/InstalledVersions.php', + 'OCA\\AllInOne\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php', +); diff --git a/nextcloud-aio/app/composer/composer/autoload_namespaces.php b/nextcloud-aio/app/composer/composer/autoload_namespaces.php new file mode 100644 index 0000000..71c9e91 --- /dev/null +++ b/nextcloud-aio/app/composer/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ + array($baseDir . '/../lib'), +); diff --git a/nextcloud-aio/app/composer/composer/autoload_real.php b/nextcloud-aio/app/composer/composer/autoload_real.php new file mode 100644 index 0000000..b8c419a --- /dev/null +++ b/nextcloud-aio/app/composer/composer/autoload_real.php @@ -0,0 +1,46 @@ += 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitAllInOne::getInitializer($loader)); + } else { + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->setClassMapAuthoritative(true); + $loader->register(true); + + return $loader; + } +} diff --git a/nextcloud-aio/app/composer/composer/autoload_static.php b/nextcloud-aio/app/composer/composer/autoload_static.php new file mode 100644 index 0000000..94afa33 --- /dev/null +++ b/nextcloud-aio/app/composer/composer/autoload_static.php @@ -0,0 +1,37 @@ + + array ( + 'OCA\\AllInOne\\' => 13, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'OCA\\AllInOne\\' => + array ( + 0 => __DIR__ . '/..' . '/../lib', + ), + ); + + public static $classMap = array ( + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'OCA\\AllInOne\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitAllInOne::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitAllInOne::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitAllInOne::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/nextcloud-aio/app/composer/composer/installed.json b/nextcloud-aio/app/composer/composer/installed.json new file mode 100644 index 0000000..87fda74 --- /dev/null +++ b/nextcloud-aio/app/composer/composer/installed.json @@ -0,0 +1,5 @@ +{ + "packages": [], + "dev": true, + "dev-package-names": [] +} diff --git a/nextcloud-aio/app/composer/composer/installed.php b/nextcloud-aio/app/composer/composer/installed.php new file mode 100644 index 0000000..df8991b --- /dev/null +++ b/nextcloud-aio/app/composer/composer/installed.php @@ -0,0 +1,23 @@ + array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'reference' => '1b16a136ebd8f63e09df061d383f34170e2cef35', + 'name' => '__root__', + 'dev' => true, + ), + 'versions' => array( + '__root__' => array( + 'pretty_version' => 'dev-master', + 'version' => 'dev-master', + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'reference' => '1b16a136ebd8f63e09df061d383f34170e2cef35', + 'dev_requirement' => false, + ), + ), +); diff --git a/nextcloud-aio/app/lib/Settings/Admin.php b/nextcloud-aio/app/lib/Settings/Admin.php new file mode 100644 index 0000000..36fbd01 --- /dev/null +++ b/nextcloud-aio/app/lib/Settings/Admin.php @@ -0,0 +1,83 @@ + + * + * @author Azul + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ +namespace OCA\AllInOne\Settings; + +use OCP\AppFramework\Http\TemplateResponse; +use OCP\IConfig; +use OCP\IDateTimeFormatter; +use OCP\L10N\IFactory; +use OCP\Settings\ISettings; + +class Admin implements ISettings { + /** @var IConfig */ + private $config; + /** @var IDateTimeFormatter */ + private $dateTimeFormatter; + /** @var IFactory */ + private $l10nFactory; + + public function __construct( + IConfig $config, + IDateTimeFormatter $dateTimeFormatter, + IFactory $l10nFactory + ) { + $this->config = $config; + $this->dateTimeFormatter = $dateTimeFormatter; + $this->l10nFactory = $l10nFactory; + } + + /** + * @return TemplateResponse + */ + public function getForm(): TemplateResponse { + $lastUpdateCheckTimestamp = $this->config->getAppValue('core', 'lastupdatedat'); + $lastUpdateCheck = $this->dateTimeFormatter->formatDateTime($lastUpdateCheckTimestamp); + + $token = urlencode(getenv('AIO_TOKEN')); + $params = [ + 'AIOLoginUrl' => 'https://' . getenv('AIO_URL') . '/api/auth/getlogin' . '?token=' . $token, + ]; + + return new TemplateResponse('nextcloud-aio', 'admin', $params, ''); + } + + /** + * @return string the section ID, e.g. 'sharing' + */ + public function getSection(): string { + return 'overview'; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority(): int { + return 0; + } +} diff --git a/nextcloud-aio/app/readme.md b/nextcloud-aio/app/readme.md new file mode 100644 index 0000000..9f63950 --- /dev/null +++ b/nextcloud-aio/app/readme.md @@ -0,0 +1,7 @@ +## How to develop the app? + +Please note that in order to check if an app is already downloaded +Nextcloud will look for a folder with the same name as the app. + +Therefore you need to add the app to one of the app directories +naming the directory `nextcloud-aio`. diff --git a/nextcloud-aio/app/templates/admin.php b/nextcloud-aio/app/templates/admin.php new file mode 100644 index 0000000..4812ad9 --- /dev/null +++ b/nextcloud-aio/app/templates/admin.php @@ -0,0 +1,16 @@ + + * + * @author Azul + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + */ + /** @var array $_ */ ?> + diff --git a/nextcloud-aio/community-containers/borgbackup-viewer/borgbackup-viewer.json b/nextcloud-aio/community-containers/borgbackup-viewer/borgbackup-viewer.json new file mode 100644 index 0000000..7f9bb0a --- /dev/null +++ b/nextcloud-aio/community-containers/borgbackup-viewer/borgbackup-viewer.json @@ -0,0 +1,71 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-borgbackup-viewer", + "image_tag": "v1", + "display_name": "Borg Backup Viewer", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/borgbackup-viewer", + "image": "ghcr.io/szaimen/aio-borgbackup-viewer", + "internal_port": "5801", + "ports": [ + { + "ip_binding": "", + "port_number": "5801", + "protocol": "tcp" + } + ], + "environment": [ + "BORG_HOST_ID=nextcloud-aio-borgbackup-viewer", + "WEB_AUTHENTICATION_USERNAME=nextcloud", + "WEB_AUTHENTICATION_PASSWORD=%BORGBACKUP_VIEWER_PASSWORD%", + "WEB_LISTENING_PORT=5801", + "BORG_PASSPHRASE=%BORGBACKUP_PASSWORD%", + "BORG_REPO=/mnt/borgbackup/borg" + ], + "secrets": [ + "BORGBACKUP_VIEWER_PASSWORD", + "BORGBACKUP_PASSWORD" + ], + "ui_secret": "BORGBACKUP_VIEWER_PASSWORD", + "volumes": [ + { + "source": "nextcloud_aio_backup_cache", + "destination": "/root", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data", + "writeable": true + }, + { + "source": "nextcloud_aio_mastercontainer", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer", + "writeable": true + }, + { + "source": "%BORGBACKUP_HOST_LOCATION%", + "destination": "/mnt/borgbackup", + "writeable": true + }, + { + "source": "nextcloud_aio_elasticsearch", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_elasticsearch", + "writeable": true + }, + { + "source": "nextcloud_aio_redis", + "destination": "/mnt/redis", + "writeable": true + } + ], + "devices": [ + "/dev/fuse" + ], + "cap_add": [ + "SYS_ADMIN" + ], + "apparmor_unconfined": true + } + ] +} diff --git a/nextcloud-aio/community-containers/borgbackup-viewer/readme.md b/nextcloud-aio/community-containers/borgbackup-viewer/readme.md new file mode 100644 index 0000000..dc3d580 --- /dev/null +++ b/nextcloud-aio/community-containers/borgbackup-viewer/readme.md @@ -0,0 +1,17 @@ +## Borgbackup Viewer +This container allows to view the local borg repository in a web session. It also allows you to restore files and folders from the backup by using desktop programs in a web browser. + +### Notes +- After adding and starting the container, you need to visit `https://ip.address.of.this.server:5801` in order to log in with the user `nextcloud` and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). +- Then, you should see a terminal. There type in `borg mount /mnt/borgbackup/borg /tmp/borg` to mount the backup archive at `/tmp/borg` inside the container. Afterwards type in `nautilus /tmp/borg` which will show a file explorer and allows you to see all the files. You can then copy files and folders back to their initial mountpoints inside `/nextcloud_aio_volumes/`, `/host_mounts/` and `/docker_volumes/`. ⚠️ Be very carefully while doing that as can break your instance! +- After you are done with the operation, click on the terminal in the background and press `[CTRL]+[c]` multiple times to close any open application. Then run `umount /tmp/borg` to unmount the mountpoint correctly. +- You can also delete specific archives by running `borg list`, delete a specific archive e.g. via `borg delete --stats --progress "::20220223_174237-nextcloud-aio"` and compact the archives via `borg compact`. After doing so, make sure to update the backup archives list in the AIO interface! You can do so by clicking on the `Check backup integrity` button or `Create backup` button. +- ⚠️ After you are done doing your operations, remove the container for better security again from the stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers#how-to-remove-containers-from-aios-stack +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-borgbackup-viewer + +### Maintainer +https://github.com/szaimen + diff --git a/nextcloud-aio/community-containers/caddy/caddy.json b/nextcloud-aio/community-containers/caddy/caddy.json new file mode 100644 index 0000000..0e78175 --- /dev/null +++ b/nextcloud-aio/community-containers/caddy/caddy.json @@ -0,0 +1,56 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-caddy", + "display_name": "Caddy with geoblocking", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy", + "image": "ghcr.io/szaimen/aio-caddy", + "image_tag": "v2", + "internal_port": "443", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "443", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "443", + "protocol": "udp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "NC_DOMAIN=%NC_DOMAIN%", + "APACHE_PORT=%APACHE_PORT%", + "NEXTCLOUD_EXPORTER_CADDY_PASSWORD=%NEXTCLOUD_EXPORTER_CADDY_PASSWORD%" + ], + "volumes": [ + { + "source": "nextcloud_aio_caddy", + "destination": "/data", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/nextcloud", + "writeable": false + } + ], + "secrets": [ + "NEXTCLOUD_EXPORTER_CADDY_PASSWORD" + ], + "aio_variables": [ + "apache_ip_binding=@INTERNAL", + "apache_port=11000" + ], + "nextcloud_exec_commands": [ + "mkdir '/mnt/ncdata/admin/files/nextcloud-aio-caddy'", + "touch '/mnt/ncdata/admin/files/nextcloud-aio-caddy/allowed-countries.txt'", + "echo 'Scanning nextcloud-aio-caddy folder for admin user...'", + "php /var/www/html/occ files:scan --path='/admin/files/nextcloud-aio-caddy'" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/caddy/readme.md b/nextcloud-aio/community-containers/caddy/readme.md new file mode 100644 index 0000000..56984d5 --- /dev/null +++ b/nextcloud-aio/community-containers/caddy/readme.md @@ -0,0 +1,22 @@ +## Caddy with geoblocking +This container bundles caddy and auto-configures it for you. It also covers [vaultwarden](https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden) by listening on `bw.$NC_DOMAIN`, if installed. It also covers [stalwart](https://github.com/nextcloud/all-in-one/tree/main/community-containers/stalwart) by listening on `mail.$NC_DOMAIN`, if installed. It also covers [jellyfin](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin) by listening on `media.$NC_DOMAIN`, if installed. It also covers [lldap](https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap) by listening on `ldap.$NC_DOMAIN`, if installed. It also covers [nocodb](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nocodb) by listening on `tables.$NC_DOMAIN`, if installed. It also covers [jellyseerr](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr) by listening on `requests.$NC_DOMAIN`, if installed. It also covers [nextcloud-exporter](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nextcloud-exporter) by listening on `metrics.$NC_DOMAIN`, if installed. + +### Notes +- This container is incompatible with the [npmplus](https://github.com/nextcloud/all-in-one/tree/main/community-containers/npmplus) community container. So make sure that you do not enable both at the same time! +- Make sure that no other service is using port 443 on your host as otherwise the containers will fail to start. You can check this with `sudo netstat -tulpn | grep 443` before installing AIO. +- If you want to use this with [vaultwarden](https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden), make sure that you point `bw.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for vaultwarden. +- If you want to use this with [stalwart](https://github.com/nextcloud/all-in-one/tree/main/community-containers/stalwart), make sure that you point `mail.your-nc-domain.com` to your server using an A, AAAA or CNAME record so that caddy can get a certificate automatically for stalwart. +- If you want to use this with [jellyfin](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin), make sure that you point `media.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for jellyfin. +- If you want to use this with [lldap](https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap), make sure that you point `ldap.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for lldap. +- If you want to use this with [nocodb](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nocodb), make sure that you point `tables.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for nocodb. +- If you want to use this with [jellyseerr](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr), make sure that you point `requests.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for jellyseerr. +- If you want to use this with [nextcloud-exporter](https://github.com/nextcloud/all-in-one/tree/main/community-containers/nextcloud-exporter), make sure that you point `metrics.your-nc-domain.com` to your server using a cname record so that caddy can get a certificate automatically for nextcloud-exporter. +- After the container was started the first time, you should see a new `nextcloud-aio-caddy` folder and inside there an `allowed-countries.txt` file when you open the files app with the default `admin` user. In there you can adjust the allowed country codes for caddy by adding them to the first line, e.g. `IT FR` would allow access from italy and france. Private ip-ranges are always allowed. Additionally, in order to activate this config, you need to get an account at https://dev.maxmind.com/geoip/geolite2-free-geolocation-data and download the `GeoLite2-Country.mmdb` and upload it with this exact name into the `nextcloud-aio-caddy` folder. Afterwards restart all containers from the AIO interface and your new config should be active! +- You can add your own Caddy configurations in `/data/caddy-imports/` inside the Caddy container (`sudo docker exec -it nextcloud-aio-caddy bash`). These will be imported on container startup. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-caddy + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/calcardbackup/calcardbackup.json b/nextcloud-aio/community-containers/calcardbackup/calcardbackup.json new file mode 100644 index 0000000..738ca2f --- /dev/null +++ b/nextcloud-aio/community-containers/calcardbackup/calcardbackup.json @@ -0,0 +1,37 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-calcardbackup", + "display_name": "Calendar and contacts backup", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/calcardbackup", + "image": "waja/calcardbackup", + "image_tag": "latest", + "restart": "unless-stopped", + "environment": [ + "CRON_TIME=0 0 * * *", + "INIT_BACKUP=yes", + "BACKUP_DIR=/backup", + "NC_DIR=/nextcloud", + "NC_HOST=%NC_DOMAIN%", + "DB_HOST=nextcloud-aio-database", + "DB_PORT=5432", + "CALCARD_OPTS=-ltm 5" + ], + "volumes": [ + { + "source": "nextcloud_aio_calcardbackup", + "destination": "/backup", + "writeable": true + }, + { + "source": "nextcloud_aio_nextcloud", + "destination": "/nextcloud", + "writeable": false + } + ], + "backup_volumes": [ + "nextcloud_aio_calcardbackup" + ] + } + ] +} \ No newline at end of file diff --git a/nextcloud-aio/community-containers/calcardbackup/readme.md b/nextcloud-aio/community-containers/calcardbackup/readme.md new file mode 100644 index 0000000..0bb04a3 --- /dev/null +++ b/nextcloud-aio/community-containers/calcardbackup/readme.md @@ -0,0 +1,15 @@ +## calcardbackup +This container packages calcardbackup which is a tool that exports calendars and addressbooks from Nextcloud to .ics and .vcf files and saves them to a compressed file. + +### Notes +- Backups will be created at 00:00 CEST every day. Make sure that this does not conflict with the configured daily backups inside AIO. +- All the exports will be included in AIOs backup solution +- You can find the exports in the nextcloud_aio_calcardbackup volume +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/waja/docker-calcardbackup + +### Maintainer +https://github.com/pailloM + diff --git a/nextcloud-aio/community-containers/container-management/container-management.json b/nextcloud-aio/community-containers/container-management/container-management.json new file mode 100644 index 0000000..9563139 --- /dev/null +++ b/nextcloud-aio/community-containers/container-management/container-management.json @@ -0,0 +1,41 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-container-management", + "display_name": "Container Management", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management", + "image": "ghcr.io/szaimen/aio-container-management", + "image_tag": "v1", + "internal_port": "5804", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "5804", + "protocol": "tcp" + } + ], + "volumes": [ + { + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", + "writeable": false + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "SECURE_CONNECTION=1", + "WEB_AUTHENTICATION=1", + "USER_ID=0", + "GROUP_ID=0", + "WEB_AUTHENTICATION_USERNAME=container-management", + "WEB_AUTHENTICATION_PASSWORD=%CONTAINER_MANAGEMENT_PASSWORD%", + "WEB_LISTENING_PORT=5804" + ], + "secrets": [ + "CONTAINER_MANAGEMENT_PASSWORD" + ], + "ui_secret": "CONTAINER_MANAGEMENT_PASSWORD" + } + ] +} diff --git a/nextcloud-aio/community-containers/container-management/readme.md b/nextcloud-aio/community-containers/container-management/readme.md new file mode 100644 index 0000000..e8c1731 --- /dev/null +++ b/nextcloud-aio/community-containers/container-management/readme.md @@ -0,0 +1,15 @@ +## Container-Management +This container allows to manage insides of other containers via a GUI inside a Web session by allowing to run docker commands from inside this container. + +### Notes +- After adding and starting the container, you need to visit `https://ip.address.of.this.server:5804` in order to log in with the user `container-management` and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). +- Then, you should see a terminal. There you can use any docker command. ⚠️ Be very carefully while doing that as can break your instance! +- There are also some pre-made scripts that make configuring some of the community containers easier. For example scripts for [LLDAP](https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap) and [Facerecognition](https://github.com/nextcloud/all-in-one/tree/main/community-containers/facerecognition). +- ⚠️ After you are done doing your operations, remove the container for better security again from the stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers#how-to-remove-containers-from-aios-stack +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-container-management + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/dlna/dlna.json b/nextcloud-aio/community-containers/dlna/dlna.json new file mode 100644 index 0000000..ac37e4e --- /dev/null +++ b/nextcloud-aio/community-containers/dlna/dlna.json @@ -0,0 +1,39 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-dlna", + "display_name": "DLNA", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/dlna", + "image": "thanek/nextcloud-dlna", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "depends_on": [ + "nextcloud-aio-database" + ], + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "NC_PORT=443", + "NEXTCLOUD_DLNA_SERVER_PORT=9999", + "NEXTCLOUD_DLNA_FRIENDLY_NAME=nextcloud-aio", + "NEXTCLOUD_DATA_DIR=/data", + "NEXTCLOUD_DB_TYPE=postgres", + "NEXTCLOUD_DB_HOST=%AIO_DATABASE_HOST%", + "NEXTCLOUD_DB_PORT=5432", + "NEXTCLOUD_DB_NAME=nextcloud_database", + "NEXTCLOUD_DB_USER=oc_nextcloud", + "NEXTCLOUD_DB_PASS=%DATABASE_PASSWORD%" + ], + "secrets": [ + "DATABASE_PASSWORD" + ], + "volumes": [ + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/data", + "writeable": false + } + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/dlna/readme.md b/nextcloud-aio/community-containers/dlna/readme.md new file mode 100644 index 0000000..47502dc --- /dev/null +++ b/nextcloud-aio/community-containers/dlna/readme.md @@ -0,0 +1,14 @@ +## DLNA server +This container bundles DLNA server for your Nextcloud files to be accessible by the clients in your local network. Simply run the container and look for a new media server `nextcloud-aio` in your local network. + +### Notes +- This container will work only if the Nextcloud installation is in your home network, it is not suitable for installations on remote servers. +- If you have a firewall like ufw configured, you might need to open at least port 9999 TCP and 1900 UDP first in order to make it work. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/thanek/nextcloud-dlna + +### Maintainer +https://github.com/thanek + diff --git a/nextcloud-aio/community-containers/facerecognition/facerecognition.json b/nextcloud-aio/community-containers/facerecognition/facerecognition.json new file mode 100644 index 0000000..d97d5c0 --- /dev/null +++ b/nextcloud-aio/community-containers/facerecognition/facerecognition.json @@ -0,0 +1,40 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-facerecognition", + "display_name": "Computing container for facerecognition", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/facerecognition", + "image": "matiasdelellis/facerecognition-external-model", + "image_tag": "v1", + "internal_port": "5000", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "API_KEY=%FACERECOGNITION_API_KEY%", + "FACE_MODEL=3" + ], + "aio_variables": [ + "nextcloud_memory_limit=2048M" + ], + "secrets": [ + "FACERECOGNITION_API_KEY" + ], + "enable_nvidia_gpu": false, + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install facerecognition", + "php /var/www/html/occ app:enable facerecognition", + "php /var/www/html/occ config:system:set facerecognition.external_model_url --value nextcloud-aio-facerecognition:5000", + "php /var/www/html/occ config:system:set facerecognition.external_model_api_key --value %FACERECOGNITION_API_KEY%", + "php /var/www/html/occ face:setup -m 5", + "php /var/www/html/occ face:setup -M 1G", + "php /var/www/html/occ config:app:set facerecognition analysis_image_area --value 4320000", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 0 --value image/jpeg", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 1 --value image/png", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 2 --value image/heic", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 3 --value image/tiff", + "php /var/www/html/occ config:system:set enabledFaceRecognitionMimetype 4 --value image/webp", + "php /var/www/html/occ face:background_job --defer-clustering &" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/facerecognition/readme.md b/nextcloud-aio/community-containers/facerecognition/readme.md new file mode 100644 index 0000000..474ed1e --- /dev/null +++ b/nextcloud-aio/community-containers/facerecognition/readme.md @@ -0,0 +1,34 @@ +## Facerecognition +This container bundles the external model of facerecognition and auto-configures it for you. + +### Notes +- This container needs imaginary in order to analyze modern file format images. Make sure to enable imaginary in the AIO interface before adding this container. +- The image analysis is currently set to fixed value of `1G`. See [this](https://github.com/search?q=repo%3Anextcloud%2Fall-in-one+1G+path%3A%2F%5Ecommunity-containers%5C%2Ffacerecognition%5C%2F%2F&type=code) +- Facerecognition is by default disabled for all users. If you want to enable facerecognition for all users, you can run the following commands before adding this container:
+**Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management. This script below can be run from inside the container-management container via `bash /facerecognition.sh`. + ```bash + # Go into the container + sudo docker exec --user www-data -it nextcloud-aio-nextcloud bash + ``` + Now inside the container: + ```bash + NC_USERS_NEW=$(php occ user:list | sed 's|^ - ||g' | sed 's|:.*||') + mapfile -t NC_USERS_NEW <<< "$NC_USERS_NEW" + for user in "${NC_USERS_NEW[@]}" + do + php occ user:setting "$user" facerecognition full_image_scan_done false + php occ user:setting "$user" facerecognition enabled true + done + + # Exit the container shell + exit + ``` +- If facerecognition shall analyze shared files & folders (`sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:app:set facerecognition handle_shared_files --value true`), groupfolders (`sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:app:set facerecognition handle_group_files --value true`) and/or external storages (`sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:app:set facerecognition handle_external_files --value true`) in Nextcloud, you need to enable support for it manually first by running the mentioned commands before adding this container. See https://github.com/matiasdelellis/facerecognition/wiki/Settings#hidden-settings for further notes on each of these settings.
+**Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/matiasdelellis/facerecognition-external-model + +### Maintainer +https://github.com/matiasdelellis diff --git a/nextcloud-aio/community-containers/fail2ban/fail2ban.json b/nextcloud-aio/community-containers/fail2ban/fail2ban.json new file mode 100644 index 0000000..78bf0a8 --- /dev/null +++ b/nextcloud-aio/community-containers/fail2ban/fail2ban.json @@ -0,0 +1,42 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-fail2ban", + "display_name": "Fail2ban", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban", + "image": "ghcr.io/szaimen/aio-fail2ban", + "image_tag": "v1", + "internal_port": "host", + "restart": "unless-stopped", + "cap_add": [ + "NET_ADMIN", + "NET_RAW" + ], + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_nextcloud", + "destination": "/nextcloud", + "writeable": false + }, + { + "source": "nextcloud_aio_vaultwarden_logs", + "destination": "/vaultwarden", + "writeable": false + }, + { + "source": "nextcloud_aio_jellyfin", + "destination": "/jellyfin", + "writeable": false + }, + { + "source": "nextcloud_aio_jellyseerr", + "destination": "/jellyseerr", + "writeable": false + } + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/fail2ban/readme.md b/nextcloud-aio/community-containers/fail2ban/readme.md new file mode 100644 index 0000000..28ab21e --- /dev/null +++ b/nextcloud-aio/community-containers/fail2ban/readme.md @@ -0,0 +1,14 @@ +## Fail2ban +This container bundles fail2ban and auto-configures it for you in order to block ip-addresses automatically. It also covers https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden, https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin, and https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr, if installed. + +### Notes +- If you get an error like `"ip6tables v1.8.9 (legacy): can't initialize ip6tables table filter': Table does not exist (do you need to insmod?)"`, you need to enable ip6tables on your host via `sudo modprobe ip6table_filter`. +- If you get an error like `stderr: 'iptables: No chain/target/match by that name.'` and `stderr: 'ip6tables: No chain/target/match by that name.'`, you need to follow https://github.com/szaimen/aio-fail2ban/issues/9#issuecomment-2026898790 in order to resolve this. +- You can unban ip addresses like so for example: `docker exec -it nextcloud-aio-fail2ban fail2ban-client set nextcloud unbanip 203.113.167.162`. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-fail2ban + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/helloworld/helloworld.json b/nextcloud-aio/community-containers/helloworld/helloworld.json new file mode 100644 index 0000000..fed1000 --- /dev/null +++ b/nextcloud-aio/community-containers/helloworld/helloworld.json @@ -0,0 +1,12 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-helloworld", + "display_name": "Hello world", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/helloworld", + "image": "ghcr.io/docjyj/aio-helloworld", + "image_tag": "%AIO_CHANNEL%", + "restart": "unless-stopped" + } + ] +} diff --git a/nextcloud-aio/community-containers/helloworld/readme.md b/nextcloud-aio/community-containers/helloworld/readme.md new file mode 100644 index 0000000..83c557a --- /dev/null +++ b/nextcloud-aio/community-containers/helloworld/readme.md @@ -0,0 +1,8 @@ +## Hello World +This container is a template for creating a community container. + +### Repository +https://github.com/docjyj/aio-helloworld + +### Maintainer +https://github.com/docjyj diff --git a/nextcloud-aio/community-containers/jellyfin/jellyfin.json b/nextcloud-aio/community-containers/jellyfin/jellyfin.json new file mode 100644 index 0000000..f084091 --- /dev/null +++ b/nextcloud-aio/community-containers/jellyfin/jellyfin.json @@ -0,0 +1,40 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-jellyfin", + "display_name": "Jellyfin", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin", + "image": "jellyfin/jellyfin", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_jellyfin", + "destination": "/config", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/media", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", + "writeable": true + } + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "backup_volumes": [ + "nextcloud_aio_jellyfin" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/jellyfin/readme.md b/nextcloud-aio/community-containers/jellyfin/readme.md new file mode 100644 index 0000000..2a78bc1 --- /dev/null +++ b/nextcloud-aio/community-containers/jellyfin/readme.md @@ -0,0 +1,20 @@ +## Jellyfin +This container bundles Jellyfin and auto-configures it for you. + +### Notes +- This container is incompatible with the [Plex](https://github.com/nextcloud/all-in-one/tree/main/community-containers/plex) community container. So make sure that you do not enable both at the same time! +- After adding and starting the container, you can directly visit http://ip.address.of.server:8096/ and access your new Jellyfin instance! +- This container should usually only be run in home networks as it exposes unencrypted services like DLNA by default which can be disabld via the web interface though. +- In order to access your Jellyfin outside the local network, you have to set up your own reverse proxy. You can set up a reverse proxy following [these instructions](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) and [Jellyfin's networking documentation](https://jellyfin.org/docs/general/networking/#running-jellyfin-behind-a-reverse-proxy), OR use the [Caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container that will automatically configure `media.$NC_DOMAIN` to redirect to your Jellyfin. +- ⚠️ After the initial start, Jellyfin shows a configuration page to set up the root password, etc. **Be careful to initialize your Jellyfin before adding the DNS record.** +- If you have a firewall like ufw configured, you might need to open all Jellyfin ports in there first in order to make it work. Especially port 8096 is important! +- If you want to secure the installation with fail2ban, you might want to check out https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban +- The data of Jellyfin will be automatically included in AIO's backup solution! +- See [here](https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers) how to add it to the AIO stack. + + +### Repository +https://github.com/jellyfin/jellyfin + +### Maintainer +https://github.com/airopi diff --git a/nextcloud-aio/community-containers/jellyseerr/jellyseerr.json b/nextcloud-aio/community-containers/jellyseerr/jellyseerr.json new file mode 100644 index 0000000..64472a8 --- /dev/null +++ b/nextcloud-aio/community-containers/jellyseerr/jellyseerr.json @@ -0,0 +1,35 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-jellyseerr", + "display_name": "Jellyseerr", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyseerr", + "image": "fallenbagel/jellyseerr", + "image_tag": "latest", + "internal_port": "5055", + "restart": "unless-stopped", + "init": false, + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "5055", + "protocol": "tcp" + } + ], + "environment": [ + "PORT=5055", + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_jellyseerr", + "destination": "/app/config", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_jellyseerr" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/jellyseerr/readme.md b/nextcloud-aio/community-containers/jellyseerr/readme.md new file mode 100644 index 0000000..0d8e049 --- /dev/null +++ b/nextcloud-aio/community-containers/jellyseerr/readme.md @@ -0,0 +1,16 @@ +## Jellyseerr +This container bundles Jellyseerr and auto-configures it for you. + +### Notes +- This container is only intended to be used inside home networks as it uses http for its management page by default. +- After adding and starting the container, you can directly visit `http://ip.address.of.server:5055` and access your new Jellyseerr instance, which can be used to manage Plex, Jellyfin, and Emby. +- In order to access your Jellyseerr outside the local network, you have to set up your own reverse proxy. You can set up a reverse proxy following [these instructions](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) and [Jellyseerr's reverse proxy documentation.](https://docs.jellyseerr.dev/extending-jellyseerr/reverse-proxy), OR use the Caddy community container that will automatically configure requests.$NC_DOMAIN to redirect to your Jellyseerr. Note that it is recommended to [enable CSRF protection in Jellyseerr](https://docs.jellyseerr.dev/using-jellyseerr/settings/general#enable-csrf-protection) for added security if you plan to use Jellyseerr outside the local network, but make sure to read up on it and understand the caveats first. +- If you want to secure the installation with fail2ban, you might want to check out https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban. Note that [enabling the proxy support option in Jellyseerr](https://docs.jellyseerr.dev/using-jellyseerr/settings/general#enable-proxy-support) is required for this to work properly. +- The config of Jellyseerr will be automatically included in AIO's backup solution! +- See [here](https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers) how to add it to the AIO stack. + +### Repository +https://github.com/Fallenbagel/jellyseerr + +### Maintainer +https://github.com/Anvil5465 diff --git a/nextcloud-aio/community-containers/languagetool/languagetool.json b/nextcloud-aio/community-containers/languagetool/languagetool.json new file mode 100644 index 0000000..03eb6b3 --- /dev/null +++ b/nextcloud-aio/community-containers/languagetool/languagetool.json @@ -0,0 +1,16 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-languagetool", + "display_name": "LanguageTool for Collabora", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/languagetool", + "image": "erikvl87/languagetool", + "image_tag": "latest", + "internal_port": "8010", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/languagetool/readme.md b/nextcloud-aio/community-containers/languagetool/readme.md new file mode 100644 index 0000000..4c2ca98 --- /dev/null +++ b/nextcloud-aio/community-containers/languagetool/readme.md @@ -0,0 +1,13 @@ +## LanguageTool for Collabora +This container bundles a LanguageTool for Collabora which adds spell checking functionality to Collabora. + +### Notes +- Make sure to have collabora enabled via the AIO interface +- After adding this container via the AIO Interface, while all containers are still stopped, you need to scroll down to the `Additional Collabora options` section and enter `--o:languagetool.enabled=true --o:languagetool.base_url=http://nextcloud-aio-languagetool:8010/v2`. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/Erikvl87/docker-languagetool + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/libretranslate/libretranslate.json b/nextcloud-aio/community-containers/libretranslate/libretranslate.json new file mode 100644 index 0000000..dad8d00 --- /dev/null +++ b/nextcloud-aio/community-containers/libretranslate/libretranslate.json @@ -0,0 +1,34 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-libretranslate", + "display_name": "LibreTranslate (deprecated)", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/libretranslate", + "image": "ghcr.io/szaimen/aio-libretranslate", + "image_tag": "v1", + "internal_port": "5000", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_libretranslate_db", + "destination": "/app/db", + "writeable": true + }, + { + "source": "nextcloud_aio_libretranslate_models", + "destination": "/home/libretranslate/.local", + "writeable": true + } + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install integration_libretranslate", + "php /var/www/html/occ app:enable integration_libretranslate", + "php /var/www/html/occ config:app:set integration_libretranslate host --value='http://nextcloud-aio-libretranslate'", + "php /var/www/html/occ config:app:set integration_libretranslate port --value='5000'" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/libretranslate/readme.md b/nextcloud-aio/community-containers/libretranslate/readme.md new file mode 100644 index 0000000..f9893f3 --- /dev/null +++ b/nextcloud-aio/community-containers/libretranslate/readme.md @@ -0,0 +1,22 @@ +## LibreTranslate +This container bundles LibreTranslate and auto-configures it for you. + +> [!WARNING] +> The LibreTranslate container and app is deprecated! +> Please use the [translate2 app](https://apps.nextcloud.com/apps/translate2) instead. +> You can activate it by first enabling the Docker-Socket-Proxy in the AIO-interface and then heading over to `https://your-nc-domain.com/settings/apps/tools` and installing and enabling the `Local Machine Translation` app. + +### Notes +- After the initial startup is done, you might want to change the default language to translate from and to via: +```bash +# Adjust the values `en` and `de` in commands below accordingly +sudo docker exec --user www-data nextcloud-aio-nextcloud php occ config:app:set integration_libretranslate from_lang --value="en" +sudo docker exec --user www-data nextcloud-aio-nextcloud php occ config:app:set integration_libretranslate to_lang --value="de" +``` +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-libretranslate + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/lldap/lldap.json b/nextcloud-aio/community-containers/lldap/lldap.json new file mode 100644 index 0000000..32f8e7e --- /dev/null +++ b/nextcloud-aio/community-containers/lldap/lldap.json @@ -0,0 +1,47 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-lldap", + "display_name": "Light LDAP implementation", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap", + "image": "lldap/lldap", + "image_tag": "v0-alpine", + "internal_port": "17170", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "17170", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "UID=65534", + "GID=65534", + "LLDAP_JWT_SECRET=%LLDAP_JWT_SECRET%", + "LLDAP_LDAP_USER_PASS=%LLDAP_LDAP_USER_PASS%", + "LLDAP_LDAP_BASE_DN=%NC_BASE_DN%" + ], + "secrets": [ + "LLDAP_JWT_SECRET", + "LLDAP_LDAP_USER_PASS" + ], + "ui_secret": "LLDAP_LDAP_USER_PASS", + "volumes": [ + { + "source": "nextcloud_aio_lldap", + "destination": "/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_lldap" + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install user_ldap", + "php /var/www/html/occ app:enable user_ldap" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/lldap/readme.md b/nextcloud-aio/community-containers/lldap/readme.md new file mode 100644 index 0000000..586aea9 --- /dev/null +++ b/nextcloud-aio/community-containers/lldap/readme.md @@ -0,0 +1,91 @@ +## Light LDAP server +This container bundles LLDAP server and auto-configures your Nextcloud instance for you. + +### Notes +- In order to access your LLDAP web interface outside the local network, you have to set up your own reverse proxy. You can set up a reverse proxy following [these instructions](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) OR use the [Caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container that will automatically configure `ldap.$NC_DOMAIN` to redirect to your Lldap. You need to point the reverse proxy at port 17170 of this server. +- After adding and starting the container, you can log in to the lldap web interface by using the username `admin` and the secret that you can see next to the container in the AIO interface. +- To configure Nextcloud, you can use the generic configuration proposed below. +- For advanced configurations, see how to configure a client with lldap https://github.com/lldap/lldap#client-configuration +- Also, see how Nextcloud's LDAP application works https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Generic Nextcloud LDAP config +Functionality with this configuration: +- User and group management. +- Login via username (or email) and password. +- Profile picture sync. +- Synchronization of administrator accounts (via the lldap_admin group). + +> For simplicity, this configuration is done via the command line (don't worry, it's very simple). + +First, you need to retrieve the LLDAP admin password that you can see next to the container in the AIO interface. There you can configure smtp first and then invite users via mail. + +Now go into the Nextcloud container:
+**Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management. This script below can be run from inside the container-management container via `bash /lldap.sh`. +```bash +sudo docker exec --user www-data -it nextcloud-aio-nextcloud bash +``` +Now inside the container: +```bash +# Get Base +BASE_DN="dc=${NC_DOMAIN//./,dc=}" + +# Create a new empty ldap config +CONF_NAME=$(php /var/www/html/occ ldap:create-empty-config -p) + +# Check that the base DN matches your domain and retrieve your configuration name +echo "Base DN: '$BASE_DN', Config name: '$CONF_NAME'" + +# Set the ldap password +php /var/www/html/occ ldap:set-config $CONF_NAME ldapAgentPassword "" + +# Set the ldap config: Host and connection +php /var/www/html/occ ldap:set-config $CONF_NAME ldapAdminGroup lldap_admin +php /var/www/html/occ ldap:set-config $CONF_NAME ldapAgentName "cn=admin,ou=people,$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapBase "$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapHost "ldap://nextcloud-aio-lldap" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapPort 3890 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapTLS 0 +php /var/www/html/occ ldap:set-config $CONF_NAME turnOnPasswordChange 0 + +# Set the ldap config: Users +php /var/www/html/occ ldap:set-config $CONF_NAME ldapBaseUsers "ou=people,$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapEmailAttribute mail +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGidNumber gidNumber +php /var/www/html/occ ldap:set-config $CONF_NAME ldapLoginFilter "(&(|(objectclass=person))(|(uid=%uid)(|(mailPrimaryAddress=%uid)(mail=%uid))))" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapLoginFilterEmail 1 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapLoginFilterUsername 1 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserAvatarRule default +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserDisplayName cn +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserFilter "(|(objectclass=person))" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserFilterMode 0 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapUserFilterObjectclass person + +# Set the ldap config: Groups +php /var/www/html/occ ldap:set-config $CONF_NAME ldapBaseGroups "ou=groups,$BASE_DN" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupDisplayName cn +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupFilter "(&(|(objectclass=groupOfUniqueNames)))" +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupFilterMode 0 +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupFilterObjectclass groupOfUniqueNames +php /var/www/html/occ ldap:set-config $CONF_NAME ldapGroupMemberAssocAttr uniqueMember +php /var/www/html/occ ldap:set-config $CONF_NAME useMemberOfToDetectMembership 1 + +# Optional : Check the configuration +#php /var/www/html/occ ldap:show-config $CONF_NAME + +# Test the ldap config +php /var/www/html/occ ldap:test-config $CONF_NAME + +# Enable ldap config +php /var/www/html/occ ldap:set-config $CONF_NAME ldapConfigurationActive 1 + +# Exit the container shell +exit +``` +It's done ! All you have to do is go to the Nextcloud administration interface to see the magic of LDAP. + +### Repository +https://github.com/lldap/lldap + +### Maintainer +https://github.com/docjyj diff --git a/nextcloud-aio/community-containers/local-ai/local-ai.json b/nextcloud-aio/community-containers/local-ai/local-ai.json new file mode 100644 index 0000000..8e2aedb --- /dev/null +++ b/nextcloud-aio/community-containers/local-ai/local-ai.json @@ -0,0 +1,46 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-local-ai", + "display_name": "Local AI", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/local-ai", + "image": "ghcr.io/szaimen/aio-local-ai", + "image_tag": "v2", + "internal_port": "8080", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "MODELS_PATH=/models" + ], + "volumes": [ + { + "source": "nextcloud_aio_localai_models", + "destination": "/models", + "writeable": true + }, + { + "source": "nextcloud_aio_localai_images", + "destination": "/tmp/generated/images/", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/nextcloud", + "writeable": false + } + ], + "enable_nvidia_gpu": false, + "nextcloud_exec_commands": [ + "mkdir '/mnt/ncdata/admin/files/nextcloud-aio-local-ai'", + "touch '/mnt/ncdata/admin/files/nextcloud-aio-local-ai/models.yaml'", + "echo 'Scanning nextcloud-aio-local-ai folder for admin user...'", + "php /var/www/html/occ files:scan --path='/admin/files/nextcloud-aio-local-ai'", + "php /var/www/html/occ app:install integration_openai", + "php /var/www/html/occ app:enable integration_openai", + "php /var/www/html/occ config:app:set integration_openai url --value http://nextcloud-aio-local-ai:8080", + "php /var/www/html/occ app:install assistant", + "php /var/www/html/occ app:enable assistant" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/local-ai/readme.md b/nextcloud-aio/community-containers/local-ai/readme.md new file mode 100644 index 0000000..2ab0599 --- /dev/null +++ b/nextcloud-aio/community-containers/local-ai/readme.md @@ -0,0 +1,21 @@ +## Local AI +This container bundles Local AI and auto-configures it for you. + +### Notes +- Make sure to have enough storage space available. This container alone needs ~7GB storage. Every model that you add to `models.yaml` will of course use additional space which adds up quite fast. +- After the container was started the first time, you should see a new `nextcloud-aio-local-ai` folder when you open the files app with the default `admin` user. In there you should see a `models.yaml` config file. You can now add models in there. Please refer [here](https://github.com/mudler/LocalAI/blob/master/gallery/index.yaml) where you can get further urls that you can put in there. Afterwards restart all containers from the AIO interface and the models should automatically get downloaded by the local-ai container and activated. +- Example for content of `models.yaml` (if you add all of them, it takes around 10GB additional space): +```yaml +# Stable Diffusion in NCNN with c++, supported txt2img and img2img +- url: github:mudler/LocalAI/blob/master/gallery/stablediffusion.yaml + name: Stable_diffusion +``` +- To make it work, you first need to browse `https://your-nc-domain.com/settings/admin/ai` and enable or disable specific features for your models in the openAI settings. Afterwards using the Nextcloud Assistant should work. +- See [this guide](https://github.com/nextcloud/all-in-one/discussions/5430) for how to improve AI task pickup speed +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-local-ai + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/makemkv/makemkv.json b/nextcloud-aio/community-containers/makemkv/makemkv.json new file mode 100644 index 0000000..22132cb --- /dev/null +++ b/nextcloud-aio/community-containers/makemkv/makemkv.json @@ -0,0 +1,59 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-makekv", + "display_name": "MakeMKV", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/makemkv", + "image": "jlesage/makemkv", + "image_tag": "latest", + "internal_port": "5802", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "5802", + "protocol": "tcp" + } + ], + "volumes": [ + { + "source": "nextcloud_aio_makemkv", + "destination": "/config", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/storage", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "/output", + "writeable": true + }, + { + "source": "/dev", + "destination": "/dev", + "writeable": false + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "SECURE_CONNECTION=1", + "WEB_AUTHENTICATION=1", + "USER_ID=33", + "GROUP_ID=33", + "WEB_AUTHENTICATION_USERNAME=makemkv", + "WEB_AUTHENTICATION_PASSWORD=%MAKEMKV_PASSWORD%", + "WEB_LISTENING_PORT=5802" + ], + "secrets": [ + "MAKEMKV_PASSWORD" + ], + "ui_secret": "MAKEMKV_PASSWORD", + "backup_volumes": [ + "nextcloud_aio_makemkv" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/makemkv/readme.md b/nextcloud-aio/community-containers/makemkv/readme.md new file mode 100644 index 0000000..ed9ce04 --- /dev/null +++ b/nextcloud-aio/community-containers/makemkv/readme.md @@ -0,0 +1,20 @@ +## MakeMKV +This container bundles MakeMKV and auto-configures it for you. + +### Notes +- This container should only be run in home networks +- ⚠️ This container mounts all devices from the host inside the container in order to be able to access the external DVD/Blu-ray drives which is a security issue. However no better solution was found for the time being. +- This container only works on Linux and not on Docker-Desktop. +- This container requires the [`NEXTCLOUD_MOUNT` variable in AIO to be set](https://github.com/nextcloud/all-in-one?tab=readme-ov-file#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host). Otherwise the output will not be saved correctly.. +- After adding and starting the container, you need to visit `https://internal.ip.of.server:5802` in order to log in with the `makemkv` user and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). +- After the first login, you can adjust the `/output` directory in the MakeMKV settings to a subdirectory of the root of your chosen `NEXTCLOUD_MOUNT`. (by default `NEXTCLOUD_MOUNT` is mounted to `/output` inside the container. Thus all data is written to the root of it) +- The configured `NEXTCLOUD_DATADIR` is getting mounted to `/storage` inside the container. +- The config data of MakeMKV will be automatically included in AIOs backup solution! +- ⚠️ After you are done doing your operations, remove the container for better security again from the stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers#how-to-remove-containers-from-aios-stack +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/jlesage/docker-makemkv + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/memories/memories.json b/nextcloud-aio/community-containers/memories/memories.json new file mode 100644 index 0000000..cb84a72 --- /dev/null +++ b/nextcloud-aio/community-containers/memories/memories.json @@ -0,0 +1,39 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-memories", + "display_name": "Memories Transcoder", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/memories", + "image": "radialapps/go-vod", + "image_tag": "latest", + "internal_port": "47788", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "NEXTCLOUD_HOST=https://%NC_DOMAIN%" + ], + "volumes": [ + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/mnt/ncdata", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", + "writeable": false + } + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "nextcloud_exec_commands": [ + "php /var/www/html/occ app:install memories", + "php /var/www/html/occ app:enable memories", + "php /var/www/html/occ config:system:set memories.vod.external --value true --type bool", + "php /var/www/html/occ config:system:set memories.vod.connect --value nextcloud-aio-memories:47788" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/memories/readme.md b/nextcloud-aio/community-containers/memories/readme.md new file mode 100644 index 0000000..88a44c4 --- /dev/null +++ b/nextcloud-aio/community-containers/memories/readme.md @@ -0,0 +1,12 @@ +## Memories +This container bundles the hardware-transcoding container of memories and auto-configures it for you. + +### Notes +- In order to actually enable the hardware transcoding, you need to add the following flag to AIO apart from adding this container: https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/pulsejet/memories + +### Maintainer +https://github.com/pulsejet diff --git a/nextcloud-aio/community-containers/minio/minio.json b/nextcloud-aio/community-containers/minio/minio.json new file mode 100644 index 0000000..2403f21 --- /dev/null +++ b/nextcloud-aio/community-containers/minio/minio.json @@ -0,0 +1,41 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-minio", + "image_tag": "v1", + "display_name": "Minio S3 Storage", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/minio", + "image": "ghcr.io/szaimen/aio-minio", + "internal_port": "9000", + "environment": [ + "MINIO_ROOT_USER=nextcloud", + "MINIO_ROOT_PASSWORD=%MINIO_ROOT_PASSWORD%" + ], + "secrets": [ + "MINIO_ROOT_PASSWORD" + ], + "volumes": [ + { + "source": "nextcloud_aio_minio", + "destination": "/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_minio" + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ config:system:set objectstore class --value 'OC\\Files\\ObjectStore\\S3'", + "php /var/www/html/occ config:system:set objectstore arguments autocreate --value true --type bool", + "php /var/www/html/occ config:system:set objectstore arguments use_path_style --value true --type bool", + "php /var/www/html/occ config:system:set objectstore arguments use_ssl --value false --type bool", + "php /var/www/html/occ config:system:set objectstore arguments region --value ''", + "php /var/www/html/occ config:system:set objectstore arguments bucket --value nextcloud", + "php /var/www/html/occ config:system:set objectstore arguments key --value nextcloud", + "php /var/www/html/occ config:system:set objectstore arguments secret --value %MINIO_ROOT_PASSWORD%", + "php /var/www/html/occ config:system:set objectstore arguments port --value 9000", + "php /var/www/html/occ config:system:set objectstore arguments hostname --value nextcloud-aio-minio" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/minio/readme.md b/nextcloud-aio/community-containers/minio/readme.md new file mode 100644 index 0000000..be41d5b --- /dev/null +++ b/nextcloud-aio/community-containers/minio/readme.md @@ -0,0 +1,18 @@ +## Minio +This container bundles minio s3 storage and auto-configures it for you. + +>[!WARNING] +> Enabling this container will remove access to all the files formerly written to the data directory. +> So only enable this on a clean instance directly after installing AIO. +> All additional users that are added via Nextcloud afterwards are going to work correctly. +> Also, after enabling and using it, make sure to not disable the container as you cannot migrate from s3 to local storage anymore and s3 is a critical part of your infrastructure from then on. + +### Notes +- The data of Minio will be automatically included in AIOs backup solution! +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-minio + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/nextcloud-exporter/nextcloud-exporter.json b/nextcloud-aio/community-containers/nextcloud-exporter/nextcloud-exporter.json new file mode 100644 index 0000000..f9159a3 --- /dev/null +++ b/nextcloud-aio/community-containers/nextcloud-exporter/nextcloud-exporter.json @@ -0,0 +1,35 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-nextcloud-exporter", + "display_name": "Prometheus Nextcloud Exporter", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/nextcloud-exporter", + "image": "ghcr.io/xperimental/nextcloud-exporter", + "image_tag": "0.8.0", + "internal_port": "9205", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "127.0.0.1", + "port_number": "9205", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "NEXTCLOUD_SERVER=https://%NC_DOMAIN%", + "NEXTCLOUD_AUTH_TOKEN=%NEXTCLOUD_EXPORTER_TOKEN%", + "NEXTCLOUD_LISTEN_ADDRESS=0.0.0.0:9205", + "NEXTCLOUD_TIMEOUT=5s" + ], + "ui_secret": "NEXTCLOUD_EXPORTER_CADDY_PASSWORD", + "secrets": [ + "NEXTCLOUD_EXPORTER_TOKEN", + "NEXTCLOUD_EXPORTER_CADDY_PASSWORD" + ], + "nextcloud_exec_commands": [ + "php /var/www/html/occ config:app:set serverinfo token --value %NEXTCLOUD_EXPORTER_TOKEN%" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/nextcloud-exporter/readme.md b/nextcloud-aio/community-containers/nextcloud-exporter/readme.md new file mode 100644 index 0000000..3efa625 --- /dev/null +++ b/nextcloud-aio/community-containers/nextcloud-exporter/readme.md @@ -0,0 +1,72 @@ +## Prometheus Nextcloud Exporter + +A Prometheus exporter that collects metrics from your Nextcloud instance for monitoring and alerting. + +### How to install + +See the [Community Containers documentation](https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers) for instructions on how to install this in your Nextcloud All-in-One setup. + +### Security & Access + +**Important:** This container is configured to bind only to `127.0.0.1` (localhost) for security reasons. Prometheus exporters typically don't include authentication, so direct network exposure is not recommended. + +#### Access Options + +1. **With Caddy Container (Recommended)**: If you also install the [Caddy community container](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy), it will automatically configure secure HTTPS access to your metrics with authentication at `metrics.your-domain.com` + + **Getting Authentication Credentials**: + - **Username**: Always `metrics` + - **Password**: After deploying the nextcloud-exporter container, the automatically generated password will be displayed in the AIO interface. Look for it in the container section below the container name "Prometheus Nextcloud Exporter". + +2. **Custom Reverse Proxy**: Set up your own reverse proxy (nginx, Apache, etc.) to provide HTTPS and authentication. See configuration guides: + - [NGINX Authentication](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) + [Reverse Proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) + - [Apache Authentication](https://httpd.apache.org/docs/2.4/howto/auth.html) + [Reverse Proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html) + - [Traefik BasicAuth](https://doc.traefik.io/traefik/middlewares/http/basicauth/) + - [Prometheus Security Best Practices](https://prometheus.io/docs/operating/security/) + +3. **Direct Local Access**: Access metrics directly from the server at `http://127.0.0.1:9205/metrics` (no authentication) + +### What it monitors +- User activity (active users hourly, daily) +- File counts and storage usage +- System health and database size +- App statistics and update availability +- Nextcloud performance metrics + +### Prometheus Configuration + +For **local server access** (if Prometheus runs on the same server): +```yaml +scrape_configs: + - job_name: 'nextcloud' + scrape_interval: 90s + static_configs: + - targets: ['127.0.0.1:9205'] + metrics_path: /metrics + scheme: http +``` + +For **Caddy integration** (secure external access): +```yaml +scrape_configs: + - job_name: 'nextcloud' + scrape_interval: 90s + static_configs: + - targets: ['metrics.your-domain.com'] + metrics_path: / + scheme: https + basic_auth: + username: 'metrics' + password: 'your-generated-password' +``` + +### Visualization + +Compatible with Grafana for creating monitoring dashboards: +- Pre-built dashboard available: [Grafana Dashboard #20716](https://grafana.com/grafana/dashboards/20716-nextcloud/) + +### Repository +https://github.com/xperimental/nextcloud-exporter + +### Maintainer +https://github.com/grotax diff --git a/nextcloud-aio/community-containers/nocodb/nocodb.json b/nextcloud-aio/community-containers/nocodb/nocodb.json new file mode 100644 index 0000000..7ef4cc5 --- /dev/null +++ b/nextcloud-aio/community-containers/nocodb/nocodb.json @@ -0,0 +1,44 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-nocodb", + "display_name": "NocoDB", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/nocodb", + "image": "nocodb/nocodb", + "image_tag": "latest", + "internal_port": "10028", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "10028", + "protocol": "tcp" + } + ], + "environment": [ + "NC_AUTH_JWT_SECRET=%NOCODB_JWT_SECRET%", + "NC_PUBLIC_URL=https://tables.%NC_DOMAIN%/", + "NC_DASHBOARD_URL=/", + "NC_ADMIN_EMAIL=admin@noco.db", + "NC_ADMIN_PASS=%NOCODB_USER_PASS%", + "PORT=10028", + "NC_DISABLE_TELE=true" + ], + "secrets": [ + "NOCODB_JWT_SECRET", + "NOCODB_USER_PASS" + ], + "ui_secret": "NOCODB_USER_PASS", + "volumes": [ + { + "source": "nextcloud_aio_nocodb", + "destination": "/usr/app/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_nocodb" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/nocodb/readme.md b/nextcloud-aio/community-containers/nocodb/readme.md new file mode 100644 index 0000000..4c1281b --- /dev/null +++ b/nextcloud-aio/community-containers/nocodb/readme.md @@ -0,0 +1,28 @@ +> [!NOTE] +> This container is there to compensate for the lack of functionality in Nextcloud Tables. +> +> When Nextcloud Tables V2 is released, I will stop checking for updates, and will no longer fix any potential issues. +> +> Some missing functionality in Nextcloud Tables: +> - Multiple view layout (Gantt, Kanban, Calendar...) +> - Field (Person, Tag, File...) +> - See more here https://github.com/nextcloud/tables/issues/103 + +## NocoDb server +This container bundles NocoDb without synchronization with Nextcloud. + +This is an alternative of **Airtable**. + +### Notes +- You need to configure a reverse proxy in order to run this container since nocodb needs a dedicated (sub)domain! For that, you might have a look at https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy. +- Currently, only `tables.$NC_DOMAIN` is supported as subdomain! So if Nextcloud is using `your-domain.com`, nocodb will use `tables.your-domain.com`. +- The data of NocoDb will be automatically included in AIOs backup solution! +- After adding and starting the container, you can log in to the web interface at `https://tables.$NC_DOMAIN/#/signin` with the username `admin@noco.db` and the password that you can see in the AIO interface next to the container. +- See https://docs.nocodb.com/ for usage of NocoDb +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/nocodb/nocodb + +### Maintainer +https://github.com/docjyJ diff --git a/nextcloud-aio/community-containers/npmplus/npmplus.json b/nextcloud-aio/community-containers/npmplus/npmplus.json new file mode 100644 index 0000000..4b666c0 --- /dev/null +++ b/nextcloud-aio/community-containers/npmplus/npmplus.json @@ -0,0 +1,32 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-npmplus", + "display_name": "NPMplus", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/npmplus", + "image": "ghcr.io/zoeyvid/npmplus", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "NC_AIO=true", + "NC_DOMAIN=%NC_DOMAIN%" + ], + "volumes": [ + { + "source": "nextcloud_aio_npmplus", + "destination": "/data", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_npmplus" + ], + "aio_variables": [ + "apache_ip_binding=127.0.0.1", + "apache_port=11000" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/npmplus/readme.md b/nextcloud-aio/community-containers/npmplus/readme.md new file mode 100644 index 0000000..a71b4af --- /dev/null +++ b/nextcloud-aio/community-containers/npmplus/readme.md @@ -0,0 +1,20 @@ +## NPMplus +This container contains a fork of the Nginx Proxy Manager, which is a WebUI for nginx. It will also automatically create a config and cert for AIO. + +### Notes +- This container is incompatible with the [caddy](https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy) community container. So make sure that you do not enable both at the same time! +- Make sure that no other service is using port `443 (tcp/upd)` or `81 (tcp)` on your host as otherwise the containers will fail to start. You can check this with `sudo netstat -tulpn | grep "443\|81"` before installing AIO. +- Please change the default login data first, after you can read inside the logs that the default config for AIO is created and there are no errors. +- After the container was started the first time, please check the logs for errors. Then you can open NPMplus on `https://:81` and change the password. +- The default password is `iArhP1j7p1P6TA92FA2FMbbUGYqwcYzxC4AVEe12Wbi94FY9gNN62aKyF1shrvG4NycjjX9KfmDQiwkLZH1ZDR9xMjiG2QmoHXi` and the default email is `admin@example.org` +- If you want to use NPMplus behind a domain and outside localhost just create a new proxy host inside the NPMplus which proxies to `https`, `127.0.0.1` and port `81` - all other settings should be the same as for the AIO host. +- If you want to set env options from this [compose.yaml](https://github.com/ZoeyVid/NPMplus/blob/develop/compose.yaml), please set them inside the `.env` file which you can find in the `nextcloud_aio_npmplus` volume **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management +- The data (certs, configs, etc.) of NPMplus will be automatically included in AIOs backup solution! +- **Important:** you always need to enable https for your hosts, since `DISABLE_HTTP` is set to true by default +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository and Documentation +https://github.com/ZoeyVid/NPMplus + +### Maintainer +https://github.com/Zoey2936 diff --git a/nextcloud-aio/community-containers/pi-hole/pi-hole.json b/nextcloud-aio/community-containers/pi-hole/pi-hole.json new file mode 100644 index 0000000..2cecb9e --- /dev/null +++ b/nextcloud-aio/community-containers/pi-hole/pi-hole.json @@ -0,0 +1,57 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-pihole", + "display_name": "Pi-hole", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/pi-hole", + "image": "pihole/pihole", + "image_tag": "latest", + "internal_port": "8573", + "restart": "unless-stopped", + "init": false, + "ports": [ + { + "ip_binding": "", + "port_number": "53", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "53", + "protocol": "udp" + }, + { + "ip_binding": "", + "port_number": "8573", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "FTLCONF_webserver_api_password=%PIHOLE_WEBPASSWORD%", + "FTLCONF_dns_listeningMode=all", + "FTLCONF_webserver_port=8573" + ], + "volumes": [ + { + "source": "nextcloud_aio_pihole", + "destination": "/etc/pihole", + "writeable": true + }, + { + "source": "nextcloud_aio_pihole_dnsmasq", + "destination": "/etc/dnsmasq.d", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_pihole", + "nextcloud_aio_pihole_dnsmasq" + ], + "ui_secret": "PIHOLE_WEBPASSWORD", + "secrets": [ + "PIHOLE_WEBPASSWORD" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/pi-hole/readme.md b/nextcloud-aio/community-containers/pi-hole/readme.md new file mode 100644 index 0000000..7254f58 --- /dev/null +++ b/nextcloud-aio/community-containers/pi-hole/readme.md @@ -0,0 +1,18 @@ +## Pi-hole +This container bundles pi-hole and auto-configures it for you. + +### Notes +- You should not run this container on a public VPS! It is only intended to run in home networks! +- Make sure that no dns server is already running by checking with `sudo netstat -tulpn | grep 53`. Otherwise the container will not be able to start! +- The DHCP functionality of Pi-hole has been disabled! +- The data of pi-hole will be automatically included in AIOs backup solution! +- After adding and starting the container, you can visit `http://ip.address.of.this.server:8573/admin` in order to log in with the admin key that you can see next to the container in the AIO interface. There you can configure the pi-hole setup. Also you can add local dns records. +- You can configure your home network now to use pi-hole as its dns server by configuring your router. +- Additionally, you can configure the docker daemon to use that by editing `/etc/docker/daemon.json` and adding ` { "dns" : [ "ip.address.of.this.server" , "8.8.8.8" ] } `. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/pi-hole/docker-pi-hole + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/plex/plex.json b/nextcloud-aio/community-containers/plex/plex.json new file mode 100644 index 0000000..5fc8712 --- /dev/null +++ b/nextcloud-aio/community-containers/plex/plex.json @@ -0,0 +1,42 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-plex", + "display_name": "Plex", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/plex", + "image": "plexinc/pms-docker", + "image_tag": "latest", + "internal_port": "host", + "restart": "unless-stopped", + "environment": [ + "TZ=%TIMEZONE%", + "PLEX_UID=33", + "PLEX_GID=33" + ], + "volumes": [ + { + "source": "nextcloud_aio_plex", + "destination": "/config", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/data", + "writeable": false + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", + "writeable": false + } + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "backup_volumes": [ + "nextcloud_aio_plex" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/plex/readme.md b/nextcloud-aio/community-containers/plex/readme.md new file mode 100644 index 0000000..7f8434a --- /dev/null +++ b/nextcloud-aio/community-containers/plex/readme.md @@ -0,0 +1,17 @@ +## Plex +This container bundles Plex and auto-configures it for you. + +### Notes +- This container is incompatible with the [Jellyfin](https://github.com/nextcloud/all-in-one/tree/main/community-containers/jellyfin) community container. So make sure that you do not enable both at the same time! +- This is not working on arm64 since Plex does only provide x64 docker images. +- This container should usually only be run in home networks as it exposes unencrypted services like DLNA by default which can be disabld via the web interface though. +- If you have a firewall like ufw configured, you might need to open all Plex ports in there first in order to make it work. Especially port 32400 is important! +- After adding and starting the container, you need to visit http://ip.address.of.server:32400/manage in order to claim your server with a plex account +- The data of Plex will be automatically included in AIOs backup solution! +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/plexinc/pms-docker + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/readme.md b/nextcloud-aio/community-containers/readme.md new file mode 100644 index 0000000..1631a95 --- /dev/null +++ b/nextcloud-aio/community-containers/readme.md @@ -0,0 +1,20 @@ +# Community containers +This directory features containers that are built for AIO which allows to add additional functionality very easily. + +## Disclaimers +All containers that are in this directory are community maintained so the responsibility is on the community to keep them updated and secure. There is no guarantee that this will be the case in the future. + +## How to use this? +Starting with v11 of AIO, the management of Community Containers is done via the AIO interface (it is the last section in the AIO interface, so only visible if you scroll down). +⚠️⚠️⚠️ Please review the folder for documentation on each of the containers before adding them! Not reviewing the documentation for each of them first might break starting the AIO containers because e.g. fail2ban only works on Linux and not on Docker Desktop! **Hint:** If the containers where running already, in order to actually start the added container, you need to click on `Stop containers` and the `Update and start containers` in order to actually start it. + +## How to add containers? +Simply submit a PR by creating a new folder in this directory: https://github.com/nextcloud/all-in-one/tree/main/community-containers with the name of your container. It must include a json file with the same name and with correct syntax and a readme.md with additional information. You might get inspired by caddy, fail2ban, local-ai, libretranslate, plex, pi-hole or vaultwarden (subfolders in this directory). For a full-blown example of the json file, see https://github.com/nextcloud/all-in-one/blob/main/php/containers.json. The json-schema that it validates against can be found here: https://github.com/nextcloud/all-in-one/blob/main/php/containers-schema.json. + +### Is there a list of ideas for new community containers? +Yes, see [this list](https://github.com/nextcloud/all-in-one/issues/5251) for already existing ideas for new community containers. Feel free to pick one up and add it to this folder by following the instructions above. + +## How to remove containers from AIOs stack? +You can remove containers now via the web interface. + +After removing the containers, there might be some data left on your server that you might want to remove. You can get rid of the data by first running `sudo docker rm nextcloud-aio-container1`, (adjust `container1` accordingly) per community-container that you removed. Then run `sudo docker image prune -a` in order to remove all images that are not used anymore. As last step you can get rid of persistent data of these containers that is stored in volumes. You can check if there is some by running `sudo docker volume ls` and look for any volume that matches the ones that you removed. If so, you can remove them with `sudo docker volume rm nextcloud_aio_volume-id` (of course you need to adjust the `volume-id`). **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management diff --git a/nextcloud-aio/community-containers/scrutiny/readme.md b/nextcloud-aio/community-containers/scrutiny/readme.md new file mode 100644 index 0000000..dc972d4 --- /dev/null +++ b/nextcloud-aio/community-containers/scrutiny/readme.md @@ -0,0 +1,16 @@ +## Scrutiny +This container bundles Scrutiny which is a frontend for SMART stats and auto-configures it for you. + +### Notes +- This container should only be run in home networks +- ⚠️ This container mounts all devices from the host inside the container in order to be able to access the drives and smartctl stats which is a security issue. However no better solution was found for the time being. +- This container only works on Linux and not on Docker-Desktop. +- After adding and starting the container, you need to visit `http://internal.ip.of.server:8000` which will show the dashboard for your drives. +- It currently does not support sending notifications as no good solution was found yet that makes this possible. See https://github.com/szaimen/aio-scrutiny/issues/3 +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-scrutiny + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/scrutiny/scrutiny.json b/nextcloud-aio/community-containers/scrutiny/scrutiny.json new file mode 100644 index 0000000..4b36829 --- /dev/null +++ b/nextcloud-aio/community-containers/scrutiny/scrutiny.json @@ -0,0 +1,56 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-scrutiny", + "display_name": "Scrutiny", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/scrutiny", + "image": "ghcr.io/szaimen/aio-scrutiny", + "image_tag": "v1", + "internal_port": "8000", + "init": false, + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "8000", + "protocol": "tcp" + } + ], + "cap_add": [ + "SYS_RAWIO", + "SYS_ADMIN" + ], + "environment": [ + "TZ=%TIMEZONE%", + "SCRUTINY_WEB_LISTEN_PORT=8000", + "COLLECTOR_API_ENDPOINT=http://127.0.0.1:8000" + ], + "volumes": [ + { + "source": "nextcloud_aio_scrutiny", + "destination": "/opt/scrutiny/config", + "writeable": true + }, + { + "source": "nextcloud_aio_scrutiny_db", + "destination": "/opt/scrutiny/influxdb", + "writeable": true + }, + { + "source": "/run/udev", + "destination": "/run/udev", + "writeable": false + }, + { + "source": "/dev", + "destination": "/dev", + "writeable": false + } + ], + "backup_volumes": [ + "nextcloud_aio_scrutiny", + "nextcloud_aio_scrutiny_db" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/smbserver/readme.md b/nextcloud-aio/community-containers/smbserver/readme.md new file mode 100644 index 0000000..9886f4b --- /dev/null +++ b/nextcloud-aio/community-containers/smbserver/readme.md @@ -0,0 +1,15 @@ +## SMB-server +This container bundles an SMB-server and allows to configure it via a graphical shell script. + +### Notes +- This container should only be run in home networks +- This container currently only works on amd64. See https://github.com/szaimen/aio-smbserver/issues/3 +- After adding and starting the container, you need to visit `https://internal.ip.of.server:5803` in order to log in with the `smbserver` user and the password that you can see next to the container in the AIO interface. (The web page uses a self-signed certificate, so you need to accept the warning). Then type in `bash /smbserver.sh` and you will see a graphical UI for configuring the smb-server interactively. +- The config data of SMB-server will be automatically included in AIOs backup solution! +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/szaimen/aio-smbserver/ + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/smbserver/smbserver.json b/nextcloud-aio/community-containers/smbserver/smbserver.json new file mode 100644 index 0000000..d095eb7 --- /dev/null +++ b/nextcloud-aio/community-containers/smbserver/smbserver.json @@ -0,0 +1,60 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-smbserver", + "display_name": "SMB-server", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/smbserver", + "image": "ghcr.io/szaimen/aio-smbserver", + "image_tag": "v1", + "internal_port": "5803", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "5803", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "445", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "139", + "protocol": "tcp" + } + ], + "volumes": [ + { + "source": "nextcloud_aio_smbserver", + "destination": "/smbserver", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/mnt/ncdata", + "writeable": true + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "/mnt", + "writeable": true + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "WEB_AUTHENTICATION_USERNAME=smbserver", + "WEB_AUTHENTICATION_PASSWORD=%SMBSERVER_PASSWORD%", + "WEB_LISTENING_PORT=5803" + ], + "secrets": [ + "SMBSERVER_PASSWORD" + ], + "ui_secret": "SMBSERVER_PASSWORD", + "backup_volumes": [ + "nextcloud_aio_smbserver" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/stalwart/readme.md b/nextcloud-aio/community-containers/stalwart/readme.md new file mode 100644 index 0000000..b34f04d --- /dev/null +++ b/nextcloud-aio/community-containers/stalwart/readme.md @@ -0,0 +1,22 @@ +> [!CAUTION] +> Be aware that the mail server is the most difficult service to deploy. +> +> Do not use this feature as a main mail server or without a redundancy system and without knowledge. + +## Stalwart mail server +This container bundles stalwart mail server and auto-configures it for you. + +### Notes +Documentation is available on the container repository. +This documentation is regularly updated and is intended to be as simple and detailed as possible. +Thanks for all your feedback! + +- See https://github.com/docjyJ/aio-stalwart#getting-started for getting start with this container. +- See https://stalw.art/docs/faq for further faq and docs on the project +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/docjyj/aio-stalwart + +### Maintainer +https://github.com/docjyj diff --git a/nextcloud-aio/community-containers/stalwart/stalwart.json b/nextcloud-aio/community-containers/stalwart/stalwart.json new file mode 100644 index 0000000..b9a4809 --- /dev/null +++ b/nextcloud-aio/community-containers/stalwart/stalwart.json @@ -0,0 +1,75 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-stalwart", + "display_name": "Stalwart", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/stalwart", + "image": "ghcr.io/docjyj/aio-stalwart", + "image_tag": "v3", + "internal_port": "10003", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "", + "port_number": "25", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "143", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "465", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "587", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "993", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "4190", + "protocol": "tcp" + }, + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "10003", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "NC_DOMAIN=%NC_DOMAIN%", + "STALWART_USER_PASS=%STALWART_USER_PASS%", + "CLAMAV_ENABLED=%CLAMAV_ENABLED%" + ], + "secrets": [ + "STALWART_USER_PASS" + ], + "ui_secret": "STALWART_USER_PASS", + "volumes": [ + { + "source": "nextcloud_aio_stalwart", + "destination": "/opt/stalwart-mail", + "writeable": true + }, + { + "source": "nextcloud_aio_caddy", + "destination": "/caddy", + "writeable": false + } + ], + "backup_volumes": [ + "nextcloud_aio_stalwart" + ] + } + ] +} diff --git a/nextcloud-aio/community-containers/vaultwarden/readme.md b/nextcloud-aio/community-containers/vaultwarden/readme.md new file mode 100644 index 0000000..81f3701 --- /dev/null +++ b/nextcloud-aio/community-containers/vaultwarden/readme.md @@ -0,0 +1,17 @@ +## Vaultwarden +This container bundles vaultwarden and auto-configures it for you. + +### Notes +- You need to configure a reverse proxy in order to run this container since vaultwarden needs a dedicated (sub)domain! For that, you might have a look at https://github.com/nextcloud/all-in-one/tree/main/community-containers/caddy or follow https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md and https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples. You need to point the reverse proxy at port 8812 of this server. +- Currently, only `bw.$NC_DOMAIN` is supported as subdomain! So if Nextcloud is using `your-domain.com`, vaultwarden will use `bw.your-domain.com`. The reverse proxy and domain must be configured accordingly! +- If you want to secure the installation with fail2ban, you might want to check out https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban +- The data of Vaultwarden will be automatically included in AIOs backup solution! +- After adding and starting the container, you need to visit `https://bw.your-domain.com/admin` in order to log in with the admin key that you can see next to the container in the AIO interface. There you can configure smtp first and then invite users via mail. After this is done, you might disable the admin panel via the reverse proxy by blocking connections to the subdirectory. +- If using the caddy community container, the vaultwarden admin interface can be disabled by creating a `block-vaultwarden-admin` file in the `nextcloud-aio-caddy` folder when you open the Nextcloud files app with the default `admin` user. Afterwards restart all containers from the AIO interface and the admin interface should be disabled! You can unlock the admin interface by removing the file again and afterwards restarting the containers via the AIO interface. +- See https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers how to add it to the AIO stack + +### Repository +https://github.com/dani-garcia/vaultwarden + +### Maintainer +https://github.com/szaimen diff --git a/nextcloud-aio/community-containers/vaultwarden/vaultwarden.json b/nextcloud-aio/community-containers/vaultwarden/vaultwarden.json new file mode 100644 index 0000000..b94996f --- /dev/null +++ b/nextcloud-aio/community-containers/vaultwarden/vaultwarden.json @@ -0,0 +1,49 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-vaultwarden", + "display_name": "Vaultwarden", + "documentation": "https://github.com/nextcloud/all-in-one/tree/main/community-containers/vaultwarden", + "image": "ghcr.io/dani-garcia/vaultwarden", + "image_tag": "alpine", + "internal_port": "8812", + "restart": "unless-stopped", + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "8812", + "protocol": "tcp" + } + ], + "environment": [ + "TZ=%TIMEZONE%", + "ROCKET_PORT=8812", + "ADMIN_TOKEN=%VAULTWARDEN_ADMIN_TOKEN%", + "DOMAIN=https://bw.%NC_DOMAIN%", + "LOG_FILE=/logs/vaultwarden.log", + "LOG_LEVEL=warn", + "SIGNUPS_VERIFY=true", + "SIGNUPS_ALLOWED=false" + ], + "volumes": [ + { + "source": "nextcloud_aio_vaultwarden", + "destination": "/data", + "writeable": true + }, + { + "source": "nextcloud_aio_vaultwarden_logs", + "destination": "/logs", + "writeable": true + } + ], + "backup_volumes": [ + "nextcloud_aio_vaultwarden" + ], + "ui_secret": "VAULTWARDEN_ADMIN_TOKEN", + "secrets": [ + "VAULTWARDEN_ADMIN_TOKEN" + ] + } + ] +} diff --git a/nextcloud-aio/compose-example.yaml b/nextcloud-aio/compose-example.yaml new file mode 100644 index 0000000..f202638 --- /dev/null +++ b/nextcloud-aio/compose-example.yaml @@ -0,0 +1,79 @@ +services: + nextcloud-aio-mastercontainer: + image: ghcr.io/nextcloud-releases/all-in-one:latest # This is the container image used. You can switch to ghcr.io/nextcloud-releases/all-in-one:beta if you want to help testing new releases. See https://github.com/nextcloud/all-in-one#how-to-switch-the-channel + init: true # This setting makes sure that signals from main process inside the container are correctly forwarded to children. See https://docs.docker.com/reference/compose-file/services/#init + restart: always # This makes sure that the container starts always together with the host OS. See https://docs.docker.com/reference/compose-file/services/#restart + container_name: nextcloud-aio-mastercontainer # This line is not allowed to be changed as otherwise AIO will not work correctly + volumes: + - nextcloud_aio_mastercontainer:/mnt/docker-aio-config # This line is not allowed to be changed as otherwise the built-in backup solution will not work + - /var/run/docker.sock:/var/run/docker.sock:ro # May be changed on macOS, Windows or docker rootless. See the applicable documentation. If adjusting, don't forget to also set 'WATCHTOWER_DOCKER_SOCKET_PATH'! + network_mode: bridge # This adds the container to the same network as docker run would do. Comment this line and uncomment the line below and the networks section at the end of the file if you want to define a custom MTU size for the docker network + # networks: ["nextcloud-aio"] + ports: + - 80:80 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + - 8080:8080 # This is the AIO interface, served via https and self-signed certificate. See https://github.com/nextcloud/all-in-one#explanation-of-used-ports + - 8443:8443 # Can be removed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # security_opt: ["label:disable"] # Is needed when using SELinux. See https://github.com/nextcloud/all-in-one#are-there-known-problems-when-selinux-is-enabled + # environment: # Is needed when using any of the options below + # AIO_DISABLE_BACKUP_SECTION: false # Setting this to true allows to hide the backup section in the AIO interface. See https://github.com/nextcloud/all-in-one#how-to-disable-the-backup-section + # APACHE_PORT: 11000 # Is needed when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # APACHE_IP_BINDING: 127.0.0.1 # Should be set when running behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) that is running on the same host. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # APACHE_ADDITIONAL_NETWORK: frontend_net # (Optional) Connect the apache container to an additional docker network. Needed when behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) running in a different docker network on same server. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md + # BORG_RETENTION_POLICY: --keep-within=7d --keep-weekly=4 --keep-monthly=6 # Allows to adjust borgs retention policy. See https://github.com/nextcloud/all-in-one#how-to-adjust-borgs-retention-policy + # COLLABORA_SECCOMP_DISABLED: false # Setting this to true allows to disable Collabora's Seccomp feature. See https://github.com/nextcloud/all-in-one#how-to-disable-collaboras-seccomp-feature + # FULLTEXTSEARCH_JAVA_OPTIONS: "-Xms1024M -Xmx1024M" # Allows to adjust the fulltextsearch java options. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-fulltextsearch-java-options + # NEXTCLOUD_DATADIR: /mnt/ncdata # Allows to set the host directory for Nextcloud's datadir. ⚠️⚠️⚠️ Warning: do not set or adjust this value after the initial Nextcloud installation is done! See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir + # NEXTCLOUD_MOUNT: /mnt/ # Allows the Nextcloud container to access the chosen directory on the host. See https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host + # NEXTCLOUD_UPLOAD_LIMIT: 16G # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud + # NEXTCLOUD_MAX_TIME: 3600 # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud + # NEXTCLOUD_MEMORY_LIMIT: 512M # Can be adjusted if you need more. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud + # NEXTCLOUD_TRUSTED_CACERTS_DIR: /path/to/my/cacerts # CA certificates in this directory will be trusted by the OS of the nextcloud container (Useful e.g. for LDAPS) See https://github.com/nextcloud/all-in-one#how-to-trust-user-defined-certification-authorities-ca + # NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes # Allows to modify the Nextcloud apps that are installed on starting AIO the first time. See https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup + # NEXTCLOUD_ADDITIONAL_APKS: imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-os-packages-permanently-to-the-nextcloud-container + # NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. See https://github.com/nextcloud/all-in-one#how-to-add-php-extensions-permanently-to-the-nextcloud-container + # NEXTCLOUD_ENABLE_DRI_DEVICE: true # This allows to enable the /dev/dri device for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if the '/dev/dri' device is present on the host! If it should not exist on your host, don't set this to true as otherwise the Nextcloud container will fail to start! See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud + # NEXTCLOUD_ENABLE_NVIDIA_GPU: true # This allows to enable the NVIDIA runtime and GPU access for containers that profit from it. ⚠️⚠️⚠️ Warning: this only works if an NVIDIA gpu is installed on the server. See https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud. + # NEXTCLOUD_KEEP_DISABLED_APPS: false # Setting this to true will keep Nextcloud apps that are disabled in the AIO interface and not uninstall them if they should be installed. See https://github.com/nextcloud/all-in-one#how-to-keep-disabled-apps + # SKIP_DOMAIN_VALIDATION: false # This should only be set to true if things are correctly configured. See https://github.com/nextcloud/all-in-one?tab=readme-ov-file#how-to-skip-the-domain-validation + # TALK_PORT: 3478 # This allows to adjust the port that the talk container is using which is exposed on the host. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port + # WATCHTOWER_DOCKER_SOCKET_PATH: /var/run/docker.sock # Needs to be specified if the docker socket on the host is not located in the default '/var/run/docker.sock'. Otherwise mastercontainer updates will fail. For macos it needs to be '/var/run/docker.sock' + +# # Optional: Caddy reverse proxy. See https://github.com/nextcloud/all-in-one/discussions/575 +# # Alternatively, use Tailscale if you don't have a domain yet. See https://github.com/nextcloud/all-in-one/discussions/6817 +# # Hint: You need to uncomment APACHE_PORT: 11000 above, adjust cloud.example.com to your domain and uncomment the necessary docker volumes at the bottom of this file in order to make it work +# # You can find further examples here: https://github.com/nextcloud/all-in-one/discussions/588 +# caddy: +# image: caddy:alpine +# restart: always +# container_name: caddy +# volumes: +# - caddy_certs:/certs +# - caddy_config:/config +# - caddy_data:/data +# - caddy_sites:/srv +# network_mode: "host" +# configs: +# - source: Caddyfile +# target: /etc/caddy/Caddyfile +# configs: +# Caddyfile: +# content: | +# # Adjust cloud.example.com to your domain below +# https://cloud.example.com:443 { +# reverse_proxy localhost:11000 +# } + +volumes: # If you want to store the data on a different drive, see https://github.com/nextcloud/all-in-one#how-to-store-the-filesinstallation-on-a-separate-drive + nextcloud_aio_mastercontainer: + name: nextcloud_aio_mastercontainer # This line is not allowed to be changed as otherwise the built-in backup solution will not work + # caddy_certs: + # caddy_config: + # caddy_data: + # caddy_sites: + +# # Adjust the MTU size of the docker network. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-mtu-size-of-the-docker-network +# networks: +# nextcloud-aio: +# name: nextcloud-aio +# driver_opts: +# com.docker.network.driver.mtu: 1440 diff --git a/nextcloud-aio/compose.yaml b/nextcloud-aio/compose.yaml new file mode 100644 index 0000000..e3ad73a --- /dev/null +++ b/nextcloud-aio/compose.yaml @@ -0,0 +1,48 @@ +# sheldon (cloud.delmar.bzh) +# https://github.com/nextcloud/all-in-one +--- +name: nextcloud-aio + +volumes: + nextcloud_aio_mastercontainer: + name: nextcloud_aio_mastercontainer + +networks: + nextcloud-aio: + name: nextcloud-aio + driver_opts: + com.docker.network.driver.mtu: 1440 + +services: + nextcloud-aio-mastercontainer: + image: ghcr.io/nextcloud-releases/all-in-one:latest + init: true + restart: always + container_name: nextcloud-aio-mastercontainer + volumes: + - nextcloud_aio_mastercontainer:/mnt/docker-aio-config + - /var/run/docker.sock:/var/run/docker.sock:ro + # network_mode: bridge + networks: ["nextcloud-aio"] + ports: + - 8080:8080 + # - 8443:8443 + environment: + AIO_DISABLE_BACKUP_SECTION: false + APACHE_PORT: 11000 + # APACHE_IP_BINDING: 127.0.0.1 + # APACHE_ADDITIONAL_NETWORK: frontend_net + BORG_RETENTION_POLICY: --keep-within=7d --keep-weekly=4 --keep-monthly=6 + FULLTEXTSEARCH_JAVA_OPTIONS: "-Xms1024M -Xmx1024M" + NEXTCLOUD_DATADIR: /mnt/data/ncdata + NEXTCLOUD_MOUNT: /mnt/data/ + NEXTCLOUD_UPLOAD_LIMIT: 16G + NEXTCLOUD_MAX_TIME: 3600 + NEXTCLOUD_MEMORY_LIMIT: 512M + # NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes deck draw.io forms gestion memories maps mail + NEXTCLOUD_ADDITIONAL_APKS: ffmpeg imagemagick libmagickcore-6.q16-6-extra nodejs npm + NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # php-imagick + NEXTCLOUD_ENABLE_DRI_DEVICE: true + NEXTCLOUD_KEEP_DISABLED_APPS: false + TALK_PORT: 3478 + diff --git a/nextcloud-aio/develop.md b/nextcloud-aio/develop.md new file mode 100644 index 0000000..6c5faf2 --- /dev/null +++ b/nextcloud-aio/develop.md @@ -0,0 +1,72 @@ +## Developer channel +If you want to switch to the develop channel, you simply stop and delete the mastercontainer and create a new one with a changed tag to develop: +```shell +sudo docker run \ +--init \ +--sig-proxy=false \ +--name nextcloud-aio-mastercontainer \ +--restart always \ +--publish 80:80 \ +--publish 8080:8080 \ +--publish 8443:8443 \ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ +--volume /var/run/docker.sock:/var/run/docker.sock:ro \ +ghcr.io/nextcloud-releases/all-in-one:develop +``` +And you are done :) +It will now also select the developer channel for all other containers automatically. + +## How to publish new releases? +Simply use https://github.com/nextcloud/all-in-one/issues/180 as template. + +## How to update existing instances to a new major Nextcloud version? +Simply use https://github.com/nextcloud/all-in-one/issues/6198 as template. + +## How to build new containers +Go to https://github.com/nextcloud-releases/all-in-one/actions/workflows/repo-sync.yml and run the workflow that will first sync the repo and then build new container that automatically get published to `develop` and `develop-arm64`. + +## How to test things correctly? +Before testing, make sure that at least the amd64 containers are built successfully by checking the last workflow here: https://github.com/nextcloud-releases/all-in-one/actions/workflows/build_images.yml. + +There is a testing-VM available for the maintainer of AIO that allows for some final testing before releasing new version. See [this](https://cloud.nextcloud.com/apps/collectives/Nextcloud%20Handbook/Technical/AIO%20testing%20VM?fileId=6350152) for details. + +Additionally, there are now E2E tests available that can be run via https://github.com/nextcloud/all-in-one/actions/workflows/playwright.yml + +## How to promote builds from develop to beta +1. Verify that no job is running here: https://github.com/nextcloud-releases/all-in-one/actions/workflows/build_images.yml +2. Go to https://github.com/nextcloud-releases/all-in-one/actions/workflows/promote-to-beta.yml, click on `Run workflow`. + +## Where to find the VPS and other builds? +This is documented here: https://github.com/nextcloud-releases/all-in-one/tree/main/.build + +## How to promote builds from beta to latest + +1. Verify that GitHub Services are running correctly: https://www.githubstatus.com/ +1. Verify that no job is running here: https://github.com/nextcloud-releases/all-in-one/actions/workflows/promote-to-beta.yml +1. Go to https://github.com/nextcloud-releases/all-in-one/actions/workflows/promote-to-latest.yml, click on `Run workflow`. + +## How to connect to the database? +Simply run `sudo docker exec -it nextcloud-aio-database psql -U oc_nextcloud nextcloud_database` and you should be in. + +## How to locally build and test changes to mastercontainer +1. Ensure you are on the developer channel per the instructions above. +1. Use the commands below from the project root to build the mastercontainer image: +``` +docker buildx build --file Containers/mastercontainer/Dockerfile --tag ghcr.io/nextcloud-releases/all-in-one:develop --load . +``` +1. Start a container with above built image. +1. Since the hash of a locally built image doesn't match the latest release mastercontainer, it prompts for a mandatory update. To temporarily bypass the update suffix `?bypass_mastercontainer_update` to the URL. Eg: `https://localhost:8080/containers?bypass_mastercontainer_update` + +## How to locally build and test changes to other containers using the bypass_container_update param +1. Ensure you are on the developer channel per the instructions above. +1. Use the commands below from the project root to build the container image: +``` +# For the "nextcloud" container +docker buildx build --file Containers/nextcloud/Dockerfile --tag ghcr.io/nextcloud-releases/aio-nextcloud:develop --load . + +# For all other containers +docker buildx build --file Containers/{container}/Dockerfile --tag ghcr.io/nextcloud-releases/aio-{container}:develop --load Containers/{container} +``` +1. Stop the containers using the AIO admin interface. +1. Reload the AIO admin interface with the param `bypass_container_update` to avoid overwriting your local changes, e.g. `https://localhost:8080/containers?bypass_container_update`. +1. Click "Start and update containers" and test your changes. Containers will not be updated, despite the button text. diff --git a/nextcloud-aio/docker-ipv6-support.md b/nextcloud-aio/docker-ipv6-support.md new file mode 100644 index 0000000..a784e7c --- /dev/null +++ b/nextcloud-aio/docker-ipv6-support.md @@ -0,0 +1,44 @@ +# IPv6-Support for Docker + +## Docker on Linux and Docker-rootless +First of all upgrade your docker installation to v27.0.1 or higher. +1. Then edit `/etc/docker/daemon.json` (or `~/.config/docker/daemon.json` in case of docker-rootless), add the below json: + +> [!WARNING] +> This will enable ipv6 for all new docker networks by default! You can alternatively create the `nextcloud-aio` network with ipv6 support by hand manually via docker network create or via compose.yaml. + +```json +{ + "default-network-opts": {"bridge":{"com.docker.network.enable_ipv6":"true"}} +} +``` + +And save the file. + +2. Reload the Docker configuration file. + +```console +sudo systemctl restart docker +``` + +3. Make sure that ipv6 is enabled for the internal `nextcloud-aio` network by running `sudo docker network inspect nextcloud-aio | grep EnableIPv6`. On a new instance, this command should return that it did not find a network with this name. Then you can run `sudo docker network create nextcloud-aio` in order to create the network with ipv6-support. However if it finds the network and its value `EnableIPv6` is set to false, make sure to follow https://github.com/nextcloud/all-in-one/discussions/4989 in order to recreate the network and enable ipv6 for it. + +## Docker Desktop (Windows and macOS) +First of all upgrade your docker desktop installation to v4.32.0 or higher. +Then, on Windows and macOS which use Docker Desktop, you need to go into the settings, and select `Docker Engine`. There you should see the currently used daemon.json file. + +1. You need to now adjust this json file: + +> [!WARNING] +> This will enable ipv6 for all new docker networks by default! You can alternatively create the `nextcloud-aio` network with ipv6 support by hand manually via docker network create or via compose.yaml. + +```json +"default-network-opts": {"bridge":{"com.docker.network.enable_ipv6":"true"}} +``` + +2. Add these values to the json and make sure to keep the other currently values and that you don't see `Unexpected token in JSON at position ...` before attempting to restart by clicking on `Apply & restart`. +3. Make sure that ipv6 is enabled for the internal `nextcloud-aio` network by running `sudo docker network inspect nextcloud-aio | grep EnableIPv6`. On a new instance, this command should return that it did not find a network with this name. Then you can run `sudo docker network create nextcloud-aio` in order to create the network with ipv6-support. However if it finds the network and its value `EnableIPv6` is set to false, make sure to follow https://github.com/nextcloud/all-in-one/discussions/4989 in order to recreate the network and enable ipv6 for it. + +--- + +**Note**: This is a copy of the original docker docs at https://docs.docker.com/config/daemon/ipv6/ which apparently are not correct. diff --git a/nextcloud-aio/docker-rootless.md b/nextcloud-aio/docker-rootless.md new file mode 100644 index 0000000..417b1eb --- /dev/null +++ b/nextcloud-aio/docker-rootless.md @@ -0,0 +1,39 @@ +# Docker rootless + +**Please note:** Due to a bug in Collabora is the Collabora container currently in rootless mode not working. See https://github.com/CollaboraOnline/online/issues/2800. In that case, you need to run a separate Collabora instance on your own if you want to use this feature. The following flag will be useful https://github.com/nextcloud/all-in-one#how-to-keep-disabled-apps. + +You can run AIO with docker rootless by following the steps below. + +0. If docker is already installed, you should consider disabling it first: (`sudo systemctl disable --now docker.service docker.socket`) +1. Install docker rootless by following the official documentation: https://docs.docker.com/engine/security/rootless/#install. The easiest way is installing it **Without packages** (`curl -fsSL https://get.docker.com/rootless | sh`). Further limitations, distribution specific hints, etc. are discussed on the same site. Also do not forget to enable the systemd service, which may not be enabled always by default. See https://docs.docker.com/engine/security/rootless/#usage. (`systemctl --user enable docker`) +1. If you need ipv6 support, you should enable it by following https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md. +1. Do not forget to set the mentioned environmental variables `PATH` and `DOCKER_HOST` and in best case add them to your `~/.bashrc` file as shown! +1. Also do not forget to run `loginctl enable-linger USERNAME` (and substitute USERNAME with the correct one) in order to make sure that user services are automatically started after every reboot. +1. Expose the privileged ports by following https://docs.docker.com/engine/security/rootless/#exposing-privileged-ports. (`sudo setcap cap_net_bind_service=ep $(which rootlesskit); systemctl --user restart docker`). If you require the correct source IP you must expose them via `/etc/sysctl.conf`, [see note below](#note-regarding-docker-network-driver). +1. Use the official AIO startup command but use `--volume $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock:ro` instead of `--volume /var/run/docker.sock:/var/run/docker.sock:ro` and also add `--env WATCHTOWER_DOCKER_SOCKET_PATH=$XDG_RUNTIME_DIR/docker.sock` to the initial container startup (which is needed for mastercontainer updates to work correctly). When you are using Portainer to deploy AIO, the variable `$XDG_RUNTIME_DIR` is not available. In this case, it is necessary to manually add the path (e.g. `/run/user/1000/docker.sock`) to the Docker compose file to replace the `$XDG_RUNTIME_DIR` variable. If you are not sure how to get the path, you can run on the host: `echo $XDG_RUNTIME_DIR`. +1. Now everything should work like without docker rootless. You can consider using docker-compose for this or running it behind a reverse proxy. Basically the only thing that needs to be adjusted always in the startup command or compose.yaml file (after installing docker rootles) are things that are mentioned in point 3. +1. ⚠️ **Important:** Please read through all notes below! + +### Note regarding sudo in the documentation +Almost all commands in this project's documentation use `sudo docker ...`. Since `sudo` is not needed in case of docker rootless, you simply remove `sudo` from the commands and they should work. + +### Note regarding permissions +All files outside the containers get created, written to and accessed as the user that is running the docker daemon or a subuid of it. So for the built-in backup to work you need to allow this user to write to the target directory. E.g. with `sudo chown -R USERNAME:GROUPNAME /mnt/backup`. The same applies when changing Nextcloud's datadir via NEXTCLOUD_DATADIR. E.g. `sudo chown -R USERNAME:GROUPNAME /mnt/ncdata`. When you want to use the NEXTCLOUD_MOUNT option for local external storage, you need to adjust the permissions of the chosen folders to be accessible/writeable by the userid `100032:100032` (if running `grep ^$(whoami): /etc/subuid` as the user that is running the docker daemon returns 100000 as first value). + + +### Note regarding docker network driver +By default rootless docker uses the `slirp4netns` IP driver and the `builtin` port driver. As mentioned in [the documentation](https://docs.docker.com/engine/security/rootless/#networking-errors), this combination doesn't provide "Source IP propagation". This means that Apache and Nextcloud will see all connections as coming from the docker gateway (e.g 172.19.0.1), which can lead to the Nextcloud brute force protection blocking all connection attempts. To expose the correct source IP, you will need to configure docker to also use `slirp4netns` as the port driver (see also [this guide](https://rootlesscontaine.rs/getting-started/docker/#changing-the-port-forwarder)). +As stated in the documentation, this change will likely lead to decreased network throughput. You should test this by trying to transfer a large file after completing your setup and revert back to the `builtin` port driver if the throughput is too slow. +* Add `net.ipv4.ip_unprivileged_port_start=80` to `/etc/sysctl.conf`. Editing this file requires root privileges. (using capabilities doesn't work here; see [this issue](https://github.com/rootless-containers/slirp4netns/issues/251#issuecomment-761415404)). +* Run `sudo sysctl --system` to propagate the change. +* Create `~/.config/systemd/user/docker.service.d/override.conf` + with the following content: + ``` + [Service] + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns" + Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" + ``` +* Restart the docker daemon + ``` + systemctl --user restart docker + ``` diff --git a/nextcloud-aio/local-instance.md b/nextcloud-aio/local-instance.md new file mode 100644 index 0000000..ad6970e --- /dev/null +++ b/nextcloud-aio/local-instance.md @@ -0,0 +1,31 @@ +# Local instance +It is possible due to several reasons that you do not want or cannot open Nextcloud to the public internet. Perhaps you were hoping to access AIO directly from an `ip.add.r.ess` (unsupported) or without a valid domain. However, AIO requires a valid certificate to work correctly. Below is discussed how you can achieve both: Having a valid certificate for Nextcloud and only using it locally. + +### Content +- [1. Tailscale](#1-tailscale) +- [2. The normal way](#2-the-normal-way) +- [3. Use the ACME DNS-challenge](#3-use-the-acme-dns-challenge) +- [4. Use Cloudflare](#4-use-cloudflare) +- [5. Buy a certificate and use that](#5-buy-a-certificate-and-use-that) + +## 1. Tailscale +This is the recommended way. For a reverse proxy example guide for Tailscale, see this guide by [@Perseus333](https://github.com/Perseus333): https://github.com/nextcloud/all-in-one/discussions/6817 + +## 2. The normal way +The normal way is the following: +1. Set up your domain correctly to point to your home network +1. Set up a reverse proxy by following the [reverse proxy documentation](./reverse-proxy.md) but only open port 80 (which is needed for the ACME challenge to work - however no real traffic will use this port). +1. Set up a local DNS-server like a pi-hole and configure it to be your local DNS-server for the whole network. Then in the Pi-hole interface, add a custom DNS-record for your domain and overwrite the A-record (and possibly the AAAA-record, too) to point to the private ip-address of your reverse proxy (see https://github.com/nextcloud/all-in-one#how-can-i-access-nextcloud-locally) +1. Enter the ip-address of your local dns-server in the daemon.json file for docker so that you are sure that all docker containers use the correct local dns-server. +1. Now, entering the domain in the AIO-interface should work as expected and should allow you to continue with the setup + +**Hint:** You may have a look at [this video](https://youtu.be/zk-y2wVkY4c) for a more complete but possibly outdated example. + +## 3. Use the ACME DNS-challenge +You can alternatively use the ACME DNS-challenge to get a valid certificate for Nextcloud. Here is described how to set it up: https://github.com/nextcloud/all-in-one#how-to-get-nextcloud-running-using-the-acme-dns-challenge + +## 4. Use Cloudflare +If you do not have any control over the network, you may think about using Cloudflare Tunnel to get a valid certificate for your Nextcloud. However it will be opened to the public internet then. See https://github.com/nextcloud/all-in-one#how-to-run-nextcloud-behind-a-cloudflare-tunnel how to set this up. + +## 5. Buy a certificate and use that +If none of the above ways work for you, you may simply buy a certificate from an issuer for your domain. You then download the certificate onto your server, configure AIO in [reverse proxy mode](./reverse-proxy.md) and use the certificate for your domain in your reverse proxy config. diff --git a/nextcloud-aio/manual-install/latest.yml b/nextcloud-aio/manual-install/latest.yml new file mode 100644 index 0000000..83bc1ef --- /dev/null +++ b/nextcloud-aio/manual-install/latest.yml @@ -0,0 +1,512 @@ +services: + nextcloud-aio-apache: + depends_on: + nextcloud-aio-onlyoffice: + condition: service_started + required: false + nextcloud-aio-collabora: + condition: service_started + required: false + nextcloud-aio-talk: + condition: service_started + required: false + nextcloud-aio-notify-push: + condition: service_started + required: false + nextcloud-aio-whiteboard: + condition: service_started + required: false + nextcloud-aio-nextcloud: + condition: service_started + required: false + image: ghcr.io/nextcloud-releases/aio-apache:latest + user: "33" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + ports: + - ${APACHE_IP_BINDING}:${APACHE_PORT}:${APACHE_PORT}/tcp + - ${APACHE_IP_BINDING}:${APACHE_PORT}:${APACHE_PORT}/udp + environment: + - NC_DOMAIN + - NEXTCLOUD_HOST=nextcloud-aio-nextcloud + - APACHE_HOST=nextcloud-aio-apache + - COLLABORA_HOST=nextcloud-aio-collabora + - TALK_HOST=nextcloud-aio-talk + - APACHE_PORT + - ONLYOFFICE_HOST=nextcloud-aio-onlyoffice + - TZ=${TIMEZONE} + - APACHE_MAX_SIZE + - APACHE_MAX_TIME=${NEXTCLOUD_MAX_TIME} + - NOTIFY_PUSH_HOST=nextcloud-aio-notify-push + - WHITEBOARD_HOST=nextcloud-aio-whiteboard + volumes: + - nextcloud_aio_nextcloud:/var/www/html:ro + - nextcloud_aio_apache:/mnt/data:rw + restart: unless-stopped + read_only: true + tmpfs: + - /var/log/supervisord + - /var/run/supervisord + - /usr/local/apache2/logs + - /tmp + - /home/www-data + cap_drop: + - NET_RAW + + nextcloud-aio-database: + image: ghcr.io/nextcloud-releases/aio-postgresql:latest + user: "999" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "5432" + volumes: + - nextcloud_aio_database:/var/lib/postgresql/data:rw + - nextcloud_aio_database_dump:/mnt/data:rw + environment: + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=nextcloud_database + - POSTGRES_USER=nextcloud + - TZ=${TIMEZONE} + - PGTZ=${TIMEZONE} + stop_grace_period: 1800s + restart: unless-stopped + shm_size: 268435456 + read_only: true + tmpfs: + - /var/run/postgresql + cap_drop: + - NET_RAW + + nextcloud-aio-nextcloud: + depends_on: + nextcloud-aio-database: + condition: service_started + required: false + nextcloud-aio-redis: + condition: service_started + required: false + nextcloud-aio-clamav: + condition: service_started + required: false + nextcloud-aio-fulltextsearch: + condition: service_started + required: false + nextcloud-aio-talk-recording: + condition: service_started + required: false + nextcloud-aio-imaginary: + condition: service_started + required: false + image: ghcr.io/nextcloud-releases/aio-nextcloud:latest + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "9000" + - "9001" + volumes: + - nextcloud_aio_nextcloud:/var/www/html:rw + - ${NEXTCLOUD_DATADIR}:/mnt/ncdata:rw + - ${NEXTCLOUD_MOUNT}:${NEXTCLOUD_MOUNT}:rw + - ${NEXTCLOUD_TRUSTED_CACERTS_DIR}:/usr/local/share/ca-certificates:ro + environment: + - NEXTCLOUD_HOST=nextcloud-aio-nextcloud + - POSTGRES_HOST=nextcloud-aio-database + - POSTGRES_PORT=5432 + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=nextcloud_database + - POSTGRES_USER=nextcloud + - REDIS_HOST=nextcloud-aio-redis + - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} + - APACHE_HOST=nextcloud-aio-apache + - APACHE_PORT + - NC_DOMAIN + - ADMIN_USER=admin + - ADMIN_PASSWORD=${NEXTCLOUD_PASSWORD} + - NEXTCLOUD_DATA_DIR=/mnt/ncdata + - OVERWRITEHOST=${NC_DOMAIN} + - OVERWRITEPROTOCOL=https + - TURN_SECRET + - SIGNALING_SECRET + - ONLYOFFICE_SECRET + - NEXTCLOUD_MOUNT + - CLAMAV_ENABLED + - CLAMAV_HOST=nextcloud-aio-clamav + - ONLYOFFICE_ENABLED + - COLLABORA_ENABLED + - COLLABORA_HOST=nextcloud-aio-collabora + - TALK_ENABLED + - ONLYOFFICE_HOST=nextcloud-aio-onlyoffice + - UPDATE_NEXTCLOUD_APPS + - TZ=${TIMEZONE} + - TALK_PORT + - IMAGINARY_ENABLED + - IMAGINARY_HOST=nextcloud-aio-imaginary + - CLAMAV_MAX_SIZE=${APACHE_MAX_SIZE} + - PHP_UPLOAD_LIMIT=${NEXTCLOUD_UPLOAD_LIMIT} + - PHP_MEMORY_LIMIT=${NEXTCLOUD_MEMORY_LIMIT} + - FULLTEXTSEARCH_ENABLED + - FULLTEXTSEARCH_HOST=nextcloud-aio-fulltextsearch + - FULLTEXTSEARCH_PORT=9200 + - FULLTEXTSEARCH_USER=elastic + - FULLTEXTSEARCH_INDEX=nextcloud-aio + - PHP_MAX_TIME=${NEXTCLOUD_MAX_TIME} + - TRUSTED_CACERTS_DIR=${NEXTCLOUD_TRUSTED_CACERTS_DIR} + - STARTUP_APPS=${NEXTCLOUD_STARTUP_APPS} + - ADDITIONAL_APKS=${NEXTCLOUD_ADDITIONAL_APKS} + - ADDITIONAL_PHP_EXTENSIONS=${NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS} + - INSTALL_LATEST_MAJOR + - TALK_RECORDING_ENABLED + - RECORDING_SECRET + - TALK_RECORDING_HOST=nextcloud-aio-talk-recording + - FULLTEXTSEARCH_PASSWORD + - REMOVE_DISABLED_APPS + - IMAGINARY_SECRET + - WHITEBOARD_SECRET + - WHITEBOARD_ENABLED + stop_grace_period: 600s + restart: unless-stopped + cap_drop: + - NET_RAW + + nextcloud-aio-notify-push: + image: ghcr.io/nextcloud-releases/aio-notify-push:latest + user: "33" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "7867" + volumes: + - nextcloud_aio_nextcloud:/nextcloud:ro + environment: + - NC_DOMAIN + - NEXTCLOUD_HOST=nextcloud-aio-nextcloud + - TZ=${TIMEZONE} + - REDIS_HOST=nextcloud-aio-redis + - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} + - POSTGRES_HOST=nextcloud-aio-database + - POSTGRES_PORT=5432 + - POSTGRES_PASSWORD=${DATABASE_PASSWORD} + - POSTGRES_DB=nextcloud_database + - POSTGRES_USER=nextcloud + restart: unless-stopped + read_only: true + cap_drop: + - NET_RAW + + nextcloud-aio-redis: + image: ghcr.io/nextcloud-releases/aio-redis:latest + user: "999" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "6379" + environment: + - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} + - TZ=${TIMEZONE} + volumes: + - nextcloud_aio_redis:/data:rw + restart: unless-stopped + read_only: true + cap_drop: + - NET_RAW + + nextcloud-aio-collabora: + command: ${ADDITIONAL_COLLABORA_OPTIONS} + image: ghcr.io/nextcloud-releases/aio-collabora:latest + init: true + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 9 + expose: + - "9980" + environment: + - aliasgroup1=https://${NC_DOMAIN}:443,http://nextcloud-aio-apache:23973 + - extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:mount_jail_tree=false --o:logging.level=warning --o:logging.level_startup=warning --o:home_mode.enable=true --o:remote_font_config.url=https://${NC_DOMAIN}/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+ + - dictionaries=${COLLABORA_DICTIONARIES} + - TZ=${TIMEZONE} + - server_name=${NC_DOMAIN} + - DONT_GEN_SSL_CERT=1 + restart: unless-stopped + profiles: + - collabora + cap_add: + - MKNOD + - SYS_ADMIN + - SYS_CHROOT + - FOWNER + - CHOWN + cap_drop: + - NET_RAW + + nextcloud-aio-talk: + image: ghcr.io/nextcloud-releases/aio-talk:latest + user: "1000" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + ports: + - ${TALK_PORT}:${TALK_PORT}/tcp + - ${TALK_PORT}:${TALK_PORT}/udp + expose: + - "8081" + environment: + - NC_DOMAIN + - TALK_HOST=nextcloud-aio-talk + - TURN_SECRET + - SIGNALING_SECRET + - TZ=${TIMEZONE} + - TALK_PORT + - INTERNAL_SECRET=${TALK_INTERNAL_SECRET} + restart: unless-stopped + profiles: + - talk + - talk-recording + read_only: true + tmpfs: + - /var/log/supervisord + - /var/run/supervisord + - /opt/eturnal/run + - /conf + - /tmp + cap_drop: + - NET_RAW + + nextcloud-aio-talk-recording: + image: ghcr.io/nextcloud-releases/aio-talk-recording:latest + user: "122" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "1234" + environment: + - NC_DOMAIN + - TZ=${TIMEZONE} + - RECORDING_SECRET + - INTERNAL_SECRET=${TALK_INTERNAL_SECRET} + volumes: + - nextcloud_aio_talk_recording:/tmp:rw + shm_size: 2147483648 + restart: unless-stopped + profiles: + - talk-recording + read_only: true + tmpfs: + - /conf + cap_drop: + - NET_RAW + + nextcloud-aio-clamav: + image: ghcr.io/nextcloud-releases/aio-clamav:latest + user: "100" + init: false + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 9 + expose: + - "3310" + environment: + - TZ=${TIMEZONE} + - MAX_SIZE=${NEXTCLOUD_UPLOAD_LIMIT} + volumes: + - nextcloud_aio_clamav:/var/lib/clamav:rw + restart: unless-stopped + profiles: + - clamav + read_only: true + tmpfs: + - /tmp + - /var/log/clamav + - /run/clamav + - /var/log/supervisord + - /var/run/supervisord + cap_drop: + - NET_RAW + + nextcloud-aio-onlyoffice: + image: ghcr.io/nextcloud-releases/aio-onlyoffice:latest + init: true + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 9 + expose: + - "80" + environment: + - TZ=${TIMEZONE} + - JWT_ENABLED=true + - JWT_HEADER=AuthorizationJwt + - JWT_SECRET=${ONLYOFFICE_SECRET} + volumes: + - nextcloud_aio_onlyoffice:/var/lib/onlyoffice:rw + restart: unless-stopped + profiles: + - onlyoffice + cap_drop: + - NET_RAW + + nextcloud-aio-imaginary: + image: ghcr.io/nextcloud-releases/aio-imaginary:latest + user: "65534" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "9000" + environment: + - TZ=${TIMEZONE} + - IMAGINARY_SECRET + restart: unless-stopped + cap_add: + - SYS_NICE + cap_drop: + - NET_RAW + profiles: + - imaginary + read_only: true + tmpfs: + - /tmp + + nextcloud-aio-fulltextsearch: + image: ghcr.io/nextcloud-releases/aio-fulltextsearch:latest + init: false + healthcheck: + start_period: 60s + test: /healthcheck.sh + interval: 10s + timeout: 5s + start_interval: 5s + retries: 5 + expose: + - "9200" + environment: + - TZ=${TIMEZONE} + - ES_JAVA_OPTS=${FULLTEXTSEARCH_JAVA_OPTIONS} + - bootstrap.memory_lock=true + - cluster.name=nextcloud-aio + - discovery.type=single-node + - logger.level=WARN + - http.port=9200 + - xpack.license.self_generated.type=basic + - xpack.security.enabled=false + - FULLTEXTSEARCH_PASSWORD + volumes: + - nextcloud_aio_elasticsearch:/usr/share/elasticsearch/data:rw + restart: unless-stopped + profiles: + - fulltextsearch + cap_drop: + - NET_RAW + + nextcloud-aio-whiteboard: + image: ghcr.io/nextcloud-releases/aio-whiteboard:latest + user: "65534" + init: true + healthcheck: + start_period: 0s + test: /healthcheck.sh + interval: 30s + timeout: 30s + start_interval: 5s + retries: 3 + expose: + - "3002" + tmpfs: + - /tmp + environment: + - TZ=${TIMEZONE} + - NEXTCLOUD_URL=https://${NC_DOMAIN} + - JWT_SECRET_KEY=${WHITEBOARD_SECRET} + - STORAGE_STRATEGY=redis + - REDIS_HOST=nextcloud-aio-redis + - REDIS_HOST_PASSWORD=${REDIS_PASSWORD} + - BACKUP_DIR=/tmp + restart: unless-stopped + profiles: + - whiteboard + read_only: true + cap_drop: + - NET_RAW + +volumes: + nextcloud_aio_apache: + name: nextcloud_aio_apache + nextcloud_aio_clamav: + name: nextcloud_aio_clamav + nextcloud_aio_database: + name: nextcloud_aio_database + nextcloud_aio_database_dump: + name: nextcloud_aio_database_dump + nextcloud_aio_elasticsearch: + name: nextcloud_aio_elasticsearch + nextcloud_aio_nextcloud: + name: nextcloud_aio_nextcloud + nextcloud_aio_onlyoffice: + name: nextcloud_aio_onlyoffice + nextcloud_aio_redis: + name: nextcloud_aio_redis + nextcloud_aio_talk_recording: + name: nextcloud_aio_talk_recording + nextcloud_aio_nextcloud_data: + name: nextcloud_aio_nextcloud_data + +networks: + default: + driver: bridge diff --git a/nextcloud-aio/manual-install/readme.md b/nextcloud-aio/manual-install/readme.md new file mode 100644 index 0000000..8e87412 --- /dev/null +++ b/nextcloud-aio/manual-install/readme.md @@ -0,0 +1,54 @@ +# Manual installation + +You can run the containers that are build for AIO with docker-compose. This comes with a few downsides, that are discussed below. + +### Advantages +- You can run it without a container having access to the docker socket +- You can modify all values on your own +- You can run the containers with docker swarm +- You can run this in environments where access to docker.io is not possible. See [this issue](https://github.com/nextcloud/all-in-one/discussions/5268). + +### Disadvantages +- You lose the AIO interface +- You lose update notifications and automatic updates +- You lose all AIO backup and restore features +- You lose the built-in [Docker Socket Proxy container](https://github.com/nextcloud/docker-socket-proxy#readme) (needed for [Nextcloud App API](https://github.com/nextcloud/app_api#nextcloud-appapi)) +- You lose all community containers: https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers +- **You need to know what you are doing, especially when modifying the compose.yaml file** +- For updating, you need to strictly follow the at the bottom described update routine +- Probably more + +## How to use this? +First, install docker and docker-compose (v2) if not already done. Then simply run the following: +```bash +git clone https://github.com/nextcloud/all-in-one.git +cd all-in-one/manual-install +``` +Then copy the sample.conf to default environment file, e.g. `cp sample.conf .env`, open the new conf file, e.g. with `nano .env`, edit all values that are marked with `# TODO!`, close and save the file.
+⚠️ **Warning**: Do not use the symbols `@` and `:` in your passwords. These symbols are used to build database connection strings. You will experience issues when using these symbols! Also please note that values inside the latest.yaml that are not exposed as variables are not officially supported to be changed. See for example [this report](https://github.com/nextcloud/all-in-one/issues/5612). + +Now copy the provided yaml file to a compose.yaml file by running `cp latest.yml compose.yaml`. + +Now you should be ready to go with `sudo docker compose up`. + +## Docker profiles +The default profile of `latest.yml` only provide the minimum necessary services: nextcloud, database, redis and apache. To get optional services collabora, talk, whiteboard, talk-recording, clamav, imaginary or fulltextsearch use additional arguments for each of them, for example `--profile collabora`. + +For a complete all-in-one with collabora use `sudo docker compose --profile collabora --profile talk --profile talk-recording --profile clamav --profile imaginary --profile fulltextsearch --profile whiteboard up`. + +## How to update? +Since the AIO containers may change in the future, it is highly recommended to strictly follow the following procedure whenever you want to upgrade your containers. +1. If your previous copy of `sample.conf` is named `my.conf`, run `mv -vn my.conf .env` in order to rename the file to `.env`. +1. Run `sudo docker compose down` to stop all running containers +1. Back up all important files and folders +1. If your compose file is still named `docker-compose.yml` rename it to `compose.yaml` by running `mv -vn docker-compose.yml compose.yaml` +1. Run `git pull` in order to get the updated yaml files from the repository. Now bring your `compose.yaml` file up-to-date with the updated one from the repository. You can use `diff compose.yaml latest.yml` for comparing. ⚠️ **Please note**: Starting with AIO v5.1.0, ipv6 networking will be enabled by default, so make sure to either enable it first by following steps 1 and 2 of https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md and then proceed with the steps below or disable ipv6 networking by editing the compose.yaml file and removing ipv6 from the network. +1. Also have a look at the `sample.conf` if any variable was added or renamed and add that to your conf file as well. Here may help the diff command as well. +1. After the file update was successful, simply run `sudo docker compose pull` to pull the new images. +1. At the end run `sudo docker compose up` in order to start and update the containers with the new configuration. + +## FAQ +### Backup and restore? +If you leave `NEXTCLOUD_DATADIR` in your conf file at the default value of `nextcloud_aio_nextcloud_data` and don't modify the yaml file, all data will be stored inside docker volumes which are on Linux by default located here: `/var/lib/docker/volumes`. Simply backing up this location should be a valid backup solution. Then you can also easily restore in case something bad happens. However if you change `NEXTCLOUD_DATADIR` to a path like `/mnt/ncdata`, you obviously need to back up this location, too because the Nextcloud data will be stored there. The same applies to any change to the yaml file. + +Obviously you also need to back up the conf file and the yaml file if you modified it. diff --git a/nextcloud-aio/manual-install/sample.conf b/nextcloud-aio/manual-install/sample.conf new file mode 100644 index 0000000..9ee01ab --- /dev/null +++ b/nextcloud-aio/manual-install/sample.conf @@ -0,0 +1,42 @@ +DATABASE_PASSWORD= # TODO! This needs to be a unique and good password! +FULLTEXTSEARCH_PASSWORD= # TODO! This needs to be a unique and good password! +IMAGINARY_SECRET= # TODO! This needs to be a unique and good password! +NC_DOMAIN=yourdomain.com # TODO! Needs to be changed to the domain that you want to use for Nextcloud. +NEXTCLOUD_PASSWORD= # TODO! This is the password of the initially created Nextcloud admin with username "admin". +ONLYOFFICE_SECRET= # TODO! This needs to be a unique and good password! +RECORDING_SECRET= # TODO! This needs to be a unique and good password! +REDIS_PASSWORD= # TODO! This needs to be a unique and good password! +SIGNALING_SECRET= # TODO! This needs to be a unique and good password! +TALK_INTERNAL_SECRET= # TODO! This needs to be a unique and good password! +TIMEZONE=Europe/Berlin # TODO! This is the timezone that your containers will use. +TURN_SECRET= # TODO! This needs to be a unique and good password! +WHITEBOARD_SECRET= # TODO! This needs to be a unique and good password! + +CLAMAV_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +COLLABORA_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +FULLTEXTSEARCH_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +IMAGINARY_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +ONLYOFFICE_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_RECORDING_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +WHITEBOARD_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. + +APACHE_IP_BINDING=0.0.0.0 # This can be changed to e.g. 127.0.0.1 if you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) and if that is running on the same host and using localhost to connect +APACHE_MAX_SIZE=17179869184 # This needs to be an integer and in sync with NEXTCLOUD_UPLOAD_LIMIT +APACHE_PORT=443 # Changing this to a different value than 443 will allow you to run it behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). +ADDITIONAL_COLLABORA_OPTIONS=['--o:security.seccomp=true'] # You can add additional collabora options here by using the array syntax. +COLLABORA_DICTIONARIES="de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru" # You can change this in order to enable other dictionaries for collabora +FULLTEXTSEARCH_JAVA_OPTIONS="-Xms512M -Xmx512M" # Allows to adjust the fulltextsearch java options. +INSTALL_LATEST_MAJOR=no # Setting this to yes will install the latest Major Nextcloud version upon the first installation +NEXTCLOUD_ADDITIONAL_APKS=imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. +NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. +NEXTCLOUD_DATADIR=nextcloud_aio_nextcloud_data # You can change this to e.g. "/mnt/ncdata" to map it to a location on your host. It needs to be adjusted before the first startup and never afterwards! +NEXTCLOUD_MAX_TIME=3600 # This allows to change the upload time limit of the Nextcloud container +NEXTCLOUD_MEMORY_LIMIT=512M # This allows to change the PHP memory limit of the Nextcloud container +NEXTCLOUD_MOUNT=/mnt/ # This allows the Nextcloud container to access directories on the host. It must never be equal to the value of NEXTCLOUD_DATADIR! +NEXTCLOUD_STARTUP_APPS="deck twofactor_totp tasks calendar contacts notes" # Allows to modify the Nextcloud apps that are installed on starting AIO the first time +NEXTCLOUD_TRUSTED_CACERTS_DIR=/usr/local/share/ca-certificates/my-custom-ca # Nextcloud container will trust all the Certification Authorities, whose certificates are included in the given directory. +NEXTCLOUD_UPLOAD_LIMIT=16G # This allows to change the upload limit of the Nextcloud container +REMOVE_DISABLED_APPS=yes # Setting this to no keep Nextcloud apps that are disabled via their switch and not uninstall them if they should be installed in Nextcloud. +TALK_PORT=3478 # This allows to adjust the port that the talk container is using. It should be set to something higher than 1024! Otherwise it might not work! +UPDATE_NEXTCLOUD_APPS="no" # When setting to "yes" (with quotes), it will automatically update all installed Nextcloud apps upon container startup on saturdays. diff --git a/nextcloud-aio/manual-install/update-yaml.sh b/nextcloud-aio/manual-install/update-yaml.sh new file mode 100644 index 0000000..70d14b4 --- /dev/null +++ b/nextcloud-aio/manual-install/update-yaml.sh @@ -0,0 +1,163 @@ +#!/bin/bash -ex + +type {jq,sudo} || { echo "Commands not found. Please install them"; exit 127; } + +jq -c . ./php/containers.json > /tmp/containers.json +sed -i 's|aio_services_v1|services|g' /tmp/containers.json +sed -i 's|","destination":"|:|g' /tmp/containers.json +sed -i 's|","writeable":false|:ro"|g' /tmp/containers.json +sed -i 's|","writeable":true|:rw"|g' /tmp/containers.json +sed -i 's|","port_number":"|:|g' /tmp/containers.json +sed -i 's|","protocol":"|/|g' /tmp/containers.json +sed -i 's|"ip_binding":":|"ip_binding":"|g' /tmp/containers.json +cat /tmp/containers.json +OUTPUT="$(cat /tmp/containers.json)" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].internal_port)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].secrets)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].ui_secrets)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].devices)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].enable_nvidia_gpu)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].backup_volumes)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].nextcloud_exec_commands)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].image_tag)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].networks)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[].documentation)')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-watchtower"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-domaincheck"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-borgbackup"))')" +OUTPUT="$(echo "$OUTPUT" | jq 'del(.services[] | select(.container_name == "nextcloud-aio-docker-socket-proxy"))')" +OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= if contains(["nextcloud-aio-docker-socket-proxy"]) then del(.[index("nextcloud-aio-docker-socket-proxy")]) else . end else . end')" +OUTPUT="$(echo "$OUTPUT" | jq '.services[] |= if has("depends_on") then .depends_on |= map({ (.): { "condition": "service_started", "required": false } }) else . end' | jq '.services[] |= if has("depends_on") then .depends_on |= reduce .[] as $item ({}; . + $item) else . end')" + +sudo snap install yq +mkdir -p ./manual-install +echo "$OUTPUT" | yq -P > ./manual-install/containers.yml + +cd manual-install || exit +sed -i "s|'||g" containers.yml +sed -i '/display_name:/d' containers.yml +sed -i '/THIS_IS_AIO/d' containers.yml +sed -i "s|%COLLABORA_SECCOMP_POLICY% ||g" containers.yml +sed -i '/stop_grace_period:/s/$/s/' containers.yml +sed -i '/: \[\]/d' containers.yml +sed -i 's|- source: |- |' containers.yml +sed -i 's|- ip_binding: |- |' containers.yml +sed -i '/AIO_TOKEN/d' containers.yml +sed -i '/AIO_URL/d' containers.yml +sed -i '/DOCKER_SOCKET_PROXY_ENABLED/d' containers.yml +sed -i '/ADDITIONAL_TRUSTED_PROXY/d' containers.yml + +TCP="$(grep -oP '[%A-Z0-9_]+/tcp' containers.yml | sort -u)" +mapfile -t TCP <<< "$TCP" +for port in "${TCP[@]}" +do + solve_port="${port%%/tcp}" + sed -i "s|$solve_port/tcp|$solve_port:$solve_port/tcp|" containers.yml +done + +UDP="$(grep -oP '[%A-Z0-9_]+/udp' containers.yml | sort -u)" +mapfile -t UDP <<< "$UDP" +for port in "${UDP[@]}" +do + solve_port="${port%%/udp}" + sed -i "s|$solve_port/udp|$solve_port:$solve_port/udp|" containers.yml +done + +rm -f sample.conf +VARIABLES="$(grep -oP '%[A-Z_a-z0-6]+%' containers.yml | sort -u)" +mapfile -t VARIABLES <<< "$VARIABLES" +for variable in "${VARIABLES[@]}" +do + # shellcheck disable=SC2001 + sole_variable="$(echo "$variable" | sed 's|%||g')" + echo "$sole_variable=" >> sample.conf + sed -i "s|$variable|\${$sole_variable}|g" containers.yml +done + +sed -i 's|_ENABLED=|_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.|' sample.conf +sed -i 's|CLAMAV_ENABLED=no.*|CLAMAV_ENABLED="no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically.|' sample.conf +sed -i 's|TALK_ENABLED=no|TALK_ENABLED="yes"|' sample.conf +sed -i 's|COLLABORA_ENABLED=no|COLLABORA_ENABLED="yes"|' sample.conf +sed -i 's|COLLABORA_DICTIONARIES=|COLLABORA_DICTIONARIES="de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru" # You can change this in order to enable other dictionaries for collabora|' sample.conf +sed -i 's|NEXTCLOUD_DATADIR=|NEXTCLOUD_DATADIR=nextcloud_aio_nextcloud_data # You can change this to e.g. "/mnt/ncdata" to map it to a location on your host. It needs to be adjusted before the first startup and never afterwards!|' sample.conf +sed -i 's|NEXTCLOUD_MOUNT=|NEXTCLOUD_MOUNT=/mnt/ # This allows the Nextcloud container to access directories on the host. It must never be equal to the value of NEXTCLOUD_DATADIR!|' sample.conf +sed -i 's|NEXTCLOUD_UPLOAD_LIMIT=|NEXTCLOUD_UPLOAD_LIMIT=16G # This allows to change the upload limit of the Nextcloud container|' sample.conf +sed -i 's|NEXTCLOUD_MEMORY_LIMIT=|NEXTCLOUD_MEMORY_LIMIT=512M # This allows to change the PHP memory limit of the Nextcloud container|' sample.conf +sed -i 's|APACHE_MAX_SIZE=|APACHE_MAX_SIZE=17179869184 # This needs to be an integer and in sync with NEXTCLOUD_UPLOAD_LIMIT|' sample.conf +sed -i 's|NEXTCLOUD_MAX_TIME=|NEXTCLOUD_MAX_TIME=3600 # This allows to change the upload time limit of the Nextcloud container|' sample.conf +sed -i 's|NEXTCLOUD_TRUSTED_CACERTS_DIR=|NEXTCLOUD_TRUSTED_CACERTS_DIR=/usr/local/share/ca-certificates/my-custom-ca # Nextcloud container will trust all the Certification Authorities, whose certificates are included in the given directory.|' sample.conf +sed -i 's|UPDATE_NEXTCLOUD_APPS=|UPDATE_NEXTCLOUD_APPS="no" # When setting to "yes" (with quotes), it will automatically update all installed Nextcloud apps upon container startup on saturdays.|' sample.conf +sed -i 's|APACHE_PORT=|APACHE_PORT=443 # Changing this to a different value than 443 will allow you to run it behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else).|' sample.conf +sed -i 's|APACHE_IP_BINDING=|APACHE_IP_BINDING=0.0.0.0 # This can be changed to e.g. 127.0.0.1 if you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) and if that is running on the same host and using localhost to connect|' sample.conf +sed -i 's|TALK_PORT=|TALK_PORT=3478 # This allows to adjust the port that the talk container is using. It should be set to something higher than 1024! Otherwise it might not work!|' sample.conf +sed -i 's|NC_DOMAIN=|NC_DOMAIN=yourdomain.com # TODO! Needs to be changed to the domain that you want to use for Nextcloud.|' sample.conf +sed -i 's|NEXTCLOUD_PASSWORD=|NEXTCLOUD_PASSWORD= # TODO! This is the password of the initially created Nextcloud admin with username "admin".|' sample.conf +sed -i 's|TIMEZONE=|TIMEZONE=Europe/Berlin # TODO! This is the timezone that your containers will use.|' sample.conf +sed -i 's|COLLABORA_SECCOMP_POLICY=|COLLABORA_SECCOMP_POLICY=--o:security.seccomp=true # Changing the value to false allows to disable the seccomp feature of the Collabora container.|' sample.conf +sed -i 's|FULLTEXTSEARCH_JAVA_OPTIONS=|FULLTEXTSEARCH_JAVA_OPTIONS="-Xms512M -Xmx512M" # Allows to adjust the fulltextsearch java options.|' sample.conf +sed -i 's|NEXTCLOUD_STARTUP_APPS=|NEXTCLOUD_STARTUP_APPS="deck twofactor_totp tasks calendar contacts notes" # Allows to modify the Nextcloud apps that are installed on starting AIO the first time|' sample.conf +sed -i 's|NEXTCLOUD_ADDITIONAL_APKS=|NEXTCLOUD_ADDITIONAL_APKS=imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value.|' sample.conf +sed -i 's|NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=|NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value.|' sample.conf +sed -i 's|INSTALL_LATEST_MAJOR=|INSTALL_LATEST_MAJOR=no # Setting this to yes will install the latest Major Nextcloud version upon the first installation|' sample.conf +sed -i 's|REMOVE_DISABLED_APPS=|REMOVE_DISABLED_APPS=yes # Setting this to no keep Nextcloud apps that are disabled via their switch and not uninstall them if they should be installed in Nextcloud.|' sample.conf +sed -i 's|=$|= # TODO! This needs to be a unique and good password!|' sample.conf + +grep '# TODO!' sample.conf > todo.conf +grep -v '# TODO!\|_ENABLED' sample.conf > temp.conf +grep '_ENABLED' sample.conf > enabled.conf +cat todo.conf > sample.conf +# shellcheck disable=SC2129 +echo '' >> sample.conf +cat enabled.conf >> sample.conf +echo '' >> sample.conf +cat temp.conf >> sample.conf +rm todo.conf temp.conf enabled.conf +cat sample.conf + +OUTPUT="$(cat containers.yml)" +NAMES="$(grep -oP "container_name:.*" containers.yml | grep -oP 'nextcloud-aio.*')" +mapfile -t NAMES <<< "$NAMES" +for name in "${NAMES[@]}" +do + OUTPUT="$(echo "$OUTPUT" | sed "/container_name.*$name$/i\ \ $name:")" + if [ "$name" != "nextcloud-aio-apache" ]; then + OUTPUT="$(echo "$OUTPUT" | sed "/^ $name:/i\ ")" + fi +done + +echo "$OUTPUT" > containers.yml + +sed -i '/container_name/d' containers.yml +sed -i 's|^ $||' containers.yml + +# Additional config for collabora +cat << EOL > /tmp/additional-collabora.config + command: \${ADDITIONAL_COLLABORA_OPTIONS} +EOL +sed -i "/^ nextcloud-aio-collabora:/r /tmp/additional-collabora.config" containers.yml +sed -i "/^COLLABORA_DICTIONARIES.*/i ADDITIONAL_COLLABORA_OPTIONS=['--o:security.seccomp=true'] # You can add additional collabora options here by using the array syntax." sample.conf + +VOLUMES="$(grep -oP 'nextcloud_aio_[a-z_]+' containers.yml | sort -u)" +mapfile -t VOLUMES <<< "$VOLUMES" +echo "" >> containers.yml +echo "volumes:" >> containers.yml +for volume in "${VOLUMES[@]}" "nextcloud_aio_nextcloud_data" +do + cat << VOLUMES >> containers.yml + $volume: + name: $volume +VOLUMES +done + +cat << NETWORK >> containers.yml + +networks: + default: + driver: bridge +NETWORK + +mv containers.yml latest.yml +sed -i "/image:/s/$/:latest/" latest.yml +sed -i 's/\( *- \(\w*\)\)=\${\2\}/\1/' latest.yml + +set +ex diff --git a/nextcloud-aio/manual-upgrade.md b/nextcloud-aio/manual-upgrade.md new file mode 100644 index 0000000..638c5a7 --- /dev/null +++ b/nextcloud-aio/manual-upgrade.md @@ -0,0 +1,122 @@ +# Manual upgrade + +If you do not update Nextcloud AIO for a long time (6+ months), when you eventually update in the AIO interface you will find Nextcloud no longer works. This is due to incompatible PHP versions within the nextcloud container. +There is unfortunately no way to fix this from a maintainer POV if you refrain from upgrading for so long. + +The only way to fix this on your side is upgrading regularly (e.g. by enabling daily backups which will also automatically upgrade all containers) and following the steps below to get back to a normal state: + +--- + +## Method 1 using `assaflavie/runlike` + +> [!Warning] +> Please note that this method is apparently currently broken. See https://help.nextcloud.com/t/manual-upgrade-keeps-failing/217164/10 +> So please refer to method 2 using Portainer. + +1. Start all containers from the AIO interface + - Now, it will report that Nextcloud is restarting because it is not able to start due to the above mentioned problem + - #### Do **not** click on `Stop containers` because you will need them running going forward, see below +2. Find out with which PHP version your installed Nextcloud is compatible by running `sudo docker exec nextcloud-aio-nextcloud cat lib/versioncheck.php`. + - There you will find information about the max. supported PHP version + - **Make a mental note of this** +3. Stop the Nextcloud container and the Apache container by running + ```bash + sudo docker stop nextcloud-aio-nextcloud && sudo docker stop nextcloud-aio-apache + ``` +4. Run the following commands in order to reverse engineer the Nextcloud container: + ```bash + sudo docker pull assaflavie/runlike + echo '#!/bin/bash' > /tmp/nextcloud-aio-nextcloud + sudo docker run --rm -v /var/run/docker.sock:/var/run/docker.sock assaflavie/runlike -p nextcloud-aio-nextcloud >> /tmp/nextcloud-aio-nextcloud + sudo chown root:root /tmp/nextcloud-aio-nextcloud + ``` +5. Now open `/tmp/nextcloud-aio-nextcloud` with a text editor, and edit the container tag: + + +| To change | Replace with | +|----------------------------------------|-----------------------------------------------------| +| `ghcr.io/nextcloud-releases/aio-nextcloud:latest` | `ghcr.io/nextcloud-releases/aio-nextcloud:php{version}-latest` | +| `ghcr.io/nextcloud-releases/aio-nextcloud:latest-arm64` | `ghcr.io/nextcloud-releases/aio-nextcloud:php{version}-latest-arm64` | + + + + - e.g. `ghcr.io/nextcloud-releases/aio-nextcloud:php8.0-latest` or `ghcr.io/nextcloud-releases/aio-nextcloud:php8.0-latest-arm64` + - However, if you are unsure check the ghcr.io (https://github.com/nextcloud-releases/all-in-one/pkgs/container/aio-nextcloud/versions?filters%5Bversion_type%5D=tagged) and docker hub: https://hub.docker.com/r/nextcloud/aio-nextcloud/tags?name=php + - Using nano and the arrow keys to navigate: + - `sudo nano /tmp/nextcloud-aio-nextcloud` making changes as above, then `[Ctrl]+[o]` -> `[Enter]` and `[Ctrl]+[x]` to save and exit. +6. Next, stop and remove the current container: + ```bash + sudo docker stop nextcloud-aio-nextcloud + sudo docker rm nextcloud-aio-nextcloud + ``` +7. Now start the Nextcloud container with the new tag by simply running `sudo bash /tmp/nextcloud-aio-nextcloud` which at startup should automatically upgrade Nextcloud to a more recent version. If not, make sure that there is no `skip.update` file in the Nextcloud datadir. If there is such a file, simply delete the file and restart the container again.
+**Info**: You can open the Nextcloud container logs with `sudo docker logs -f nextcloud-aio-nextcloud`. +8. After the Nextcloud container is started (you can tell by looking at the logs), simply restart the container again with `sudo docker restart nextcloud-aio-nextcloud` until it does not install a new Nextcloud update anymore upon the container startup. +9. Now, you should be able to use the AIO interface again by simply stopping the AIO containers and starting them again which should finally bring up your instance again. +10. If not and if you get the same error again, you may repeat the process starting from the beginning again until your Nextcloud version is finally up-to-date. +11. Now, if everything is finally running as usual again, it is recommended to create a backup in order to save the current state. Consider enabling daily backups if doing regular upgrades is a hassle for you. + +--- + +## Method 2 using Portainer +#### *Approach using portainer if method 1 does not work for you* + +Prerequisite: have all containers from AIO interface running. + +##### 1. Install portainer if not installed: +```bash +docker volume create portainer_data +docker run -d -p 8000:8000 -p 9443:9443 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest +``` +- If you have a reverse proxy + - you can setup and navigate using a domain name. +- For the **standard** AIO install + - Open port 9443 on your firewall + - navigate to `https://:9443` +- Accept the insecure self-signed certificate and set an admin password +- If prompted to add an environment + - add local + +##### 2. Within the local portainer environment navigate to the **containers** tab +- Here you should see all the various containers running + +##### 3. Now we need to stop the `nextcloud-aio-nextcloud` and `nextcloud-aio-apache` containers + +- This can be done by selecting the checkbox's next to the containers' name and clicking the **Stop** button at the top + - or you can click into individual containers and stop them there + +##### 4. Find the version of PHP compatible with the running nextcloud container +- navigate to ```nextcloud-aio-nextcloud``` and click on ```logs```, you should see something along the lines of: +```logs +This version of nextcloud is not compatible with >=php 8.2, you are currently running php 8.2.18 +``` +Make **note** of the version which is compatible, rounding down to 1 digit after the dot. + - In this example we would want php 8.1 since anything with 8.2 or above is incompatible + +##### 5. Find the correct container version +In general it should be ```ghcr.io/nextcloud-releases/aio-nextcloud:php8.x-latest-arm64``` or `ghcr.io/nextcloud-releases/aio-nextcloud:php8.x-latest` replacing `x` with the version you require. +However, if you are unsure check the ghcr.io (https://github.com/nextcloud-releases/all-in-one/pkgs/container/aio-nextcloud/versions?filters%5Bversion_type%5D=tagged) and docker hub: https://hub.docker.com/r/nextcloud/aio-nextcloud/tags?name=php + +##### 6. Replace the container +- Navigate to the ```nextcloud-aio-nextcloud``` container within portainer +- Click ```Duplicate/Edit``` +- Within image, change this to the correct version from Step 5 +- Click ```Deploy the container``` + - if you are prompted to force repull the image click the slider and press pull image + +*Navigate to the nextcloud-aio-nextcloud logs and you will see the container updating* + +Once you see no more activities in the logs or a message like ```NOTICE: ready to handle connections```, we've done it! + +#### Now you can handle everything through the AIO admin interface and stop and restart the containers normally. + +--- + +##### 7. Last Step is removing portainer if you don't want to keep it + +```bash +docker stop portainer +docker rm portainer +docker volume rm portainer_data +``` +- Make sure you close port 9443 on your firewall and delete any necessary reverse proxy hosts. diff --git a/nextcloud-aio/migration.md b/nextcloud-aio/migration.md new file mode 100644 index 0000000..5e9b8b2 --- /dev/null +++ b/nextcloud-aio/migration.md @@ -0,0 +1,105 @@ +# How to migrate from an already existing Nextcloud installation to Nextcloud AIO? + +There are basically three ways how to migrate from an already existing Nextcloud installation to Nextcloud AIO (if you ran AIO on the former installation already, you can follow [these steps](https://github.com/nextcloud/all-in-one#how-to-migrate-from-aio-to-aio)): + +1. Migrate only the files which is the easiest way (this excludes all calendar data for example) +1. Migrate the files and the database which is much more complicated (and doesn't work on former snap installations) +1. Use the user_migration app that allows to migrate some of the user's data from a former instance to a new instance but needs to be done manually for each user + +## Migrate only the files +**Please note**: If you used groupfolders or encrypted your files before, you will need to restore the database, as well! (This will also exclude all calendar data for example). + +The procedure for migrating only the files works like this: +1. Take a backup of your former instance (especially from your datadirectory, see `'datadirectory'` in your `config.php`) +1. Install Nextcloud AIO on a new server/linux installation, enter your domain and wait until all containers are running +1. Recreate all users that were present on your former installation +1. Take a backup using Nextcloud AIO's built-in backup solution (so that you can easily restore to this state again) (Note: this will stop all containers and is expected: don't start the container again at this point!) +1. Restore the datadirectory of your former instance: for `/path/to/old/nextcloud/data/` run `sudo docker cp --follow-link /path/to/old/nextcloud/data/. nextcloud-aio-nextcloud:/mnt/ncdata/` Note: the `/.` and `/` at the end are necessary. +1. Next, run `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chown -R 33:0 /mnt/ncdata/` and `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chmod -R 750 /mnt/ncdata/` to apply the correct permissions. (Or if `NEXTCLOUD_DATADIR` was provided, apply `chown -R 33:0` and `chmod -R 750` to the chosen path.) +1. Start the containers again and wait until all containers are running +1. Run `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ files:scan-app-data && sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ files:scan --all` in order to scan all files in the datadirectory. +1. If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below. + +## Migrate the files and the database +**Please note**: this is much more complicated than migrating only the files and also not as failproof so be warned! Also, this will not work on former snap installations as the snap is read-only and thus you cannot install the necessary `pdo_pgsql` PHP extension. So if migrating from snap, you will need to use one of the other methods. However you could try to ask if the snaps maintainer could add this one small PHP extension to the snap here: https://github.com/nextcloud-snap/nextcloud-snap/issues which would allow for an easy migration. + +The procedure for migrating the files and the database works like this: +1. Make sure that your old instance is on exactly the same version like the version used in Nextcloud AIO. (e.g. 23.0.0) You can find the used version here: [click here](https://github.com/nextcloud/all-in-one/search?l=Dockerfile&q=NEXTCLOUD_VERSION&type=). If not, simply upgrade your former installation to that version or wait until the version used in Nextcloud AIO got updated to the same version of your former installation or the other way around. +1. First, on the old instance, update all Nextcloud apps to its latest version via the app management site (important for the restore later on). Then take a backup of your former instance (especially from your datadirectory and database). +1. If your former installation didn't use Postgresql already, you will now need to convert your old installation to use Postgresql as database temporarily (in order to be able to perform a pg_dump afterwards): + 1. Install Postgresql on your former installation: on a Debian based OS should the following command work: + ``` + sudo apt update && sudo apt install postgresql -y + ``` + 1. Create a new database by running: + ``` + export PG_USER="ncadmin" # This is a temporary user that gets created for the dump but is then overwritten by the correct one later on + export PG_PASSWORD="my-temporary-password" + export PG_DATABASE="nextcloud_db" + sudo -u postgres psql < + **Troubleshooting:** If you get an error that it could not find a driver for the conversion, you most likely need to install the PHP extension `pdo_pgsql`. + 1. Hopefully does the conversion finish successfully. If not, simply restore your old Nextcloud installation from backup. If yes, you should now log in to your Nextcloud and test if everything works and if all data has been converted successfully. + 1. If everything works as expected, feel free to continue with the steps below. +1. Now, run a pg_dump to get an export of your current database. Something like the following command should work: + ``` + sudo -Hiu postgres pg_dump "$PG_DATABASE" > ./database-dump.sql + ``` + **Please note:** The exact name of the database export file is important! (`database-dump.sql`)
+ And of course you need to to use the correct name that the Postgresql database has for the export (if `$PG_DATABASE` doesn't work directly). +1. At this point, you can finally install Nextcloud AIO on a new server/linux installation, enter your domain in the AIO interface (use the same domain that you used on your former installation) and wait until all containers are running. Then you should check the included Nextcloud version by running `sudo docker inspect nextcloud-aio-nextcloud | grep NEXTCLOUD_VERSION`. On the AIO interface, use the passphrase to connect to your newly created Nextcloud instance's admin account. There, install all the Nextcloud apps that were installed on the old Nextcloud installation. If you don't, the migration will show them as installed, but they won't work. +1. Next, take a backup using Nextcloud AIO's built-in backup solution (so that you can easily restore to this state again). Once finished, all containers are automatically stopped and is expected: **don't start the container again at this point!** +1. Now, with the containers still stopped, we are slowly starting to import your files and database. First, you need to modify the datadirectory that is stored inside the database export: + 1. Find out what the directory of your old Nextcloud installation is by e.g. opening the config.php file and looking at the value `datadirectory`. + 1. Now, create a copy of the database file so that you can simply restore it if you should make a mistake while editing: `cp database-dump.sql database-dump.sql.backup` + 1. Next, open the database export with e.g. nano: `nano database-dump.sql` + 1. Press `[CTRL] + [w]` in order to open the search + 1. Type in `local::/your/old/datadir/` which should bring up the exact line where you need to modify the path to use the one used in Nextcloud AIO, instead. + 1. Change it to look like this: `local::/mnt/ncdata/`. + 1. Now save the file by pressing `[CTRL] + [o]` then `[ENTER]` and close nano by pressing `[CTRL] + [x]` + 1. In order to make sure that everything is good, you can now run `grep "/your/old/datadir" database-dump.sql` which should not bring up further results.
+ 1. **Please note:** Unfortunately it is not possible to import a database dump from a former database owner with the name `nextcloud`. You can check if that is the case with this command: `grep "Name: oc_appconfig; Type: TABLE; Schema: public; Owner:" database-dump.sql | grep -oP 'Owner:.*$' | sed 's|Owner:||;s| ||g'`. If it returns `nextcloud`, you need to rename the owner in the dump file manually. A command like the following should work, however please note that it is possible that it will overwrite wrong lines. You can thus first check which lines it will change with `grep "Owner: nextcloud$" database-dump.sql`. If only correct looking lines get returned, feel free to change them with `sed -i 's|Owner: nextcloud$|Owner: ncadmin|' database-dump.sql`. +The same applies for the second statement, check with `grep " OWNER TO nextcloud;$" database-dump.sql` and replace with `sed -i 's| OWNER TO nextcloud;$| OWNER TO ncadmin;|' database-dump.sql`. +1. Next, copy the database dump into the correct place and prepare the database container which will import from the database dump automatically the next container start: + ``` + sudo docker run --rm --volume nextcloud_aio_database_dump:/mnt/data:rw alpine rm /mnt/data/database-dump.sql + sudo docker cp database-dump.sql nextcloud-aio-database:/mnt/data/ + sudo docker run --rm --volume nextcloud_aio_database_dump:/mnt/data:rw alpine chmod 777 /mnt/data/database-dump.sql + sudo docker run --rm --volume nextcloud_aio_database_dump:/mnt/data:rw alpine rm /mnt/data/initial-cleanup-done + ``` +1. If the commands above were executed successfully, restore the datadirectory of your former instance into your datadirectory: `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine sh -c "rm -rf /mnt/ncdata/*"` and `sudo docker cp --follow-link /path/to/nextcloud/data/. nextcloud-aio-nextcloud:/mnt/ncdata/` Note: the `/.` and `/` at the end are necessary. (Or if `NEXTCLOUD_DATADIR` was provided, first delete the files in there and then copy the files to the chosen path.) +1. Next, run `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chown -R 33:0 /mnt/ncdata/` and `sudo docker run --rm --volume nextcloud_aio_nextcloud_data:/mnt/ncdata:rw alpine chmod -R 750 /mnt/ncdata/` to apply the correct permissions on the datadirectory. (Or if `NEXTCLOUD_DATADIR` was provided, apply `chown -R 33:0` and `chmod -R 750` to the chosen path.) +1. Edit the Nextcloud AIO config.php file using `sudo docker run -it --rm --volume nextcloud_aio_nextcloud:/var/www/html:rw alpine sh -c "apk add --no-cache nano && nano /var/www/html/config/config.php"` and modify only `passwordsalt`, `secret`, `instanceid` and set it to the old values that you used on your old installation. If you are brave, feel free to modify further values e.g. add your old LDAP config or S3 storage config. (Some things like Mail server config can be added back using Nextcloud's webinterface later on). +1. When you are done and saved your changes to the file, finally start the containers again and wait until all containers are running. + +Now the whole Nextcloud instance should work again.
+If not, feel free to restore the AIO instance from backup and start at step 8 again. + +If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below. + +## Use the user_migration app +A new way since the Nextcloud update to 24 is to use the new [user_migration app](https://apps.nextcloud.com/apps/user_migration#app-gallery). It allows to export the most important data on one instance and import it on a different Nextcloud instance. For that, you need to install and enable the user_migration app on your old instance, trigger the export for the user, create the user on the new instance, log in with that user and import the archive that was created during the export. This then needs to be done for each user that you want to migrate. + +If the restored data is older than any clients you want to continue to sync, for example if the server was down for a period of time during migration, you may want to take a look at [Synchronising with clients after migration](/migration.md#synchronising-with-clients-after-migration) below. + +# Synchronising with clients after migration +#### From https://docs.nextcloud.com/server/latest/admin_manual/maintenance/restore.html#synchronising-with-clients-after-data-recovery +By default the Nextcloud server is considered the authoritative source for the data. If the data on the server and the client differs clients will default to fetching the data from the server. + +If the recovered backup is outdated the state of the clients may be more up to date than the state of the server. In this case also make sure to run `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ maintenance:data-fingerprint` command afterwards. It changes the logic of the synchronisation algorithm to try an recover as much data as possible. Files missing on the server are therefore recovered from the clients and in case of different content the users will be asked. + +>[!Note] +>The usage of maintenance:data-fingerprint can cause conflict dialogues and difficulties deleting files on the client. Therefore it’s only recommended to prevent dataloss if the backup was outdated. + + +If you are running multiple application servers you will need to make sure the config files are synced between them so that the updated data-fingerprint is applied on all instances. diff --git a/nextcloud-aio/multiple-instances.md b/nextcloud-aio/multiple-instances.md new file mode 100644 index 0000000..7a6ae18 --- /dev/null +++ b/nextcloud-aio/multiple-instances.md @@ -0,0 +1,226 @@ +# Multiple AIO instances +It is possible to run multiple instances of AIO on one server. + +There are two ways to achieve this: The normal way is creating multiple VMs, installing AIO in [reverse proxy mode](./reverse-proxy.md) in each of them and having one reverse proxy in front of them that points to each VM (you also need to [use a different `TALK_PORT`](https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port) for each of them). The second and more advanced way is creating multiple users on the server and using docker rootless for each of them in order to install multiple instances on the same server. + + +## Run multiple AIO instances on the same server with docker rootless +1. Create as many linux users as you need first. The easiest way is to use `sudo adduser` and follow the setup for that. Make sure to create a strong unique password for each of them and write it down! +1. Log in as each of the users by opening a new SSH connection as the user and install docker rootless for each of them by following step 0-1 and 3-4 of the [docker rootless documentation](./docker-rootless.md) (you can skip step 2 in this case). +1. Then install AIO in reverse proxy mode by using the command that is described in step 2 and 3 of the [reverse proxy documentation](./reverse-proxy.md) but use a different `APACHE_PORT` and [`TALK_PORT`](https://github.com/nextcloud/all-in-one#how-to-adjust-the-talk-port) for each instance as otherwise it will bug out. Also make sure to adjust the docker socket and `WATCHTOWER_DOCKER_SOCKET_PATH` correctly for each of them by following step 6 of the [docker rootless documentation](./docker-rootless.md). Additionally, modify `--publish 8080:8080` to a different port for each container, e.g. `8081:8080` as otherwise it will not work.
+**⚠️ Please note:** If you want to adjust the `NEXTCLOUD_DATADIR`, make sure to apply the correct permissions to the chosen path as documented at the bottom of the [docker rootless documentation](./docker-rootless.md). Also for the built-in backup to work, the target path needs to have the correct permissions as documented there, too. +1. Now install your webserver of choice on the host system. It is recommended to use caddy for this as it is by far the easiest solution. You can do so by following https://caddyserver.com/docs/install#debian-ubuntu-raspbian or below. (It needs to be installed directly on the host or on a different server in the same network). +1. Next create your Caddyfile with multiple entries and domains for the different instances like described in step 1 of the [reverse proxy documentation](./reverse-proxy.md). Obviously each domain needs to point correctly to the chosen `APACHE_PORT` that you've configured before. Then start Caddy which should automatically get the needed certificates for you if your domains are configured correctly and ports 80 and 443 are forwarded to your server. +1. Now open each of the AIO interfaces by opening `https://ip.address.of.this.server:8080` or e.g. `https://ip.address.of.this.server:8081` or as chosen during step 3 of this documentation. +1. Finally type in the domain that you've configured for each of the instances during step 5 of this documentation and you are done. +1. Please also do not forget to open/forward each chosen `TALK_PORT` UDP and TCP in your firewall/router as otherwise Talk will not work correctly! + +Now everything should be set up correctly and you should have created multiple working instances of AIO on the same server! + + +## Run multiple AIO instances on the same server inside their own virtual machines +This guide will walk you through creating and configuring two (or more) Debian-based VMs (with "reverse proxy mode" Nextcloud AIO installed in each VM), behind one Caddy reverse proxy, all running on one host physical machine (like a laptop or desktop PC). It's highly recommend to follow the steps in order. Steps 1 through 4 will need to be repeated. Steps 5 through 8 only need to be completed once. All commands are expected to be run as root. + +
PLEASE READ: A few expectations about your network +This guide assumes that you have forwarded ports 443 and 8443 to your host physical machine via your router's configuration page, and either set up Dynamic DNS or obtained a static outbound IP address from your ISP. If this is not the case, or if you are brand-new to networking, you probably should not proceed with this guide, unless you are just using it for educational purposes. Proper network setup and security is critical when it comes to keeping your data safe. You may consider hosting using a VPS instead, or choosing one of Nextcloud's trusted providers. +
+ +
A note for VPS users +If you want to do this on a VPS, and your VPS is KVM-based and provides a static IP address, you can likely benefit from this guide too! Simply replace the words "host physical machine" with "VPS" and follow along. +
+ +**Before starting:** Make sure your host physical machine has enough resources. A host machine with 8GB RAM and 100GB storage is sufficient for running two fairly minimal VMs, with 2GB RAM and 32GB storage allocated to each VM. This guide assumes you have these resources at the minimum. This is fine for just testing the setup, but you will probably want to allocate more resources to your VMs if you plan to use this for day-to-day use. +If your host machine has more than 8GB memory available, and you plan to enable any of the optional containers (Nextcloud Office, Talk, Imaginary, etc.) in any of your instances, then you should definitely allocate more memory to the VM hosting that instance. In other words, before turning on any extra features inside a particular AIO interface, make sure you've first allocated enough resources to the VM that the instance is running inside. If in doubt, the AIO interface itself gives great recommendations for extra CPU and RAM allocation. + +**Additional prerequisites:** Your host physical machine needs to have virtualization enabled in it's UEFI/BIOS. It also needs a few tools installed in order to create VMs. Assuming your host machine is a bare-bones Ubuntu or Debian Linux server without a desktop environment installed, the easiest way to create VMs is to install *QEMU*, *virsh*, *virt-install*, and a few extra packages to support UEFI booting and network config ([more info](https://wiki.debian.org/KVM)). You only need to do this once. To do this, run this command (**on the host physical machine**): + +```shell +# For host machines running Ubuntu Server or Debian: +apt install --no-install-recommends qemu-system qemu-utils libvirt-clients libvirt-daemon-system virtinst ovmf bridge-utils dnsmasq-base +``` + +**Let's begin!** This guide assumes that you have two domains where you would like to host two individual AIO instances (one instance per domain). Let's call these domains `example1.com` and `example2.com`. Therefore, we'll create two VMs named `example1-com` and `example2-com` (These are the VM names we'll use below in step 1). + +**Once you're ready, follow steps 1-4 below to set up your VMs. You will configure them one at a time.** + +1. Choose a name for your VM. A good choice is to name each VM the same as the domain name that will be used to access it. +2. Choose the distribution you'd like to install within the VM: +
Ubuntu Server 22.04.4 LTS +

Downloading the .ISO image

+ You must first download an .ISO image to your host machine, and then provide virt-install with the path to that image. + +
# Skip this part if you've already downloaded this image
+   curl -o /tmp/ubuntu-22.04.4-live-server-amd64.iso https://releases.ubuntu.com/jammy/ubuntu-22.04.4-live-server-amd64.iso
+   
+ Note: You may choose a different place to store the .ISO file, but it needs to be somewhere accessible by QEMU. "/tmp" and "/home" work well, but choosing a location like "/root" will cause the next command to fail. +

Creating the VM

+ Now create the Ubuntu Server VM (Don't forget to replace [VM_NAME]): +
virt-install \
+   --name [VM_NAME] \
+   --virt-type kvm \
+   --location /tmp/ubuntu-22.04.4-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \
+   --os-variant ubuntujammy \
+   --disk size=32 \
+   --memory 2048 \
+   --graphics none \
+   --console pty,target_type=serial \
+   --extra-args "console=ttyS0" \
+   --autostart \
+   --boot uefi
+   
+

Using a different version of Ubuntu Server

+ To use a different Ubuntu Server release, visit this page and find the version you want. You will need to adjust the filename and URL for the curl command, and the location and os-variant for the virt-install command, accordingly. +
+
Debian 11 +

Creating the VM

+ Create the Debian VM (Don't forget to replace [VM_NAME]): +
virt-install \
+   --name [VM_NAME] \
+   --virt-type kvm \
+   --location http://deb.debian.org/debian/dists/bullseye/main/installer-amd64/ \
+   --os-variant debian11 \
+   --disk size=32 \
+   --memory 2048 \
+   --graphics none \
+   --console pty,target_type=serial \
+   --extra-args "console=ttyS0" \
+   --autostart \
+   --boot uefi
+   
+
+
Debian 12 +

Creating the VM

+ Create the Debian VM (Don't forget to replace [VM_NAME]): +
# If the os-variant "debian12" is unknown, try "debiantesting" instead
+   virt-install \
+   --name [VM_NAME] \
+   --virt-type kvm \
+   --location http://deb.debian.org/debian/dists/bookworm/main/installer-amd64/ \
+   --os-variant debian12 \
+   --disk size=32 \
+   --memory 2048 \
+   --graphics none \
+   --console pty,target_type=serial \
+   --extra-args "console=ttyS0" \
+   --autostart \
+   --boot uefi
+   
+
+ +3. Navigate through the text-based installer. Most options can remain as default, but here are some tips: +
For the Ubuntu Server installer + When asked about the "type of installation", you can leave the default "Ubuntu Server" without third-party drivers. You can leave the HTTP proxy information blank. In the "Profile Configuration" section, you can set "Your servers name" (hostname) to the same value as the name you gave to your VM (for example, "example1-com"). The installer will only let you create a non-root user. Note down the password you use here! You may skip enabling Ubuntu Pro. You can allow the partitioner to use the entire disk, this only uses the virtual disk that you defined above in step 2. You'll eventually be given the option to install additional software. Although "Nextcloud" is listed here, you almost certainly do not want to select this option, since you are setting up Nextcloud AIO. You'll be asked about installing "SSH server", this is entirely optional (This lets you easily SSH into the VM in the future in case you have to perform any maintenance, but even if you do not install an SSH server, you can still log in using the "virsh console" command). Finally, disregard the "[FAILED] Failed unmounting /cdrom." message, and press return. +
+
For the Debian installer + When asked, you can set the hostname to the same value as the name you gave to your VM (for example, "example1-com"). You can leave the domain name and HTTP proxy information blank. Allow the installer to create both a root and a non-root user. Note down the password(s) you use here! You can allow the partitioner to use the entire disk, this only uses the virtual disk that you defined above in step 2. When tasksel (Software selection) runs and asks if you want to install additional software, use spacebar and your arrow keys to un-check the "Debian desktop environment" and "GNOME" options. The "SSH server" option is entirely optional (This lets you easily SSH into the VM in the future in case you have to perform any maintenance, but even if you do not install an SSH server, you can still log in using the "virsh console" command). Make sure "standard system utilities" is also checked. Hit tab to select "Continue". Finally, disregard the warning about GRUB, allow it to install to your "primary drive" (again, it's only virtual, and this only applies to the VM- this will not affect the boot configuration of your host physical machine) and select "/dev/vda" for the bootable device. +
+4. Configure your new VM: + + After it has finished installing, the VM will have rebooted and presented you with a login prompt. For Debian, just use `root` as the username, and enter the password you chose during the installation process. Ubuntu restricts root account access, so you'll need to first login with your non-root user, and then run `sudo su -` to elevate your privileges. + + We will now run a few commands to install docker and AIO in reverse proxy mode! As with any other commands, carefully read and try your best to understand them before running them. + + **Each time you reach this step and run the `docker run` command below, you'll need to increment the `TALK_PORT` value. For example: 3478, 3479, etc... You may use other values as long as they don't conflict, and make sure they are [greater than 1024](https://github.com/nextcloud/all-in-one/discussions/2517). Be sure to note down the Talk port number you've assigned to this VM/AIO instance. You will need it later if you decide to enable Nextcloud Talk.** + + Run these commands (**on the VM**): + ```shell + apt install -y curl + + curl -fsSL https://get.docker.com | sh + + # Make sure you increment the TALK_PORT value every time you run this! + docker run \ + --init \ + --sig-proxy=false \ + --name nextcloud-aio-mastercontainer \ + --restart always \ + --publish 8080:8080 \ + --env APACHE_PORT=11000 \ + --env APACHE_IP_BINDING=0.0.0.0 \ + --env TALK_PORT=3478 \ + --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + ghcr.io/nextcloud-releases/all-in-one:latest + ``` + The last command may take a few minutes. When it's finished, you should see a success message, saying "Initial startup of Nextcloud All-in-One complete!". Now exit the console session with `Ctrl + [c]`. This concludes the setup for this particular VM. + + + --- +6. Go ahead and run through steps 1-4 again in order to set up your second VM. When you're finished, proceed down to step 6. *(Note: If you downloaded the Ubuntu .ISO image and no longer need it, you may delete it now.)* +7. Almost done! All that's left is configuring your reverse proxy. To do this, you first need to [install it](https://caddyserver.com/docs/install#debian-ubuntu-raspbian). Run (**on the host physical machine**): + ```shell + apt update -y + apt install -y debian-keyring debian-archive-keyring apt-transport-https curl + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list + apt update -y + apt install -y caddy + ``` + These commands will ensure that your system is up-to-date and install the latest stable version of Caddy via it's official binary source. +8. To configure Caddy, you need to know the IP address assigned to each VM. Run (**on the host physical machine**): + ```shell + virsh net-dhcp-leases default + ``` + This will show you the VMs you set up, and the IP address corresponding to each of them. Note down each IP and corresponding hostname. + Finally, you will configure Caddy using this information. Open the default Caddyfile with a text editor: + ```shell + nano /etc/caddy/Caddyfile + ``` + Replace everything in this file with the following configuration. Don't forget to edit this sample configuration and substitute in your own domain names and IP addresses. `[DOMAIN_NAME_*]` should be a domain name like `example1.com`, and `[IP_ADDRESS_*]` should be a local IPv4 address like `192.168.122.225`. + ```shell + # Virtual machine #1 - "example1-com" + https://[DOMAIN_NAME_1]:8443 { + reverse_proxy https://[IP_ADDRESS_1]:8080 { + transport http { + tls_insecure_skip_verify + } + } + } + https://[DOMAIN_NAME_1]:443 { + reverse_proxy [IP_ADDRESS_1]:11000 + } + + # Virtual machine #2 - "example2-com" + https://[DOMAIN_NAME_2]:8443 { + reverse_proxy https://[IP_ADDRESS_2]:8080 { + transport http { + tls_insecure_skip_verify + } + } + } + https://[DOMAIN_NAME_2]:443 { + reverse_proxy [IP_ADDRESS_2]:11000 + } + + # (Add more configurations here if you set up more than two VMs!) + ``` + After making this change, you'll need to restart Caddy: + ```shell + systemctl restart caddy + ``` +9. That's it! Now, all that's left is to set up your instances through the AIO interface as usual by visiting `https://example1.com:8443` and `https://example2.com:8443` in a browser. Once you're finished going through each setup, you can access your new instances simply through their domain names. You can host as many instances with as many domain names as you want this way, as long as you have enough system resources. Enjoy! + +
A few extra tips for managing this setup +
    +
  • You can easily connect to a VM to perform maintenance using this command (on the host physical machine):
    virsh console --domain [VM_NAME]
  • +
  • If you chose to install an SSH Server, you can SSH in using this command (on the host physical machine):
    ssh [NONROOT_USER]@[IP_ADDRESS] # By default, OpenSSH does not allow logging in as root
  • +
  • If you mess up the configuration of a VM, you may wish to completely delete it and start fresh with a new one. THIS WILL DELETE ALL DATA ASSOCIATED WITH THE VM INCLUDING ANYTHING IN YOUR AIO DATADIR! If you are sure you would like to do this, run (on the host physical machine):
    virsh destroy --domain [VM_NAME] ; virsh undefine --nvram --domain [VM_NAME] && rm -rfi /var/lib/libvirt/images/[VM_NAME].qcow2
  • +
  • Using Nextcloud Talk will require some extra configuration. Back when you set up your VMs, they were (by default) configured with NAT, meaning they are in their own subnet. The VMs must each instead be bridged, so that your router may directly "see" them (as if they were real, physical devices on your network), and each AIO instance inside each VM must be configured with a different Talk port (like 3478, 3479, etc.). You should have already set these port numbers (back when you first configured the VM in step 4 above), but if you still need to set (or want to change) these values, you can remove the mastercontainer and re-run the initial "docker run" command with a modified Talk port like so. Then, the Talk port for EACH instance needs to be forwarded in your router's settings DIRECTLY to the VM hosting the instance (completely bypassing your host physical machine/reverse proxy). And finally, inside an admin-privileged account (such as the default "admin" account) in each instance, you must visit https://[DOMAIN_NAME]/settings/admin/talk then find the STUN/TURN Settings, and from there set the proper values. If this is too complicated, it may be easier to use public STUN/TURN servers, but I have not tested any of this, rather I'm just sharing what I have found so far (more info available here). If you have figured this out or if any of this information is incorrect, please edit this section!
  • +
  • Configuring daily automatic backups is a bit more involved with this setup. But for the occasional manual borg backup, you can connect a physical SSD/HDD via a cheap USB SATA adapter/dock to a free USB port on your host physical machine, and then use these commands to pass the disk through to a VM of your choosing (on the host physical machine and on the VM):
    virsh attach-device --live --domain [VM_NAME] --file [USB_DEVICE_DEFINITION.xml]
    +   virsh console --domain [VM_NAME]
    +   # (Login to the VM with root privileges)
    +   mkdir -p /mnt/[MOUNT_NAME]
    +   mount /dev/disk/by-label/[DISK_NAME] /mnt/[MOUNT_NAME]
  • + To create the XML device definition file, see this short guide. An SSD/HDD is recommended, but nothing is stopping you from using something as simple as a flash drive for testing if you really want. Finally, to actually perform a manual backup, make sure your disk is properly mounted and then simply use the AIO interface to perform the backup. +
  • If you want to shave off around 8-10 seconds of total boot time when you reboot your host physical machine, a simple trick is to lower the GRUB_TIMEOUT from the default five seconds to one second, on both the host physical machine and each of the VMs. You can also remove the delay, but it's generally safer to leave at least one second. (Always be extremely careful when editing GRUB config, especially on the host physical machine, as an incorrect configuration can prevent your device from booting!)
  • +
+
diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/Chart.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/Chart.yaml new file mode 100755 index 0000000..03627c2 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/Chart.yaml @@ -0,0 +1,13 @@ +name: nextcloud-aio-helm-chart +description: A generated Helm Chart for Nextcloud AIO from Skippbox Kompose +version: 11.9.0 +apiVersion: v2 +keywords: + - latest + - nextcloud + - helm-chart + - open-source + - cloud +sources: + - https://github.com/nextcloud/all-in-one/tree/main/nextcloud-aio-helm-chart +home: https://github.com/nextcloud/all-in-one/tree/main/nextcloud-aio-helm-chart diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/readme.md b/nextcloud-aio/nextcloud-aio-helm-chart/readme.md new file mode 100755 index 0000000..edf6c77 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/readme.md @@ -0,0 +1,45 @@ +# Nextcloud AIO Helm-chart + +> [!NOTE] +> For an enterprise-ready and scalable deployment method based on Helm Charts (also available for Podman), please [contact Nextcloud GmbH](https://nextcloud.com/enterprise/). + +> [!IMPORTANT] +> This Helm-Chart is not intended to be used with Ingress as it handles TLS itself via the built-in apache container and exposes a Loadbalancer port itself on the Cluster. See the [apache service](https://github.com/nextcloud/all-in-one/blob/main/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml). However if the Cluster is used behind NAT, you can adjust `APACHE_PORT` to a different one than 443 and do the TLS offloading on an external Reverse Proxy that forwards the traffic to the configured port via http. If you really need the Ingress feature, please [contact Nextcloud GmbH](https://nextcloud.com/enterprise/) as we offer an enterprise-ready and scalable deployment method based on Helm Charts that also allows Ingress to be used. + +You can run the containers that are build for AIO with Kubernetes using this Helm chart. This comes with a few downsides, that are discussed below. + +### Advantages +- You can run it without a container having access to the docker socket +- You can run the containers with Kubernetes + +### Disadvantages +- You lose the AIO interface +- You lose update notifications and automatic updates +- You lose all AIO backup and restore features +- You lose all community containers: https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers +- **You need to know what you are doing** +- For updating, you need to strictly follow the at the bottom described update routine +- You need to monitor yourself if the volumes have enough free space and increase them if they don't by adjusting their size in values.yaml +- Probably more + +## How to use this? + +First download this file: https://raw.githubusercontent.com/nextcloud/all-in-one/main/nextcloud-aio-helm-chart/values.yaml and adjust at least all values marked with `# TODO!`
+⚠️ **Warning**: Do not use the symbols `@` and `:` in your passwords. These symbols are used to build database connection strings. You will experience issues when using these symbols! + +Then run: + +``` +helm repo add nextcloud-aio https://nextcloud.github.io/all-in-one/ +helm install nextcloud-aio nextcloud-aio/nextcloud-aio-helm-chart -f values.yaml +``` + +And after a while, everything should be set up. + +## How to update? +Since the values of this helm chart may change in the future, it is highly recommended to strictly follow the following procedure whenever you want to upgrade it. +1. Stop all running pods +1. Back up all volumes that got created by the Helm chart and the values.yaml file +1. Run `helm repo update nextcloud-aio` in order to get the updated yaml files from the repository +1. Now download the updated values.yaml file from https://raw.githubusercontent.com/nextcloud/all-in-one/main/nextcloud-aio-helm-chart/values.yaml and compare that with the one that you currently have locally. Look for variables that changed or got added. You can use the diff command to compare them. +1. After the file update was successful, simply run `helm install my-release nextcloud-aio/nextcloud-aio-helm-chart -f values.yaml` to update to the new version. diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-deployment.yaml new file mode 100755 index 0000000..992e66d --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-deployment.yaml @@ -0,0 +1,107 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-apache + name: nextcloud-aio-apache + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-apache + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-apache + spec: + securityContext: + # The items below only work in pod context + fsGroup: 33 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 33 + runAsGroup: 33 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" + - name: APACHE_HOST + value: nextcloud-aio-apache + - name: APACHE_MAX_SIZE + value: "{{ .Values.APACHE_MAX_SIZE }}" + - name: APACHE_MAX_TIME + value: "{{ .Values.NEXTCLOUD_MAX_TIME }}" + - name: APACHE_PORT + value: "{{ .Values.APACHE_PORT }}" + - name: COLLABORA_HOST + value: nextcloud-aio-collabora + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: NEXTCLOUD_HOST + value: nextcloud-aio-nextcloud + - name: NOTIFY_PUSH_HOST + value: nextcloud-aio-notify-push + - name: ONLYOFFICE_HOST + value: nextcloud-aio-onlyoffice + - name: TALK_HOST + value: nextcloud-aio-talk + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: WHITEBOARD_HOST + value: nextcloud-aio-whiteboard + image: ghcr.io/nextcloud-releases/aio-apache:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-apache + ports: + - containerPort: {{ .Values.APACHE_PORT }} + protocol: TCP + - containerPort: {{ .Values.APACHE_PORT }} + protocol: UDP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + add: ["NET_BIND_SERVICE"] + volumeMounts: + - mountPath: /var/www/html + name: nextcloud-aio-nextcloud + readOnly: true + - mountPath: /mnt/data + name: nextcloud-aio-apache + volumes: + - name: nextcloud-aio-nextcloud + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud + - name: nextcloud-aio-apache + persistentVolumeClaim: + claimName: nextcloud-aio-apache diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-persistentvolumeclaim.yaml new file mode 100755 index 0000000..773d198 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-apache + name: nextcloud-aio-apache + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.APACHE_STORAGE_SIZE }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml new file mode 100755 index 0000000..404ee62 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-apache-service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-apache + name: nextcloud-aio-apache + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + type: LoadBalancer + externalTrafficPolicy: Local + ports: + - name: "{{ .Values.APACHE_PORT }}" + port: {{ .Values.APACHE_PORT }} + targetPort: {{ .Values.APACHE_PORT }} + - name: {{ .Values.APACHE_PORT }}-udp + port: {{ .Values.APACHE_PORT }} + protocol: UDP + targetPort: {{ .Values.APACHE_PORT }} + selector: + io.kompose.service: nextcloud-aio-apache diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-deployment.yaml new file mode 100755 index 0000000..2e9ccb9 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-deployment.yaml @@ -0,0 +1,100 @@ +{{- if eq .Values.CLAMAV_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-clamav + name: nextcloud-aio-clamav + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-clamav + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-clamav + spec: + securityContext: + # The items below only work in pod context + fsGroup: 100 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 100 + runAsGroup: 100 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:20250927_081431 + command: + - mkdir + - "-p" + - /nextcloud-aio-clamav/data + volumeMounts: + - name: nextcloud-aio-clamav + mountPath: /nextcloud-aio-clamav + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + containers: + - env: + - name: MAX_SIZE + value: "{{ .Values.NEXTCLOUD_UPLOAD_LIMIT }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-clamav:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-clamav + ports: + - containerPort: 3310 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /var/lib/clamav + subPath: data + name: nextcloud-aio-clamav + volumes: + - name: nextcloud-aio-clamav + persistentVolumeClaim: + claimName: nextcloud-aio-clamav +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-persistentvolumeclaim.yaml new file mode 100755 index 0000000..ebb1968 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.CLAMAV_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-clamav + name: nextcloud-aio-clamav + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.CLAMAV_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-service.yaml new file mode 100755 index 0000000..8dc8597 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-clamav-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.CLAMAV_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-clamav + name: nextcloud-aio-clamav + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "3310" + port: 3310 + targetPort: 3310 + selector: + io.kompose.service: nextcloud-aio-clamav +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-deployment.yaml new file mode 100755 index 0000000..07f0922 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-deployment.yaml @@ -0,0 +1,67 @@ +{{- if eq .Values.COLLABORA_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-collabora + name: nextcloud-aio-collabora + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-collabora + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-collabora + spec: + containers: + - args: {{ .Values.ADDITIONAL_COLLABORA_OPTIONS | default list | toJson }} + env: + - name: DONT_GEN_SSL_CERT + value: "1" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: aliasgroup1 + value: https://{{ .Values.NC_DOMAIN }}:443,http://nextcloud-aio-apache:23973 + - name: dictionaries + value: "{{ .Values.COLLABORA_DICTIONARIES }}" + - name: extra_params + value: --o:ssl.enable=false --o:ssl.termination=true --o:mount_jail_tree=false --o:logging.level=warning --o:logging.level_startup=warning --o:home_mode.enable=true --o:remote_font_config.url=https://{{ .Values.NC_DOMAIN }}/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+ + - name: server_name + value: "{{ .Values.NC_DOMAIN }}" + image: ghcr.io/nextcloud-releases/aio-collabora:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-collabora + ports: + - containerPort: 9980 + protocol: TCP + securityContext: + capabilities: + add: + - MKNOD + - CAP_SYS_ADMIN + - SYS_CHROOT + - FOWNER + - CHOWN +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-service.yaml new file mode 100755 index 0000000..ebe7bf3 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-collabora-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.COLLABORA_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-collabora + name: nextcloud-aio-collabora + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9980" + port: 9980 + targetPort: 9980 + selector: + io.kompose.service: nextcloud-aio-collabora +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-deployment.yaml new file mode 100755 index 0000000..abfa8b0 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-deployment.yaml @@ -0,0 +1,108 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-database + name: nextcloud-aio-database + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-database + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-database + spec: + securityContext: + # The items below only work in pod context + fsGroup: 999 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 999 + runAsGroup: 999 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:20250927_081431 + command: + - mkdir + - "-p" + - /nextcloud-aio-database/data + volumeMounts: + - name: nextcloud-aio-database + mountPath: /nextcloud-aio-database + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + containers: + - env: + - name: PGTZ + value: "{{ .Values.TIMEZONE }}" + - name: POSTGRES_DB + value: nextcloud_database + - name: POSTGRES_PASSWORD + value: "{{ .Values.DATABASE_PASSWORD }}" + - name: POSTGRES_USER + value: nextcloud + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-postgresql:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-database + ports: + - containerPort: 5432 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /var/lib/postgresql/data + subPath: data + name: nextcloud-aio-database + - mountPath: /mnt/data + name: nextcloud-aio-database-dump + terminationGracePeriodSeconds: 1800 + volumes: + - name: nextcloud-aio-database + persistentVolumeClaim: + claimName: nextcloud-aio-database + - name: nextcloud-aio-database-dump + persistentVolumeClaim: + claimName: nextcloud-aio-database-dump diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-dump-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-dump-persistentvolumeclaim.yaml new file mode 100755 index 0000000..4913545 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-dump-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-database-dump + name: nextcloud-aio-database-dump + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.DATABASE_DUMP_STORAGE_SIZE }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-persistentvolumeclaim.yaml new file mode 100755 index 0000000..7b753e2 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-database + name: nextcloud-aio-database + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.DATABASE_STORAGE_SIZE }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-service.yaml new file mode 100755 index 0000000..9451d90 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-database-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-database + name: nextcloud-aio-database + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "5432" + port: 5432 + targetPort: 5432 + selector: + io.kompose.service: nextcloud-aio-database diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml new file mode 100755 index 0000000..44458a8 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.FULLTEXTSEARCH_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-elasticsearch + name: nextcloud-aio-elasticsearch + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.ELASTICSEARCH_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-deployment.yaml new file mode 100755 index 0000000..9dcc9d6 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-deployment.yaml @@ -0,0 +1,85 @@ +{{- if eq .Values.FULLTEXTSEARCH_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-fulltextsearch + name: nextcloud-aio-fulltextsearch + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-fulltextsearch + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-fulltextsearch + spec: + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:20250927_081431 + command: + - chmod + - "777" + - /nextcloud-aio-elasticsearch + volumeMounts: + - name: nextcloud-aio-elasticsearch + mountPath: /nextcloud-aio-elasticsearch + containers: + - env: + - name: ES_JAVA_OPTS + value: "{{ .Values.FULLTEXTSEARCH_JAVA_OPTIONS | default "-Xms512M -Xmx512M" }}" + - name: FULLTEXTSEARCH_PASSWORD + value: "{{ .Values.FULLTEXTSEARCH_PASSWORD }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: bootstrap.memory_lock + value: "true" + - name: cluster.name + value: nextcloud-aio + - name: discovery.type + value: single-node + - name: http.port + value: "9200" + - name: logger.level + value: WARN + - name: xpack.license.self_generated.type + value: basic + - name: xpack.security.enabled + value: "false" + image: ghcr.io/nextcloud-releases/aio-fulltextsearch:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 5 + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 5 + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + name: nextcloud-aio-fulltextsearch + ports: + - containerPort: 9200 + protocol: TCP + volumeMounts: + - mountPath: /usr/share/elasticsearch/data + name: nextcloud-aio-elasticsearch + volumes: + - name: nextcloud-aio-elasticsearch + persistentVolumeClaim: + claimName: nextcloud-aio-elasticsearch +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-service.yaml new file mode 100755 index 0000000..ae75947 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-fulltextsearch-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.FULLTEXTSEARCH_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-fulltextsearch + name: nextcloud-aio-fulltextsearch + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9200" + port: 9200 + targetPort: 9200 + selector: + io.kompose.service: nextcloud-aio-fulltextsearch +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-deployment.yaml new file mode 100755 index 0000000..5e54704 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-deployment.yaml @@ -0,0 +1,69 @@ +{{- if eq .Values.IMAGINARY_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-imaginary + name: nextcloud-aio-imaginary + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-imaginary + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-imaginary + spec: + securityContext: + # The items below only work in pod context + fsGroup: 65534 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 65534 + runAsGroup: 65534 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: IMAGINARY_SECRET + value: "{{ .Values.IMAGINARY_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-imaginary:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-imaginary + ports: + - containerPort: 9000 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-service.yaml new file mode 100755 index 0000000..a5fb326 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-imaginary-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.IMAGINARY_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-imaginary + name: nextcloud-aio-imaginary + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9000" + port: 9000 + targetPort: 9000 + selector: + io.kompose.service: nextcloud-aio-imaginary +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-namespace-namespace.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-namespace-namespace.yaml new file mode 100755 index 0000000..212715e --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-namespace-namespace.yaml @@ -0,0 +1,11 @@ +{{- if and (ne .Values.NAMESPACE "default") (ne .Values.NAMESPACE_DISABLED "yes") }} +apiVersion: v1 +kind: Namespace +metadata: + name: "{{ .Values.NAMESPACE }}" + namespace: "{{ .Values.NAMESPACE }}" + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + labels: + pod-security.kubernetes.io/enforce: restricted + {{- end }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-networkpolicy.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-networkpolicy.yaml new file mode 100755 index 0000000..c54f880 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-networkpolicy.yaml @@ -0,0 +1,36 @@ +{{- if eq .Values.NETWORK_POLICY_ENABLED "yes" }} +# https://github.com/ahmetb/kubernetes-network-policy-recipes/blob/master/04-deny-traffic-from-other-namespaces.md +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-deny-from-other-namespaces +spec: + podSelector: + matchLabels: + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: {} + egress: + - {} # Allows all egress traffic +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-webserver-allow +spec: + podSelector: + matchExpressions: + - key: io.kompose.service + operator: In + values: + - nextcloud-aio-apache + policyTypes: + - Ingress + ingress: + - {} # Allows all ingress traffic +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml new file mode 100755 index 0000000..62794e3 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-nextcloud-data + name: nextcloud-aio-nextcloud-data + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS_DATA }} + storageClassName: {{ .Values.STORAGE_CLASS_DATA }} + {{- else if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.NEXTCLOUD_DATA_STORAGE_SIZE }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml new file mode 100755 index 0000000..1644464 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-deployment.yaml @@ -0,0 +1,241 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-nextcloud + name: nextcloud-aio-nextcloud + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-nextcloud + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-nextcloud + spec: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment! + securityContext: + # The items below only work in pod context + fsGroup: 33 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 33 + runAsGroup: 33 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + {{- end }} # AIO-config - do not change this comment! +# AIO settings start # Do not remove or change this line! + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:20250927_081431 + command: + - chmod + - "777" + - /nextcloud-aio-nextcloud + - /nextcloud-aio-nextcloud-trusted-cacerts + volumeMounts: + - name: nextcloud-aio-nextcloud-trusted-cacerts + mountPath: /nextcloud-aio-nextcloud-trusted-cacerts + - name: nextcloud-aio-nextcloud + mountPath: /nextcloud-aio-nextcloud +# AIO settings end # Do not remove or change this line! + containers: + - env: + - name: SMTP_HOST + value: "{{ .Values.SMTP_HOST }}" + - name: SMTP_SECURE + value: "{{ .Values.SMTP_SECURE }}" + - name: SMTP_PORT + value: "{{ .Values.SMTP_PORT }}" + - name: SMTP_AUTHTYPE + value: "{{ .Values.SMTP_AUTHTYPE }}" + - name: SMTP_NAME + value: "{{ .Values.SMTP_NAME }}" + - name: SMTP_PASSWORD + value: "{{ .Values.SMTP_PASSWORD }}" + - name: MAIL_FROM_ADDRESS + value: "{{ .Values.MAIL_FROM_ADDRESS }}" + - name: MAIL_DOMAIN + value: "{{ .Values.MAIL_DOMAIN }}" + - name: SUBSCRIPTION_KEY + value: "{{ .Values.SUBSCRIPTION_KEY }}" + - name: APPS_ALLOWLIST + value: "{{ .Values.APPS_ALLOWLIST }}" + - name: ADDITIONAL_TRUSTED_PROXY + value: "{{ .Values.ADDITIONAL_TRUSTED_PROXY }}" + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" + - name: SERVERINFO_TOKEN + value: "{{ .Values.SERVERINFO_TOKEN }}" + - name: NEXTCLOUD_DEFAULT_QUOTA + value: "{{ .Values.NEXTCLOUD_DEFAULT_QUOTA }}" + - name: NEXTCLOUD_SKELETON_DIRECTORY + value: "{{ .Values.NEXTCLOUD_SKELETON_DIRECTORY }}" + - name: NEXTCLOUD_MAINTENANCE_WINDOW + value: "{{ .Values.NEXTCLOUD_MAINTENANCE_WINDOW }}" + - name: ADDITIONAL_APKS + value: "{{ .Values.NEXTCLOUD_ADDITIONAL_APKS }}" + - name: ADDITIONAL_PHP_EXTENSIONS + value: "{{ .Values.NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS }}" + - name: ADMIN_PASSWORD + value: "{{ .Values.NEXTCLOUD_PASSWORD }}" + - name: ADMIN_USER + value: admin + - name: APACHE_HOST + value: nextcloud-aio-apache + - name: APACHE_PORT + value: "{{ .Values.APACHE_PORT }}" + - name: CLAMAV_ENABLED + value: "{{ .Values.CLAMAV_ENABLED }}" + - name: CLAMAV_HOST + value: nextcloud-aio-clamav + - name: CLAMAV_MAX_SIZE + value: "{{ .Values.APACHE_MAX_SIZE }}" + - name: COLLABORA_ENABLED + value: "{{ .Values.COLLABORA_ENABLED }}" + - name: COLLABORA_HOST + value: nextcloud-aio-collabora + - name: FULLTEXTSEARCH_ENABLED + value: "{{ .Values.FULLTEXTSEARCH_ENABLED }}" + - name: FULLTEXTSEARCH_HOST + value: nextcloud-aio-fulltextsearch + - name: FULLTEXTSEARCH_INDEX + value: nextcloud-aio + - name: FULLTEXTSEARCH_PASSWORD + value: "{{ .Values.FULLTEXTSEARCH_PASSWORD }}" + - name: FULLTEXTSEARCH_PORT + value: "9200" + - name: FULLTEXTSEARCH_USER + value: elastic + - name: IMAGINARY_ENABLED + value: "{{ .Values.IMAGINARY_ENABLED }}" + - name: IMAGINARY_HOST + value: nextcloud-aio-imaginary + - name: IMAGINARY_SECRET + value: "{{ .Values.IMAGINARY_SECRET }}" + - name: INSTALL_LATEST_MAJOR + value: "{{ .Values.INSTALL_LATEST_MAJOR }}" + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: NEXTCLOUD_DATA_DIR + value: /mnt/ncdata + - name: NEXTCLOUD_HOST + value: nextcloud-aio-nextcloud + - name: ONLYOFFICE_ENABLED + value: "{{ .Values.ONLYOFFICE_ENABLED }}" + - name: ONLYOFFICE_HOST + value: nextcloud-aio-onlyoffice + - name: ONLYOFFICE_SECRET + value: "{{ .Values.ONLYOFFICE_SECRET }}" + - name: OVERWRITEPROTOCOL + value: https + - name: PHP_MAX_TIME + value: "{{ .Values.NEXTCLOUD_MAX_TIME }}" + - name: PHP_MEMORY_LIMIT + value: "{{ .Values.NEXTCLOUD_MEMORY_LIMIT }}" + - name: PHP_UPLOAD_LIMIT + value: "{{ .Values.NEXTCLOUD_UPLOAD_LIMIT }}" + - name: POSTGRES_DB + value: nextcloud_database + - name: POSTGRES_HOST + value: nextcloud-aio-database + - name: POSTGRES_PASSWORD + value: "{{ .Values.DATABASE_PASSWORD }}" + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: nextcloud + - name: RECORDING_SECRET + value: "{{ .Values.RECORDING_SECRET }}" + - name: REDIS_HOST + value: nextcloud-aio-redis + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: REMOVE_DISABLED_APPS + value: "{{ .Values.REMOVE_DISABLED_APPS }}" + - name: SIGNALING_SECRET + value: "{{ .Values.SIGNALING_SECRET }}" + - name: STARTUP_APPS + value: "{{ .Values.NEXTCLOUD_STARTUP_APPS }}" + - name: TALK_ENABLED + value: "{{ .Values.TALK_ENABLED }}" + - name: TALK_PORT + value: "{{ .Values.TALK_PORT }}" + - name: TALK_RECORDING_ENABLED + value: "{{ .Values.TALK_RECORDING_ENABLED }}" + - name: TALK_RECORDING_HOST + value: nextcloud-aio-talk-recording + - name: TRUSTED_CACERTS_DIR + value: "{{ .Values.NEXTCLOUD_TRUSTED_CACERTS_DIR }}" + - name: TURN_SECRET + value: "{{ .Values.TURN_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + - name: UPDATE_NEXTCLOUD_APPS + value: "{{ .Values.UPDATE_NEXTCLOUD_APPS }}" + - name: WHITEBOARD_ENABLED + value: "{{ .Values.WHITEBOARD_ENABLED }}" + - name: WHITEBOARD_SECRET + value: "{{ .Values.WHITEBOARD_SECRET }}" + image: ghcr.io/nextcloud-releases/aio-nextcloud:20250927_081431 + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment! + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + {{- end }} # AIO-config - do not change this comment! + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-nextcloud + ports: + - containerPort: 9000 + protocol: TCP + - containerPort: 9001 + protocol: TCP + volumeMounts: + - mountPath: /var/www/html + name: nextcloud-aio-nextcloud + - mountPath: /mnt/ncdata + name: nextcloud-aio-nextcloud-data + - mountPath: /usr/local/share/ca-certificates + name: nextcloud-aio-nextcloud-trusted-cacerts + readOnly: true + terminationGracePeriodSeconds: 600 + volumes: + - name: nextcloud-aio-nextcloud + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud + - name: nextcloud-aio-nextcloud-data + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud-data + - name: nextcloud-aio-nextcloud-trusted-cacerts + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud-trusted-cacerts diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-persistentvolumeclaim.yaml new file mode 100755 index 0000000..ee55be2 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-nextcloud + name: nextcloud-aio-nextcloud + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteMany + resources: + requests: + storage: {{ .Values.NEXTCLOUD_STORAGE_SIZE }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-service.yaml new file mode 100755 index 0000000..18cf84d --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-nextcloud + name: nextcloud-aio-nextcloud + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "9000" + port: 9000 + targetPort: 9000 + - name: "9001" + port: 9001 + targetPort: 9001 + selector: + io.kompose.service: nextcloud-aio-nextcloud diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-trusted-cacerts-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-trusted-cacerts-persistentvolumeclaim.yaml new file mode 100755 index 0000000..d18f7a8 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-nextcloud-trusted-cacerts-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-nextcloud-trusted-cacerts + name: nextcloud-aio-nextcloud-trusted-cacerts + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.NEXTCLOUD_TRUSTED_CACERTS_STORAGE_SIZE }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-deployment.yaml new file mode 100755 index 0000000..799e439 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-deployment.yaml @@ -0,0 +1,93 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-notify-push + name: nextcloud-aio-notify-push + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-notify-push + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-notify-push + spec: + securityContext: + # The items below only work in pod context + fsGroup: 33 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 33 + runAsGroup: 33 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: NEXTCLOUD_HOST + value: nextcloud-aio-nextcloud + - name: POSTGRES_DB + value: nextcloud_database + - name: POSTGRES_HOST + value: nextcloud-aio-database + - name: POSTGRES_PASSWORD + value: "{{ .Values.DATABASE_PASSWORD }}" + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: nextcloud + - name: REDIS_HOST + value: nextcloud-aio-redis + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-notify-push:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-notify-push + ports: + - containerPort: 7867 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /nextcloud + name: nextcloud-aio-nextcloud + readOnly: true + volumes: + - name: nextcloud-aio-nextcloud + persistentVolumeClaim: + claimName: nextcloud-aio-nextcloud diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-service.yaml new file mode 100755 index 0000000..2b7bfcc --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-notify-push-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-notify-push + name: nextcloud-aio-notify-push + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "7867" + port: 7867 + targetPort: 7867 + selector: + io.kompose.service: nextcloud-aio-notify-push diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-deployment.yaml new file mode 100755 index 0000000..820e684 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-deployment.yaml @@ -0,0 +1,73 @@ +{{- if eq .Values.ONLYOFFICE_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-onlyoffice + name: nextcloud-aio-onlyoffice + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-onlyoffice + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-onlyoffice + spec: + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:20250927_081431 + command: + - chmod + - "777" + - /nextcloud-aio-onlyoffice + volumeMounts: + - name: nextcloud-aio-onlyoffice + mountPath: /nextcloud-aio-onlyoffice + containers: + - env: + - name: JWT_ENABLED + value: "true" + - name: JWT_HEADER + value: AuthorizationJwt + - name: JWT_SECRET + value: "{{ .Values.ONLYOFFICE_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-onlyoffice:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 9 + initialDelaySeconds: 60 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-onlyoffice + ports: + - containerPort: 80 + protocol: TCP + volumeMounts: + - mountPath: /var/lib/onlyoffice + name: nextcloud-aio-onlyoffice + volumes: + - name: nextcloud-aio-onlyoffice + persistentVolumeClaim: + claimName: nextcloud-aio-onlyoffice +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-persistentvolumeclaim.yaml new file mode 100755 index 0000000..80de727 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.ONLYOFFICE_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-onlyoffice + name: nextcloud-aio-onlyoffice + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.ONLYOFFICE_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-service.yaml new file mode 100755 index 0000000..6ff9afa --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-onlyoffice-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.ONLYOFFICE_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-onlyoffice + name: nextcloud-aio-onlyoffice + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "80" + port: 80 + targetPort: 80 + selector: + io.kompose.service: nextcloud-aio-onlyoffice +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-deployment.yaml new file mode 100755 index 0000000..015da80 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-deployment.yaml @@ -0,0 +1,76 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-redis + name: nextcloud-aio-redis + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-redis + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-redis + spec: + securityContext: + # The items below only work in pod context + fsGroup: 999 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 999 + runAsGroup: 999 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-redis:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-redis + ports: + - containerPort: 6379 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /data + name: nextcloud-aio-redis + volumes: + - name: nextcloud-aio-redis + persistentVolumeClaim: + claimName: nextcloud-aio-redis diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-persistentvolumeclaim.yaml new file mode 100755 index 0000000..51b4f58 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-persistentvolumeclaim.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-redis + name: nextcloud-aio-redis + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.REDIS_STORAGE_SIZE }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-service.yaml new file mode 100755 index 0000000..af82a0b --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-redis-service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-redis + name: nextcloud-aio-redis + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "6379" + port: 6379 + targetPort: 6379 + selector: + io.kompose.service: nextcloud-aio-redis diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-deployment.yaml new file mode 100755 index 0000000..bb6f2a1 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-deployment.yaml @@ -0,0 +1,87 @@ +{{- if eq .Values.TALK_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-talk + name: nextcloud-aio-talk + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-talk + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-talk + spec: + securityContext: + # The items below only work in pod context + fsGroup: 1000 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: TALK_MAX_STREAM_BITRATE + value: "{{ .Values.TALK_MAX_STREAM_BITRATE }}" + - name: TALK_MAX_SCREEN_BITRATE + value: "{{ .Values.TALK_MAX_SCREEN_BITRATE }}" + - name: INTERNAL_SECRET + value: "{{ .Values.TALK_INTERNAL_SECRET }}" + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: SIGNALING_SECRET + value: "{{ .Values.SIGNALING_SECRET }}" + - name: TALK_HOST + value: nextcloud-aio-talk + - name: TALK_PORT + value: "{{ .Values.TALK_PORT }}" + - name: TURN_SECRET + value: "{{ .Values.TURN_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-talk:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-talk + ports: + - containerPort: {{ .Values.TALK_PORT }} + protocol: TCP + - containerPort: {{ .Values.TALK_PORT }} + protocol: UDP + - containerPort: 8081 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-deployment.yaml new file mode 100755 index 0000000..d59c60c --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-deployment.yaml @@ -0,0 +1,82 @@ +{{- if eq .Values.TALK_RECORDING_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-talk-recording + name: nextcloud-aio-talk-recording + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-talk-recording + strategy: + type: Recreate + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-talk-recording + spec: + securityContext: + # The items below only work in pod context + fsGroup: 122 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 122 + runAsGroup: 122 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: INTERNAL_SECRET + value: "{{ .Values.TALK_INTERNAL_SECRET }}" + - name: NC_DOMAIN + value: "{{ .Values.NC_DOMAIN }}" + - name: RECORDING_SECRET + value: "{{ .Values.RECORDING_SECRET }}" + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-talk-recording:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-talk-recording + ports: + - containerPort: 1234 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + volumeMounts: + - mountPath: /tmp + name: nextcloud-aio-talk-recording + volumes: + - name: nextcloud-aio-talk-recording + persistentVolumeClaim: + claimName: nextcloud-aio-talk-recording +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-persistentvolumeclaim.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-persistentvolumeclaim.yaml new file mode 100755 index 0000000..5996144 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-persistentvolumeclaim.yaml @@ -0,0 +1,18 @@ +{{- if eq .Values.TALK_RECORDING_ENABLED "yes" }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + io.kompose.service: nextcloud-aio-talk-recording + name: nextcloud-aio-talk-recording + namespace: "{{ .Values.NAMESPACE }}" +spec: + {{- if .Values.STORAGE_CLASS }} + storageClassName: {{ .Values.STORAGE_CLASS }} + {{- end }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.TALK_RECORDING_STORAGE_SIZE }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-service.yaml new file mode 100755 index 0000000..4410ed7 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-recording-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.TALK_RECORDING_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-talk-recording + name: nextcloud-aio-talk-recording + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "1234" + port: 1234 + targetPort: 1234 + selector: + io.kompose.service: nextcloud-aio-talk-recording +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-service.yaml new file mode 100755 index 0000000..675a272 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-talk-service.yaml @@ -0,0 +1,44 @@ +{{- if eq .Values.TALK_ENABLED "yes" }} +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-talk + name: nextcloud-aio-talk-public + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + type: LoadBalancer + externalTrafficPolicy: Local + ports: + - name: "{{ .Values.TALK_PORT }}" + port: {{ .Values.TALK_PORT }} + targetPort: {{ .Values.TALK_PORT }} + - name: {{ .Values.TALK_PORT }}-udp + port: {{ .Values.TALK_PORT }} + protocol: UDP + targetPort: {{ .Values.TALK_PORT }} + selector: + io.kompose.service: nextcloud-aio-talk +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-talk + name: nextcloud-aio-talk + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "8081" + port: 8081 + targetPort: 8081 + selector: + io.kompose.service: nextcloud-aio-talk +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-deployment.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-deployment.yaml new file mode 100755 index 0000000..804c5d2 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-deployment.yaml @@ -0,0 +1,79 @@ +{{- if eq .Values.WHITEBOARD_ENABLED "yes" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-whiteboard + name: nextcloud-aio-whiteboard + namespace: "{{ .Values.NAMESPACE }}" +spec: + replicas: 1 + selector: + matchLabels: + io.kompose.service: nextcloud-aio-whiteboard + template: + metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-whiteboard + spec: + securityContext: + # The items below only work in pod context + fsGroup: 65534 + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: 65534 + runAsGroup: 65534 + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} + containers: + - env: + - name: BACKUP_DIR + value: /tmp + - name: JWT_SECRET_KEY + value: "{{ .Values.WHITEBOARD_SECRET }}" + - name: NEXTCLOUD_URL + value: https://{{ .Values.NC_DOMAIN }} + - name: REDIS_HOST + value: nextcloud-aio-redis + - name: REDIS_HOST_PASSWORD + value: "{{ .Values.REDIS_PASSWORD }}" + - name: STORAGE_STRATEGY + value: redis + - name: TZ + value: "{{ .Values.TIMEZONE }}" + image: ghcr.io/nextcloud-releases/aio-whiteboard:20250927_081431 + readinessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + livenessProbe: + exec: + command: + - /healthcheck.sh + failureThreshold: 3 + periodSeconds: 30 + timeoutSeconds: 30 + name: nextcloud-aio-whiteboard + ports: + - containerPort: 3002 + protocol: TCP + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-service.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-service.yaml new file mode 100755 index 0000000..8c8cb5a --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/templates/nextcloud-aio-whiteboard-service.yaml @@ -0,0 +1,19 @@ +{{- if eq .Values.WHITEBOARD_ENABLED "yes" }} +apiVersion: v1 +kind: Service +metadata: + annotations: + kompose.version: 1.37.0 (fb0539e64) + labels: + io.kompose.service: nextcloud-aio-whiteboard + name: nextcloud-aio-whiteboard + namespace: "{{ .Values.NAMESPACE }}" +spec: + ipFamilyPolicy: PreferDualStack + ports: + - name: "3002" + port: 3002 + targetPort: 3002 + selector: + io.kompose.service: nextcloud-aio-whiteboard +{{- end }} diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/update-helm.sh b/nextcloud-aio/nextcloud-aio-helm-chart/update-helm.sh new file mode 100755 index 0000000..8088285 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/update-helm.sh @@ -0,0 +1,531 @@ +#!/bin/bash + +[ -z "$1" ] && { echo "Error: Docker tag is not specified. Usage: ./nextcloud-aio-helm-chart/update-helm.sh "; exit 2; } + +DOCKER_TAG="$1" + +# The logic needs the files in ./helm-chart +cp -r ./nextcloud-aio-helm-chart ./helm-chart + +# Clean +rm -f ./helm-chart/values.yaml +rm -rf ./helm-chart/templates + +# Install kompose +curl -L https://github.com/kubernetes/kompose/releases/latest/download/kompose-linux-amd64 -o kompose +chmod +x kompose +sudo mv ./kompose /usr/local/bin/kompose + +# Install yq +sudo snap install yq + +set -ex + +# Conversion of docker-compose +cd manual-install +cp latest.yml latest.yml.backup + +# Additional config +# shellcheck disable=SC1083 +sed -i -E '/^( *- )(NET_RAW|SYS_NICE|MKNOD|SYS_ADMIN|CHOWN|SYS_CHROOT|FOWNER)$/!s/( *- )([A-Z_]+)$/\1\2=${\2}/' latest.yml +cp sample.conf /tmp/ +sed -i 's|^|export |' /tmp/sample.conf +# shellcheck disable=SC1091 +source /tmp/sample.conf +rm /tmp/sample.conf +sed -i '/OVERWRITEHOST/d' latest.yml +sed -i "s|:latest$|:$DOCKER_TAG|" latest.yml +sed -i "s|\${APACHE_IP_BINDING}:||" latest.yml +sed -i '/APACHE_IP_BINDING/d' latest.yml +sed -i "s|\${APACHE_PORT}:\${APACHE_PORT}/|$APACHE_PORT:$APACHE_PORT/|" latest.yml +sed -i "s|\${TALK_PORT}:\${TALK_PORT}/|$TALK_PORT:$TALK_PORT/|g" latest.yml +sed -i "s|- \${APACHE_PORT}|- $APACHE_PORT|" latest.yml +sed -i "s|- \${TALK_PORT}|- $TALK_PORT|" latest.yml +sed -i "s|\${NEXTCLOUD_DATADIR}|$NEXTCLOUD_DATADIR|" latest.yml +sed -i "s|\${ADDITIONAL_COLLABORA_OPTIONS}|ADDITIONAL_COLLABORA_OPTIONS_PLACEHOLDER|" latest.yml +sed -i "/name: nextcloud-aio/,$ d" latest.yml +sed -i "/NEXTCLOUD_DATADIR/d" latest.yml +sed -i "/\${NEXTCLOUD_MOUNT}/d" latest.yml +sed -i "/^volumes:/a\ \ nextcloud_aio_nextcloud_trusted_cacerts:\n \ \ \ \ name: nextcloud_aio_nextcloud_trusted_cacerts" latest.yml +sed -i "s|\${NEXTCLOUD_TRUSTED_CACERTS_DIR}:|nextcloud_aio_nextcloud_trusted_cacerts:|g#" latest.yml +sed -i 's/\${/{{ .Values./g; s/}/ }}/g' latest.yml +yq -i 'del(.services.[].profiles)' latest.yml +# Delete read_only and tmpfs setting while https://github.com/kubernetes/kubernetes/issues/48912 is not fixed +yq -i 'del(.services.[].read_only)' latest.yml +yq -i 'del(.services.[].tmpfs)' latest.yml +# Remove cap_drop in order to add it later again easier +yq -i 'del(.services.[].cap_drop)' latest.yml +# Remove SYS_NICE for imaginary as it is not supported with RPSS +yq -i 'del(.services."nextcloud-aio-imaginary".cap_add)' latest.yml +# cap SYS_ADMIN is called CAP_SYS_ADMIN in k8s +sed -i "s|- SYS_ADMIN$|- CAP_SYS_ADMIN|" latest.yml + +cat latest.yml +kompose convert -c -f latest.yml --namespace nextcloud-aio-namespace +cd latest + +if [ -f ./templates/manual-install-nextcloud-aio-networkpolicy.yaml ]; then + mv ./templates/manual-install-nextcloud-aio-networkpolicy.yaml ./templates/nextcloud-aio-networkpolicy.yaml +fi +# shellcheck disable=SC1083 +find ./ -name '*networkpolicy.yaml' -exec sed -i "s|manual-install-nextcloud-aio|nextcloud-aio|" \{} \; +cat << EOL > /tmp/initcontainers + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - chmod + - "777" + volumeMountsInitContainer: +EOL +cat << EOL > /tmp/initcontainers.database + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - mkdir + - "-p" + - /nextcloud-aio-database/data + volumeMounts: + - name: nextcloud-aio-database + mountPath: /nextcloud-aio-database + securityContext: +EOL +cat << EOL > /tmp/initcontainers.clamav + initContainers: + - name: init-subpath + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - mkdir + - "-p" + - /nextcloud-aio-clamav/data + volumeMounts: + - name: nextcloud-aio-clamav + mountPath: /nextcloud-aio-clamav + securityContext: +EOL +cat << EOL > /tmp/initcontainers.nextcloud +# AIO settings start # Do not remove or change this line! + initContainers: + - name: init-volumes + image: ghcr.io/nextcloud-releases/aio-alpine:$DOCKER_TAG + command: + - chmod + - "777" + volumeMountsInitContainer: +# AIO settings end # Do not remove or change this line! +EOL + +# shellcheck disable=SC1083 +DEPLOYMENTS="$(find ./ -name '*deployment.yaml')" +mapfile -t DEPLOYMENTS <<< "$DEPLOYMENTS" +for variable in "${DEPLOYMENTS[@]}"; do + if grep -q livenessProbe "$variable"; then + sed -n "/.*livenessProbe/,/timeoutSeconds.*/p" "$variable" > /tmp/liveness.probe + cat /tmp/liveness.probe + sed -i "s|livenessProbe|readinessProbe|" /tmp/liveness.probe + sed -i "/^ image:/r /tmp/liveness.probe" "$variable" + fi + if grep -q volumeMounts "$variable"; then + if echo "$variable" | grep -q database; then + sed -i "/^ spec:/r /tmp/initcontainers.database" "$variable" + elif echo "$variable" | grep -q clamav; then + sed -i "/^ spec:/r /tmp/initcontainers.clamav" "$variable" + elif echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + sed -i "/^ spec:/r /tmp/initcontainers.nextcloud" "$variable" + elif echo "$variable" | grep -q "fulltextsearch" || echo "$variable" | grep -q "onlyoffice" || echo "$variable" | grep -q "collabora"; then + sed -i "/^ spec:/r /tmp/initcontainers" "$variable" + fi + volumeNames="$(grep -A1 mountPath "$variable" | grep -v mountPath | sed 's|.*name: ||' | sed '/^--$/d')" + mapfile -t volumeNames <<< "$volumeNames" + for volumeName in "${volumeNames[@]}"; do + # The Nextcloud container runs as root user and sets the correct permissions automatically for the data-dir if the www-data user cannot write to it + if [ "$volumeName" != "nextcloud-aio-nextcloud-data" ]; then + sed -i "/^.*volumeMountsInitContainer:/i\ \ \ \ \ \ \ \ \ \ \ \ - /$volumeName" "$variable" + sed -i "/volumeMountsInitContainer:/a\ \ \ \ \ \ \ \ \ \ \ \ - name: $volumeName\n\ \ \ \ \ \ \ \ \ \ \ \ \ \ mountPath: /$volumeName" "$variable" + # Workaround for the database volume + if [ "$volumeName" = nextcloud-aio-database ]; then + sed -i "/mountPath: \/var\/lib\/postgresql\/data/a\ \ \ \ \ \ \ \ \ \ \ \ \ \ subPath: data" "$variable" + elif [ "$volumeName" = nextcloud-aio-clamav ]; then + sed -i "/mountPath: \/var\/lib\/clamav/a\ \ \ \ \ \ \ \ \ \ \ \ \ \ subPath: data" "$variable" + fi + + fi + done + sed -i "s|volumeMountsInitContainer:|volumeMounts:|" "$variable" + if grep -q claimName "$variable"; then + claimNames="$(grep claimName "$variable")" + mapfile -t claimNames <<< "$claimNames" + for claimName in "${claimNames[@]}"; do + if grep -A1 "^$claimName$" "$variable" | grep -q "readOnly: true"; then + sed -i "/^$claimName$/{n;d}" "$variable" + fi + done + fi + fi + if grep -q runAsUser "$variable" || echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + if echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + USER=33 + GROUP=33 + echo ' {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment!' > /tmp/pod.securityContext + else + USER="$(grep runAsUser "$variable" | grep -oP '[0-9]+')" + GROUP="$USER" + rm -f /tmp/pod.securityContext + fi + sed -i "/runAsUser:/d" "$variable" + sed -i "/capabilities:/d" "$variable" + if [ -n "$USER" ]; then + cat << EOL >> /tmp/pod.securityContext + securityContext: + # The items below only work in pod context + fsGroup: $USER + fsGroupChangePolicy: "OnRootMismatch" + # The items below work in both contexts + runAsUser: $USER + runAsGroup: $GROUP + runAsNonRoot: true + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + seccompProfile: + type: RuntimeDefault + {{- end }} +EOL + if echo "$variable" | grep -q "nextcloud-deployment.yaml"; then + echo " {{- end }} # AIO-config - do not change this comment!" >> /tmp/pod.securityContext + fi + sed -i "/^ spec:$/r /tmp/pod.securityContext" "$variable" + fi + fi +done +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i 's|nextcloud-aio-namespace|"\{\{ .Values.NAMESPACE \}\}"|' \{} \; +# shellcheck disable=SC1083 +find ./ -name '*service.yaml' -exec sed -i "/^status:/,$ d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "s|manual-install-nextcloud-aio|nextcloud-aio|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "/medium: Memory/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i "/kompose.cmd/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "s|emptyDir:|emptyDir: \{\}|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "/hostPort:/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "s|ReadOnlyMany|ReadWriteOnce|" \{} \; +# shellcheck disable=SC1083 +find ./ -name 'nextcloud-aio-nextcloud-persistentvolumeclaim.yaml' -exec sed -i "s|ReadWriteOnce|ReadWriteMany|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "/accessModes:/i\ \ {{- if .Values.STORAGE_CLASS }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "/accessModes:/i\ \ storageClassName: {{ .Values.STORAGE_CLASS }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*persistentvolumeclaim.yaml' -exec sed -i "/accessModes:/i\ \ {{- end }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name 'nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml' -exec sed -i "/{{- if .Values.STORAGE_CLASS }}/i\ {{- if .Values.STORAGE_CLASS_DATA }}\n storageClassName: {{ .Values.STORAGE_CLASS_DATA }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name 'nextcloud-aio-nextcloud-data-persistentvolumeclaim.yaml' -exec sed -i "s/{{- if .Values.STORAGE_CLASS }}/{{- else if .Values.STORAGE_CLASS }}/" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*deployment.yaml' -exec sed -i "/restartPolicy:/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*apache*' -exec sed -i "s|$APACHE_PORT|{{ .Values.APACHE_PORT }}|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*talk*' -exec sed -i "s|$TALK_PORT|{{ .Values.TALK_PORT }}|" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*apache-service.yaml' -exec sed -i "/^spec:/a\ \ type: LoadBalancer" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec sed -i "/^spec:/a\ \ type: LoadBalancer" \{} \; +echo '---' > /tmp/talk-service.copy +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec cat \{} \; >> /tmp/talk-service.copy +sed -i 's|name: nextcloud-aio-talk|name: nextcloud-aio-talk-public|' /tmp/talk-service.copy +# shellcheck disable=SC1083 +INTERNAL_TALK_PORTS="$(find ./ -name '*talk-deployment.yaml' -exec grep -oP 'containerPort: [0-9]+' \{} \;)" +mapfile -t INTERNAL_TALK_PORTS <<< "$INTERNAL_TALK_PORTS" +for port in "${INTERNAL_TALK_PORTS[@]}"; do + port="$(echo "$port" | grep -oP '[0-9]+')" + sed -i "/$port/d" /tmp/talk-service.copy +done +echo '---' >> /tmp/talk-service.copy +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec grep -v '{{ .Values.TALK.*}}\|protocol: UDP\|type: LoadBalancer' \{} \; >> /tmp/talk-service.copy +# shellcheck disable=SC1083 +find ./ -name '*talk-service.yaml' -exec mv /tmp/talk-service.copy \{} \; +# shellcheck disable=SC1083 +find ./ -name '*service.yaml' -exec sed -i "/type: LoadBalancer/a\ \ externalTrafficPolicy: Local" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*service.yaml' -exec sed -i "/^spec:/a\ \ ipFamilyPolicy: PreferDualStack" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i "s|'{{|\"{{|g;s|}}'|}}\"|g" \{} \; +# shellcheck disable=SC1083 +find ./ \( -not -name '*service.yaml' -name '*.yaml' \) -exec sed -i "/^status:/d" \{} \; +# shellcheck disable=SC1083 +find ./ \( -not -name '*persistentvolumeclaim.yaml' -name '*.yaml' \) -exec sed -i "/resources:/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name "*namespace.yaml" -exec sed -i "1i\\{{- if and \(ne .Values.NAMESPACE \"default\"\) \(ne .Values.NAMESPACE_DISABLED \"yes\"\) }}" \{} \; +# Additional config +cat << EOL > /tmp/additional-namespace.config + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + labels: + pod-security.kubernetes.io/enforce: restricted + {{- end }} +EOL +# shellcheck disable=SC1083 +find ./ -name "*namespace.yaml" -exec sed -i "/namespace.*/r /tmp/additional-namespace.config" \{} \; +# shellcheck disable=SC1083 +find ./ -name "*namespace.yaml" -exec sed -i "$ a {{- end }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*.yaml' -exec sed -i "/creationTimestamp: null/d" \{} \; +VOLUMES="$(find ./ -name '*persistentvolumeclaim.yaml' | sed 's|-persistentvolumeclaim.yaml||g;s|.*nextcloud-aio-||g' | sort)" +mapfile -t VOLUMES <<< "$VOLUMES" +for variable in "${VOLUMES[@]}"; do + name="$(echo "$variable" | sed 's|-|_|g' | tr '[:lower:]' '[:upper:]')_STORAGE_SIZE" + VOLUME_VARIABLE+=("$name") + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$variable-persistentvolumeclaim.yaml" -exec sed -i "s|storage: 100Mi|storage: {{ .Values.$name }}|" \{} \; +done + +# Additional config +cat << EOL > /tmp/additional.config + - name: SMTP_HOST + value: "{{ .Values.SMTP_HOST }}" + - name: SMTP_SECURE + value: "{{ .Values.SMTP_SECURE }}" + - name: SMTP_PORT + value: "{{ .Values.SMTP_PORT }}" + - name: SMTP_AUTHTYPE + value: "{{ .Values.SMTP_AUTHTYPE }}" + - name: SMTP_NAME + value: "{{ .Values.SMTP_NAME }}" + - name: SMTP_PASSWORD + value: "{{ .Values.SMTP_PASSWORD }}" + - name: MAIL_FROM_ADDRESS + value: "{{ .Values.MAIL_FROM_ADDRESS }}" + - name: MAIL_DOMAIN + value: "{{ .Values.MAIL_DOMAIN }}" + - name: SUBSCRIPTION_KEY + value: "{{ .Values.SUBSCRIPTION_KEY }}" + - name: APPS_ALLOWLIST + value: "{{ .Values.APPS_ALLOWLIST }}" + - name: ADDITIONAL_TRUSTED_PROXY + value: "{{ .Values.ADDITIONAL_TRUSTED_PROXY }}" + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" + - name: SERVERINFO_TOKEN + value: "{{ .Values.SERVERINFO_TOKEN }}" + - name: NEXTCLOUD_DEFAULT_QUOTA + value: "{{ .Values.NEXTCLOUD_DEFAULT_QUOTA }}" + - name: NEXTCLOUD_SKELETON_DIRECTORY + value: "{{ .Values.NEXTCLOUD_SKELETON_DIRECTORY }}" + - name: NEXTCLOUD_MAINTENANCE_WINDOW + value: "{{ .Values.NEXTCLOUD_MAINTENANCE_WINDOW }}" +EOL +# shellcheck disable=SC1083 +find ./ -name '*nextcloud-deployment.yaml' -exec sed -i "/^.*\- env:/r /tmp/additional.config" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*fulltextsearch-deployment.yaml' -exec sed -i 's/{{ .Values.FULLTEXTSEARCH_JAVA_OPTIONS }}/{{ .Values.FULLTEXTSEARCH_JAVA_OPTIONS | default "-Xms512M -Xmx512M" }}/' \{} \; + +# Additional config +cat << EOL > /tmp/additional-apache.config + - name: ADDITIONAL_TRUSTED_DOMAIN + value: "{{ .Values.ADDITIONAL_TRUSTED_DOMAIN }}" +EOL +# shellcheck disable=SC1083 +find ./ -name '*apache-deployment.yaml' -exec sed -i "/^.*\- env:/r /tmp/additional-apache.config" \{} \; + +# Additional config +cat << EOL > /tmp/additional-talk.config + - name: TALK_MAX_STREAM_BITRATE + value: "{{ .Values.TALK_MAX_STREAM_BITRATE }}" + - name: TALK_MAX_SCREEN_BITRATE + value: "{{ .Values.TALK_MAX_SCREEN_BITRATE }}" +EOL +# shellcheck disable=SC1083 +find ./ -name '*talk-deployment.yaml' -exec sed -i "/^.*\- env:/r /tmp/additional-talk.config" \{} \; + +cat << EOL > templates/nextcloud-aio-networkpolicy.yaml +{{- if eq .Values.NETWORK_POLICY_ENABLED "yes" }} +# https://github.com/ahmetb/kubernetes-network-policy-recipes/blob/master/04-deny-traffic-from-other-namespaces.md +kind: NetworkPolicy +apiVersion: networking.k8s.io/v1 +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-deny-from-other-namespaces +spec: + podSelector: + matchLabels: + policyTypes: + - Ingress + - Egress + ingress: + - from: + - podSelector: {} + egress: + - {} # Allows all egress traffic +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + namespace: "{{ .Values.NAMESPACE }}" + name: nextcloud-aio-webserver-allow +spec: + podSelector: + matchExpressions: + - key: io.kompose.service + operator: In + values: + - nextcloud-aio-apache + policyTypes: + - Ingress + ingress: + - {} # Allows all ingress traffic +{{- end }} +EOL + +cd ../ +mkdir -p ../helm-chart/ +rm latest/Chart.yaml +rm latest/README.md +mv latest/* ../helm-chart/ +rm -r latest +rm latest.yml +mv latest.yml.backup latest.yml + +# Get version of AIO +AIO_VERSION="$(grep 'Nextcloud AIO ' ../php/templates/containers.twig | grep -oP '[0-9]+.[0-9]+.[0-9]+')" +sed -i "s|^version:.*|version: $AIO_VERSION|" ../helm-chart/Chart.yaml + +# Conversion of sample.conf +cp sample.conf /tmp/ +sed -i 's|"||g' /tmp/sample.conf +sed -i 's|=|: |' /tmp/sample.conf +sed -i 's|= |: |' /tmp/sample.conf +sed -i '/^NEXTCLOUD_DATADIR/d' /tmp/sample.conf +sed -i '/^APACHE_IP_BINDING/d' /tmp/sample.conf +sed -i '/^NEXTCLOUD_MOUNT/d' /tmp/sample.conf +sed -i '/_ENABLED.*/s/ yes / "yes" /' /tmp/sample.conf +sed -i '/_ENABLED.*/s/ no / "no" /' /tmp/sample.conf +sed -i 's|^NEXTCLOUD_TRUSTED_CACERTS_DIR: .*|NEXTCLOUD_TRUSTED_CACERTS_DIR: # Setting this to any value allows to automatically import root certificates into the Nextcloud container|' /tmp/sample.conf +sed -i 's|17179869184|"17179869184"|' /tmp/sample.conf +# shellcheck disable=SC2129 +echo "" >> /tmp/sample.conf +# shellcheck disable=SC2129 +echo 'STORAGE_CLASS: # By setting this, you can adjust the storage class for your volumes. This should be a fast storage like SSD backed storage!' >> /tmp/sample.conf +echo 'STORAGE_CLASS_DATA: # Allows to set a dedicated storage class for the Nextcloud data volume. This can be a bit slower storage than the one above. ⚠️ Warning: only set this for new installations, not existing ones!' >> /tmp/sample.conf +for variable in "${VOLUME_VARIABLE[@]}"; do + echo "$variable: 1Gi # You can change the size of the $(echo "$variable" | sed 's|_STORAGE_SIZE||;s|_|-|g' | tr '[:upper:]' '[:lower:]') volume that default to 1Gi with this value" >> /tmp/sample.conf +done +sed -i "s|NEXTCLOUD_STORAGE_SIZE: 1Gi|NEXTCLOUD_STORAGE_SIZE: 5Gi|" /tmp/sample.conf +sed -i "s|NEXTCLOUD_DATA_STORAGE_SIZE: 1Gi|NEXTCLOUD_DATA_STORAGE_SIZE: 5Gi|" /tmp/sample.conf + +# Additional config +cat << ADDITIONAL_CONFIG >> /tmp/sample.conf + +NAMESPACE: default # By changing this, you can adjust the namespace of the installation which allows to install multiple instances on one kubernetes cluster +NAMESPACE_DISABLED: "no" # By setting this to "yes", you can disabled the creation of the namespace so that you can use a pre-created one +NETWORK_POLICY_ENABLED: "no" # By setting this to "yes", you can enable a network policy that limits network access to the same namespace. Except the Web server service which is reachable from all endpoints. +SUBSCRIPTION_KEY: # This allows to set the Nextcloud Enterprise key via ENV +SERVERINFO_TOKEN: # This allows to set the serverinfo app token for monitoring your Nextcloud via the serverinfo app +APPS_ALLOWLIST: # This allows to configure allowed apps that will be shown in Nextcloud's Appstore. You need to enter the app-IDs of the apps here and separate them with spaces. E.g. 'files richdocuments' +ADDITIONAL_TRUSTED_PROXY: # Allows to add one additional ip-address to Nextcloud's trusted proxies and to the Office WOPI-allowlist automatically. Set it e.g. like this: 'your.public.ip-address'. You can also use an ip-range here. +ADDITIONAL_TRUSTED_DOMAIN: # Allows to add one domain to Nextcloud's trusted domains and also generates a certificate automatically for it +NEXTCLOUD_DEFAULT_QUOTA: "10 GB" # Allows to adjust the default quota that will be taken into account in Nextcloud for new users. Setting it to "unlimited" will set it to unlimited +NEXTCLOUD_SKELETON_DIRECTORY: # Allows to adjust the sekeleton dir for Nextcloud. Setting it to "empty" will set the value to an empty string "" which will turn off the setting for new users in Nextcloud. +NEXTCLOUD_MAINTENANCE_WINDOW: # Allows to define the maintenance window for Nextcloud. See https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/background_jobs_configuration.html#parameters for possible values +SMTP_HOST: # (empty by default): The hostname of the SMTP server. +SMTP_SECURE: # (empty by default): Set to 'ssl' to use SSL, or 'tls' to use STARTTLS. +SMTP_PORT: # (default: '465' for SSL and '25' for non-secure connections): Optional port for the SMTP connection. Use '587' for an alternative port for STARTTLS. +SMTP_AUTHTYPE: # (default: 'LOGIN'): The method used for authentication. Use 'PLAIN' if no authentication or STARTLS is required. +SMTP_NAME: # (empty by default): The username for the authentication. +SMTP_PASSWORD: # (empty by default): The password for the authentication. +MAIL_FROM_ADDRESS: # (not set by default): Set the local-part for the 'from' field in the emails sent by Nextcloud. +MAIL_DOMAIN: # (not set by default): Set a different domain for the emails than the domain where Nextcloud is installed. +TALK_MAX_STREAM_BITRATE: "1048576" # This allows to adjust the max stream bitrate of the talk hpb +TALK_MAX_SCREEN_BITRATE: "2097152" # This allows to adjust the max stream bitrate of the talk hpb +ADDITIONAL_CONFIG + +mv /tmp/sample.conf ../helm-chart/values.yaml + +ENABLED_VARIABLES="$(grep -oP '^[A-Z_]+_ENABLED' ../helm-chart/values.yaml)" +mapfile -t ENABLED_VARIABLES <<< "$ENABLED_VARIABLES" + +cd ../helm-chart/ +for variable in "${ENABLED_VARIABLES[@]}"; do + name="$(echo "$variable" | sed 's|_ENABLED||g;s|_|-|g' | tr '[:upper:]' '[:lower:]')" + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-deployment.yaml" -exec sed -i "1i\\{{- if eq .Values.$variable \"yes\" }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-deployment.yaml" -exec sed -i "$ a {{- end }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-service.yaml" -exec sed -i "1i\\{{- if eq .Values.$variable \"yes\" }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-service.yaml" -exec sed -i "$ a {{- end }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-persistentvolumeclaim.yaml" -exec sed -i "1i\\{{- if eq .Values.$variable \"yes\" }}" \{} \; + # shellcheck disable=SC1083 + find ./ -name "*nextcloud-aio-$name-persistentvolumeclaim.yaml" -exec sed -i "$ a {{- end }}" \{} \; +done + +# Additional case for FTS volume +# shellcheck disable=SC1083 +find ./ -name "*nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml" -exec sed -i "1i\\{{- if eq .Values.FULLTEXTSEARCH_ENABLED \"yes\" }}" \{} \; +# shellcheck disable=SC1083 +find ./ -name "*nextcloud-aio-elasticsearch-persistentvolumeclaim.yaml" -exec sed -i "$ a {{- end }}" \{} \; + +cat << EOL > /tmp/security.conf + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} +EOL +# shellcheck disable=SC1083 +find ./ \( -not -name '*collabora-deployment.yaml*' -not -name '*apache-deployment.yaml*' -not -name '*onlyoffice-deployment.yaml*' -name "*deployment.yaml" \) -exec sed -i "/^ securityContext:$/r /tmp/security.conf" \{} \; + +# shellcheck disable=SC1083 +find ./ -name '*collabora-deployment.yaml*' -exec sed -i "/ADDITIONAL_COLLABORA_OPTIONS_PLACEHOLDER/d" \{} \; +# shellcheck disable=SC1083 +find ./ -name '*collabora-deployment.yaml*' -exec sed -i "s/- args:/- args: \{\{ .Values.ADDITIONAL_COLLABORA_OPTIONS | default list | toJson \}\}/" \{} \; + +cat << EOL > /tmp/security.conf + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + add: ["NET_BIND_SERVICE"] +EOL + +# shellcheck disable=SC1083 +find ./ -name '*apache-deployment.yaml*' -exec sed -i "/^ securityContext:$/r /tmp/security.conf" \{} \; + +cat << EOL > /tmp/security.conf + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} # AIO-config - do not change this comment! + securityContext: + # The items below only work in container context + allowPrivilegeEscalation: false + capabilities: + {{- if eq (.Values.RPSS_ENABLED | default "no") "yes" }} + drop: ["ALL"] + {{- else }} + drop: ["NET_RAW"] + {{- end }} + {{- end }} # AIO-config - do not change this comment! +EOL +# shellcheck disable=SC1083 +find ./ -name '*nextcloud-deployment.yaml*' -exec sed -i "/image: .*nextcloud.*aio-nextcloud:.*/r /tmp/security.conf" \{} \; + +chmod 777 -R ./ + +# Seems like the dir needs to match the name of the chart +cd ../ +rm -rf ./nextcloud-aio-helm-chart +mv ./helm-chart ./nextcloud-aio-helm-chart + +set +ex diff --git a/nextcloud-aio/nextcloud-aio-helm-chart/values.yaml b/nextcloud-aio/nextcloud-aio-helm-chart/values.yaml new file mode 100755 index 0000000..10603a7 --- /dev/null +++ b/nextcloud-aio/nextcloud-aio-helm-chart/values.yaml @@ -0,0 +1,75 @@ +DATABASE_PASSWORD: # TODO! This needs to be a unique and good password! +FULLTEXTSEARCH_PASSWORD: # TODO! This needs to be a unique and good password! +IMAGINARY_SECRET: # TODO! This needs to be a unique and good password! +NC_DOMAIN: yourdomain.com # TODO! Needs to be changed to the domain that you want to use for Nextcloud. +NEXTCLOUD_PASSWORD: # TODO! This is the password of the initially created Nextcloud admin with username admin. +ONLYOFFICE_SECRET: # TODO! This needs to be a unique and good password! +RECORDING_SECRET: # TODO! This needs to be a unique and good password! +REDIS_PASSWORD: # TODO! This needs to be a unique and good password! +SIGNALING_SECRET: # TODO! This needs to be a unique and good password! +TALK_INTERNAL_SECRET: # TODO! This needs to be a unique and good password! +TIMEZONE: Europe/Berlin # TODO! This is the timezone that your containers will use. +TURN_SECRET: # TODO! This needs to be a unique and good password! +WHITEBOARD_SECRET: # TODO! This needs to be a unique and good password! + +CLAMAV_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +COLLABORA_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +FULLTEXTSEARCH_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +IMAGINARY_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +ONLYOFFICE_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +TALK_RECORDING_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. +WHITEBOARD_ENABLED: "no" # Setting this to "yes" (with quotes) enables the option in Nextcloud automatically. + +APACHE_MAX_SIZE: "17179869184" # This needs to be an integer and in sync with NEXTCLOUD_UPLOAD_LIMIT +APACHE_PORT: 443 # Changing this to a different value than 443 will allow you to run it behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). +ADDITIONAL_COLLABORA_OPTIONS: ['--o:security.seccomp=true'] # You can add additional collabora options here by using the array syntax. +COLLABORA_DICTIONARIES: de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru # You can change this in order to enable other dictionaries for collabora +FULLTEXTSEARCH_JAVA_OPTIONS: -Xms512M -Xmx512M # Allows to adjust the fulltextsearch java options. +INSTALL_LATEST_MAJOR: no # Setting this to yes will install the latest Major Nextcloud version upon the first installation +NEXTCLOUD_ADDITIONAL_APKS: imagemagick # This allows to add additional packages to the Nextcloud container permanently. Default is imagemagick but can be overwritten by modifying this value. +NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS: imagick # This allows to add additional php extensions to the Nextcloud container permanently. Default is imagick but can be overwritten by modifying this value. +NEXTCLOUD_MAX_TIME: 3600 # This allows to change the upload time limit of the Nextcloud container +NEXTCLOUD_MEMORY_LIMIT: 512M # This allows to change the PHP memory limit of the Nextcloud container +NEXTCLOUD_STARTUP_APPS: deck twofactor_totp tasks calendar contacts notes # Allows to modify the Nextcloud apps that are installed on starting AIO the first time +NEXTCLOUD_TRUSTED_CACERTS_DIR: # Setting this to any value allows to automatically import root certificates into the Nextcloud container +NEXTCLOUD_UPLOAD_LIMIT: 16G # This allows to change the upload limit of the Nextcloud container +REMOVE_DISABLED_APPS: yes # Setting this to no keep Nextcloud apps that are disabled via their switch and not uninstall them if they should be installed in Nextcloud. +TALK_PORT: 3478 # This allows to adjust the port that the talk container is using. It should be set to something higher than 1024! Otherwise it might not work! +UPDATE_NEXTCLOUD_APPS: no # When setting to yes (with quotes), it will automatically update all installed Nextcloud apps upon container startup on saturdays. + +STORAGE_CLASS: # By setting this, you can adjust the storage class for your volumes. This should be a fast storage like SSD backed storage! +STORAGE_CLASS_DATA: # Allows to set a dedicated storage class for the Nextcloud data volume. This can be a bit slower storage than the one above. ⚠️ Warning: only set this for new installations, not existing ones! +APACHE_STORAGE_SIZE: 1Gi # You can change the size of the apache volume that default to 1Gi with this value +CLAMAV_STORAGE_SIZE: 1Gi # You can change the size of the clamav volume that default to 1Gi with this value +DATABASE_STORAGE_SIZE: 1Gi # You can change the size of the database volume that default to 1Gi with this value +DATABASE_DUMP_STORAGE_SIZE: 1Gi # You can change the size of the database-dump volume that default to 1Gi with this value +ELASTICSEARCH_STORAGE_SIZE: 1Gi # You can change the size of the elasticsearch volume that default to 1Gi with this value +NEXTCLOUD_STORAGE_SIZE: 5Gi # You can change the size of the nextcloud volume that default to 1Gi with this value +NEXTCLOUD_DATA_STORAGE_SIZE: 5Gi # You can change the size of the nextcloud-data volume that default to 1Gi with this value +NEXTCLOUD_TRUSTED_CACERTS_STORAGE_SIZE: 1Gi # You can change the size of the nextcloud-trusted-cacerts volume that default to 1Gi with this value +ONLYOFFICE_STORAGE_SIZE: 1Gi # You can change the size of the onlyoffice volume that default to 1Gi with this value +REDIS_STORAGE_SIZE: 1Gi # You can change the size of the redis volume that default to 1Gi with this value +TALK_RECORDING_STORAGE_SIZE: 1Gi # You can change the size of the talk-recording volume that default to 1Gi with this value + +NAMESPACE: default # By changing this, you can adjust the namespace of the installation which allows to install multiple instances on one kubernetes cluster +NAMESPACE_DISABLED: "no" # By setting this to "yes", you can disabled the creation of the namespace so that you can use a pre-created one +NETWORK_POLICY_ENABLED: "no" # By setting this to "yes", you can enable a network policy that limits network access to the same namespace. Except the Web server service which is reachable from all endpoints. +SUBSCRIPTION_KEY: # This allows to set the Nextcloud Enterprise key via ENV +SERVERINFO_TOKEN: # This allows to set the serverinfo app token for monitoring your Nextcloud via the serverinfo app +APPS_ALLOWLIST: # This allows to configure allowed apps that will be shown in Nextcloud's Appstore. You need to enter the app-IDs of the apps here and separate them with spaces. E.g. 'files richdocuments' +ADDITIONAL_TRUSTED_PROXY: # Allows to add one additional ip-address to Nextcloud's trusted proxies and to the Office WOPI-allowlist automatically. Set it e.g. like this: 'your.public.ip-address'. You can also use an ip-range here. +ADDITIONAL_TRUSTED_DOMAIN: # Allows to add one domain to Nextcloud's trusted domains and also generates a certificate automatically for it +NEXTCLOUD_DEFAULT_QUOTA: "10 GB" # Allows to adjust the default quota that will be taken into account in Nextcloud for new users. Setting it to "unlimited" will set it to unlimited +NEXTCLOUD_SKELETON_DIRECTORY: # Allows to adjust the sekeleton dir for Nextcloud. Setting it to "empty" will set the value to an empty string "" which will turn off the setting for new users in Nextcloud. +NEXTCLOUD_MAINTENANCE_WINDOW: # Allows to define the maintenance window for Nextcloud. See https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/background_jobs_configuration.html#parameters for possible values +SMTP_HOST: # (empty by default): The hostname of the SMTP server. +SMTP_SECURE: # (empty by default): Set to 'ssl' to use SSL, or 'tls' to use STARTTLS. +SMTP_PORT: # (default: '465' for SSL and '25' for non-secure connections): Optional port for the SMTP connection. Use '587' for an alternative port for STARTTLS. +SMTP_AUTHTYPE: # (default: 'LOGIN'): The method used for authentication. Use 'PLAIN' if no authentication or STARTLS is required. +SMTP_NAME: # (empty by default): The username for the authentication. +SMTP_PASSWORD: # (empty by default): The password for the authentication. +MAIL_FROM_ADDRESS: # (not set by default): Set the local-part for the 'from' field in the emails sent by Nextcloud. +MAIL_DOMAIN: # (not set by default): Set a different domain for the emails than the domain where Nextcloud is installed. +TALK_MAX_STREAM_BITRATE: "1048576" # This allows to adjust the max stream bitrate of the talk hpb +TALK_MAX_SCREEN_BITRATE: "2097152" # This allows to adjust the max stream bitrate of the talk hpb diff --git a/nextcloud-aio/php/README.md b/nextcloud-aio/php/README.md new file mode 100644 index 0000000..af82481 --- /dev/null +++ b/nextcloud-aio/php/README.md @@ -0,0 +1,65 @@ +# PHP Docker Controller + +This is the code for the PHP Docker controller. + +## How to run + +Running this locally requires : + +### 1. Install the development environment + +This project uses Composer as dependency management software. It is very similar to NPM. +The command to install all dependencies is: + +```bash +composer install +``` + +### 2. Access to docker socket + +The `root` user has all privileges including access to the Docker socket. +But **it is not recommended to launch the local instance with full privileges**, consider the docker group for docker access without being `root`. +See https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user + +### 3. Run a `nextcloud-aio-mastercontainer` container + +This application manages containers, including its own container. +So you need to run a `nextcloud-aio-mastercontainer` container for the application to work properly. + +Here is a command to quickly launch a container : + +```bash +docker run \ +--rm \ +--name nextcloud-aio-mastercontainer \ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ +--volume /var/run/docker.sock:/var/run/docker.sock \ +ghcr.io/nextcloud-releases/all-in-one:latest +``` + +### 4. Start your server + +With this command you will launch the server: + +```bash +# Make sure to launch this command with a user having access to the docker socket. +SKIP_DOMAIN_VALIDATION=true composer run dev +``` + +You can then access the web interface at http://localhost:8080. + +Note: You can restart the server by preceding the command with other environment variables. + +## Composer routine + +| Command | Description | +|-----------------------------------------|----------------------------------------| +| `composer run dev` | Starts the development server | +| `composer run psalm` | Run Psalm static analysis | +| `composer run psalm:strict` | Run Psalm static analysis strict | +| `composer run psalm:update-baseline` | Run Psalm with `--update-baseline` arg | +| `composer run lint` | Run PHP Syntax check | +| `composer run lint:twig` | Run Twig Syntax check | +| `composer run php-deprecation-detector` | Run PHP Deprecation Detector | + + diff --git a/nextcloud-aio/php/composer.json b/nextcloud-aio/php/composer.json new file mode 100644 index 0000000..892bdd5 --- /dev/null +++ b/nextcloud-aio/php/composer.json @@ -0,0 +1,38 @@ +{ + "autoload": { + "psr-4": { + "AIO\\": ["src/"] + } + }, + "require": { + "php": "8.4.*", + "ext-json": "*", + "ext-sodium": "*", + "ext-curl": "*", + "slim/slim": "^4.11", + "php-di/slim-bridge": "^3.3", + "guzzlehttp/guzzle": "^7.5", + "guzzlehttp/psr7": "^2.4", + "http-interop/http-factory-guzzle": "^1.2", + "slim/twig-view": "^3.3", + "slim/csrf": "^1.3", + "ext-apcu": "*" + }, + "require-dev": { + "sserbin/twig-linter": "@dev", + "vimeo/psalm": "^6.0", + "wapmorgan/php-deprecation-detector": "dev-master" + }, + "scripts": { + "dev": [ + "Composer\\Config::disableProcessTimeout", + "php -S localhost:8080 -t public" + ], + "psalm": "psalm --threads=1", + "psalm:update-baseline": "psalm --threads=1 --monochrome --no-progress --output-format=text --update-baseline", + "psalm:strict": "psalm --threads=1 --show-info=true", + "lint": "php -l src/*.php src/**/*.php public/index.php", + "lint:twig": "twig-linter lint ./templates", + "php-deprecation-detector": "phpdd scan -n -t 8.4 src/*.php src/**/*.php public/index.php" + } +} diff --git a/nextcloud-aio/php/composer.lock b/nextcloud-aio/php/composer.lock new file mode 100644 index 0000000..bb81d69 --- /dev/null +++ b/nextcloud-aio/php/composer.lock @@ -0,0 +1,4801 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "19598625395cc28e64f15d2719f8f98f", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "http-interop/http-factory-guzzle", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory-guzzle.git", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7||^2.0", + "php": ">=7.3", + "psr/http-factory": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Factory\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "An HTTP Factory using Guzzle PSR7", + "keywords": [ + "factory", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/http-interop/http-factory-guzzle/issues", + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" + }, + "time": "2021-07-21T13:50:14+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3832547db6e0e2f8bb03d4093857b378c66eceed", + "reference": "3832547db6e0e2f8bb03d4093857b378c66eceed", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-09-22T17:29:40+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "php-di/invoker", + "version": "2.3.7", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2025-08-30T10:22:22+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.1.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-08-16T11:10:48+00:00" + }, + { + "name": "php-di/slim-bridge", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Slim-Bridge.git", + "reference": "02ab0274a19d104d74561164f8915b62d93f3cf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Slim-Bridge/zipball/02ab0274a19d104d74561164f8915b62d93f3cf0", + "reference": "02ab0274a19d104d74561164f8915b62d93f3cf0", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-di/invoker": "^2.0.0", + "php-di/php-di": "^6.0|^7.0", + "slim/slim": "^4.2.0" + }, + "require-dev": { + "laminas/laminas-diactoros": "^2.1", + "mnapoli/hard-mode": "^0.3.0", + "phpunit/phpunit": ">= 7.0 < 10" + }, + "type": "library", + "autoload": { + "psr-4": { + "DI\\Bridge\\Slim\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP-DI integration in Slim", + "support": { + "issues": "https://github.com/PHP-DI/Slim-Bridge/issues", + "source": "https://github.com/PHP-DI/Slim-Bridge/tree/3.4.1" + }, + "time": "2024-06-19T15:47:45+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "slim/csrf", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Csrf.git", + "reference": "179cbcf40ee1d246d4906aefed42d3e62066974b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Csrf/zipball/179cbcf40ee1d246d4906aefed42d3e62066974b", + "reference": "179cbcf40ee1d246d4906aefed42d3e62066974b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0" + }, + "require-dev": { + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Csrf\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + } + ], + "description": "Slim Framework 4 CSRF protection PSR-15 middleware", + "homepage": "https://www.slimframework.com", + "keywords": [ + "csrf", + "framework", + "middleware", + "slim" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Csrf/issues", + "source": "https://github.com/slimphp/Slim-Csrf/tree/1.5.0" + }, + "time": "2024-06-08T16:37:18+00:00" + }, + { + "name": "slim/slim", + "version": "4.15.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "17eba5182975878a0ab9b27982cd2e2cfcb67ea2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/17eba5182975878a0ab9b27982cd2e2cfcb67ea2", + "reference": "17eba5182975878a0ab9b27982cd2e2cfcb67ea2", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4 || ^2", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^2.6", + "httpsoft/http-message": "^1.1", + "httpsoft/http-server-request": "^1.1", + "laminas/laminas-diactoros": "^2.17 || ^3", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.1", + "phpstan/phpstan": "^1 || ^2", + "phpunit/phpunit": "^9.6", + "slim/http": "^1.3", + "slim/psr7": "^1.6", + "squizlabs/php_codesniffer": "^3.10", + "vimeo/psalm": "^5 || ^6" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "docs": "https://www.slimframework.com/docs/v4/", + "forum": "https://discourse.slimframework.com/", + "irc": "irc://irc.freenode.net:6667/slimphp", + "issues": "https://github.com/slimphp/Slim/issues", + "rss": "https://www.slimframework.com/blog/feed.rss", + "slack": "https://slimphp.slack.com/", + "source": "https://github.com/slimphp/Slim", + "wiki": "https://github.com/slimphp/Slim/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2025-08-20T18:16:16+00:00" + }, + { + "name": "slim/twig-view", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Twig-View.git", + "reference": "b4268d87d0e327feba5f88d32031e9123655b909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/b4268d87d0e327feba5f88d32031e9123655b909", + "reference": "b4268d87d0e327feba5f88d32031e9123655b909", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-message": "^1.1 || ^2.0", + "slim/slim": "^4.12", + "symfony/polyfill-php81": "^1.29", + "twig/twig": "^3.11" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpstan": "^1.10.59", + "phpunit/phpunit": "^9.6 || ^10", + "psr/http-factory": "^1.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Views\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + } + ], + "description": "Slim Framework 4 view helper built on top of the Twig 3 templating component", + "homepage": "https://www.slimframework.com", + "keywords": [ + "framework", + "slim", + "template", + "twig", + "view" + ], + "support": { + "issues": "https://github.com/slimphp/Twig-View/issues", + "source": "https://github.com/slimphp/Twig-View/tree/3.4.1" + }, + "time": "2024-09-26T05:42:02+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "twig/twig", + "version": "v3.21.1", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "reference": "285123877d4dd97dd7c11842ac5fb7e86e60d81d", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.21.1" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2025-05-03T07:21:55+00:00" + } + ], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Future/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-08-27T21:42:00+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v2.1.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "reference": "55a6bd071aec26fa2a3e002618c20c35e3df1b46", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/parser": "^1.1", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2.3" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.22.1" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v2.1.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T17:10:27+00:00" + }, + { + "name": "amphp/cache", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/46912e387e6aa94933b61ea1ead9cf7540b7797c", + "reference": "46912e387e6aa94933b61ea1ead9cf7540b7797c", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/serialization": "^1", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + } + ], + "description": "A fiber-aware cache API based on Amp and Revolt.", + "homepage": "https://amphp.org/cache", + "support": { + "issues": "https://github.com/amphp/cache/issues", + "source": "https://github.com/amphp/cache/tree/v2.0.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:38:06+00:00" + }, + { + "name": "amphp/dns", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/dns.git", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/process": "^2", + "daverandom/libdns": "^2.0.2", + "ext-filter": "*", + "ext-json": "*", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Dns\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Wright", + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async DNS resolution for Amp.", + "homepage": "https://github.com/amphp/dns", + "keywords": [ + "amp", + "amphp", + "async", + "client", + "dns", + "resolve" + ], + "support": { + "issues": "https://github.com/amphp/dns/issues", + "source": "https://github.com/amphp/dns/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-01-19T15:43:40+00:00" + }, + { + "name": "amphp/parallel", + "version": "v2.3.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/parallel.git", + "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parallel/zipball/321b45ae771d9c33a068186b24117e3cd1c48dce", + "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/cache": "^2", + "amphp/parser": "^1", + "amphp/pipeline": "^1", + "amphp/process": "^2", + "amphp/serialization": "^1", + "amphp/socket": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "files": [ + "src/Context/functions.php", + "src/Context/Internal/functions.php", + "src/Ipc/functions.php", + "src/Worker/functions.php" + ], + "psr-4": { + "Amp\\Parallel\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "support": { + "issues": "https://github.com/amphp/parallel/issues", + "source": "https://github.com/amphp/parallel/tree/v2.3.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-08-27T21:55:40+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "reference": "3cf1f8b32a0171d4b1bed93d25617637a77cded7", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/parser/issues", + "source": "https://github.com/amphp/parser/tree/v1.1.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T19:16:53+00:00" + }, + { + "name": "amphp/pipeline", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/pipeline.git", + "reference": "7b52598c2e9105ebcddf247fc523161581930367" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", + "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "php": ">=8.1", + "revolt/event-loop": "^1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.18" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Pipeline\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Asynchronous iterators and operators.", + "homepage": "https://amphp.org/pipeline", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "iterator", + "non-blocking" + ], + "support": { + "issues": "https://github.com/amphp/pipeline/issues", + "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2025-03-16T16:33:53+00:00" + }, + { + "name": "amphp/process", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/sync": "^2", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.4" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Process\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A fiber-aware process manager based on Amp and Revolt.", + "homepage": "https://amphp.org/process", + "support": { + "issues": "https://github.com/amphp/process/issues", + "source": "https://github.com/amphp/process/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-19T03:13:44+00:00" + }, + { + "name": "amphp/serialization", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/serialization.git", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", + "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "phpunit/phpunit": "^9 || ^8 || ^7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Serialization\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Serialization tools for IPC and data storage in PHP.", + "homepage": "https://github.com/amphp/serialization", + "keywords": [ + "async", + "asynchronous", + "serialization", + "serialize" + ], + "support": { + "issues": "https://github.com/amphp/serialization/issues", + "source": "https://github.com/amphp/serialization/tree/master" + }, + "time": "2020-03-25T21:39:07+00:00" + }, + { + "name": "amphp/socket", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/socket.git", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", + "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/dns": "^2", + "ext-openssl": "*", + "kelunik/certificate": "^1.1", + "league/uri": "^6.5 | ^7", + "league/uri-interfaces": "^2.3 | ^7", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "amphp/process": "^2", + "phpunit/phpunit": "^9", + "psalm/phar": "5.20" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php", + "src/Internal/functions.php", + "src/SocketAddress/functions.php" + ], + "psr-4": { + "Amp\\Socket\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Non-blocking socket connection / server implementations based on Amp and Revolt.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "support": { + "issues": "https://github.com/amphp/socket/issues", + "source": "https://github.com/amphp/socket/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-21T14:33:03+00:00" + }, + { + "name": "amphp/sync", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/217097b785130d77cfcc58ff583cf26cd1770bf1", + "reference": "217097b785130d77cfcc58ff583cf26cd1770bf1", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/pipeline": "^1", + "amphp/serialization": "^1", + "php": ">=8.1", + "revolt/event-loop": "^1 || ^0.2" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "amphp/phpunit-util": "^3", + "phpunit/phpunit": "^9", + "psalm/phar": "5.23" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Amp\\Sync\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + } + ], + "description": "Non-blocking synchronization primitives for PHP based on Amp and Revolt.", + "homepage": "https://github.com/amphp/sync", + "keywords": [ + "async", + "asynchronous", + "mutex", + "semaphore", + "synchronization" + ], + "support": { + "issues": "https://github.com/amphp/sync/issues", + "source": "https://github.com/amphp/sync/tree/v2.3.0" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-08-03T19:31:26+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "danog/advanced-json-rpc", + "version": "v3.2.2", + "source": { + "type": "git", + "url": "https://github.com/danog/php-advanced-json-rpc.git", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/danog/php-advanced-json-rpc/zipball/aadb1c4068a88c3d0530cfe324b067920661efcb", + "reference": "aadb1c4068a88c3d0530cfe324b067920661efcb", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^5", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "replace": { + "felixfbecker/php-advanced-json-rpc": "^3" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/danog/php-advanced-json-rpc/issues", + "source": "https://github.com/danog/php-advanced-json-rpc/tree/v3.2.2" + }, + "time": "2025-02-14T10:55:15+00:00" + }, + { + "name": "daverandom/libdns", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/DaveRandom/LibDNS.git", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DaveRandom/LibDNS/zipball/b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "reference": "b84c94e8fe6b7ee4aecfe121bfe3b6177d303c8a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "Required for IDN support" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "LibDNS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "DNS protocol implementation written in pure PHP", + "keywords": [ + "dns" + ], + "support": { + "issues": "https://github.com/DaveRandom/LibDNS/issues", + "source": "https://github.com/DaveRandom/LibDNS/tree/v2.1.0" + }, + "time": "2024-04-12T12:12:48+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + }, + "time": "2024-04-30T00:40:11+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "kelunik/certificate", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/kelunik/certificate.git", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "reference": "7e00d498c264d5eb4f78c69f41c8bd6719c0199e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=7.0" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "^2", + "phpunit/phpunit": "^6 | 7 | ^8 | ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Kelunik\\Certificate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Access certificate details and transform between different formats.", + "keywords": [ + "DER", + "certificate", + "certificates", + "openssl", + "pem", + "x509" + ], + "support": { + "issues": "https://github.com/kelunik/certificate/issues", + "source": "https://github.com/kelunik/certificate/tree/v1.1.3" + }, + "time": "2023-02-03T21:26:53+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v5.0.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "reference": "8c64d8d444a5d764c641ebe97e0e3bc72b25bf6c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v5.0.0" + }, + "time": "2024-09-08T10:20:00+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + }, + "time": "2025-08-13T20:13:15+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + }, + "time": "2025-08-01T19:43:32+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + }, + "time": "2025-08-30T15:50:23+00:00" + }, + { + "name": "revolt/event-loop", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Revolt\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "ceesjank@gmail.com" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "Rock-solid event loop for concurrent PHP applications.", + "keywords": [ + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" + ], + "support": { + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + }, + "time": "2025-01-25T19:27:39+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "spatie/array-to-xml", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "reference": "7dcfc67d60b0272926dabad1ec01f6b8a5fb5e67", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" + } + ], + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], + "support": { + "source": "https://github.com/spatie/array-to-xml/tree/3.4.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-12-16T12:45:15+00:00" + }, + { + "name": "sserbin/twig-linter", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/sserbin/twig-linter.git", + "reference": "932c7f1dcc79cd54aa011804d42aa7bbb14a970f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sserbin/twig-linter/zipball/932c7f1dcc79cd54aa011804d42aa7bbb14a970f", + "reference": "932c7f1dcc79cd54aa011804d42aa7bbb14a970f", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0", + "php": "^7.4|^8.0", + "symfony/console": "^5.4 || ^6.1", + "symfony/finder": "^5.4 || ^6.1", + "twig/twig": "^2.5 || ^3" + }, + "require-dev": { + "phpunit/phpunit": "^7.3||^8.2|^9.5", + "squizlabs/php_codesniffer": "^3.3", + "vimeo/psalm": "^4.7 || ^5.8" + }, + "default-branch": true, + "bin": [ + "bin/twig-linter" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sserbin\\TwigLinter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "sserbin", + "email": "sserbin@users.noreply.github.com" + } + ], + "description": "Standalone cli twig linter (based on symfony-bridge-twig)", + "keywords": [ + "lint", + "linter", + "twig" + ], + "support": { + "issues": "https://github.com/sserbin/twig-linter/issues", + "source": "https://github.com/sserbin/twig-linter/tree/3.1.2" + }, + "time": "2025-06-03T06:31:48+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.26", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f", + "reference": "492de6dfd93910d7d7a729c5a04ddcd2b9e99c4f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.26" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-26T12:13:46+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-07T08:17:47+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.24", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "73089124388c8510efb8d2d1689285d285937b08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/73089124388c8510efb8d2d1689285d285937b08", + "reference": "73089124388c8510efb8d2d1689285d285937b08", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.24" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T12:02:45+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f96476035142921000338bad71e5247fbc138872" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-09-11T14:36:48+00:00" + }, + { + "name": "vimeo/psalm", + "version": "6.13.1", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51", + "reference": "1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51", + "shasum": "" + }, + "require": { + "amphp/amp": "^3", + "amphp/byte-stream": "^2", + "amphp/parallel": "^2.3", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "danog/advanced-json-rpc": "^3.1", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/language-server-protocol": "^1.5.3", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", + "netresearch/jsonmapper": "^5.0", + "nikic/php-parser": "^5.0.0", + "php": "~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^6.0 || ^7.0", + "symfony/filesystem": "~6.3.12 || ~6.4.3 || ^7.0.3", + "symfony/polyfill-php84": "^1.31.0" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "amphp/phpunit-util": "^3", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "danog/class-finder": "^0.4.8", + "dg/bypass-finals": "^1.5", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.19", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^6.0 || ^7.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalm-review", + "psalter" + ], + "type": "project", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev", + "dev-6.x": "6.x-dev", + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + }, + { + "name": "Daniil Gentili", + "email": "daniil@daniil.it" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php", + "static analysis" + ], + "support": { + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" + }, + "time": "2025-08-06T10:10:28+00:00" + }, + { + "name": "wapmorgan/php-deprecation-detector", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/wapmorgan/PhpDeprecationDetector.git", + "reference": "42cd3786c8971bf80921a3add061cd44df3e73e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wapmorgan/PhpDeprecationDetector/zipball/42cd3786c8971bf80921a3add061cd44df3e73e3", + "reference": "42cd3786c8971bf80921a3add061cd44df3e73e3", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=5.4", + "symfony/console": "^3.4|^4.0|^5.0|^6.0" + }, + "replace": { + "wapmorgan/php-code-fixer": "self.version" + }, + "suggest": { + "ext-json": "Adds ability to store report in JSON format", + "macfja/phar-builder": "To build phar" + }, + "default-branch": true, + "bin": [ + "bin/phpdd" + ], + "type": "package", + "extra": { + "phar-builder": { + "name": "phpdd-dev.phar", + "events": { + "command.package.end": "cp phpdd-dev.phar phpdd-`cat bin/version.txt`.phar && chmod +x phpdd-`cat bin/version.txt`.phar && rm bin/version.txt", + "command.package.start": "git describe --tags > bin/version.txt" + }, + "include": [ + "bin", + "data" + ], + "output-dir": "./", + "compression": "BZip2", + "entry-point": "bin/phpdd" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "wapmorgan\\PhpCodeFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Analyzer of PHP code to search issues with deprecated functionality in newer interpreter versions.", + "support": { + "issues": "https://github.com/wapmorgan/PhpDeprecationDetector/issues", + "source": "https://github.com/wapmorgan/PhpDeprecationDetector/tree/master" + }, + "time": "2024-01-30T21:54:23+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "sserbin/twig-linter": 20, + "wapmorgan/php-deprecation-detector": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "8.4.*", + "ext-json": "*", + "ext-sodium": "*", + "ext-curl": "*", + "ext-apcu": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/nextcloud-aio/php/containers-schema.json b/nextcloud-aio/php/containers-schema.json new file mode 100644 index 0000000..5ed57e3 --- /dev/null +++ b/nextcloud-aio/php/containers-schema.json @@ -0,0 +1,232 @@ +{ + "type": "object", + "description": "AIO containers definition schema", + "minProperties": 1, + "required": ["aio_services_v1"], + "properties": { + "aio_services_v1": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "minProperties": 2, + "required": ["image", "container_name", "image_tag"], + "properties": { + "image": { + "type": "string", + "minLength": 1, + "pattern": "^(ghcr.io/)?[a-z0-9/-]+$" + }, + "expose": { + "type": "array", + "items": { + "type": "string", + "pattern": "^([0-9]{1,5})$" + } + }, + "cap_add": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Z_]+$" + } + }, + "cap_drop": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Z_]+$" + } + }, + "depends_on": { + "type": "array", + "items": { + "type": "string", + "pattern": "^nextcloud-aio-[a-z-]+$" + } + }, + "display_name": { + "type": "string", + "pattern": "^[()A-Za-z 0-9-]+$" + }, + "environment": { + "type": "array", + "items": { + "type": "string", + "pattern": "^.*=.*$", + "minlength": 1 + } + }, + "container_name": { + "type": "string", + "pattern": "^nextcloud-aio-[a-z0-9-]+$" + }, + "internal_port": { + "type": "string", + "pattern": "^(([0-9]{1,5})|host|(%[A-Z_]+%))$" + }, + "stop_grace_period": { + "type": "integer" + }, + "user": { + "type": "string", + "pattern": "^[0-9]{1,6}$" + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "minProperties": 3, + "properties": { + "ip_binding": { + "type": "string", + "pattern": "^((%[A-Z_]+%)|127\\.0\\.0\\.1)?$" + }, + "port_number": { + "type": "string", + "pattern": "^(%[A-Z_]+%|[0-9]{1,5})$" + }, + "protocol": { + "type": "string", + "pattern": "^(tcp|udp)$" + } + } + } + }, + "healthcheck": { + "type": "object", + "additionalProperties": false, + "minProperties": 6, + "properties": { + "interval": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "timeout": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "retries": { + "type": "integer" + }, + "start_period": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "start_interval": { + "type": "string", + "pattern": "^[0-9]+s$" + }, + "test": { + "type": "string", + "pattern": "^.*$" + } + } + }, + "aio_variables": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Z_a-z-]+=.*$" + } + }, + "restart": { + "type": "string", + "pattern": "^unless-stopped$" + }, + "shm_size": { + "type": "integer" + }, + "secrets": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Z_]+$" + } + }, + "ui_secret": { + "type": "string", + "pattern": "^[A-Z_]+$" + }, + "image_tag": { + "type": "string", + "pattern": "^([a-z0-9.-]+|%AIO_CHANNEL%)$" + }, + "documentation": { + "type": "string", + "pattern": "^https://.*$" + }, + "devices": { + "type": "array", + "items": { + "type": "string", + "pattern": "^/dev/[a-z]+$" + } + }, + "enable_nvidia_gpu": { + "type": "boolean" + }, + "apparmor_unconfined": { + "type": "boolean" + }, + "backup_volumes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^nextcloud_aio_[a-z_]+$" + } + }, + "nextcloud_exec_commands": { + "type": "array", + "items": { + "type": "string", + "pattern": "^(php /var/www/html/occ .*|echo .*|touch .*|mkdir .*)$" + } + }, + "profiles": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z-]+$" + } + }, + "read_only": { + "type": "boolean" + }, + "init": { + "type": "boolean" + }, + "tmpfs": { + "type": "array", + "items": { + "type": "string", + "pattern": "^/[a-z/_0-9-:]+$" + } + }, + "volumes": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "minProperties": 3, + "properties": { + "destination": { + "type": "string", + "pattern": "^((/[a-z_/.-]+)|(%[A-Z_]+%))$" + }, + "source": { + "type": "string", + "pattern": "^((nextcloud_aio_[a-z_]+)|(%[A-Z_]+%)|(/dev)|(/run/udev))$" + }, + "writeable": { + "type": "boolean" + } + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/nextcloud-aio/php/containers.json b/nextcloud-aio/php/containers.json new file mode 100644 index 0000000..4506c09 --- /dev/null +++ b/nextcloud-aio/php/containers.json @@ -0,0 +1,895 @@ +{ + "aio_services_v1": [ + { + "container_name": "nextcloud-aio-apache", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/2105", + "depends_on": [ + "nextcloud-aio-onlyoffice", + "nextcloud-aio-collabora", + "nextcloud-aio-talk", + "nextcloud-aio-notify-push", + "nextcloud-aio-whiteboard", + "nextcloud-aio-nextcloud" + ], + "display_name": "Apache", + "image": "ghcr.io/nextcloud-releases/aio-apache", + "user": "33", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "%APACHE_PORT%", + "protocol": "tcp" + }, + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "%APACHE_PORT%", + "protocol": "udp" + } + ], + "internal_port": "%APACHE_PORT%", + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "NEXTCLOUD_HOST=nextcloud-aio-nextcloud", + "APACHE_HOST=nextcloud-aio-apache", + "COLLABORA_HOST=nextcloud-aio-collabora", + "TALK_HOST=nextcloud-aio-talk", + "APACHE_PORT=%APACHE_PORT%", + "ONLYOFFICE_HOST=nextcloud-aio-onlyoffice", + "TZ=%TIMEZONE%", + "APACHE_MAX_SIZE=%APACHE_MAX_SIZE%", + "APACHE_MAX_TIME=%NEXTCLOUD_MAX_TIME%", + "NOTIFY_PUSH_HOST=nextcloud-aio-notify-push", + "WHITEBOARD_HOST=nextcloud-aio-whiteboard" + ], + "volumes": [ + { + "source": "nextcloud_aio_nextcloud", + "destination": "/var/www/html", + "writeable": false + }, + { + "source": "nextcloud_aio_apache", + "destination": "/mnt/data", + "writeable": true + } + ], + "restart": "unless-stopped", + "backup_volumes": [ + "nextcloud_aio_nextcloud", + "nextcloud_aio_apache" + ], + "read_only": true, + "tmpfs": [ + "/var/log/supervisord", + "/var/run/supervisord", + "/usr/local/apache2/logs", + "/tmp", + "/home/www-data" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-database", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Database", + "image": "ghcr.io/nextcloud-releases/aio-postgresql", + "user": "999", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "5432" + ], + "internal_port": "5432", + "secrets": [ + "DATABASE_PASSWORD" + ], + "volumes": [ + { + "source": "nextcloud_aio_database", + "destination": "/var/lib/postgresql/data", + "writeable": true + }, + { + "source": "nextcloud_aio_database_dump", + "destination": "/mnt/data", + "writeable": true + } + ], + "environment": [ + "POSTGRES_PASSWORD=%DATABASE_PASSWORD%", + "POSTGRES_DB=nextcloud_database", + "POSTGRES_USER=nextcloud", + "TZ=%TIMEZONE%", + "PGTZ=%TIMEZONE%" + ], + "stop_grace_period": 1800, + "restart": "unless-stopped", + "shm_size": 268435456, + "backup_volumes": [ + "nextcloud_aio_database", + "nextcloud_aio_database_dump" + ], + "read_only": true, + "tmpfs": [ + "/var/run/postgresql" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-nextcloud", + "image_tag": "%AIO_CHANNEL%", + "depends_on": [ + "nextcloud-aio-database", + "nextcloud-aio-redis", + "nextcloud-aio-clamav", + "nextcloud-aio-fulltextsearch", + "nextcloud-aio-talk-recording", + "nextcloud-aio-imaginary", + "nextcloud-aio-docker-socket-proxy" + ], + "display_name": "Nextcloud", + "image": "ghcr.io/nextcloud-releases/aio-nextcloud", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "9000", + "9001" + ], + "internal_port": "9000", + "secrets": [ + "DATABASE_PASSWORD", + "REDIS_PASSWORD", + "NEXTCLOUD_PASSWORD", + "TURN_SECRET", + "SIGNALING_SECRET", + "FULLTEXTSEARCH_PASSWORD", + "IMAGINARY_SECRET", + "WHITEBOARD_SECRET" + ], + "volumes": [ + { + "source": "nextcloud_aio_nextcloud", + "destination": "/var/www/html", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/mnt/ncdata", + "writeable": true + }, + { + "source": "%NEXTCLOUD_MOUNT%", + "destination": "%NEXTCLOUD_MOUNT%", + "writeable": true + }, + { + "source": "%NEXTCLOUD_TRUSTED_CACERTS_DIR%", + "destination": "/usr/local/share/ca-certificates", + "writeable": false + } + ], + "environment": [ + "NEXTCLOUD_HOST=nextcloud-aio-nextcloud", + "POSTGRES_HOST=nextcloud-aio-database", + "POSTGRES_PORT=5432", + "POSTGRES_PASSWORD=%DATABASE_PASSWORD%", + "POSTGRES_DB=nextcloud_database", + "POSTGRES_USER=nextcloud", + "REDIS_HOST=nextcloud-aio-redis", + "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "APACHE_HOST=nextcloud-aio-apache", + "APACHE_PORT=%APACHE_PORT%", + "AIO_TOKEN=%AIO_TOKEN%", + "NC_DOMAIN=%NC_DOMAIN%", + "ADMIN_USER=admin", + "ADMIN_PASSWORD=%NEXTCLOUD_PASSWORD%", + "NEXTCLOUD_DATA_DIR=/mnt/ncdata", + "OVERWRITEHOST=%NC_DOMAIN%", + "OVERWRITEPROTOCOL=https", + "TURN_SECRET=%TURN_SECRET%", + "SIGNALING_SECRET=%SIGNALING_SECRET%", + "ONLYOFFICE_SECRET=%ONLYOFFICE_SECRET%", + "AIO_URL=%AIO_URL%", + "NEXTCLOUD_MOUNT=%NEXTCLOUD_MOUNT%", + "CLAMAV_ENABLED=%CLAMAV_ENABLED%", + "CLAMAV_HOST=nextcloud-aio-clamav", + "ONLYOFFICE_ENABLED=%ONLYOFFICE_ENABLED%", + "COLLABORA_ENABLED=%COLLABORA_ENABLED%", + "COLLABORA_HOST=nextcloud-aio-collabora", + "TALK_ENABLED=%TALK_ENABLED%", + "ONLYOFFICE_HOST=nextcloud-aio-onlyoffice", + "UPDATE_NEXTCLOUD_APPS=%UPDATE_NEXTCLOUD_APPS%", + "TZ=%TIMEZONE%", + "TALK_PORT=%TALK_PORT%", + "IMAGINARY_ENABLED=%IMAGINARY_ENABLED%", + "IMAGINARY_HOST=nextcloud-aio-imaginary", + "CLAMAV_MAX_SIZE=%APACHE_MAX_SIZE%", + "PHP_UPLOAD_LIMIT=%NEXTCLOUD_UPLOAD_LIMIT%", + "PHP_MEMORY_LIMIT=%NEXTCLOUD_MEMORY_LIMIT%", + "FULLTEXTSEARCH_ENABLED=%FULLTEXTSEARCH_ENABLED%", + "FULLTEXTSEARCH_HOST=nextcloud-aio-fulltextsearch", + "FULLTEXTSEARCH_PORT=9200", + "FULLTEXTSEARCH_USER=elastic", + "FULLTEXTSEARCH_INDEX=nextcloud-aio", + "PHP_MAX_TIME=%NEXTCLOUD_MAX_TIME%", + "TRUSTED_CACERTS_DIR=%NEXTCLOUD_TRUSTED_CACERTS_DIR%", + "STARTUP_APPS=%NEXTCLOUD_STARTUP_APPS%", + "ADDITIONAL_APKS=%NEXTCLOUD_ADDITIONAL_APKS%", + "ADDITIONAL_PHP_EXTENSIONS=%NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS%", + "INSTALL_LATEST_MAJOR=%INSTALL_LATEST_MAJOR%", + "TALK_RECORDING_ENABLED=%TALK_RECORDING_ENABLED%", + "RECORDING_SECRET=%RECORDING_SECRET%", + "TALK_RECORDING_HOST=nextcloud-aio-talk-recording", + "FULLTEXTSEARCH_PASSWORD=%FULLTEXTSEARCH_PASSWORD%", + "DOCKER_SOCKET_PROXY_ENABLED=%DOCKER_SOCKET_PROXY_ENABLED%", + "REMOVE_DISABLED_APPS=%REMOVE_DISABLED_APPS%", + "ADDITIONAL_TRUSTED_PROXY=%CADDY_IP_ADDRESS%", + "THIS_IS_AIO=true", + "IMAGINARY_SECRET=%IMAGINARY_SECRET%", + "WHITEBOARD_SECRET=%WHITEBOARD_SECRET%", + "WHITEBOARD_ENABLED=%WHITEBOARD_ENABLED%" + ], + "stop_grace_period": 600, + "restart": "unless-stopped", + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "backup_volumes": [ + "nextcloud_aio_nextcloud" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-notify-push", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Notify Push", + "image": "ghcr.io/nextcloud-releases/aio-notify-push", + "user": "33", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "7867" + ], + "internal_port": "7867", + "secrets": [ + "REDIS_PASSWORD", + "DATABASE_PASSWORD" + ], + "volumes": [ + { + "source": "nextcloud_aio_nextcloud", + "destination": "/nextcloud", + "writeable": false + } + ], + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "NEXTCLOUD_HOST=nextcloud-aio-nextcloud", + "TZ=%TIMEZONE%", + "REDIS_HOST=nextcloud-aio-redis", + "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "POSTGRES_HOST=nextcloud-aio-database", + "POSTGRES_PORT=5432", + "POSTGRES_PASSWORD=%DATABASE_PASSWORD%", + "POSTGRES_DB=nextcloud_database", + "POSTGRES_USER=nextcloud" + ], + "restart": "unless-stopped", + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-redis", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Redis", + "image": "ghcr.io/nextcloud-releases/aio-redis", + "user": "999", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "6379" + ], + "internal_port": "6379", + "environment": [ + "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "nextcloud_aio_redis", + "destination": "/data", + "writeable": true + } + ], + "secrets": [ + "REDIS_PASSWORD", + "ONLYOFFICE_SECRET", + "RECORDING_SECRET" + ], + "restart": "unless-stopped", + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-collabora", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/1358", + "display_name": "Collabora", + "image": "ghcr.io/nextcloud-releases/aio-collabora", + "init": true, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 9 + }, + "expose": [ + "9980" + ], + "internal_port": "9980", + "environment": [ + "aliasgroup1=https://%NC_DOMAIN%:443,http://nextcloud-aio-apache:23973", + "extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:mount_jail_tree=false --o:logging.level=warning --o:logging.level_startup=warning --o:home_mode.enable=true %COLLABORA_SECCOMP_POLICY% --o:remote_font_config.url=https://%NC_DOMAIN%/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+", + "dictionaries=%COLLABORA_DICTIONARIES%", + "TZ=%TIMEZONE%", + "server_name=%NC_DOMAIN%", + "DONT_GEN_SSL_CERT=1" + ], + "restart": "unless-stopped", + "nextcloud_exec_commands": [ + "echo 'Activating Collabora config...'", + "php /var/www/html/occ richdocuments:activate-config --wopi-url='http://nextcloud-aio-apache:23973' --callback-url='http://nextcloud-aio-apache:23973'" + ], + "profiles": [ + "collabora" + ], + "cap_add": [ + "MKNOD", + "SYS_ADMIN", + "SYS_CHROOT", + "FOWNER", + "CHOWN" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-talk", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/1358", + "display_name": "Talk", + "image": "ghcr.io/nextcloud-releases/aio-talk", + "user": "1000", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "ports": [ + { + "ip_binding": "", + "port_number": "%TALK_PORT%", + "protocol": "tcp" + }, + { + "ip_binding": "", + "port_number": "%TALK_PORT%", + "protocol": "udp" + } + ], + "expose": [ + "8081" + ], + "internal_port": "%TALK_PORT%", + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "TALK_HOST=nextcloud-aio-talk", + "TURN_SECRET=%TURN_SECRET%", + "SIGNALING_SECRET=%SIGNALING_SECRET%", + "TZ=%TIMEZONE%", + "TALK_PORT=%TALK_PORT%", + "INTERNAL_SECRET=%TALK_INTERNAL_SECRET%" + ], + "secrets": [ + "TURN_SECRET", + "SIGNALING_SECRET", + "TALK_INTERNAL_SECRET" + ], + "restart": "unless-stopped", + "profiles": [ + "talk", + "talk-recording" + ], + "read_only": true, + "tmpfs": [ + "/var/log/supervisord", + "/var/run/supervisord", + "/opt/eturnal/run", + "/conf", + "/tmp" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-talk-recording", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Talk Recording", + "image": "ghcr.io/nextcloud-releases/aio-talk-recording", + "user": "122", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "1234" + ], + "internal_port": "1234", + "environment": [ + "NC_DOMAIN=%NC_DOMAIN%", + "TZ=%TIMEZONE%", + "RECORDING_SECRET=%RECORDING_SECRET%", + "INTERNAL_SECRET=%TALK_INTERNAL_SECRET%" + ], + "volumes": [ + { + "source": "nextcloud_aio_talk_recording", + "destination": "/tmp", + "writeable": true + } + ], + "shm_size": 2147483648, + "secrets": [ + "RECORDING_SECRET", + "TALK_INTERNAL_SECRET" + ], + "restart": "unless-stopped", + "profiles": [ + "talk-recording" + ], + "devices": [ + "/dev/dri" + ], + "enable_nvidia_gpu": true, + "read_only": true, + "tmpfs": [ + "/conf" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-borgbackup", + "image_tag": "%AIO_CHANNEL%", + "image": "ghcr.io/nextcloud-releases/aio-borgbackup", + "init": true, + "environment": [ + "BORG_REMOTE_REPO=%BORGBACKUP_REMOTE_REPO%", + "BORG_PASSWORD=%BORGBACKUP_PASSWORD%", + "BORG_MODE=%BORGBACKUP_MODE%", + "SELECTED_RESTORE_TIME=%SELECTED_RESTORE_TIME%", + "RESTORE_EXCLUDE_PREVIEWS=%RESTORE_EXCLUDE_PREVIEWS%", + "BACKUP_RESTORE_PASSWORD=%BACKUP_RESTORE_PASSWORD%", + "ADDITIONAL_DIRECTORIES_BACKUP=%ADDITIONAL_DIRECTORIES_BACKUP%", + "BORGBACKUP_HOST_LOCATION=%BORGBACKUP_HOST_LOCATION%", + "BORG_HOST_ID=nextcloud-aio-borgbackup", + "BORG_RETENTION_POLICY=%BORG_RETENTION_POLICY%" + ], + "volumes": [ + { + "source": "nextcloud_aio_backup_cache", + "destination": "/root", + "writeable": true + }, + { + "source": "%NEXTCLOUD_DATADIR%", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_nextcloud_data", + "writeable": true + }, + { + "source": "nextcloud_aio_mastercontainer", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer", + "writeable": true + }, + { + "source": "%BORGBACKUP_HOST_LOCATION%", + "destination": "/mnt/borgbackup", + "writeable": true + }, + { + "source": "nextcloud_aio_elasticsearch", + "destination": "/nextcloud_aio_volumes/nextcloud_aio_elasticsearch", + "writeable": true + }, + { + "source": "nextcloud_aio_redis", + "destination": "/mnt/redis", + "writeable": true + } + ], + "secrets": [ + "BORGBACKUP_PASSWORD" + ], + "devices": [ + "/dev/fuse" + ], + "cap_add": [ + "SYS_ADMIN" + ], + "cap_drop": [ + "NET_RAW" + ], + "apparmor_unconfined": true, + "read_only": true, + "tmpfs": [ + "/tmp", + "/nextcloud_aio_volumes" + ] + }, + { + "container_name": "nextcloud-aio-watchtower", + "image_tag": "%AIO_CHANNEL%", + "image": "ghcr.io/nextcloud-releases/aio-watchtower", + "init": true, + "environment": [ + "CONTAINER_TO_UPDATE=nextcloud-aio-mastercontainer" + ], + "volumes": [ + { + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", + "writeable": false + } + ], + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-domaincheck", + "image_tag": "%AIO_CHANNEL%", + "image": "ghcr.io/nextcloud-releases/aio-domaincheck", + "init": true, + "ports": [ + { + "ip_binding": "%APACHE_IP_BINDING%", + "port_number": "%APACHE_PORT%", + "protocol": "tcp" + } + ], + "internal_port": "%APACHE_PORT%", + "environment": [ + "INSTANCE_ID=%INSTANCE_ID%", + "APACHE_PORT=%APACHE_PORT%" + ], + "secrets": [ + "INSTANCE_ID" + ], + "stop_grace_period": 1, + "read_only": true, + "tmpfs": [ + "/etc/lighttpd", + "/var/www/domaincheck" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-clamav", + "image_tag": "%AIO_CHANNEL%", + "display_name": "ClamAV", + "image": "ghcr.io/nextcloud-releases/aio-clamav", + "user": "100", + "init": false, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 9 + }, + "expose": [ + "3310" + ], + "internal_port": "3310", + "environment": [ + "TZ=%TIMEZONE%", + "MAX_SIZE=%NEXTCLOUD_UPLOAD_LIMIT%" + ], + "volumes": [ + { + "source": "nextcloud_aio_clamav", + "destination": "/var/lib/clamav", + "writeable": true + } + ], + "restart": "unless-stopped", + "profiles": [ + "clamav" + ], + "read_only": true, + "tmpfs": [ + "/tmp", + "/var/log/clamav", + "/run/clamav", + "/var/log/supervisord", + "/var/run/supervisord" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-onlyoffice", + "image_tag": "%AIO_CHANNEL%", + "display_name": "OnlyOffice", + "image": "ghcr.io/nextcloud-releases/aio-onlyoffice", + "init": true, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 9 + }, + "expose": [ + "80" + ], + "internal_port": "80", + "environment": [ + "TZ=%TIMEZONE%", + "JWT_ENABLED=true", + "JWT_HEADER=AuthorizationJwt", + "JWT_SECRET=%ONLYOFFICE_SECRET%" + ], + "volumes": [ + { + "source": "nextcloud_aio_onlyoffice", + "destination": "/var/lib/onlyoffice", + "writeable": true + } + ], + "secrets": [ + "ONLYOFFICE_SECRET" + ], + "restart": "unless-stopped", + "nextcloud_exec_commands": [ + "echo 'Activating OnlyOffice config...'", + "php /var/www/html/occ onlyoffice:documentserver --check" + ], + "profiles": [ + "onlyoffice" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-imaginary", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Imaginary", + "image": "ghcr.io/nextcloud-releases/aio-imaginary", + "user": "65534", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "9000" + ], + "internal_port": "9000", + "environment": [ + "TZ=%TIMEZONE%", + "IMAGINARY_SECRET=%IMAGINARY_SECRET%" + ], + "restart": "unless-stopped", + "cap_add": [ + "SYS_NICE" + ], + "cap_drop": [ + "NET_RAW" + ], + "profiles": [ + "imaginary" + ], + "read_only": true, + "tmpfs": [ + "/tmp" + ], + "secrets": [ + "IMAGINARY_SECRET" + ] + }, + { + "container_name": "nextcloud-aio-fulltextsearch", + "image_tag": "%AIO_CHANNEL%", + "documentation": "https://github.com/nextcloud/all-in-one/discussions/1709", + "display_name": "Fulltextsearch", + "image": "ghcr.io/nextcloud-releases/aio-fulltextsearch", + "init": false, + "healthcheck": { + "start_period": "60s", + "test": "/healthcheck.sh", + "interval": "10s", + "timeout": "5s", + "start_interval": "5s", + "retries": 5 + }, + "expose": [ + "9200" + ], + "internal_port": "9200", + "environment": [ + "TZ=%TIMEZONE%", + "ES_JAVA_OPTS=%FULLTEXTSEARCH_JAVA_OPTIONS%", + "bootstrap.memory_lock=true", + "cluster.name=nextcloud-aio", + "discovery.type=single-node", + "logger.level=WARN", + "http.port=9200", + "xpack.license.self_generated.type=basic", + "xpack.security.enabled=false", + "FULLTEXTSEARCH_PASSWORD=%FULLTEXTSEARCH_PASSWORD%" + ], + "volumes": [ + { + "source": "nextcloud_aio_elasticsearch", + "destination": "/usr/share/elasticsearch/data", + "writeable": true + } + ], + "restart": "unless-stopped", + "profiles": [ + "fulltextsearch" + ], + "secrets": [ + "FULLTEXTSEARCH_PASSWORD" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-docker-socket-proxy", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Docker Socket Proxy", + "image": "ghcr.io/nextcloud-releases/aio-docker-socket-proxy", + "init": true, + "internal_port": "2375", + "environment": [ + "TZ=%TIMEZONE%" + ], + "volumes": [ + { + "source": "%WATCHTOWER_DOCKER_SOCKET_PATH%", + "destination": "/var/run/docker.sock", + "writeable": false + } + ], + "restart": "unless-stopped", + "read_only": true, + "tmpfs": [ + "/tmp" + ], + "cap_drop": [ + "NET_RAW" + ] + }, + { + "container_name": "nextcloud-aio-whiteboard", + "image_tag": "%AIO_CHANNEL%", + "display_name": "Whiteboard", + "image": "ghcr.io/nextcloud-releases/aio-whiteboard", + "user": "65534", + "init": true, + "healthcheck": { + "start_period": "0s", + "test": "/healthcheck.sh", + "interval": "30s", + "timeout": "30s", + "start_interval": "5s", + "retries": 3 + }, + "expose": [ + "3002" + ], + "tmpfs": [ + "/tmp" + ], + "internal_port": "3002", + "environment": [ + "TZ=%TIMEZONE%", + "NEXTCLOUD_URL=https://%NC_DOMAIN%", + "JWT_SECRET_KEY=%WHITEBOARD_SECRET%", + "STORAGE_STRATEGY=redis", + "REDIS_HOST=nextcloud-aio-redis", + "REDIS_HOST_PASSWORD=%REDIS_PASSWORD%", + "BACKUP_DIR=/tmp" + ], + "secrets": [ + "WHITEBOARD_SECRET", + "REDIS_PASSWORD" + ], + "restart": "unless-stopped", + "profiles": [ + "whiteboard" + ], + "read_only": true, + "cap_drop": [ + "NET_RAW" + ] + } + ] +} diff --git a/nextcloud-aio/php/data/.gitkeep b/nextcloud-aio/php/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nextcloud-aio/php/domain-validator.php b/nextcloud-aio/php/domain-validator.php new file mode 100644 index 0000000..57506b8 --- /dev/null +++ b/nextcloud-aio/php/domain-validator.php @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nextcloud-aio/php/psalm.xml b/nextcloud-aio/php/psalm.xml new file mode 100644 index 0000000..d7ce38c --- /dev/null +++ b/nextcloud-aio/php/psalm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/nextcloud-aio/php/public/automatic_reload.js b/nextcloud-aio/php/public/automatic_reload.js new file mode 100644 index 0000000..7b14a3c --- /dev/null +++ b/nextcloud-aio/php/public/automatic_reload.js @@ -0,0 +1,19 @@ +document.addEventListener("DOMContentLoaded", function(event) { + if (document.hasFocus()) { + // hide reload button if the site reloads automatically + let list = document.getElementsByClassName("reload button"); + for (let i = 0; i < list.length; i++) { + // list[i] is a node with the desired class name + list[i].style.display = 'none'; + } + + // set timeout for reload + setTimeout(function(){ + window.location.reload(1); + }, 5000); + } else { + window.addEventListener("beforeunload", function() { + document.getElementById('overlay').classList.add('loading') + }); + } +}); diff --git a/nextcloud-aio/php/public/before-unload.js b/nextcloud-aio/php/public/before-unload.js new file mode 100644 index 0000000..33c473b --- /dev/null +++ b/nextcloud-aio/php/public/before-unload.js @@ -0,0 +1,3 @@ +window.addEventListener("beforeunload", function() { + document.getElementById('overlay').classList.add('loading') +}); \ No newline at end of file diff --git a/nextcloud-aio/php/public/containers-form-submit.js b/nextcloud-aio/php/public/containers-form-submit.js new file mode 100644 index 0000000..b7ffd2d --- /dev/null +++ b/nextcloud-aio/php/public/containers-form-submit.js @@ -0,0 +1,88 @@ +document.addEventListener("DOMContentLoaded", function () { + // Hide submit button initially + const optionsFormSubmit = document.getElementById("options-form-submit"); + optionsFormSubmit.style.display = 'none'; + + const communityFormSubmit = document.getElementById("community-form-submit"); + communityFormSubmit.style.display = 'none'; + + // Store initial states for all checkboxes + const initialStateOptionsContainers = {}; + const initialStateCommunityContainers = {}; + const optionsContainersCheckboxes = document.querySelectorAll("#options-form input[type='checkbox']"); + const communityContainersCheckboxes = document.querySelectorAll("#community-form input[type='checkbox']"); + + optionsContainersCheckboxes.forEach(checkbox => { + initialStateOptionsContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state + }); + + communityContainersCheckboxes.forEach(checkbox => { + initialStateCommunityContainers[checkbox.id] = checkbox.checked; // Use checked property to capture actual initial state + }); + + // Function to compare current states to initial states + function checkForOptionContainerChanges() { + let hasChanges = false; + + optionsContainersCheckboxes.forEach(checkbox => { + if (checkbox.checked !== initialStateOptionsContainers[checkbox.id]) { + hasChanges = true; + } + }); + + // Show or hide submit button based on changes + optionsFormSubmit.style.display = hasChanges ? 'block' : 'none'; + } + + // Function to compare current states to initial states + function checkForCommunityContainerChanges() { + let hasChanges = false; + + communityContainersCheckboxes.forEach(checkbox => { + if (checkbox.checked !== initialStateCommunityContainers[checkbox.id]) { + hasChanges = true; + } + }); + + // Show or hide submit button based on changes + communityFormSubmit.style.display = hasChanges ? 'block' : 'none'; + } + + // Event listener to trigger visibility check on each change + optionsContainersCheckboxes.forEach(checkbox => { + checkbox.addEventListener("change", checkForOptionContainerChanges); + }); + + communityContainersCheckboxes.forEach(checkbox => { + checkbox.addEventListener("change", checkForCommunityContainerChanges); + }); + + // Custom behaviors for specific options + function handleTalkVisibility() { + const talkRecording = document.getElementById("talk-recording"); + if (document.getElementById("talk").checked) { + talkRecording.disabled = false; + } else { + talkRecording.checked = false; + talkRecording.disabled = true; + } + checkForOptionContainerChanges(); // Check changes after toggling Talk Recording + } + + function handleDockerSocketProxyWarning() { + if (document.getElementById("docker-socket-proxy").checked) { + alert('⚠️ Warning! Enabling this container comes with possible Security problems since you are exposing the docker socket and all its privileges to the Nextcloud container. Enable this only if you are sure what you are doing!'); + } + } + + // Initialize event listeners for specific behaviors + document.getElementById("talk").addEventListener('change', handleTalkVisibility); + document.getElementById("docker-socket-proxy").addEventListener('change', handleDockerSocketProxyWarning); + + // Initialize talk-recording visibility on page load + handleTalkVisibility(); // Ensure talk-recording is correctly initialized + + // Initial call to check for changes + checkForOptionContainerChanges(); + checkForCommunityContainerChanges(); +}); diff --git a/nextcloud-aio/php/public/disable-clamav.js b/nextcloud-aio/php/public/disable-clamav.js new file mode 100644 index 0000000..18b0808 --- /dev/null +++ b/nextcloud-aio/php/public/disable-clamav.js @@ -0,0 +1,5 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Clamav + let clamav = document.getElementById("clamav"); + clamav.disabled = true; +}); \ No newline at end of file diff --git a/nextcloud-aio/php/public/disable-collabora.js b/nextcloud-aio/php/public/disable-collabora.js new file mode 100644 index 0000000..3064ef5 --- /dev/null +++ b/nextcloud-aio/php/public/disable-collabora.js @@ -0,0 +1,5 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Collabora + let collabora = document.getElementById("collabora"); + collabora.disabled = true; +}); \ No newline at end of file diff --git a/nextcloud-aio/php/public/disable-docker-socket-proxy.js b/nextcloud-aio/php/public/disable-docker-socket-proxy.js new file mode 100644 index 0000000..7491042 --- /dev/null +++ b/nextcloud-aio/php/public/disable-docker-socket-proxy.js @@ -0,0 +1,7 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Docker socket proxy + let dockerSocketProxy = document.getElementById("docker-socket-proxy"); + if (dockerSocketProxy) { + dockerSocketProxy.disabled = true; + } +}); diff --git a/nextcloud-aio/php/public/disable-fulltextsearch.js b/nextcloud-aio/php/public/disable-fulltextsearch.js new file mode 100644 index 0000000..d6d8f92 --- /dev/null +++ b/nextcloud-aio/php/public/disable-fulltextsearch.js @@ -0,0 +1,5 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Fulltextsearch + let fulltextsearch = document.getElementById("fulltextsearch"); + fulltextsearch.disabled = true; +}); \ No newline at end of file diff --git a/nextcloud-aio/php/public/disable-imaginary.js b/nextcloud-aio/php/public/disable-imaginary.js new file mode 100644 index 0000000..1850598 --- /dev/null +++ b/nextcloud-aio/php/public/disable-imaginary.js @@ -0,0 +1,5 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Imaginary + let imaginary = document.getElementById("imaginary"); + imaginary.disabled = true; +}); \ No newline at end of file diff --git a/nextcloud-aio/php/public/disable-onlyoffice.js b/nextcloud-aio/php/public/disable-onlyoffice.js new file mode 100644 index 0000000..a087bd8 --- /dev/null +++ b/nextcloud-aio/php/public/disable-onlyoffice.js @@ -0,0 +1,7 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // OnlyOffice + let onlyoffice = document.getElementById("onlyoffice"); + if (onlyoffice) { + onlyoffice.disabled = true; + } +}); \ No newline at end of file diff --git a/nextcloud-aio/php/public/disable-talk-recording.js b/nextcloud-aio/php/public/disable-talk-recording.js new file mode 100644 index 0000000..72c5de3 --- /dev/null +++ b/nextcloud-aio/php/public/disable-talk-recording.js @@ -0,0 +1,4 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Talk-recording + document.getElementById("talk-recording").disabled = true; +}); diff --git a/nextcloud-aio/php/public/disable-talk.js b/nextcloud-aio/php/public/disable-talk.js new file mode 100644 index 0000000..c37d72a --- /dev/null +++ b/nextcloud-aio/php/public/disable-talk.js @@ -0,0 +1,5 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Talk + let talk = document.getElementById("talk"); + talk.disabled = true; +}); \ No newline at end of file diff --git a/nextcloud-aio/php/public/disable-whiteboard.js b/nextcloud-aio/php/public/disable-whiteboard.js new file mode 100644 index 0000000..50e1215 --- /dev/null +++ b/nextcloud-aio/php/public/disable-whiteboard.js @@ -0,0 +1,5 @@ +document.addEventListener("DOMContentLoaded", function(event) { + // Whiteboard + let whiteboard = document.getElementById("whiteboard"); + whiteboard.disabled = true; +}); diff --git a/nextcloud-aio/php/public/forms.js b/nextcloud-aio/php/public/forms.js new file mode 100644 index 0000000..3adc399 --- /dev/null +++ b/nextcloud-aio/php/public/forms.js @@ -0,0 +1,90 @@ +"use strict"; + +function showPassword(id) { + let passwordField = document.getElementById(id); + if (passwordField.type === "password" && passwordField.value !== "") { + passwordField.type = "text"; + } else if (passwordField.type === "text" && passwordField.value === "") { + passwordField.type = "password"; + } +} + +(function (){ + let lastError; + + function showError(message) { + const body = document.getElementsByTagName('body')[0] + const toast = document.createElement("div") + toast.className = "toast error" + toast.prepend(message) + if (lastError) { + lastError.remove() + } + lastError = toast + body.prepend(toast) + setTimeout(toast.remove.bind(toast), 10000) + } + + function handleEvent(e) { + const xhr = e.target; + if (xhr.status === 201) { + window.location.replace(xhr.getResponseHeader('Location')); + } else if (xhr.status === 422) { + disableSpinner() + showError(xhr.response); + } else if (xhr.status === 500) { + showError("Server error. Please check the mastercontainer logs for details. This page will reload after 10s automatically. Then you can check the mastercontainer logs."); + // Reload after 10s since it is expected that the updated view is shown (e.g. after starting containers) + setTimeout(function(){ + window.location.reload(1); + }, 10000); + } else { + // If the responose is not one of the above, we should reload to show the latest content + window.location.reload(1); + } + } + + function enableSpinner() { + document.getElementById('overlay').classList.add('loading'); + } + + function disableSpinner() { + document.getElementById('overlay').classList.remove('loading'); + } + + function initForm(form) { + function submit(event) + { + if (lastError) { + lastError.remove() + } + let xhr = new XMLHttpRequest(); + xhr.addEventListener('load', handleEvent); + xhr.addEventListener('error', () => showError("Failed to talk to server.")); + xhr.addEventListener('error', () => disableSpinner()); + xhr.open(form.method, form.getAttribute("action")); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + enableSpinner(); + xhr.send(new URLSearchParams(new FormData(form))); + event.preventDefault(); + } + + form.onsubmit = submit; + console.info(form); + } + + function initForms() { + const forms = document.querySelectorAll('form.xhr') + console.info("Making " + forms.length + " form(s) use XHR."); + for (const form of forms) { + initForm(form); + } + } + + if (document.readyState === 'loading') { + // Loading hasn't finished yet + document.addEventListener('DOMContentLoaded', initForms); + } else { // `DOMContentLoaded` has already fired + initForms(); + } +})() diff --git a/nextcloud-aio/php/public/img/favicon.png b/nextcloud-aio/php/public/img/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..743566e3acc8be472a70e1d4d8002831adbee4d8 GIT binary patch literal 90022 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 literal 0 HcmV?d00001 diff --git a/nextcloud-aio/php/public/img/jo-myoung-hee-fluid-dark.webp b/nextcloud-aio/php/public/img/jo-myoung-hee-fluid-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..314048f984f4b5f95cecfa162ad07ff26e1e3d52 GIT binary patch literal 97010 zcmZ^Kb9AKH7H@2$V|VNhI<{@wwr!(hJL%ZAlkV8IZL419&di;;>%I41tyQ&7o$q}6 z?7e^33gRLnCmz5+s=@+tDspUu8=s$%l;N^~selmepm-QDqlAcw3V(=m!HI!_H8lHh zH-FvXa2SM~-~0gd`97Q%MSDX6cz1bko#dNGUf(A_)<%;6OM)&>-M5dEiYwi(uhQ2N zmyZqAub2Fn?=O{;mNVUVzJPZH-uL@@cfY#@kOYW)-~Id^Fh+33y9)4ojD79>$XKGkpFGpO>+b(}^zD5~0GI-D z-pSi^uXN9SF92BpMgZUl@|FI{_6@(&XCS9d7x3QO{nNJuuzGe6zyVah@;-mAc91_0$N`&lHQ`Nj5uCyS>FaQxA~#rX_)^nJ^2 zBlzg-^EvQ6ew2Ps1)M;@A$O=Qw*JC0(7w~!gG*x@0lp}HVa+e!_wy|Mh%PC6*26#X zEwG^KQiE*EIa-ORPX51sU}qbeX$Lp5eAB}{2N0Q4_Nqa2=ACXt*QEhnm%XzV zRviU;^x`>4_K^IxxfM!tmu%eKi=e&;6Jq%1r(>F4L}DZPfy-p&yOh4R@c-XJqT-4F zEcn0w*q_wjh3Njxw#jmB53WAm0y^gec(E0RpHK_*Nw`%oL!2w(0X0s9B-*F_=<|WU z*{AM@d130Ik#6|V_$QF#I+I)09d7+*!hmB(!FHgKEI2f#_}NZj4~#O_ z=TVusRY+>bg(AfZ#kCX8p4&_72!mXPW|}sVpRNI&h1!hZMxb2Las2Q3f5-mwJtQ!T z{`Y;nZ1MW^B8Uht(@AT2MRv zryVM_XnMZPx8UD2Ao%apbqzG(yfT6EYW~(c_Y}o{91Dvo&AX#xcj%@8XoltmYP9-6 zZs)S2$jM9i!qdg^%P?MnG8PYevehiN4CjXHL}SorJoq-{V)JI(|0^~%9tydRTB5Z7 z41Q4Gb`|WZcAC%YL`0CgX0{`{8*9bM8~>a9c;iR>1UDgxTMeaK2DL(9CC&GpRmh(? zQhUOczI5ahLEf_D4K?hY0h5l0SmRnUl#;EP-1rjbrR06kj4RMf1a-$vu8oURIdy97 zoJz`G1r4>0fvVGDYZiY;OpV&@>OU7VXY7Md1piJm3M~3gFz!b+ z{x;sZ3E3D|XVa)K6>da%Q?UXM(&wh4$7#Ml5#`a$YqWo#em%Q?^7qKuk`*nvQ{@mH ztBOb%7fcA9W;qM+If8P^v7l6WJB4^Dmr2H^YEL^%!;s1%m>h)ue_UvAP09c!phsz$ zo!tPgGZJ^V@S9C>sE_9<*z)G#+E$23ikM^zctwD8@DJ<$`Rag3{EY?tID8Q?>aeh4NjN1Jj=j8W zamT`XDz&DdEMJ-%uw~xEP4$880-b&w zWg@@EiNrVM(=Y?5wgzb?f7dI}va>(#>^7%SCC1O%|J{iakQejA9LvfTX3qA$la^|g zjvdXAtGB16v3Jga>DN)wYJ{Z9InjKNI29`wJ`=eyoUvCD=+)Y-tEY8u&rt zLoB}kN!&z=R5@lnBnB(|5!GcNl02nqf~!y#4X4q{busbw!CfV|1e_O&jks3)1E&r1 zF|C+tz!kwVWj zi}^yFl(SBlmi?uS>eL`xBsOO5$>E8%r49c{s@2Kgt9zm-Kgz1rRh&RBD9mWHf-Wc1 z?_~u~@dB=|p1$tbA-dnd6WuM~hAFNf?|8W`B1qA!=nO*#k&v-!M(@%IKMzM`py?Qj-eo>wCM-4j`C&H^v*fL3U$nZOv6@@ARw*pJSqM%oG z^j}iHw%sasA+)OK*&$x>y_f`#g6GUK5o_onUCLnxjBI^_Co!s3_~o?5!WBm&36C&6 z3S0IjK=T4r`~ETKnV9YPZgH}jUGbUWuYR1YD2+kF-TzZkZ8|hwa&3|#MkN4#B!;>C z<;);_m(N*usEF`^zYCbr?752h%Gbd62y1mLgHWsE^W9Zqon2MEv`xtnS#c8MpfsTb zAGmpji)lvh(B!VJ5ymi6sL5x_u8_)G%KVT7X$zT4hkSYXC@3&$wN$n`1rgtFdU#OK zf$o@|pN_x%(;Xa_GCK1vTK-uMe3+^0{@5g1>r9=%$&~Tl08{A>CVnBy$YIhfn408B z^S!wyiTGC$KY^AUFXiNg6JI~u2Xtp77oVwau^?f6@DJqaN)zYbNc1=$;K;{V0=lE0 zZ>HCf$Yy}HstS!*350is-AgA3g7AOqO?A@x$ie7r_Z%Qdb0%TJw_fSPJ=9=-tgJW$ zmv(a%^4?{|l$aeW4dJK%%Z8@a77@#b9860jk6tSA%lI&yU@Lgy)1ERwb_$1gixv;7 z2NuI0wVD2YUJYjSR>%7`Rztj(svDGt)F_N~;o-iL~`+Y#M;G(zmca8_TBmO*< z%I$Ib=ss(TI=Xr8Oxbz4cvjkx=h)HoF4SAIIM@0n?c;xvi@;}3mA*kBc^I_okC6*poPP*XVapPw0;a>8d`vg0H?%Zq zuB^V+8F=T0ZnBz2w%_BCCl)UzcH~a3jZqEwP#Mo)LgAXM0r>lL(Q+~Ko%Dmy82 zg_pUwg%CAR>iQNr_&`l3>Y^`0!Ke$HKM@h_N`=E*fH}^_mvRz(I{lem+5_-UYz%`k zQU2<3!*2|KNKOGgVoi&FcCPfs?3|GqXjA5s4_5c5U!1ayHEb;MTYG)G&-I5#!S@zJ zkmv-<f9?W*jpFj}@DT3$m9(S&^6gE=wtwA+MDJmsxV!`Lf*`l*C{GTLj!({F zlV>5(2SU^E2H@7rQ@Z{pLp7jtWL}IPNqp$693tp^po7Umt@yEvUPj;%8F;AKLPJTG zj{JFS@7B*Z;e2ee2A{X=uYOvN)0vO^i1Uzlxqc3iB1LkWi(ET&Ury^j(;F?0yI!Pt zr)wz(hM4fX*DFj;euLPq$oHMW3h)tQaTFsHF}oE{bz<3~IQzD#YpGN5kbfRhXKQ`= z0<9o4p-=3@m+h+Vr3IBO1HFE1T~tna;)fu>BUG@~VW@K$aWC^Nx%;~lZp$dd)A1ES zV;alR`>`@1nv!6n;&JNsdNd2)S0Dr6i!tE8+kgJGU2?ZP)OQ@WDqmE{G_f=AjNzmD zY>k{+9^sYmCiv^yfn{%AJRAolzQ8dcVRl1L2fN1j37*Qm=P+MgXRmeJb+PX$29)kr zh_ro2eVmh~rJ2jKaRZ9jjD*z&-2rZBr1j2jQYbm#y(yL%#)SS`-;cNZY3V)NsdDm5 zH>)8(i()F)ueZ)2niET19%Ey%NHU7v9IYmVNwCbT_P1>WtP}4&pP{nn>>J-dv;E_Q zpXesN;@kV)^NRZq)ZHN;g#ridYZ_FSdo%znW!Yvl4s~EtG33Ft5JCiG5Gyr``3N$*1f9bc#icHT)gLVuYA>} z;LO=hm9?oreLrFYob}TqZnd`#CWQAc;f&kQ;mOl0Y z9vU@8k{NSH93CkHE6S?wY^xMR%*N4#y|{zmwkC!)e%*Xvj7bVaC(6~(;m0O7!+*sLxjVOwOn zLTBj2uVuCG%L^RWN|#W>yrBC;tqIy;4_!e`B6Tuasz$S?DNxy3u&w zHxff30FX#f?0u&-jiJDGtow|6>H4xVc%9**2Scq7Q+d0M`wLF@Q&-&7tGq*(c+ccX z9qY#!NJv%P&h3jwXurR)-h!L+PU@Lc=!GSMbM0G4?O2rIxVOC!uW-AF=yFJ~t-YTM zt3*L@uVrYLH#$>>=3;-GapU|Q%>AgIZHB`>uv*y=&|mTSADH;!KXY;1WI5VeKkc$T;mM#x47BbqO(s zfw0A)Zb=qeb~Mk0+2aEYMPPf~<4?aQKPrxa>^#ZoVPiSRU%qXJ{uCP|$=Ng~q`?O$ zUlGPnE=BUtpCm}MZ&AoBE( zVJN0I7A`L2ac8u;;B~s(z+J{hCn?0xF(D?5k)%|OLKp&oEA}EWe`RUKRd4u3wNafv zADhN?uI!slHOD24ZgGC3@OiQMPIdjA!OyeXas_&um3&#CquLg+2@^cmBSC?cb~vZJ zjP;ntPWH%!0H*IvQQ#LMd2&`$NC+-JXWi|@G7~`&YK}1>pB{=2#~)(($GmBWp0#ay zG}V!;9(1F&P)PrvVg6!GL?^)kpT&+FR8PL{%?J?K=xDbsXUuDG#T|^Fw|roFWyC3Y zXu;;DT?qk^uS;xH#VgG2d5#Xdlh-$=*68r#xn4Sd2SGgmZ^<=@p&cip%r$mWAf>Gi z`PW>0*qs0BY>mOL6w;4w{Zy_MX(72otV%}Run^8hgKt1G*#=N=z4Cn_RU}DcGIK0? zv^0zfe8n>^Bsk|*Nww|)TfiuzoobaeUxLFF^MJD>f4L&ZKJTYcA_wFm|3gaIm%H!q zb~%AmTP%R_x-mommq2`7mU0?)bmM)GVV-_c6*t1?m%0} zZjCLmDYD4k0b6k6< zxR!`xG&R5o9JvjMvN-i|Lud97$U4=%fz{Jh#eMBDj+CJKLH5DRAlaKoLxSl#VQni$ zXe{B*nvDF`DePnZqRr4Lemf-;J+$AJC&w>g@oyfj?hTxaRPiCdj|y{>28PXmF`stH zAUr#jcVbY(%=H_pwCtb^Tj{3b7O5nD?-i4qkKixQwbzUm8tWCXMHa&($cSiw3t?B= zZ(~X-KN2NedwU}|MJNv1WF94_CR572M({fcA@7Uu;Wr47Rhu$hlyB7_rB~VzS11@9 ziK)moBVY)%HZ(Ib>|5!$j^m3JV%|qCj4*zx7Lgq* zllVu0jeJE7lipXm(oFpa3Vh}ybetYf*fJSteRL4Nu&ALgj;bJ0O7Vl&MW!dcWEEw> zqjfXR?ODspGW!+@i*{9S1V|YnJ>tT@2C-#i)M$Kp1N!ChbcE}iega*EciG6NU$N#R ziuvp}mLV+2_J1=Vs^^iWv{oTtPjS~b852_h`T{HU^Gbt=BST>TZ?t-LTpXCelfzXx3V>t8s>FIn9v2erAaY2>cB31W*22%u>a8Zxj@L zDs?L_p6S*$-?&{@o<)EtrbN8pZ+k{j=Wo$s%@Q$G%r}L0ed8pePU1s@eHV8`` zzSlH%B1bOkfK@2R^}Qpy#bL`3@-wCTI43{L50)HL1f03C3g6R{lRAHCQO-Ax!%--c z0euyL^7KMr@UMkK_n^wX7@?jm-Kx_s3w7hp^yDg)q21<>uqSy81OM8x5yUm&0I?R? zM;6hX-2}zzDa*af_&?mW->KS-2@EeR$*ttGjUVVcrm{1n@iO>%h)1&P zE=!V~Xeh1zEgLX=b*n)UL)#MJ@~Lj5U|;&sf4OTs8i^Q=^jXGjnaH|%Z-7bp>${M2 zk57546rPRuN{{G~sH?FCyUo!djy(TizZ$SwVSD&*y>XE%WE*P2keEYs^z^p9Mk%L$ zrGp;B5@~8nC(O8R*UNs3CBLvGHP#4l+2LteI#gJ*2DsxoL^73;O)I!@@fML{VXRy* z$Zo*eSkHWc2D{g0vfX^P6QRG-YX)p9=r6dhR3Ob_lelaXQ$=Zg!~f#yk6EJ+fGeo; zJPrGPk5)>L>`Bn&SY$3Z!&tsT0+yp(Sw zxmZV}r%0!S2FxiS*`8_&oh4Z(q;OuZv)~Km04k4G08lGEkR?_;62eKy8K*zmvK^|p zUvWF80zOdAXy9=`xXCuTRK^b^SHB(sZGQ!(6SH7YhY1$oz00F7NQRKyzNqmcZV`t5 z&x?B(Bp82{AntjPbC3P%v*i;j#|g-JpgDSJ7HFOEnOBoTq{us4e@KF|^LnZB_@31P z{v69S%e@o0VmL!S3jAf><2T50*?U>A{Y{XC#KLe6vTX%Ai`gGmIYjS6Kl8&xItZAn z9XfAI+^!J)qfh7DyR=ABAeU9PBt+lCBeyBqDD_Np`G8!-Tq;;6e#rszT4;x3AoY93 zy9HT!5{BBVJH)|G(`i-)Mk-a@WOoI#{G)_$9)8_Qii@yhQXw0hBr8!hyB2Z>q<1K& zblkc6XJrPD?{j1zN1$L-9R}lh(nVUKamo%1*W?xx$aGs2SmU=_`R&QfTw4 z>%sc1X$ofN%-wTxePEcW=&TGWcfB4r#plQcqYCMZ#|jB09J8@2=8OUwl(aPi1-7Zz z?ysG@gqc^#jio4AWd!1m*!h6$FyaiaU?ViM+}7uR1@7S|RFs+&h0F!@IHh zVB;2+Q6eCA*nw~_S4g4N1?sPSf6qteXs`HDlSk~7Y1TqkJiwF z2lZO9-_C#JLit@m(nP~gJ+g6ckMVR-vIlYE6(xLzGh!ZCf+&C|g@#g|$bDw_DyQMg zAbgRAPd+ueEX)9mFLT`*QeBdyLsWhF_IUq;!isW$F+FoVE6Cq?_Os`b^gdccxpBg8 z*Qi0U3*TPf@8ANdAruL0)$MwzF8wGQXH@YskWmUdLQ_mT>mwoF`J`tJscJu>lrG0n zZ^qPu-f3N+n{TtAd~fp$JYH>(;^|sN&p8i=ex=_72o!#>XvtkA=HmgcpjUkV8r}CR z)Q3gb5whAXB94lA4>@Jv+-$l|Zz|!yk-JuX3X#u(?b#H{9HR6;NUG;Q#Hfz6O=I1g zSi-5U$An9Ma!Jb;(bKq|Yk?OOYfj-U%@ckf2tA#@bp%)Oiu{+Q=UC@mNgYoJVK1ug zwJ5DG>3LQ2Xo~YHNlXLI@t$CrFP4?SZ+czP+o2F>#2Rvv+wVW=fatWaB)WyKo=YpD ziAw-sLvZC8E-Jl7}>@uZP34xq54Zs}x%5NGGK8wI# zBAenn-l=%Yf^|3|=zBY178la&4owRH|e(cx%espv0r5OyI5Qbuj{y zL}-@At($?OR9+wgqH z)EWrI4ZrrJOm~TCLaNH}0cW}p^%@FZv2R)tD!0Yc-FE#@7C0SoZBE{*!vbs3qsF4r z>OdmzKM-a9VK)`NQipit9IaVwd@VzMwh}C>A!w($r7AygQueKgbyjSzdu5|ixrAWy zpg6XfIdt@a!T%J2J_~~~uLV`k1OhUSz4J%;S=o@|T~XAg96rX&>7?TC2Td^e^bFvz zjDTI122DZtiiS!oh@{~K|KEdgfo`p)W^5_rcGw;nd_!cDsP^MtZy386jHuzrSXTfr z$gkrW{c_?F+ca3l%97I?&1~iWh_V%ELKAmon51e~#sa2jLZ%eQ6A|O0?J#<~-KNRW zOUwd39Xs9h^B`IC@AlE@O$+Cmeh%LbFIkoa#AJ1Xk>RJRruR_KZ zP|ol96Az0oomVt=8rLkcHDP+yXJ4;JS{Hop40JFYc-mn7zEp^s3W?KlD#b9H8?(ZE zy~I6}8^wkDPs*g~%J3i;PZYnKi)=Y_9d*h!pTFhlP=)nj)=%(fMkIFtdLuKJ0d|C;f>J9^j#6(VTDkS>_|fP?)pw^b$R>`#IppJr3o^!&DWd7?BA0dH1*1fWgJ)ijVn2|-pXN^XvoW>5I0A` z1{}ZyM}smKqtYn%hw~7{cvn%gDk;S;aa8<)^!KRlx61H|Aq_tlg|B!5flyrwP|kC| zBegMR?ua8j?R_DFqPo<3y4}HAylg7CdX)@Oj$mSx=-hO{BcDIk)CU(=m|bAE-ZKg)X>e$Lz%<9W z`0{*e8u|<60kzEoJ7jx9wzw^FJ8KdX*X^63w%XoLy|eKbyBjFpN^r0~ib>}Y6u@}z zy%;aS|))U`W zGrld4ryOc(ljxg7xnHUxeh0gSSBfsx`|c>q1Qto)x->|3P&Ko2+CMbDDydeGz3Y)1 zJH0o#*-BhS4SDaP=J8Guz2M?MnjHb*!u_tq3S^`qE*i0u_#6E8IvXaz-%>F*QFcdd z1)QsGLMKjF+>2-e)3-{_zz^!;rBQKN>TQLSg>~zp?mMAcE4J_DK3vI#69x8q8PT&k znwoU$mLg$eQg^d%(x^z1T1oS;t%4i;r?a5tZ}70s&c9FSn5;()nWyMH4Y*EXx!7MV zyV_JA-~|6v+Ix2Wd4CE{^`ZX|WU26;wuDqENXPEy+R2g~b#sC;X?xQ#;WB7}Ea;?( z8KU*5`>!pgTp*b>zXEkNY;GH@7L*8@0VT0?g+)%t%Dja#+5)Ps5P|b)O98)>JfFFm zc>98ANalTBcK^2Plp~L|)UI6Wz9y{9G&Z1=i#muId$gg#W2JM^jeAJ*a9^=&-&+pR z+3K=gCly^g`D7U`;9OS^wgG-ls~4C8okUj@;O=7yVxH~wYK)8^?l;@Uidh}&bs4tP z4~$CpnlM7Q%$P>l9e0l9p{@#%5+`Cl(f2~g<3)x+gRMaLzErNsmX zQVQP{zADZsbsku`Z@3>3%r@y_tXp7hCGOTKg!Qub7Ap0lv6>Uj&2~L9-RYi; z{p!GD>p&VG`iK23365lR*=JT<(J616s&%_xZy43JGG@0PFi|KWyMKpMgo`4dHO$2Z z`!N%m%_X@I3FiTF*i2-K5ZIm#LP#Pc&)xA3e{FB%%80xfY`EXB758nmttazTM;q41XHlMZraJp$bkl73*sN3T4fyk~L zRdmIl{wBqD+8Uxli_vYTBHuxMm!^tYA#UwiO}Bw9< z!64WIgRur$Yeh_ik+MLW^h6`(yA&W99DfUIK3lb_Wl@gynl?uvIo3q&1LmQ~?NrZa zv4z=TNU2nfkL;6#nEQt8)&${-$LP`=RqI(~323~~7(!v@y0EY}MpKVtHk$oxLk#e8 z`gsrC4Qwbw;4!d+c}ah-8Vb2pjKBRL%=DCktuv&wG+;py6>JnBs* z-4q_5bm~T}QgnMDv`Ux+gA)-AWi-Hl|D{APGT7#LMbeAzy;xlFR?jn$z+XI5<$rr) z(u?^^?R)O}>P??|QvGDRLi1i8DMzZDlhJ7`-JB`|Ds~e%sl{G5=Q@-w-9zgyiQah+! zzt1VA7loe}c=V7X!$Rvdk~aLhn@WHh3ql{3VGV!sJIj+lBjR|O^JqPBE2ZO`U8_T< zH+&^6WW1OblO!~+Pjnl|o|7an0T*$O{>rD{Loa*40!#CWRCN}^bw>THQ??5u-#G}; z3e1~H29zI*QKUl&$*E7LoT~A z4AVk04D%NXTdVgiH>si{K_$D?iU!;``6_KXpjop-`{ZU;R&l;caA-Well|+7J$+n? zCCn@}-3MYrrH)+gKO7N_7aebaK+nspxFq^ylFg45GEcX-k7tYXdTE)LjVAZfr%#^9 z)rCy}N6A>2uA@ntOb&M_83MGfbSI0h11EOpVTD)!$D#NW6Stig zd_We_upwK^(`bELHkyWj>7wSBk<6w0gZ3BkE?Y$AHMEPTPsrl`U|^kIbyimF25nJ= zuHwsYy~YlW*!0Ae$l*mF;TJp|jpP=pX?qgE3|(}Z5j0FzfEoK$si04%?ppE@-Sn{l z!RHl9dqMM}z9Ma2xFS@*kC+_Z<3aZDrpzl!LXwjaDz&X}>$cGI1(JIo_!^1iPbZ~v zr;f8mXUQ?0Q&A@J2yRNwj$x@%Y7HE6qPfiL4b!TsMr!gx^mphQkWf#Q`J#Tc3=(xV~$Nf>ghW4rjYp% zLpPIA=%(|n%Zu!RU{AfgNpiRwNHsq+Tgv zmjz|ohad6;NwD#UvSeYz$raT9^6CbI2=bh4AY1khF_fi*!qKt&r_dMDD!d0^()&s! zQR-Dg7>sSl$8K%_y8j+i5H!!e3rc#LM$4i3LJ>8+R2FCssQF|BUbpXDx_+DQunE-^ zO3{N|Ch#j6mKN@Jj0hBn-aF~(Jd@6cKsEi0aLRky$Y;uzj&pgg^n^DKJSDUD)3G!M zhrh%+YF%v%g*4`*nqKm90h9fa$ca3g+P=$F|^K28?0yHHmehzCYU7pUJmsrZfYw9^LOwh>*95CbQ2 zE4|xQ%@3}dw#ai~C+j3mxl7oz$}}fCW?OM?uxXiluAG?b*mp2Aivf+$E2}5YpB1uYbENI%T0e4 zSL(j-^Nq^_J~|#kw}koNyp^yZeJ9qw7ZKHqcd|_C3O(jWyHB%m{ZK}ee_lZ1pYwna z(Q5hVul8x*=>H7@bPDZ0MdA?CK`*38*t~;wRc>y`V<)Hb(5no;KlVG@F2D(ga<~)i zQt|hF+fl-e8}iDEZb~>PO+U3>;NP}a0;T5YS`LaN;351a^eR=(jC$Hj*zUd7U~y6> zGB)x1RxP02bV;B>vBB7FVr5?JyGd8}SgMOL!cKcwP49dr+!-9^LRRBmu7yQ04?$_= zS0SDd;vJR=b|+8sdKA#xY69@EjtW<5bvw^^RE};+^m<1tZJcwOuq_SrH`vr@|5Vk6 z5HDQZnSCQhbm5KMN$8eRJ> zG9eAh)r|k30Wc zZRi+1B`YB^v3bEtOD`l?q$o~}=i0pn**eYQW^zu9_^y@OOl$G>loqFrfS``zSVv4V zP=;OV1aNeDExG_#{&CgR*^LO-l1C0QL~@zEB#_5dAE3L@u1p`AE7vawO5yZ{Hu9T~ zH8rVa|6qYnAY64s1w6$kJn?Nidkhp=YKuGby?S0?&yZ1fp3WPevb_>j`icvh4=E4$ zRhPz>KA7=9^@sSQt2TmX&E(`S(Az5@7hSeI`ZqUmt5r~*iT!mvt|+i5FOwfq79~`d zcmr?x7L=)suR&Lb4Sj7Ob!PmqAHqSAtDK0(9WH|lbz9nvcJtB>mYDli-uBwy0$-8K z2hZNOFSho=RFzz3T?R_Gve!9{{}S8JqO^;%2GSpY2t^N9EV|OCs|9>VwWHF6^57C? zH6C39?ex;daNlC0og50T*vdR?%UDRFW!N{$t$bocrkNWAW&<6ulJz>M{)!Ie>McC| zIvu7SEnV`viUokD0bEp1l9*xnwh`lUwIa2< zE*^B}Bdm;)e7BDoNyzj_Tv+tXXQX+GPr0uAN{wj6cvEH4`020NkILOukWLsw-o@Wa zP@rfWl}9=n#@CA{UL(@~ipznA(j}=Jy5jW^UnZ9z$sdlVT%imtI~$^5K4s zRMQR9KUaU#!n^k#mVlGDS4V*J`}+v;ePAzY!6!d8stB+|By*$sz#?T=D>RbmHwg2q z^l=va;9l9_g5U`hFMib5?)2BjKOb+I;yQgMe~<+J7Ql5l2E~qwT@{$s^igHc=8J08LH$2nr4Yjp99(*Ppib@XIN^3sXR!0~D&PC+{F&Y5G6 zpG;m8-ite1g7Q&EvNJAIjc47_B5?YTO6rS7-isQ zL$6}L>>|3p%OUqzo5ap36GQs+ahuKgmINPn@mkUc&(!%Mev1}5>G{`yj#UgUKB@^K zK^8`yY6EVCoR%6VLC^i!I@gGxE6;^Qfu!Fok>{w89|*zzheefaFg*;Fp{(l-4%+5z zBM1=wbboBjllyL+d`l=I(@uQSjV-x>(;a%5KowmTt+}1u1+=d3dz7;g!8vL?+8q)` z;kb|~TLpHBdZ{(5?R+%!D>wl)yf&6LJP4$~4qHy_oNI=|+1H}mWXA)bmGW`D^3!j2 zX5r<3*L3_7@0%x27*v(=iCK(jT9cI@7B!YFhT^a$Sj}G18ZRWYv*r==vQmW}x=!84 zSgp0XNDxC5De9+4- z5|g*x;_ORNhqqN(GcYo>fxf!*$Wi7z<@R%6vnn3HJ%~{8V!K1p5ZNu%;YVqCyF+&3 z7pv-RzMfP|uIB4{DsPRHfob11#nR7ma*Np3>!8fsdSaJ={WGBjbzDl%2dajcyv3AA z00^??rJvO=3{{!WDr<`?fq^6LXPcb5xEC1Zblz_psq1pfRT`=oA8#niQm;buSIDLj zD4=WpemSZL>f6Z+B9q)QTrHgLP?c1DJ^#+0p>#pZgA~qa9s{XyMzyAb>Y3L4eXA!H z!#(W4PhrqFvJmbGO?QE`L2wKqw*;5oIgG{$VB1qTiClQa)XR#;|7E{HU<`H$x?Dx_pK-fG3gS0@(-l;*C;|YqBlpXgq98piC$s9aYv-+r}=Jf z*F^2L?j?T+^dv+OLES11Pati&BAm7FJL?xk(Pw6KUbqa0ULQzcnYZt}K!~yo37t~H zkIpu7yKJ?Hx5bQ$9Aq)dA4wx-x@ot@{%{+D2p|IQqFe9gvDp=rpfB+C+;gRgi%2Y* z3>Y8H^kbR1!Ej)7&Y@ofubH(Bd{BR`>PbIq!m|~oIZ)~bAaql4C2a#~@_arzv8sm{R=!(B13J@B?@jL~D=xft&H0N1X zmt1R?$T4yU!+1;>GPo+ov9AzjllM9*ANHG!lxN}^ImdJHD*qk;Jl0zhox6f-{0PSFiJ zrezIqRLmWtoh6KkvLPhoWF=4?Z-#bxHhhb3BSr;794{z(xz8wa_dSj`>_kdLbxH!Y z&VOwi=(;QkY@}-BH7Snmst%=9yK}G*pG@eK3PmoNr=Dj$Lu@tU2WVJ>l zqOVle%zM2FtG4A5nO<+w>-fDvhzGE2QLbWzWB4B;qzPdu^SS;0lBbz;4QsUnvOq&? zX{(xge*NnIo)H>EsEP24LF*V~0Y8hhDQ~NwV7y|MH^1Ee^ied#rh~z)Klk{F+cn!U*H7usH+Lggo0PgFoB1x zS(WC3vUcF9hrdtT!5p3bqFwP~ZD~*NikPoq@Egy#pJnAM_Dcj$D@r9OBRQ+)V3b^f0@P%sA&_tkw0eZLsgq$B%VvR*W_##1dn6c$I#MlgfD%$OJUCvc#F%K+(;Ux0a!w^ zY^{0gt%W3@)nN{)3pH%IK#*h}H0H5VjMqbgCW-#lmzKL!RA*`VJy*8G*?||Y=JKs$ z;VwZ3W=!i`12S6f6@yFk*`bxp%z^_cZH3j}m-lQNDK{p9_+-0qaCf8Jb+OAy&Pw;K z@b7FSeg16#aN4;-Zu*oAtzu9;AQK7X+Kzfj6Emeq}WzV!v0t|h)Tk1wZ!C*mOre@l>a1esPW4mR0HKRD3mJ(ihu)e+f|;p%%u zda9gwD8YvC4bQnA&DJ&uY#)J8%+7p^7((APu6;Uxm>?z3ZQir&m3@mb_e<~Lw%=i zRoMCnQ*Bo?rMr6Bw;qf>YLpLjd@uZEYb^}j^%t%Rg>53$hGQc8$uF9$HTvmxdl30W zu$yGZIIop^f%Tym^1FvN2B(BG+gvE#kDoox62iPBz^}?Y%+;s`f~Q>JKsWs|U+lcX znfebu;FGN_pzl=VhPIa}X1^J>O$J;KMqud&&Z$^rvl^L^Vw@#`e8E8;0l`JzPfRVB z{NQ&`iY}F+Ytp;Tr1__`twbi@<0N9-mHEu)HrSv#c3K@rEx5aZ#93eq%3>K^oG}nO zDG|RBxp7fTHf;x7Mdl%N2j^weEvJut)sb6s;_;e1fHMt4G0ar-NQ$B!o#S%f)iLcMHc{PfiRYMAFFgBRAC|4h4 zdmN&QfL6j%4SiF=fc2qu!*ZZs5$e_AO|He7L~CdzsV%bqU#IP7HBH!0XgI$ONpVu zQORYJ5X91MR>VMAkr-{l`g|F?qs0QPu`)}bkjAoop$I#3*Fz1`o*{YplkXSZbrmww zhYVISB_5F66?VLAV$N3ojGNs)l6_9pa?Vu$Y_ucb4EOGGoX?_){3fh@ZFjJuURp+o z>Vx2<#|+wjRY|uR0^3Iy#PNX^Fl<>i6((?Sr)EN_N+uHDDvFhsgFnLE?iCd2dvnmh z$-j#cr$)K=O6z~rJ^KK%1Cj_jUA`#b%SzYg9TRWG$eG(9T-k=oeEb0H=m>)=Tfw!e z+nKU4k4VkN$&hVU|B)ET1#a^;WFf#L*PZ@43ssV+M)n;);Z^3ZQI7wZFJj9BIlbfu$#y_Zv5qtN?7Q({ zl4?uCAP>zuHK*iprO-NHd=Fa7{rzRnwfjxLTox!?Oh}spd%JR4^@spX{SKU`%F-ln zw6!?k2Om3Sbg1)DjetjB(N1(Olq-vxwlj)1xDCEEj1wV10~Az0>Ot_aPat0eGz=`E zlmt`EIYjASn*SGMIf{88k(b(3i0~6kv6ip9iaRVO>6pL=t=dmkH<=Qp1yRgMKG37> z6UV41Eqa|YKWa&WVK57fReRouQL%61SV!ohy>MdWgm`+;SvdygSOcZ zyo>_!kVI;2xid9P#~y_g;2q0(@J^Bq5q}Qm|G*m*`K|=Zn^h}qWFHgrIS==@HvJjla?ezsj$i;6b_-(Gz>ttB z=sEcsUBxRB6t%oe@Gnw524UNV>XW&dF?TP=`QM}~{=rD4#(}iY)^h$D#xknHaUB&c zB00`40pZrAHJ48CYjsqe4Ayf*yS-X#2Pq(>3G7Xzo!etZk zFc7g>k84-@VslxY|7|qc-mLVVEzDVbRKY&=mf1Je=;kKMW!*wL$w!s(_9p|*g93{> z-Tq<8mIY|NcPDDVtO6mu;oyCoL%fEd-3ONawZI*HN#0xHkFMLWn|utt@p_r zWph=hM$des{D{1fRs28O@vczBvcaB(DbPwFV&yo%(Z*NyRLmg<Q4MBPGBl|WDg7U|zA?J8rt9{^wrwXJ z+eXKB(y?vZ>Dab9HaoU$8y#CW{k*u}H}0)JRcD_u&fc}EcCA{q=9~vc3g5ob>Rza@ zx&eszf2u>Afng}92g}{3CW5%@6DSyqkeK2lSFP3*^@h^ci2(cH;$16!Ubj%ihL7c* z28Id8&VK;>ILda^89v{@^3wPevoR0<1kx0H=SLwTPBRhO%9kt+7P1f~TAMHT5lwU! zL!|?!5~M<+`H1ed@f$-U7VQcgCKtL@3^hu6$0=VkBZicM3`Gv}A6!@r8qvIzbUOef za<-K1)pysPb_$Db{~$C%!{@GuD4jFl>T$(055Y5qheeoz{^$JfudD6EiBq|`F;;XY z&xAYl992k%?8$aTNPbphXcyo*K?U5DASFVnKYDaa>@I*I_#4r!rWTaC#u|#cZsOD3 zlyVUkV(eiP(2lstRUxDS957p_Mq<0U+ilMa@fj@m0l^lk^#gqHVS|&NM+AIj26xjB zG0+F5IXcUBx$UNbVM1$#$sLF!l?P+aM2&_#w~}g`!p0}@{aho%6&MNg)1oL8_E-q+ z%1r+=xT--~DB%yywSpGx^~AR0pSEQQzPXzHg$|Q0JJj3*|B8QSfRv&TrAvA87y_x& zSD3Yfpr1!%h7o-It7?o-+Yc7`^7;27gqHj+IF(6u=nc->BlGHlfl5?6MP(47Lqhd4 z4tjPB{kO+(S}4bI%q658zxvqW$dVSxVuuxsW?`BTo)J`#yf@T$S#bOrQJk5ypXu0} zDEq%@mMHPfs898SX$c{F6l*9AG%`uG+lpGlApbvI$kzeHJJ6D;B2!47>(4t&r8u#E zH`v{bT;md^&dNfA_@K$l6&%G%C9e$zpBHsRDSB;(wDCt+2eQd&emL6hB~#is#Hd4& zCb*Cu%MU$VrQAq7Rm5FF0!wIm-oP^4Bl$M-DVc+Y~21H3{Vg8O0m;H&ELOOx&x0FYxoupi;OH zckq8scK*J*sysm(mD!Dtm8=#ImAV0JcTz{A)lWPk*$uadJni=FF>*hweX)jvpjnO) z-x{!V!9?(sk*OVBGN3+lfF2{6QAXRcj(Rt}1#-IUZIR(XH5Br+AR11Q2G- z8Sx;v@7NydBaJ*NzCLJt@}HNs^qm_N=xu59H0ReEemTg&E6Te1i+k<=GB7yx9ec0XsY4L@oWW6;!_ z7N5LxK^G&m&3e>s3_kD49u97_8Jmd$sb9u9tHHRDCj|ElzyFrNOKa>cI^2wZ)^C?-Ja+x-ok#I8%x`E;1EiI)dm_Vu2u&+9NlJCq0p{EG;q zGB>|*gapZ!&>>DZU<5(d53vBnrlDJ|vJN`5qFQZD1v-Ug(-K~CbUP1EjVIf?s8H(e zh$+VuBkSB@PMg&xEhGIwGqcV*OqrLU;kJ-QMDV?Q?#@R@9f|!^TT*}1l6DCB{IABq zwAYl>e`HMlV#dliNRki=>}w)DN9Aw*-&JoXS;6mm0%wGxaM75_V+ZV2|JZ9ru@kRN zTZxCoU;Hp(glP&b8^Z`}G++B}<1@nHNkfNilEO;G1w82k(4DF8xS@FS+6;5vNK!3I zlWmB0YxgqG2ARvO@W3_em!_(r7{VmZpjf%SLu5NM5}nd$A7AO^`Iwru`BrOZ1EL@r zglSGyim*UI&L!jdle)OcK>0F#jIqBxL;Md3(U$z8^-z%Wze8Yn;`J@gnlILFrAch( zK~Ua8TpX5d*q?(#d{2EH={Lz%2*jHOJ1-suqS9yhsrnox@9OiaO8FH1Lhfj}ZM)3T zvZ+g0d4l!!vnZS|f9VEvxN>lh_}vn6b-wmV6f`%*ScSXY{zh*tRg=6g9b?XT`S)P~ z-zV|gL{J@PL)Lofyaq{YK6{nyEeZZ_-=_;5I)BB?-tQkW-7LE9bcX`)}NhCVqkIqrotXC zxbDao$|cbco9uC-44f~)^QY(&5<%ZE=F$a}V31W2h=|Cy2i$Fg-7U~T-6XVl0~A!` zevm_JWRY8j`OrYKZvzLgsB5Wr);p)sOaFtm0F_M)W$h&a5ZmJ>dHH?k^&4!+mpY|D zz+Vxu-UF8lePj(*irR(W#M&n#XNu zd19Le)e|<3ujGY$`l9~k?Mvq>k4B-_qUU{512-iAqDAn*vffq2B2yjLyvb#)Ab7X? z8x}l7amz{e35h=vi`A0SlviKkoARHSR`o_}L2I9pEsafRuD0rR2(W>B*XjHB)<^38 ziT1iLj;_nHFUy!`s7<1S`lX?>!a(yOor0j2vB<_~5UzkHJ8XRQv7F$iEJ?kPH7&Br z1o{lpyu2ITw`{}j$2jX44tbG^vq{bN;5Pg~g^8)qy9o#E=LPKvLMEYmdjv?oM|MVx zdCM&RITa5eBiEL9ES zW#)#yE|DiQTWBpHxVBPwi!Q=EBt4iZiX4a%(3%L!AH?gDOK%wlM=V9Kr|8O0w8wKr*C*>24#`6M ze4xBn?=)00(TlHI+ods$me{%Tg~u==$m~zAT`lwlvlkObUnlfmkTW6XEjUA=&c`@PXgH{6xo4L)CxA>Ui}0m*`e_*N9~bdgwu%soDzYnJUT*%7>Uh=-icq z$lBuHCuVlzrrwNLjkk9fKy+);0die}RIAoC&aiM3$T~azHN(QP^le_o5?n3Xff|l2_td@v zvG{qfXu2NUoR^J0yEy&XaGs$@U~PzMhOCz6{$kOus6Y z8Q&d!<^Vg*=0jsCe9<67)4l2wDJl9eGZUo+epmXia_w14PMfbdv^d^FQ ztH-u*Vzz-yg`H?2)@No{Us(r{zM51Bej0w_t;;5)EQ!O_m~%9N@##8U;xYx$=B28A zZ8^~)$jdJ(v^bN~4va_kbxyal&rLid^VfeW}m*1hc zV)N|DpuI>e(Tt?P($bYmxCvf{8)9l*{lS<(T6G$T1JgM0v8^{0o%gN;b{hr9_f$M7AX$JX@6&DKJtZ*cKLM&336e~Sbt7=g<2A<07nk*Czugy-MhUO{5!S^ zp0SFoL+F~^$6Fm8=i=?&TYBM32BLxq8b&OS8S=89%=999ACv?UgiBSEhh{rj5_vQ* ze_FawHK>dglK_@Hy6QlvfMB)5*5PxrnP+su9i}v;dcaK`;)VvsJoq68N49PN@-L@n zY_M`Q=>LkqA(AsUnT4h7?O#C!Mj3P?wcw-Mp^@#P}GEN)3I0trkeGY?;nDb(3u2*#}KQhqcIg3$W}?UAklvBEewhx=laj9d{4OBpZQs$5I)5(@vN z$Pxj`>QRYuSJ&!cMC5~C9fNVDN-~ENkVFwl-rfhA+?g+BUW%w~Aj6*r+zn03mEHI+ z4acc^2l6-m>B(MALSkHfZ4jB+V>nmrMO+-&!)QK`qo_MSYV!AITI#>Rn*|p5Pw6&a z)YmV*&|k0(FdN?K)j(oT&3AUhnd6uh;xMa^^&!s)m%ji?Tmr%4{wXIaz0W1`=Dj6`uM_AeAWVJC!6Ssy3BrMV62!88F}43I6e5q{Urc~g%;b3#vDvakZ{wA}Rd z-N>7L=$j!T{(*pHj;914E(ry-U}Df$447l&(q<0!zXU1jFXMZd8U>;P1DWsET z-VOD?u&Qmlwym&29{R3y z%v~8|Xt+zt(IM+T2YJq0PpvPHKgr=~@F-nCepS(8y;1G}<{13WBItBL8)rjHDGeb$ zMa+ijn`)8Wt(5eEZc%-m#0J<^h@Yl~(K{-FJTlaqx_dX(*W%|S1J47pGjzuz4|mz? zQ!X6ywPGo5mFvpw8vUEbs(NZxo#^rx0p4eed-)CF)A}Ot#B2c#^e~&XeH`l-*HrD1 zuKFOn$kZ_GcU`Z(Z}qun7nquz=U_-feE0Uh^mDDIW68e_=KdZG{eOi1SXbeVGh-MH#f&M*;=!Q z;a!(%xq=A4x5tQ!tUjdoY;azGQn`2)Ffj>EN~$;qG!+5fz0aL1e&7; z!X(Gm+y;cYNZyP<`-H9l?vgTUZ0p5RUq!JU#_BjjDGTEJPhTcZLlbWhh<%3Ojc0`j z!nIv&O|Pt%Z>p*%q|kW{l%M=?Nqw4~$@6ON#!IT- z1A(O&4X0ZOuIEAFcqE2j??Po*eLhP$RS|g^>^Nx&5B2LYrtF;*6 zO_1d`zBoLQp2gz%9SEJ?xo6pK(&k3|9c939)`+;WqOmg}8&T_{J`t6lj&kTmk}mDR z-vjxB|KvFvdL>q)ISma^a_CbLvs2OO#TtC%!4IZJ&?GBI=*MnFM-EhRUWX$m&CvRc z>QUWOpsk+L%O^`fPRUFPDPwR4*5^4|4ijbuY0 zr0lbG8U%WnV@{ZlRN**FH*<~_owQXrJ~s2lf9FLK)+#s}wSB{_@fMEn z<=I!waVdh|l>)EnbHE|*O-T52`8)xiuL-%?`UHZ6ezR5KHCvpIFZ*Z94Y!0&5G1J6 zH_vyH3P(XM|L#AEo#kL(AoDQ~oq~#`hOcp2qwMEIW(KRl?W)#0jF@lmK?aJIRYRqy zS>i=6{l<{Ef+Dn{QHh?-t}l>enQ*+960J8LXH_>)!*Rj2-~BLzzw5W18x3QD1IOj zM?)#fpQR3xrRo2;x8f#`oRc0)CWbU?icAmym@j;GTOv3F4(78S3h<2IrW!>;zRErg zvEpY+7E+~K#AO)=X?cd7;mu0pud@J|_*QNUrX=lVpWg=w@8Sg?8aRy{C@2E(Wqe@Y zz42_6*QD}h_g7B6>n#2I3aH#a?T*dM$X5bjP_ZK!cqHz0)IYD!^Lp`h4mL1Ma~%-0 z7IYEod%j7P1+iVq-|O=7;g(B5zQxgk)N^r>eF+rYJM`huv51-_L**T!?`&@Sg^4tp zYpzw3+L^7-?d(>*piUC@NV|2MN9@SMQOxfj{R=L8|6P4G60+oD>iA$`A&l)~NYel* z+`ckq>bA-AX_a(`1JD2WpUK~d39@xknXi`tE{Wx)8b2tO`R@1;of)iw1lk>4Ia-m( z-N&qPodaLKix&NqFkUr`+6N6KRFR&B_Np>0glLHRv#&kEg?=H*Z0DJ#mBqim)s(1dqz&aeKkn7bUAQR)< zWFY*HLr-j~QghAYHV87x#DO{IYf-!AY2>Zrr#ZsetQ(0T!2_ULZuh})y&A1u6$KF8 z)UlK;$`&;n{BS4KJX#8ctYTuz(Bv-zpOpB04 zfG3OXZDE+08DjXVcbj)}uXzX={MA2wNJr4{h1EY7Cz_bWzK8EsUjN`+T zs{!H`NwjjwEr^hc5xdLQYG{ms4gGF26)ci?wVaK7kX=OUd+T$qC3+AED^^uO;g+;tlHmt+efE8+z-k@C|m zCJvk;+xVc>12$~N2WaJqX_Zwgevl;rAr@Hufr0?O6g7p=oVv_EB7_bP_$y9*#unhY z>sBUJ)pSnPfPl>Cx@}bU=<|Ml3naTb z=^0vf{>o#c<(RywHDW}wXuqR7Tlt3C`5b`cWv2DmqyrD*rUMy3WVm*G15WeM9}hyJxgRjX!w(s?mwv z|C?f4p!iDo=$-h1MsdEnpv*8^Dx|-mR;oOc<1jwUEy&U%k)j66sRt~zmZML6S2_(W z-TnExoE1G&)@`m=Ln7=VkLRn@7ba_v#|-@){&eu6t>)W{A)x`BE&fsa8I+8YUyCa- zj|VZaLAoTAYWuTh64I_F)t5GQAd;-Yu*2XdDCd{wrRp3JPRH3!>$WiEJV#sqGO-0* z!CWo@IuUH9xXRCqIW!?K$7ExrF!iSZ6WLGbm3*_?Po&Tk=xZ$i^y?scq2B2m%a9k(oph?#M#IsFovo8z^9!nfipOe zPswjJuRnAJ??$Ed7BIohxnYv211_6xQuzJ;$@q8Wi(3|M1@~tkTvZsG5*x?5I)>EW6JtC@#{grd*kiU{%(lYxte5Ko*u%OVFV7iKz%26WJH>Z6m z-}A1cJyX3nqCevARN$1jsO7Y2H^?Xo8QD&MnJu;lU!?w9hdWnnrInP*#Uhm$?PqDH zMb?m%$7nnQPmvDBt;dvTO*f`-B8FT)=^WK~Z-Q}f@$!7xcTh%UGjlBCjS2*vzmQ{r z!{9Z~4p?7Pi##k`p4C!+k3Sd@RpFV7qOn>pVGV62-B|w-@%YNEhh~C-46Lg_o-U!? zpgSJ6)JHQr-O=rS^yH1J%JW98mQB(lu-B0F3)h17!Uu5x;9YHkGCY{O&7hl_H|hna zIgN}L>8#}cL`oE#DdrOgbngVV{tC0U6e?E@C>Vd$Q6%ecCn%Y)Ub&gKKNzrW`$91HSaAhi6%e(!w6FhT4Pd2aQ|Nkl@tY)D}S5b6A9RWnLnj`mH7Ri%BmhRI>4=#9Y&eA|Kpj0eRIau6PbDr; z_GZHB4iXfALE`i&=q4E1LLIys)|XzH(;?hc z`#y;dG$ru&X#tf20HAPvm*cM{P@Y9>bh$lnwYYkk#FMQEQOT^W+uOQAA^T1}5Aes4 zJ{av5DEUu}Mi6~yf6YD@O3|q`w$VZY`N8jb!z0X;IcCZio{Q|~WIt#)Kz!7RG^<%T zu^ZyQihm3~!?IuKXX-;3BXMn6f75k_svD3YH}(Z`+2~uxEX4}*8hz835>%RjG2LBd zS5tN9K#0+c)z&oj&C|~PERKmJ^!u6uWbhqph-`>73#x| z^&|JeNrA&L87nxv>@Z(4!UB7|rezN~R?5Bd04vHub^>?HE;lmB?+G>^H22*}!y0(B zI~HxK&pI$i9wyWX1EcA$+MK?8+^gelie}Y0pcjrg~syZ~JhmXdY zuMt?90JQqvj7UZg*yZUFZ8gPY&OECw8U|qg?G|t&0Ey>~@an5sR=U9FYds~Md`u0K zGI_K!xpfITpl5H!tV0fJr{e*97gXQbNihci(yMvU6DDtWSH>D;--E_ptExUc1_zhN z_HZ%BmqUL-5zU0R>Ko;K#N(~I^9W~7odid-kM`XM#BR&w?#lKdH0R%$2(|p^o8kvT z#VrS*kNdhepbo1DywRMb5}}siCjd91D2 zfDP6dU(iwUxSjT8q?HAKwf#xWuz z*l#0|JGJM~t6hO*A~@UqWmgb*nJ1H*Tbbl~5usV->TNG2z^06ZBY71F8hXeZ%dv1G z*sC=s++u&MX4u3?N+qw?CisiZ#zq9hTh^iCgmXHuBcC%EUm_!RV@xy!fg`J9L2Rl& zil5)n!Cp>Ox}1*uq4!QXgp8F;wDGAN$N)ZSWuLh^l-OhgxpriV9o1a9N(y;r+p24Pcj!?9V`+`1>BsrysOANT=_zFr@VAsRoFshUIF-( znGVUk1@NT!m)I-}aWH|NllX*+Mrh(Zq8csdbQ$S8nRD6|2urp1aV)nK6VzeQ!Ip~W zYO18$7%z&-Nm_o4P9&#BHgP7=3hi8*zU=J7k%U7ePbx5+!%R((tPa?@XgjwUYFR@N z8gc&OlA;t(R45u(X}L;ZW6(N=U-F)Y2NKM{c4DC6T#L6ka~!wCLi|SEsb4(?YB#Ct z{;D9FcYqmg7aWW^V)HP0*i8VoZ2COJ)Gs0N)c1%Ef(Nf-J6c^+j$cy zw@)1vO~Tr=k2DNLdXhIjdv29t8Tbf8pmg+IXoL8rvupxv4%Xd}ZjdwyLUF5c^{Ak} z-)imNblN})<-!t;OFkFUO8iKVHLws$A4lORs2&S&&Y4XI6W3kR-hM8RZ^gYAKPSfi z^<|8IN+jDc^gxY%B8UJ`wYbgf6Fgvuof~snh3Mtao54P)L@hdG4vJ76P~G8FJR0!-S|cnn4Y)LVX7{&*s&9W2nCSL&eUD|NU>$2RJRG$*v*?Abavz5u3$#X zIqKb5O@m?s+la^C_ozc{;wwRZz*hSXW=j6dcm2(Alle`{l=63qSKGBSd7icy;?|_v zmmGeHz3ASq$BVWpKXR0yC`ZWvlF+hdDf;joX~aoax|^B(-}vzZc``{@aJFoAfNu~K z_Sy0M9QS~wu4Dp5e$h;XRohHCKt0Uk_#X~aPz9E${LEsN9rV;Oxt{#tX3_nx?qer= z=F&jw$<$Q%^DYVP&}HC3E6wZAyD^t9GJ>(jkA%h~RFEk`krxSK-@+5Ut537!O4Oxc zlD?$ZwzGD3+T)xuB^dZPA3^VrXotbtU8V)30SpofuCZ=fUQ&tqlJFh|)$VGH5>d7( z1%g4)3sz1|GY_w|L2q~{iB;KtqS1%mAbhcF0uueagY^zzumEh>b5qowBV3BhhZ;K4SlJn>*d3U8Rce^Egg6 z-Z&@=;6P$0{KqJ)Y%U`3%Hs)fWrQGx=weq0b4Vx(IDum6?J1mRL#f3dz>?^p=*}6E zH!xRIL8JoPKmiq`%I^hK%4We2ov?JzP(K!sJ4GU0THw$NxIe#FhMr%%kS{#*IG%YM zc;0-AaH1iSyJw6XJF#2%0qqqJpK_DI$Y=>dh(Ae8Y-DnN{={8=2c`QsXgCu*P(iVL z*?Uo)-m(+p(6nrgJMK~%t*Hgk3nbJZIV=9Sd$Fzs7n9f`aS84bO9nx*RkV#-C67N1 z8rAH&63=cS)`W+2?0u#E**S4LidjWhHv&L5O-5qivu!Y8>NpQD(q!(Urd%byNa zMK@H|5^p6^Od!FH&o0UE?kVh%11@jE_Kb;ofO~BivMW3~>22fh)9+RL*FONj{#^HL zXg7o(iYEp00DzQriELLoEUdy+m-cpTGdDg5*73s9Pz>!G5^ykP!=bX!$koBR074lj zepo~-6x4%gz(;&g8YZh!wNI@lyZ56VjgwX-3$A5cZ>D}HQvl6Py|Brzpk2SA980WJ zK597=-lkjxQS>MzvTj_Hp+{A2N~%bA(E|ff$@FFOr?j+LBD+K~KI~_ePRDE4;ninO z`R&{(1kB+qd-dAy>1~1yOOn#%PBYML=lNe$iXtMdG8#^V!9jfV;#RJ??ife$?;%b! zzOkL~;m{dm#^MGJ zOn7K$EyD4w7jqyAu)h0m!<|Y`N^LLNO2OoXVO9c@9%v&2BhU$}*NXU7-HXFox&RSx zsk~me0Qa2EMFFPE&P%K9V|~Cg!zwuoC?{Oto*!6}r!>&}Oqqg%bj-zymjE82LaaZA zZ#d2~w{~YjuKzA=)J}$A82J3%woNPqF}J^{6bMdMA<$!!sWG-rY07pO;(qWBOME3S z_>-m#w)5?ZbgIzgb#mH1Tb@;iBw|D2*FU^8xMb^JN7;+Zg~_T0TGlc(jn8=@{!%sT zp*7ruLCJES>IUObpBMPYOiBSj!DjGW@wQcjN_DVm&xx7(PHg;lIgr}2$7hAwaT0%2 zo`qFKU*~}4$%n=4O=L!ZB%LBYOP%oHH4Oa>PgAcJW=NIN6L!9{)j6>X@wkF8&8i$Y z<`wDyld8mC#{f?GRBM#>^Biw#`q{tUI2yd+Go`2io|_RyoYW|j@3~4 zCWsfAk^3Y3?Fs(`NgwMR?UdEOBg4m3zH8T!5e;Fbu#Iw?#NDGVqVr;H3 zYwKxhdB7xo0`Iy*q^1wvYE~%w+3a+n-O|m;_4P6QBRe2~-_T7wHazhh5-zKq|)e40n`;v3BR6(Pjdp@0k0ROh*2TIlbQjEifMOf zn65+|dp|mBNA8QU5c8S-9&a@?2J=aTYqpaDG&YcWUt1hXf{G>g7(cajx4@h{UY3(z zu5VT4R{z7Lnl?&V?P~Utya*xdD5Hhnmz|(&ifzi@6iGGIE+CLof2TJK%08X~Pfh27 z*%C0U=ELZ+!a~wNj}I8SeQl$Vwqov6&z&6^tiRycVXfi;qBzy*aU#c6b$$}w@Xr`^N#jK>*_+U64Di9A z27!CdoEAWVF7A|T7~xit_GJgO3fX(#C*3@?A#WoMeS|T)Ic~{IlF7h)a#p_qmpYfI z>K6n6)7G0k0O^$3OLZTYo1baf;o)rk*gKH156-oEWOjBQ$hFE~^f+ccS%atT1uapM z{A958GeqdxHN3aU)sQ+>joo4gt)9Ai@Pt8_nq7gYIE zDelm`Saw@gT3mh+T#uVUBM`e*J( zTdCyAlg?{=V&XG2!nRhO3C9+b;@aEulZx&I7k^Wq2gL;X>oUa|DBFen5n}vo8`iymkFm8z8?T@C2u@VsQb%o zMI>k;+0)@;ns)V#K|JQ-gp>{J!-@Q*Lfw*B8S)D%WkIIqwdvJGq`lpW`~;7BqscyR zD=vQfIW>|B=$zPpl*{UBzbp9ZU+)YYA4fLO24c$;0)p%ADB#Eata(s0>mpo)fteC| zZNC2^xn+@O@$&3jOR=yaKP-SSQG}M;-62i_U?w3dEs^xj$3skc1n(OXgKVmoWnU@P ztjnXkmpwlW->Ip@L%dPeM;+bA^7=hIFcI9bm(l6}OmI?Yh_APgfK*q30|DNH5#0%-QJhxY$`p9cV`eFDBYC*(i^xKMh23w)FR zibVx^={UT07-eJ4gOuB(!PXiR(Ipzu7t)O+O_A)hm9F`R2&RK&ksp$Tq=PUHF(D^(RX?TsNoCwC_LHR+ zp!H6hM&aCUSfF3KIXYw>S0(hBd^R_?iy+gy;(hMR9D?jIStmRd?(&#;FNN0_fLCVv z)Yc-7hkLWcMP&kmYYY)QTN~JoNT>pw==rwZ-3d7_bV|xW4Yy@E&#anuTs>H$Ni1TM zkW0R4cR^_`8b(nS^NPw9j7=9VO@tS_zxyIH3mkp^u8Ff9+E->1B6m45**T&w9u0pb zHvQ9T_r^Pb0AMcl4A7p`SDn$svI5)4Z{ zlO}))iOpCqzY~a2YP`lzNdKk=$~A*gp<=}k0KiSV*R=Ku;M5Cosfjeb>O||BQqE|* zI3>_m99EgoA+&xsXMvUE7Z&hAkzO|36~oQtu}Bbj?bi}OBUKSI+k4$tOvRLL@OqB; z{hOXDMB@iIDvIYUCCm&`{k(B%#j?2M^onoPby+351ByrM%W|+9YKlrKOku%A|8RW#N zah?$hSyo)ZNx`A8g-Ys)g7P!-Ho| zmx>fuvc{%AJQI>A5)-=mmo#Y^c{LC5cP$A@KGM&ofnr^ZmKv!mb@s(TvW2A}h%f7^ zME`k-%CGXrCp$i#hD;A(7x;=M*yrm8MO;u%ARMFim(%s50C1G#GPEa0ESvap+*3sz zFcCANzQt0D7Sny&36_U%@qNr?s&}tl6m8ClA&USlvvWwYx0{FrcqB|!jZ;F)=<4{? z`ns@UApX2sM#ZXV0ZbmFmCiKElYJ%vubIo`lAL*rkl1#1wh@g*_Nz9poQ zdKb{_g>i`e6hcQ<*fLcm>v6wRVXmB=XgpL>*oz1K)@HwZGCWz@Fy-Ap`FT#ueFB9hU_sGVS0n{!C|NW|iAa49)TY+Xe7Kh%j>zVjM4 zM925LPcz-DJwDw5v>RlA*xJwqiKP+TLU2G;Db42=QIbQvk)Oq z87LRsW3lK9ccY6=(;a)}@|{d$JlGSaKgiuJvo*mkp38-NYw;D4yOO9+Pz7=7a<<=b z;CjmW=1vrVNqyB^O~^|(6Gdq|2=f3A3nT0aEn0YGv5p4_qY7sH>v^%c^!v3A3@~|Y zPkwC(gjk%Aat-918TASx&%U)bzc|=61KIKm=$v>xhqqPVr;IwB8=BjIESV zwk=f@eob2|$ENy43*1yoBGFMnJJ%{mXsLsk@h?ptT|tYpO5@S>^&lG!j3!~z;WenARqhW0Lh_n z;ZG$zgX(j*gqQR%maICy6+B4>S&(G`csAdb{P>Zp>j*b!IPaTXpyvT$^6Es4d1FRW zMUlg4_rq}nJ>PX^nXysv!8Rx^$r)HdThZFOSU2gDMj|5~D&;aTE>Q8N6MTFvo!hJF zYfb`4(UvN<}qG+syxfyAU!o%gjuYQYHgc?P2w5uBq^-Y`xO zzui^GV3yA6ODZt36uYtDJ&cpS^Md&^hWy+tYh3J$9jG- znHu?r;2+9Vajo|1f@==}MFA7Y4X*`sFO{{V!u5X4A&LV{KS4CbLZ>3+hZH?|BbN<* zm~}5#G}Rz578s=vGPL-{A2P#UfI20xCcs6Y#XxU#8n`$ZVJmJwc!m~feZC=(wU>u% zJQLRDr3X$qP!nf{%1*(gsI>BCot2FQE9Ja_3IXdMA!Tn}xk#<3XS&1PkpS^T#tMvV zetRseMGx}+4;NVU?fiMxl)Y0UPGsALQFv5IHaRNLqe%n>Lk0jvyvFf(f8z@U0E7U7 z)cZsL00f#JqIxRR_M(`f2FcWW{v}^@B-g{q_A0>Ehh#+;r-EPL!AwjoIyqh~KsZ$2 zQaOfW0-sxuqCZMA#6fV6s-Tx0tHuV}iVDT|L=|3C{M19MqIb?A|6uF+er|8aBD#Bg zvnRwWfsM>6jhz);e=$d$R;p`K&M+4&{rN&{tHgvW3BoKKBUjXN$UD2s2)Jp; zln84~?wZHqBp&b)o`RXVJHK0-VXfWFa&mpAYlu^A#zaTRrQDMEM=?n2T%@3nveD3O;z4u}1;enue%GZY$N+%FN=g0Qh(OrO(GW4x|(c zVZP+iHiCLV8-s?OcX|=}jpIw6)Y=#|TEWqLza22BS2Y5(8(oQ&~49LG{Hup@wN{(e2koUkA!J!8pZ+%R-|P1S%ZQ*cFLVhXtkHxbAiU2 z+6b^2db7AG?1%4*sEw(79VT0>bX>qcIA+aiSce_?tnYLdyAbbSkT|DG1r09a85|Bx z#@&@yV%Vks$eimh#PAQriVK&3h}ogt0f2vt+~$VY7EMlow-xszgi6YNz4gjTgxTnv zRv#quOjm9k05e;CKA%-$?j4Eo`zVSrk+m{F$#26NEhhhPWW{&F+ixW>vuhq8K0Y=H zil`tAzb<_wy@RxNNT^0~E;g=X>{fvo2#Ne?=&h@GBisFwq;hiO>%PwP0EeejR=lp( zB6^F+UB`Y&F33c&P!p2GcM5iy^8TGDfw%}%jor&qyrZYSO~Bxh#5=j$Y5p86i+9T} z^o&tj&-3x+HDVPtZWlLwA0w;Mz2#YxJ?&*ZUGE$kc1$-XrS(7r%IDKXK0`$n*0ni- zY$i^%PfR-Sa zy)QD_f@HX64}xio)}U;v4HD3@78zpsn}di^6|H#PQVOCubnVq}aW~0UjE*iKDX73L z7Rp>~v(hfuk+#}+NPa$&;7k^I`b>~3(L?I*>fZj&VT%72z--5P#zD|Z7KIVhWu`Z= zS7@P^LPUfy&}Hmt6hWrj2)(=EdA21YskEN_ywiLlwy|PQjP=Na+IaJ^e2U}S3m%I^ z`LkurrWW083;d*k|6=ECITOa++^-A!T4W;MuHCzy%ny6wAWYFSb1MG~x8-otzdLnTvm2@Pf8ESqBOQ zH)@%R;`D{E%qk(R5#OS)wyj~Jp6k$MAx}_BU%Y4CL&E78cR-yuQ);qRv|Bu#502^-(7G(|8!ZKa z_=*)HSV*GK`8ag)zHS%p; z%5mUBXL!z#y&?(Nvx^5jYhd4|aSmjhWxpRwSz$D#YD7HxpnenM2IG~Hd#6kI53w3b zL`ca`86zxr91>SP2N!nz*`>h5IT!va1W2fjmgJw1bVU~T@=HlIvmdYj(SUm+K=1Lfk?^)6Q^XZ4uCMk@>>@an_6zE! zZylU-1AZ1ihnSYs0v^$*#m^nf!jBsFIinzi>J#^R(vHtkA7{!x`q6{4dZH8VJd+m` zYb%S1;RLU(CrLkfy|!iX87bT1L|MWPaI@0x%rnP#Ucj#E*&F_XAkS-%cOEq0wz8@5 zGFL)7TFzw?V$P}(U|+b=F!loRP8)JRJaro0l9iB-6CSM5S9!vy9NUiYb}hmFQ2*L- zJN#*Zw=u+s-=*&y^UvhAKIO?=JaH7(CfNo&WbEWF&%)I8Jl4wn__|S^EaGPHH^pKk zeZiYiwKt(ffy3`CaSuv7euJc=j0vrD2Q5bi85dMx;JZp*{_EdakX6)IWz~SXNI1Co zFf1?3;c=evO4Dd?o#R?^yGY{ST06s4Hc-cQ2^|h)jiR@lH#F~yEehp-aMN&A*FKX( z?Pj}sG=2=rG8mFlX(JNBhJ!!XM%uBfLsrf<`8HVHsk^tUO`HW@c>|u_uC%pj^~29B znkO`J0pC6|oFJ6K{uwlun=wiNOZ;}&vE^IN1EJf{lSG8%G)Wn2&SGci6i;ChHu{0k z)wRqFf#rj1v@*G#OzCX&9uX&#&Y%AS*gz-03%k5(EkL8kmNmoht96^eZvA6u-5yY-LiJPy*f>o7uoyk3l4x8PO7JNn3;JG z`sVvi&PjE-gJaj0W&^u@UKslP-jMkdYXRWrN7y|XdcY>(xeacy^T^nMGSm>*UJ1;6 z@3Wccy~I5hNtiqO-g}41|Kx4p#JEoDM2ah!Av*W89_QEi4An3#sNzD)~q3Rxc zP``wr5akG~5xq^(e;v}uf)AUnisM{Pc70-X@B2E|)3 zAvdvKuU{@Ce1>t-__g1QWcku8TO@|$zGG1KNaEf12V7x9M7C|PSFwfBY5su&)R08))aliw@Vfk6C`FV)e8Ec->B8-gVk6#hM_xOgo84G8|aGs51MwvCyDj0mZy{iQu>_WwWv8Q+>lE7Sd-ly{4 z2v$22Hsn&}A_wUszrc@WgI9oEUin={ zK!X#_hVks9QJm}X4#{|trX%6)@}@>=;T4x0RALJy8o`eb+W4C*LMk0fmu->}8q!@S za`=t@dQO3xX-^b@FJ3)@wyh77&z8NBTbX(T%To3z7TR=S1XR+31h1kYRZE!fYJotE zN)Pu27bmDz=G_A{a517f0!1S)WV6z2RGac_KmP>gzh&P%_239H7wsC3fb_FX5%R;W zbfHWobkAwHj{VW!l{41ZQ%y%ri(V=Iw4SL7c*^lwRBI(?^a5wH~^Q$F(p2wbsW+jMy;Ja(Q zApIN%da1*}u|`8Up$_66#Lb+{1c)%#LO9E_fCR-d8jk?V&8r+Wc?2N~H}+{@IHZVQ z6j8O{kghZRNc>q_v zF3u}&0BJ^;-+|OYarC)dMq*fE+6Rz>a<$bI&Q%!6Rk&&kLiCZ1Yj&w0b5sq1 z6Jw$`Sx5qrpQm|4}`jc*~yNffIP3S{85#R0000PbLa#hI{Cl=0000W4Hj5f zfLvcN?*TtYr9iU%ZD5UNjAbtF>c<`ntEy~piaf^DVQE%F8|y0KI5iWEB9m7+qdf!8 zlz=;S*(`0Sf`4JBSdgJD&l74WDkLSP4Pqzu$Y(fqZF-JT)uo0bc*Q+^1N~JM&NxVs zJM+l#X*E?~>?=W}X}`IM8~L}%yYi_TjGF8rMH5(qAbD?SH!NfPqxqhxE|qL~2peO~ zU+8F75N+gw6ZZKwbyo$yuxpw&Z6V?(#G$uN%#ekX5WJ_Ysf5Y*_UnKY3adM9>Rdpi0Uvy!poE!;+=7C0`Q!0Dexka&KWOSanf!K| zT7ujnzOBm&Q_~5k%KH1}qsDWcH3JDzyln1rs7K?JR|2Am7{IyC5FRi|s-`|&1bHlQ z&k`Xjf?hhMm273~rTiuxsUQ5uS*}^mW3cdyAi0res@ySW2)l5YA*U%zE>8L3P##nS zl%G`B{SZJtZ_p0vZ-Duj7M?BhvYa|`3CTv$sOS5~`@BI~8WN6yTF#lIpueWtVv2ni z8{rX2p_v*W(9Gu+bA&|;s4)ieVaBQwo3QRJEvN6-l}l6U55{a~@JS)8pYY^e$mvr< ze+9+PTiYd~5Z>$sOo9;X9K921d9DJdjML@-D}{G)J!!B%qK8sI>rigt;P(lP9AcTo z9(D5wASz&2Z1yrg_HcF;+z=QL0d-Vz*tcF`qHJOstls*(2awF`%Qzj9m$|K zo{G`)f-aWwi!g4t{uY%NP?44{r9;A3FJ`?DMmCO!_Zzm2|k<%l`%qhGZC${mUaw)Pq3uW-nTO*b$ACq_};qY1!-`%y!H;K1T{) z_zWZ2PB2}$aEg3RXYZ-rn(LfhXa~%LV?_@Y{7+^dCBMg!WM+ei!v5uxw@Ghb_2|y9 z)F6HvRL@4>Y@;J`ggTb_q-;2>&bJRmMHD{oAo}}*yVaO9L>be^iNttI<e2>G-#N3R?TvWngU*nG&G9QB4p}MROr5>#3crfJtu%>FD z>$HK%s8HY7w@m9SJ!;%}xo1{3;)nnMxnhj~0qXIM@MO>c00005v>7|W4ov_eXpNbW6adR%cP#W7 ziPROvkHna%9VbjavJ21}zEY>#5l*dV%0h{}zC#p__>xlhT!}nz4Y(jB|6yD9djy)Y z2#rBD(34j*P%ng-d9POu3bPIDv^x1ZV{jO5^4U=@`_3`vnSg z&q-(jB$xw*7we?16s_r%78o*#IHQWn@t~pJfH!{>OaV9Vo_g>E84LD~qsgDgX{D$w!Vegrl%z>H;h9s@ z;E%pc_}Q7a9LQ5B(GbI7VE9zBPOwzQ`?R#0{24wV{G*_g91k4;4$WX+gH!a{>QH{E zEDh1(pw`NqY8m@a2tP7>J)v<5Zl7CM)m&8D!AKgo62`k#^I#J92j!>T!X=!*W>D2y zz1U{gfIdoJIiD4Pj&d^W63rG8`CPzd{ge{&{whUry5emeL6Ni*#|z0w=|6QM=l)o1 z$CN(W?D5p!Y;&mI1c9apNSE-HlcdrXQP)E5C{bZD<1>_2l3k~y&<|* zF(-fk7k~_a0000BMUuw&>CqOjg4jQCu&Sq~5km4yuv0e+V=}9@piNkXE3QvaE4hnG z=jK3{`*-K6#g5{A@Foz8wD{@TY55<>I!jiIh}6(jxfnj15TW4;NEhf*(OQ3ViPsy}3vOp7I?Atd?MZVEltb(|jdPpPduDphgsn$lJ42WCemJYAfeZU)SglW8uy1e=3b;1D%gWZm*|i zO6h9a1yV!@(8Chun5eT^BY(}Sf{n_(PisG@Xf-Ih#xHf%xBl1-E4>1p<90OV59{nh zghg|SY2-eAW#~cZ7wY6MoVWcz$6V{6xZM;B0QsN*0004G`@rEbA3=)M2pZVWQl02t z^W|;TETzip&-FGKeoc{ebwaF|r>ikgd)c~#g=~ciM*n}<+bPaL1=TLdwmz5ZkkKTk z&sE%m3Fl&$n!_}!%X(3rCg3#Fomxd`lrU*m0hWWUE7HHn$Z`6p8!xj?KD<{|qs0>6 zq@D+de)D3t0zSYlU*@XiZKFGkgDRvT%`{@CilS0|j`OCQyiY8tQop{VqY3#`;D^(2 zTwhLR2?2s2c3Sg>;Y>( zhsqL8OVY)}r_awjdaa5crR_cs?SJz^FynTa{xZ;OJ_)l*^Ku*XLvc@j86 zS@xKvPad3^@e1=Z&^6D(44&Zk2PbEtBzqYVf%y^>??2o`2_Iq^pGeo=fk?sl^Vc7Hf+0*BEat$F=Id{Ts*-=%v=oIbHcVda zcvjYASfZtfTgAH_oN00Zm!mt`>sVdEzM9z%7@5k^(%GNxHs@zkx$aBGb9tHh$ha&- zwBa*s@H^K$usb$r(k@0XG_LE0LmOu@n7Zx6 z000)S9NQ61FvAX#;Y;LV58wY6Xg|i2m5}}MwM!5mE${Tr2mnV*mPR<1r-?&2vE=ar zwjffUY}FSg7M#4p_~nKA>XM1Fn0nSq9DjO6aQt0N1+DN``BzzBnY1e>E)^hm{N ze$+e^wTEPa-c`k+4EoK=5Xsx`9LFLiv8khTt)i0H@Tq_I%16nwo+i2xy;xz+>f!7K zv}nA#yOTtj?N<~uj7rKFcGLh(Af^qC%r&(z7!xy>tR=)22XeX46}>H!(#lxLM>_k+ zj(ClpV(76@n`?gG8AYf*5Tu#GI#btg{q&8IUuvE4SzD64{rj81zrtVD>C9nC!_C@~ z*roA|o?l4?4o#-hQ+?%>{zFM3f?Bx4h?F6=ZY5L4Q?28j?ez?b`H9O+qeivA!dd=A z*(x~zz=7~fhr-R2l-+STb!6Vmtg+UcUn=X-Gf9N)#l+xEqLS9Io33BL)<3Qjin$rD z_}5j4Lvr;wFIT@DO+Wwu01(Uo00001pnwdjRw7;nPLqV%0_c9gg}rRKz_<37gA)Vm?XNEImClee%-`4pTv0|;g>d3DdZFo(!GHho_y5v zl^JJ@W=uGyYm=nMSqWx=o2B9fBrBFzVgdRn+AYoX+-s(sB7qx~cFEFsq>*kB&??sGC%(HytxP-gV(KAtL(Tu zQnDL1fY}Ya)~iJl@m1f6BQf#(u`DZ<3Vf%Bb1BVWrcN*M&>}zEVC-obg1d z&e6prNcT=|oyELFzYY%U^wa+K@$VSQN(+ce9J=6{Jh z9C{gp)`}+W7&7xK=$dRMgR|gb{KzlHLbQv~REyVP?KruF0_qp_Aoo6jx_%}5KUPkG z#ely3PKDS(1f*hHyIHJX;Vl6NeS5}4jLGWOT7996-Ny8`_8smHot^uw=Ia`eRL34* z1ZupmAn0N}ATTIL5j9&@rFR6=A5Jkstj0(`Of_)SCt*-XkxGKt6KeHh9T2}$!z zk>{5T0cKv+pd%}>g9x~4C`0&pkrjDTEX#_4ftfDki*>+dCExX%QzMjledw1kA9?w? zJb6lzUeiU{=%-&~#=RYg7|hIh9tY1IOx*dV?*`~bOCKDXsES;wfWygppJukK=R{A%>h6J}AtP`ktu-96lqA%iEiUjOx*m{PHH8BG z_reaNIjeTk0B3_Hu^f=~C}P-U&xy{R&fszmxOUe#>{qqvn5%0@zaMQ!spVZC!9fGp zI-&FcK`nSsBbdvc+|ovIWgzNR!~(%i05nU?IT=<-C4r=!fHPVWUSufZlb#`RTqu4cGd zXP!uKRw?#o59rvwb}l5KOyfK;12vieMcc<;T_S57_Vx?`kX7WPNHVJjp~5;{eJs2# z3QoD7YepU7gNgV75E(u#w|n1lzkwnIR)MeA32Z-?YduKX{y#Af$F42SK&z{-T#w%7 zo+}|I$tsSh+d%-LRGt6uSGdUvMkiTY)XfJ|o*h|2l&O3{`hW16lc2OI2&{6g{ei-s zgCVY8qeY`e`xbu#e`3Dv1EO9~9#w-gUUEn|kKxhG#9@z9KtN2I2CF7{{!WF08nwLs z9i{nL;`d07*H$IL6uDK>%+22&#Rf{anF6?)CJ9SO z%M6GByMd_&1D=kKHeV1L`n0pCx$T5%9;WWP*9_R8IE#jyQErB+#$5dX4*H?f$%(-O z0|K+~gRq0t+4#MOYW$dDFupOXV|u|xJPRBr1_op3HCS10-O(qqYSVtaJUz)X)=e_M zsL}B!GutOrK|}o_9ggU4o8YO!v@U|h+7B&T1$F=arNq~Ie`26r+^dqyFm~l%1n-?S z?Ni}jmzpkTL6Iu$NRcT-g$}uETKuU$B#T&-cw}KJwmYz-*>8SbDjmI ztnR^9vapOCYC11>Rouj7y!8820BnbA)qKS@oRe)S@YHq&PRUdiE7Fi^Pkp_ii@lhK zrrf_5C2Dr;!;>r|+chWmdiNUcU$1mpL#iwv&&;XtS>NT_g00000 z007}?0y(A#tPfdR&E32uJXv&{bx(g9{f*&dvH) zpd2B7y1!G!#n+&M`0_)L?@?3c09$8sc*)T>@eNi_(MpqOzIEVA(ltC3iiM=Lk-mS1 zGD^=aMy8P<8PXiq+2Uk0xf4z!L5eoQYza+;T7)o#J! zZfg80diV>?3H?+^PjWF>WD=DkibMk>-(eNec<{V=y0`|&@Vz%wnMDAow>&&N4vk`j zR;Gji0COd#u@+&aPK0@ihWRa>h_q@y+hDrcYZAo0cmh(Qle~kngt8rN2pDc0-9M*4 z`2nxcAU+kAWQBsH0>{DjM#QKGm1RbHdYK3S`1(Cnz%^* zxNAqy<(G}+cBi%g?bf`D8zWP_xE@-7h^@u{QJ|uy>bgSF$)tN%>m$~E)nKpyhS`?X z02_XJt^fc400G7RlvQj5>gFi2CD0Hp$S*U1b3CnvCQy1gwod61o7KDNCUU*O=iOTe z?F9Sem69Ed#8Je-I)xP;E=O0=X_w#N>Ur`cAx?_WNZG}TWUDrj)?y| z25gzw7FKyLQ!Ld|$%gmWYUZ^gi&fx6oeR~1@(i=N)uE&aj}x99m9i}$meRm zxW4>ErJSV$@yvqLCj>`k!caQ<$~+laGw&HTc2AdK9SKz$b5!C6Wr2ug(Khr5pCSoZ zMnct*;9y9b7^35oGqoxppb0fJz5qAS0b_|!00000g&+sr@xW}OBZ?zC`7akTwOc@; zoaZ&aTBsj#gp2+<2S01ecgF)*(s*W>Lcb}VUFcv9G9cUlIh`F{)Q2=YC(mHV)yXO06=kND&@iy_Df4kgc64bOUjI}9kV`lbTRN6D%{ZVbQ4MaPyAn?tEIv8jd1s*7c{rr% z5RxpZCXP<=D|KEnp)CU^a|bemo+2vtWx*Ie^XgO~avo%+K)Z>-J01MLcpA@yX*cNn zAO-w*#tKhqJEk3y#W>VMNeesGOQISXvS`Q};Vo{|PcTd_%k@wH5z1mgfe%EPm^NH< zTtc_74i7Y@ROW7QH@HxDngO*u@w{@-{C+r4cRVV~!Zui8O|_I+^sgmbrY++OJErmZ zd+|vin?$qJA7UG-0AC38IqdpPDT`y3fo+(!s+9|1AnW@$ZUnxE29WsngfBl3(fO|^ zTeOAQV`9a?&BWi+obfcf62Y+>^TAP|000000006X@Aq*G#5XLeAwh+Vg2#ljG4k2+ zudCVETgDn$04``F-@zaD0of9^oRD~bRQ^N|FHnFX=&Y=qAqIkNJd~NkZTelGV^H;H z)rSCPU%*K0Cl_9(75B5cQ0YFad6p9Dph9&U0FQOfE}R#nHCb_d9)f=6PXG)Bvx2`! zVxR@9yMlbKvLwqvuFcLc*Q*SCQJ>k?b?*cR%}{1l81#p>flIzPDP@o|8VDzKP)1*y zMd_GS7JSR%^veKxHtqn2YhxaO5}|JPGm}NV;||>3l$hTcmf~C&-XCi^a%j_EtOL32 zb%GE5*C5=bikhG17m)WRCBC_JiHi?$_XyA+v}jF-Wa?mJJ0w-}+cVaG9x&W#vsynC z>KETt3)1o#MZDVu0S05&Yf3(zuoOxVDIhogVTn9%fK+xqn3qt|#gM|&QFr%laVmrg z5%iseWMSuVR0000000Hnv4B@~9YQ%gj7qI&kG9cSytfyKtSojfw zfSp;y*C; z^>u^ynBTbOJz*-XVS=e95WpHukR1qjL!KX?uE$S|(1e!QG$I}-gH6czW7%vG&`uug zI59h8ckF7}3KRL_h7zuyCR%+>2xmSE;Yxy*k3$5x*Q0Rs*FC9FR_q2c7u=a+RjOht z9P2A0iXHnJGl0QLFS1yCpc@=iyE_-CX{?_Kt%_T;PyMRY`tF0&z<8vT8Morc`gwnn z?OXOq=O**v!H_QJ0!=^zT{6#rvqOLY00001A1BmN<&+9AKE=DF7fI|*5tpYx8;=xk zXLuqJ1aEVE0^uN#GEW!#5pOmFfWjAurgA3_O~Q1gm{?8-D+5e*YQTuqYaN!#zy*$R zg#y4?;9tdQAk6=#@gfPU>|WS~OFLAnRrWq!03yk*F~Bz-*qlu&IDNeq7z?rGEX;n| zK@3WRgOO+rLCn;M3d;u-ImU}59*ZAED0RD^0R^*@LoVXnVnZLDee)g4U;m;?D&HG; z!La+YO+D<8W|2~AfZvC{C`45bCxh4k>yiKf0004Y@-fXRqI8!Osg;?I0~I z^(2M*^$yFza92_mOYwr+!6cYri4i}k>l33U0a@a#_??X~(mQQ~r%XgX1x5Zf&Ey@t z#$uIPOa;5&jK4`!LZb>g^019Bu>MRfFjXpgCKS7!bqAV9VBDGo?k~WE&?yae3ooLD zrr~^zx5^V+t9oU>Q2EF+HYU|M-2gfb*z6`vV5gDr0Y&u3VoKo~pd_?KM7M8mxc(N9ekjAfsfJy68b)mwa*o5XE=&BD8OJj2}XkL36eU#|K#x1tfV zA-s}0Qk1IV-&`f}OHLWG3ETXc7tlL%8&cXMuAl(_uQF~ej1OU1LArjM&x`DrA-mtu zWI#E}dX9t!9<-nQ=Hd&8fTbMCm z?v_7{0015X^S}TA002ag0Xw{5x{&Zd@9fLw&%Hzh=YLBel1sKwNwE%4jHXWTfJ5lt zMh&^LQ;|Xgb~A67^jj1kF^_&Ju)WKY?W{K(Sh$BpiMw&#t7N{?1g9Ut9*tcPFz-wq zaVv`o{+>nC`)wwJsW!qv*mEmSWxUeAO!()X$WfK&m+2{=98+vtfaCFChgZj9n6TrL zdT3#>i~vCZscdQb7QI^ZFy*;*_2K&Ms4zk>!$;IQn+Y+fE~FE>c4KnR*)nczOk<=l z0ocO?fB*mzeqFxs|No-v3R*6_OA^{30nR>F5R+f5c(@M-4IP$MB+I9A9DYkKD|uF= zgD*gy4}ZEv9NU?eH%$zY8J{Y6{wnvP}bXfMkqTIU>7eYNWT|atVV4UF+q;Zr_)y$4TvH9OBOsc zwe>?MGjv`KnKRrjCCD0Q6r(}STvML3sn_=^vDT98NLl>Ve>Kqn(43PqtW{}5{sKAJ z28KPsr`89RKmZM*zyJUMw28}VF5wH4nQUOD({#vB>yHkj^bUO^0gXW}>nCHqV(vBB zS+k{wjBE$Lh6lt5j_fg7{ux$}?0?hQ6IU&8Q*>LBOp%tH5wboJE@b?mLt{yOqJMng zfpu(7dc@i#iEySZ{aQCZ9z;_6?-&T-i_jh*U~1woxl|b_oP{^6@H=lFZew-H9FlBK z>1xI5Cfty?UICy}G^X{qz5wC&1oF{0S68&aylO|P`mLh*$wOS+d5v#T0O)QRAOHXW z6rsw5C~ikHz+X)qsiln=HS)m-i@;A}$5UJ9{AE#JCY}~_H!+r8Fj~z@Xc2gMR^yK~ z{5&7k5Gl_ffd)8}Th&5?L&1k%lrSKG095)l z$d$gtxBwBNzyx6o!z=gz00007Fzgq6zY`gv8v)eDPnZIwX_bXVyEarZDl5j+pOifk z^hG8fgBHpan+0BG6%06=T*X&;*tCI0k*G-un?9y@UNnjghJ`?uE8s|471HNJqFQw3 zbVay2wJcN6gj&OM>d#Tn7uN98>w+VlukX{qQhCBEG zOpzT}0004$TN)D$_m#6Gx%=)5Lwr^} zJAU(BS0(eXpo12Lxbyau@E%>Q4b+EgUm(H)YD-}Luh*l5c*9+9L{b}}ntygOREi$M zAC7p*Wj9HCS*YNt%e>lKsNsAe_NO8;aY7x9UH2rnb;$n{`gxd%tw2F|pkuYIZ{8zp zf!ze!&5otImZDY+iiI7{h70pL&;SP2?l1rV002VKnwSR+4pU{bWYF2H_m!-uwnPI! z`_tNM(6Zth7?px&{<{9p>^UtcJhMI*prvNB;B)9)i>|^y7v3ZKy#m^BECq3I)awJc z;|wRdYKhkqktd##mwi(cTYePh1F`X9E^&o>+4Jstho^8r0%a>K2<-EsQyC)`Zog1@ z&`VyybDnYtTLmYOM0SOw$;u6IvSyXBNzZL zo*X{6=3V)OT5F*AuK&~q{XZxt+;klF7T zsPr1W;)jp=nbG@=i;0Bt<6#3L9h@r~up(cvZ7lOgoql0bM+}d11n!&;hV{gn_D;b; zo@YwoR!=J^=#Y&=v_8y-oQ2yPL}y)kJ-KM;Feik8#qUoIkFEJ;60A3nex4ojbHI`( zcCRPPgAq~kGklpm8jiN~u^--%I&3xsa+&(!5hN8vZPmB|x?7S|SRPNXSa1ie_BU~& zW+d~3a^&b`(o<#?Rq4!!izrox@Bjb+02CZhdK8C#qDKlJXTS^=VF^o!MiHWQdqW1D zG}1A|>R@y4Y~sEW0%z=?XyqTD4pM1OT;CfM;|NS~zgu2RVMwV5Qk4v2BY@3z(cV!? zSGe)qb5z{x8&6db3Js%~(#!SY9<1lmd7cEnrl)xpb!@S}&qp(&iYmQSZM5h6LCN>T zWvZi24uXN_VgXpvk%SxVb3-Cw5zl%oj0^I7^AZSh_aVm}&$mA5uVO<50RR91DchPJ zE_{E-B7+Kqj32616&(BcJ7w5Q!m+RLs0Wp|<&kTf$49bf+S0{5^0SrguQ?S{?$-ZV zFHqF?kP~+}x6d4}4;YtUyGg33LirIx`W27>1y0AE%2*$ zJ2#ec%NJ557eCWfaj~PTkSAwqVVDrBtG3jl!vd?J!cz5aQaYlfWNh zK!-8_001o`eTAk+hDaf~LWeQPG~%D7&J3>sM{bRE?L_A$Snw6qgRLHw$3B8emmA{$ zQ3{Sg;Y!9sUdF_1j=)@zRQbu06RJ}!;oGbiDD!}T003z(Sgw<{zs9wHEL7&Z^a6!# z$Ef-$Iy(JkX>zV7!77v4>9ywX57~cSItv8Qxz+H-`7}M3tkNR{TLQ=oek9)j2O7?n9$jloK?UWV=i`&SSJLC*jq*Q^P2slQ|4CC9d!2t0cHAA5#g~5o^%l$& zxIxD(ag5fho~cFU5I*O{o`;r=t8k*0*8<_VCaSH?GgXUeV{}RS-rK#t!6=#d)xgte z(THi=L#i>qU_sWAb_*j@y&xBg#H0UBdC8 zD@i!PaLj)usId3|%K&QJg?AL5H6*Q;UbG*%(C3@@Mr-SW*IONrKEw=AT-L3Ih0ks2 zXtw@TmD|HJZ41^d000KA0TE1kU;qFELZa2)H1D$8fc0ooX%=#YA-e+XD@oe}ha#N< z-e?k0B+{xJv{dkX?;V@8g_HGPw+D9xh}<&z*pRM+r6|F^FSn(%^BO3CROlMxh&V!? zq%s@|BRN2YW67`RPRuA!^)yO}AI*8t1%DhC@g$3Jlbdh?H^bP1;79w2bO!?)VV_sb zeYRgiT>x`cP^TNIR$p36&ITV7<(i-hb^X^XIT|wXCv(YN-=#Z&upqx4LC1J;tpHv`;-f5q}->c=aI+z#WQv!)8@WO{r z!}aRJNpIQwXa|-2dPFV@J3h(f8Cc0j*fBv?>1!OhCDU<68$ZA}FZFIXkG4}MF&@oZ zYvCI*og~FN2Ooh}8jiZezo*F)Ex<=J?a0W|aqoS0>i(mVB}?epb^SIIl4zCS;1fby zjEIdNb2B#sR-wV%9?)pj+RuG87bs-(B17oW6k` zg=4gemHa&^@j;YlgL>HARKo)0f0bbSSlTP6XfaOh4rmC^BKa!H*EZI5pPeufM!hww zmhljUi;D8H38a6TSh;rJwD~ImasB`R000Oltuz7=sbN0$YgvZ(2aqhl6}-XO8bbC> zi^#pz0TN_tGaB@egpWNzTn*@z*f$rRG&Au7Z^6t&P^Uq^0fC2pHEw4=|D*-PL#2}K zddxph&m-aDQ^0j8h}02FrVtKt16w_5yEpxR02z|(U?B_raCkEmod5vI26d2nyo&BM z68-17J}z@$sMJM6j^fi$lDAmQE>UB3y|kucK*%mrBAgJQvC5EIx-FV9Y#j;71(I^* z90C+Yl@Qyh`zA};v^R_UdESWllGyBltTc~-Oj5P30007a4WV7O_%UwMYfLu_Ky`FPTuT>w02yz=Bc;`V?TC zCWSh*T>~GQ-*9#Kf;DwWiI-8gec|qgJi0Jrz|Av_Mjtj8B|mF6b-+huJ?YP;5v9*s z4i6g}(y2^ODp?3+bEHI7%vkH}3wBT20+B#%mH+?%xFTB^-hp*bU^b0H3t z4?^U9Cc@-kzD#wSzg|<%W`8S?aQTSJ!D#KBGJaFVTy&A?K#5QDO5Kv+ch}kW;eO{a zC+yG%ec1>?01=Lm>0wLybu zRc+8TT}p9|YLe|c~ z3nQgOqLnbR$nb=!SF7Ot{@cU67VB7S4Hj3YHvBZtw^0brVX5G8TPtn{$XecFkG%KH z!TQ*31W>7zi3C!`y&6Fp0FP=>Px$PE#vbu^-Dm{W0F(P#!iC_%(SY+6%A| zPXAI(q7wb-#^8D`P$~jkO~0CZ5%aagS|Dg3j0e&Q0?L^HdAVVWA;+LVBxh`bq<@W8 zy|-O+wR}qm|2G8?Ky2s$17gZYo2>C$hV`*K(>Z!So5nlOqV(wDZyf|%NI&&D9tJoq z=aw=)lK<+b1Sl+qm`xLrzJ{HsIv^$(+(ucCW}GgKgOFXVKVG7H4 zdlbV$yS=cPX^VYt-8e`V9WVFoG2-Gdy_6_abXKsTC;YBTfrQR7{=)NFd}(~8(Qa1j zmz0Q&1a8d{*NN*Dl`z_SM5w!1=&XEXW_1q`K5shbT&Hr!X3x%r-Xt@X#d^x&?G;27 zi!wRKi$LQ;uHfz|dtf=`IOoU?ygSCc2GY$mSURJ`-U@gL@rGLeL1Cmq51uV#WWvZo z8uJ-4uF*$G{ZEAZnqU9`00eBdy;uUjkz6S#2^r0fct{b@>v1GxsQL@T4a$ z-y<%ig2MAxr}yz^?YhELmsS_hN6(OC;ds8Kr~4k@v1X*V zh`rNUOI~%$a*eJ;fC#!+fh1aUL2!X&gF1*aG0{7Jh|-Ynnl5aV=jGuwF9s~6g|xkY zf=g?=XGWPU$DG_1b@1PmMkZVbX+z$p(&)-?R2{O+O@wD7ant|+01-bW*8%?~=yFsp z0-ZowENY@4_}3U^s{?=u#I6{tO>$NtS#*@!gTW?K27~BG)T53&Yu7g9usL1tye{ z`|@!Q0~{9f$S!9X;ZstxQ?N+HEA`RqKi^kO&8sj|dc|g!?q~{4A4Ea^!dY9!6BZ;< z>yf3Qu0%nGao(y2HAe6dq)W_GfNO~XC^ohD3Pf``b@q?sx!>-uHY0Q(UwuDTbmIS= z=9#7^R{kY(WpeMm1noraKuH3s)GBi8AW>G`e2-XOgSW4ZqFB;e-55+qP$*&Flyw_r zcezrL5k!14V5-cL)ayBrKWRahr{TQWVr?~kG=2UYj;TZ${%cu_;$xC!M&hL?d1=oQ zWbVX@ku9C+jBVDV;Ln%%tg~L9KascTguMVblUHgOYTy6>2o1TF%XkE>Eb^wZAVe2- z=q!3@bpRNhduY!WAi;u)Fg(t@>GXX=QR=IM6&uzK+l_<~uq^YjuSH8X!zg<-ZW&8- z;z`)fp1tHFZvWB?t^7=4$h)dd&Se}*oc+ghX;}lm31uupJrjh14IwjYgCtwAJ`^w)QaPAytP6sIM*{zs z5IW69Aw?M)o8M`GgT7HED?bISUT>;1;SKk04u)V;buwy#u#vn*QY?hKfPda}sN?vW?vXYAI8&h0o`)X?-Xt{JN1!ybBl6fj&y7Z3tQo`w z03#Mk)(lxR<;FK1W_{yvkL`cwVVmbnn@B$Mr+9V++&vNqaSBxBe7#e2W?j<-`oy+v z+qP}nwylnBbZpyp(y?vZosM(ze*ZXQoU60;&AzUxU3*p4tU2wl`*BiTBis7MLN%e! zZ2~ZQhW`rCG+HiH|3fEui*@1zbQ- z+(^ox#|uXIaOmj;IV&HnwUS0#4KgYd#p}MH4(VoB>~A|zVWLO24soS_F0ceb3RWd# znXeEPbK~8Zm6V8q{IXA$TO__h#3G~-m&Go@#I8p!*KI zsN*e+HvGG7ps*V~*JEDqa@g+9WIe+KP zuBC3}uj{SHHZzrONCtU2aRcH?s6NW?9DQBAwI^c%}2D z1?M_8>Vl>I&&*^P7m)I0F4m>h7F9ZC?3{ZUO&UMu%s5q58MjXb$4^MsCz9kVKZfMG z(=_Fh6}uSshLBcUM)>!N>IBoHo9BvTL* zOByua1xdYjq0kcO!zhm@aRC%!RrfaUy5WJTp{zIHN@;{MZ#lHGaR#?PEf1baOOMw7 zm#8{qf+7x(r93_=%ve3UA^;0NR+Z0s6abcm5oF}xT>ASQh0(_fceUa)l9CAg1#iLXlTydOWS^pf$b3xA64qo? zQY*?iQ95vH(jDjY{hq(?0kgspm<^)RtI||zkP{5KB|`|O0EG2awvgopaB%WB3mw-R zMbf`X)BZlb^?~u}p6b8qH4W{gT@)`Egii5d%xwET(78kIMg&yKwl0=VmYw<177qt% zdL|Z)IL41!y*}D*_XXS+$Cftwd?=Ra9nY*PZ+*1Ov@Y*XFv64D!DyN&|1A#Kl zj?7u(O>U4{W@(@~#*%9Zw2uKXOx8?)>M(I_X7{hfg$Zdd~5*cXzR-pr)ir&_|8#<#9<@WOVe=;<8L zW5xYPMaZW1S+6#wC_(uimY6pixH;J+z$&}*|EhCP&Oi>t+kMzyZUpW0vRjcdh|jB- zVtG4S+_Y*^oG75xvNqESnKKL>Uwnr$BG?^^OWOCH%DA;HJ3Wf&nA2QE)r)<*j4(p4 zccM_*HK%)sU`na$(4!zfqmSNd*iIzU8&mkVcq6g}IV^XvK`D)gZ;uOkD!wn}U$mii zLdwh1J9dK0#IU0g*fZBQ-s`jQhx5CeHWHY!f%3t3C7A&|e8UDoR~^3U`_hTYpheU$ z&b+=UKS57u)cvocE{#muqK-R^@-K;NS@-sgM#j?*PgG~V?Ot5KFO=a2T&(s3=Q>3N z7Kw}i{NknL;zc>C(8$0*Rd{dDw4GI#QhyjSRUV0@sX&@W_XkKfB!+Dat7@`|gVHu5+(HbBlz|NmUd$q17kCf@X7%T)qk z#C6V?M$8oLHiGdaGp$8d>Zc@QbBNh9gAGK?wKnT2dr)#XMuob?Ww=N#RyZPrD@1=A zZpU~{3~EKPQNZlVT0Qz;s8Dku!qK08(w1%R4@*7b`-9wbwn0(~T8^^?OSW*fcY+fB z>hAT;RGB~s$eJA0sq1SG=2w?F);N$5eo?USnKZj{JmFLqjAhyMo)>ok5Qo4gVj-Vz zLIsoZAZC&22$2PY=k+J0imMB;*3JB09?xq3sNcN**B+4ISUuEA+?WFl5>yn7H3be} z{F&cAeq(D1Q$Qi4paVZ>e+Q!qbhJ`oalmiKZb3Ee!!ho0 z|I~RulS9+>XhcvBy3>5% z0>EuNH8qysg?PSMaP`Eu%0?#+NY9XP1j5rV6XiXx-g`w^X+8gD9F+c#s4AX@*$0nw{uCU*`|)9c05F(=Mm^zKgf zUQMAf&q23%m_9>0D72V%U~OShehfBes<21enP%T~|MI_P`NT>ce`*?w!qJv{2wNx| zYc1Q-bD1)I*!I#nmDwMg(f{OUC4^ZYsZvS5VA6tM;$+ni|s z>CV9PW6*^K&E%!2;yk0O2St2U6>3G#Vh(F0c*b#ilDp(%4eHYwuhK$=e(}?afV9BlYpAZ(HAhGYS3(auC9HXbk>aq9Bb0{8J zDe9l97^gx)rJc>!&~1AQivP46(*#t}ccq51FCHE?i?bO(W7Lf$#a*p;5H(eas|P;^ zpCc6ikg>QT>*pPA4NJBzh$ka}G#_SOqjq`z1qG`p@BN3+yR6Qt&g+u9dcS>}YkU$G z6$$({k+nvW2X`Kkbcg@;46J)_t1K%(9o#-_MPV8&t3Z|#`R^b4N+GFfw2pq9NwC%8 zc9m^$NbN2Q2(IYE+DKn1tPuSC_s;sWcb5=fAZRiPB59bE9G(zd6m7v#s$wi=X5b8L zLDAfN+Qa1zT*X0R!YieANqm@Mm>-i35k3GlQLGs6t*Ht;FD)S>+Qu^qa6kosFV@VL zj~`eXu#?>ul!gD_cLB(lP)M@<@ZBjlC?s*TFwQq~2&He3QNGEA(Mom%I9(nZ@QL*x zfSv#z;7`%FW0mDJe{>`&$O~~<{hV$ZX9$|90!--{x2k#6Wo(-$&B1fVBd~ciL=D~A3vX*29z5H{IDzt>?u_+iVB@Bga~17>p=C77rVi34R<$M$9ih< z)eVW#JN?ra5>>TGL`fbSfdT3_Ns-5+*51lAM-+RX?{EQ;??~1jkeEV&%lb;&3F@0A z*p-UYk$$c!RW2VnJ72s|Rng@*Uh#>1nt)~9dkA*ohRC~S!I^RX(?(5AUib`?Ok)*WQ*`O<$6|0+i zlsC7Oor0Y!Kt{VH)c^?N|NO9iB*9M!;7~E(n-c+$ z4K9EL#K451?&$^MkNkB)Du+7*nJ*Cx$gZYwj#%u_)Z-%etZ^tOMv^ATM)bB4P|b<9 z1l1aHk?{}u2*_f>M&;br+Yx8UG&5}4=YL{KKW{nqv9>n7p++ivH7r zFs&rgM&gEqU>Ee8(f7(`)=;i;h4BF(`_kK#teoS}Q?Wj0n}K2BwQ-JBVztCOa-zUO zFrPav>g_8%9$zFYRw+V4sQ4CDfQHj{Cf(|o;(u4@r4psw!m=VZvMGxi+4u0PgxT;y z{ww2$CGU#Zwxo%qIe2V(Yn8E&WBc~e*X^)bG#t&J&bR`$I-e=P0*iB}{FaHEOnqr1(OcZRj zTzH?J*W$X|e!HCTBnHS&+ba|;39UfjjVZ&Se}M?Y{}=F3k)=N`wp>Aiu#hmrIME=n zJ^_FclNIhHbkaFRu@M-Gec>rYy8ZT`T0BDlXI<9)QHG%MBxf+UXHz)bvh{}(z<&N^ zl%N9GWRyF12tEv}UV&ljwMj-QIMi`x-lSfbNjQeCL2Q8-8IR--PPK8PAjw;6^IQl2 zZ9$*9XnOODntg%of*`7q%uJKun=rVo_2;KeTGkxQE)QVUS!2j|;2$5HXIy#3?-zVi zEknkU9DL%SeKQ{x9NN+^tnM!H^OVvtLjB@`)|3l`oBWNZ{LB^!%MQLgmXyEcmU_?a zfnaqF*>4O&Q5ZZ>#T}Qr676RSVbs6YCv(m&(SV18<}h4fnHbzSv?6h#Kiv?Me;Guw0{TV6^dmx_G>R%!B-ATt36p{*&|+y zT%YN={0_OHv?ySrLOuabN7~jj#4KNwe$$yi*s6Q@m)k^_KE|)7T2A@8Kqg&%{9?o3 zIJ4`lWra&$&^_=&j|4uIa{Wf0#y17Q@FJ8oY1PNg)Ln59x!>k~x^R~A2==#hgKLlB z@bc8b(N(^}|(yRB}i?s20GB4^EP-B~PoE95eZxwG|5eq91?G`MS*==&L}*x5QXvODG{b3>qwi0We1oe5n>we>;a z+;@qW{xfbdHL#%A+A_=knYaw*4vw;6q^vDWtu#5%GOd;_HtB=^$%DRKm7L z-hyQyrVHx&rT<|(P!|9}9PT|y5Ta;OFRvd}8e9^ZmY@3>jL;Ii7qy#=qnn}`{IRSU z1@ql{Pv8Mj)f^~NfE#OoM}d(%GD@`^#^kg^`l`@EQW?YE3Z#T9x6@{bp8;_Br8->; zEuxmelau6;IJP6nAVT<}^t0Ov0_p1;i&q&(%rz9W@B|>QxodvKyIF<%VCOKH(CeTl zTHouIZEadqlD1rZBa{9$tDG5Pbg!8L$EWzr2>@!A=~x37dW!K-ltRJ~#3wrI62Dqj z(^Z8ljTWF@M8-C@B;voy#*w+8Gpaegf{0hZIPJC*BvsFL0pUqhh(AbEubLp$e(y%8 z^R}AICg!N~Go5KR|AArs-ys@n<_@N7b*7o_G$Yhl1_h9tm}T+Sx{w(!{R;#|u|hz6 za)3}4yR{L$G5%5c@~dcLne1M@wSgYPcI8(+1^wDSkSHDrn*+ET($!VE3QmDJl=Xm} zJc#oanRuGKnZr3ifR6eX^&id~Skah2?2;cIR8R25cm!Nu6=JsHLq%OF_zkWBvGJx5 zvHq?HxxqNQjTt(ke^n%~qb4>?ps$Ls33#b7YMWQWruul|@Ml{LYQ#*pyE`6zYm}uz z1g{Kw(!zU}zh_9wvIv=e5Ll^XEBcOFjz(1Jv(w=~n+Zss(Y{FvU)70XnpKHGXlE~| ztda(YczqgmMen%3&>ZtDPL%~@T=G^X9o)YZz{Za7nr8!R^^MbYc7R`}%5F%;Q`6 zKA3Du%|SZ@=vjkt(rDObv2IC!+Jc~imFv6?VL1M@8|b>#{Y{%S*ABmg&9qskA6814b*zDu_#+u-f=S=hW$ZXGLW4S7(03-}MQCO1 z>fg0sfZ?L~^$Y71blVEL53L%=x10$AN?a#$6O6M%*rlwI?Y0JO;X8^G^HA){1^hgv z|3ogVVHY~^Ksaza3sBnr=no4!SKD%TBbE=XH?sZO8W#{*HGg2Z(O`&S;U5(s3eX=& z$>p|FqXkRG^HHqdr0+{-I{_=U5!&C;i(`lvk>AAa5g>+N0q_=i7$)UX`^Wnk$LH3# zSg+A}wXHv6_ibP=ZSfp}&l?tC#IlN)xv@-O;n9#8$?0@_NMCKcM`#j!QhL2r@a()a zOO#pNj0uxv7dL2HIPB6J)W#U(Htx?OaG8KNNtLA$`M6q7BaR4oRn$m2a$`_D13-2D zi+zyg4r_E^gE=J3*%t6AcsHh{P~8EZC(yTrmLwxns6R6$3_ zVIz0qa^eaL-b=|S8ov>kDG4b`Kh9*NVW_PZ%_w%O)l6G9Fa_WkPCX@*MgE#2sKauo zIkXdW5o&gQw9F@^WnF5R5k=^V4au7rc>Cqprf|y%?R(D`7uG_eN-hVv#;7{41K?S# z1VKD!SAjjF+_7|&sl1<>6OKY{D+sE)OY#hl!*fYS`zO%z%t#y|6nW@E(uSjZkPmmF z#{<_T500~W?tI3MtBJM~lS^@0M8f{Svngk?qDK~dEh2Gds6sPI01oa;AD|-3zG}Cs z{d6IE3OUn6qv{Xd=R{AlS)8b-*#B!~G5Uph2vomsuH|xt|pdqzoC(UnM2{8N7&ekQfm=@e1t{m7y!aA*a6n zHoN%))yR_9+aA5chS0bV3?5eXc~~}3R=7WI+IA_htZ-YHtovK6**V|Ne1K+oK3I|A{8jpVB z*s$!4$sdg~Vxtw~ZeoM?%HGtjEN{Va3MEV9y#(!}`FE*G@Sn-1m|?9x^_hH*o(|VW z>)+ogJTZWfEgXcth@~F5rhjh1djRO-nF);nI5FoGPoSh4J9Pf4F^2gL4-v^3s}Pjf zx~J7$9f+9z-x}^)Si=>mI{`4$9te34mKl1HHmjVxwRB<1>IaamWe5G(vGR4a8(t)m(BHd+4E;~bPrnT+P=VB&QKDD7H+hT-b zOS4a)b9%TaYgA9x8+6=rR^08V<-`$#U5CqbT+^ZUQ);VOtr6#72d9x;-|t10m5bVXKIMRm{JSb5J2-gWZC!+tlH?` zRWvsD{!&z*ISk3J!pygku;;VqV4``LLJE{#J!;^vc_0re{J`Km6YL4%Q$y|^uZ-yI zN0k%$wLbFA9}^{PaJ{IN3K&h_q|q`^=Yq=aDQn0IpnZT4oo@walU6XQuGBrnv7@ACTwLI8r#`+@#miB@c4tjx#q#Uciu#XmMC@i za@ijV)Y1wvKbdta+fag!Y*%s?MyLsSR|8=D$sjY7Vp&=crva_l?w|Fil-kK1)s%DD zWQPrGnOi*;^psL0716{Tzfd7LPM_P3%CoUZ!D;86`U(-!oHf!BJjV z#gblQSh61h)K=VuUo{65VLw==4bYHbDYYVGk3OWJG>0WgHM3R_C5vxG2A0_P9;z8P z5+A_QOgTyPzoHDuhL=PM1q$b_;_q<#V;@u8Il(1l>KX-xx*Uk|asV>$ae=le=QR~{ zmxXW^4a`pmK5-i6U~QoB$*(2}CK}@esXzL8#-Qk}O?RJKE!T|pBSnE*;Z9NU*`19* ztEHP*TPI;5-Q9+wy|pmB9QEmT(P=Qj^Q~kbP&`rh5F*m34^bB=Wc@LekN0zZ09d6s zqEX!-s7G7qu*de#7C<3Thg2%wE+!J|{X>l44j{)q&A?65Z=rQA%q)Dw1W68)spq;y zk}r~(=roQ6H83mZr!MMHHlAtRKMpWW@YftSe2dlCtz9gNk_b>*#X!R0Inah`cZ>%M9HZ2gBPPRU@(3o=f4&@7 z5dvC-(iH+?U3Z7e+39)^4yO{KIrkn!37Lxy$VW<1R9-m@h-B=`PgCa?IQZnUCim z;5xN3>%l@h?J|D+)x(mkD;>%xo^ia(4K_aF3&RUQ^eKil5LGdYgj&tlnAMom{m7>9 zQc<-K=pg~oex5!MgxAof?G>?~+``38zfyNQ5=g&g&2mfKKaeQm^IWlClPF?< z?g?ao2iI#{dVPH&!$BF*Vj){ShGyBQFehhk#tE2QyO=Sv@xAlVE+m{p;4(KkY}6Z= zZxzUxuYT@j(oU<%qBaa=G&FfWZ0#U>xsesOOLP-J6kdouVled4b{bYWeE664d%HFL z>WwzJw+JTkRcz<%Kr*Dn=`&cw#n1(bgdZm5K}=)c5wvo$rC6tuX1Fo6lQaW1$^zkQ(Z?;n$M=u0rni*l@ks8Fk^?c(SGd^X zdZcSrMDWo(ySV^P*)RrnAOL{*%um>%$)w;7W!p0$h*HsV@`vfV?-UIwxR8@~;RzVwV=+%I`9nuASL{20?&Q`c*OJQ{sPPKtlcN)yiLE7K_uowxuu z?|T=)iLY@BK5{!4*gJ)=-(4G@XC=d6AonM}^plp{D0|f}CBL-N?>dg41Bv&e?HCqi zd%i1xB}d7!7gHsBUZd3d%9}lOaLiqPYjS*L2$4Mq~}TF|8_5>S7UDR|h>&Zzf3 zf>?XbDd$L9WCP&xtU#ZckD2^k z`75T(dqPUFOH}wApAw}(LSct5K`%FbBMLRvufr|xapsm$dc)?}YbqQIF`ou2hNkZ2 zv4(6PP3$J?yJE&r@eYTQd$91^Yz+==e=t~iNLh__9#&Z10TH8A_082yDF?IZ>}HcM zkC|HC;st58?EB?(r*r&rCjedM=R*QE$%Iy-*O8ikPB4g9ZuHq5U6b31-e+jF3I$}z zhCQpqsgsRNd3W-jdGQl?`G`LD4cz*Z95QBGax?;0Ixh}=BX<>N)nT#_QTSsa!Q?4H zK&W9Yoy&8f1z;k4G6HvJsrxMb2S6mOl}y2IT+Icz+*@vn6Iqc2hHpoI9MLU+;>|8jLbop0yPp}i`Oj}mPql8M_y45is#AuZg(q|-C75&oALuHgTBA8 z(Ai4N-#gv{vb@wnif@{`F~iL;1oiT7|1xYc+0RUtLrZh#>=~C9iN4{Zk3(ElZ%$K@ z45JOxE_ZNru5-FKQwR=gk?q;37}G_Ujtl*;)Bj#qW$kC->SPC?C6 zKMVkE=_skZUEMmH0*PSTjl8By=|LUwSER(rQ$!+wNti_4#cM!xmJc^z{|FctH;>7R zKk-k)?z8;~tpc&Bai(^XX&_Xm;g98j*4nz8E@jC{K3Qng`xD(65_r^a(_orB*>zY#u++YXxMa@ znz07V8b}u!2j;|SPhXy=Cv+zR_CM3%{Vtz9beP|Ot z^lxu$1UWtG`LQ+Z++pT~n=MM*@(57Xp~Qhd`z|p!KdrBAQ%DQ(&WN6!87f9}6Au+@ zpGA8`J2VH}$n^7s3*cF7T#M`xtD&HOlcj$(b@{y&$EC@T>ac$HV}_9|K?NC-^u}*g z3+k^Q?^J@+3>p^6R2CW(YBN;jYN1=63z&q!{yhePDs68bpNf2DbC*SG&Ov!=mP zkI5FKP6>yn?Lj4kVLEYUya@RXJFF7;Hp1{c*D6S>V)ai3gQpn4bv9&|Me7&ce5 zGkQ1n&>!tNML<9{!Wx^(Y)W+P%%K^Mw#D zCMlJzxp=2<(!yHP2p;!;pI=*5(!hZ-En+N-SU`^z*nj_Pc?^c2}bm{w&F zi{~QEBK+#>`pTvY?2->sEIJL&A|5gK^|Q-4Tm)Y&P>5~L^WjJOYdjvfp%(zc9?EOJ z95PHDrP-yWlFpp+>LlGhV!poJd?Jy%cGzih8<6(5FCCJS^IAIgfpDk4M&bKU+f?{b zNp#iy{Rx_nTq!I5FjVIHr#qtFe2pCRB4wt-BrLkRDlJ#Ei#w!y2L?R?^q_^rq#&v) zdYTE;xa-`w*LMX0ifslxwE^@!$x)^*@zXno@en%}r&_T!E~#v)X&-u-9->8kS<>vc z7%>7p9f&t%cWIulf-UM_E9gPn{|Tvqj?v745}mU*4*@aRt@0dU9ZZ*;V9W-!b<;#* z2fb}27j3y&-*kALjwEkVX@mV!!V*MC2DbGUA2uuwRR&W zL88DqP~Gdm8U)ipkhMR#CCz8gJcuASRZp3A1#K88ZJ$}au8ss#tkxEqsB(C)=y}3d zT7<|h)vTc2R&u{Wtij=Ww%)_jAQ|hE>h~(KMlm7Fm6CQ>yxfp*NZG#@2{QD#5;(Z)4|Zf1rrYi zYs<5wW~MHvP1SaFwLwu`yvOo_g++)Y5TN}V!ZVq#Yuy;4KdarZH@SH8J5+QL#Kych zNSNmv-5g=svK1vD?j9kIbPP64E@;c-pU#oF!|i=*M{oW98hyAR>F9F;dr8=oSe)Ma zB_V;tI}A-o zp81*G3I(#zgsqtSL}W|ANHA``vyb6CnSrtPV&f7X_;i0m3!8DoROx9izF`tb3l#~A z>(w{1_xM0>?SjIfJJ>+{)`>ulAh4BsM{>39@xwaiTv1h9NTM;(gYWPaUQIQ+sJDO=*E4eAr&@1ibzJ0%T*RB+9!VZL>{DMb2F2KW?Ee4K$`2ym$7v2T z^-nT`QXpJVdl#5mo2id*ZrojL8mGd`>+br9a32jaZ^oQQ;^;?8)uOS|amh@gVM=<)`)NSCZ z-WfFCuI{wJ379ZxOB)=JW%r=Ey&)*iY2MF{vr$r{tSndDZuA)s-v8_2RA=R;8~`u7 z_EuSUecrP#bB^$xHg#ZVs&lGja_Tle|3{K7zMy2ns8QOnIfHc9<$#R)Iex{n;V~FA z@U-O8!%t+*=agvR%w`JGSFv8Tm`)B{RSunIQ_L-jdLw+VBV!2=;S4*4lb}DEf9yqShMX4XlK%In67Ra#N28jbYgzP=!LZF=5rw*TU zPP5##><4>2{w342v9+)3g#QaG0PsmR4nP|a4$!p{9(_}MfRq%oT}Hba!g6f1j3^mo zeX#LPr5o>Eg}d|iYqjRkT|8(yEN2<37p4Izvn0rsw)nJhmG0q4D?S4)X6N?+up>+> zS~IS^BY|v#CA6Mcvfs__+5A9|N!_KBW)+pA5!-2I(_p!Gsbh-p$6!T^XdF>F-@<0* zx?Oq5!>B$TN*fn_s&Q)hQDb-JYdN6oPOdqK70xE*Aa~V}bgKTct;MxwD`zlw3v;$| zdIc1_(gK@5)&`>o`*@J1mpxL#Mh&3z_#WJL&`CUGeqK!eZtiV8dyE&D!F>S(H=>Ta zbrjuRjOGz%DI6=Rw5YInf;IOk6r9E4=0LsjT+{k;akxcf2TFnxqUp-HoSi-s=ZpT$ z+UQx?mZ|o{;>Kpz*VHD{gXOx8SPpx7Dxz>K=Dbk+G&HZdk}(hE(jgIpNKMqJTj+c+ z??0JO5Z zf!Frx5gqe$IHOACUp^zN1Rp-v(4{J?mo=o=K4~c7 z75sg6APZP$%1L;$WLps)U6W2*X9~blb@&JNFrrg^XWot&mOKE_0s+#&K#U655u32T zA29G*0+m6t&LUuqO_Cy@3>ASAu11iP3$QE9pN>aZ2R-N+PjkpG?epV$#uNPP)?3$> z5XwJmEu!%?`T9VZwbC&w+|IZB9RfGea9`a5018L6Gj=&VE87YXs3z485`fB4L-Ib{ z;Z}38rujq(7)Wlsy6e|@H~k@XaFt&m;g#Cu+n~AHGseeqH+83?f`|hrm`(D~en&cU zI8RcOmObQjhGdM!+|oh1!on))DJH{zSI_NyTp*xE3btnmkW?qua9^=Vqmo{T=sgF| z?pfbBo_IztW$E;IrQm;IP7iM0q$>nS$OZ@_;0!n^T{JnIUkSld=H>r=p}f&99G60X zxMM8@<=94}oTfZ2eGGk|d+KbW5~@xa|M1?%K}zgeiQ>-E=Akt}QKOizBE7P8ED=eV z1^Jykax)ML^~gWqI~Hc({yS7_I4XfnxfK5_X%xG9iQQt*?ER>EnzulJj=DB#va?i^ zb6mm^lXU^KvKE$QmJOy`eoIo}`q^Q#@@BTZ~N z3)rn3^S_uu%Q;!hhAxvvms`SaIp!%Y2{xdq5z<%bxyF{>r%Q=t)#kwJR!(+cfeX&5 z^ByPRu63Jy|B#8)+G*Lh6ZO$MK_zMdmGKO9s=?uI?^GMC71_rBtVnFCDd0Su0;_9| z_dov8C>t`dM+RL*V+XpUb~e2uV7+i--bB=c>t{^G@`k`mwcEkNE!0O+GVQ4WP`^zTXN0*`7#-9BF>8=QRTV`1U zi>;`sQO7Q-xm{$!rr0sRHebr^rD!5cu7e`Y2HP}6$rtiW>?1_zu?zu3tl!T zedoRTyvdBS?|m(FgmXAD+55u;4Jj}$p*&m!J+^}Bk^z| z5u7bNbW3^z-PF;AB+>P-l}|&R8OsO@w0tWZNvvP1ge^~= z8K;HEY`I~kwXt(zhH#ntn8o109>2l8Z{0Gk42>+siJ1iL?)c^;?NC-Z!WR@nQQR+a zK2{qPeg>JVe4R`@tZqhm?MI}N4R$px-Bxp^`3~S#P*QMz8;uPJJ&_ z_t<{?m-CYK?X(o8>ZtN$vS4X+#tVmIb5Q^+e`X7=eEOFAQF8cGslS0|jhsztjt0b7 z1gkkR8?2utyG&J14SIV!WO`-m3zdwKbj^R!LHYuDph9rFWUj(vkiIn8`Dj?IW}Zv- zL04FC2_t2sj=&_b$<@dAd+ge@I>@({9Z%!Q#+|n`KS0i`nc~W_LI*0ZDtT7}x!p;>b}oLDKjT{|%1BSnG{X|czKU7*(#dRH z7N4FCw1+ph@_9gTi!$h*n$)xbDS2V=1vD+{qNHpVzx~bY_$zU)#!62*gT{@=*U8(z zxoiX-3n6@ujD2FpLjMDh?ZHHAdd@K~0h^L?^y5E%(w-7*5fK@BScjVPoa_wNY& zfX^GstaO+q2_*+*mEN$qtT0lEtxn3>xlEbYa7!=CwVfA__BntpGPiE}MFMv-rDqq? zv)v5m_$<$Dz~LHif?k$=_2=me5hYD5G(f;> zvO6EUJ(*-@Z=*Ezn+80&P!4h=ld|PIo8bSx(rt{C`u_w3p!dy^XfbEKx+>!@bNjA4 zTW!zF4Is;Y-y$n8HVTcI+`Pi;d5BjI z!Ava$dU_k1R`MwuK*1rV87pOBV~AQg`mp%u>CZ{|ZMlLPD%L?{t@H8|7eKv%p$C;l zw#(TjMD?4bc7;z(ldXI#_yh{|p8mja9{e(-W(3%Pmc>1>x9;&j);?4z&e$Kkp9TkG6*s8XI1Tsd!XQNNr09-a`5$Y)SNSMQWr z!WygmW{WJ~ER{)zabQDgx(?#Qy`H}4+wLXF@s>Xcb7Pug98y;beuaccGWQFDUj{qg z_fK>CR#T*XNcng@6Joe;t?%f?K zj)n8|YTok$!;~X84bPVcg6NhUoqbBo`~sEy%2(uCMS}Wsv##&$3hb1+BTANjzhk$h zTD9r{Bwjs-uaUmhVB-m+!9I5#nj)ceL$$yJmOv$s)Jd#&(dP3DmGVJ`RtF9~y&j8U z$^oU+IvM^TQm3a%Hy|nSYsMrBVPyIv0nhQM@m{RP&X&M0IizvqoAom0S z3KM}!U|lU#*2(jKMszrt=9O>!L;?9SqbNz1C84A>B>$9*HjYd5;f2Y$F+#Kul?xo< zmf`9*yzVZ5XeE^aulhx6gse)Qps{BJt{EQ|Jus*^)Eah)i#+FD(5$sX9=UY&)y@1- z9DQ{lRKL)>+f=5Ypys3VaW_ov%B}eSn-sXKR?e9+|6U}|G)(n%9I8wiD8bI__3kJ!3D$oVqRG_?xx4lJlu?KX%9X7>quA{( zSK<_hFZb=#WK>YL8QLHhTio?KlEX{r(7z9Bq$pvhHU2gk{#*Pm@;p_^DlgepB< zM@D(?A5iZz^}x0zkFnbf{OD56t_FxZ9hHSvAfc=lnyvId>5@Nd#d%-VUeXdsL*;nc z#j>ahlbH#W>pJMD;%ZSqYmjMr0bgwkFA!z+cH7&&pdUS~p4Y|CBL3041+ccMBV3zCf$o6E3`c9l-z+I%1GRcr;UO5! z(RtDMFf5A)`)u#Jy26F0Kn>>*ud#)#?yyb=VkYSGQ8;{$l%2GiQ6G(mt(>LK$EA{gMx`lMdAsVQ51`G|~y&$TShY38T zxI02lRM^R-#_&G-CU%!KUd(!WL>El^QzBlW^2U2!wjNQR&QqUUb8#fVv>6#e+zzSHkm$aeJPGSKuHHpY z!E7xlYy%&{0N*xjblu-s~ta+1&^a3kT z=24}jKvmvhihNpE-)F>b!h66zE;q&4)hW@r{AOowq=_d?(>%%%cFzQ?)Z*OJ9W4&a zrOv((@5-N}I;m&_;Rhz-n}r7cxE=UgdkzT{c+5V!^UYquUN$&$BjH<0wJGLrKz+9< ztBRYGQR{8C=FB$l;tl->x=QP5Ggg3gZG`|n%=gx&q}>E)^V*_V;@~d(IBF?RPQ&mb zcYBztcIpZGeg39$H=@7ThtRE)me|_lQKkL|0_`#4^eWSG zCAsfBQXx8e8962r-h_6N8_v)o#{B?z%yqAiIvb>=%2z%gSp+GvoP2uJ6}w#Mr$ERV zP5EopEv4dM)KCn7-=4_S0W3ULu8H(f14~|XD9mbWP-9AGR4e|yAG*$(|26q6_WlNb zD7(z#9sM5SXQ$#;0Ab$E2)c7D8M(ok$YS3o1j+bS0X&Sp6JCWo`}Jtn)4LB3)bb_Z zH4RaIy%Ab*d&bK6!a@VD_|x}7(a^^cBMcaVbbx`qtq9uhx0pSe2ewgl2n1Jr>;LgN zAORhnB<~DT5aSxIAk54JUpm{$5Zs4wufE?ez&l!vN%9*^W9Tiylb(K@mtZqsgLJdP z&Z~u(e#NW!p(n-N3i^pcbLbo$K}mLwHdOld^3FN&8{v=xk!!tfS-)y6^FA1*TBQ3e z07K`c7zO^~D?veATbyI!QgKjdpcu=L5X5bO({s-}hhH6KaGJE?Flb@v?Jpn3bwzl0 zBTznVWSS*N2E>vhvq6Sxg1vvF-`^m-`AQBa(CVy}$}VgAQSLi|wYXVp4M)OUpn58L zHAT+`JSv}ntFT@X^ES~L`vwT+L?=EEXIjp}CZELCYIdgt9=adB;8hAmRp14L5)LoY zP8L(lK6@zC2qNWZ_OV50BfRat@ZOi3EqLs}7#mK1Si~Hi#qtGZwBDiKvLD(_MN|3t zzMn3f+f)j2zc<@yw%0gH#=Iz9e`ES&=#Qx=j&ETPNqM>4n>^ijr$mevb(ZJ z;9N=IoiqZs1*nnu0|rG7jrr2Wt&qiv^2(UzqzW<@_~ljb@3<2^VXq`Klmb-P-%U54 zTI+PcxDrZqal09irjS{qc|iB?S!iEcLHO+r@kt*fOkF04#IY^y{iwxegV-GTN-^sV znzHi!DC}7x*5bY}CeOgM;)MS>`oHswBi&PDy&s+HbqMBhS#T@+_O&p~G-qk0{eksj zd_@gJrfl!$%x+6=<U;~5)IpR*rr zwq{cfCN>ax%cft;Q$~eWK+qd~ltQy@@X&~?>~AiS@g^#IOP$_iH*2?!X6}r`TNcNl z&PY%N`{c}Awm$}SWfF-9J~SVeml{5Nlqf(c9%b-*67P~pd8N```6$)bpSuG1+v%|v z%rl#X!*uQks3YU}rVn*h3VZ4~-{a!O?ee7T2<2LpS8jeC1`90+bA^qBYgwIX_hQ2Q z^FrT88Za=}R~;|*{C000000EcUqf8g|PnGEu&TNFpet7)k^3~9gf zwybey?EV94RNW0^(7dwE7zP=!2SvT%8OXd=8=nUVznA@Qp0*W4$m&A}0)_&h3nq=& z(x2YkucKsv2cJb6o$FpWw>_KyjGc*k3Ar63@=0JI19ibTc;IXTx_xvSY5zZpR(eZ+ zP|N)`7ytTecZwgGNy3+v1tRF9geR6q!lxF4t|D^uup>I=gbTUpiln5|vVEH&gMO4gr{Zu@}*dp9)#L_P8v9p;>oc0Z6+oJwdyO;OW z0Gi+qnE;>M1nOdfS!=%riW~-pldcC7r_stoL^Q|mLgziNZAQeb?+Q4guZ-Ba+7es? z$?KFW_N0kX+xCxq_BtHw0spkkHF38&UZkWt zu(|&?i2EWUR=;}!>(1WFVNcZwK87cN`5~~8HE?4R6*j8d`=>Q?lOS8d#HTSBrH~v* z6I=iQ00000-?mYyRtZO+_dF-H7vDjuH6H_8Hi@~OLnf*b&KR$dFi+^Ez3DmgZr4Hf z!O@W4EdP*0D2FKj^^#2+GKQFb!ho?Ze3KT1^f;dl^h&hf84EfECY!tbsW(ixP1x_2 zmr_T&KfE^iQ%0NQyG|)ugOQWW<5H)nvx=>trR_gQXoY4RK*OlrJ#btEH3KLNB?fpY z7DVx?O^oD5nfVGe1v*fR%sFxQLeI6MD)FGKBf6D(K6v<^p)ti|3iTY^J&AKl0%;E= z>Z;K@sDM_X#EFXWY$hL^e+d0WRKC{Q9=~pO#(UWqbHb(^>F6-?9rJm^Uy7vKCUUVy z+b)xQl!Bt$_4)2AS{yvut)mk9Mw46=WXY0z&QFr-oG?bgT?o#$iY zx}~;qsOkW*#nuFy=u6RfkS#rJ_}|p0l(Y8(t%@K?!3)KaT%r_HUG2O?pRa~s<%1jE z7K9qwXWm&UEC`EI^1u*-nhUw=5uY7IN7Grk=V!RxX;kFFkEq^8WBwL4rG4~Qod5if z_;~z;uHhU;maVM;Q7zP5q5ED09`sxxz@}!}DTtInO0D#B0NF(&HCn7>H+HYnVgiW$Qeu*1wB(Z z8*SJ0LlV?1P(oa@G5*c)X7i@NeAP$QU2DZIF#EBdWKYyab`s{t%&2OKC~RcbElYG| zSH_%TLRY&|6=7yF#`QQ!hM!n8r$`xtjpcg$%YE2@7Jn19 zHH63SH^f0n`XWtcgTT|l#dZ+$_b`pn_DHD6`v=t?|gc!DVwx16AtA!vt%_ zN6vUeXEvCLI|B7PB2}<7Fi-LTt?LI%*JdAHme(mczBXy*31eeSp{TDdFH21@037cb zAA1%v}O$0s$NZm-z`IuWlmz) z!=?1vFkLvt>{2L!cJ7~@470_PCdqvljJQWcJ0m9;LErkO0* zWgKxL^Yhc)8W<-9AHM^(W}($|Mfc=o5iQXMvo;Ocg4-!$>L`VJhNy zzo^b(7z=JJGLnub`r_-uJ-odANidyG+fh9gS(8h(U0t^{eUEvC8#~l1=vMrYYRdYl z*Z@SdeObbs6L*^QkXd&ewS`LYgM9@n56zw5+!)APyWs11VLKr_sH#JpZzq$nVwLEn zE$<_1L-Hs(p?){aqDU$7uhj1llYD*%_3uY#TxANA-|NUF?i?{S!un;i z>#%Q0+gk}gMJ17_wN5wVTp*?`))=&oMQJX8CHB!)4u&Uim|w^;%Bv}5WEu6LO*e1L zY~}y}0Mxijpa1{>0001ktg{UdL<3iV0E%D=`8$k~q5dxJQS{G@ng=ecJV-(pWLnc- zs~$erL)?{Y#-!QXk!`b(L)7knoIT12)WNKW_D^RBAC9d3ry&HhZXrs6>qgi=Wcw$f zYwo45(BuDht@xYbSBm6n9YgDi{}RO?%eHMaSUnGt?-PHl62!fx$*zWyK2GABRE1SM zx9X|&*+V7o<}XRCK%R;O#NSZIm)9{|%qFU$d>~8}EWdw1FQ%mSY6OkIaQ!Bu zTSYIvYFox|v}+EV6A5JX)8FQpF&>!mufpF>h{P1zzBBS5{>-kjZonAF!z~fXrY|3h~B9Cvj(_4EUm#vwtyngMVEA0!lQhlQu9R}(A zCF6V`73P^(P9A>89DTWs?Af(R?aZ_zoSefDMWT&VaS&LE7mZK=h-L z0>W>d%&M3YF;OS1krweR%>q(G3F5w4hk@jRYgBK6(m7 z^v&sWtV0QtcYa7Qn);j3Tglvd2CCN^ch2$%&a3!$fgVdIaY>Zr+!8yIj1Bzwx;Q&z z8OLbneTO3nG$0bZP5_?BCU8^qrZ6 z1?fL^`_yA^mbrp)y%fIrsc#v(MsAzHuqLSEipS8kig!f_%|@m|td|F`K>a49n*|a) zE|eXnv-QENu2q2WFHc&*To}Jbxx+C`*TE1utj$%iQ8%DE$dd>z`AGbdDDw^0$k=O7 z{yu(&h&s%sr&Nvg?YaC&6F)G^Bgh=>6tUWdK}u?XuP7LhQeF*CLq@(K)zjT|m+T(B zs=+~Vm2(c{WB{(Paelz_rN>#}z-f8xW^VQw(ol@cw(q@V0@8PxLk1g4#bawWS2HjB z!S(q7uvd|`!7C0_*&Lf0lyExoi@b&6s8DfRC{I^h!l{8FB{}HprPJ^_vM?(G0z4Nq zvNf)^c>n+a000&gOH3pP0Wp&=)GUEKU7OS(_}$1JW6yg_$E{o``1)mNdAC^Gnp(h= zu{xN6j=W*3`NX3QE0iAXTh92l&|<(iEa0BD593*q!u4~+Ap4?%G~y4B{OJj7{nHxA z5-?bnEj=|65ffHD?AGswxaEsZoIx?vC>}MLOc1*yLmuX;%rZMGBV!^tI~?|bOpl^es)>k~VzyXr@u8Fei+LW#*{orhE$ZQ@Sh@kb%IqtjFm^wHWm}W!rZ4aI! z{W)qrN$OAa%oi@DZ~j1sA;`HzD0k2^#0k^tAcGS~Txo%yR;1Wr0M3%l?_NJc4*biY zHFGl@vOAQ8B~&lo)LMg1jo8&G1uiBtw`LYq&*+nzUz?*V8(dE`~lX5_|nZWn*C z3G{cYw_@Z+MEGI*6UF^TbY*ig@Njd~XG?e=G39i`ToN|xL4Gf2My1S-8-Qj=v7E3G znz+qF3l2#ckEeBj{CB^|V6>TC@MqbL1*QC|G;Zi@w9?CL zwJ=KhOZ*N%QVZgdsqhq;j>qdek%UiV*~MtnYvk}2{c8K+C%&{;!OE#?YfmYjr+eSu ztb8+>%#_NTr_>MymiU>Zgg-5Bp7%*3JPH*PU?}xjl*y)AF(KufVK0?$H-1=Es8+CD zkkHQhd!p+Z%K0ONB~7ZSdk?3Fo7UL<2|A?U*&CW|GR4HMO=DBzbzAMWDMt>Q8l0jI zF7(lZcIg!HB{eCV%b$Ak)mElXyzwbyuN zFf1>B*&#DVf$8K)(Z|i;WL}`Sz6c!MBme*a0000RG*2i1%U~NR0F^(Hu(W}SFGb|E z_L*=@BD4BxqrtM4O~~51d@&i_AZY}H(*~9#Hx{V5w66&{zzM9%!Xv#IpHB}gqJi;0 zy3ZK$jc_Xa4FVR2^!yi5lIDo;a>g-Z+|N;8KUGbm6glVtBHuM~x(p;E*7TK{;{W;+ z(_n=dT`S76RPX}p);F0Z1CEbTit}KOdEAFuBH`u=Cs~^E6s1aL75I4Gk9Fz#3p!}Y zVwi_C)2yXStmSutK`Qleg$3h3xBq(8$_DW^PYYtUqeyvAd`|p#&(MCd z?dVNLm8Du-f5=n^(YO+lQ`N(oPOeO)<_bo|?JU1WHTrjVkQGkao^kb0!w00g(_&Z; zk$df+%E$c5u%>ZkG26ra{6?T(NOTs7>aySE7&+t1bKVy2V>A&uUoF9;p2iC)VF!K>o;kjVqsJl{ zdI(e~KKB1><#F2!sE*#Xk`B9n3-8)SY45ayd^aVP)lPRj9u=Por|;o1*_oY9k-BO= zfT++;ezWaQu8=!%I$I?mKWO$?c4F@g@a+BaXIt2b(~Zdye@f`Q;hZ@@WK?asH)?mB zd&bW|M2f&C0hyr8Uzqq!i{fc)tG7eBmYQF43U)u;i~b+XPnmWis$fLJ4n6EwA;%DA z>G0kl28;QhiNhswrts1A6@-)M7n`t@_kN~Y)WC<;YLHC89@cPrghVRkQE0lf-nG#m z{mc*O5tqBf6``Ri(jWPIS}0k)GOxm6t((}vPpUH?peD;J#lR^QB>7zB4J`=I*FBu0 z$aIXtJ~}H4yG(Gaj6a6b?QsF0*R1S^^bbxDdGN^kCD`Ue@nDE0fnh+-TIWCj00000 z2$K64XO^D9G)~(Er~;6iIIP+eDYrxqZiv}di*KamSo=&?ZNG4KA=N>2OpJdj|Fej$ zf>qsHoSX7FYxDNm1(pnvAZ1h2zH+Qc*SmEcScB{}NmmWy7KO8g5ajTflM#Vbc~?xq zTJyY;TW2`{$L@WrQoGHkZ=nt zcz@?wtg9yFJ47B_0i(vX-ejxncvp{Db%smexEtdX`2q#HU^eifPcH7B= z1VcBwAbQXM*qAkt8c`_#fP^AZa(P36qDy2?Q?7S4n3lE~=E|7#J90Q6h zxB5h(fBe2ARHGn7rO;p2bn3rP8oAEoM|$3Z+gjmO802h(dmac=l%t-g(C;1J#gsKc zzWlqHW4W_22=0YMh9&ZNmY40_qq3ikBai(h=&v0Q0~iJA5GSK6(u1q%ujd?PK^!ox zb=^j=>9H`DPhH0fXSP^YKXi#9Hx`t+oI8%CY^ha+bN5jc!Ul)0^}#Mkt24x6`!@e{ ztYYCK`a;HW@CbS8?cxvB5qGCy7)D+92Au^TmU?A5bfYCEQD0NPhY@JnjZOC3@4Nhd z4?ZP2eOQy}+|Zq`yLfb{6pwj0eU(`IsbN;FzwryNm-Rj3oTE*Tt84H_(IZ8Y*^3jw z!1`4GZ7Cq7%L2=Wiv?CbJ-+%mV@;{{>TNF52gR+H+O4_!5!ZoC0vKD>8kJO=Q{c8A z8m!}nWric(*qg0?A977ho5heC^?JcyN+tgIYB!u;E9t>9b;CiqSYXg%s&pl||X7_hi zKLsxtxDFdzM?A!+=j}=hDbD}MZH~ENbPo51&b8{66)1}JKy|BSMGD;VuD})%BzEeJ zB3MPqg=_&%<_)`_t)k4CFQ0<2r+FuWhb?EPh*muhUSQ@;$iO%N0000SU4SqE8cB~# z2sJkD)UMH>0Gk7lbUl1xNAzT?TKb2#*;I_s3}{T!>jDEYt|qDmO$66C#H>`Fe zfLwTSw;rJm!OQ>!Ov&AIMcI5#Plf!KxOo4@(QG(R_WZtoD{Zg1zpfgoUkX~6U~{HT zb4u;KeF$tGHed}xoOqLudtIXV@hWZsH6DT}kY|9b&mg!|D5zC_sA+)#XS0~c+y&Hz zet%zYsE)783Rj~J-GaZae1df zHnHg9c+;C-o1-fmTu&`~3y+(a|6cEI7`uz?K)El|;giiNuH{l%gtJUkoh~?v=5L)J zZ!V@qSf=~?g1bGr)oE|6cR#mPpu#%m#`n5xA zTS3u%1TEiIByDhkqd9<;f!{w|Zw1CB-nWrii6}=LgUcd1pE#)<6teZka?Um9*guds z0iq{fKuB&Sk0&E0K~V=0VHFLz>t3aLpY-yh8tH@PPL4t_IhzI0>>+1qie`yc#!Cte~}W1>wOzc-<5&# zb9c7(G?a52!Qi)+3S4k6vrNm)!I6KfwJy;Bir(`nT7draAp5*&kKl2!cB9$z5rdn? zNLc|eN3WzQxT1Ehz-lPmR=B7?XaBUx3eYhm52xt zrqD;|LQWW=$C9`By-@954L?}Es>?68!hiwfZUB!A00_tRz2MY$U;qFI)bhXp0001J zE`s_K_6)`k6C;VbgDz0D+~Hox;CxYV$X^$EX`$HxdyY9(y>mpfIG4v}2(@{!uON@X zr&_bU>W`c=a`M2Q1kylY03~`9wzU_M8qj>{2uW&vVi1A|ex;{S3sm_@dsCFA=dV($ zJ{5{~cM?LBw19LF29c%^yo#a4;Cyz!h!Nti)CO6^0qcRf+vrAI-;e@IEZF}BHl*eE zL`*~~k*w(VpHYQN>B7}ObE)peuSqdjoVUSeG6ug~Dze~OmQo{&0=p*SWC8REo{%Hx zV6Gr|qEpAM7R^(Z*$JQ&;lSM=_`CT#S9Rh>TiT0yr%nSA^0(#<`L>jl#E%CJ*AXYV z1I6B1vDPWGNIs9I%gd$UNSq{$W076*XzBwsY6om6H5ZcbkrRuB0`q1hIQ1(#pz9du zieI99&FG~H7wiUl;`2^|ZDY~I@uxPwH%3-AxSm?}7aun<{=MGZF?Sci=p~f(*BzOZ zfb~z^utPORRrAok-Z1$pWEeNw3UHRS!jv#c(?fV9P+QBLJAChOCGc}6!U+q-=&^zE z@H}8R68%F3d&7V;J>D**{f^n|ibO6I-}XeNCke$eeNSbtyV9)HxGC+VeQTHG?1>P; z>)5`^?uXd>5X~4`%*=b~QM|P-=;>DPm`LpV>X9)b$PhzvFX$Yw^mA_GPU6?tp7G|L z$!yzeYkIp`yjah*pH@K@`0@s>kvAaboJDg@l2#1WpEqv$82z!G4nibtaFI38F6@-v z#}B2Gv#ceU#6tuu9b+#~+Y&)xeaG%j{q27IrX4X1GhYAT);V(JVf*qB=eYTK=jA!) z)V7`cH(>k6{_G5PjemC?WFHq@T^GFwq1W>A83dh4PvC2@%CLr(A?4ZK*^5!3&+kgN7XP6M+Aw`-@f@8Q7PKd72BA6Axo!eIh zg65b}(LNtipPS^E9igwAJEPExqu(bZC;PcF1xiSrb3a8A_0CGJqy)a0Ac-*C3TJjD z%8fJX6s32lBSw)fB7<0xTEYpK77M`XN5UVg38u4^5)sw2y^cPMV;Hl^{MyV#O8$s> zT83knn<8J`F!}!{yUIR2>dnRD2WEuB&Mm;Ux$-~a8R;F)%OI2I!S&vTcN8XLvsv_g7)fGIPvP?{0)kA(S*yK>-}IWRSUfCfB_o}2zsmi|RJ zFgYx|x%O^oyv<(WzEb`z)H06)y5_KvBC0&Gz%Klxvc?uFh&X!U*dNwvyj^x1C1kth zSYmxjBjH=psPs?FUH-&!7)-?~uBO63s(75uQo`S5 zCuW)JHY0tqTuZff=FKKwopTd&garY&UzegtM>Xru+Zkq7fD|C)u%T+50t54KKbXz< z-br2aLo~sGj~2`#;Q>uN6Q!YQ-BN(2w;}g8-OO!cCqhI*bWTMaPxnf4Cp36&yY3=^ z=lYwbhJ8v+eCm(ate0S1*8W=izRW#J6+A4g<|JkCuGrQ>!F;&PpNt2{CROg!@Idy| z@3owIINIael|vcw%CoA-Q|R(|H@jDIrQWo_=F4CpK*m~s7f^y`4y~yG>&lNgy8)w> z%#HC&zW@2VLZ``NDlwl7YN1wAgB|6Lw-G@9XGmzBvb^NaCo^hj1Nh3P8(NPlU~AdbSmMM-z!ZNq3&0$+Eu)hCG!_$Q5NMmvIIlTtkH%3Z zMvU{kh5f_bFl;4wiY3G+Y2 zd=A0=;1g6*Ma7EUHs_b8uoBJ>D|I43#zf(bj0<=XGrbf~L1hrYiOld5P(J^(*xY;1 z&XW&~v(k-xQgj!sz0kTnL$7C4p=u*`0RI({0h$`q4;-XjpY`*ueO$p#Mx)BxOoL`) zHD>aj*;M%-`t%-WZr^W>q8lmiG~^QtWRl=he_Dp#>G`f4DFz(q#BK)!dL1u{W+hsq zfdCCqyGhMm6I%@7iYkY*y3(n}x*E!9=n-oN-?twXVl0=~`^YNKAX`>&9uHPMHu1Y% z6VdY;pH7>T3I=ZtK-Du<-4(_rEn6#+ySGFniL_JNp6jQ|zq4=|e>#$1GGb3kOfI}Z z+e-Iy{__$()M#*%Ia?Q_w-k6fmYD}HVTeLHUD-x3AWKti_T&Fv6S2=_%HS{&=&&YpRjk~s!{<7j}twQZ{|4^+b{HXAnk9l?GU5c&e z;bLX~Mge+H-G2>psPs?FUH-%;(cYn$mrBit7@RFjiRF*fut>{bjSc?aCV;eerNqN; zg3Km=WLw}oEXh1p(J*r8&4Iko`EjER1dx%FGrSfc6J~Yx-2qChVpGV&e4xMPrOab; zedb9IWNQiP@H~jmLK34cc-)2k=J?hqUcLj`1yQa$oYyvk<*Glrsj~2FO1>im9uS>m z%N^YzDDP1UTQYB<1-{X*)A!cQE(DkKgW#s&Bb475_hYtquh-H6(abQl@38lHYx7O6 zqd=`vWU^~&*4*vE-w@sQt0`LL3mJP-K;FAp)a zG-zhtItybHUmZb{_(+=MxHgs^sK5i47kAW+EY;ZpToXZ#mNT^a=L_h`+EL?VT z_?{H;o~8GBS+?_}zGPmbx-nLrZ{fXhw{+}lQE4IRO6ioU>0>V2e{WtS+BN#LpDF^+ z=6J6kJE}mS;?fjJ)V&jkps;|9#p?%^Oxd{wRJrk9^dbvZuhD-Q5?E+iM!?1pf%~&% z^U5f05e>$YuxQq3B^GrYc;3?$+;zaq7GGN2!$!07h!Nx3OL6R)@1HkO9=~U6HzPXS ztd?KF8WH&;bWo80KY}!EPF<4748W{jc zSFaicOprw2-wU$a@R#Eh)|(v|@9p5L0nx;ICMkd}RXJI_5RSWYNfzvTbHK{17Hk$2 z-mqJN}LneAv#QA5JIDWk|_=|2iR%X9U&+l#F8_}QFh_cHWnJKj{P<;GTB@s68o)t%; zA4WU`{aj!Rhhdz`q(Jd7ULCD%LIXs|KT-bG%V2=boM&D-n7l?+B{I`ZR6-SnHFM#K zY9rCOwykZ>+*~!iP#E71OR~SRuE{;fE_v=CgE4j3173;$c+|&#%$mf_Ch(p3Ek<7f zLT#bEf=B97TM0*mP%R)r6nTq80BGH#T3z%xTa17J05-R4HjHTHNsF7$t9P1K_Z~_* z%{A9{lXlxpDmv#H4zx;u z*Yo{VMhzbeaK}BS#|sw(Dxb?>_GAb~(a#uqh$gl*MQuWU3OFmD+p4IJ`IrtyGpBm- zsAAANCN2`$hCdKfX&6p2iq@Fc8@L*IQKpHH=N#M~=Q)4?w*z{wTG#+2z!0L+SI;Q! zCiNc>k?|UMQt_4+gAS1IY@mr|11+?cJ#$E%de?-TAR<|w)W~lkNG?w6+!s*i{RbSh zP@Lq}SeLXM^=t8uEw^YXwj}yS!b{yi{RW0X)H$BuOc#2_G4HG2Ge4PTPfd?J9ZqLa zqb$#@MPP~8H@Eb2R@5L$nLHbc5izca*#9syFt8U|4X38%8d*wg5!1h-E-*$b9yz76 zs6?F(qGk1~B+TLV)9YzWQd)^u_9eLJ--hDIh6jt8QYznj3`ZKXINOF$C@zfuf&40A z5BVqle~D?QvHhG%f1EGWzY@~@yR>#w@uYG8q`ej6q2OZxy(jL!hPl*tNe1gRs#PQ6 zEfebgd99Nqo3j6-fVwEk$^(?WZ8!RuL>{_!uM?lHPMlDffJ)9xnJRM?Q^;tOQyTwo zoHP3M+rv;z2{#h3{J%eNro9;mm6mOeHzjI%elL}nQ>Rt=O%__ITP!~Eo^bVAY3v#Q zPc+zyNq7X@z&PHp$R*oFoW#>q^6zVs3r@e?_1P`B8N7LW5pTRXaacHY`vo~?`6(L7 z>#Pay4bmt?Yx!QVchoOmHH;cWh6R}2eZzHKzC^QH9~HnO(F zIplv4(hwcQ7VzqO$eQQZ;$mt}xs_`UE65n~9)XlR#|>PQP8>v0Vuj7rZvLMcd7d-k zos#1P=o!2#XkkKxCO_k8wgvu(Rt&T&cJmT|J^-`7 z4Xf9IGmZ`MBMILj25n*3bE~ghk4HpLq~;o>vx`a^g=}=mdHP8;YS#@%My6n7CN{|J zHwTgGCt z`+1T}1jyCd1Fb)+?iN3@|G$&>qT6omkr(J$nm#M#o7qM>%Egc(pLRQv+`<`H(>z_} zn;l{&9sh5UuPypXYpGTeXqtrlowcJeU10^yMq$X$x`VJMa#RZ^Pbm=|N4P79HTki6;IyoinsO&hOG zGkb$<|6Oe+jF~65^V*Rg62Dr4*8hZ|hfi80&WBizag*o66N+qQ#BH<7ppf)SY5g6~ zlh=`StVegCvfIOnfXi9A6K*7FsNybqDwZTUoN_bz1b^-nSJu&64|vUNln)*J+hw3v z)|5DyHU@g1w--|?WnkRsFeLk1>c;L?2PyY*w3XrS^iOma%f;?#|G?&$YDS-mRMg$m zYou3%ZhtZm+Qh;gtJ&Y@a>=n=P-{;jz}C1OP^>&aml6JouTW| z@ih7Y`Usv{04?!GGWP8cfOQPLTv#*)ZweZz9fy852Ju+>&>W*wEy#7sqB|e{T~`)I zs_aUMG4=F>I?;ylp$Dr4+HUK?1YN^K=&9%r8?KrAmE+gYW!=9iXsDuhEfd<|jEi^m z?(vanSr#3xQ|dDCW#oZ%0FI(%Iv}&=rMF1}m`*vuJvfz!Ty|m?9+!lE480X@05toP zS>M2$;ArJ!2I)N!i*8uk-&k2y`vOW)c#e zlCKp)?CD>w7E0TDnDM#mw_&|$yMln-=IRcTb2D3-IVv-xwd7qNIB=ldrw$l+j@reK zg6Qby0l4b=m-_sI%xhK#5sNX0=HD}S)XV`dkxL9;gO`mbRn=OasO5^$v^nig!Igpj zw!n3A(a|?@w2M>jIlf71QrlEn_u6X$e4Urqd8_X0)^JL6f;+8ysfFEhSiYlR$bKv? z$I@d+WGt6Mx-*X0is&CMV7iIF{$8&HK%qB7e^zSUl~o=RBU_W? z8|nf0!Ln=VMSRVbTX0rJ&LxFrSV^)2agR^i&+SMYCrI(%)qC-s$~ZchF{% zn!Z+910nX&SVwDK(PG}GFA~eMx_yL}yx0Q^G)#!d|9H$wDyO*Rc!eERRb&?kVx@%4 zIn{B3ImqUevbkyUF)DbdEv|X;Hka0{=t(m)QI&GIbXE0sI7}m-Eu%-0zYd8c9 zUhmnF4fN_H#JeU$0s`^QxV2}OfclO~T>~+ix`f3U$4bl?kKAp35!<)09gFK0`aEH1 zGJ1iBmYi@ZkOlAmd!S?PzZ;dH8o zMc{8q*DK|_%E6#2_5&+fWVyVirV%o=qVSi!!le`~u0ipxz3|9M552#OY*n*#)A;FI z!hOTfPKjAulHfGv3vX+mjWJq2{jhniwQeM;5~r}NJ#T7d$|3qAjY|FBwe!2R@I z93FLE_~%r!0<)X3Wwj~>74C_C3-{UxV`HH3T$GB{XD-(28C)hMBG(Y}RePU8-SULc zIB%_naTgz*H;n_gI-=NESG4w#|Gaie~(^Ehx z|T&;`3wTm-EYKVY8=KC(2VUoExb)?&z z(w`T(Iu<3HG{`-4Aq#Lvm?&hJKEo zK<-P(gwn^RgOv1TgJqkGCtwj7n_A8(>q`|P=3ZRCg3+X8hE3J*5R7wjtZHu5rL+3C z%&Kz@Y(OUH0|-Dy1oz<~P@QC3BwOS5$}$TD+zb#0o?|4^d!a^pE%rj*Hat98?rsc| z&$ojphd)2mHQSbSH+$uND>>X0Iv@SJQ-tA7Z>6G=&8exCe3&#B#P29Y*~>K_rGWQWjVvVn|*jtKb7(( zW>|Q7k-bjv^s4()}+jp?N+ng+Nk`PbDzLla%nnY^@_$oP)1Raw%b-PiGP+XFwoedMj(bJbdAG z)=4~MRSnBI08!INzWurO60f1L_&~Z)#qJ$Ih{Gi+py{abl^UZvfe2Ut&m}FbgsEYh zzTmL0bScBQ*MF+A~FXOj}2)leqBDoGYp$-Naj)C|TUKN{@uJk);14M(C zPkCwOGvk=6?r&1MiVhSOemMHg3CC;HZc7}yD7WnQIw=Hx z&U4es65pa1M>?ekTA(Hx`!FNFXy1eE4AA?Y+|3E^Z3f$Pz~ygwy4+Qvj+sSpbCUh$ zi2J;+j*yh{`j=fFDjqEK9piS)z!8zpSwxe`+n7U&-FVgxsJhkPoDf{Te)Q`=mUb!4V~!0n6?em&iJTi>#ZGTn8ue5{RWR1zr42LG4ni}T~hOr zqf&`U7Y7TYBS0jq3wer+$;Yk>7EWblCkiHEHg%{CQ7r~Nm$+SztoKX}lc88%&YKDz zB5DSR;6%43FM+YS5qdZvULU4BUvQkS3RRtH@Dd&3%NEm;%^P$dZP{KKE9P6Am?x6P==~c)_gbOf*c3Xh14|nOD}MrPmfbnwwH0nj+ zDy~k8Y9r%#!H5J?Wg;kX)BpfCDfltw2ol8qyYFHeLCYQNRXmD{6>VX=z*QO35?VEi z1EUs>TXAL{pCDx;DaqfR44hYtR0NOup1Y-28?8Va=pfHQ`LnBE7G2xx=_7gnZ&}!h z+&l@z5)iTOYdQ6sDQYMs4xynxSe^U-=j5l++IUz3mxKi_NuNt#%&Y&&+3|b{U2+b$ zKqOw{O&E2jyo1@3Mp!mBtD%>jusb`DCj-AS6M~2%6ns2ly1X^X=j3%X|zcr-(ZsbRabaEu@_^2|astTl4%X>+(s=sr2H2qGeSb-0QM@RAq zi<B3GiS@i{JYT>vxAm!gg|~ zNipL2ibIZjj&krkUH4AknnN@D%kScy6MuP$t>LUdvkjP;1#LCiwrL({?+E?5GeExa z&r9-5tqySh=sCQ(lO8?gqhJ>~L6oyt17Bvh;o8h66os5nxuWMwW38WX9D7!n069B} z`qV?J69-D~LX_s{jryEH_PIm5vmKk4YtRRc1yry^ZW|iR%*w3(jXZh@!sYdK7g=@logv0h=geG1QW8wCEySehLQh03<9 z1;r0|^>IL-;dgmeC`ht>U9$r8G+#3Tr_<)saDWVB*4|Z!6V|LP#967+0VFSK(km*} zk>$Uicn0&PQ0$3tmaRDRW#f^HZFI-v)R8C!b}+VQDj8r2a@}%SGp;|p3k(z6QCpTV@YKg`-* ziuglvON9CVc9|V4IPa!+rM6p7=O0m6o|^N4^=0B%c{5ysIQxZELnTYPT(PScVhcAh zNb=vN#39^6L58`6JzXP+|3CUAQwlq32T^@Hm|AXahetM@o~*sd68GrtZ%yG@DE>$Q zExW`SngM&pf{GXZCFjQc9TuOtbTNSiR7o|%?-`&&nQ+NMG7|SO7rfkNgp*j?*#w%7KnM)0ub! zb@@@On){E5rBG^&^jg`WJ@d{OgxU@kYljX1CoUc_kp2N8_QP4~r{(`=nyKP9Y$qxz zDq5Bo%udnUvraTnAcU!2o9>9=loLgD7`&IQe>I2_>`pIcIZ zD{vCgHv8)WxW*?_PUeFwV5b{4@S45$-CN&vMt@5ei_QXAh3@&BN&d^O=qA?^kc@al z_Z-n>MbQ=wr8&dE&V&UGH$X%7OV^7@^R*~3=l=!Fd+ z&-HVQI3<43f$st9KbmkV3UU`O4wvi!^q|fvdzc_8hU{Gzgn-p^IWd$gKb70u9374p zhDgy`BDW_i#tXX9_RLOo#xa(4eet)}(oVbwb-4hoAwGxKn25_Y1d@DF?+f7Es6zQ8 zxk^PrJ&?O{jEYktHuu7u4kzQCUXjq)5w+pm#lS-w8a8kJg?%^uTR>wcMd^$>1sQ>@ zVIBGg5U^E>ututbWTJq;BpkICG8bYfv+;ToApg8$t1tE*OkM{X<3 zgf@3y#gPkO`9C)WrN9YmJ^(>%{Eu}|4I{}Ca$=JQeH}Rp+R(WVuXR^Gn}P-yP#Y@? zspJTg9vR_E>em#9nzxrvHZ;Ce#p4GU>UEyA5XD(T1U{CC| z+@#z%iYst;`@S|khj9j9vY=azLjj$ii-O2fzUJG7TQ}k3iRrN;3wv%TD-(#I4vje` zxK+2QF9P%}Cu9|9USvz1#qkdNL;Z2m|Hz~Z8MTw5Ss167+eqHND8b{V<*VrPv|+r)K^(~$;gYlK(4-Of$$HxZB=iyeHVZ-?+uJJY zIEa6Y%Y0B-+*EEyH|A>xxDA8p7V3n^W8XHbdKGs$0a=LR)5JsLjD!P7&awho) zLKWeu>WS+F#q8m2D@v{-G7%cl&~1!a)n32kci=%y!@N=_`2Z~UYP8}p>(%mNPyhe` z9|qG?zymMYGM7|s{}{XiySMtd`b${%eBnw7Q((L1-mH*Uwy_BO-r;fsyGu~K=l)q= z9>?f4vW9oi9*{*mLVn$!fO9mB1vXQ*OyMroa7I$g>0vlbHhw<0w9rA*PoLdS#ICPwiUc zu({V$%xJlz%Lt5lMI{H2YC@a0K&}u+M@v5Z_`0E9A5Ev43Qze5yrOB26Gydc zZH-MTN5ikD^(X*S`z~qjRRJnoX01~0r+9^d)CL-YsX~4G^)gUIyWpYWDD9Q&y6~_AShN_K3><-*z zjv@2|=-RO(tSJ9;owT%`cbuW>RckaZ(_F8k9k*yr^&dptnyGK0lAZRc-j!?YcH6b;h*dM;_t#lab8AWcXdC zmL4&L0`S0LD9i`el?z%SDhC-8WF^axUzP(>x+l4ABjp^3?^`QB9y^UTh;A338KcWO zEq=?K2wKCous62S;1lJ%*bjQUH*#Up1iSzjhIf@cwM|8$qe{Dmlae3Jd=h!@vsO%9 z*Af}o^Er}sot8}(b}(&|6$^k{loo3YDEQ^7J%np|H4RvRH|3e)YRzaixN1xuYHokP zwOf6fE*bKbj>pnA47n9+YqtuYjJymK{~2TA9Sa)**-#jh)A;!;KMS;x!LfW~;fIu_ z!5usLC;MumlEi?ASjO@^SX(ABTUOPh&!X**zq`6>P}CSdAjpIASl?MmTq-C9)Mz8U zI`_abyLtcHO--e7JLeZs%4w{ardel(UZR|f=<{Fu%?>7bjDu(ha75&SRO`?O|6IBm zjkq!UT3EjG|Jr4Cz1JdtunkW4sL5cx8@Z35U(b@!Cv)_wF1=5ZSKon1fKXlH zLh-p9EY^c(Kby40qb@n_l)mcdFq^izv>VdmPP4K>`=`OeiOtpetfG(r00DYiES&UF zrx&P)uf=g*jf+O8so#G4HLr%#@z<8Q3+FVao9yZG2iL8b%N#@VSJjn(`Y_~n=I%@p z_GT(B3jQJ>j_Ecc*`9s2(7&q&c`mb|g4bFT7$3!PUX6=Jx1>%bWSjumanKIk_w&__ zOn3g)`1}~6W_2eag886cy2<}{9;9%CTubyr)K3%Md_ybMfali|qc5;SdzeQTN?vVy z7#dahinbJ2tLvLtqN@-GLvN5+x)2wwE0osVR)o7*ZL!o=Sa`}7asFO+W7&6V&;U_qC+uoXLV z_#jarH}=cVJq8Uc-l&lhzekG|yoLp-Wh(d;kCLK4<;&(0KwUjM#gkp?zhC&<8UV=o z!1(fS6^FDjM8ZMb5U?lB}(EzLFVvw+<;vLmdeH^>n?ca8F(WW~r=l;=UZ zf9rkTUYKO6Ixu#tC#<lSbRhujXxP-^eTFMyRlI7NY0^ zX@3iTAKL4=5WoNc07{$fn{{z?F4rEc2pZl<2vdEk%a5ZL2;%I`_4VgA?d`OMcm99>|Fp!BixZr#*U3P5>g7LtKw3PIrJu)gx9|OO(|bl(9%OG zEtbUk`OY_}*(sj*HKkz~Z-ycEGYpWjOub*Erj=c3pC|;Q<={d$ERc5yEc!2Lt@4nZR!6(7p4LTrbx!^2Pj60YY_wR%PvxG~ywpw-W%cGC6CEG*%1 zEUI{J2OPr)gS2Q`d^EOMbbQzf4*|$1>F3Z(T0_eF{`9u~8P+qlsdK1rf18Jkbs!YU zmkoyh9+jtHF6jc$`UrwV2!G$?pNPoi;T*ow^?y2}2>U7rtSh{g81WggY7lD<=gt`; zk0)QysO2kiW|vgT&LG#W=af=zge~^z5|YCeaiRBrmLu0vHMoS=zW4Lzva{CcZRZ95 z%vp-MhD#D8x(gzUcn)f9OaOUcdIVF*B3qMbi|x_9Q|BxvU?9jT<1qCz>Y1nYI)X&F ziqx7;B4Om3Q7nRK1DikP6me`vn7$`axGsS^aUQ@<#n0|TT9rt6xHNZKtb(RZF7KOv zW4ma@%3pWDcV<}kGIqvcsbF`iP?_uV06?<5d1EXK(K~ zPMmw`9Q8nrl^b?iT8t;{nPd4rgg)u@|J=YXnW=27grd+(O-!^;RMcwq27!RrngEZ= zHO*j~pT40U{|MsPAFk`7M(=s7@yq!)9SpbZ<)IBNsvQu0_1d=@=lVDAC$66Ehitsm zEL$;QH55$&QuPmy8JKYRbO|+Y-a6a6v1D~~99#u@IInrjf8ts+BzdP{`rWEE6S}u* z&{F6K?_q@qi;Y)zSBo@n4U+-}y+#FYdY{wKL}t350Aspo$uS&I9h%t%1T_k|6MR9! zrubq7BzCI!$HqC~=+*+f@GZdUD$FmXpIujmt0VO5j409>7n7IT;d3F=~7rSR5b~`jBOe zCJp-^rUTb=u#hi|7g4GUD zmu8D*Isr&xvYgC>>l;J<0+kI`*^+=0$08!L=%2U%es|2kx31pAco!^?7rLi5p5dVh}>vd_YX zU9d?oeE7e^>OXCtj%>;a6_&~^Giin3D;7_ZKWh?(mDtkO>~2QUeV_oelK=z0vxgu6 zujbcpTahYs6r|{WktP9Z#zx!Bi0FeAqZM|K!@?NXiA}9(A`k7w0mUJ{BQKi@19m?~ zfzQd`4a&B(GS)}YDOiZ6m9x9IO!Qe1c)W+9YzQ|(hfnyeIhlfI+0MF9{9}pGeK*QhVaMS@=Y>`W@-WLqch!Y99OuEc3830Y~5p4Ex2c{9! zhOjn6zW3g|q9K12t(o_Cch}>jS2dTWQ59alQ>|ct6w=0$oRM`!|AxDlrO~&{a%QE4 z%Nb2X{>%41}ClXkCf&RH36Aa>F;0w8Xh(8?kZKVxak z+l7+uTRxc9tE$OP5IZV$@p;55ZKarQPzztY?>`k`#bn-!z(sxq1D;(WFCgT>(qn2j zh8}w_&F_3-04!~SU)^#FAD!Xb?stoEf1asVv#uHFLPD%s{sam<>e3Pc=d9m8w;}pF8FMirmR19dlEw|PJ#+H}m z2cLLs^&zZKb^x;q-%)FHRN&54RgTW;Ci^|P;owMYEza&M(J;`( zRTq-E_xgL(Mq();otGE(r z|3fV@^7?FJaAT}WLUhpV+PM&h@YES-4!yZiS51Ghp9s}H%~FLiE=9_lRY)3e0r2b{ zoDf;(p9tzfeyj!f5V+m)>0Slwq|!jGw)tz3b&P$aGUu{4-Hu=pzZs;sO))2X^Hfai z6a$=ZFiX5Jm!~O+n0~5@{n*_=)w^KC*3Xqbh%tV^tpben==an%Q0rTJLcmoc$3$Uy#A~eUnP|Iqg{VNB(+9NJdpiPlB-8 zC(rCfB$rk{iJ$lEJ2v{d`LursVcpIqi7%LAEz{ZgbNPtaCWJ9w^jctD`jU3Vd3Sp>j`2G4u5I!#T|&;h6-N2IPDGJE z{Epz0L94mKGEi`Pjx^B%=>Seg9SMI^17IfQW&i@I0pjszfa-Z0`2Z3G z000v#0bZ->xDGIG{a|U6uHsVxwg;l}ML;XBbqTt}X%Ib?eFMh1!C9rZ+|Z%B%1K-5 zexb-xxMUV~SaXw9A;x*Eu;+Qx+D9PRCVYUIPgYno$2pluuhKE&uYQr5PvAreCBl8X zl}M(!hmJan9RDndG}dPqJK{4Wvd7~)3!a8@q^lv70k z#p~F}MiQ=_QsoDTXlQU7r(lhXt{fMtdL z?1m2|3{0hgmY9^RI}r6DlXx_cwphwrQ@`Syg2Ssj)r%%_7(n~F^AIt;C{3tVl zxmi&KKXRQ#_L?)u*Y{ z1w?4|NmG?W#P9R{5@*NVwBt5uPWN#S_X5s|@Ok>=0MpLaws^dFwHn5>e{)>@d9a*^ zH1ikO(c55t&8wSa)$HvVp%7g%dK$38?B zF_=OGPQ-Hz>K&;sF?EPv(+H_?56#{4!*nzB;r*gT)VWx76}|(i@pU8`lbe%w_;_3jb1$b~iB7Byxyv1f@gyT0C zQ0ON`pnR)mDagl^=xOZeTYTo&1*G zRy(2p`QTYjQDPet1uzk_6_|fvBuMpxa*$@1so_F|3C#2XQo=63jteeTqiUW727JRviY00C(x0000b z{R_CnB(Sf4a5KrsYCZ>rg3Ujr8C-oBuw)ayWjmn}iDi@-PFQ}CR_=UzU+^qf4Zsst zEtt$A@^sol3@B!W@%<7^KG>?BMS^kPumACP>O zsOPyJ%5-o43PD&aIX=T!Rvac(L`CU6$vy-!qxvNGbbrpc7wYY7qGCMPMUEr1o~WwqJ(x416aPnVwI~|w@9L~GI!>=5$dsxLH3VmSlIjcp?mOt1Zj}1w zPLftv8tr{q>R$_?j@CgR+^l=dwNQV{cJ)*)(6`=%mZim3kH#VdkKxOw<3hHq<~PcE z7)&W}w#6VR9t4CU)4$$AYfTJ^er`zoFU37fxS30Q983;;Y(lk7|FG5(ngWPQq0a`b zR8s|%Fm4Z^e{E{^p zUznWi*qMqBadzZL@-BtdNp4P`Zzq3``onOl@;>neq!MKh=DZmdZsA|`feo4CtKRR! z!N&`m?TRYR%Ba(*q&6|~6ON5XgyHt`B_b98)!JOeU(zw>{D8E~t+we#o>q)Yy zw49JL|7tc|y}$a$La7B)@T)F&S(3%y)islP3ah9~jkU}a zmf!#w4cUMI01S6++1FG#`L#3}BLQPt?`JPU+ZwQBzsPPj8+-$~(r0@z(Wew`NC#)f zwiuYv0B9WisvLneaXbIvni6Mr>+wre zV>ZVpDuj}rma;GF?j9T1*Rax=)`bu(h_KO_&V7_kO$WuX#4wvU@(%>EKKQao^GfEk zL>uzEMMlqIg8TgogrA-u+ge^*0?1f+Hsbh=`LUQn7&8@FguL3_xkVn-Bn0B74m@G4 zM~I9FE)9aJa|7G#saoLb?Jm69Q#Jf<{X!}z3_*96zZZt)z~&v6W08i0)B1n`&tzYT z%$F<|BZ}7g^(_N6Ao@uNG#>3+C;V(JCsOj*zvmwh(2NHLt*5KL^}NwCv*vz1n0r zQ-raC0eHkkViaF^jwU^I_0!VFvkuoen0UC=udDZ)qD-*N19I8c65W zb};tO)c8$GHI=s&u6>>;ZkACTT%?fR{H~y-7xJ{m_cVI5n-jxb;W?ub1I`p0#edTu z#i{d)=4G4>)Zo9!*zQq+uPVR>p8{D}MeY(fGh7>@aA&XwFfF$O?Y7 z+%qJR>9iQrUMZ0fI4i)9$nI)aIkdFKjNihB^BH|mfVb00ja5j$rtn!wmnp87n`o7d z-{@WriGJbJSJ%p1w`i&A2K&|TX|pCRl5)9}M4=!)+p-wFn&&GYYvXScK}uK8&xfPiPqfm`2RkB~E@4ay~?R)GcTB zyrGDpRd^kUIlZQMAJ_k&3)S~hiePUi?h+fgOoj~B#~T~rj>QI^b8D%Unkh+79{4vo z#Z;>Rs0E#PO~b|x;wX>bA?S43R2IG~JQVGtDN&W|>fn#>Z<2X5N<=C%=~%_8EZUhM z(5XL0Lx)pX2$nInX&Y_Mk1RY}%+Z_9D7IjI0L9#+{nLPNKIM_*{+>S>4A3yJJAFBE zMtQ^g8Muzm=fmf4UsyIeIsvE#>dVMGTD#Inm`-?teVA^L(6;&}O?_AKuQe(k@%nuy z_$|v9c*lsv_Ph$RAC)5~~sk(Iwjx|&uA+Fo~sEozM^s&ApdbAj{JOxjIYm-_Z= z*Cpm2g6h>*F@63jiS~5;^(9;J=*AcHZq{k+<*Rmg-! zkY@nHx_P{HxQ#s@7`1-ja-Vu@t%{vam^9I!`Oon<mJ)yaK*xcAH_7w3mOdDi~8SRr2f z50=7mu0+6*fQPzv7L6z@i%LMrHCbW74hNmfkyLUfak>~Fk`ITXj(+lKRUS9+k~!{c zNZrV=*?>%^JR94C#>HcaLEsH9P3|Wh}MHroSHmPIwCL=&;WJ>H+@ppP#(;L4X?3A8li`A#V zf}zx4^OclLDg5A$C|jQWVutI_wmov*fw`db;$MDT{*jJ?WtgnSavaw<6yH)92Num>Uhsw@mklBWEi~DDpEzESgTnnk zRu%#@n*l(1v;$5_OgZW$)+idy+2ac}W%~_|Qx0p_w)4C4JCTg-Ll^GCc{*vK1r&~+Gy>{1uvmxBPtcOwmi?ZnMVLUA-shk5L+LFU$8KNzvoN>LMZOd~`XwobloI1@ zd|!X_)Kq%u+5UVDT`OXf=<8tmq&-X=gI8m1Iv{tt=r1vdpKXK4U$6ihXA2fY*%FFO zh!g{Oy`+^R`1q3(7R0$0oHEtCkJK2YLFld1n~>l4&T_j}>-;3qCH$yz)eJY>z!OBV zOA5ofraj7)Aiy#Cg(406NtJ$B`9X_DJ?!aH>UZj~HaqshPL$8WC(s{49bTq-TO#C| z9-aYlfEj!u46rDSIZG|P`T-gQ41tuC%}WIWb$wn6QocNJWB!kMd+_vG9dLB3Sse+Q zBa!#a85BkY07dNm*75!8P=YnJrADS8zf4^oWVlK&`7?pvmb{Y+^H_-9Yrx2e2jl;x?YfOAy6>H7bbfL>0&vjRoLJxhFPB-Zu>vCm zyLnv%clXgGYr0mN5KAGN4^=qq2=jMkXC9hth4`O01ocyohn0ad%3Se5_oSyl&tkDC z$`OAUGWJ>`z94d#b4N&gZ+==l{gV|b(i|84_+|6ouuixKWQ2&1CE@4+0oJ}Hdm2^s zO#xT@^8O`bXGfKJ?^g;<$9h%|gOb<&WKcx7)oOV{EtyQD@Oeaj07rOkR$^Xzbq><* zZO9#5Ik??GApT15wF0mJ#BxXz5NW#!WFO}fE5 zv6|liY!v3Y%ilX}Sgn`p!bN_$1}KDDdeHd~`&tkcaHC$El%>C4KT~s*6H_*fn(8^X zb5^&7GY>Nc6WlohzUtzuH}ZFLzd7O&F9bTqmBCkTp+Y$S)wx8J-gL3;vu7>u&Nsr@ zk#WvPTYy&G!)kM#6Q1eNCFIa^CCF;5Yc!>}0pdwG0001E^G&~zQyo^g(E-}IpkvqF zHQN@h=`Z3C6VeFg`zgsvIyfahOz3_wfjilWfMw?ooPBW2hrLQC@mb`4Kk!x@3RCjTDkfWmagfzABu*KKy-Q> zhjjt=;&ZF*@6e#D!uPgg1kpI!GE+&XDGs0x#uwXDm`djU`@-;M+BGni()U!yl;-HX zC4&sa!=(C*A%sJVlm%@7>97YykmV*3jSf#fuHA%w_n zW!EI)C2kGBQN73e&j}LmR6cU6x`5IIZwWI30;}#Pj12i5r+jJqc2|`U0{?OaAXWCF zhic34*#xyA*O9Q!?ITuy&#Q9ocF5zPI*KNkl4Nuu(0pNQTa-dgcQ%YCo|p*fX_F7Q zlo6h#apYwtSq$6ECe|G;RK@&WvGBq|Z_uM7WrnU;Wqvd@tx zKmIvk--WR7e`nZE;ApT$q!hx)bh?v$ZQYx44gTb7!jH((n;FQ;O&cA?({}{?W`jyN znrrx0IS;Pm1^>slc3uRyZ(QaA+VrbFE?SOy`G;g4B1~P4j_I=s5J~h(FF4V93bV@@mzzss)S1+CjY)J66Z(e$;P-YsjGhd$iylRbE7*b7y<4w{)qm zYuRUv%Al@-)p(NRHCzit=1>6Y!+vaG`POOP-3>3!BZ_5s;{BgzCZ?fX0!X(Fg?O?f z5Z3=T%wzPN(#|~DjnUCuPP%83TO|RFmHkC&x1!hKwx1bBV)Nr@{Y{#fFxg@tJ30|P zJwg*&Ze+DSt}B1hxZtg2N2iAgFF}Ir5~E;u3PUE~WwwN+D3+r@9y9_zrlPPXTgu(_ ze8|F&T5aG^@5$N&Qibx`KdW?WU8KhaL2P?kY|n+ZXzLP9keoXo(?k|VM!H#Ku8qZ) jxmjKnM8DF1D+a2N%=TS4x61fK`weJY#gTiDVt@bu2nQ_ILo*tSh%`IU;gB-E|DB?2ly2Du;#bPrIduee7^V$R-GxV2M2(!!ZgGE9^I$! zCW{Zj0H!kl7`)v2Eb{~N*$wN_^CK8MsR;&o=7JNlNfW@6K=Q8!olg|{iy)a3k^9aS z%*q&!d-7vnvgbRX$$I}6Po#&UbMnK^KEGseD+maN{J>=ob*}qOJOLl!pD95SPbv3+ zi_JT~20zIEZoHlW>nUfNPXH706_HBt&hsJg1|0cI2jq6z*a|q?JQleDM}wscmEnk~BG+wb zkSk>`p*|?x8?3~dU_Nxt`Za-eZjW#1pC_-u(Ag8l#Wf;R87q$@yf>aLcIAitkTmvtB@~V!Hhtk6(c?U z|BE*?ydrPAzFd zH-el9HYR&TmE8ej1+10^0%MuW6y*MYGW>@Xr|b^seg8)*qe71#XZ`!I^!Pogsfa*Vun^@JDe0OxM)vu)-9c zrrue(bX}w4g7FIW>zRW^0;k^p6MXbX@k^5Z<7<}KK@{QX|KR83ihtMjdl}^GsI14+ zzW~awEkgYN*Iv?uTmQgnhyTC#XLlFN|H5mH)_43z$#*XRes5G}bkt6F+j2Zog~HeR z_m2=%pMbLe7(qgG^7MZJj3P{fd-1B>i2pGPNUZ)FNuBRymSpnmzIcUuxn}jos8Fe( zHda9q&7c352WvU?ZESO8{_{1fUo3_GHyFHp4~`E6k?p7u78rjiWdf- z?|%f=*DwnI*mN6$i(3N`1hJI8J?|%io7$e;{@ciZhu$!~IY{8le0hb3e+klmUQM#r zMLk}i%rt-yu=4Ti<*OzKLS`trbgB8XSG@m9J+)OdFD9#W&HVFT_JX_67b{gd)~lOj zh19Z9{>P)XzFXj4t+WwZ5doP%ZsEWA^_Et7MEZ;AeiuDl{Zh{`@Ku9=q08k3%KJi` zxH9>>e*soKkhU1_s=rqIUu8RPjBVv!h*K6U zrKpT=#X~M&)Og0e8+LlPG=H}yxy3EZZ5M{FB10`pH1 zA=IJ5&%@(SIL-S3T~O5kn=tB{;w0`t|F?@u)&lE$AZt8I3>?0y6F+`C;45< zX_obL47~Joyh%sWWkw+Ztxpjl8@B0(7%rD%f4o@l%W=Hm-q_Q3!+lxH9nb|!)@0U5 zvOL8MAuwGoo))Q}Q_>kJ5AauB-0Fo0gz-30P~$m!zG;UzG{0noB!r0ctET{3MtduM zLGt6wsV7x)$@iP0L9LjOW=lBYVrR^h3>%NyAGkV(di)cyePU$L<%sRc zbXRsU6`0#!V%t84xuE|LTA$Z{c{Ac@p<2!MS+jX(-;>+V?R2;lK}|#&j!wCaQIh#uyrb2S4QH()1c#paEsYR0ok1a{i7}j8LFPNFETQY<;)n^((x76 z_XTCHLA$n$)QB+M*nVbwZGU=nZ-l2R!Jz3}AOm)JpOv9tdDk!tHk3@S&hU5cR^PD>6#! zqO%k_=2muoITCefv&X<{Sv2NELXwIm27e5Px|1(j5ZkCdtwQ;J3grDG?IooF8Xhm) zIgc_mv4ZKTV)Ks4VPWt~5rWma3wYHGeVEA3onp`T2fm*#XdI?ePVRmnIC!-t49TKs z+TvOGh>n!;g%a>|>}qV(R-a*&F~nO!FWrCcSMRccnAnl#b}?onkVDhB_V9)5uV(~| z|C{naQjXRHu5}|>IK6>p<2UZ#1rD(YT zk%&CctMh)%X%MR_`0YESaeSSOcAY!6FJ+?RfX&TLvdi~B(Fz<-Bg*fSm#1rM5gSq~ z3YlMsdL)hhqGnSBK%(?4F+1Y5s*Q))-$?&`{CukciL@~h`4$b4=L%0PjBGx&6;~PFJUsi@4QOL5RqiH+eQWgvPI~^^&Fz5r7VKS(Qe{=7zjgV2S2D& z_;9p3gZ;obk(C9(xln0fq}`owJttEbH~HfBFZTe+;jf@8ME7gO>OIKNG-I1Lx1F12ccr!n+}qf5cBu7 z%Z5c{l?B&Ohkx*eP8WAXL5{uz@_{y$-sz9f{)GIx zlb{K14w*1%pUT}aJ$K6n-bT+Hs_Ad$z&Lf>2%YELCbVaDE&MYNrvrfR^V12}W=xbN zZ(Z8xvEQ%40W0d5M2Rn}lrIHWCEb(t9g=1gN|!5>y+ChMI$?ibbe7#clu9)>+qPKe ze4mpuW2#=b>PyIqwg=+N13sKJjrY15vEHB3k(onJtty0Bx<&u>sjQ& zRE$LQbvr%B4ZUR@_kr=Mf_nymfVU-nfm5fG>234@aJIzns>Z)w=<%6&=~Ti#uhK*6 zfbK}T!Ft;+&Er43e|bsPchkh$@K4DI0l`b*_>afF99u0%)VWxbRi@FO_;6ffL;^1? zwkDV619(vHvbzJo=O8wwmaxR3y?eo%4XQq#4<}&_W$JvJ$V20T-D(a7OF@M|P+Q;Y zsDpWZed3B*@R&~eESIHA=10; z19eoWd@l&BMl{U0IOyE|RdxNX6DwdHx?nLGPpK`wLkhBiHTQdlr7T|S4_?wQ+$qQb zf*I~rNWrLY_;ghzvm&Ca?_Z1NLVG^adw`5s(bQ%$L5_PMJr-7oX} z3zwG+14G9Z{hjYygS+jIzd08oah00Wcrivx=DpBZZ3SF5dQsCcR3ap_Xl}fJ-KeTa zC0346_sL|cX8#X1)xaR?bEpkMS_hn%GyozMw zyG0P@4cKYcz7;|mI3Ofzn?#7a=FZyjlhbJ+k07B|0mjy8acslEJ96Rph;Ta88wPmb&%LO%Ag*<0(j(}KgFUT-Bw^K;)9 z(AY5`&f8?I%B66Wx)wxvCLh5>QfuSL)^=N1^V-|cbEJK`1)eBW)bK{%Sc+hT!M6&9 zk%&0-M~t~7hmpra>bRHMGD?WMxqjepYKA62r z$kYhspW1QOryY*u3*D9mgo?*-aJ2vablZ)Fsm(@p1~f${iNY098#YfF0t_#qB?A&s zMtxU3eIY&%c`m@{fwz+XKt+yrD(o+xj7h-SRkkbEGyN<^EKeeE&=>B55uV=hYyG2+ z>;Qq`!bN*mM$!pyjgQNrPeN)!B%bgeYlIgymv(E=+u%c&V?vdeLBA6_R=c!x*qrwD ze%Rg^B|{vH6DcR1L-Xm_cq;y-z@*HIgU}9buLiUSAW&uL? zsvyKpPS=Ss&0ht*&gy>-q_t%BE3~_xt7!vzM0}t3n<(=s_;w2K5nHoS=A-e4Onx6} zfT_2Iyh*t<@SH7n&(_|yoJ-tW_6^*&m33*ZOMYI@%+fmJQ(yYs=^#c>TD|BybYxsH zNdKJ=-6(7I7E)(HTznHs(%ns*QF|&U9In`IgpZs1!u2(81azl zbZA2{go1GRm(==ixvZdkKjVcIK$ucUNhypG2|`u<_MZ^-n>J#1jjS(G5iZMei+c@` zw=ji0Cipe8Am0p9j`{dr!tUHAau3}%H%N(chU`U(Sg1cUHzOTcS-JVFGE;-vkX2Bb!sNrD z9B)KSvc|62kbJj3e(twQNcp;0|3~7`)Z%P|Bm_&+!o6-|wBt#Lb_9t*e-xx@4oDGP zTlIAbx<&b}m=lfO_fHs2-+qbvSBO>nf`fQ*4W`NcFdS0TNMnXgqIs`IL862h8Rk!i zJammA9qchFHur@JYBt*(1J4i`oY!=ux{VFway@hR?imI?GngH{P8_^_;}~OMZ>lD8 zboZpWV1CDtxVoaz&~dgs{4Ky+MoO-9MOmjO)uZ*z;$N0Jm%@qX)Bj%3g4IiA<*zbR zzKMiB6CMQNN1jFhQWyzOU&2zK^}kHRVS=f^MZEo@6XHM<dJwNb9li0IdeYgDqL+PsdecLN?ec|oFC{sO zYh;OQP^b@f8TlN<6Sdr4#J4(^)T%u2*fs@{;&tl@RvMQezJ@rxRAEaCd2 zD15=$@c_aRKb<$x;3^o|9%6UT-vV1?VeRhbpeSaK$>r*`QBcv^X${cmFFmhlkiHYP zufXie8@}(7%>jBnslo>%?xZ_+{Csz~iaz7Qz=_9^?-f7(RaB*`!0P$=QcnCMl(LF5 zj90tpZBt0Nj>!g75t>m>b3GV~=1k#z26Z&X;%&iK6TFhe@p(u+!doO!^|m_nk3JS# z4@q6qp7J_>B1;w8==e+dY|~TO*w(`_DZZSJ^>WGv4%<8O%n*7RT@6#7iUPhpL!AA= zi*gizF{6+acq7v&yH42)<>ofp=fTj#byjvLpPEZ@5Y}0(6XT}%F$UKt58+vTSTS{T z{ZmMSp)+{19=$mUGuxW0N`0*~k?P5vT$?|Za?k-%#&I;f({E7fy9~=S$4SVyPcyiVo7n&iPlMXshw;hF**=*;p$M$3^r098VmFNao^Y@PMFU()MPB_Vt2!F#~Gl| zWb2BF8!QS}5Ln=ps-0QSDIi*Y6=#9u>wHP;C+7lE!}`){`X@X5@{~&!OcZMHX}j&h^mxdEs}_Yn zJqXiKV*WOJm4Uwg9*0lDy@U4^TcBQ1ZpSIKbl97Wz$7y%t8?l*Vy+=|DysDxQ_|=n zS0p9q%xQ^|BmwRYF~-L#MOCH`D#oEjc|buBkaU zeAOK;I-%{0;d;Kv>^o*UvScjWmXygYY_0d3U8hnUh*he)=?dz{`aQOz;7N}*&gm7* zeUYE4962Y)1Od#D9{ z1eu{&w>xDTn3A@tV(zIyx8NZUdGt{tzc`5Oie}4d^o+U09AHzrJ&UKY&)uW0D5SN? zTj=o>z+Ok}0b#W{<&bs=ghCD*ijuFCG<70~^7T4&RDZYyji#25Y~MGsSjP|}n>E3` z5BtUzO2h<5iWa8U?bCa{oAx{0L*%+vgM?O1K_{i3^>iTGJvRODeEsfqZ%~NuWC)2@ z1u|L)T#EIX)94!c4OPA#)Izst@jzTN}VgSOpSW9JL=@wkV4r4;B&i8m2gw zk(BXlyN={D(#;3;x((hpqHBiGIqeaMMB6y(Zt|$wkP?d5TCs!fKiWk8XE1^F_8bx< zs}OY$rQ6JDH>!;@tW9%TQ?!j?yI&q9`?m12L%r(Suwq)bLlm;wHye~j>JEKG94~Xf z%2Y;^l5c`7v{NQ9fOjzO$Xc-O44FDO-9)ur-t2Vywzfq9`L= zu?o%H*|>0{hPSEtg53MMPkK03yW)@eB&Mh+u)Guf*e(ge*qoX zs8HSuGeX2DfkT$GKNWAuTB@XMFeKF#K#zAgtq#0q2pZG$2=QPmrf7TY%ZIB3iu|g? z5mhP>Ou?XT#Dgt^^$@g|cE7Rj$m4lZ!}tpDm0G49upj~^f5*^d!=9Uxx7>(>S~w-l z-|Z@`GSRhg6oEc;MI7e)1XgFV+vMb|0aFd!sq>*HmbA)u*6}J zCaYTSCgV(^KZ)Mn$9#3IEqpXwqAz=*#Bs0)m-tmcQw7s$&r8ACb^EUK4(a8dG~jrA z^%LO$f%m`?O_0a1TyGX&QD4G2VB>62%RSK9Uz>C;9KIG6+#{V!ja|L16fJ z8-=igPoI+mJ<=opOUY>xK(Mdy6S*jCjAZPm?rsZ4A6bO2((45np+uP|K9ZC!o8<&5 z^i!ZcYnNJlVJp7in{Q}{74G())TECHjt>y^+ljH%f7ZUzIdI-U#v6(gO_KNv;3b&; z?P0ozKzv$!RCCWJe4z4id}wa-b6m${ zzuuYm(u8+Xw%$}LXb&1v4zCwF&f02Z_S?QC7xO_tNM^RaG^%!n%!|Q`t0+h!cN?2yubqX!JrQMVxa*tqAyf%VkTVCq- z#4<1ARo;IQwbx?=0`|+g5aFx3-@zk<+Huytc_y+rM!>0WH{G3SxSPkvJUwakAh=C+ zRwjv|pg=tGZK!*OwB7T2H8AMY5o6^UR;FkoQhJ%vYAx8i{p4$t(|B7S&sUz%vfRd~ z(R$f?pEK&1je-@97#rX^a_c;%z{BL?hR`Z1p^N=Q!j_E(5C2?1S*6>#g+lFMrnBsJ z8EpOzY~>2>;-Us#vt>&)gvO+n!BUK#2K*^WAmpa2*c~FHQ+~oRy6GUc9!gTLu{yVP z@H0O?B8*;iv|0j$6HV*^oD@R6%o;z%@by(7ThJJ*>`*P`DJoOhpO>nfeTXYh4cjHT zW=Yd}aQxl!sd9Mapii2kO(IM9Ilc-nWTdnL$3r0y`jtd}_T)2?d{&e5-y$KdK}b9ASec$tS%W8uGov z&NSsTnQ~o=&TCW(JdVEA&!)UfEindTA3C!%^6M8-w;E2VVMpCg zMN_mfrNqg7AQPer5*eHt+QfM*q=lUBowoH0M)M-{Rm&&-g!XLLQh1jlfQz@k<3nc4 z^d<5;^#uWI-N_jk8+r2?Z`>rm&^962-#TMRvop-KM2+zqV<}UcN1yY8#=gw9JZp}8 z+0c2pROEfS+yROW9dzGb^1{rvi<>A(90FYgx z!LFG~c7R*hz_)Z3Gd~e^f*<%Tl=julIUKPTxw68|RC=k|C7zPR|V>y|eh z1Zi2gmNx@@c|sySOya~AmrHt4@9Z8s2)@yDW8RhYrOa88)BzE&miO0Q@$CcrNK9RV zNr$U2sGw9Q0XW3E*e0HxwPheL2k2hR{;}P%jo)m#s9nBc4rmTG{h%=XAxw5QIG3zD z#80F*nt<$1an@|mM~Uw=QayALp>l2Q)+#3%Jqq;dC;&#SJpWfx+w3i+;V`w?slPUe zR208aYigM!bz25`zXZ*H>(r?12QOMay%t**5$dFP0Sv;8jC0jKJyC(_K^k_1!>yGzz>CK)DcK0E6diRgqJhwI z7fmR!=M4p`W<%~I1p>g`)ovN0H+@wDy^vHu7vIOoCG&P?Wi|GsmYbOZOG8^3|!FZ;VSYynsLb> z6=um$LDSI~%W0d!wI?1?QtmK$)V@+~MIS0Wt-E|Mv>pg}(L(hpynb}WlpHSrRY1IV za*laV)stv2ZK=!m*;AH;vBx>$9US=hAmIu1T+Cx?S0c#moUI$|=R0xat8J5u{ANJ6 zZMV(Qd%lA;H8Gs?t;!=EE;KjjGUn}4SQb!C81dCS_vwgnc~&zsM@+00_*I*^G|XpZ z4aX#PLnT$y+kLB|ILT4OY<6ksIqe}Pi+XDv@jG8o76~brVJ@pFxsCr9URStPs|Q8| zgUymQNJ@fKCKHJ&(=E6|{={%ey7usjpYc)>`A4WkI=NmI&kNs%{lbBJu4s~jt%=+u zvIwk7L6tWvoA)jnuK&& zWS7wH-v)Usev%{$oitil<*i=#^M*oMxGTzlbA`$109|uGpP8(#dR~aR*-mGXZQn+l zUzn#LyuRy#!N@l4O+?VA70p4gH2)BPBr45h{>cyj`plK2Nu)mRHJqFnL~yF{_yiI8 zLspjrtFCZ3G&FV{*ar+q8je&NCSaQ$O`5-4Z`Ge_yoS*;K;b^du zuX8rLY@Ce0HFzQdEGz@II}tmc#&M1~_>N!c=SrvoZ;%IZV&rx1PkM8v#o3%ZHdC^a zmir1qmG3wx{35S!t9_zhJz(NJeh6tFRpeez$QPE2LPTwIKtr!=g@UH+j7EQuR8I=0 z76C7Ac6R#Fx^YCsZU+1wzb>#%@g6I9sQbVnLV?^K4Y$?&8v&71?{YBIB0g>}r8O31 z31+9GAu?R;ex^-7Ek9d%mJw-7!n8jIGE7$f%=g#-Skja{)bFM5@u3UI=bk-F8FDGU zr5Q>}=^kLmFjY$b7=$O_O2`g(vO(E7lW|W+5 zPI0dsp5W&Lf|Tq}IF=B-bLir=@K6&ma@$~SVAAo;8P}TG_EWe{9*@bBKb(BsMS=hN zeh{j&-umO!-99lpPSBQ>d4^D{Ai2Z2Qm zS8J|<_t(8)4y{?>72;DvkT%j5uOn zP6A=O>D!$@N6G;3M+I?hXPgS<4IarA7`n8+7E8GlaOYf7yl&?9n&TLq%NVU=W&!TF9^-aB$=IH^!W#R%Fbf|6d#qbPXR5H6PbJ@d1yE_|>dKK$le& z*iU);VP{?mu9ISaN61*Tlo8}cdTwWI-Ne@U$vm8obcbLD$PSl&^Pzv6hofb1^Tw)H z3yPS~f9mHu*H1hQXXtCyk+v49&ec_dJTKGrkVaY0vhYZ5^=&~gWfONu8o`V0B0sreKXHAoEcNeE0?`Q3CY1-#T zq(L>#z_HSsy&qz*#8Vu{EwZcrC2%We&~Cjw2tQwpZl(={`Rur0C0!6Z{oqb|yU~37N9R`KX;G6)xZV`g5aj>L!)7 zU~~(*U+Zcgn6NL_&(x-_%tl8%=-JhNtCYDAeSR9>Y47wxBX8{F$IO}z`%PTUc2kSt z`@4x(A$$U<1!QDCC)w)o8Ign0K*We)AV=fGuO(_KJ69SsOu$M8m%;g4WUlAc#>FcZO%&`%|cVae~BIX`!W(C+K$M6LUGm$R{0t>TEb_(Wgb zUx?}?%dzy1T*c9TJwJ%UWLdTKgO9?>yYtBjU-Lb>lknF~tJ6nGt3p>Qf`=6RUw7tX zZuBPS7q&USu!qy)R8Wsa$+Lwm>e&=N;%hcOj+{$X`_=yyvuxsWd_JBO;xALdPIL|AejiR3-{fdpwAKU zrp=`r_9?yoLzWYMNtHMm^^E|kW$3apq~BqIEnaq1Lp@=L4oF9Y@Mo=ci%h4Qon zH|P^?K8r9iyU*1rZK}IZ!c-fuGB#|QG1q=y14)eps+2CCl<)9WCn zsoIlJ7!WObx$yJAs|To_-0^IqTvph0V5s?pb?CCn^R}g-dL;ngmQKYB=$~$y5;x|V zedTGN{@%vqO1|7XCj6S_!R5`oCB~s2deQRTciH5&b=GgADZH?Z2Lg{uB}6$eKi~Ay zPFt(Hg?TZuBG(g#JK#b|ObUke$hw@lEMSS(qxsfs2LAja7L7RQYEijyGN5d|ym^`5 zZlWI$xfJ%1cmc)q$>9^uHqbq^Dyo3dq@f(~$lj?xMfFh-Ce}L!nO28-92QM+MscCThX`g`vugLE9pN5>3zbJWgX4+0|9@>6hT=#qk5pj z1KQ!~Y;P56&5BQq^HzvimCH4Z(taT8`2|blG+qD5T~3sNmPEpclf1x#v+K?bijJ_a zDxiSrj{apG9Ia_`dNi ztfbb;oN3zp8&W(l@EC$T!;@N{Qy^GtiIpwe8tFvFF0u>Lbb0 zLX*R>f4etmBbaD`o)8=5D(Ak33d|@tM<-9Vez^EEKHmpk`_q6{!2Vp2f>^g4+Z<3w zPj_QU(Cf0x|B)l~l(|5YdH-hmHC>^(o|S#wuuyY-$@jEHar{U+}y0f8JRS6C(G z%VX7IV!t9>b#$MjPO#J#1%tXcc$+nuny3_yJS9`HDt~iuNbzU+zNxQHcU%{M3~||4 zVefX?AiE}XpF5Q-rx?Gd74HGO+NI5Uu&D>%(|z@YG6=Z$hUW_wY?bL7;EG3Kn@J!h zFBCv}T6Z3qlzC&v5V!Gz-gtlOXF5z-vr(kmN7P9r=MGq6?bulbA@S*!O0~4^+o`kcg4?8 zRd~$!(*PPSxMCACt~C1lw+MU+XK|9c28l_yEx)7Tlcjk})>TE&d{MR3?_Pg^1KOZ; zY+NC9C|J&;ZnmnrE5ZroF{hgeBOW(|x8jBf)ub=zSKP^>OEThgeOx4)>@FQR4&`Z0 z6GkD~&p9yS^rv+GjiZA-@{<9pmd;3g+vJqqGO{(FDueL+*SUZfCpD(GWHmD^DbE7v zNmkpOU_~Z-BQdRW&U$--U97&FgSwkmY zA+jakFtFtwtJ`l14$yEvtJh!f{o0bRnq6i5;j$$%gC2@Honoiyd`n90c=9#!m&7#I z>YMgi3(2hRX0>U}>L6X+cUF0yZ-b%s(_*vV%=GbP!MO#D*V;A-_DEfpUKdYjujbR| zj;Y>5%Uc=!o;~wRZK-Er*5|)l(f#^mU#pCKGYzwX>8_Jk-?&9*V-Q+6Keus(nuSaS zfjXPHhQ9&)Cctc;THl*DGaYL?mg6Rh5zjH_iIrJ4S<{#;6_xE<^NvAtFw2Ey44dTw zWK+PqCA#Su7|fzDy#CX}r{fo}ZViJkOmrX8X0JCcA@1cG7rXW^#Uc;RSSDNu%6Xdl zZT+S-;K3-h?^n1A{PdP-{*|_mU|r)wJVi+*!VSs}T?Xb1k}kU^2zdYQvp!-gE%Z7g zooY;DdW_k4=p+Ce1ZvAMTkPJfa1i!AUc2J};b7#ywexmAe=KeIWvsTo1la8OMvR~! z(~LHD!cfD);|J(GMwtDWrQY1Z+o{n~H@3n%dYbC7Zx`vR+>T9MXE-9RWQLA>0napu ztV=Z5;M}ocL#V7C!1BDI$=M{E?KPes-teJGvuc)DWKPeH?gz#Xigr~Gs+IP7+u$VzF@y_BS6SIx^N1~%Wwxx~ z2sDe;6f2|}>ykYkNMLhl>9|J@2}onz{NCA;iuJPhu{IxK`uOVAw458Va|=AG6XUJ1 zKjC@|f#<0W%iTj=55o#x%HU-#3Y+zE0M_hvgU8eu7Q3?MLB~fK>PBu;UlHjMevHi zE4?4=wX-6d%kM5KH|GyNKBvynQO zi8ITLGzRQt_%~z)3zgdu5hl*PHg0b;(POSyS{piUi(u%Wh~B4c74mGHV!RV{ch^pc zHQr)XTX46#=A*#e%m}GFN4OGd^n-^|tWxSK&`=)JjBI(CAT2!mHHW@y0Np&T{)*99 zszs)_gCz4#^)=>ZJ789VvGYWv9`QR4gXC;N$KDUl-)w+3KAFG`S%;3HG%L9K4{5dI_^?!9shcP^|Y6J?ahe z>93~B{dIqdOSTHW2AJ* z@>xW)!y#XRcwf3amCQ#f4e`WC{g6G0S;6V_Ej=e%ZL!oC8S=-$%VwpV-K3;=WR2KE#=F!omOTmwZNCbwIst*PcRuj<&m&hI zX9*l#kLKq|Tv)FBXPe_43zHe^8TM!%Xz60&yP=Uz8#Zg4UQvv`G=y`kWd>?XrKII_ zg`6K+Zj6YpR@trqxUB=yyuD`KNJqj%OFJPaj@r5(*dSBNNvSA6pS5t#RlOf>+CXY| zB4jhJZ8MV(jLS3~q!G5FMO;lfv0d%UpN1ZTP_&O;>x*+Jt%lQ1x~UrqWGJ)8VXDk? zot-16`N23}uE3`P@8Y?$V&wF;ziJFPU84eh(`@|)F-yhFvPeDwDH+oJL!*Q-)cKS`xcA zXyxyp(zzuUp1;1JpgYKMg9%-HJw9OC8)Mh`R;4rs?qh28?Pmq{j+tD3St<1xH~Yqt ziZ7|JSe9U$qh}?~2&-b|N6Dl#Sk51In4%6nUaMafbMV;PfYHVYg32Eo53yY=1-u1= zI3=#}=iy1H1UEVROBY&^OmR5ue7fYk=^Jy$4d!f;zJDE3XBd;Yn*F3VqEs0c-dKW@ z*SOP|k}aI)94*qsID5dW`4*UA%56)E?!8DudQ^A}8DCvl5&^8gZeN*FtimEU*z_nx z7#wPgU$TrP^`LAw4na$0A|&287mxk{Ydzs=Q+wbDKbJpq!mEOY%(cX{uU+qE40`ke zP5a5$tL<(RpT5-*3Um!@NyYyuQb!%30GV%)v+kZ1XZeP+xRRQ`o?(5Ti1_)E?96;w zudQ!OHkVe3+a&WnH7Dl;cQ!S<$N6AI_4thQ?x6pz?N86G+=$<9NJWu)Gl{sjttjgL zc6U&|4sbH<7xGM5Cf-67H|GBOe8CCphWaQO%!ogIRpM4Mpn?S#A?pWG(;cI3cQ19 zx!#p$FepucHGSoEKHOY%**ab4+n?kJ7dFWPrthgG>U^3&m!K+z%=$LQ+;6)yr^qMcZwszn z(Nd>fB4spabEZff|@sbW36wne5c>~_ct zr7AXx@Vl-mhbL-{pEr;KjFNYyj1;z3D2Tso$-GRY80tupVV3&OKSFkRf=lBf+`8+K zZeegdQ0_-UcFel_<)>LSJidz6Gy!699PjkV)17C6-Ys9A{V2wCqL+b3SQ8HF)(gY_ zMwEo;{7a}sx`wCO@ zU|B^-pq-+vvif2-Jm{yS!xMy_{yUsdb!U^>T|ZJhkj)pL4}=1rew+Y+|9H+h*wWFp zl}1w)Lo7G5SOJ!GvVU)YpgKWd?2aC3Aa%#vxeuHT4DNna;1f3^&iW*? zF*My@96aV@TtA9NT+*hRgw5M7Qs5!?HKAGT)?-f7q#kyx6P+vz^JO3&&{-OVOWA(h zl1bt4^o^eQC+(>)Ht-w$M&;2ZP7J+w4_ZUNKw~{l2#Nm+m9wdj;}XhVu+-3S)(2bzgB*Ok$dH@nR56|!yMw9A5U-oO!p5!1u3$B-jfI7Ve_2Y zj7BHjjGOR{(0w$gHaT%zWRBXqDeysbJRt>&xgV^P$hrvU5*%WR(X?vbq*#3~B z3ih%)Tkmd_arGRazgQZe)cZHsE05$=v^+lJeb48jy`Wz4<7$m!7hTOIP5G76F z!XZZFmM*XlUf*4!hDM!k6T{c0p2rCELlPQ*67o4HieKlxdmocgvEZEcc@;{R(*5%U zj>5d6VJr2W^o=gMU;9CMD#j}w4>8EdmBt_m3*%4GY`c}mW93Xo^fsSHFO)7ww;Fc= zej=nOzQJBV;zva1L8elX>?cKB7>7f~8x_5$QG7GKa>GDTJo{6`&F$6h5VzF$nS5== zuQ)tW^IbOJG(0{LKi? z@k4hh0X@D>i}HhjH!F<7*O(g%0;PN?uXZ0*sgA2MzU5fxXre$Fq-P$SGak1D??N^y zAlnvAsgk4wk4^3evnt_k6iJvXx!s%I`e#2=nIs<9uxgk|rt#HO+Bj6W!W$@GyH`4Y zKFfzS&oWkd}B+2tylKbb3%_0!rX;R&g^Ox~>20)g%= zuh0E9^d%atVW?P{I|x&BthM_+KA7lmMK2BO8>hS?)NUEI0SG_+kPi4+I=WpfuRPS0 z0E8*%ChFD)J-9C1)xf*8Ex_bR>L0^>CcMe0m(VAW`25$)7#Km?*Ie)qe7OqV z0;~NxU*2QTfsaq`84>m)L+;nB?3@<9*dBxI1FR(_cU`WkRjkec|*;~=;L(T064N00x9vA2$@ zqxT+u&%xc@U5mTBLvg1_(c&e)Iy^8qiz@z5`B1O3(l+tp46^8K-JsK!sufhPzqBvveG!lXoGZCh zUfMh;fc$tJ(GNnFmEhgcsA^j2YUNZU`^DdmikEa+sTkcVEv|N|!oLozn z^;RKKOS>0m!rEI?@2hnPJWy2dR?=BkI0=4aysTL)Ye%1gcfi;&a zUH@0}Lx;@Dy+~#)miC>!(t)?!-=A!4&YId?X2w!(al<+A|H9=i&z7h&!~rqZ{kc1c zLstI%{n+0S`?`_m^Q>QA6Vvwh<>U_O=)V@lYmVEWF9iIRNIT&Q9MU zSHt2u+-AX^oIn^`WxG|_ERdh!`T59PJ?_fk?b}PjY1Mi6jRqS@NL*5SF7`)_0sxDxDkq%<9IV3k$U|9H?1M8$#}=?IZpC6P(L6v(_eDZT($woTzF zAI*7`VgIG-^kLIu7J@v{V6D_go#tIb>|g}T*brPna{qIQI^4sCl{+iK3oBR8+9M}d z4~?;a`23V`5{ahteEf%B5&3Vo@c7a(Eu$8erk*Gs8Uz@}y(etr??cp3!SauN$+oiR zLbfkod(YRaYALZ&-252{>9H%ik=Z7@tgc)>XZvxuYH?b3%Ew@SN)jRL&FE!bZ38lX zfADK;iB?fN20f2#4}YP3aZ4jK*3;UwWhd^ zwM~Uilc920qxSvW!;#+z8hgGDg@5Oo5E-}pv7D;(7cHr}zyBW5UqpG=e{*o1EMHUw1a^SCwOoYVH%h1f|L^53RV=RKfSPTjfAhdEkGUY=N>H=BRuW<=b< zy3m;(HNIU*8X{**M91HK;?QdS*Uy*K&5T#&29-mZYdAD!X24G40oQE##tF?OQ?SQE$($arRVj zQG^PIISh)EOjg8y;P_ta1(1^+dlLONL+9L#xKsrnL01ZT7`uk>yV@<#-Z2b1lS7Zr zUN+c|1;apdLr>~V`Fexj)WSAY#Tqc$3q}?lZMH)`!)$v~m&FeBY)}cEg@DqbrRdxGEep%rcdlJ0QJtb+^ zhOl#BJSg7k}hgD%)suCqr35?>l@ zU8X~D0IVb=-3Gr131nY|JI#cFU|=x@`NDg~o{~UPE%yDI{qFKNsiUB6De2Wi{_+uk zlx$KA`FW!?V@@1>(;xjxJjca61SQYN{3B&^kXZ=mC0%E-$X$7Pb*FIU#QFt#R6gd# z?$1NkGw?l9!$R(H3>s1V#zxF+fU8A?(g))x3+C~w-EV%+11Go@WzCMbq~L}Suh?nR zt=sMskT1~cJz_dxeehW%2*tLr~60e^C5!e9Z8Wu!(y>zNkW9b zr5QQIL@vybl|}au41`hfZRHjiSdX{WaKa>Fb1w{6)-i^tsBW|oOq;w3M&s5OF^ zQYsV5VrfBrWd#O84);Uz#pP+nmS0=q~rnK`(oZ z-!e14*ay)Jq%VNB^x&9Uh#$VEYJMN4lvp38p%nu(#z}9X?tJB?om`w(vo*(TwaZ-i zt?+hTQAeA9V`?=>;Ljkm>lF}CzZ%hKcgB1cRa~E4FuO~AufZ3R$mq%ommr*VNAch2 ztFx?X7A~d);UwH0Pm_jTm_V<=`fr~n=wcU2&0!28s?TI^@4hdwtJD}jo^_miphiqo z*&o7;_NZln{?~Ja`_iFPHqXQgz-Pwh4W`kM{nBIS&IiRwXA<4l+`4~IjpZ_5kIbif|+?TZeGWeF3 zA&6N>YioIrBZ<<6?fKY^`Ozy1t-de;6_GLRAtL%pb z{2jD~8rtWb$WVzlK-)@jlW^^#YtHqHdR4)lG!g#lOUm^W3^^B2bs!w=#ut66ylyHsXlZ9PsHx{sLGm@?^VP@%C#&6UlwF!=fb&e04=N#J<;KJzq7nofF znS34{TX5iz9kgs6{3YLcBqV0U*9e+o8|%}$aFpH2KvLrQ{A@st%shlAuCL6T1y)p? z&{44;yep!ym&LvzSR93;wJ>O8WZ_``dYa`MlT+_ul*w9u;OCmCafo|%oxVQLo7(~P zuCn!*XIG*7{#S?L@>PdZ7!+-o&)`$fyH5eYq;z{L31Rv{P7TFF>%&+lIpVFSKFW*#2=c`z-?aEn(8B+)QIowF zdOtwf8H(+I1^QC*bJ{lKdrf`ZL@G*U(Jo5hc^!5jW6h^=M$R^(4$tu3nTzumgCw2Sm% zWrh!2HgBJhaaC@o9H zr@RUp3R*9ZZLlA8jcS8t3eCA!Ytuh`AE$04@ajHTD48qbusF4j?&w-AJQ#3ZMum$6 zUgO`nvo4JD?UX!lOuIk}gHh(3=D^0KxCGOdFe^M+4zuOfr%&Z0HAj#~H6Romn0yX# z;F&6MC!3xEJs9mbZ>q~0>$z`U(PfS8%^lFk=~r4#N=H-ifiV=3q6c%$4Q~!#%01~dy46oc z|CFKsH!I5k;RB@Ai*TNX5lb$5Gh|IDMY$ejay2wn4*Mu##f_*@KBg`yx#Nj}*PGKV z95AONf1N0B3%MQal%ihtXa(=$MaYerATFKI!jN2ZyGSb??pf5ElaW%2c9-@EiUn`B zfMZ%9Er$y7SG zp+Tq^gbj|AOxg@<0uZ_r3#d5I;I7?x|A_a+q*Ehr5pEhFt91=qC^3#{c&95PJArlI^5U)3h zKEYN97TAS?y~%0Z(LqQJ-}|E~;)$8imKAwSF}_cT*Z7KCOlbEVsgyVdqz=ivKmr>~ zY8OxYYwD`UNe7If2_AwMGyIrJRHlk;GQGtgMK%VNI~bg>jf%7$+~ptm+_Bbzx%at zL6W?(Gy1eJrp=eX2zo-=nNL{>e_c@e>(ddZfyHo{&M2MU3{~AM#JVj_;BGV-J0EXx zI4#ILLcqZ!=Uy|N6ISOHbk#rbhabMTM`=hp)rCVN=^!heGkV1Tubcul(-TZihLMqO zGWn;(g&rF2$C_&J_|><47lL~N-xBY-vf*=BNb=8rKB0>FUCf0!x=DY5kNkOHlPq%N z$5Z`Y5!V-FSaFVB6oCkqdN;n4*%7V=L3lD>COT!};K$Wc&pWqN~C{(U9k7v0kUI??9D+o%1)fpU+^`W_V3QtL4 z%>$R5?~07mRu)mV2VR#qT>tt}SubhWvXh;&GHt|+s|Zet?F??Jp{(yZLmV!|+0%=7 z|K!x{S*|i1$}FsFw|!QrqKdp@;P0mYxEg`5GgadM;5vfe0Z@6LJh`&yiZXw*M@-MM zOnDs<==d$GEotQ*M!xSDiVN*1ODIO3gk^JEO#PyfJ5sR`qExF#SLl^nZ#4i(qnA)S zpY!!LBVs?I`jEYD_o4LK&o zVF(dnILb?o$J*J_e@O7c_L;tIpswC3To*YY7Zc*Gv1kyGp_#W%( zB_=k#Del-la`$ZEN;sGEX}`9B`>YT@^}j0>U{;Y5{r)vprh*_IA8WDJV=?#&9zeSR(16k3dmCZ9pEK_oU%pfZ;#2(#7z{D+QI;~` zNZ;(%lG1~p&Kl{dmO-_98qs@y_ZfNEIW~%op~g(*AV;#}CxI!BX z(>tifL*E!#1KB&vJvN@74|24>{E8(YV4nfUgpACEjI%o361zbqG;`b!!-6rZzm{1G zSyOUNhqV&3hRSr}(}Uo3Yctnfeh-%g#G?9o!P;i({N&)m)nuE>xfQllPC;qB7N0Fk zUxF2+YzlJ8eklDWo}{NNY#Yj+4DP+rQ;z^8uYtnl8ud+VSuV66rHhod;9(FSB8OUV zT)z8D%CaPcl1`HKwU+u$>BsCOXr}HQDSdUx99yG$B1BF}K=Cwc|9$snDgZMaOX?C4 z?C5c@3?=qb2fe@J{AnJ57>;|Ntg9#V-Ex*PD%R%3lqG=i^>w`{jOhF#aBl_=Ks4_Y z8es9O;O+7s)TKh;Vs})Ek|zm5*Dt9d@BRI9?-dxsrzuDJ^>U^M+mo&SbBUKgTKo>F zq(rjP0^L>L4qi3oorl_!WDZi;;tN08a_Z)pu7N|H&jdD9Q~(NY+Nf6icqW#R>3h@7iygxB1o8cK5yaae)shzQ1yb3*w zvyhpB5bEqCX*DM!#c~rwu~2Vr!6bLWYsc;RYU)TbAKI?*Wl)_5rr*!`OkuSc9HNc+ z@@w7)j#icEZ;eQx4{;w`?%&^xVpFcI3jBHJNU50<9ze?j_y{zEKP!0I{TAEh`-k$h zY+_LPyWV2)zOH-~`Vj9xLs+A*Ulrm8?fKos4~jD|3;#hvTeA(EzkpB#wcSw5F8`vD zG(RrLH=J10&xr1TD|+~zjJf?OT!O5$lE4HSnFVovo>s>olE;Ep?^cs7#J!V1o1fc@ zs;%!!XE3S;uAzxNRS*MHnZ4oIW0`50>l(#^rSWbeLS{`k|2Jx@JXSJYULm>oIoLB) z|L5$$u|CSNdOrSDBbz|FdrrqQ_W(GmziM+<#7$3xkPh1yZrt@x5PU#3pM2AAZU{5N zl@%>_M1|X`ogRzl+mLM>3($Z;rzBJlGH@Lk-@#zBiM#&;N%bFsJB^#Ww}FuL0z1Q$ z&bv+#Tw}q(xhKZxi$b?qT+DO;2{pk2f*4s@ZaS~*U{Va57v8gppD&)d-pj@!1cgcQ zjTjn}?)SHLT+zl7%P<3Ux3qXVBk;S=oNAx$O7>k(`CCGlE8r>MS7vQ}i_S~k#fq?+ zLo;j&@6x5p*q7FOK2x7H?O?SsT8{eAO@wp(6pM!hzOw|ONi>}QbRq!XaGeLJiHY^Q z>@9;NdXKmcesLAgmhw~DOXs3=k)wBf1 z)Vmrdg0Bse=DRG<@&#O)yn~$%vA3taR~_HZG%Qr`<9F9;;YHer(%45VDRsVEz;NM-6XtOVq zWRm&b2a)#fC(Tp+I6c1KEZ-5U#QzK#rm)q~BL{p6~I^>7HfFgF=j6 zR~>o16_9`Tnka>GBpa7mz=s06v8di)9st<;Oh3t6Y@DnuBNv|) zmxZj#YEJ{1DxWuepVzGDVo3dzGW(ZJXBf;{8&(ex=cW*d2ObbDBoZS0bbDl;=9kck z0$k~C0UsWjCZOV?0-iH)>RWuBG5g^F>juERF2!;MsNYTJet(dV!sq%gp8X|@L{^p9 zgMRYBO3c!b)+1Y~XH+oU z;@?RFd{wl36bWxYRV)KHdshu>X}q;d`-*YzCquJPFEam6LgrX9|AW5&)`QS{=BT+i zTjqUjvoNJwC#KpfAc8fH`vV>J^(cmK1vD~`JRj_5!DOn(te{Kt?1!=sG=NtPZ4`3d zfipGP%n%l3#TNFu^)$5n6UL8E3QUs@Cr_XkWQ>Qu;XYt`Zh{w&9c{S+4ICoZbP2=U zXS+&K;-zplu@uO0Y&4kakLvb@WZ*BrWq~C3uqRA6SSM$*L}W*Wuzg@unWpvxvha9A zNE6!)(>U;$L%3RfC2lBV@44CM03!fyE?Up3|xID|Fu zGgWH5>7J~Qd#_CS>v)|7>|Oga(*M_w*91oJ9E#-ulsqW==TW7 z;VNe#_T`7+nW>r%Z~uVILH5C*3n}W>>75L@Q9F}=c7owsOd(q5#@47H&2WBqPhmRGPq=;e(7?`$aGe zTMrw+r=yBjzJvKOds_ z;2e6AlHFP{L5gm!{X{=^UQetNAn}Yr_*=%d75KU)kG)n-?w_6!^7VMI*20h$)Bo>b z=h79>tRH&KV7Y&kp;B_ZtjoX17ZuhuX+Fsr_nYyc3Q4WaU!q+c!09&(S9}V8^91r~ zKRCaNZp!Q`J)_B-r+6{Ul`jeS%6sOzgF&_^6S8ALwLkdS)DLc--a#MAz^PdZk%E0?#D)o9wTf>+ol>)OA0GayBd;TrS*z4afX}G3p zY}`)YEcmgY*%d`D7DeB^?danTY%uh8t6Yj#tOgD`&^8iFf@c(%lBP1w;lh6P%$_Wc z#$vIOPM+!;Fm2xDkt=5X+g9*QEJkQI-54ceLx=|)DuXgsmcpO9a7n`x+1uOnKzR1(FSG)^bnZ9z!36{e0%dQh z^wv@YS``d)cV=iRw&wXg(GO$T|9l++nvf7QR>rXY+H+<(`O6!Mx54ySBvzMy5TmXr zKV6rDOko#j)=Z;P?hlUn`m5kGMkG<4VHdS+Q%t!W;xx|FTe@pI#GBGEYi{ygxKLDE zXxWXGrw&ZJcNi|tzOs39ojHy-4_i!%rAuI_kT&{dTN(bU#-k%mGo55Z6$<(Hyb$+v zeiWZhYE%D(S-IU+G|ha`*x+-B!vJrBp^#tUyG27S?)LtvyL?GG71#gsi2!~0ZiMEy zdp@YoA$lT>9r0b@L0mU!>8*>p6>!{`reog4OMY!W6B9hnE>0D-FYyiUBn21g}e ze=2a7sdCnQMdfFpOkqmy>|Xpi1?)HGTCpj4rlYMPwset1wx^S5ibShy@bD0i%q@?( zUwTNm8Tb5c3Stv~_z969&CVBrUBlnwQl_@PGTrPw05pif4E{#c*pkW0pH@j)xwD`@ zTO;+aNd5gkDy{bttgfgY-1*w7#hKsAjMeasHhbXt>VZBSQE*yfc3?4PwMU_%4{VWo z5D-&k6r@~7?BVI+K*Zzco~@p1lnt?TuwP>iqeTn!@?!ssGiC$u=Dl_ttQka z%wu^Gyne_*V2ITf6dVXX$c^85KgxSU?BW+AUq>>t1VS=FaSp6keLlVMA*-m z-O9>P%NTEB=>~KJay|pZaYjVrzm@ubl?`kMF309&r{(-7oy|BJBd_&PnJem%+l(46 z06Ej}PEw0noKI76#M!oN`f0E{-$eB+-8qCP21v&0Y!tsU#4;Xf&0JOKcJy!&5f%Rm3b z|Mx~Px7vXDzY@R#4V1Fng+FTHDdtgjA6Qd{0)Wk8hE!E1H+)t<=fD&VqIz~NGL8m7 zK}d#l0k$*NU;Zoe|920}>}&!6b?AZ4SH9^{tvkLDZU6vURa!Y##lOXP#r?bg?=3w1Vu(E`hKVuN?RKr1JvebK)9>-5IDdI+K>6eY0Q$jA_&+5<1o!evQpnv!>5_r}R~qOq zSiQZE7@d!9ib8N3rvGjnFh9ZxBPZdJa8W4&ZIcAr);qCM++x0zwG252ngnz#KYifpz!}UMeZ$^0Cb;DWSa>@i;UOI5HXg= z_NA*a^;y^>I#HRvi${m-bVxZ=$tg1>Dfhwj9(_R(MLPP(3&jBdFj)ce*RPs4b| z!jMP8e4eQ%>z4Wsutibl9rCKP=GU1BLR^?nn$YG$Ew^9|issev9|T zTzVRUe#c?4j*SrkO!01dKK|Kt);FYp>q7VWQWytNL5T08GZWZsGWPqb1R&hHKgp?8 zHUpDKu6IH!N#U#NZcbHSX9VSfHYY+52Hk;yD00*9={91Lm<|@y<%V%N4A#V!?R$AS zVl3wfqMu;Ax3P!5;vinlggot*9leuseuMSke6I`)m80 z22=|Gz-?Wj96&-ZAZaHgiZPkv2=iw$oo8APo*y8)WKM#Z6F9o?&<5eoKy3o5bBReR ztDzP^oLCwB8Z5h^9suAU3vPM?A65!&`^z$12x^Xi`<(Z)w&ifYJ}6DC{VMy=sa+BO)oN zKez80Td(#F5@Dc+4Dt9)YCva4e4|(pZ}~FxQIr1(m~}i_9e~(Mi~yqvCt30G%d)Ws zOR}%fFWxo1qX0bC*A0yYaDR*qD7_gIZS4&csi-I!nU;8IyG@Yh|5H8;L#Q;EY zBH9z)lRZAPYiK@Y>fuq|UqroR$c;$ z_Iq)szkczD4A5p=IV?*QB&;jO(m^ zL<`m+;eU%+)FH@O*p!a%mc!e0ue9*$kU}frIc)m~Ccx3>Us8rSNpSlZhz0_N5?U|| zfNY;ZNOO&&mv{yHF4!9;Zn+&qhxo|O1O*ZR^v3gZRK>7EIMV0aM;IBJMI~?k_zn`~ zvns;3AGY>6q6H`treyHxm4}W5a(n^R05Yup8Jke@>kaM&H1<+ty*jIKqq+6N+PCO? zPsFny=3*lO=EK|F}9qbKsDu-ImS;_OQ^wXHbhb{Q0ImqOBhWP|E`9e|`FmDlvotRi{oJZbyDQfy)WHbKr_ZqtK?m zi!2eUt^C1H1=LX3)zBRMVe9aL#P^7wj(#k+{{*y4`Kk zWhG=iOuY`1W8%(>Cser1TjRc%yX7farjHE0@VnlBUyUTiQDXw z8Z0kn|MvDWtulP&_8YW>V{oA@89s_x*u?TRGpNJa-dw%#3h!e)gwLC+d5=t$Awl}l zSkd8lYB{?bNO|}bzRwv~$|H{Nh|yZE$4pmn41%#)&|B{IaPQ|hoJRY>l@rf!s_&Th znjzA&0b`*HBhAaNO4TYn{%3ZP&gZ(e8fmSR>fft(@1*%~^tE8f~EN zj_&ubwRoLZM|UU*K_Llc>lHtKX$4Mmanme>O+WXn1I714>GcEKfl;oKn5&N7DWTcB zQMb8i{JhVHDVxo=m<<>AFNenCiTSig>AHJ-VqlS+YGolzj-PV5R^sfPOX84AY*n+BpJ{OQpSZ)mqK_?Ip&Q}062AKhEl+G#ED$5f338Gh;QV6$}-xM3bN z&N)lGt`B6Tj3x5SYvP+-ziJfI?Z7}+UJswc9>SbgOu znJ^P{F)*~UJ%!&vd17E)iVNS;V?Cq-w))$2c)f&S7$N@K(%0gtw@Nx~8Dk{n^`|w% z7DoQBrDR8avK#vwuJO(^?!jHL6hvaH9r376fl~|FD3=KDUyhHt*Uyag)Xk(yDtwyB zLlSlnZA9*8T-syYx1t+A@jyCmm6)C=J>*Sol%pPJBsqWo$|90-6d7()RIVTm`luMn)6{jC!7!1Ed?0JDSiNrxaqhw>Dr7hGZQ*jlL-s3_3yMcsX%x=ZO z-`6bi=hVA)T?Lt-9S~Rk+vmtPJ)ER8xe-fa!q;d^Pb79YM!c`tAVlc22GwOFvVW0| zTURx_;kKP;y)@6V9SE7wBnL=#<}1cR(FOvGzGLwIQc7A>huf3isGF#6r22GzN#^nE zaUZMbMFK&YDX+ldOwH=Xx* zu0Qo%5Cu?V_NoOc5t=?vee%0aqDP0Ihkz;s;8?ipQT(N2>=76YbdPy(leD(^*<6i> z;g_HdYgMstOR)T?MW&E5BK0oljGf4y+1XE}@?uL?szaN$MzVziv!E1>{0FcMLmxhg7NQ51NH#JF z28|M0ll?eJKiF+wW=0!2st-JGI41dFb}NVlO0C0fvF` z0Y=jXkF7rpqm>)lF4+1(ja)L8(TgYc80X9k@+ejbt>z3-gk_nDC~na)*yTlNa4mRr zn7g@LM#zq)N)e4pt>Du%6U9uGq^ayDr>U_wwsJ`0HPqM0TTVX?o_VXXw6F7-h)ogt zO0Lkd5IUm*V*!nsdmh_O=h$QMCoMYwOq%d8QtByv#ohbNKEMuvY{kIfN{x2QKpfRF zo@gBkNC}>b%ZF7aY>T>sxIU7Oh;jYXp)xB9ZpisrdJU5VJy~W_yW(1|!j3V|PVKpZ zJL&0LV(_o!+2Q9c-=X*?GFW583(do%F6@h!WwIaOaRbn>aZSg@>;{E_*{Biwu4V9_rl_ZS=gDKFumVf2w=+Z@!=hT|dxNEQV?dS9^^su2w(;e045VDdy}*KTZ|jiJLf2hX0l!&Y}H(jm?22WjX}j3XRKqI|6e0;6Qv+g z$S*pDW!{Hzwa;B)s%usXaii>Y5GM&;+lXzskDJ*L=a%DPp7@uoCMA8Xo4>DSaEuKO z2K;rovfyQ%K0Q$GV4mH%y)k{677?m&@6eB47g#Zk?Ll&{p3b4AwloVqz| zl5o_7j;d@LS{!3UCc{36&YaU!QS`>J!Bx!fyednN-YhiDcws=QF8dj}FSm9rXtH`r z17vSBZBddz#~AZ_W!P&$-2=dBw&pjYLoZgK_&9Y-q5_qAOGYLcGO{8U1*Mtif%+t? zsU$2(gND@2WQ~i-tQG}4-Gc>E5%L~ruNA_Lv1A7I#T4!ZgYitub7=f3+WC(GXB%sv zX_=3tKV^BMd;^jcZsUDmGt0a8JI?sY-n6sjMx%J6QaI%IInPlgAax8td!uT00H}n= z%%gl3%gko|10xhLiJ@VZ{1(*)09ZCm8o3Hlig}-T%vElfeLkl)9AFD-fvF~#7Z<`H zpfax%rP}4>^Ji_T*Tq{s~N#Rk=%$8k>;ZC`|&fPlN;Hl)a3}nU&X=@0| zcoWk)%nh3%$44Wo6}*m-dQ+dHo2b7a{6dV7$|cxVM4hyrbilZYN*r1i?`R+)_vwj2 z=?gA23L1GYHmrY^JyOGS|uOrW$8)SDIP%x@# zzmm&2>3?EVIL5e%(t4mf>{7@_$adH45>*?`J+`M84U1&QHbx}A;RJ?>jgBy5hbh`h z!SQPCLXxeD;<4cCDqUwhHw9p05zD(8k~I{vRiA|%27M0%mei!{{*K)S9`B4?HZRdk zRaIxTMdz;C#L(`1U7rqo;!rS1)M~zd?@-}H3u#3kAy^JS?en3HrTr~fPgXFYRF{@6Sk>Jod0Gg z9v843=~}pLI40?B(EU4^Hy#V-#|R0GHvrPSo}ReA;NqQJIijn)tudV%KDyUr2gh%M zBTHiGi;!0cKDB(eOtIZOxC!e?&TODa)6f-UW}EKAJ$(^(SUb4cZzeZ(&K3HBYS?}* zCiu|^I-Hq804exG%C{x_6jMS?pJjrJOShNVJp@a~;kmz_VSvEkV=!G((qyw&v?j9w zUcPQNFR7|t1*d`YJ$C*SU9GB(9rM|Z`9W|VQJHS0 zpYiK$iwjd~eifZ(C+Q%wmStaiRwe9($#lPv$-wLQ)Xzk8P5i3vlY2Uj0cet(BA3#o z0!PUDig6YC&Zw=mlRU>;IQ7Tg^8Em7j)LtRFGD-RTcQB;pa(p$Q*Y%wy2uslOeOn} zBH4+y==-Tiy(8|(b6&>v%|ZvyqVLfNK|4nkC(nJmNCvT8pOEj+ZPTkZxuXrx;OC-K z()gpAFBgKAuMrK<;d~&iQ2i~sF*y9Ga@{5X0x7@eM44ow$}-=(K$<6B=?lakv*ijm zl(p1r2(z;VYDfhHPXK_Sg!}jXwt`BTY6Q^*X*$w)Vl9c!y*JK_S1+2i@9nlVv*nUA z+#KSQD~t{PbXv_^&r^{Z@fZe8xlsLYV{1w0Ohy1-fKf6E7dSs-BP@#px4=e?Axzn{ zo!EiOy`!GL1_pGwm4PUj4>q$@BZbNO{g*q~fh^l2dqsOK$CFQb4INM!=6OQyjYO1m z0-;I#9T#8WN8?`Dx{-|0Q@x%jX3EuzYgwyZs6|{{JlU?be1Q6qzcr0pFkgaOf?@UX z6@LmuRd(Sj5cti1>hGIUv8=+Yct4y9fHqCPZ3a>s!o2r-00gabKs|SEIDo zhraX)nDT9yh1%+ezfcm`yc*WK$p#>9jtM9B2WX==9t6h;d0TDznGi!uKOu9^!2P~5 ziB1iT+w%##B5lEMmEvyT5GfW(l56SWx#>ZX&7WO@kB*oRS_&7F{yOoFK+H&0cl8^u zs3B#mn9)w`vClPQ!YbN0dq%$OcAWsTFwf*PKKxS8#Zozl@OhZjt__TYshT`2`1o7Z zs`t?06+#&LbCvH!d+Se!47_*-^XQ(DI=G=UQc1MLEQVTZY%;v6Uw&yvx(c^x1An40 zqZHXG8Mlsa8k91jtF?DI@g3HR!{F;}V|IoQv_SZI12g~+wwUR`_4(+!ojHnloECEpdtuCrn^4;Z;TFc*2ek}Ifm=)tpy zoef<}k!W9@-i^1DCUL@eIP?!}1~i!YiG~JMhq#&S#NANJ8m5Ai&I2R`P@{9nU%KcQ zGt69Bl?U{~Rbvrjlt75?< zdc6bqbevjXM{j1-;alsOmQ$PA_9H@c7FuX7*ZOKR_$+%|8^Cs* zL#iQ`j+=w)2(MJF(UWTUv=wS6^Osim>URLNmmVd9?Pp=KpVv| zVKxQCEJQP1Bi^2~%Y=!4Mg*#3md&^wH3sBW z!dyojQQm0Y*m02tx4=i;#WtkGA(1Y$DFA>O6YN6ydu_1$AM(F4dSIU_hCpBZg#nBS zGpB-t4TRh-x}?}aQXFKQif+u9P~TIhuft7=2O{9d*vMHEF%h(d--g~wIE=dK!LxJC>1ZgU7fPJkZJN8l=A2Ka zX@@zwB4Juok8>+AQ`1wz{MIkLFaN`Wk#|=?Nx-a7XQ2cX z4FLYnuWNvpMBGxqO6^YE-vxa^4{`sToLbBEJ0o*rrXQF^Th$FooZfT}|FRG#9=O;` zE-CT%NaG&U%NF8+@}synSl{eeLr*@!BIpgXW)7rdzzT@-#>-@1pg~W_ElKux6 ztVZ*P&fu;t#YA!+o$@#B#0fqM*82F*UqIE~)N(jl3DRD~Qe!*;9Ks7I-`pUJn-M!) zJD44?US`uwNPihwChJo(rV*K9;G)*egirp&1BRZ|C-8e*I6_O@IhmFpbq4a2|L3<2 zj`U%&Yt8nfe)5)gM2fDK3F!n7*vtN&rtdB(__@}aJ!fL>LCdg44$;c zgXSj;mthW=M&&)Kwu$;+lLCSg67{Z@qZ?}H1o*qgAF|7Lfc^opw!NjXmvFJd^|l-m zuICdB3>VuOoi*>hH)Vc!5igdIq>z6wM%lYQKCO^XbUhtjXl4Ka000014*s=(D@GPv zTMWiP=m2ye4PGFn0>N;vo!*)JF!x;azZ(Wbc!D~N`8aFh@!oo?4#n@-s-u>#5NobJ z1K+z4Y*ZUq3d7ed6#^NL|FBK@fVR(Hc?30^cVRFJwOstS7MEot3`#)(ymy;$uJ14d z2N5b7-T)1j=p{PmXX2zaoeN(ObeD}QGM?LE)JEDwq9xYPX}b}^n+v$1k)Tl#dPkwH zSTc`y*ljiI6BwmC2`lU8z|lW2N06yTb?F)`2^ zWR?M4TxEX%$~}2@^O7Mn&2$jugqm~|ATr`H$M%f2^Tn4+4sfh4<=DxHno~UB-(`;W z4aZ`of7t8ZiL0E?Xt|TbnLJ#4?cV9XqW$$IHx~q*#f$1PgI$-5fue^x9GYnn0i-ZE z>x*}W@Z2mF*gWfm5YEP_m>xxUPtd*35J@IJEu_#6o)~u$DEMIHys;GX>tH9QtF!|d z(Cn@O>21xfc-h$xr7+muWM&QH?0006TV9SxW$<}z{Jw^G%wVwo=daO=Zs&{Jqd6W!q&EvE6 z3ZaOY#GJ(@Jc+LT=`v`mCcT$O1`MoMywpdHgY8*xVE-vvHp22xCfC3-tPvt$FlN@)Xs8_duaFl3)1J}C{9eMFIUPAMy_^~V{UfvU? zAf3YrFwgm-R|(EKlEK2|4w|I~Z%xK)4c2_ZhQV|PY;kS#fB|{n000L! zSZ!DzZ9kBrpafPKZ};*I`C7UWouo+EGv?t?A@i(mo&0j>CFxRvQ-mR51dZiUZS4C6 z=^)4ea^67%QKiDa@!6@cQQ2)+I_)4YfwuuD*f0vpplw=Afb!JhhMN~oWu*FxYTHjD z--=TDG&HvK19;2QAwHPYax_IX1Sonsj4<2DNZ+}hc}7Wo*FZknhj-5T5NF&SI!TuF z0?{^5ohIcd6d6|S&)9%n`lb*Lh#ibI19EH+e;=-Qu}Ghk-j>re7xU}Q5Pwd1xovW|CBA$A-&9q|>qB3TuKTSUz zAbYru77#mGAwH$G8AL=RxdjA7SU6&(0j9};#e?fPOpPs?atlqFgdgiR+PFz;Xv#L- zf9k~g9jFr<_Gka5x~p>`F1{ms1-Fivv0)AF5__V-#-5+B93=rZxxP~BMF12!$aHxs z=)Rq~>g0a6004avttw>Ck|!NS4PrOIPnC+Su7e)|;+WS&KmZkB--iQu0^VksV62jC zWU|d9x^>~Ezyuxz3~&aGu)LSZ>;Ny4Uy-B%EemV|JW&$3<`?#tn#<~VStm59TLI1K zNk9M|bHE`IF#G@}5;_o&=H%jp`y!r?CwX0Nv*2B^YnDGOeUMA|TiSH2;JQ z$!m&qWqFz|bgK{1FQ6V{3kT!qa>ACfaKxAO>N{Rn^6JM(EWGEioTR{WRZ08fH0UEB zd66qX5%~OMVv$owHNZ*jVGnkFU67Sa{&Yu7X>o?e`D5##l7>ZJ;)rZJa33L8q$SV^8 z5>ILSO~yFW))c(xlLO9hPqVd~kXgEHHv4<3S3P@{KF0r9^RmF#CW}wNVOx=Oe6`0H zxYp*_oX#mq>IAB_@Fz{y(xG)*&T=tgO_9cIgs+zss37+ORt~Q%tOhpQvAbrh9u@Gz zikPp_@&wHNN@9?<8taQZN3e202CorE)2hQ(I3GrHwh1#+B(C_*9^Zy8zLS^+o)q@5 z50y~{zu|*?w>^5g^9p%+so-wx&A^@V6^FZA{qLW^-sI!=uvWUcutBMlp=IJBBr~k$ z;kUOhf6BqeS2i?4bowo*20HJ(UF3@vj(k8itHP3^9eZ*pVU~*mY6L5dxfql|wy0mi zxNH`?5J)N={c=F1h-SQWP$QS=n=KE_eU#&qqaT-Z%ifN;td()+SYMh{k0D1`7- zzt>l6(6*~9z+J`9SIYp_#vri_cmVW8@{LvGH$jweOc3;K#7RTn0O+Xb>emL5{x||w zPtd!0+L)_2>RYd@Y2& zpX!{%c4t^Jd%#Z-AI~?gTptb6kS^BV7J3}<1HmY{NZP7+V{?u_fGjy4e%KrAyme5$CH`sOIO*FX`RCqSg5{dZ(el9g?rDf9ChI6vsjDC1 zgQxI1UlYjCO=ZZ)f=79TFAmoPwK5i94cz*x@000a3xLd--)OUvtX&%rj zta2%qEy_oX0d0WkeM;b53`AT?(iZV!r)e%oP?y80NIW1!eul31;j8@?*^yQol4yJi zA|Fz27kWo01Ctj3h2+{p$xEcF*QyJ3D`D!ua*4$ikY0M(xpr3shInjxS0$ekiFvs2P@`Wo5t>Ye<*{;e#?+~+4YpI* zaXm;$mHZPFlspJ?&$MA()f(Gt05!-BYm%r+UN8aOg3NAiH|?+9T&Ecl^=XW}z1;y`}?6qX)d zJYiJk2u>e%`rG7lz1rpPrxps$UCJzt*uc^A!VGT7>08V~ETVJiavkuO|FP1W#vL@l zANuVF2XBN?U()`BkZt#MrI#>&I2oBn09qu9Vo(M1)XjjF3XZhJXa3b(af#m)^Tjwk zoVo0=Z;%q#NI%pjR-yPXLF_8bPewMpRT5o!_Kp78E)ko)%QlfVVgLJCclW8sAQ60p zOR9z;l+RVSH{WkBQ~jlmP}yGx_|%9Rh4#dw-mDF1x*g_w2{uhwLu1CS!`8~x;ay2# z3k{0k$}JI<9POtAB0N%K$d~PE3_+B26yD3DgV1tdDp=&OFaQ7m0000009O$fZG-_s zCXRawpq2YVBv?|%SkeFp>f@-hR543K_tZ~?JHcGeibo8MV<*CJvwtFUG4-?FeB*-~ zBSrkeJTv1&pf+H}_p@)(&W&b&z98qA!bk|sF)$^JNh$mloiPAOH+(E{LMSJqZsPL@ zHCg_vc-!=TT($Y-AI&7BFj2^9V__^88Fy8)TPxkj_3q83?;9haQmj2l)an4zdBMw4^AsN^v#|5aEz zqS@sYhjE!iu(UInV*+`gEIbl_?%XP5R7ovZrEpg1?a-%$m%(E((pIY;5&L~Lu`BvC z7&d&lLV;I(;w(34n*x+Yr9*zi0||31h_3E;kwb76q}kZ(Qh&ocn-HV$$to|ig(msZ z5lXb>wE(mjrkV&2#K@&!Sv~ZlFKGGY(E(1iUHV%LScV)sFH2z=Pg+OV^-PdB{ZE=6 z4@F2T$I5i?ee%@glTjdxpUy-{F4^uh0z{9)RV)%iFO#(dcZs+`t$e5`eyno=PlY zF_iCmD#x9;ycqXr*^t6$c!SBg+f0Kl8@AFx%vCmhKfFA^f!F128CAI&sJS6uRgwnf zs;u!1Sp1R05ItcR{5tuAJ4}ahS^|%kqb%0&KxzZRzqZRLSUdEkVT39aGLneq zcq=ZEv~nsTfOTY7=8ANM%wQ)ewHhf5?4F4JrLkVZ?t?D%gh7H-p(oTNGng)io+Xce zn~ZWz0clCP1Z#H%q0MtIdos%9nE+h(2D(T1uQs$Hyv++)>ox=TcGmuY{$iW!eQ!iS zDWB9-%tWFkwBc)M>huuU=u{sl9C!?%CGxJZFDHDzHiv3uXF_97nWliBCS(Z0Y9$3D zUxjA$MYlrkxDm=t$z3(CJ?FOL;o$9W1;t|MUo9(@LNUmdLyZ+@A9Jef=K+`9uu!rm z9#K^l+1P(gRWn+;FL=IwMMF(%B|YdrAh-tVo&Qic{Bj{_P|zWNn&><2OpQ~DG+1nm z_cxJ8)xQQPjhjD1I*?BLHKey?ivPy%!Cs%M`+=YBS~R>kh0o*mkBGCv&k`JZ-9zYO zrUk?8{Q)GbUu?)3Oh)v!gzz8AK#wMKW)oyCq|!I2vF?%B^Y?@F8mtFBRjCg_^~zp5 z8(`vxwIS8%O#TE5q&eoFc_^0;qf!ez_pThuQ~&?~0y?-z08lIeRv=}PfeTv;Z4^(7 zVzf>YaMakIpjj_X9Q}_{@*dFpfUM!4s6YKC8_=K+tu^Ss(%YIXHAi+*C!1!MuB~;}FTp5*;BIp16k#?>&wp1u> z6o)08VmyZT1e0%+mf3$D@*0T`vMb*pSP;^amAM2f@*PrQyS?fZB2gLORa!8?5)wtD4Py0+sLSry0@4OXpPkFw5EIC{@Our_C1?oQov*5tPDWXMyuQe_ zM?QH@<+T26x@?oEaJX%IPfSWBi9T04zs1)kfO__72Sfnb;#*qJ-9^i&TFUBt^HeGG zIYNBXyhd0wb|=t%nxkFW^~~Sff@k4}iMRE)H&U3i^2XX^PHw#fj!V{>#8nFoib~L< zo7=oB?KJ^OC#5{WXlumkRwt=fI_qO!%-BJ$0{rY+%`JVw|D$XsN^9yOZAX z`l$rhBl>o8k&oa20000003e(4O40&aKE`rm1Lbq^TdJgy?hqjK2gYb6y&%8}${Qkq zc|fZKSXufU{kvnsFyny$G8x(`=>m;q8wz45Yc=Fo02;1sMUWy^;1nOHH`6a!zJCYZ z8s#*7cup8AnOM<*sKm%ZYJluq@1)N?xAn7OG>YD6JQ4c{Y`G^f>?dMu6bQg7zL_vn zE+#$dcA&y0;?;pcY@c)}dUv7-6P^K^_nI`!J!B}x3Vc3X88PRkl>1OeLhWw)!{e6& zZlF+g_Q(=t3ta~$Fgru^?YEgMozJ7kD{LY-0B8o}ToCW7e6WzI^;jqYFahqgNoQ(lfD(j)dYxW=$494I`fu+1b zOC3vPd({H`R(q8F_nSIckI2 zTb&SX9RlJ}7uHML&tG^$vNkL)R7K`J$lt zLi%)?zf{t8=yDvIO)3n^*}xlb(O)86NJ_A_V4c9`54dC@?q*ca{IzF{v=UZkhd2NL z26xVjp8yK611B67wDNG3Km%Zp10p<9WWwtsh=A>D(^u}q+Moac0GygR0_KhcuZ1u9pQY`uqX~EDy#aua;gBs178Et#Cw}r%5>f5imL4y+dATF-h-MOY}C* znlBRrx@;9Tl?33~F!1|F0YCMWDJgRZAW$0^O0Td-=poOKC$Cl~&TVEyYdl2jMlO1u z_G<@2Hs`v)m%R1=wP2=7GB)!I+2UNZa(KB})QBfBct5jwvL&LFa9uMu# z(zjDkK7kxjvd*5fCx3)LuF0mKQKTCji z)_Hf%Eu-C9X(;Yrzx35l7_k7l!z|)f5jv;IsbRz8OX+TDy5^b^E-Rjhvc1G0pv+fc zV#k~(G9CSz(cdfu5`YUQ>`&M5EBWB%mICsWkuNQ;0S58G&NhK3k! zRaV@~xA+A%^e)z#QFCJlTk9f*(RwqiVB!InfI{Ntn!&<7bJb!Q8-?V$j-sGLG4BjQ zPe{xF-^hVb01kz{YAUx+t}Z{w0qe8JG?8X{;GD*QYSmEqf65UNSe%474y$36j~`dXi`8@IS1r&mMybjTLgU zv-+r3sE;v-M>ES0R<13E0Ka;JCj<{fdARu1tw!$h!iTc;cDf_Fs$Q8Qq6)W-xGN{#Q8W;F;#pfdG zaZEq6riobN8CK9@X@$-t)0d%iq9;aySi0F7sE5)P6n+;>b^5UMzEsk(P+N2 z8NzEpCg7pL!bL#cu>iqJfATaOmtDC2_?kOa(Qhd3N3yy%q*Fd(EX?Q>iUS~o@tl8o zOFS=mMN978nS}{sR~MXhdF;wnk?qyB9FJ)=Sqpqz1yDf52pMOfDhtzx(nrJ;8t>UOx>OUY%QNanpf9LXYJMj(564c6aX1g6SZn7{fpIOmdkdNIDo z%X7l(0fNzo>=PS6&s|w!dEW2(FaUe%bzRA4d@-r(CE$*AX;`L%RDPoBxal-Vfj->( zepONU+{)NMl_?OKTaPwxGdTlnBH@+4-IC|vCCxYdIfaUCk2=#Gagel%zS9A%E@w}C zZ!Eb4uzfYIic17}%e_#4bh?5ca%Mn~ze3HxlNL;LBmq_C$ljs2g&8E=Hm7Z;WU9=6 zga4ZEx!*QI%+oE1_;BOL%wRemR2nA*vKDdN{+TK(n%YBld5;d3+_e0y<3C`ig z^x?d_66<{{MF$^RdP8|oje*Rsd*)muZIQ62-sV`a|QO;$KHZQp{8gZ~dQvHeUey!som9yjyF_r+*rf7H^19svf3^sQvQPC>+)a5DQBt2>d}2isLdY&p57e z1MCt}V=KznL*Z~;c;#!UEFV!13jCdOw1~hG#AadG+-UMjsYyD#2&qj{Rs(U@4g0Kh zIl^@>u%+#+o&uU;R|Vz~h%vRyJFd?}qa!RGQOd5i$bgVWrHO*Wu?z%zI$PKMu9@Hjh(UW1h_~4^_S1`J=H3N zJm`JCLMUX0ftBQTJu5t%@*}*!(1CG32JGk=Pb}gDKSt(hm$B>jZ_>t(xJRY_B12x=*1*~Y z@53<@S19AR^*Qf_pK^3>t%vOn=Y#R@BHu}p(H_h%DZieFfZ8M@9fJ?dWA_u+Vsuvg_mz0VP)F5{0ofKeO!WI5JR8R&g;Uzdg*O}qukrr5XI_B^)|K991A zOhSi(0xCs19J9N3LJCZk{?J=@oAJXqcUX0#u&9s4*h-Lsyvuos;NpcO1P-S7#?3EL zle}NovD2$S(j=^kI`0jMLVyyFME;y<-6*duj?)v?>S16d0L&jwk@U})vp9|`B5824$cT@0Jl?wtmEv?n)ftTp${D zlO%uzvJP>tD}in z=<~ntUB}kHB@5t%B>n5XhbaL-H|`!v00@Fx9`#sr)4$8W_G(8`J=K3d{EQ z?rB`Sn8KoaPEPZ5W2uJ)cz<`5(WNTW;G*9^@p7}??2}&P2weRagNYK0XFcCNo74sh z{QyZSvyVT!k5`D4sq-z9TeAG|!H!ud@q3lVwr=uJUh_bh6;6NxeC0FH|H(h#EoXRB z2rw)@F4=asA%SGonbs>!(OA(<&$jcb*!hu|BH+w|bLOId92zWFG4nLlqO}gXo5M3_ zTlNP9kMd26DunIPTW+FynF!%aWwJ`FG-3i@#wT_@oW?*}VBizI1T+D8l{H5!PBFi+ zALAFRYl!`=Vym|U;Dg?O)`^%*h85R*EcEa)ygEQ6mS*+p3)g+v&P$3T`|7$sI_M<$ z%7K|c+?TF0YUREM!!mu#di?VQQByic00E!;3}0>mK*rXPz>j;d9<)%tKnVuL4KeQI zvtKnQ$fH!cG2Dy^zW)2O=fKH{u^z5a>DW9+$TqZz!}GVgzV#XlTY4Qp8^jG{@8IGg zGx3cAHv!mh>>y#P@Y_d0QB%lWg17Xb@Zs?|)cgU(Onvz;>EO6MET z7onFx?{}t;Synl25DrMgl;0@CBS7XoZ&^Q4>ag) z4~EYW6J%B_FDB=Q=zt%Ca$?U~?}#Axu@=6HMMnQSv+M#6+{oLE1K1PAK3f)<7= z%g-N7oR3rJP2Y#8vOoX}T~tPq*1AeZO6g!@2x$)_1~&i^{AC4F+f03VKKEgl zYTyxRZP|{i|EcEz!`)ZhWI-Tp;UeCm9_{r|CuKay-z*zm&;;)=xxHdu?|P(|n5A2U zvu5G|j=o=CU;P<*#jD!aV}~;ZSai55h_N73kXWW;Db+PaJUF%8QYnTw+|aHhWl0ZQ zFB_J`zkg|FSnWVrU*a=2GCY&~lJ+LtY0Z)EWd zpd*jpg9!G?6Km3!2qXosIZW{fg|Jm+w=9>H%upffhi76HO4fC;-)gYCpq@Wn{1rGr zKB&?@8>Wbhu{Ki5-4Jr;-e&Mpmbm@*8NGc^HhXiQ9SMV=utq*M&2oh%79X`vI5I-) zpI8UdJiO}ErWY7a*4<#8l{3IWTV?Lcf2u{@*xt+i+&?Mj?jpJbm_S69;9?Wk2EFo9 zbIY5chFE7bz*IXZ0y1!gNN6u}IgYxOt&gRQ(M{AD*vg&e{opVw&9r+qlkM0cW+(I` z-N}kg9|XvzVshQiT}I3lCqnJ52@(+alj}w#U&l;LVz3-Vs>gX8r+6ks2f7sS;uf_{ zR5mSQpsR|62Ng6(P zyFu&gWD!8EXbNH;$S*%~hoX4mWFqyn8&euKmT-etlYT%rY__cu9(6IGC7>30Mq}Fs zu2o0*FRui6T!nck+sq}5ng291{VH00{Pu?!z|C~ic5RmcIe2YFysy*i3$;^x=yZ=< z945shklnMZCeUknoasah5y8i=jwv7j3p54C9nuaJI3o2<=O_W+KPOXiM*6cZl!wg< zfFP?>_E#1aXzaySAZKlNIh>Uybne%xKg3dJK(U^KRTnWSwkF*a!>9AMU8d5}&mey?&LYCHMeo3;v)~qD-s-_rX89fs z{w&FsD$L#5TPV0AxQ-4A|89l&if9rje?>2pDPq;n##dS+VFet203Ok@=r!tXfxwP3 zY`1$9+Y|OAFVg+gSxU(yRYaxDJ-ZXbV~^_LFs+s~+_Rovy*aESW<@mzVns1ud9RGU zO_zML4?9loj?Ls>)+{owxB;MVku+mG2!p^BQ_gb-6^`pw20uXJD^jTLG_rBe9c|(P z6uZmMJ0D(CEt0cp?lc>xT?0ND6vPF*FFTKY{}G>kX*&z+mxn%&Fv<+vQ~?{WQ=o#} zHNu2vddQw5MR?K{w1L#P#)PO(xYO5UK3fyV;!5AI-$f(wW!|J5%qgN(5RBBH5m z_|D-drPd3TEsLKWlq;<$cIi3vDju;uoSaX?n_EU*Js>8!O;zu*yJLX;SqWtX^-u^p5$$H!4fH@Gn~ z_x@pH|Lm2F1tj|7zT$r0N&$7TAIz?C#`e-!=?os!)9=$G(Hk;O;0W5iT; zD0JlqGHXfdGM$R$H>9p}Cb3_|^qL^M=ezZ6SM-v7n8m-vnjo-+o* zSk;jI1VrD?X#quN4wa-po{r_m_gB(=Jb3eJACiF>e)|8a&03wKJW&ZAhi%9i`aSu= zJMLC=Ct4L}5S}1MW}GYqT-bvO^|AUxbAeZqN~eAu@w)aHl+i5cVWz(#!VEyY~7Zko{X+a!}=RkEWxEJhk*!02Bu4lOmY=@(okkqDc zC;UTGrQ7nUNkERy>DVwN?-eDC)6MnwfJG6e;j|YVD`l zq_*dAMaF@7TL!uKbQ=h5A}!J!1tBWv{X0VfH#|Gf zFKN;wpxICG9_W=#P=AuDRrY~T3lie?MN6zfvRB}iGzmiN8oLe0maXTyX;f=|=);6@ zAl4&eks8Dxnbo~bM4fhtpB03#vBux~Zb(4W$>$Lhhdcsa6=s_FJ4U} zoXT_SXPsR7CE4>$>VBIyYDNFg?Y`PGDeHNaR zugsr3mSSg{9dpNi_oQ-A&m=+_pA?JDMxES4arTcu-xFO;&Q1=%y|Qe`Tgm`uW;XXxggqA-`ZoJutq#$K{F0ItMvv5)<|r zFeY`?!;kI!brs>A@Yb~HXsMP^^4et@MvG%2p^MA(WqYN@ePbU%m&{oWng;)@jjS>i z)QT3aRr!@Bdtzjk9p0}--&(ph??Fe+hA`{}7xS`dEVs;RSyB%Au4%(JCABv&N%Q!y z3eoNYPAM6=QV*tqg6Z@MAZ3O|RH{dedRj|I zhF!o@zq*stxffn8ghq^&6DL+yBs1so0m(QB4+Qhr(wvmDaXAsZmt>Nu!&E zfi_oYy308;yIk&}W9(n}-S)9|Wiy5iY{hB^PyUe zhUPhG0|zpc$LEO{9IMuU>l@9S^P)ME4BVnBRtQdKM{Y^6 zBZATI=aTJ3!>}7c0cCE~Oa|2>OprC;agjj8Smk1UWe|v?0~c zV%!RKP@W@LnkAy|TUJ}&l_`~*kK8HrtAPa1KBQx zvFU#4`|iO4G(FL^r2usbm8i5~jPQ`dFT?5k2Pu#AwXDuUA)BM_PzRmV->?jlLWGaR z{dc}B!N#zVsOmDDVV@CC$3EA<7A+F{fm5|O=RqHtXW4&Sx1iOq38_(hk-WtiX7D zPN4TNmjYIUfav|1v*a8)=N}JS(SPmCQ*#%b2+rfqF$Yw*1j=%I>ufZpv=# z%D_HzY+@mU_E#MI!MxZMy!tpL2~a|+s{_7g!72~APmksH#nTpLgihvR@EF+kZx-j^ zQ-6=*WkC7T+O`KwNwzR3w1;p2vwySN5XcC!da#lOX! z`H)e=JTD*Kt=c#76V0fCXH;%Hm^y4PBZw(=>Ch(T+p0_d&;)6wI12v;Rj?JH9LouO zobg#UDOHO0-p%tY4zIT3r-b%AB}NKreeKZ_?nd^wOjPnV+*ddvPqLEvh6StHSVz9) zaw1zGX7rv5pEEKW5|nA-mfAE*dF&O4Nlb{)Sj0ixc_}mQmG}I_5k;4uj0B3o=q1I` z>ogXx3Pg~cAUOUrg&R=mhnYl_;-CO@PbLY!IR=Fo?htr>#izm}(DhU*Y!rj$flW%Y z^*6Bsq@16_QR8p;j*g3r(+U56a@Z85$&hY8`mQw-4=kD%yt88pdHHojd0tsC4>Jox z1In!@33rmEGh+9PcX~6GmO|P(#J~@8iAVzWt!$0aai?=FyF3cMh*KpnX%vjDD|!o8 zLby^56Vw;ztrXu223{zf>TNj^wr9|}*H?ROYF6}T`q=G=r!m}5QKQRzz$!hi1oMoUs2uSucO&$`78>s`z9Sov(>!(cOyR#NJLdY*)fOh=-IR(D^KRW^ z@RB1{UqEeboX^r^NLK+0*46t5dmEomeAv0I>0cU6Gegk&KxDPrs$We5`9e5 zStum2<4^Jp2BaXL0lJtZh;f1Qtr`@|Z*t+a96A$udBxtZ)AY=U^w|SqT4O(o3(vU* zhjIzTwti~ky5nzbHt8jy20nVDR4d{Mkv*om3FPbz67A&!hSCHuN9@)^p=RyMH#b&Cz-xBeXi z$*!@8{vQ2gbJaCYl9BxAHF=HuU_=Bwg55S);pYz+id#gs8(VHK0^@sG0BmdkG!%Au zIv^g`CrThU{Hg`KZnkNpU^^^i11E_hx6$6)v~G(O?2ny#KfdKD7H;xHb}S4op!7OM zdnf+zhUTfxf)IhS*C9eFrrGhnrc5?F_U&~9$s8=xiV9=V=WYwDL;q(T+no3_Q{P0w zcb7BTJ`V@>cbRE+q(>9vBUk4Lyr>Q`n=u0Fu+Y5#WawlF$GELEbGQydGkO(axk{)9 zAPP7QG_8*_n<*a$NgO4sXh;t>Hm8P zm6v;iKH%)whw@U64P9+knadh350CPPb)f z6}lAXh`2!Mjh5@rwrI;!G16l5RnEkfg*U~J4aboUwup`h1svFmd*;tgBqqzo*Xi_L z>zhYnk;Xo^p{>z`mq;QbugA~=*fPu-Aw`8?`g4|9%;T5wQ#Jqq2eZvH0<|Ys;W|ld zOB1n!PswKM2oS1prh428DM!cB&S_?W*{t4jDzPuuzzP^4Sqg=su@Ft)X%BAE_ojAy zBgb+Sgq`u<`7=qL#q^?;c8k3=AsYeN*`?=c|=f8uEahEPzEk`A3T0iHGb)Q;(3W7Y%_GuPyID z;#(NH5tK+LEMsY@Q_aazEzp|4f>Fx5)G`53j`bgQxMPVe_dA<%Y!uLxmkYI)sx3EN z*J<+silOBQrm&gP3EFjjxlaqiA7$)QtA3PL<9I(-SkZ&Y{FanxO%|?9W@@t1V5G@M z9rbz5QH{Wnv;@>QjsZpz9j0Cps4}`!Jf+#VkQ0i37SLz`3D9Fq966)_r@`IuX+IEy zsW`H9BeNa5mhk90OV6PW$vGwGoq<^y99^FsiM3rR*U{)q>I64c63D*aSKFW;0-uq- z6F`lBy~$bUmSc>GzgTJFjt9gm;peJtQ)A+NtwA)md`32ulM~*GKs)bZx-Q!s#&QEA<2r(7(TB;j-2+ zg41r2R!*7ig%6NxR50%Y9(ZCM84qV8|4z-vnkS&aK;&x+c~0ySgBf-P_evGg^cp0Z zGS#`r5h`MQlS=DaT^zF>0`h@A6{dzWs&i;05wS;}Nx}~qNmIdR^AU|PNLWZrpg9d- z-*|dE+!Pq%CT0cIK)jub77KBxwgBvW9@F@n04ql>Oz2ib|FCt8)Fjq_;x)Zw+C*14 z;1rArqIexDEayDWmGza<)6_jP{;8H=i29N&U`pZ=H6yy+K}<{%JfG{(#ceRF64l^l z-4gHPgq5zwz}<7`A^eIshMDYs2XmC|pUl22u`P_F*Ny=yVx>!s2r-x+;fTVAepB{) z8CH!)UEh&fX}<;3RNsK!cC7FqmL!(RMxKPLEAXP*Q4jyFzT^qV5JXES1jkY z_lv&fVBg`q)X5quF@NsJBco%tXfh6aJ4$Qcr?^bpp{y%5tncSQ3n@+w%}g8!UScX` zy!0#MObH1V<;+CNz8*g?jrq(K|NXjS(_RW_oVH?4O1bXJ3uN1X~x5i5w z>j>$uO>pEi8f9aM2qQ_cm43+p!5VmTWivbONTbxW9UD2KP<^b_|kKyrB;)ZIF4S+ zG%v39$>skGH0*O~>fio^4xhF;SYna?5^hz}*;(ey=Zh|ym>yLB<|b^@g!`fqX#?x8KGrXCbn=h~xrW$Fkdkhkt9OM*y7UusJ}%ej09zR@qH; ztwSZbH#bH~z98tN3pYrYi*^Uqg}3l=2p|w{63&zR%~{_hY%pCf&uB`^KQ))~ zI;xz_Lv8J}{mkHCzIq&5al0|V@$%?Dk07ZxZ*U_;wKw8{JwZ;cb8cC9ZNmJbdoUt) zYa{BodAZ5K?qGPg$$OCq)PzSG$;x#*Kh}`tV8RYWw!G}3=g0>L^Eo=X%l2Yq7T)Cu zg7f(lhCI8>>sSq#i~?s%?I`mB+kjc>_mo?43CquQe_0)r^4m8B0r|8jbKwsGY>rgl zHeOzb6yeEuDAacNSim{5Zae7WIP&18ocg6Mpq2 zcbq&*aEc9mV)mwS($J%B<5EDj5y$`!*=&e0@^|rC3aywh#8rtqTb@md+b9)YAk0QS z9TK|Bc%1t~)rLX?@!5y%G0{6wN_VPElo|D?Lyf=6hbdPz^7xg;0avV&!nlWENGZOh z^huxOOgmt@ZT@E(3xT1aB#`PmZ)H1QbWb=4QRWZppFhDiZAGWFXu$~~d&wz5RACh{Bu=YA89 zIX+>|4^#JhGnL5dW3V)!vqYe4wcPo0%=b=i!N=Ic*ghO!_f|^Y)ZLmNi{>vA7#^Vu z!dQIX@t(Gt|6RC5T~Nq7ZUWAvwjFwolG*I|SLM-iEm+uC3|wP{5L~9PQ_FCJ)GK@M zdIl-HJm0Ddpvgi7$1pSBoX8~I%{!#h0KZN%Xmi`BQ>DWW_4Hb{-i^-dtL$EbvQm_f zNS6uCQwK;_!kc}|Y?JZbSt1Vs-;N`vaLX2iuKz`f?hsrXMc_QI(YDa%*ZqMWj!F&o z6Q9qEEuc(i#&@Wo+EA5F;c)S#s_t``$RO5MjlT(Q2cy;I)8-&rDqA$@necD@Mo=rCTz+#|u!p{Gt+du#|!-NncNYF-9^wn8_ zC2PPa`jmo18a8ssPbHxx!CKlER>iHkwn#)_WpM;{tbSek17S0@UNY|nuE`lJ2GAa< z{H6zKDLI+0)SXqYO8Y!>D-mz$%EebA(B?O1B*_nQ{McAfNOtA_X^#^Ydu<6~XIEjPh>IuuSJ(B`{7xrVw;%!jIXiSfo4C7iTpJjLIG zsUe|k;KoYUX@oVLRFQ9Bx75h3=>cq(?#!Ob&7FS<)+6rgUL+-@VPe_UEuD;sS5=l|CO> zouA%{aM-ZSpo%;e+1rRCq@$e=;;PaY!g3CmFQS;+X%>omJ&%R#D|&6Fq0Gk=YP1{o z>0J4*Hk&6_7#V;53BwaNeI~Wc5;vBah4O9p5LU`&cs>yVLnZFVTmuaiVW=Qb&biDr zPoUv<0rZ=sXsc*i0I7{MRqO(cXF@U2zcRx*5vs^Ez4;ipUZU{Pb{;LK6Z_^+3zNWM zLKYslR0b4%V%X5Yx}lmSm+BmbO(Vt5$s%JTW7&VI4?tp`i77ybJsh7jPq1rg`xO;V zdfux~x6I63J)MzIds!yw<6_4tmk(oJud$wZ1ax_q#N4tn;|U9fBtmf9w2O81<=89a zUk7bjbstRy+GJ z7NSe3We&z~u|=ktEf5)GJBi83$Kuk_f(N> zptiNzg+D4SUc}!~Gwfev+*{iZg#qhIyZ+0m?}9{qdM^&YN*{@BM+PK^-AX|rlKuFu z?*4-NMH7nSCCL++a}eMK^3W}biuXmEC(^%(_?ciT(&?~^Y#HK;$wwmXgZv7}SxQWR zgcItcWy<7^aslZcjJ-ArXw4d5c=>+6AC=rl^)Zp%@wUErh*OEBxtc{Up_1cN&IBg~ z&0&|;O?9^rHXJ#bSPg5|sdaFO)UtltwBKu}A-JFQ*Kut)9$cc)sek|)v9vOLp;dg- zKAC_EEq!y`J&#XuBQ%OAxh3z+p;6;8K zUUG)9SH(k55?mQVEOdg>_e4@OXTlc0wHcWKt7(XZa zmjEFr{$QAv%x(E;;CN;-ME2sj4v*bNe~YHOjhWE>WcaKmsO86OGBvsSG7mtPa3MqIp?2ZkAh4 z)rFcr0ifYpbo|k5>djtyN~(SIIa5;FImo;^<@am5Ul2yjCOZTXfGn-IunIKhXU+PC zH;wuC8HD?QRexL48}q)E_mr{z+MOX5I$z&=FL*v+FoEeA`!gb18xPw(Pkwc*5VeX3 zK$5yhv``KKY^HEFIy~>3|NX-vJ%%4(-IlQzxOMn&!y}N(V|Lzjp~=;=E6iiy-KJpm z-KKtzKLR5&0nCuvF9m)eJI=gCvKW&zZGPeAg`P_wV55dQkr!l85yfb)${|KJ_pKtc478RGETDtAtvdV)>HdRFVyKm0We3g55* zu|f}*4=QiC=InN3Hb{NG^t$EjeRbB_fdvYB0{Sr{nR7daWQcMG+-)O{PP*CbZGaSvcs`g0ps!fJSi} z%IH6-qB!n14x4bh!5*bq$~t?162D!yP0Zc#cwRu)S>)w`@Yz?L;W}%TwY@ydr#9HR zw4_}FyY0D$X$_dNn-2aP=UhXFTrt`auNxHPEO@DSNTkVP6!a849g3`fXXmm6_A_co zom4y+dq*C8v8T+^N;hb6x}}j3<>;`)c9TRjFL2h`fP!Ck31`7`cIelOQ-pJ?&^XR( zW#33_2bwN*=C(xo3wQB=4NJ63aRRO|7*t8o z@lDS=1}d-C!{%Wa95x8C+CxfZZbYHcVmMb*S27@LmKVXT8OHF+Re2)#*6d*0KJ_;ISujQbfP zoPk~O;%}u-MSo&AS(ia-d3R*s2f)CcHeALq@C%g@eg!$<*wdmAF`Ox{s5-zcQd7sM z=Xt;hdo1SW3I^Co@W8Gt1J3%Eyxl}c#By8TvRzmcQ~|f21A5Wb!9-s5|90^z2cqcS zSf}n2fY($Zh8~%?z-vMpidl!Z<+&xRmemNb?TU`fUyCR~oKP@m>xt$-BGA9Mbnl=w z)Gi`)UPH;!OKyXPoNn4av1I4E7|$ohZo!IJ)b0?+eUN*@PA%Y@tcl{L)AQ) z^`A&ZA9fZBoQhrj_*&8nh*6x5DN4h{fL3>^=M7`ccN|UyerYef<(+Pi<)y;RfJ;#J z{ezq?iDcs{N#tSUOI4}N6)m`VyWC+PKSR_eIc@)W(5rWdiFdeN@<|+PQo3uV$&J$U zvwE?TnH}4<{bdZUW3G!f2_|D&Wt1@MWY(J(tYe8yRtCg1f7lRs5CpxTu_BjFT0xs1 zyYmY_zPal)8ZK3`Con|LDz< zJaRsOCA>W(x*4-Tu*=YH3P~H$PCAOPz6S-^mF*wWE1}$bho5%H28<{$r6s+JlGO(R z{c%)HjLcBaSY3DYU+$w-XgI_Vi8u|-PO)~t9z=x=LYJ{D_^6BwgnPR?m9)mkgcR>dgm5ge!P{`wh;c`_4EktS3=Ee2 zlak zj7>*|_yba>iwQu@B}SWSA*tl9!xUTkivKST1v7^%pa)3UX$9@QY~E&6G|$i}B*kaS zLWMTbeH&qUXX*W_PWmhbui?r$rZiJzy&@e0#V@w>cy?AcNj^$H4PnYH^qv8m-%r#v zDUwEF_>+lSDu3iD46?K(SGzr7wuWO&4Cp70!sfp-%MyJ^tCmbGHvgAz3S~;=w zj}6cqZ6(A!oQULh__?y1D z`*_EoB&j=_(?_8mPWRwW^LtRqAd)15=f=I_u4-D(0M4aq#G&a{QA=*!5A6Wo4D$d` zBOW#}q^`EA%3PmyzwuC_3UJZ2R;>8EAs5nwSOL21woJi5r#W{x4}4?h{(*thavs54h9ka{lL-wue!i767Og)YLh3dm4q_i%#*cAbniOhODho|?=)k!V;a^o@ zN{so~P$RtY(-)RM<}=kD}XHw59#QZ_yLnZ*dnQ#*x2Uv7jT)wM66*R&0w5lWwJ zkmlM?u;%Ri#BuS4&QtWjov_4&k*WbwUZYQPj>r#Zsfd3ao*G8xssG9p1yPEo1?cO%&jPLG|F! z3gNe#)XpBVi){rSE#CU-0v7%gcuT zVAfEyA`d>3F`YY5nO8{bNi`CF6*IN?68%0BYL9!uv+hP`76yPX?gKv(1sd~o@Nybx zuWo0MsUvNsIMH>Y2uFF`Y9buMHQ+X%NJ7V2--{r@?qpem=jD1sUyI|#{^I%s7}KGY zeVnhvA>O&~AK?P%EZu+>b+PDj9KKdWvwb5Y&nkrSzGr9R?VA#B?`&NIPHh8S% zvy6XiPNO2(QY!x|ryMqw{0QVX#0WAwk;^RI+}7;`)L5~jko-5;v-7+x+0W7RO92^n z$J@A8)%oxSnEzn>(EI&rn!y5mQDqGE5uIj6^s-A>%Uw|oG@=oJ$@BEwiiWzy*@ zgboi)tdL)SAjuy{@(S+fB6Bl-rQ#)GY<*u9a`)=F32efd?0~>AaWO-mJX)TRoMOHD)P?Nx7C8E-Jr#g$%nU|2cJ`2ff98R(Ux0=J&&kCN z-&Br-^DN-1K?7YX@d|5dimESihxh5sT&e6%=0&+3QZnQswLnoMP-Bkc1d46=x_+r)dDhFc!WM5Y!p+#IP;q{o&kb zL>_mmyW9PwMuG?6=(t0dzAiJjY9M0I264lYRY?Y6e;Frf!80~_V{B!2sMcD?U~V9Q zu3B-kcO*EiuzEvhmJ^0UoGH(V_#oIDIJKzxYQO-5f7)kA3IhH`;zV3hXaJC&6rqGQ zfFr@!5mH{9{+;rSD!lR=@er95yTzy!twy^&2&Z)DJ$?}0v~KB_nc{<^}ir3#j zRoan24g8GiPyjB_8>}Og597x1Kr28M)KwR+3stYs!6zaKr07;EHC!e$b)5If)(xuVKgN3g10zQ zKSZ@1iD9J@#S2$R*Fv}_qI+}3nkoJc6#!5LpQ5xz41o3ij~TUJ$2+$0R3|>Nr9+hs zBJ2s{)?;;t`n*u=4=>TTv|)+RDk6*H5mtf%GVY%XMBjHC=;cQh``1CJP-gdqlb0S% z|7wym<{*&1~4Yl z!}BN+YgfTB8oL<5nuUERZE@xIjX#MCPMb@2HSaapG6}t);eem?l{61|uCe>$rDM{^qwK$ z$GmwDgz5%9msCZSjl18lE%S2{kuBS}uC$VF%OwO&8`+XQAv9I0=%Q0#-Ofmdwih|W zlzPMWP7?Iu-q|>tlb7IM`+`w=HF19417a!AK$)VIEv5J&4Bqjo^!v|Py2}RKAG}yE zN!+GfT0~(x?9SZwzSFV|wUHmGE^IsxiQj^ka}rHz5O`^|c4=)6dUA^&XVHt-Zj63l zUjgCbd1VQZt7YN$w6lw&?U6q13Sec)&P4ds9vRrg`1?kUFOt1jb0_#Fg z=j%U?&>UW0UID08N8IP~lO`eT>(PT374W?Yp^;@C9Qde9ISj~OpU#kqW~4fF351tY!ZKZ|@$kmqJGT+9f>#Im(9=QtPT zo4$tA!HAbVtu2U@I$%}rEexW95qYjDJSP5yPXZcl2|p5mutZ`N*+#}sJq5WG{o6zb zV=frs8-m76VUxW-TYYfDQOPB9ABK`z^^caWj1s{N&wt&YBH2NxwRunD0fB+ib~2v= z+|$2El*8j<-R=^J%Li(7P^Gb#X0ZGI~XLtw1u^d(LVUmeJy1z|*eGa^g!% zKf>%aYHOrt!rx7&lnMV97(m+e4aHx0&lJOt0t@-a3x}S0`OIC5jjusLMw9eaQ17o( z@%}}SxP6@foks13sfG3^_{`QpU#?1_;e2`dK^H*Ne)}c^yrs`d;`jm$SU88~f6;x0 zAyLsieF{i+ZE{9HcarZPW?;SljIPWj&-e6_>nDWS0CqkLZ2bqjo?J|i8&h{TLw@U! z0dK=xiP%EyWFmP4DYtQ?hQ4kdK?R5tLBq(F}4NTRKv+uk8ma3DeRibd2M`xf{d8V^<@}-m}I7&vqcN z3aXHmQ$l-!m|1D35RU!MdzSS2imRy0$mfc*ZL!gS$}_O4hn~3~9uTe223lN@|Du0IwCg4SQ|u*1 zAnm5(r;BnwVct8Mz-UNDAVA;~F_}Dkp3k8qhd2qcIM)cr42vTR4JG`)D#nA1%{$CU zZ{UrV&^!R-AF$qbA73Bx_D-7ltydXN`Adn{eVs}52en+~XG#DvhnSLiVkF4^oKN0q z3p)hb&9<+NpQgl{a3)8kkZ@c_nDCTFUb(I3)i!U>&f8H_ z*daigB;zFs1(%dmlOiw!{&JU7^$To!kojy><>_UczTa`6C5flOo3(w!!eHaV3$9LV z6EG^JB!#AEraFbgink)6h-Y!wKt=ew1+Og?IXic%GBd>bmZbx~0N9jeP~|9>Nhs)} zB#C^f{OcVATu=K;NmIo3?^+L@&nefmR;7_sWd^m18AGSA*RTA^RhKNSWQa3zBX{X_ z)JcLq3dBl6N*)uwTh(;g4lR)%U{sba7gZ^<^}Ie1^i4i2mSG-RWh%=#J`X+xp{UD5 zIHWkJc!|t(OV20(L!|_>B~OrcpH`50U$dz=01Sh~xHsnO41S$UjDdO}OW>B6uXP$a zOHA-3f>*T$RR!7|>`0{A5>;;D0M~!nuf)0`^h&cGLMTOt>Q>#$>R@^3)h*aV{sX?R zGn~ zNFzit77tT)9`%8{iC|mU-vtjfhD<+0T=M_4NxHMwj=9mXl8ou9+gVV@h9Bvxu(MdJk3I$qrE;xn5 zrlg@xwbyTaoI(dU_~%SX#Ga*EiD*i#VVpXnTv#k~2U>T1E&z_9%@R)<$Fuoz9jXWss&1*v>_3?Nw-D+{5_@G*erZONocEo9c`q_>G)+Hk7x7c*l#;?| zV+77{6JBENlA<|>-qcZNOF*>t+iJ?gqFSi&oPE(M!vmm}Aq5~2X5H>{3)U?etc|R) zZuX&8Z8Z&)*tJ8qUR$dz)6mr<6Oz?O|J#)T#JK@SN-@OEf!zip>{O#fXF3h{9w=B{hz z8waLEU*@h-G*uzgNzUT?FE4)J{&rprvy|UCq;aPX+QmLLG6y6$uiL!slk#SEbIK?B z`(CbUnR5#vhs^Qu83qN}lo)=T@o{!_LXtL2m4!YaOAc^dCMB-ZX@wC8Ts8-XJ`j7y9uW>nG!9Fu4GCXkb^%^ouKXW)v#utXW){J8%IBi(alj<9uYrhm zi3+^qr{cq0bFE_9FE!$~@qV4ChPM|6S7+jAGiC(#@s07gdKI4c<@B6q*22SU6yt&- zes4x*6xoQLKwuG-zPh~bq}H{y=-Za;?3C{cKpv(al;D}Jvs0$l>bR}5@Cq=82Mi;< z)woi=2C9;1ibfp>A~^IQW^yarlb&H$C=#Sl^5Y z1LyZ$TdelUhQu}pM-kDFJ!_k4CJdc`IZggNCYNSaRqCy~ho>Kvs-~R+yZ4VfXqsb+ zPP2_qUVM{So1yvM#?`BK0x8=)S`8dyCDZ3GIhzxUCZ>-!X|c)23QoAOEbutA^wn$b=>?=5jolQVf8Ryr6f1N09F~#lMhCk z7@ESr(McW!KqO)xghXTiaNFU?0xD5BLvbo6=Xjoec9gK*B$Y-%3*!wsNYxAt0{V_% zK@}w1&zhtXSk#R26vX*wJMO)OOqS4aalM@w5WmD?;oEoeoL}l8QC1Xi^A;+ra52S) z+Q;BD6@BPB;|*3Y2RPc18zTh*hYId^hl2P&3ew(9K$@gYDRt#B;1W#S)0my^d>Q>z9%H3eG5D$!0K;MkQ!fpV@YVVFoBabNdzgvX+1IxqCgn&>&ZB-uw=gxP zx9DAFs4ZN6cdBQ>_%#)ADqM!_tq5pA9IJrYvF~{}&Nw>8G9;O|)~-eMZ;0~b2lHLAqAKRxilaq_fH zOMN!r$pfjJg~U3}UEIL6RM=I+TNH|NF=%$lkk<;HprD@RdUN6F9}9L~=*9Q5&J=EH z^|L!QG#yLOS0$MaxOV%>*3fxv6x-N@g0C)xvWNteW|3r}XL;a{DpC6JMP`nn*j7_9 zFws`g{Q1mb1n)TitN>rwCOdd_crK?v5y;`D=nh16;L7);k%Q{4Z%F zUp_R@Ag$G`dVkNtb3;K29{iAi>$#fb7>9&tbQ*V)_i9!!%qZ$=pHpuY;g+?Qu^vbe zFJxW$7FU-6bo9c;}QTvUZ8Od^Plk_;ifOE$IMc6%HH-&G)VI9<^b@faoni) zaUq~+RhLu!zHZTUWk6$aa~~vl?1qQQ1way-R)*NoR||0q%!m6(Ju0T#nx~vt->b^L z5|St^ihp{ozBHg;ScX3O)Tyd>fI>gB(!F_iH2AGA7U4@wsL%8IlMKCMU69H~sw!T8 z@?QSwk&;+@1P@HP&`5a)PI`NmP`i`K=K=$rh*O3yW|mBd5=%RlW9)@kC>UqDbCJ*X$Hie6mo-Xef*m)hCxUz;A{MUrZ`Yu)V}ORpe{CVIS6my zcD8wL7!OZ928CX1i)T3tDgLHZNRi>8V?Sx$65@awzH6Bru!Omgg_TO6HZ9qQieU#N z0s`@fk^w|OL-P{;Asr9Hil?J(vW2FbdHogJ6Cgh`ys!6#k)aH)M$4re{@+`SIJMb~ z*3waOF={w;@ap>%8AJjiae|A5kzHtTwZrMYkd*`&|G}=18QcyRpJjYHsi$eC)}|WE zSCTP=4E@AsO9hv`pW^|v%#pPB@P5DmsY38X3L%iC1jr@*jeUt!Y`T3;8C>FEI{FVb zzAxc2J)H7hY#MiD$!nPesU)m%uaoZN1lRYchKMRq8~t0pC^9oR$Jcd;4t;IY{p-7q zBSnnc4(pr0nMA8;SD5Hr}vx9#%AEzo5R}!z2D2C;7FjimAg>oHwFS}SfeAL z_NNKH!XDksJ|#)TJRQGnfWvv+=<>1>^JWV+>Zv-E07V&0xAj7p1Q}uOr61T$jud-^ zk(9;V6bE{dw4c4aR+$)6JY?iCN?hPQT;6uvjZh=)bBQ%-HN`5$tQS!n-HX+Zk#Bu$ zALa+NuoDUfx4G*}$|ODIo?Yno4%1me9QG^_c#oB4O4CkdKJ%0oBsGt&NKnHr$nuKU z0(FzP#PtTa=y((mAmRgzuZAdBl7qPq-ysY^(;qUAKM9zh^^}T0Az>z1N?ElH>M(qz z_a0tsKK3{&!u(ROQ zQP=^>D#Yep1Gu+GBVCgg818&g!aNYepfncjI%*4x>tFDPB<1yj8nC)NC_cI2_Z?g# zoV$lAH+_VOLV!Z^XM1$0FV=+smI74&xW~SF&H8YHP7G(_Y%2Xr9@^mNb-CUL$CFHO z>kRjDV8n%3-kuXTzaA$UU<|o$N*4Ip?1XIFP*2Xwu?vt)t2mW% zou-MTG$hKQK4mb$UMJ`#ILkq*M3N{5YS5 zIAuvo7tSyH=r`@`&%YbBhv2{}e;%z5>{}aAfzWpwSysnKvcAFtuZ9I?EYWasa>TPL z`@xPtER_N{p%Aw@gcKZzWlTTjG~f^BOan)Ba3%nMr$fnNp*d3e9~zxtbZ05XH7~L& zhn)-4-ksCw~`*K3BUNBaY4fBBr^zt~g*FrLFGp%DLma97!++=VGd}hRh zVDKgMnr~j$z6txadB7vLVc(h!cGjKZ!aleTjmmd=KG16c`nHBuM*Co71Ke8(;XrI( z@>#Dg`>u<6z)u8TTBslA@ni^e@(;`0sIE!GmSy4>^Hxy)*GSO=j8^w#$UCg!H-Ykg z3(hJo=3_t*!^pFE=Vx4^ev^f^C&D^2hG%~g{z3XD>j{9vi(-L3DNr<{;BW$g!-Y_p z5JQM7of`~GBCMD0WLc<>lV&_zf^@W_%zYRcx}S%i7qcPG)r!5km9PoeJMBYnUvl-B zmqoV&o4EHv59*!}K9GzTt7013z*9G7^8Cff`al2xs@H69_#&wqFVaQ{fNg&q&GOD) z?_3KQ4?+>uX~$g$_Y5zqYxAK;?!1#rW6_wqfrbdo;(^acIheW1C)YfT6myRu>7fGx7L5Ct0{UasJ@5fh1*K z9~MRFV|@}g48M6_CG^5&7EV_r|Ec|;ul z?~d=)&si2DN_5U6&i&DY@e)r+DAAkt?H##WP87xA{2gu;#_N`#h5LD=sKR>5KtQj@ z2{)Ev!&pFGs$@8DU3oX0XkzI4i|1XP=YX}0Ym)x)=>I80C=WX=pejl@l&R?NFmo#K zTX!mRtH=n-0@1v(@6r9oFm1g@P*dch*NN*#>R^G5PRJGa^O#>t7a~pCgJOX?RXpO4 zXf{p+Hz9Z8{a8+qUZZ)0PDzom>6-74J}ME_yq3!A6C6>QG6^aI#E>Z=_lCq07X7| zS%&A`6zB&LJC}B?gn3L%jp8rcyGm8j360;++M#3e3t@v?Khq@3EJs0K23jzkgy)sd zi!+~h#w=)Ij1n;RJYDp}c!y1mWG?YtLxIlcP3|U>$fh|9wwBXQLc$sXMY4$`$hpW0 zFnQz?W3tI#iw#%Q{|~GLie9j9)OIm#;>UuEI9S07UFLutGJugdhPcMbt-&<(048ZP z00Edoy3@IJ^jZ@`VAUtu{tYPx;|uxhgf053vGC)8ag$<9_7KxxULg7(!(6oAl}aIf zPLB>C5@J+<2Dd$ox87~hx?mCzq?8#5-q3{vR56k9O!6^T{5MrgpN>4Md6FG+Vv?q00BWUr9{k0D1}atGsA9vQje0ZCvB+slO4p}2XsVC& zO##xM{m~8Pz49t-!Cxw_73fm%Kk(qi$TWfzdH@A_d`0a;DaA#5dVz}HXl$_iY*S5iv z`aFX!Q_&$$PNH0Nx2Fkn*wlp@j^DyrlBu?b-fiR+W)eg$;M-HXZ~5*MlEmO+&pQQ+ zWpCFCt>jb#53;Z}x3rf)bx^P71 z?6P1*9)#nM$F9OZ!Q#>#gC8tSrdX<} zGPk~lK$h+j@KnlB1vrp|x<7zT1@ZYqd|1T8oCs_0Jmu@m-F!|~!c3CFF6z4@HCw^| zS+9h+*rI~^mg%@}+5Z&4h)A36{x3*>3}|CF&(=cHCm9%oIx8~fE(k=cV)#o0C&& zyWp~6#dJ^!t>eU;;X=2*XIL29?N2YSlfnh5H0>Qw#CG;T%@d^NQ^~5RRP{U{_%vxE zU_3ZMJATwm6MJ7L&IO-;puw#+pa|Aa-niIEqRqQV|7hcQ;LsQ+e7r78IQj<8y=HW= zb`O>eZYBk&8qdBy=BS4J%q~BwzGDg+M{DKCHUebKiJkoNx+xx&v6etOL+R#a0H)V% zO@6j2D*t-ZPa#({&?{vQ`)7su&$}XwSCw9#VbI$Q>Tm^Wcf_ z?aUW1JdVh`!_5eKcaWQvLjG1f$%SR#t2TCisPMF2grHGcM^i5b-r`@(wlXl`yDo=( zh~Sf-Dxu{d*NC4nw8K;QwwWiuTA)K`;`(=s-g*-4!sz+##cg#B9LP?tZgiZfAGBCF zz;ItOHMF1cD>JYFU;tTfES0)5^aEja*TNrkANCHaW9rKc-sw97^1Tv9HtyN|@j@Z6 z9W7}!`NHF4c1UU~pH*+)KI-Y5jp>wdiClh?!pv4Il79(iOP_^Za*|Qnb3lNFkl|ps z5=sXRSz(H)_}8#;z4dX2>tPK&jmToOp08f?OR+Lf#n-Nr^s zsIb<9&l!h;M({Q}u+RZS@PuN`87HOwkLTKX`V7Df@;B31U>pv{7z3In_N8v8d*NMr z+WvPd`TWUqqCBxW5p7m-_wCR{g!84&Hd9jWE4*s}to)(%>z0=Y^Hdok{u=D&<)cE@ zESg`{(k*_EM~7Z|4_eB&>ybz71Lim%ib1bgC*MAKE!WXPg09lLz? zs|&%_q-*{1MC03{n+rIqd*dkk z7CjGozNzImd*Fi}lG~zn*FsqRWisdLU*P;u$x>hXWz?1__*A2z zqeeXy1Clg6W%hTM9;u9OS~K@iO#@S>hI+!mhI;lbgJ1Wo#x4Yop69>z|I`-(74lfw z>{PSjQPS&-TTZ|7MMb&|i85>CNM0{WZlAi`!(*|xrP2eXT|^6+bkJ&`?xLLLNB3xlexfjFK(W81S#l9um)#n*Yj>d%cB{fgR-KA=PKi(#%NJaIbvfS zKU#U@l)WV=f?*OQmrK3{X1mwySWE#zalr*JpGU#?4&ZKa$)%siZ0VAR)M`Mm? z@eSv^iT8Wf3pm?D8dJ}oz_o_x6uZv5h5=V99M$1gcsINP{P+dKOB;|!k`?{Gm}`r zVpM`W(L!O6wZpCWT8?O$R)I3nfBK-%>zdxH_xtm*`jJhC8!nwdhH6H`3ytRqO$<)= zD}#x{Y>4C_uQN_;yvTM~GD2T%q!|*AsIm}h;7_)d5aUeNWv6iEkgy%bV#T2wtAriq ze;WS1e*j;u=I~%Ee?!$%nj+<>8t|Or_?Kdj%}0e?MVP8q)V8j3+oXoz*n2qcknNSO z8yq1Czp6F^|B7yDn&wvVcpyu#NP?J2McFmSn9#fX?Cb0h0zt3H>a;NjbDR$>9qS-n zWd>|_|5p23NH(Y2MbXG<0cu?v_hl z=;3!(8IHVU4lH8Dg7;tHMIVJtWBWh!#+pN)bKw(Y1G_;O+xI~RKk&qtNy09acpPLt zC(cy*$l?-ekey#wY==l<5oiILcJTFS!50ABE};h70-@sQi#g)t<%xxI7X9OK8F<;Z zKkjTNlGGNEFit8tt0qKyy)GP94E+Rac(HEBUvtHwzjS`@gtpDX6MNuL;JNd;&Aq@E z*qU(xbQLodD_O?ZgPv(iJs>VFZOHUMUzqn<@nw&bjO{#$FD?fqCeLGL?I9AiBUH7F z)gO3@01n4wepW!H0mUD%Lzld2SEw;!0BN@6nY1bYIRR#(m(y9tqLQItR15RoBOE#) zEmZ1#-Tw%9*1iWGT40bs!R_9x-M&vwQ!@payLk9HC&eaGv_QFl3W&$0!E6Jt?Zvz) zu3G~qGY*E@yaisS;Y#RsWQ=3Ipg-rtnP~>D{*|_zq;aFIPYIjycGX$?E#86>agh9@ z5FryZl%BtGGyr}VbbREh<;NNsIu+Vq zF>WwS`S`*1cH=H7T|fk-Q1uT#lK$uRU`3>sTp?7HsN`=lqidN~^i3T--d(lEbm;Hi z^wG(+W_gV~#D0k}6(k?)&2y%@Z-u-d!$GTtr%Aq-CK;#WA=2bBUS(q^pD}o;T{e-c zB-5bdnyRNRzA-MM+uxuElf5zNLqw|5p^cJ--=^zhDcZ!*o=K^*<1;GN2!%fp`qunO1aYm@1ZPzpS?QC?76S1z4?OgWHpm>g(Q~|-@bxU zg9hCE=9zUY6a(etm}`OaUTC^FtLESaTqRr>4NUW4PS-ynJJlG5_ri}VMu$2>;18W5z1?t2I{%o zP$)j=0H)p!%$48+yw;WqDr~)g!TyqFVPOpyxaH?V=3o~zzUvA_%n6i0hUmlyWA%fO zVC}rQ$-Ck7#~u^&Jo?mWj&;pi{iqf9S&hDbI7WTrhRFOQswVc6t+R^185>(6*2=rp zev=}vPckZ_TbNt?UQjc*K_G8{qQwe5wJot!iJ0s+7Ocgf0I6^aHh$*z$P`Qg7Gfjb zs00CCRBaV^KtpyJqu&QL430uOC9YpkGR#Z{UKvC8s0j~H2!A&{lHL))WO@sOy!wU5 zlo&Hv7YTTK)1EgwtdLTIFr_9UUjxE}^3+gc`m@3pvFx zMG|j1P|sdL-7t)he+oNBiACj>=OL19?m;tv6CfioQInN%tbbK>Q;3~GY5R+>VZA@n zeAu61zXMftyHrHMB5e0`uhqgJjOUS94F{;bFz=D@f_m{=lf3NGWP1cKaxsERXWEf# z=dF+#IC$y4Z7Zn)|7_pCZQhaIeb`Gc7(|5hLed!kAK7mWpX}{Fqyh60=RPYCYTP!B z|C9*`hAjhtpt>q}`51!p3*g|1Ag%0AxVgS3`X!xBzI0=No5-3t&n5}HV4ceg>l)g9 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 +{% endblock %} diff --git a/nextcloud-aio/php/templates/components/container-state.twig b/nextcloud-aio/php/templates/components/container-state.twig new file mode 100644 index 0000000..4cf5dd4 --- /dev/null +++ b/nextcloud-aio/php/templates/components/container-state.twig @@ -0,0 +1,27 @@ +{# @var c \App\Containers\Container #} +
  • + + {% if c.GetStartingState().value == 'starting' %} + + {{ c.GetDisplayName() }} + (Starting) + {% elseif c.GetRunningState().value == 'running' %} + + {{ c.GetDisplayName() }} + (Running) + {% else %} + + {{ c.GetDisplayName() }} + (Stopped) + {% endif %} + {% if c.GetDocumentation() != '' %} + (docs) + {% endif %} + + {% if c.GetUiSecret() != '' %} +
    + Show password for {{ c.GetDisplayName() }} + +
    + {% endif %} +
  • \ No newline at end of file diff --git a/nextcloud-aio/php/templates/containers.twig b/nextcloud-aio/php/templates/containers.twig new file mode 100644 index 0000000..8db6beb --- /dev/null +++ b/nextcloud-aio/php/templates/containers.twig @@ -0,0 +1,625 @@ +{% extends "layout.twig" %} + +{% block body %} + + + +
    + +
    + + + +
    +
    + +
    +
    +

    Nextcloud AIO v11.10.0

    + + {# Add 2nd tab warning #} + + + {# timezone-prefill #} + + + {# js for optional containers and additional containers forms #} + + + {% set hasBackupLocation = borg_backup_host_location or borg_remote_repo %} + {% set isAnyRunning = false %} + {% set isAnyRestarting = false %} + {% set isWatchtowerRunning = false %} + {% set isDomaincheckRunning = false %} + {% set isBackupOrRestoreRunning = false %} + {% set isApacheStarting = false %} + {# Setting newMajorVersion to '' will hide corresponding options/elements, can be set to an integer like 26 in order to show corresponding elements. If set, also increase installLatestMajor in https://github.com/nextcloud/all-in-one/blob/main/php/src/Controller/DockerController.php #} + {% set newMajorVersionString = '25 Autumn' %} + + {% if is_backup_container_running == true %} + {% if borg_backup_mode == 'backup' or borg_backup_mode == 'restore' %} + {% set isBackupOrRestoreRunning = true %} + {% endif %} + {% endif %} + + {% for container in containers %} + {% if container.GetDisplayName() != '' and container.GetRunningState().value == 'running' %} + {% set isAnyRunning = true %} + {% endif %} + {% if container.GetDisplayName() != '' and container.GetRestartingState().value == 'restarting' %} + {% set isAnyRestarting = true %} + {% endif %} + {% if container.GetIdentifier() == 'nextcloud-aio-watchtower' and container.GetRunningState().value == 'running' %} + {% set isWatchtowerRunning = true %} + {% endif %} + {% if container.GetIdentifier() == 'nextcloud-aio-domaincheck' and container.GetRunningState().value == 'running' %} + {% set isDomaincheckRunning = true %} + {% endif %} + {% if container.GetIdentifier() == 'nextcloud-aio-apache' and container.GetStartingState().value == 'starting' %} + {% set isApacheStarting = true %} + {% endif %} + {% endfor %} + + {% if is_daily_backup_running == true %} +

    Daily backup currently running. (Mastercontainer logs) (Borg backup container logs)

    + {% if automatic_updates == true %} +

    This will update your containers, the mastercontainer and, on Saturdays, your Nextcloud apps if the backup is successful.

    + {% if is_mastercontainer_update_available == true %} +

    When the mastercontainer is updated it will restart, making it unavailable for a moment. (Logs)

    + {% endif %} + {% endif %} + {% if has_update_available == false %} +

    The whole process should not take more than a few minutes.

    + {% elseif automatic_updates == true %} +

    The whole process can take a while as your containers will be updated.

    + {% endif %} +

    Reload ↻

    +

    If the daily backup is stuck somehow, you can unstick it by running sudo docker exec nextcloud-aio-mastercontainer rm /mnt/docker-aio-config/data/daily_backup_running and afterwards reloading this interface.

    + {% elseif isWatchtowerRunning == true %} +

    Mastercontainer update currently running. Once the update is complete the mastercontainer will restart, making it unavailable for a moment. Please wait until it's done. (Logs)

    +

    Reload ↻

    + {% else %} + {% if is_backup_container_running == false and domain == "" %} + {% if isDomaincheckRunning == false %} +

    Domaincheck container is not running

    +

    This is not expected. Most likely this happened because port {{ apache_port }} is already in use on your server. You can check the mastercontainer logs and domaincheck container logs for further clues. You should be able to resolve this by adjusting the APACHE_PORT by following the reverse proxy documentation. Advice: have a detailed look at the changed docker run command for AIO.

    + {% elseif is_mastercontainer_update_available == true %} +

    Mastercontainer update

    +

    ⚠️ A mastercontainer update is available. Please click on the button below to update it. Afterwards, you will be able to proceed with the setup.

    +
    + + + +
    + {% else %} + {% if not hasBackupLocation %} +

    The official Nextcloud installation method. Nextcloud All-in-One provides easy deployment and maintenance with most features included in this one Nextcloud instance.

    +

    You can either create a new AIO instance or restore a former AIO instance from backup. See the two sections below.

    + {{ include('includes/aio-config.twig') }} +

    New AIO instance

    + {% if apache_port == '443' %} +

    AIO is currently in "normal mode" which means that it handles the TLS proxying itself. This also means that it cannot be installed behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else). If you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else), see the reverse proxy documentation. Advice: have a detailed look at the changed docker run command for AIO.

    + {% else %} +

    AIO is currently in "reverse proxy mode" which means that it can be installed behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) and does not do the TLS proxying itself.

    + {% endif %} +

    Please type in the domain that will be used for Nextcloud and submit it.

    + {% if skip_domain_validation == true %} +

    Please note: The domain validation is disabled so any domain will be accepted here! Make sure you do not make a typo here as you will not be able to change it afterwards!

    + {% endif %} +
    + + + + +
    + {% if skip_domain_validation == false %} +

    Make sure that this server is reachable on port 443 (port 443/tcp is open/forwarded in your firewall/router and 443/udp as well if you want to enable http3) and that you've correctly set up the DNS config for the domain that you enter (set the A record to your public ipv4-address and if you need ipv6, set the AAAA record to your public ipv6-address. A CNAME record is, of course, also possible). You should see hints on what went wrong in the top right corner if your domain is not accepted.

    +
    + Click here for further hints +

    If you do not have a domain yet, you can get one for free e.g. from duckdns.org and others. Recommended is to use Tailscale

    +

    If you have a dynamic public IP-address, you can use e.g. DDclient with a compatible domain provider for DNS updates.

    +

    If you only want to install AIO locally without exposing it to the public internet or if you cannot do so, feel free to follow this documentation.

    +

    If you should be using Cloudflare Proxy for your domain, make sure to disable the Proxy feature temporarily as it might block the domain validation attempts.

    + {% if apache_port != '443' %} +

    If you run into issues with your domain being accepted, see these steps for how to debug things.

    + {% endif %} +

    Hint: If the domain validation fails but you are completely sure that you've configured everything correctly, you may skip the domain validation by following this documentation.

    +
    + {% endif %} + +

    Restore former AIO instance from backup

    +

    You can alternatively restore a former AIO instance from backup.

    + {% endif %} + + {% if is_instance_restore_attempt == false %} + {% if hasBackupLocation %} + {% if borg_backup_mode in ['test', 'check'] %} + {% if backup_exit_code > 0 %} +

    Last {{ borg_backup_mode }} failed! (Logs)

    + {% if borg_backup_mode == 'test' %} +

    Please adjust the path and/or the encryption password in order to make it work!

    + {% elseif borg_backup_mode == 'check' %} +

    The backup archive seems to be corrupt. Please try to use a different intact backup archive or try to fix it by following this documentation

    +
    + Reveal repair option +

    Below is the option to repair the integrity of your backup. Please note: Please only use this after you have read the documentation above! (It will run the command 'borg check --repair' for you.)

    +
    + + + +
    +
    + {% endif %} + {% elseif backup_exit_code == 0 %} +

    Last {{ borg_backup_mode }} successful! (Logs)

    + {% if borg_backup_mode == 'test' %} +

    Feel free to check the integrity of the backup archive below before starting the restore process in order to make ensure that the restore will work. This can take a long time though depending on the size of the backup archive and is thus not required.

    +
    + + + +
    + {% endif %} +

    Choose the backup that you want to restore and click on the button below to restore the selected backup. This will restore the whole AIO instance. Please note that the current AIO passphrase will be kept and the previous AIO passphrase will not be restored from backup!

    +

    Important: If the backup that you want to restore contained any community container, you need to restore the same backup a second time after this attempt so that the community container data is also correctly restored.

    +
    + + +
    +
    + +
    + {% endif %} + {% elseif borg_backup_mode == 'restore' %} + {% if backup_exit_code > 0 %} +

    Last restore failed! (Logs)

    +

    The restore process has unexpectedly failed! Please adjust the path and encryption password, test it and try to restore again!

    + {% endif %} + {% endif %} + {% endif %} + + {% if not hasBackupLocation or borg_backup_mode not in ['test', 'check', ''] or backup_exit_code > 0 %} + {% if borg_remote_repo and backup_exit_code > 0 %} +

    + You may still need to authorize this pubkey on your borg remote:
    {{ borg_public_key }}
    + To try again, resubmit your location and rerun the test. +

    + {% endif %} + +

    + Please enter the location of the backup archive on your host or a + remote borg repo url + if stored remotely; and the encryption password of the backup archive below and submit all values: +

    +
    +
    +
    +
    + + + +
    + {{ include('includes/backup-dirs.twig') }} +

    ⚠️ Please note that the backup archive must be located in a subfolder of the folder that you enter here and the subfolder which contains the archive must be named 'borg', or the backup container will not be able to find the backup archive!

    + {% endif %} + {% else %} +

    Everything set! Click on the button below to test the path and encryption password:

    +
    + + + +
    + {% endif %} + {% endif %} +

    How to reset the AIO instance?

    +

    If something should be going wrong, for example during the initial installation, you can reset the instance by following this documentation.

    + {% endif %} + + {% if was_start_button_clicked == true %} + {% if current_channel starts with 'latest' or current_channel starts with 'beta' or current_channel starts with 'develop' %} +

    You are running the {{ current_channel }} channel. (Logs)

    + {% else %} +

    No channel was found. This means that AIO is not able to update itself and its component and will also not be able to report about updates. Updates need to be done externally.

    + {% endif %} + {% endif %} + + {% if is_backup_container_running == true %} +

    Backup container is currently running: {{ borg_backup_mode }} (Logs)

    +

    Reload ↻

    + {% endif %} + + {% if domain != "" %} + {% if isAnyRunning == true %} + {% if isApacheStarting != true %} + {% if hasBackupLocation %} +
    + Click here to reveal the initial Nextcloud credentials + {% endif %} +

    Initial Nextcloud username: admin

    + {% if hasBackupLocation %} + {# nextcloud_password needs to be duplicated due to a bug in Firefox. See https://github.com/nextcloud/all-in-one/issues/638. #} +

    Initial Nextcloud password: {{ nextcloud_password }}

    + {% else %} +

    Initial Nextcloud password: {{ nextcloud_password }}

    + {% endif %} +

    Open your Nextcloud ↗

    + {% if not hasBackupLocation %} +

    If your Nextcloud does not open when clicking the button above, see this documentation

    + {% endif %} + {% else %} + {% if isAnyRestarting == false %} +

    Containers are currently starting. You might inspect the container logs by clicking on Starting next to each container for further details.

    +

    Reload ↻

    + {% else %} +

    It seems at least one container was not able to start correctly and is currently restarting.

    +

    To break this endless loop, you can stop the containers below and investigate the issue in the container logs before starting the containers again.

    +
    + + + +
    + {% endif %} + {% endif %} + {% endif %} + + {% if isApacheStarting == false and is_backup_container_running == false %} + {{ include('includes/aio-config.twig') }} + {% endif %} + + {% if was_start_button_clicked == true %} +

    Containers

    +
      + {# @var containers \AIO\Container\Container[] #} + {% for container in containers %} + {% if container.GetDisplayName() != '' %} + {% include 'components/container-state.twig' with {'c': container} only %} + {% endif %} + {% endfor %} +
    + + {% if has_update_available == true %} + {% if is_mastercontainer_update_available == false %} +

    ⚠️ Container updates are available. Click on Stop containers and Start and update containers to update them. You should consider creating a backup first.

    + {% endif %} + {% else %} + {% if is_mastercontainer_update_available == false %} +

    Your containers are up-to-date.

    + {% if newMajorVersionString != '' and isAnyRunning == true and isApacheStarting != true %} +
    + Note about Nextcloud Hub {{ newMajorVersionString }} +

    If you haven't upgraded to Nextcloud Hub {{ newMajorVersionString }} yet and want to do that now, feel free to follow this documentation

    +
    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if isAnyRunning == true %} + {% if isApacheStarting != true %} + {% if is_mastercontainer_update_available == true %} +

    ⚠️ A mastercontainer update is available. Please click on the button below to stop your containers in order to update the mastercontainer.

    + {% if current_channel starts with 'latest' %} +

    You can find the changelog here

    + {% elseif current_channel starts with 'beta' %} +

    You can find the changelog here

    + {% elseif current_channel starts with 'develop' %} +

    You can find all changes here

    + {% endif %} + {% endif %} +
    + + + +
    + {% endif %} + {% else %} + {% if isBackupOrRestoreRunning == true %} +

    Restore or Backup currently running. Cannot start the containers until Restore or Backup is complete.

    + {% else %} + {% if was_start_button_clicked == false %} +

    Clicking on the button below will download all docker containers and start them. This can take a long time depending on your internet connection. Since the overall size is a few GB, this can take around 5-10 min or more. Please be patient!

    + {% endif %} + {% if is_mastercontainer_update_available == true %} +

    ⚠️ A mastercontainer update is available. Please click on the button below to update it.

    +
    + + + +
    + {% else %} + {% if was_start_button_clicked == false %} +
    + + + {% if newMajorVersionString != '' %} +
    + {% endif %} + +
    + {% elseif has_update_available == false %} +
    + + + +
    + {% else %} +
    + + + {% if bypass_container_update == true %} + + {% endif %} + +
    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if was_start_button_clicked == true %} + + {% if is_backup_section_enabled == false %} +

    Backup and restore

    +

    The backup section is disabled via environmental variable.

    + {% else %} + {% if is_backup_container_running == false and not hasBackupLocation and isApacheStarting != true %} +

    Backup and restore

    +

    Please enter the directory path below where backups will be created on the host system and submit it. It's best to choose a location on a separate drive and not on your root drive.

    +

    + To store backups remotely instead, fill in the + remote borg repo url and submit it. + You will be provided with an SSH public key for authorization at the remote afterwards. +

    +
    +
    +
    + + + +
    + {{ include('includes/backup-dirs.twig') }} + {% endif %} + {% endif %} + + {% if is_backup_section_enabled == true %} + + {% if hasBackupLocation %} + {% if is_backup_container_running == false %} +

    Backup and restore

    + {% if backup_exit_code > 0 %} +

    Last {{ borg_backup_mode }} failed! (Logs)

    + {% if borg_backup_mode == "check" %} +

    The backup check was not successful. This might indicate a corrupt archive (look at the logs). If that should be the case, you can try to fix it by following this documentation

    +
    + Reveal repair option +

    Below is the option to repair the integrity of your backup. Please note: Please only use this after you have read the documentation above! (It will run the command 'borg check --repair' for you.)

    +
    + + + +
    +
    + {% endif %} + {% if has_backup_run_once == false %} +

    The initial backup was not successful.

    + + {% if borg_remote_repo %} +

    + You may still need to authorize this pubkey on your borg remote:
    {{ borg_public_key }}
    + To try again, click Create backup. +

    + {% endif %} + +

    You may change the backup path again since the initial backup was not successful. After submitting the new value, you need to click on Create Backup to test the new value.

    +
    +
    +
    + + + +
    + {% endif %} + {% elseif backup_exit_code == 0 %} + {% if borg_backup_mode == "backup" %} +

    Last {{ borg_backup_mode }} successful on {{ last_backup_time }} UTC! (Logs)

    + {% else %} +

    Last {{ borg_backup_mode }} successful! (Logs)

    + {% endif %} + {% endif %} + {% endif %} + + {% if is_backup_container_running == false and isApacheStarting == false %} + {% if has_backup_run_once == true %} +
    + Click here to reveal all backup options (including an option for automatic updates) + {% endif %} +

    Backup information

    +

    This is your encryption password for backups: {{ borgbackup_password }}

    +

    Please save this password in a safe place. You won't be able to restore from backup if you lose this password!

    +

    All important data from your Nextcloud AIO instance such as the database, your files and the mastercontainer's configuration files, will be backed up.

    +

    The backup uses a tool called BorgBackup, a well-known server backup tool that efficiently backs up your files and encrypts them on the fly.

    +

    By using this tool, backups are incremental, differential, compressed and encrypted – so only the first backup will take a while. Further backups should be fast as only changes are taken into account.

    + {% if borg_remote_repo != '' %} +

    + Backups get created remotely at:
    + {{ borg_remote_repo }} + {% if has_backup_run_once == true %} +
    Your borg ssh public key is:
    {{ borg_public_key }} + {% endif %} +

    + {% else %} +

    Backups will be created in the following directory on the host: {{ borg_backup_host_location }}/borg

    + {% endif %} +

    Be aware that this solution does not backup files and folders that are mounted into Nextcloud using the external storage app, but you can add further Docker volumes and host paths that you want to back up after the initial backup is done.

    +

    For information about backup retention, see this.

    +

    Daily backups can be enabled after the initial backup is done. Enabling this also allows you to enable an option to update all containers, Nextcloud, and its apps automatically.

    +

    For further documentation and options on this backup solution refer to this section and below.

    + + {% if isApacheStarting != true %} +

    Backup creation

    +

    Clicking on the button below will create a backup.

    +
    + + + +
    + + {% if has_backup_run_once == false %} +

    Reset backup location

    +

    + If the configured backup host location {{ borg_backup_host_location }} + {% if borg_remote_repo %} + or the remote repo {{ borg_remote_repo }} + {% endif %} + is wrong, you can reset it by clicking on the button below. +

    +
    + + + + +
    + {% endif %} + + {% if has_backup_run_once == true %} +

    Backup Viewer

    +

    There is now a community container that allows to access your backups in a web session. See this documentation.

    + +

    Backup check

    +

    Click on the button below to perform a backup integrity check. This is an option that verifies that your backup is intact. It shouldn't be needed in most situations.

    +
    + + + +
    + +

    Backup restore

    +

    Choose the backup that you want to restore and click on the button below to restore the selected backup. This will overwrite all your files with the chosen backup so you should consider creating a backup first. You can run an integrity check before restoring your files but this shouldn't be needed in most situations. Please note that this will not restore additionally chosen backup directories! The restore process should be pretty fast as rsync, which only transfers changed files, is used to restore the chosen backup.

    +
    + + + + +
    + +

    Daily backup and automatic updates

    + {% if daily_backup_time == "" %} +

    By entering a time below and submitting it, you can enable daily backups. It will create them at the entered time in 24h format. E.g. 04:00 will create backups at 4 am UTC and 16:00 at 4 pm UTC. When creating the backup, containers will be stopped and restarted after the backup is complete.

    +
    + + + +
    +
    + +
    + {% else %} +

    Daily backups will be created at {{ daily_backup_time }} UTC. A notification about the result of the backup will be sent.

    + {% if automatic_updates == true %} + Also your containers, the mastercontainer and, on Saturdays, your Nextcloud apps will be automatically updated. + {% endif %} +

    To change your backup time first disable Daily Backups, then enter your new backup time, and then re-enable them.

    +
    + + + + +
    + {% endif %} + +

    Back up additional directories and docker volumes of your host

    +

    Below you can enter directories and docker volumes of your host that will be backed up into the same borg backup archive. Make sure to press the submit button after changing anything.

    +
    + + + + +
    +

    Each line and entry needs to start with a slash or letter/digit. Only a-z, A-Z, ., 0-9, _, -, and / are allowed. If the entry begins with a letter/digit slashes are not supported. Two valid entries are /directory/on/the/host and my_custom_docker_volume. You need to make sure that all given directories exist or the backup container will fail to start!

    +

    Be sure to individually specify all storage that you want to back up as storage will not be mounted recursively. E.g. providing / as additional backup directory will only back up files and folders that are stored on the root partition and not on the EFI partition or any other. Excluded by the backup will be caches and a few other directories. If you want to back up the root partition you should make sure to stop all services before the backup so it can run correctly. For automating this see this documentation

    +

    Please note that the chosen directories/volumes will not be restored when you restore your instance, so this would need to be done manually.

    + {% if additional_backup_directories != "" %} +

    This option is currently set. You can disable it again by clearing the field and submitting your changes.

    + {% endif %} + {% endif %} + {% endif %} + {% if has_backup_run_once == true %} +
    + {% endif %} + {% endif %} + {% endif %} + {% endif %} + + {% if is_backup_container_running == false %} + {% if isApacheStarting == false %} +

    AIO passphrase change

    +
    + Click here to change your AIO passphrase +

    You can change your AIO passphrase below:

    +
    + + + + + +
    +

    The new passphrase needs to be at least 24 characters long. Allowed characters are the latin characters a-z, A-Z, 0-9 and spaces.

    +
    + {% endif %} + {% endif %} + {% endif %} + {% if is_backup_container_running == false %} + + {{ include('includes/optional-containers.twig') }} + +

    Timezone change

    + {% if isAnyRunning == true %} + {% if timezone != "" %} +

    The timezone for Nextcloud is currently set to {{ timezone }}.

    + {% endif %} +

    Please note: You can change the timezone when your containers are stopped.

    + {% else %} + {% if timezone == "" %} +

    To get the correct time values for certain Nextcloud features, set the timezone for Nextcloud to the one that your users mainly use. Please note that this setting does not apply to the mastercontainer and any backup option.

    +

    You can configure the timezone for Nextcloud below (Do not forget to submit the value!):

    +
    + + + + +
    +

    You need to make sure that the timezone that you enter is valid. An example is Europe/Berlin. You can get valid values by looking at the 'TZ identifier' column of this list: click here. The default is Etc/UTC if nothing is entered.

    + {% else %} +

    The timezone for Nextcloud is currently set to {{ timezone }}. You can change the timezone by clicking on the button below.

    +
    + + + + +
    + {% endif %} + {% endif %} + {{ include('includes/community-containers.twig') }} + {% endif %} + {% endif %} + {% endif %} + + {% if isApacheStarting == true or is_backup_container_running == true or isWatchtowerRunning == true or is_daily_backup_running == true %} + + {% else %} + + {% endif %} + +
    +
    +{% endblock %} diff --git a/nextcloud-aio/php/templates/includes/aio-config.twig b/nextcloud-aio/php/templates/includes/aio-config.twig new file mode 100644 index 0000000..fbb7023 --- /dev/null +++ b/nextcloud-aio/php/templates/includes/aio-config.twig @@ -0,0 +1,44 @@ +
    + Click here to view the current AIO config and documentation links + {% if was_start_button_clicked == true %} +

    Nextcloud's config.php file is stored in the nextcloud_aio_nextcloud Docker volume and can be edited by following the config.php documentation.

    +

    You can run Nextcloud's usual occ commands by following the occ documentation.

    + {% endif %} + +

    + {% if nextcloud_datadir starts with '/' %} + Nextcloud's datadir is getting stored in the {{ nextcloud_datadir }} directory. + {% else %} + Nextcloud's datadir is getting stored in the {{ nextcloud_datadir }} Docker volume. + {% endif %} + See the NEXTCLOUD_DATADIR documentation on how to change this. +

    + +

    + {% if nextcloud_mount == '' %} + The Nextcloud container is confined and local external storage in Nextcloud is disabled. + {% else %} + The Nextcloud container is getting access to the {{ nextcloud_mount }} directory and local external storage in Nextcloud is enabled. + {% endif %} + See the NEXTCLOUD_MOUNT documentation on how to change this.

    + +

    Nextcloud has an upload limit of {{ nextcloud_upload_limit }} configured (for public link uploads. Bigger uploads are always possible when users are logged in). See the NEXTCLOUD_UPLOAD_LIMIT documentation on how to change this.

    + +

    For Nextcloud, a memory limit of {{ nextcloud_memory_limit }} per PHP process is configured. See the NEXTCLOUD_MEMORY_LIMIT documentation on how to change this.

    + +

    Nextcloud has a timeout of {{ nextcloud_max_time }} seconds configured (important for big file uploads). See the NEXTCLOUD_MAX_TIME documentation on how to change this.

    + +

    + {% if is_dri_device_enabled == true and is_nvidia_gpu_enabled == true %} + Hardware acceleration is enabled with the /dev/dri device and the Nvidia runtime. + {% elseif is_dri_device_enabled == true %} + Hardware acceleration is enabled with the /dev/dri device. + {% elseif is_nvidia_gpu_enabled == true %} + Hardware acceleration is enabled with the Nvidia runtime. + {% else %} + Hardware acceleration is not enabled. It's recommended to enable hardware transcoding for better performance. + {% endif %} + See the hardware acceleration documentation on how to change this.

    + +

    For further documentation on AIO, refer to this page. You can use the browser search [CTRL]+[F] to search through the documentation. Additional documentation can be found here.

    +
    diff --git a/nextcloud-aio/php/templates/includes/backup-dirs.twig b/nextcloud-aio/php/templates/includes/backup-dirs.twig new file mode 100644 index 0000000..390bf69 --- /dev/null +++ b/nextcloud-aio/php/templates/includes/backup-dirs.twig @@ -0,0 +1,6 @@ +

    The folder path that you enter must start with / and must not end with /.

    +

    An example for Linux is /mnt/backup.

    +

    On Synology it could be /volume1/docker/nextcloud/backup.

    +

    For macOS it may be /var/backup.

    +

    On Windows it might be /run/desktop/mnt/host/c/backup. (This path is equivalent to 'C:\backup' on your Windows host so you need to translate the path accordingly. Hint: the path that you enter needs to start with '/run/desktop/mnt/host/'. Append to that the exact location on your windows host, e.g. 'c/backup' which is equivalent to 'C:\backup'.) ⚠️ Please note: This does not work with external drives like USB or network drives and only with internal drives like SATA or NVME drives.

    +

    Another option is to enter a specific volume name here: nextcloud_aio_backupdir. This volume needs to be created beforehand manually by you in order to be able to use it. See this documentation for an example.

    diff --git a/nextcloud-aio/php/templates/includes/community-containers.twig b/nextcloud-aio/php/templates/includes/community-containers.twig new file mode 100644 index 0000000..f74e375 --- /dev/null +++ b/nextcloud-aio/php/templates/includes/community-containers.twig @@ -0,0 +1,42 @@ +

    Community Containers

    +

    In this section you can enable or disable optional Community Containers that are not included by default in the main installation. These containers are provided by the community and can be useful for various purposes and are automatically integrated in AIOs backup solution and update mechanisms.

    +

    ⚠️ Caution: Community Containers are maintained by the community and not officially by Nextcloud. Some containers may not be compatible with your system, may not work as expected or may discontinue. Use them at your own risk. Please read the documentation for each container first before adding any as some are also incompatible between each other! Never add all of them at the same time!

    +{% if isAnyRunning == true %} +

    Please note: You can enable or disable the options below only when your containers are stopped.

    +{% else %} +

    Please note: Make sure to save your changes by clicking Save changes below the list of Community Containers. The changes will not be auto-saved.

    +{% endif %} +
    + Show/Hide available Community Containers +
    + + + + {% for cc in community_containers %} +

    + + +

    + {% endfor %} + + +
    +
    diff --git a/nextcloud-aio/php/templates/includes/optional-containers.twig b/nextcloud-aio/php/templates/includes/optional-containers.twig new file mode 100644 index 0000000..572af5f --- /dev/null +++ b/nextcloud-aio/php/templates/includes/optional-containers.twig @@ -0,0 +1,201 @@ +

    Optional containers

    +

    In this section you can enable or disable optional containers.

    +{% if isAnyRunning == true %} +

    Please note: You can enable or disable the options below only when your containers are stopped.

    +{% else %} +

    Please note: Make sure to save your changes by clicking Save changes below the list of optional containers. The changes will not be auto-saved.

    +{% endif %} +
    + + + +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + +
    +

    Minimal system requirements: When any optional container is enabled, at least 2GB RAM, a dual-core CPU and 40GB system storage are required. When enabling ClamAV, Nextcloud Talk Recording-server or Fulltextsearch, at least 3GB RAM are required. For Talk Recording-server additional 2 vCPUs are required. When enabling everything, at least 5GB RAM and a quad-core CPU are required. Recommended are at least 1GB more RAM than the minimal requirement. For further advice and recommendations see this documentation

    +{% if isAnyRunning == true %} + + + + + + + + + +{% endif %} + +{% if is_collabora_enabled == true and isAnyRunning == false and was_start_button_clicked == true %} +

    Collabora dictionaries

    + + {% if collabora_dictionaries == "" %} +

    In order to get the correct dictionaries in Collabora, you may configure the dictionaries below:

    +
    + + + + +
    +

    You need to make sure that the dictionaries that you enter are valid. An example is de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru.

    + {% else %} +

    The dictionaries for Collabora are currently set to {{ collabora_dictionaries }}. You can reset them again by clicking on the button below.

    +
    + + + + +
    + {% endif %} + +

    Additional Collabora options

    + + {% if collabora_additional_options == "" %} +

    You can configure additional options for collabora below.

    +

    (This can be used for configuring the net.content_security_policy and more. Make sure to submit the value!)

    +
    + + + + +
    +

    You need to make sure that the options that you enter are valid. An example is --o:net.content_security_policy=frame-ancestors *.example.com:*;.

    + {% else %} +

    The additioinal options for Collabora are currently set to {{ collabora_additional_options }}. You can reset them again by clicking on the button below.

    +
    + + + + +
    + {% endif %} +{% endif %} diff --git a/nextcloud-aio/php/templates/layout.twig b/nextcloud-aio/php/templates/layout.twig new file mode 100644 index 0000000..e20ca3e --- /dev/null +++ b/nextcloud-aio/php/templates/layout.twig @@ -0,0 +1,21 @@ + + + AIO + + + + + + + +
    + {% block body %}{% endblock %} +
    +
    +
    +
    + + + diff --git a/nextcloud-aio/php/templates/login.twig b/nextcloud-aio/php/templates/login.twig new file mode 100644 index 0000000..cf5cc0c --- /dev/null +++ b/nextcloud-aio/php/templates/login.twig @@ -0,0 +1,25 @@ +{% extends "layout.twig" %} + +{% block body %} + + +{% endblock %} diff --git a/nextcloud-aio/php/templates/setup.twig b/nextcloud-aio/php/templates/setup.twig new file mode 100644 index 0000000..f1d4d1d --- /dev/null +++ b/nextcloud-aio/php/templates/setup.twig @@ -0,0 +1,16 @@ +{% extends "layout.twig" %} + +{% block body %} + +{% endblock %} diff --git a/nextcloud-aio/php/tests/.gitignore b/nextcloud-aio/php/tests/.gitignore new file mode 100644 index 0000000..58786aa --- /dev/null +++ b/nextcloud-aio/php/tests/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/nextcloud-aio/php/tests/package-lock.json b/nextcloud-aio/php/tests/package-lock.json new file mode 100644 index 0000000..ea2b429 --- /dev/null +++ b/nextcloud-aio/php/tests/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.51.1", + "@types/node": "^22.13.10" + } + }, + "node_modules/@playwright/test": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", + "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/nextcloud-aio/php/tests/package.json b/nextcloud-aio/php/tests/package.json new file mode 100644 index 0000000..ebfa99e --- /dev/null +++ b/nextcloud-aio/php/tests/package.json @@ -0,0 +1,8 @@ +{ + "name": "nextcloud-aio-mastercontainer-tests", + "version": "1.0.0", + "license": "AGPL-3.0-or-later", + "devDependencies": { + "@playwright/test": "^1.51.1" + } +} diff --git a/nextcloud-aio/php/tests/playwright.config.js b/nextcloud-aio/php/tests/playwright.config.js new file mode 100644 index 0000000..191a7f5 --- /dev/null +++ b/nextcloud-aio/php/tests/playwright.config.js @@ -0,0 +1,29 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests', + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: 0, + workers: 1, + reporter: [ + ['list'], + ['html'], + ], + use: { + baseURL: process.env.BASE_URL ?? 'http://localhost:8080', + trace: 'on', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + ignoreHTTPSErrors: true, + }, + }, + ], +}) diff --git a/nextcloud-aio/php/tests/tests/initial-setup.spec.js b/nextcloud-aio/php/tests/tests/initial-setup.spec.js new file mode 100644 index 0000000..c88cd8e --- /dev/null +++ b/nextcloud-aio/php/tests/tests/initial-setup.spec.js @@ -0,0 +1,96 @@ +import { test, expect } from '@playwright/test'; +import { writeFileSync } from 'node:fs' + +test('Initial setup', async ({ page: setupPage }) => { + test.setTimeout(10 * 60 * 1000) + + // Extract initial password + await setupPage.goto('./setup'); + const password = await setupPage.locator('#initial-password').innerText() + const containersPagePromise = setupPage.waitForEvent('popup'); + await setupPage.getByRole('link', { name: 'Open Nextcloud AIO login ↗' }).click(); + const containersPage = await containersPagePromise; + + // Log in and wait for redirect + await containersPage.locator('#master-password').click(); + await containersPage.locator('#master-password').fill(password); + await containersPage.getByRole('button', { name: 'Log in' }).click(); + await containersPage.waitForURL('./containers'); + + // Reject IP addresses + await containersPage.locator('#domain').click(); + await containersPage.locator('#domain').fill('1.1.1.1'); + await containersPage.getByRole('button', { name: 'Submit domain' }).click(); + await expect(containersPage.locator('body')).toContainText('Please enter a domain and not an IP-address!'); + + // Accept example.com (requires disabled domain validation) + await containersPage.locator('#domain').click(); + await containersPage.locator('#domain').fill('example.com'); + await containersPage.getByRole('button', { name: 'Submit domain' }).click(); + + // Disable all additional containers + await containersPage.locator('#talk').uncheck(); + await containersPage.getByRole('checkbox', { name: 'Whiteboard' }).uncheck(); + await containersPage.getByRole('checkbox', { name: 'Imaginary' }).uncheck(); + await containersPage.getByRole('checkbox', { name: 'Collabora' }).uncheck(); + await containersPage.getByRole('button', { name: 'Save changes' }).click(); + await expect(containersPage.locator('#talk')).not.toBeChecked() + await expect(containersPage.getByRole('checkbox', { name: 'Whiteboard' })).not.toBeChecked() + await expect(containersPage.getByRole('checkbox', { name: 'Imaginary' })).not.toBeChecked() + await expect(containersPage.getByRole('checkbox', { name: 'Collabora' })).not.toBeChecked() + + // Reject invalid time zones + await containersPage.locator('#timezone').click(); + await containersPage.locator('#timezone').fill('Invalid time zone'); + containersPage.once('dialog', dialog => { + console.log(`Dialog message: ${dialog.message()}`) + dialog.accept() + }); + await containersPage.getByRole('button', { name: 'Submit timezone' }).click(); + await expect(containersPage.locator('body')).toContainText('The entered timezone does not seem to be a valid timezone!') + + // Accept valid time zone + await containersPage.locator('#timezone').click(); + await containersPage.locator('#timezone').fill('Europe/Berlin'); + containersPage.once('dialog', dialog => { + console.log(`Dialog message: ${dialog.message()}`) + dialog.accept() + }); + await containersPage.getByRole('button', { name: 'Submit timezone' }).click(); + + // Start containers and wait for starting message + await containersPage.getByRole('button', { name: 'Download and start containers' }).click(); + await expect(containersPage.getByRole('main')).toContainText('Containers are currently starting.', { timeout: 5 * 60 * 1000 }); + await expect(containersPage.getByRole('link', { name: 'Open your Nextcloud ↗' })).toBeVisible({ timeout: 3 * 60 * 1000 }); + await expect(containersPage.getByRole('link', { name: 'Open your Nextcloud ↗' })).toHaveAttribute('href', 'https://example.com'); + + // Extract initial nextcloud password + await expect(containersPage.getByRole('main')).toContainText('Initial Nextcloud password:') + const initialNextcloudPassword = await containersPage.locator('#initial-nextcloud-password').innerText(); + + // Set backup location and create backup + const borgBackupLocation = `/mnt/test/aio-${Math.floor(Math.random() * 2147483647)}` + await containersPage.locator('#borg_backup_host_location').click(); + await containersPage.locator('#borg_backup_host_location').fill(borgBackupLocation); + await containersPage.getByRole('button', { name: 'Submit backup location' }).click(); + containersPage.once('dialog', dialog => { + console.log(`Dialog message: ${dialog.message()}`) + dialog.accept() + }); + await containersPage.getByRole('button', { name: 'Create backup' }).click(); + await expect(containersPage.getByRole('main')).toContainText('Backup container is currently running:', { timeout: 3 * 60 * 1000 }); + await expect(containersPage.getByRole('main')).toContainText('Last backup successful on', { timeout: 3 * 60 * 1000 }); + await containersPage.getByText('Click here to reveal all backup options').click(); + await expect(containersPage.locator('#borg-backup-password')).toBeVisible(); + const borgBackupPassword = await containersPage.locator('#borg-backup-password').innerText(); + + // Assert that all containers are stopped + await expect(containersPage.getByRole('button', { name: 'Start containers' })).toBeVisible(); + + // Save passwords for restore backup test + writeFileSync('test_data.json', JSON.stringify({ + initialNextcloudPassword, + borgBackupLocation, + borgBackupPassword, + })) +}); diff --git a/nextcloud-aio/php/tests/tests/restore-instance.spec.js b/nextcloud-aio/php/tests/tests/restore-instance.spec.js new file mode 100644 index 0000000..e93cf34 --- /dev/null +++ b/nextcloud-aio/php/tests/tests/restore-instance.spec.js @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'node:fs'; + +test('Restore instance', async ({ page: setupPage }) => { + test.setTimeout(10 * 60 * 1000) + + // Load passwords from previous test + const { + initialNextcloudPassword, + borgBackupLocation, + borgBackupPassword, + } = JSON.parse(readFileSync('test_data.json')) + + // Extract initial password + await setupPage.goto('./setup'); + const password = await setupPage.locator('#initial-password').innerText() + const containersPagePromise = setupPage.waitForEvent('popup'); + await setupPage.getByRole('link', { name: 'Open Nextcloud AIO login ↗' }).click(); + const containersPage = await containersPagePromise; + + // Log in and wait for redirect + await containersPage.locator('#master-password').click(); + await containersPage.locator('#master-password').fill(password); + await containersPage.getByRole('button', { name: 'Log in' }).click(); + await containersPage.waitForURL('./containers'); + + // Reject example.com (requires enabled domain validation) + await containersPage.locator('#domain').click(); + await containersPage.locator('#domain').fill('example.com'); + await containersPage.getByRole('button', { name: 'Submit domain' }).click(); + await expect(containersPage.locator('body')).toContainText('Domain does not point to this server or the reverse proxy is not configured correctly.', { timeout: 15 * 1000 }); + + // Reject invalid backup location + await containersPage.locator('#borg_restore_host_location').click(); + await containersPage.locator('#borg_restore_host_location').fill('/mnt/test/aio-incorrect-path'); + await containersPage.locator('#borg_restore_password').click(); + await containersPage.locator('#borg_restore_password').fill(borgBackupPassword); + await containersPage.getByRole('button', { name: 'Submit location and encryption password' }).click() + await containersPage.getByRole('button', { name: 'Test path and encryption' }).click(); + await expect(containersPage.getByRole('main')).toContainText('Last test failed!', { timeout: 60 * 1000 }); + + // Reject invalid backup password + await containersPage.locator('#borg_restore_host_location').click(); + await containersPage.locator('#borg_restore_host_location').fill(borgBackupLocation); + await containersPage.locator('#borg_restore_password').click(); + await containersPage.locator('#borg_restore_password').fill('foobar'); + await containersPage.getByRole('button', { name: 'Submit location and encryption password' }).click() + await containersPage.getByRole('button', { name: 'Test path and encryption' }).click(); + await expect(containersPage.getByRole('main')).toContainText('Last test failed!', { timeout: 60 * 1000 }); + + // Accept correct backup location and password + await containersPage.locator('#borg_restore_host_location').click(); + await containersPage.locator('#borg_restore_host_location').fill(borgBackupLocation); + await containersPage.locator('#borg_restore_password').click(); + await containersPage.locator('#borg_restore_password').fill(borgBackupPassword); + await containersPage.getByRole('button', { name: 'Submit location and encryption password' }).click() + await containersPage.getByRole('button', { name: 'Test path and encryption' }).click(); + + // Check integrity and restore backup + await containersPage.getByRole('button', { name: 'Check backup integrity' }).click(); + await expect(containersPage.getByRole('main')).toContainText('Last check successful!', { timeout: 5 * 60 * 1000 }); + containersPage.once('dialog', dialog => { + console.log(`Dialog message: ${dialog.message()}`) + dialog.accept() + }); + await containersPage.getByRole('button', { name: 'Restore selected backup' }).click(); + await expect(containersPage.getByRole('main')).toContainText('Backup container is currently running:', { timeout: 1 * 60 * 1000 }); + + // Verify a successful backup restore + await expect(containersPage.getByRole('main')).toContainText('Last restore successful!', { timeout: 3 * 60 * 1000 }); + await expect(containersPage.getByRole('main')).toContainText('⚠️ Container updates are available. Click on Stop containers and Start and update containers to update them. You should consider creating a backup first.'); + containersPage.once('dialog', dialog => { + console.log(`Dialog message: ${dialog.message()}`) + dialog.accept() + }); + await containersPage.getByRole('button', { name: 'Start and update containers' }).click(); + await expect(containersPage.getByRole('link', { name: 'Open your Nextcloud ↗' })).toBeVisible({ timeout: 5 * 60 * 1000 }); + await expect(containersPage.getByRole('main')).toContainText(initialNextcloudPassword); + + // Verify that containers are all stopped + await containersPage.getByRole('button', { name: 'Stop containers' }).click(); + await expect(containersPage.getByRole('button', { name: 'Start containers' })).toBeVisible({ timeout: 60 * 1000 }); +}); \ No newline at end of file diff --git a/nextcloud-aio/readme.md b/nextcloud-aio/readme.md new file mode 100644 index 0000000..3d82e63 --- /dev/null +++ b/nextcloud-aio/readme.md @@ -0,0 +1,1111 @@ +# Nextcloud All-in-One + +> [!NOTE] +> Nextcloud AIO is actively looking for contributors. See [the forum post](https://help.nextcloud.com/t/nextcloud-aio-is-looking-for-contributors/205234). + +The official Nextcloud installation method. Nextcloud AIO provides easy deployment and maintenance with most features included in this one Nextcloud instance. + +Included are: +- Nextcloud +- High performance backend for Nextcloud Files +- Nextcloud Office (optional) +- High performance backend for Nextcloud Talk and TURN-server (optional) +- Nextcloud Talk Recording-server (optional) +- Backup solution (optional, based on [BorgBackup](https://github.com/borgbackup/borg#what-is-borgbackup)) +- Imaginary (optional, for previews of heic, heif, illustrator, pdf, svg, tiff and webp) +- ClamAV (optional, Antivirus backend for Nextcloud) +- Fulltextsearch (optional) +- Whiteboard (optional) +- Docker Socket Proxy (optional, needed for [Nextcloud App API](https://github.com/cloud-py-api/app_api#nextcloud-appapi)) +- [Community containers](https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers) +
    And much more: + +- Simple web interface included that enables easy installation and maintenance +- [Easy updates included](https://github.com/nextcloud/all-in-one#how-to-update-the-containers) +- Update and backup notifications included +- Daily backups can be enabled from the AIO interface which also allows updating all containers, Nextcloud and its apps afterwards automatically +- Instance restore from backup archive via the AIO interface included (you only need the archive and the password in order to restore the whole instance on a new AIO instance) +- APCu as local cache +- Redis as distributed cache and for file locking +- Postgresql as database +- PHP-FPM with performance-optimized config (e.g. Opcache and JIT enabled by default) +- A+ security in Nextcloud security scan +- Ready to be used behind existing [Reverse proxies](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) +- Can be used behind [Cloudflare Tunnel](https://github.com/nextcloud/all-in-one#how-to-run-nextcloud-behind-a-cloudflare-tunnel) +- Can be used via [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) +- Ready for big file uploads up to 10 GB on public links, [adjustable](https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud) (logged in users can upload much bigger files using the webinterface or the mobile/desktop clients since chunking is used in that case) +- PHP and web server timeouts set to 3600s, [adjustable](https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud) (important for big file uploads) +- Defaults to a max of 512 MB RAM per PHP process, [adjustable](https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud) +- Automatic TLS included (by using Let's Encrypt) +- Brotli compression enabled by default for javascript, css and svg files which reduces Nextcloud load times +- HTTP/2 and HTTP/3 enabled +- "Pretty URLs" for Nextcloud are enabled by default (removes the index.php from all links) +- Video previews work out of the box and when Imaginary is enabled, many recent image formats as well! +- Only one domain and not multiple domains are required for everything to work (usually you would need to have one domain for each service which is much more complex) +- [Adjustable location](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) of Nextcloud's datadir (e.g. good for easy file-sharing with host system on Windows and MacOS) +- By default confined (good for security) but can [allow access to additional storages](https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host) in order to enable the usage of the local external storage feature +- Possibility included to [adjust default installed Nextcloud apps](https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup) +- Nextcloud installation is not read only - that means you can apply patches if you should need them (instead of having to wait for the next release for them getting applied) +- `ffmpeg`, `smbclient`, `libreoffice` and `nodejs` are included by default +- Possibility included to [permanently add additional OS packages into the Nextcloud container](https://github.com/nextcloud/all-in-one#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup) without having to build your own Docker image +- Possibility included to [permanently add additional PHP extensions into the Nextcloud container](https://github.com/nextcloud/all-in-one#how-to-add-php-extensions-permanently-to-the-nextcloud-container) without having to build your own Docker image +- Possibility included to [pass the needed device for hardware transcoding](https://github.com/nextcloud/all-in-one#how-to-enable-hardware-acceleration-for-nextcloud) to the Nextcloud container +- Possibility included to [store all docker related files on a separate drive](https://github.com/nextcloud/all-in-one#how-to-store-the-filesinstallation-on-a-separate-drive) +- [LDAP can be used as user backend for Nextcloud](https://github.com/nextcloud/all-in-one/tree/main#ldap) +- Migration from any former Nextcloud installation to AIO is possible. See [this documentation](https://github.com/nextcloud/all-in-one/blob/main/migration.md). +- Migration in the other direction (e.g. from AIO to a VM-based installation) is also possible. +- [Fail2Ban can be added](https://github.com/nextcloud/all-in-one#fail2ban) +- [phpMyAdmin, Adminer or pgAdmin can be added](https://github.com/nextcloud/all-in-one#phpmyadmin-adminer-or-pgadmin) +- [Mail server can be added](https://github.com/nextcloud/all-in-one#mail-server) +- Nextcloud can be [accessed locally via the domain](https://github.com/nextcloud/all-in-one#how-can-i-access-nextcloud-locally) +- Can [be installed locally](https://github.com/nextcloud/all-in-one/blob/main/local-instance.md) (if you don't want or cannot make the instance publicly reachable) +- [IPv6-ready](https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md) +- Can be used with [Docker rootless](https://github.com/nextcloud/all-in-one/blob/main/docker-rootless.md) (good for additional security) +- Runs on all platforms Docker supports (e.g. also on Windows and Macos) +- Included containers easy to debug by having the possibility to check their logs directly from the AIO interface +- [Docker-compose ready](./compose.yaml) +- Can be installed [without a container having access to the docker socket](https://github.com/nextcloud/all-in-one/tree/main/manual-install) +- Can be installed with [Docker Swarm](https://github.com/nextcloud/all-in-one#can-i-run-this-with-docker-swarm) +- Can be installed with [Kubernetes](https://github.com/nextcloud/all-in-one/tree/main/nextcloud-aio-helm-chart) +- Almost all included containers Alpine Linux based (good for security and size) +- Many of the included containers run as non-root user (good for security) +- Many of the included containers have a read-only root-FS (good for security) +- Included containers run in its own docker network (good for security) and only really necessary ports are exposed on the host +- [Multiple instances on one server](https://github.com/nextcloud/all-in-one/blob/main/multiple-instances.md) are doable without having to deal with VMs +- Adjustable backup path or remote borg repository from the AIO interface (good to put the backups e.g. on a different drive if using a local backup path) +- Possibility included to also back up external Docker Volumes or Host paths (can be used for host backups) +- Borg backup can be completely managed from the AIO interface, including backup creation, backup restore, backup integrity check and integrity-repair +- Other forms of [remote backup](https://github.com/nextcloud/all-in-one#are-remote-borg-backups-supported) are indirectly possible +- Updates and backups can be [run from an external script](https://github.com/nextcloud/all-in-one#how-to-stopstartupdate-containers-or-trigger-the-daily-backup-from-a-script-externally). See [this documentation](https://github.com/nextcloud/all-in-one#how-to-enable-automatic-updates-without-creating-a-backup-beforehand) for a complete example. + +
    + +## Screenshots +| First setup | After installation | +|---|---| +| ![image](https://github.com/user-attachments/assets/6ef5d7b5-86f2-402c-bc6c-b633af2ca7dd) | ![image](https://github.com/user-attachments/assets/939d0fdf-436f-433d-82d3-27548263a040) | + +## How to use this? +>[!WARNING] +> You should first make sure that you are not using docker installed via snap. You can check this by running `sudo docker info | grep "Docker Root Dir" | grep "/var/snap/docker/"`. If the output should contain the mentioned string `/var/snap/docker/`, you should first uninstall docker snap via `sudo snap remove docker` and then follow the instructions below. ⚠️ Attention: only run the command if this is a clean new docker installation and you are not running any service already using this. + +> [!NOTE] +> The following instructions are meant for installations without a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) already being in place. If you want to run AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else), see the [reverse proxy documentation](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md). Also, the instructions below are especially meant for Linux. For macOS see [this](#how-to-run-aio-on-macos), for Windows see [this](#how-to-run-aio-on-windows) and for Synology see [this](#how-to-run-aio-on-synology-dsm). + +1. Install Docker on your Linux installation by following the official documentation: https://docs.docker.com/engine/install/#supported-platforms. +>[!WARNING] +> You could use the convenience script below to install docker. However we recommend to not blindly download and execute scripts as sudo. But if you feel like it, you can of course use it. See below: + +
    + Using the convenience script + +```sh +curl -fsSL https://get.docker.com | sudo sh +``` + +
    + +2. If you need ipv6 support, you should enable it by following https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md. +3. Run the command below in order to start the container on Linux and without a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) already in place: + ``` + # For Linux and without a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else) already in place: + sudo docker run \ + --init \ + --sig-proxy=false \ + --name nextcloud-aio-mastercontainer \ + --restart always \ + --publish 80:80 \ + --publish 8080:8080 \ + --publish 8443:8443 \ + --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ + --volume /var/run/docker.sock:/var/run/docker.sock:ro \ + ghcr.io/nextcloud-releases/all-in-one:latest + ``` +
    + Explanation of the command + + - `sudo docker run` This command spins up a new docker container. Docker commands can optionally be used without `sudo` if the user is added to the docker group (this is not the same as docker rootless, see FAQ below). + - `--init` This option makes sure that no zombie-processes are created, ever. See [the Docker documentation](https://docs.docker.com/reference/cli/docker/container/run/#init). + - `--sig-proxy=false` This option allows to exit the container shell that gets attached automatically when using `docker run` by using `[CTRL] + [C]` without shutting down the container. + - `--name nextcloud-aio-mastercontainer` This is the name of the container. This line is not allowed to be changed, since mastercontainer updates would fail. + - `--restart always` This is the "restart policy". `always` means that the container should always get started with the Docker daemon. See the Docker documentation for further detail about restart policies: https://docs.docker.com/config/containers/start-containers-automatically/ + - `--publish 80:80` This means that port 80 of the container should get published on the host using port 80. It is used for getting valid certificates for the AIO interface if you want to use port 8443. It is not needed if you run AIO behind a web server or reverse proxy and can get removed in that case as you can simply use port 8080 for the AIO interface then. + - `--publish 8080:8080` This means that port 8080 of the container should get published on the host using port 8080. This port is used for the AIO interface and uses a self-signed certificate by default. You can also use a different host port if port 8080 is already used on your host, for example `--publish 8081:8080` (only the first port can be changed for the host, the second port is for the container and must remain at 8080). + - `--publish 8443:8443` This means that port 8443 of the container should get published on the host using port 8443. If you publish port 80 and 8443 to the public internet, you can access the AIO interface via this port with a valid certificate. It is not needed if you run AIO behind a web server or reverse proxy and can get removed in that case as you can simply use port 8080 for the AIO interface then. + - `--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config` This means that the files that are created by the mastercontainer will be stored in a docker volume that is called `nextcloud_aio_mastercontainer`. This line is not allowed to be changed, since built-in backups would fail later on. + - `--volume /var/run/docker.sock:/var/run/docker.sock:ro` The docker socket is mounted into the container which is used for spinning up all the other containers and for further features. It needs to be adjusted on Windows/macOS and on docker rootless. See the applicable documentation on this. If adjusting, don't forget to also set `WATCHTOWER_DOCKER_SOCKET_PATH`! If you dislike this, see https://github.com/nextcloud/all-in-one/tree/main/manual-install. + - `ghcr.io/nextcloud-releases/all-in-one:latest` This is the docker container image that is used. + - Further options can be set using environment variables, for example `--env NEXTCLOUD_DATADIR="/mnt/ncdata"` (This is an example for Linux. See [this](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) for other OS' and for an explanation of what this value does. This specific one needs to be specified upon the first startup if you want to change it to a specific path instead of the default Docker volume). To see explanations and examples for further variables (like changing the location of Nextcloud's datadir or mounting some locations as external storage into the Nextcloud container), read through this readme and look at the docker-compose file: https://github.com/nextcloud/all-in-one/blob/main/compose.yaml +
    + + Note: You may be interested in adjusting Nextcloud’s datadir to store the files in a different location than the default docker volume. See [this documentation](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) on how to do it. + +4. After the initial startup, you should be able to open the Nextcloud AIO Interface now on port 8080 of this server.
    +E.g. `https://ip.address.of.this.server:8080`
    +⚠️ **Important:** do always use an ip-address if you access this port and not a domain as HSTS might block access to it later! (It is also expected that this port uses a self-signed certificate due to security concerns which you need to accept in your browser)

    +If your firewall/router has port 80 and 8443 open/forwarded and you point a domain to your server, you can get a valid certificate automatically by opening the Nextcloud AIO Interface via:
    +`https://your-domain-that-points-to-this-server.tld:8443` +5. Please do not forget to open port `3478/TCP` and `3478/UDP` in your firewall/router for the Talk container! + +# FAQ +- [TOC](#faq) + - [Where can I find additional documentation?](#where-can-i-find-additional-documentation) + - [How does it work?](#how-does-it-work) + - [How to contribute?](#how-to-contribute) + - [How many users are possible?](#how-many-users-are-possible) +- [Network](#network) + - [Are reverse proxies supported?](#are-reverse-proxies-supported) + - [Which ports are mandatory to be open in your firewall/router?](#which-ports-are-mandatory-to-be-open-in-your-firewallrouter) + - [Explanation of used ports](#explanation-of-used-ports) + - [Notes on Cloudflare (proxy/tunnel)](#notes-on-cloudflare-proxytunnel) + - [How to run Nextcloud behind a Cloudflare Tunnel?](#how-to-run-nextcloud-behind-a-cloudflare-tunnel) + - [How to run Nextcloud via Tailscale?](#how-to-run-nextcloud-via-tailscale) + - [How to get Nextcloud running using the ACME DNS-challenge?](#how-to-get-nextcloud-running-using-the-acme-dns-challenge) + - [How to run Nextcloud locally? No domain wanted, or wanting intranet access within your LAN.](#how-to-run-nextcloud-locally-no-domain-wanted-or-wanting-intranet-access-within-your-lan) + - [Can I use an ip-address for Nextcloud instead of a domain?](#can-i-use-an-ip-address-for-nextcloud-instead-of-a-domain) + - [Can I run AIO offline or in an airgapped system?](#can-i-run-aio-offline-or-in-an-airgapped-system) + - [Are self-signed certificates supported for Nextcloud?](#are-self-signed-certificates-supported-for-nextcloud) + - [Can I use AIO with multiple domains?](#can-i-use-aio-with-multiple-domains) + - [Are other ports than the default 443 for Nextcloud supported?](#are-other-ports-than-the-default-443-for-nextcloud-supported) + - [Can I run Nextcloud in a subdirectory on my domain?](#can-i-run-nextcloud-in-a-subdirectory-on-my-domain) + - [How can I access Nextcloud locally?](#how-can-i-access-nextcloud-locally) + - [How to skip the domain validation?](#how-to-skip-the-domain-validation) + - [How to resolve firewall problems with Fedora Linux, RHEL OS, CentOS, SUSE Linux and others?](#how-to-resolve-firewall-problems-with-fedora-linux-rhel-os-centos-suse-linux-and-others) + - [What can I do to fix the internal or reserved ip-address error?](#what-can-i-do-to-fix-the-internal-or-reserved-ip-address-error) + - [How to adjust the MTU size of the docker network](#how-to-adjust-the-mtu-size-of-the-docker-network) +- [Infrastructure](#infrastructure) + - [Which CPU architectures are supported?](#which-cpu-architectures-are-supported) + - [Disrecommended VPS providers](#disrecommended-vps-providers) + - [Recommended VPS](#recommended-vps) + - [Note on storage options](#note-on-storage-options) + - [Are there known problems when SELinux is enabled?](#are-there-known-problems-when-selinux-is-enabled) +- [Customization](#customization) + - [How to change the default location of Nextcloud's Datadir?](#how-to-change-the-default-location-of-nextclouds-datadir) + - [How to store the files/installation on a separate drive?](#how-to-store-the-filesinstallation-on-a-separate-drive) + - [How to allow the Nextcloud container to access directories on the host?](#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host) + - [How to adjust the Talk port?](#how-to-adjust-the-talk-port) + - [How to adjust the upload limit for Nextcloud?](#how-to-adjust-the-upload-limit-for-nextcloud) + - [How to adjust the max execution time for Nextcloud?](#how-to-adjust-the-max-execution-time-for-nextcloud) + - [How to adjust the PHP memory limit for Nextcloud?](#how-to-adjust-the-php-memory-limit-for-nextcloud) + - [How to change the Nextcloud apps that are installed on the first startup?](#how-to-change-the-nextcloud-apps-that-are-installed-on-the-first-startup) + - [How to add OS packages permanently to the Nextcloud container?](#how-to-add-os-packages-permanently-to-the-nextcloud-container) + - [How to add PHP extensions permanently to the Nextcloud container?](#how-to-add-php-extensions-permanently-to-the-nextcloud-container) + - [What about the pdlib PHP extension for the facerecognition app?](#what-about-the-pdlib-php-extension-for-the-facerecognition-app) + - [How to enable hardware acceleration for Nextcloud?](#how-to-enable-hardware-acceleration-for-nextcloud) + - [With open source drivers MESA for AMD, Intel and **new** drivers `Nouveau` for Nvidia](#with-open-source-drivers-mesa-for-amd-intel-and-new-drivers-nouveau-for-nvidia) + - [With proprietary drivers for Nvidia :warning: BETA](#with-proprietary-drivers-for-nvidia-warning-beta) + - [How to keep disabled apps?](#how-to-keep-disabled-apps) + - [How to trust user-defined Certification Authorities (CA)?](#how-to-trust-user-defined-certification-authorities-ca) + - [How to disable Collabora's Seccomp feature?](#how-to-disable-collaboras-seccomp-feature) + - [How to adjust the Fulltextsearch Java options?](#how-to-adjust-the-fulltextsearch-java-options) +- [Guides](#guides) + - [How to run AIO on macOS?](#how-to-run-aio-on-macos) + - [How to run AIO on Windows?](#how-to-run-aio-on-windows) + - [How to run AIO on Synology DSM](#how-to-run-aio-on-synology-dsm) + - [How to run AIO with Portainer?](#how-to-run-aio-with-portainer) + - [Can I run AIO on TrueNAS SCALE?](#can-i-run-aio-on-truenas-scale) + - [How to run `occ` commands?](#how-to-run-occ-commands) + - [How to resolve `Security & setup warnings displays the "missing default phone region" after initial install`?](#how-to-resolve-security--setup-warnings-displays-the-missing-default-phone-region-after-initial-install) + - [How to run multiple AIO instances on one server?](#how-to-run-multiple-aio-instances-on-one-server) + - [Bruteforce protection FAQ](#bruteforce-protection-faq) + - [How to switch the channel?](#how-to-switch-the-channel) + - [How to update the containers?](#how-to-update-the-containers) + - [How to easily log in to the AIO interface?](#how-to-easily-log-in-to-the-aio-interface) + - [How to change the domain?](#how-to-change-the-domain) + - [How to properly reset the instance?](#how-to-properly-reset-the-instance) + - [Can I use a CIFS/SMB share as Nextcloud's datadir?](#can-i-use-a-cifssmb-share-as-nextclouds-datadir) + - [Can I run this with Docker swarm?](#can-i-run-this-with-docker-swarm) + - [Can I run this with Kubernetes?](#can-i-run-this-with-kubernetes) + - [How to run this with Docker rootless?](#can-i-run-this-with-podman-instead-of-docker) + - [Can I run this with Podman instead of Docker?](#can-i-run-this-with-podman-instead-of-docker) + - [Access/Edit Nextcloud files/folders manually](#accessedit-nextcloud-filesfolders-manually) + - [How to edit Nextclouds config.php file with a texteditor?](#how-to-edit-nextclouds-configphp-file-with-a-texteditor) + - [How to change default files by creating a custom skeleton directory?](#how-to-change-default-files-by-creating-a-custom-skeleton-directory) + - [How to adjust the version retention policy and trashbin retention policy?](#how-to-adjust-the-version-retention-policy-and-trashbin-retention-policy) + - [How to enable automatic updates without creating a backup beforehand?](#how-to-enable-automatic-updates-without-creating-a-backup-beforehand) + - [Securing the AIO interface from unauthorized ACME challenges](#securing-the-aio-interface-from-unauthorized-acme-challenges) + - [How to migrate from an already existing Nextcloud installation to Nextcloud AIO?](#how-to-migrate-from-an-already-existing-nextcloud-installation-to-nextcloud-aio) +- [Backup](#backup) + - [What is getting backed up by AIO's backup solution?](#what-is-getting-backed-up-by-aios-backup-solution) + - [How to adjust borgs retention policy?](#how-to-adjust-borgs-retention-policy) + - [How to migrate from AIO to AIO?](#how-to-migrate-from-aio-to-aio) + - [Are remote borg backups supported?](#are-remote-borg-backups-supported) + - [Failure of the backup container in LXC containers](#failure-of-the-backup-container-in-lxc-containers) + - [How to create the backup volume on Windows?](#how-to-create-the-backup-volume-on-windows) + - [Pro-tip: Backup archives access](#pro-tip-backup-archives-access) + - [Delete backup archives manually](#delete-backup-archives-manually) + - [Sync local backups regularly to another drive](#sync-local-backups-regularly-to-another-drive) + - [How to exclude Nextcloud's data directory or the preview folder from backup?](#how-to-exclude-nextclouds-data-directory-or-the-preview-folder-from-backup) + - [How to stop/start/update containers or trigger the daily backup from a script externally?](#how-to-stopstartupdate-containers-or-trigger-the-daily-backup-from-a-script-externally) + - [How to disable the backup section?](#how-to-disable-the-backup-section) +- [Addons](#addons) + - [Fail2ban](#fail2ban) + - [LDAP](#ldap) + - [Netdata](#netdata) + - [USER_SQL](#user_sql) + - [phpMyAdmin, Adminer or pgAdmin](#phpmyadmin-adminer-or-pgadmin) + - [Mail server](#mail-server) +- [Miscellaneous](#miscellaneous) + - [Requirements for integrating new containers](#requirements-for-integrating-new-containers) + - [Update policy](#update-policy) + - [How often are update notifications sent?](#how-often-are-update-notifications-sent) + - [Huge docker logs](#huge-docker-logs) + +### Where can I find additional documentation? +Some of the documentation is available on [GitHub Discussions](https://github.com/nextcloud/all-in-one/discussions/categories/wiki). + +### How does it work? +Nextcloud AIO is inspired by projects like Portainer that manage the docker daemon by talking to it through the docker socket directly. This concept allows a user to install only one container with a single command that does the heavy lifting of creating and managing all containers that are needed in order to provide a Nextcloud installation with most features included. It also makes updating a breeze and is not bound to the host system (and its slow updates) anymore as everything is in containers. Additionally, it is very easy to handle from a user perspective because a simple interface for managing your Nextcloud AIO installation is provided. + +### How to contribute? +See [this issue](https://github.com/nextcloud/all-in-one/issues/5251) for a list of feature requests that need help by contributors. + +### How many users are possible? +Up to 100 users are free, more are possible with [Nextcloud Enterprise](https://nextcloud.com/all-in-one/) + +## Network + +### Are reverse proxies supported? +Yes. Please refer to the following documentation on this: [reverse-proxy.md](https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md) + +### Which ports are mandatory to be open in your firewall/router? +Only those (if you access the Mastercontainer Interface internally via port 8080): +- `443/TCP` for the Apache container +- `443/UDP` if you want to enable http3 for the Apache container +- `3478/TCP` and `3478/UDP` for the Talk container + +### Explanation of used ports +- `8080/TCP`: Mastercontainer Interface with self-signed certificate (works always, also if only access via IP-address is possible, e.g. `https://ip.address.of.this.server:8080/`) ⚠️ **Important:** do always use an ip-address if you access this port and not a domain as HSTS might block access to it later! (It is also expected that this port uses a self-signed certificate due to security concerns which you need to accept in your browser) +- `80/TCP`: redirects to Nextcloud (is used for getting the certificate via ACME http-challenge for the Mastercontainer) +- `8443/TCP`: Mastercontainer Interface with valid certificate (only works if port 80 and 8443 are open/forwarded in your firewall/router and you point a domain to your server. It generates a valid certificate then automatically and access via e.g. `https://public.domain.com:8443/` is possible.) +- `443/TCP`: will be used by the Apache container later on and needs to be open/forwarded in your firewall/router +- `443/UDP`: will be used by the Apache container later on and needs to be open/forwarded in your firewall/router if you want to enable http3 +- `3478/TCP` and `3478/UDP`: will be used by the Turnserver inside the Talk container and needs to be open/forwarded in your firewall/router + +### Notes on Cloudflare (proxy/tunnel) +Since Cloudflare Proxy/Tunnel comes with a lot of limitations which are listed below, it is rather recommended to switch to [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if possible. +- Cloudflare Proxy and Cloudflare Tunnel both require Cloudflare to perform TLS termination on their side and thus decrypt all the traffic on their infrastructure. This is a privacy concern and you will need to look for other solutions if it's unacceptable for you. +- Using Cloudflare Tunnel might potentially slow down Nextcloud since local access via the configured domain is not possible because TLS termination is in that case offloaded to Cloudflare's infrastructure. There is no way to disable this behavior in Cloudflare Tunnel. +- It is known that the domain validation may not work correctly behind Cloudflare since Cloudflare might block the validation attempt. You can simply skip it in that case by following: https://github.com/nextcloud/all-in-one#how-to-skip-the-domain-validation +- Make sure to [disable Cloudflares Rocket Loader feature](https://help.nextcloud.com/t/login-page-not-working-solved/149417/8) as otherwise Nextcloud's login prompt will not be shown. +- Cloudflare only supports uploading files up to 100 MB in the free plan, if you try to upload bigger files you will get an error (413 - Payload Too Large) if no chunking is used (e.g. for public uploads in the web, or if chunks are configured to be bigger than 100 MB in the clients or the web). If you need to upload bigger files, you need to disable the proxy option in your DNS settings. Note that this will both disable Cloudflare DDoS protection and Cloudflare Tunnel as these services require the proxy option to be enabled. +- If using Cloudflare Tunnel and the Nextcloud Desktop Client [Set Chunking on Nextcloud Desktop Client](https://github.com/nextcloud/desktop/issues/4271#issuecomment-1159578065) +- Cloudflare only allows a max timeout of 100s for requests which is not configurable. This means that any server-side processing e.g. for assembling chunks for big files during upload that take longer than 100s will simply not work. See https://github.com/nextcloud/server/issues/19223. If you need to upload big files reliably, you need to disable the proxy option in your DNS settings. Note that this will both disable Cloudflare DDoS protection and Cloudflare Tunnel as these services require the proxy option to be enabled. +- It is known that the in AIO included collabora (Nextcloud Office) does not work out of the box behind Cloudflare. To make it work, you need to add all [Cloudflare IP-ranges](https://www.cloudflare.com/ips/) to the wopi-allowlist in `https://yourdomain.com/settings/admin/richdocuments` +- Cloudflare Proxy might block the Turnserver for Nextcloud Talk from working correctly. You might want to disable Cloudflare Proxy thus. See https://github.com/nextcloud/all-in-one/discussions/2463#discussioncomment-5779981 +- The built-in turn-server for Nextcloud Talk will not work behind Cloudflare Tunnel since it needs a separate port (by default 3478 or as chosen) available on the same domain. If you still want to use the feature, you will need to install your own turnserver or use a publicly available one and adjust and test your stun and turn settings in `https://yourdomain.com/settings/admin/talk`. +- If you get an error in Nextcloud's admin overview that the HSTS header is not set correctly, you might need to enable it in Cloudflare manually. +- If you are using AIO's built-in Reverse Proxy and don't use your own, then the certificate issuing may possibly not work out-of-the-box because Cloudflare might block the attempt. In that case you need to disable the Proxy feature at least temporarily in order to make it work. Note that this isn't an option if you need Cloudflare Tunnel as disabling the proxy would also disable Cloudflare Tunnel which would in turn make your server unreachable for the verification. See https://github.com/nextcloud/all-in-one/discussions/1101. + +### How to run Nextcloud behind a Cloudflare Tunnel? +Although it does not seems like it is the case but from AIO perspective a Cloudflare Tunnel works like a reverse proxy. So please follow the [reverse proxy documentation](./reverse-proxy.md) where is documented how to make it run behind a Cloudflare Tunnel. However please see the [caveats](https://github.com/nextcloud/all-in-one#notes-on-cloudflare-proxytunnel) before proceeding. + +### How to run Nextcloud via Tailscale? +For a reverse proxy example guide for Tailscale, see this guide by [@Perseus333](https://github.com/Perseus333): https://github.com/nextcloud/all-in-one/discussions/6817 + +### How to get Nextcloud running using the ACME DNS-challenge? +You can install AIO in reverse proxy mode where is also documented how to get it running using the ACME DNS-challenge for getting a valid certificate for AIO. See the [reverse proxy documentation](./reverse-proxy.md). (Meant is the `Caddy with ACME DNS-challenge` section). Also see https://github.com/dani-garcia/vaultwarden/wiki/Running-a-private-vaultwarden-instance-with-Let%27s-Encrypt-certs#getting-a-custom-caddy-build for additional docs on this topic. + +### How to run Nextcloud locally? No domain wanted, or wanting intranet access within your LAN. +If you do not want to open Nextcloud to the public internet, you may have a look at the following documentation on how to set it up locally: [local-instance.md](./local-instance.md), but keep in mind you're still required to have https working properly. + +### Can I use an ip-address for Nextcloud instead of a domain? +No and it will not be added. If you only want to run it locally, you may have a look at the following documentation: [local-instance.md](./local-instance.md). Recommended is to use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817). + +### Can I run AIO offline or in an airgapped system? +No. This is not possible and will not be added due to multiple reasons: update checks, app installs via app-store, downloading additional docker images on demand and more. + +### Are self-signed certificates supported for Nextcloud? +No and they will not be. If you want to run it locally, without opening Nextcloud to the public internet, please have a look at the [local instance documentation](./local-instance.md). Recommended is to use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817). + +### Can I use AIO with multiple domains? +No and it will not be added. However you can use [this feature](https://github.com/nextcloud/all-in-one/blob/main/multiple-instances.md) in order to create multiple AIO instances, one for each domain. + +### Are other ports than the default 443 for Nextcloud supported? +No and they will not be. If port 443 and/or 80 is blocked for you, you may use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if you want to publish it online. If you already run a different service on port 443, please use a dedicated domain for Nextcloud and set it up correctly by following the [reverse proxy documentation](./reverse-proxy.md). However in all cases the Nextcloud interface will redirect you to port 443. + +### Can I run Nextcloud in a subdirectory on my domain? +No and it will not be added. Please use a dedicated (sub-)domain for Nextcloud and set it up correctly by following the [reverse proxy documentation](./reverse-proxy.md). Alternatively, you may use [Tailscale](https://github.com/nextcloud/all-in-one/discussions/6817) if you want to publish it online. + +### How can I access Nextcloud locally? +Please note that local access is not possible if you are running AIO behind Cloudflare Tunnel since TLS proxying is in that case offloaded to Cloudflares infrastructure. You can fix this by setting up your own reverse proxy that handles TLS proxying locally and will make the steps below work. + +Please make sure that if you are running AIO behind a reverse proxy, that the reverse proxy is configured to use port 443 on the server that runs it. Otherwise the steps below will not work. + +Now that this is out of the way, the recommended way how to access Nextcloud locally, is to set up a local dns-server like a pi-hole and set up a custom dns-record for that domain that points to the internal ip-adddress of your server that runs Nextcloud AIO. Below are some guides: +- https://www.howtogeek.com/devops/how-to-run-your-own-dns-server-on-your-local-network/ +- https://help.nextcloud.com/t/need-help-to-configure-internal-access/156075/6 +- https://howchoo.com/pi/pi-hole-setup together with https://web.archive.org/web/20221203223505/https://docs.callitkarma.me/posts/PiHole-Local-DNS/ +- https://dockerlabs.collabnix.com/intermediate/networking/Configuring_DNS.html +Apart from that there is now a community container that can be added to the AIO stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers/pi-hole + +### How to skip the domain validation? +If you are completely sure that you've configured everything correctly and are not able to pass the domain validation, you may skip the domain validation by adding `--env SKIP_DOMAIN_VALIDATION=true` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used). + +### How to resolve firewall problems with Fedora Linux, RHEL OS, CentOS, SUSE Linux and others? +It is known that Linux distros that use [firewalld](https://firewalld.org) as their firewall daemon have problems with docker networks. In case the containers are not able to communicate with each other, you may change your firewalld to use the iptables backend by running: +``` +sudo sed -i 's/FirewallBackend=nftables/FirewallBackend=iptables/g' /etc/firewalld/firewalld.conf +sudo systemctl restart firewalld docker +``` +Afterwards it should work.
    + +See https://dev.to/ozorest/fedora-32-how-to-solve-docker-internal-network-issue-22me for more details on this. This limitation is even mentioned on the official firewalld website: https://firewalld.org/#who-is-using-it + +### What can I do to fix the internal or reserved ip-address error? +If you get an error during the domain validation which states that your ip-address is an internal or reserved ip-address, you can fix this by first making sure that your domain indeed has the correct public ip-address that points to the server and then adding `--add-host yourdomain.com:` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) which will allow the domain validation to work correctly. And so that you know: even if the `A` record of your domain should change over time, this is no problem since the mastercontainer will not make any attempt to access the chosen domain after the initial domain validation. + +### How to adjust the MTU size of the docker network +You can adjust the MTU size of the docker network by creating it beforehand with the custom MTU: +``` +docker network create --driver bridge --opt com.docker.network.driver.mtu=1440 nextcloud-aio +``` +When you open the AIO interface for the first time after you execute the `docker run` command, it will automatically connect to the `aio-nextcloud` network with the custom MTU. Keep in mind that if you previously started the mastercontainer without creating the network with the extra options, you will need to remove the old `aio-nextcloud` network and recreate it with the new configuration. + +If you want to use docker compose, you can check out the comments in the `compose.yaml` file for more details. + +## Infrastructure + +### Which CPU architectures are supported? +You can check this on Linux by running: `uname -m` +- x86_64/x64/amd64 +- aarch64/arm64/armv8 + +### Disrecommended VPS providers +- *Older* Strato VPS using Virtuozzo caused problems though ones from Q3 2023 and later should work. + If your VPS has a `/proc/user_beancounters` file and a low `numproc` limit set in it + your server will likely misbehave once it reaches this limit + which is very quickly reached by AIO, see [here](https://github.com/nextcloud/all-in-one/discussions/1747#discussioncomment-4716164). +- Hostingers VPS seem to miss a specific Kernel feature which is required for AIO to run correctly. See [here](https://help.nextcloud.com/t/help-installing-nc-via-aio-on-vps/153956). + +### Recommended VPS +In general recommended VPS are those that are KVM/non-virtualized as Docker should work best on them. + +### Note on storage options +- SD-cards are disrecommended for AIO since they cripple the performance and they are not meant for many write operations which is needed for the database and other parts +- SSD storage is recommended +- HDD storage should work as well but is of course much slower than SSD storage + +### Are there known problems when SELinux is enabled? +Yes. If SELinux is enabled, you might need to add the `--security-opt label:disable` option to the docker run command of the mastercontainer in order to allow it to access the docker socket (or `security_opt: ["label:disable"]` in compose.yaml). See https://github.com/nextcloud/all-in-one/discussions/485 + +## Customization + +### How to change the default location of Nextcloud's Datadir? +> [!WARNING] +> Do not set or adjust this value after the initial Nextcloud installation is done! If you still want to do it afterwards, see [this](https://github.com/nextcloud/all-in-one/discussions/890#discussioncomment-3089903) on how to do it. + +You can configure the Nextcloud container to use a specific directory on your host as data directory. You can do so by adding the environmental variable `NEXTCLOUD_DATADIR` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used). Allowed values for that variable are strings that start with `/` and are not equal to `/`. The chosen directory or volume will then be mounted to `/mnt/ncdata` inside the container. + +- An example for Linux is `--env NEXTCLOUD_DATADIR="/mnt/ncdata"`. ⚠️ Please note: If you should be using an external BTRFS drive that is mounted to `/mnt/ncdata`, make sure to choose a subfolder like e.g. `/mnt/ncdata/nextcloud` as datadir, since the root folder is not suited as datadir in that case. See https://github.com/nextcloud/all-in-one/discussions/2696. +- On macOS it might be `--env NEXTCLOUD_DATADIR="/var/nextcloud-data"` +- For Synology it may be `--env NEXTCLOUD_DATADIR="/volume1/docker/nextcloud/data"`. +- On Windows it might be `--env NEXTCLOUD_DATADIR="/run/desktop/mnt/host/c/ncdata"`. (This path is equivalent to `C:\ncdata` on your Windows host so you need to translate the path accordingly. Hint: the path that you enter needs to start with `/run/desktop/mnt/host/`. Append to that the exact location on your windows host, e.g. `c/ncdata` which is equivalent to `C:\ncdata`.) ⚠️ **Please note**: This does not work with external drives like USB or network drives and only with internal drives like SATA or NVME drives. +- Another option is to provide a specific volume name here with: `--env NEXTCLOUD_DATADIR="nextcloud_aio_nextcloud_datadir"`. This volume needs to be created beforehand manually by you in order to be able to use it. e.g. on Windows with: + ``` + docker volume create ^ + --driver local ^ + --name nextcloud_aio_nextcloud_datadir ^ + -o device="/host_mnt/e/your/data/path" ^ + -o type="none" ^ + -o o="bind" + ``` + In this example, it would mount `E:\your\data\path` into the volume so for a different location you need to adjust `/host_mnt/e/your/data/path` accordingly. + +### How to store the files/installation on a separate drive? +You can move the whole docker library and all its files including all Nextcloud AIO files and folders to a separate drive by first mounting the drive in the host OS (NTFS is not supported and ext4 is recommended as FS) and then following this tutorial: https://www.guguweb.com/2019/02/07/how-to-move-docker-data-directory-to-another-location-on-ubuntu/
    +(Of course docker needs to be installed first for this to work.) + +⚠️ If you encounter errors from richdocuments in your Nextcloud logs, check in your Collabora container if the message "Capabilities are not set for the coolforkit program." appears. If so, follow these steps: + +1. Stop all the containers from the AIO Interface. +2. Go to your terminal and delete the Collabora container (`docker rm nextcloud-aio-collabora`) AND the Collabora image (`docker image rm nextcloud/aio-collabora`). +3. You might also want to prune your Docker (`docker system prune -a`) (no data will be lost). +4. Restart your containers from the AIO Interface. + +This should solve the problem. + +### How to allow the Nextcloud container to access directories on the host? +By default, the Nextcloud container is confined and cannot access directories on the host OS. You might want to change this when you are planning to use local external storage in Nextcloud to store some files outside the data directory and can do so by adding the environmental variable `NEXTCLOUD_MOUNT` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used). Allowed values for that variable are strings that start with `/` and are not equal to `/`. + +- Two examples for Linux are `--env NEXTCLOUD_MOUNT="/mnt/"` and `--env NEXTCLOUD_MOUNT="/media/"`. +- On macOS it might be `--env NEXTCLOUD_MOUNT="/Volumes/your_drive/"` +- For Synology it may be `--env NEXTCLOUD_MOUNT="/volume1/"`. +- On Windows it might be `--env NEXTCLOUD_MOUNT="/run/desktop/mnt/host/d/your-folder/"`. (This path is equivalent to `D:\your-folder` on your Windows host so you need to translate the path accordingly. Hint: the path that you enter needs to start with `/run/desktop/mnt/host/`. Append to that the exact location on your windows host, e.g. `d/your-folder/` which is equivalent to `D:\your-folder`.) ⚠️ **Please note**: This does not work with external drives like USB or network drives and only with internal drives like SATA or NVME drives. + +After using this option, please make sure to apply the correct permissions to the directories that you want to use in Nextcloud. E.g. `sudo chown -R 33:0 /mnt/your-drive-mountpoint` and `sudo chmod -R 750 /mnt/your-drive-mountpoint` should make it work on Linux when you have used `--env NEXTCLOUD_MOUNT="/mnt/"`. On Windows you could do this e.g. with `docker exec -it nextcloud-aio-nextcloud chown -R 33:0 /run/desktop/mnt/host/d/your-folder/` and `docker exec -it nextcloud-aio-nextcloud chmod -R 750 /run/desktop/mnt/host/d/your-folder/`. + +You can then navigate to `https://your-nc-domain.com/settings/apps/disabled`, activate the external storage app, navigate to `https://your-nc-domain.com/settings/admin/externalstorages` and add a local external storage directory that will be accessible inside the container at the same place that you've entered. E.g. `/mnt/your-drive-mountpoint` will be mounted to `/mnt/your-drive-mountpoint` inside the container, etc. + +Be aware though that these locations will not be covered by the built-in backup solution - but you can add further Docker volumes and host paths that you want to back up after the initial backup is done. + +> [!NOTE] +> If you can't see the type "local storage" in the external storage admin options, a restart of the containers from the AIO interface may be required. + +### How to adjust the Talk port? +By default will the talk container use port `3478/UDP` and `3478/TCP` for connections. This should be set to something higher than 1024! You can adjust the port by adding e.g. `--env TALK_PORT=3478` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and adjusting the port to your desired value. Best is to use a port over 1024, so e.g. 3479 to not run into this: https://github.com/nextcloud/all-in-one/discussions/2517 + +### How to adjust the upload limit for Nextcloud? +By default, public uploads to Nextcloud are limited to a max of 16G (logged in users can upload much bigger files using the webinterface or the mobile/desktop clients, since chunking is used in that case). You can adjust the upload limit by providing `--env NEXTCLOUD_UPLOAD_LIMIT=16G` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and customize the value to your fitting. It must start with a number and end with `G` e.g. `16G`. + +### How to adjust the max execution time for Nextcloud? +By default, uploads to Nextcloud are limited to a max of 3600s. You can adjust the upload time limit by providing `--env NEXTCLOUD_MAX_TIME=3600` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and customize the value to your fitting. It must be a number e.g. `3600`. + +### How to adjust the PHP memory limit for Nextcloud? +By default, each PHP process in the Nextcloud container is limited to a max of 512 MB. You can adjust the memory limit by providing `--env NEXTCLOUD_MEMORY_LIMIT=512M` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and customize the value to your fitting. It must start with a number and end with `M` e.g. `1024M`. + +### How to change the Nextcloud apps that are installed on the first startup? +You might want to adjust the Nextcloud apps that are installed upon the first startup of the Nextcloud container. You can do so by adding `--env NEXTCLOUD_STARTUP_APPS="deck twofactor_totp tasks calendar contacts notes"` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and customize the value to your fitting. It must be a string with small letters a-z, 0-9, spaces and hyphens or '_'. You can disable shipped and by default enabled apps by adding a hyphen in front of the appid. E.g. `-contactsinteraction`. + +### How to add OS packages permanently to the Nextcloud container? +Some Nextcloud apps require additional external dependencies that must be bundled within Nextcloud container in order to work correctly. As we cannot put each and every dependency for all apps into the container - as this would make the project quickly unmaintainable - there is an official way in which you can add additional dependencies into the Nextcloud container. However note that doing this is disrecommended since we do not test Nextcloud apps that require external dependencies. + +You can do so by adding `--env NEXTCLOUD_ADDITIONAL_APKS="imagemagick dependency2 dependency3"` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and customize the value to your fitting. It must be a string with small letters a-z, digits 0-9, spaces, dots and hyphens or '_'. You can find available packages here: https://pkgs.alpinelinux.org/packages?branch=v3.22. By default `imagemagick` is added. If you want to keep it, you need to specify it as well. + +### How to add PHP extensions permanently to the Nextcloud container? +Some Nextcloud apps require additional php extensions that must be bundled within Nextcloud container in order to work correctly. As we cannot put each and every dependency for all apps into the container - as this would make the project quickly unmaintainable - there is an official way in which you can add additional php extensions into the Nextcloud container. However note that doing this is disrecommended since we do not test Nextcloud apps that require additional php extensions. + +You can do so by adding `--env NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS="imagick extension1 extension2"` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and customize the value to your fitting. It must be a string with small letters a-z, digits 0-9, spaces, dots and hyphens or '_'. You can find available extensions here: https://pecl.php.net/packages.php. By default `imagick` is added. If you want to keep it, you need to specify it as well. + +### What about the pdlib PHP extension for the facerecognition app? +The [facerecognition app](https://apps.nextcloud.com/apps/facerecognition) requires the pdlib PHP extension to be installed. Unfortunately, it is not available on PECL nor via PHP core, so there is no way to add this into AIO currently. However you can use [this community container](https://github.com/nextcloud/all-in-one/tree/main/community-containers/facerecognition) in order to run facerecognition. + +### How to enable hardware acceleration for Nextcloud? +Some container can use GPU acceleration to increase performance like [memories app](https://apps.nextcloud.com/apps/memories) allows to enable hardware transcoding for videos. + +#### With open source drivers MESA for AMD, Intel and **new** drivers `Nouveau` for Nvidia + +> [!WARNING] +> This only works if the `/dev/dri` device is present on the host! If it does not exist on your host, don't proceed as otherwise the Nextcloud container will fail to start! If you are unsure about this, better do not proceed with the instructions below. Make sure that your driver is correctly configured on the host. + +A list of supported device can be fond in [MESA 3D documentation](https://docs.mesa3d.org/systems.html). + +This method use the [Direct Rendering Infrastructure](https://dri.freedesktop.org/wiki/) with the access to the `/dev/dri` device. + +In order to use that, you need to add `--env NEXTCLOUD_ENABLE_DRI_DEVICE=true` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) which will mount the `/dev/dri` device into the container. + + +#### With proprietary drivers for Nvidia :warning: BETA + +> [!WARNING] +> This only works if the Nvidia Toolkit is installed on the host and an NVIDIA GPU is enabled! Make sure that it is correctly configured on the host. If it does not exist on your host, don't proceed as otherwise the Nextcloud container will fail to start! If you are unsure about this, better do not proceed with the instructions below. +> +> This feature is in beta. Since the proprietary, we haven't a lot of user using proprietary drivers, we can't guarantee the stability of this feature. Your feedback is welcome. + +This method use the [Nvidia Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html) with the nvidia runtime. + +In order to use that, you need to add `--env NEXTCLOUD_ENABLE_NVIDIA_GPU=true` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) which will enable the nvidia runtime. + +If you're using WSL2 and want to use the NVIDIA runtime, please follow the instructions to [install the NVIDIA Container Toolkit meta-version in WSL](https://docs.nvidia.com/cuda/wsl-user-guide/index.html#cuda-support-for-wsl-2). + +### How to keep disabled apps? +In certain situations you might want to keep Nextcloud apps that are disabled in the AIO interface and not uninstall them if they should be installed in Nextcloud. You can do so by adding `--env NEXTCLOUD_KEEP_DISABLED_APPS=true` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used). +> [!WARNING] +> Doing this might cause unintended problems in Nextcloud if an app that requires an external dependency is still installed but the external dependency not for example. + +### How to trust user-defined Certification Authorities (CA)? +> [!NOTE] +> Please note, that this feature is only intended to make LDAPS connections with self-signed certificates work. It will not make other interconnectivity between the different containers work, as they expect a valid publicly trusted certificate like one from Let's Encrypt. + +For some applications it might be necessary to establish a secure connection to another host/server which is using a certificate issued by a Certification Authority that is not trusted out of the box. An example could be configuring LDAPS against a domain controller (Active Directory or Samba-based) of an organization. + +You can make the Nextcloud container trust any Certification Authority by providing the environmental variable `NEXTCLOUD_TRUSTED_CACERTS_DIR` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used). The value of the variables should be set to the absolute paths of the directory on the host, which contains one or more Certification Authorities certificates. You should use X.509 certificates, Base64 encoded. (Other formats may work but have not been tested!) All the certificates in the directory will be trusted. + +When using `docker run`, the environmental variable can be set with `--env NEXTCLOUD_TRUSTED_CACERTS_DIR=/path/to/my/cacerts`. + +In order for the value to be valid, the path should start with `/` and not end with `/` and point to an existing **directory**. Pointing the variable directly to a certificate **file** will not work and may also break things. + +### How to disable Collabora's Seccomp feature? +The Collabora container enables Seccomp by default, which is a security feature of the Linux kernel. On systems without this kernel feature enabled, you need to provide `--env COLLABORA_SECCOMP_DISABLED=true` to the initial docker run command in order to make it work. If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used. + +### How to adjust the Fulltextsearch Java options? +The Fulltextsearch Java options are by default set to `-Xms512M -Xmx512M` which might not be enough on some systems. You can adjust this by adding e.g. `--env FULLTEXTSEARCH_JAVA_OPTIONS="-Xms1024M -Xmx1024M"` to the initial docker run command. If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used. + +## Guides + +### How to run AIO on macOS? + +> [!NOTE] +> On macOS, it is recommended to use OrbStack instead of Docker Desktop which has much better compatibility with docker for Linux compared to Docker Desktop. See https://orbstack.dev/ + +Generally, on macOS, there is only one thing different for the docker run command in comparison to Linux: instead of using `--volume /var/run/docker.sock:/var/run/docker.sock:ro`, you need to use `--volume /var/run/docker.sock.raw:/var/run/docker.sock:ro` to run it after you installed [Docker Desktop](https://www.docker.com/products/docker-desktop/) (and don't forget to [enable ipv6](https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md) if you should need that). Apart from that it should work and behave the same like on Linux. + +Also, you may be interested in adjusting Nextcloud's Datadir to store the files on the host system. See [this documentation](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) on how to do it. + +### How to run AIO on Windows? +On Windows, install [Docker Desktop](https://www.docker.com/products/docker-desktop/) (and don't forget to [enable ipv6](https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md) if you should need that) and run the following command in the command prompt: + +``` +docker run ^ +--init ^ +--sig-proxy=false ^ +--name nextcloud-aio-mastercontainer ^ +--restart always ^ +--publish 80:80 ^ +--publish 8080:8080 ^ +--publish 8443:8443 ^ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config ^ +--volume //var/run/docker.sock:/var/run/docker.sock:ro ^ +ghcr.io/nextcloud-releases/all-in-one:latest +``` + +Also, you may be interested in adjusting Nextcloud's Datadir to store the files on the host system. See [this documentation](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) on how to do it. + +> [!NOTE] +> Almost all commands in this project's documentation use `sudo docker ...`. Since `sudo` is not available on Windows, you simply remove `sudo` from the commands and they should work. + +### How to run AIO on Synology DSM +On Synology, there are two things different in comparison to Linux: instead of using `--volume /var/run/docker.sock:/var/run/docker.sock:ro`, you need to use `--volume /volume1/docker/docker.sock:/var/run/docker.sock:ro` to run it. You also need to add `--env WATCHTOWER_DOCKER_SOCKET_PATH="/volume1/docker/docker.sock"`to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`). Apart from that it should work and behave the same like on Linux. Obviously the Synology Docker GUI will not work with that so you will need to either use SSH or create a user-defined script task in the task scheduler as the user 'root' in order to run the command. + +> [!NOTE] +> It is possible that the docker socket on your Synology is located in `/var/run/docker.sock` like the default on Linux. Then you can just use the Linux command without having to change anything - you will notice this when you try to start the container and it says that the bind mount failed. E.g. `docker: Error response from daemon: Bind mount failed: '/volume1/docker/docker.sock' does not exists.` + +Also, you may be interested in adjusting Nextcloud's Datadir to store the files on the host system. See [this documentation](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) on how to do it. + +You'll also need to adjust Synology's firewall, see below: + +
    +Click here to expand + +The Synology DSM is vulnerable to attacks with it's open ports and login interfaces, which is why a firewall setup is always recommended. If a firewall is activated it is necessary to have exceptions for ports 80,443, the subnet of the docker bridge which includes the Nextcloud containers, your public static IP (if you don't use DDNS) and if applicable your NC-Talk ports 3478 TCP+UDP: + +![Screenshot 2023-01-19 at 14 13 48](https://user-images.githubusercontent.com/70434961/213677995-71a9f364-e5d2-49e5-831e-4579f217c95c.png) + +If you have the NAS setup on your local network (which is most often the case) you will need to setup the Synology DNS to be able to access Nextcloud from your network via its domain. Also don't forget to add the new DNS to your DHCP server and your fixed IP settings: + +![Screenshot 2023-01-20 at 12 13 44](https://user-images.githubusercontent.com/70434961/213683295-0b39a2bd-7a26-414c-a408-127dd4f07826.png) +
    + +### How to run AIO with Portainer? +The easiest way to run it with Portainer on Linux is to use Portainer's stacks feature and use [this docker-compose file](./compose.yaml) in order to start AIO correctly. + +### Can I run AIO on TrueNAS SCALE? +With the Truenas Scale Release 24.10.0 (which was officially released on October 29th 2024 as a stable release) IX Systems ditched the Kubernetes integration and implemented a fully working docker environment. + +For a more complete guide, see this guide by @zybster: https://github.com/nextcloud/all-in-one/discussions/5506 + +On older TrueNAS SCALE releases with Kubernetes environment, there are two ways to run AIO. The preferred one is to run AIO inside a VM. This is necessary since they do not expose the docker socket for containers on the host, you also cannot use docker-compose on it thus and it is also not possible to run custom helm-charts that are not explicitly written for TrueNAS SCALE. + +Another but untested way is to install Portainer on your TrueNAS SCALE from here https://truecharts.org/charts/stable/portainer/installation-notes and add the Helm-chart repository https://nextcloud.github.io/all-in-one/ into Portainer by following https://docs.portainer.io/user/kubernetes/helm. More docs on AIOs Helm Chart are available here: https://github.com/nextcloud/all-in-one/tree/main/nextcloud-aio-helm-chart#nextcloud-aio-helm-chart. + +### How to run `occ` commands? +Simply run the following: `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ your-command`. Of course `your-command` needs to be exchanged with the command that you want to run. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management + +### How to resolve `Security & setup warnings displays the "missing default phone region" after initial install`? +Simply run the following command: `sudo docker exec --user www-data nextcloud-aio-nextcloud php occ config:system:set default_phone_region --value="yourvalue"`. Of course you need to modify `yourvalue` based on your location. Examples are `DE`, `US` and `GB`. See this list for more codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#Officially_assigned_code_elements **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management + +### How to run multiple AIO instances on one server? +See [multiple-instances.md](./multiple-instances.md) for some documentation on this. + +### Bruteforce protection FAQ +Nextcloud features a built-in bruteforce protection which may get triggered and will block an ip-address or disable a user. You can unblock an ip-address by running `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ security:bruteforce:reset ` and enable a disabled user by running `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ user:enable `. See https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/occ_command.html#security for further information. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management + +### How to switch the channel? +You can switch to a different channel like e.g. the beta channel or from the beta channel back to the latest channel by stopping the mastercontainer, removing it (no data will be lost) and recreating the container using the same command that you used initially to create the mastercontainer. You simply need to change the last line `ghcr.io/nextcloud-releases/all-in-one:latest` to `ghcr.io/nextcloud-releases/all-in-one:beta` and vice versa. + +### How to update the containers? +If we push new containers to `latest`, you will see in the AIO interface below the `containers` section that new container updates were found. In this case, just press `Stop containers` and `Start and update containers` in order to update the containers. The mastercontainer has its own update procedure though. See below. And don't forget to back up the current state of your instance using the built-in backup solution before starting the containers again! Otherwise you won't be able to restore your instance easily if something should break during the update. + +If a new `mastercontainer` update was found, you'll see a note below the `Stop containers` button that allows to show the changelog. If you click that button and the containers are stopped, you will see a new button that allows to update the mastercontainer. After doing so and after the update is gone through, you will have the option again to `Start and update containers`. It is recommended to create a backup before clicking the `Start and update containers` button. + +Additionally, there is a cronjob that runs once a day that checks for container and mastercontainer updates and sends a notification to all Nextcloud admins if a new update was found. + +### How to easily log in to the AIO interface? +If your Nextcloud is running and you are logged in as admin in your Nextcloud, you can easily log in to the AIO interface by opening `https://yourdomain.tld/settings/admin/overview` which will show a button on top that enables you to log in to the AIO interface by just clicking on this button. + +> [!Note] +> You can change the domain/ip-address/port of the button by simply stopping the containers, visiting the AIO interface from the correct and desired domain/ip-address/port and clicking once on `Start containers`. + +### How to change the domain? +> [!NOTE] +> Editing the configuration.json manually and making a mistake may break your instance so please create a backup first! + +If you set up a new AIO instance, you need to enter a domain. Currently there is no way to change this domain afterwards from the AIO interface. So in order to change it, you need to edit the configuration.json manually using `sudo docker run -it --rm --volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config:rw alpine sh -c "apk add --no-cache nano && nano /mnt/docker-aio-config/data/configuration.json"`, substitute each occurrence of your old domain with your new domain and save and write out the file. Afterwards restart your containers from the AIO interface and everything should work as expected if the new domain is correctly configured.
    +If you are running AIO behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else), you need to obviously also change the domain in your reverse proxy config. + +Additionally, after restarting the containers, you need to open the admin settings and update some values manually that cannot be changed automatically. Here is a list of some known places: +- `https://your-nc-domain.com/settings/admin/talk` for Turn/Stun server and Signaling Server if you enabled Talk via the AIO interface +- `https://your-nc-domain.com/settings/admin/theming` for the theming URL +- `https://your-nc-domain.com/settings/admin/app_api` for the deploy daemon if you enabled the App API via the AIO interface + +### How to properly reset the instance? +If something goes unexpected routes during the initial installation, you might want to reset the AIO installation to be able to start from scratch. + +> [!NOTE] +> If you already have it running and have data on your instance, you should not follow these instructions as it will delete all data that is coupled to your AIO instance. + +Here is how to reset the AIO instance properly: +1. Stop all containers if they are running from the AIO interface +1. Stop the mastercontainer with `sudo docker stop nextcloud-aio-mastercontainer` +1. If the domaincheck container is still running, stop it with `sudo docker stop nextcloud-aio-domaincheck` +1. Check that no AIO containers are running anymore by running `sudo docker ps --format {{.Names}}`. If no `nextcloud-aio` containers are listed, you can proceed with the steps below. If there should be some, you will need to stop them with `sudo docker stop ` until no one is listed anymore. +1. Check which containers are stopped: `sudo docker ps --filter "status=exited"` +1. Now remove all these stopped containers with `sudo docker container prune` +1. Delete the docker network with `sudo docker network rm nextcloud-aio` +1. Check which volumes are dangling with `sudo docker volume ls --filter "dangling=true"` +1. Now remove all these dangling volumes: `sudo docker volume prune --filter all=1` (on Windows you might need to remove some volumes afterwards manually with `docker volume rm nextcloud_aio_backupdir`, `docker volume rm nextcloud_aio_nextcloud_datadir`). +1. If you've configured `NEXTCLOUD_DATADIR` to a path on your host instead of the default volume, you need to clean that up as well. (E.g. by simply deleting the directory). +1. Make sure that no volumes are remaining with `sudo docker volume ls --format {{.Name}}`. If no `nextcloud-aio` volumes are listed, you can proceed with the steps below. If there should be some, you will need to remove them with `sudo docker volume rm ` until no one is listed anymore. +1. Optional: You can remove all docker images with `sudo docker image prune -a`. +1. And you are done! Now feel free to start over with the recommended docker run command! + +### Can I use a CIFS/SMB share as Nextcloud's datadir? +Sure. Add this to the `/etc/fstab` file on the host system:
    +` cifs rw,mfsymlinks,seal,credentials=,uid=33,gid=0,file_mode=0770,dir_mode=0770 0 0`
    +(Of course you need to modify ``, `` and `` for your specific case.) + +One example could look like this:
    +`//your-storage-host/subpath /mnt/storagebox cifs rw,mfsymlinks,seal,credentials=/etc/storage-credentials,uid=33,gid=0,file_mode=0770,dir_mode=0770 0 0`
    +and add into `/etc/storage-credentials`: +``` +username= +password= +``` +(Of course you need to modify `` and `` for your specific case.) + +Now you can use `/mnt/storagebox` as Nextcloud's datadir like described in the section above this one. + +### Can I run this with Docker swarm? +Yes. For that to work, you need to use and follow the [manual-install documentation](./manual-install/). + +### Can I run this with Kubernetes? +Yes. For that to work, you need to use and follow the [helm-chart documentation](./nextcloud-aio-helm-chart/). + +### How to run this with Docker rootless? +You can run AIO also with docker rootless. How to do this is documented here: [docker-rootless.md](https://github.com/nextcloud/all-in-one/blob/main/docker-rootless.md) + +### Can I run this with Podman instead of Docker? +Since Podman is not 100% compatible with the Docker API, Podman is not supported (since that would add yet another platform where the maintainer would need to test on). However you can use and follow the [manual-install documentation](./manual-install/) to get AIO's containers running with Podman or use Docker rootless, as described in the above section. Also there is this now: https://github.com/nextcloud/all-in-one/discussions/3487 + +### Access/Edit Nextcloud files/folders manually +The files and folders that you add to Nextcloud are by default stored in the following docker directory: `nextcloud_aio_nextcloud:/mnt/ncdata/` (usually `/var/lib/docker/volumes/nextcloud_aio_nextcloud_data/_data/` on linux host systems). If needed, you can modify/add/delete files/folders there but **ATTENTION**: be very careful when doing so because you might corrupt your AIO installation! Best is to create a backup using the built-in backup solution before editing/changing files/folders in there because you will then be able to restore your instance to the backed up state. + +After you are done modifying/adding/deleting files/folders, don't forget to apply the correct permissions by running: `sudo docker exec nextcloud-aio-nextcloud chown -R 33:0 /mnt/ncdata/` and `sudo docker exec nextcloud-aio-nextcloud chmod -R 750 /mnt/ncdata/` and rescan the files with `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ files:scan --all`. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management + +### How to edit Nextclouds config.php file with a texteditor? +You can edit Nextclouds config.php file directly from the host with your favorite text editor. E.g. like this: `sudo docker run -it --rm --volume nextcloud_aio_nextcloud:/var/www/html:rw alpine sh -c "apk add --no-cache nano && nano /var/www/html/config/config.php"`. Make sure to not break the file though which might corrupt your Nextcloud instance otherwise. In best case, create a backup using the built-in backup solution before editing the file. **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management + +### How to change default files by creating a custom skeleton directory? +All users see a set of [default files and folders](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/default_files_configuration.html) as dictated by Nextcloud's configuration. To change these default files and folders a custom skeleton directory must first be created; this can be accomplished by copying your skeleton files `sudo docker cp --follow-link /path/to/nextcloud/skeleton/ nextcloud-aio-nextcloud:/mnt/ncdata/skeleton/`, applying the correct permissions with `sudo docker exec nextcloud-aio-nextcloud chown -R 33:0 /mnt/ncdata/skeleton/` and `sudo docker exec nextcloud-aio-nextcloud chmod -R 750 /mnt/ncdata/skeleton/` and setting the skeleton directory option with `sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:system:set skeletondirectory --value="/mnt/ncdata/skeleton"`. Further information is available in the Nextcloud documentation on [configuration parameters for the skeleton directory](https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/config_sample_php_parameters.html#skeletondirectory). + +### How to adjust the version retention policy and trashbin retention policy? +By default, AIO sets the `versions_retention_obligation` and `trashbin_retention_obligation` both to `auto, 30` which means that versions and items in the trashbin get deleted after 30 days. If you want to change this, see https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/file_versioning.html. + +### How to enable automatic updates without creating a backup beforehand? +If you have an external backup solution, you might want to enable automatic updates without creating a backup first. However note that doing this is disrecommended since you will not be able to easily create and restore a backup from the AIO interface anymore and you need to make sure to shut down all the containers properly before creating the backup, e.g. by stopping them from the AIO interface first. + +But anyhow, is here a guide that helps you automate the whole procedure: + +
    +Click here to expand + +```bash +#!/bin/bash + +# Stop the containers +docker exec --env STOP_CONTAINERS=1 nextcloud-aio-mastercontainer /daily-backup.sh + +# Below is optional if you run AIO in a VM which will shut down the VM afterwards +# poweroff + +``` + +
    + +You can simply copy and paste the script into a file e.g. named `shutdown-script.sh` e.g. here: `/root/shutdown-script.sh`. + +Afterwards apply the correct permissions with `sudo chown root:root /root/shutdown-script.sh` and `sudo chmod 700 /root/shutdown-script.sh`. Then you can create a cronjob that runs it on a schedule e.g. runs the script at `04:00` each day like this: +1. Open the cronjob with `sudo crontab -u root -e` (and choose your editor of choice if not already done. I'd recommend nano). +1. Add the following new line to the crontab if not already present: `0 4 * * * /root/shutdown-script.sh` which will run the script at 04:00 each day. +1. save and close the crontab (when using nano the shortcuts for this are `Ctrl + o` and then `Enter` to save, and close the editor with `Ctrl + x`). + + +**After that is in place, you should schedule a backup from your backup solution that creates a backup after AIO is shut down properly. Hint: If your backup runs on the same host, make sure to at least back up all docker volumes and additionally Nextcloud's datadir if it is not stored in a docker volume.** + +**Afterwards, you can create a second script that automatically updates the containers:** + +
    +Click here to expand + +```bash +#!/bin/bash + +# Run container update once +if ! docker exec --env AUTOMATIC_UPDATES=1 nextcloud-aio-mastercontainer /daily-backup.sh; then + while docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-watchtower$"; do + echo "Waiting for watchtower to stop" + sleep 30 + done + + while ! docker ps --format "{{.Names}}" | grep -q "^nextcloud-aio-mastercontainer$"; do + echo "Waiting for Mastercontainer to start" + sleep 30 + done + + # Run container update another time to make sure that all containers are updated correctly. + docker exec --env AUTOMATIC_UPDATES=1 nextcloud-aio-mastercontainer /daily-backup.sh +fi + +``` + +
    + +You can simply copy and paste the script into a file e.g. named `automatic-updates.sh` e.g. here: `/root/automatic-updates.sh`. + +Afterwards apply the correct permissions with `sudo chown root:root /root/automatic-updates.sh` and `sudo chmod 700 /root/automatic-updates.sh`. Then you can create a cronjob that runs e.g. at `05:00` each day like this: +1. Open the cronjob with `sudo crontab -u root -e` (and choose your editor of choice if not already done. I'd recommend nano). +1. Add the following new line to the crontab if not already present: `0 5 * * * /root/automatic-updates.sh` which will run the script at 05:00 each day. +1. save and close the crontab (when using nano the shortcuts for this are `Ctrl + o` then `Enter` to save, and close the editor with `Ctrl + x`). + +### Securing the AIO interface from unauthorized ACME challenges +[By design](https://github.com/nextcloud/all-in-one/discussions/4882#discussioncomment-9858384), Caddy that runs inside the mastercontainer, which handles automatic TLS certificate generation for the AIO interface on port 8443, is configured to accept traffic on any valid domain in order to make the AIO interface as convenient to use as possible. However due to this, it is vulnerable to receiving DNS challenges for arbitrary hostnames from anyone on the internet. While this does not compromise your server's security, it can result in cluttered logs and rejected certificate renewal attempts due to rate limit abuse. To mitigate this issue, it is recommended to place the AIO interface behind a VPN and/or limit its public exposure. + +### How to migrate from an already existing Nextcloud installation to Nextcloud AIO? +Please see the following documentation on this: [migration.md](https://github.com/nextcloud/all-in-one/blob/main/migration.md) + +## Backup +Nextcloud AIO provides a backup solution based on [BorgBackup](https://github.com/borgbackup/borg#what-is-borgbackup). These backups act as a restore point in case the installation gets corrupted. By using this tool, backups are incremental, differential, compressed and encrypted – so only the first backup will take a while. Further backups should be fast as only changes are taken into account. + +It is recommended to create a backup before any container update. By doing this, you will be safe regarding any possible complication during updates because you will be able to restore the whole instance with basically one click. + +For local backups, the restore process should be pretty fast as rsync is used to restore the chosen backup which only transfers changed files and deletes additional ones. For remote borg backups, the whole backup archive is extracted from the remote, which depending on how clever `borg extract` is, may require downloading the whole archive. + +If you connect an external drive to your host, and choose the backup directory to be on that drive, you are also kind of safe against drive failures of the drive where the docker volumes are stored on. + +
    +How to do the above step for step + +1. Mount an external/backup HDD to the host OS using the built-in functionality or udev rules or whatever way you prefer. (E.g. follow this video: https://www.youtube.com/watch?v=2lSyX4D3v_s) and mount the drive in best case in `/mnt/backup`. +2. If not already done, fire up the docker container and set up Nextcloud as per the guide. +3. Now open the AIO interface. +4. Under backup section, add your external disk mountpoint as backup directory, e.g. `/mnt/backup`. +5. Click on `Create Backup` which should create the first backup on the external disk. + +
    + +If you want to back up directly to a remote borg repository: + +
    +How to do the above step for step + +1. Create your borg repository at the remote. Note down the repository URL for later. +2. Open the AIO interface +3. Under backup section, leave the local path blank and fill in the url to your borg repository that you noted down earlier. +4. Click on `Create backup`, this will create an ssh key pair and fail because the remote doesn't trust this key yet. Copy the public key shown in AIO and add it to your authorized keys on the remote. +5. Try again to create a backup, this time it should succeed. + +
    + +Backups can be created and restored in the AIO interface using the buttons `Create Backup` and `Restore selected backup`. Additionally, a backup check is provided that checks the integrity of your backups but it shouldn't be needed in most situations. + +The backups themselves get encrypted with an encryption key that gets shown to you in the AIO interface. Please save that at a safe place as you will not be able to restore from backup without this key. + +Daily backups can get enabled after the initial backup is done. Enabling this also allows to enable an option that allows to automatically update all containers, Nextcloud and its apps. + +Be aware that this solution does not back up files and folders that are mounted into Nextcloud using the external storage app - but you can add further Docker volumes and host paths that you want to back up after the initial backup is done. + +--- + +### What is getting backed up by AIO's backup solution? +Backed up will get all important data of your Nextcloud AIO instance required to restore the instance, like the database, your files and configuration files of the mastercontainer and else. Files and folders that are mounted into Nextcloud using the external storage app are not getting backed up. There is currently no way to exclude the data directory because it would require hacks like running files:scan and would make the backup solution much more unreliable (since the database and your files/folders need to stay in sync). If you still don't want your datadirectory to be backed up, see https://github.com/nextcloud/all-in-one#how-to-enable-automatic-updates-without-creating-a-backup-beforehand for options (there is a hint what needs to be backed up in which order). + +### How to adjust borgs retention policy? +The built-in borg-based backup solution has by default a retention policy of `--keep-within=7d --keep-weekly=4 --keep-monthly=6`. See https://borgbackup.readthedocs.io/en/stable/usage/prune.html for what these values mean. You can adjust the retention policy by providing `--env BORG_RETENTION_POLICY="--keep-within=7d --keep-weekly=4 --keep-monthly=6"` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used) and customize the value to your fitting. ⚠️ Please make sure that this value is valid, otherwise backup pruning will bug out! + +### How to migrate from AIO to AIO? +If you have the borg backup feature enabled, you can copy it over to the new host and restore from the backup. This guide assumes the new installation data dir will be on `/mnt/datadir`, you can adjust the steps if it's elsewhere. + +1. Set the DNS entry to 60 seconds TTL if applicable +1. On your current installation, use the AIO interface to: + 1. Update AIO and all containers + 1. Stop all containers (from now on, your cloud is down) + 1. Create a current borg backup + 1. Note the path where the backups are stored and the encryption password +1. Navigate to the backup folder +1. Create archive of the backup so it's easier to copy: `tar -czvf borg.tar.gz borg` +1. Copy the archive over to the new host: `scp borg.tar.gz user@new.host:/mnt`. Make sure to replace `user` with your actual user and `new.host` with the IP or domain of the actual host. You can also use another way to copy the archive. +1. Switch to the new host +1. Go to the folder you put the backup archive and extract it with `tar -xf borg.tar.gz` +1. Follow the installation guide to create a new aio instance, but do not start the containers yet (the `docker run` or `docker compose up -d` command) +1. Change the DNS entry to the new host's IP +1. Configure your reverse proxy if you use one +1. Start the AIO container and open the new AIO interface in your browser +1. Make sure to save the newly generated passphrase and enter it in the next step +1. Select the "Restore former AIO instance from backup" option and enter the encryption password from the old backup and the path in which the extracted `borg` folder lies in (without the borg part) and hit `Submit location and password` +1. Choose the latest backup in the dropdown and hit `Restore selected backup` +1. Wait until the backup is restored +1. Start the containers in the AIO interface + +### Are remote borg backups supported? +Backing up directly to a remote borg repository is supported. This avoids having to store a local copy of your backups, supports append-only borg keys to counter ransomware and allows using the AIO interface to manage your backups. + +Some alternatives, which do not have all the above benefits: + +- Mount a network FS like SSHFS, SMB or NFS in the directory that you enter in AIO as backup directory +- Use rsync or rclone for syncing the borg backup archive that AIO creates locally to a remote target (make sure to lock the backup archive correctly before starting the sync; search for "aio-lockfile"; you can find a local example script here: https://github.com/nextcloud/all-in-one#sync-local-backups-regularly-to-another-drive) +- You can find a well written guide that uses rclone and e.g. BorgBase for remote backups here: https://github.com/nextcloud/all-in-one/discussions/2247 +- Here is another one that utilizes borgmatic and BorgBase for remote backups: https://github.com/nextcloud/all-in-one/discussions/4391 +- create your own backup solution using a script and borg, borgmatic or any other to backup tool for backing up to a remote target (make sure to stop and start the AIO containers correctly following https://github.com/nextcloud/all-in-one#how-to-enable-automatic-updates-without-creating-a-backup-beforehand) + +--- + +### Failure of the backup container in LXC containers +If you are running AIO in a LXC container, you need to make sure that FUSE is enabled in the LXC container settings. Also, if using Alpine Linux as host OS, make sure to add fuse via `apk add fuse`. Otherwise the backup container will not be able to start as FUSE is required for it to work. + +--- + +### How to create the backup volume on Windows? +As stated in the AIO interface, it is possible to use a docker volume as backup target. Before you can use that, you need to create it first. Here is an example how to create one on Windows: +``` +docker volume create ^ +--driver local ^ +--name nextcloud_aio_backupdir ^ +-o device="/host_mnt/e/your/backup/path" ^ +-o type="none" ^ +-o o="bind" +``` +In this example, it would mount `E:\your\backup\path` into the volume so for a different location you need to adjust `/host_mnt/e/your/backup/path` accordingly. Afterwards enter `nextcloud_aio_backupdir` in the AIO interface as backup location. + +--- + +### Pro-tip: Backup archives access +You can open the BorgBackup archives on your host by following these steps:
    +(instructions for Ubuntu Desktop) + +Alternatively, there is now a community container that allows to access your backups in a web session: https://github.com/nextcloud/all-in-one/tree/main/community-containers/borgbackup-viewer. + +```bash +# Install borgbackup on the host +sudo apt update && sudo apt install borgbackup + +# In any shell where you use borg, you must first export this variable +# If you are using the default backup location /mnt/backup/borg +export BORG_REPO='/mnt/backup/borg' +# or if you are using a remote repository +export BORG_REPO='user@host:/path/to/repo' + +# Mount the archives to /tmp/borg +sudo mkdir -p /tmp/borg && sudo borg mount "$BORG_REPO" /tmp/borg + +# After entering your repository key successfully, you should be able to access all archives in /tmp/borg +# You can now do whatever you want by syncing them to a different place using rsync or doing other things +# E.g. you can open the file manager on that location by running: +xhost +si:localuser:root && sudo nautilus /tmp/borg + +# When you are done, simply close the file manager and run the following command to unmount the backup archives: +sudo umount /tmp/borg +``` + +--- + +### Delete backup archives manually +You can delete BorgBackup archives on your host manually by following these steps:
    +(instructions for Debian based OS' like Ubuntu) + +Alternatively, there is now a community container that allows to access your backups in a web session: https://github.com/nextcloud/all-in-one/tree/main/community-containers/borgbackup-viewer. + +```bash +# Install borgbackup on the host +sudo apt update && sudo apt install borgbackup + +# In any shell where you use borg, you must first export this variable +# If you are using the default backup location /mnt/backup/borg +export BORG_REPO='/mnt/backup/borg' +# or if you are using a remote repository +export BORG_REPO='user@host:/path/to/repo' + +# List all archives (if you are using the default backup location /mnt/backup/borg) +sudo borg list + +# After entering your repository key successfully, you should now see a list of all backup archives +# An example backup archive might be called 20220223_174237-nextcloud-aio +# Then you can simply delete the archive with: +sudo borg delete --stats --progress "::20220223_174237-nextcloud-aio" + +# If borg 1.2.0 or higher is installed, you then need to run borg compact in order to clean up the freed space +sudo borg --version +# If version number of the command above is higher than 1.2.0 you need to run the command below: +sudo borg compact + +``` + +After doing so, make sure to update the backup archives list in the AIO interface!
    +You can do so by clicking on the `Check backup integrity` button or `Create backup` button. + +--- + +### Sync local backups regularly to another drive +For increased backup security, you might consider syncing the local backup repository regularly to another drive. + +To do that, first add the drive to `/etc/fstab` so that it is able to get automatically mounted and then create a script that does all the things automatically. Here is an example for such a script: + +
    +Click here to expand + +```bash +#!/bin/bash + +# Please modify all variables below to your needings: +SOURCE_DIRECTORY="/mnt/backup/borg" +DRIVE_MOUNTPOINT="/mnt/backup-drive" +TARGET_DIRECTORY="/mnt/backup-drive/borg" + +######################################## +# Please do NOT modify anything below! # +######################################## + +if [ "$EUID" -ne 0 ]; then + echo "Please run as root" + exit 1 +fi + +if ! [ -d "$SOURCE_DIRECTORY" ]; then + echo "The source directory does not exist." + exit 1 +fi + +if [ -z "$(ls -A "$SOURCE_DIRECTORY/")" ]; then + echo "The source directory is empty which is not allowed." + exit 1 +fi + +if ! [ -d "$DRIVE_MOUNTPOINT" ]; then + echo "The drive mountpoint must be an existing directory" + exit 1 +fi + +if ! grep -q "$DRIVE_MOUNTPOINT" /etc/fstab; then + echo "Could not find the drive mountpoint in the fstab file. Did you add it there?" + exit 1 +fi + +if ! mountpoint -q "$DRIVE_MOUNTPOINT"; then + mount "$DRIVE_MOUNTPOINT" + if ! mountpoint -q "$DRIVE_MOUNTPOINT"; then + echo "Could not mount the drive. Is it connected?" + exit 1 + fi +fi + +if [ -f "$SOURCE_DIRECTORY/lock.roster" ]; then + echo "Cannot run the script as the backup archive is currently changed. Please try again later." + exit 1 +fi + +mkdir -p "$TARGET_DIRECTORY" +if ! [ -d "$TARGET_DIRECTORY" ]; then + echo "Could not create target directory" + exit 1 +fi + +if [ -f "$SOURCE_DIRECTORY/aio-lockfile" ]; then + echo "Not continuing because aio-lockfile already exists." + exit 1 +fi + +touch "$SOURCE_DIRECTORY/aio-lockfile" + +if ! rsync --stats --archive --human-readable --delete "$SOURCE_DIRECTORY/" "$TARGET_DIRECTORY"; then + echo "Failed to sync the backup repository to the target directory." + exit 1 +fi + +rm "$SOURCE_DIRECTORY/aio-lockfile" +rm "$TARGET_DIRECTORY/aio-lockfile" + +umount "$DRIVE_MOUNTPOINT" + +if docker ps --format "{{.Names}}" | grep "^nextcloud-aio-nextcloud$"; then + docker exec nextcloud-aio-nextcloud bash /notify.sh "Rsync backup successful!" "Synced the backup repository successfully." +else + echo "Synced the backup repository successfully." +fi + +``` + +
    + +You can simply copy and paste the script into a file e.g. named `backup-script.sh` e.g. here: `/root/backup-script.sh`. Do not forget to modify the variables to your requirements! + +Afterwards apply the correct permissions with `sudo chown root:root /root/backup-script.sh` and `sudo chmod 700 /root/backup-script.sh`. Then you can create a cronjob that runs e.g. at `20:00` each week on Sundays like this: +1. Open the cronjob with `sudo crontab -u root -e` (and choose your editor of choice if not already done. I'd recommend nano). +1. Add the following new line to the crontab if not already present: `0 20 * * 7 /root/backup-script.sh` which will run the script at 20:00 on Sundays each week. +1. save and close the crontab (when using nano are the shortcuts for this `Ctrl + o` -> `Enter` and close the editor with `Ctrl + x`). + +### How to exclude Nextcloud's data directory or the preview folder from backup? +In order to speed up the backups and to keep the backup archives small, you might want to exclude Nextcloud's data directory or its preview folder from backup. + +> [!WARNING] +> However please note that you will run into problems if the database and the data directory or preview folder get out of sync. **So please only read further, if you have an additional external backup of the data directory!** See [this guide](#how-to-enable-automatic-updates-without-creating-a-backup-beforehand) for example. + +> [!TIP] +> A better option is to use the external storage app inside Nextcloud as the data connected via the external storage app is not backed up by AIO's backup solution. See [this documentation](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/external_storage_configuration_gui.html) on how to configure the app. + +If you still want to proceed, you can exclude the data directory by simply creating a `.noaiobackup` file in the root directory of the specified `NEXTCLOUD_DATADIR` target. The same logic is implemented for the preview folder that is located inside the data directory, inside the `appdata_*/preview` folder. So simply create a `.noaiobackup` file in there if you want to exclude the preview folder. + +After doing a restore via the AIO interface, you might run into problems due to the data directory and database being out of sync. You might be able to fix this by running `occ files:scan --all` and `occ maintenance:repair` and `occ files:scan-app-data`. See https://github.com/nextcloud/all-in-one#how-to-run-occ-commands. If only the preview folder is excluded, the command `occ files:scan-app-data preview` should be used. + +### How to stop/start/update containers or trigger the daily backup from a script externally? +> [!WARNING] +> The below script will only work after the initial setup of AIO. So you will always need to first visit the AIO interface, type in your domain and start the containers the first time or restore an older AIO instance from its borg backup before you can use the script. + +You can do so by running the `/daily-backup.sh` script that is stored in the mastercontainer. It accepts the following environment variables: +- `AUTOMATIC_UPDATES` if set to `1`, it will automatically stop the containers, update them and start them including the mastercontainer. If the mastercontainer gets updated, this script's execution will stop as soon as the mastercontainer gets stopped. You can then wait until it is started again and run the script with this flag again in order to update all containers correctly afterwards. +- `DAILY_BACKUP` if set to `1`, it will automatically stop the containers and create a backup. If you want to start them again afterwards, you may have a look at the `START_CONTAINERS` option. +- `STOP_CONTAINERS` if set to `1`, it will automatically stop the containers at the start of the script. Implied by `DAILY_BACKUP=1`. +- `START_CONTAINERS` if set to `1`, it will automatically start the containers at the end of the script, without updating them. Implied by `DAILY_BACKUP=1`. +- `CHECK_BACKUP` if set to `1`, it will start the integrity check of all borg backups made by AIO. Note that the backup check is non blocking so containers can be kept running while the check lasts. That means you can't pass `DAILY_BACKUP=1` at the same time. The output of the check can be found in the logs of the container `nextcloud-aio-borgbackup`. + +One example to do a backup would be `sudo docker exec -it --env DAILY_BACKUP=1 nextcloud-aio-mastercontainer /daily-backup.sh`, which you can run via a cronjob or put it in a script. + +Likewise to do a backup check would be `sudo docker exec --env DAILY_BACKUP=0 --env CHECK_BACKUP=1 --env STOP_CONTAINERS=0 nextcloud-aio-mastercontainer /daily-backup.sh`. + +> [!NOTE] +> None of the option returns error codes. So you need to check for the correct result yourself. + +### How to disable the backup section? +If you already have a backup solution in place, you may want to hide the backup section. You can do so by adding `--env AIO_DISABLE_BACKUP_SECTION=true` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`! If it was started already, you will need to stop the mastercontainer, remove it (no data will be lost) and recreate it using the docker run command that you initially used). + +## Addons + +### Fail2ban +You can configure your server to block certain ip-addresses using fail2ban as bruteforce protection. Here is how to set it up: https://docs.nextcloud.com/server/stable/admin_manual/installation/harden_server.html#setup-fail2ban. The logpath of AIO is by default `/var/lib/docker/volumes/nextcloud_aio_nextcloud/_data/data/nextcloud.log`. Do not forget to add `chain=DOCKER-USER` to your nextcloud jail config (`nextcloud.local`) otherwise the nextcloud service running on docker will still be accessible even if the IP is banned. Also, you may change the blocked ports to cover all AIO ports: by default `80,443,8080,8443,3478` (see [this](https://github.com/nextcloud/all-in-one#explanation-of-used-ports)). Apart from that there is now a community container that can be added to the AIO stack: https://github.com/nextcloud/all-in-one/tree/main/community-containers/fail2ban + +### LDAP +It is possible to connect to an existing LDAP server. You need to make sure that the LDAP server is reachable from the Nextcloud container. Then you can enable the LDAP app and configure LDAP in Nextcloud manually. If you don't have a LDAP server yet, recommended is to use this docker container: https://hub.docker.com/r/nitnelave/lldap. Make sure here as well that Nextcloud can talk to the LDAP server. The easiest way is by adding the LDAP docker container to the docker network `nextcloud-aio`. Then you can connect to the LDAP container by its name from the Nextcloud container. There is now a community container which allows to easily add LLDAP to AIO: https://github.com/nextcloud/all-in-one/tree/main/community-containers/lldap + +### Netdata +Netdata allows you to monitor your server using a GUI. You can install it by following https://learn.netdata.cloud/docs/agent/packaging/docker#create-a-new-netdata-agent-container. Apart from that there is now a way for the community to add containers: https://github.com/nextcloud/all-in-one/discussions/392#discussioncomment-7133563 + +### USER_SQL +If you want to use the user_sql app, the easiest way is to create an additional database container and add it to the docker network `nextcloud-aio`. Then the Nextcloud container should be able to talk to the database container using its name. + +### phpMyAdmin, Adminer or pgAdmin +It is possible to install any of these to get a GUI for your AIO database. The pgAdmin container is recommended. You can get some docs on it here: https://www.pgadmin.org/docs/pgadmin4/latest/container_deployment.html. For the container to connect to the aio-database, you need to connect the container to the docker network `nextcloud-aio` and use `nextcloud-aio-database` as database host, `oc_nextcloud` as database username and the password that you get when running `sudo docker exec nextcloud-aio-nextcloud grep dbpassword config/config.php` as the password. Apart from that there is now a way for the community to add containers: https://github.com/nextcloud/all-in-one/discussions/3061#discussioncomment-7307045 **Please note:** If you do not have CLI access to the server, you can now run docker commands via a web session by using this community container: https://github.com/nextcloud/all-in-one/tree/main/community-containers/container-management + +### Mail server +You can configure one yourself by using either of these four recommended projects: [Docker Mailserver](https://github.com/docker-mailserver/docker-mailserver/#docker-mailserver), [Mailu](https://github.com/Mailu/Mailu), [Maddy Mail Server](https://github.com/foxcpp/maddy#maddy-mail-server), [Mailcow](https://github.com/mailcow/mailcow-dockerized#mailcow-dockerized-------) or [Stalwart](https://stalw.art/). There is now a community container which allows to easily add Stalwart Mail server to AIO: https://github.com/nextcloud/all-in-one/tree/main/community-containers/stalwart + +## Miscellaneous + +### Requirements for integrating new containers +For integrating new containers, they must pass specific requirements for being considered to get integrated in AIO itself. Even if not considered, we may add some documentation on it. Also there is this now: https://github.com/nextcloud/all-in-one/tree/main/community-containers#community-containers + +What are the requirements? +1. New containers must be related to Nextcloud. Related means that there must be a feature in Nextcloud that gets added by adding this container. +2. It must be optionally installable. Disabling and enabling the container from the AIO interface must work and must not produce any unexpected side-effects. +3. The feature that gets added into Nextcloud by adding the container must be maintained by the Nextcloud GmbH. +4. It must be possible to run the container without big quirks inside docker containers. Big quirks means e.g. needing to change the capabilities or security options. +5. The container should not mount directories from the host into the container: only docker volumes should be used. +6. The container must be usable by more than 90% of the users (e.g. not too high system requirements and such) +7. No additional setup should be needed after adding the container - it should work completely out of the box. +8. If the container requires being exposed, only subfolders are supported. So the container should not require its own (sub-)domain and must be able to run in a subfolder. + +### Update policy +This project values stability over new features. That means that when a new major Nextcloud update gets introduced, we will wait at least until the first patch release, e.g. `24.0.1` is out before upgrading to it. Also we will wait with the upgrade until all important apps are compatible with the new major version. Minor or patch releases for Nextcloud and all dependencies as well as all containers will be updated to new versions as soon as possible but we try to give all updates first a good test round before pushing them. That means that it can take around 2 weeks before new updates reach the `latest` channel. If you want to help testing, you can switch to the `beta` channel by following [this documentation](#how-to-switch-the-channel) which will also give you the updates earlier. + +### How often are update notifications sent? +AIO ships its own update notifications implementation. It checks if container updates are available. If so, it sends a notification with the title `Container updates available!` on saturdays to Nextcloud users that are part of the `admin` group. If the Nextcloud container image should be older than 90 days (~3 months) and thus badly outdated, AIO sends a notification to all Nextcloud users with the title `AIO is outdated!`. Thus admins should make sure to update the container images at least once every 3 months in order to make sure that the instance gets all security bugfixes as soon as possible. + +### Huge docker logs +If you should run into issues with huge docker logs, you can adjust the log size by following https://docs.docker.com/config/containers/logging/local/#usage. However for the included AIO containers, this should usually not be needed because almost all of them have the log level set to warn so they should not produce many logs. diff --git a/nextcloud-aio/reverse-proxy.md b/nextcloud-aio/reverse-proxy.md new file mode 100644 index 0000000..56e42fe --- /dev/null +++ b/nextcloud-aio/reverse-proxy.md @@ -0,0 +1,1075 @@ +# Reverse Proxy Documentation + +> [!NOTE] +> Please note that AIO comes secured with TLS out-of-the-box. So you don't need to necessarily set up your own reverse proxy if you only want to run Nextcloud AIO which is much easier. See [the normal readme](https://github.com/nextcloud/all-in-one?tab=readme-ov-file#how-to-use-this) in that case. However if port 443 should already be used because you already run a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else), you need to follow this reverse proxy documentation to set up Nextcloud AIO. + +> [!TIP] +> If you don't have a domain yet, [Tailscale is recommended](https://github.com/nextcloud/all-in-one/discussions/6817). If you don't have a reverse proxy yet, [Caddy is recommended](https://github.com/nextcloud/all-in-one/discussions/575). + +## Introduction +In order to run Nextcloud behind a web server or reverse proxy (like Apache, Nginx, Caddy, Cloudflare Tunnel and else), you need to: +1. add a specific config to your web server or reverse proxy. [See the documentation below.](#1-configure-the-reverse-proxy) +2. specify the port that AIO's integrated Apache container shall use via the environmental variable `APACHE_PORT` (that runs inside its own container and published this port on the host) and adjust the `docker run` command of AIO. [See the documentation below.](#2-use-this-startup-command). +3. Open the AIO interface at port `8080` and type in and validate your domain. [See the documentation below.](#4-open-the-aio-interface) + +Here one example with all reverse proxy settings for Linux: +``` +sudo docker run \ +--init \ +--sig-proxy=false \ +--name nextcloud-aio-mastercontainer \ +--restart always \ +--publish 8080:8080 \ +--env APACHE_PORT=11000 \ +--env APACHE_IP_BINDING=0.0.0.0 \ +--env APACHE_ADDITIONAL_NETWORK="" \ +--env SKIP_DOMAIN_VALIDATION=false \ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ +--volume /var/run/docker.sock:/var/run/docker.sock:ro \ +ghcr.io/nextcloud-releases/all-in-one:latest +``` + +
    + +Explanation of the command + +- `sudo docker run` This command spins up a new docker container. Docker commands can optionally be used without `sudo` if the user is added to the docker group (this is not the same as docker rootless, see FAQ in the normal readme). +- `--init` This option makes sure that no zombie-processes are created, ever. See [the Docker documentation](https://docs.docker.com/reference/cli/docker/container/run/#init). +- `--sig-proxy=false` This option allows to exit the container shell that gets attached automatically when using `docker run` by using `[CTRL] + [C]` without shutting down the container. +- `--name nextcloud-aio-mastercontainer` This is the name of the container. This line is not allowed to be changed, since mastercontainer updates would fail. +- `--restart always` This is the "restart policy". `always` means that the container should always get started with the Docker daemon. See the Docker documentation for further detail about restart policies: https://docs.docker.com/config/containers/start-containers-automatically/ +- `--publish 8080:8080` This means that port 8080 of the container should get published on the host using port 8080. This port is used for the AIO interface and uses a self-signed certificate by default. You can also use a different host port if port 8080 is already used on your host, for example `--publish 8081:8080` (only the first port can be changed for the host, the second port is for the container and must remain at 8080). +- `--env APACHE_PORT=11000` This is the port that is published on the host that runs Docker and Nextcloud AIO at which the reverse proxy should point at. +- `--env APACHE_IP_BINDING=0.0.0.0` This can be modified to allow access to the published port on the host only from certain ip-addresses. [See this documentation](#3-limit-the-access-to-the-apache-container) +- `--env APACHE_ADDITIONAL_NETWORK=""` This can be used to put the sibling apache container that is created by AIO into a specified network - useful if your reverse proxy runs as a container on the same host. [See this documentation](#adapting-the-sample-web-server-configurations-below) +- `--env SKIP_DOMAIN_VALIDATION=false` This can be set to `true` if the domain validation does not work and you are sure that you configured everything correctly after you followed [the debug documentation](#7-how-to-debug-things). +- `--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config` This means that the files that are created by the mastercontainer will be stored in a docker volume that is called `nextcloud_aio_mastercontainer`. This line is not allowed to be changed, since built-in backups would fail later on. +- `--volume /var/run/docker.sock:/var/run/docker.sock:ro` The docker socket is mounted into the container which is used for spinning up all the other containers and for further features. It needs to be adjusted on Windows/macOS and on docker rootless. See the applicable documentation on this. If adjusting, don't forget to also set `WATCHTOWER_DOCKER_SOCKET_PATH`! If you dislike this, see https://github.com/nextcloud/all-in-one/tree/main/manual-install. +- `ghcr.io/nextcloud-releases/all-in-one:latest` This is the docker container image that is used. +- Further options can be set using environment variables, for example `--env NEXTCLOUD_DATADIR="/mnt/ncdata"` (This is an example for Linux. See [this](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) for other OS' and for an explanation of what this value does. This specific one needs to be specified upon the first startup if you want to change it to a specific path instead of the default Docker volume). To see explanations and examples for further variables (like changing the location of Nextcloud's datadir or mounting some locations as external storage into the Nextcloud container), read through this readme and look at the docker-compose file: https://github.com/nextcloud/all-in-one/blob/main/compose.yaml + +
    + +> [!Note] +> If you run into troubles, see [the debug section](#7-how-to-debug-things). + +--- + +> [!IMPORTANT] +> If you need HTTPS between Nextcloud and the reverse proxy because it is running on a different server in the same network, simply add another reverse proxy to the chain that runs on the same server like AIO and takes care of HTTPS proxying (most likely via self-signed certificates). Another option would be to create a VPN between the server that runs AIO and the server that runs the reverse proxy which takes care of encrypting the connection. + +> [!NOTE] +> Since the Apache container gets created by the mastercontainer, there is **NO** way to provide custom docker labels or custom environmental variables for the Apache container. So please do not attempt to do this because it will fail! + +## Content + +The process to run Nextcloud behind a reverse proxy consists of at least steps 1, 2 and 4: +1. **Configure the reverse proxy! See [point 1](#1-configure-the-reverse-proxy)** +1. **Use this startup command! See [point 2](#2-use-this-startup-command)** +1. Optional: if the reverse proxy is installed on the same host and in the host network, you should limit the apache container to only listen on localhost. See [point 3](#3-limit-the-access-to-the-apache-container) +1. **Open the AIO interface. See [point 4](#4-open-the-aio-interface)** +1. Optional: if the reverse proxy is outside the host network, configure AIO to trust it. See [point 5](#5-optional-configure-aio-for-reverse-proxies-that-connect-to-nextcloud-using-an-ip-address-and-not-localhost-nor-127001) +1. Optional: get a valid certificate for the AIO interface! See [point 6](#6-optional-get-a-valid-certificate-for-the-aio-interface) +1. Optional: how to debug things? See [point 7](#7-how-to-debug-things) + +## 1. Configure the reverse proxy + +### Adapting the sample web server configurations below +1. Replace `` with the domain on which you want to run Nextcloud. +1. Adjust the port `11000` to match your chosen `APACHE_PORT`. +1. Adjust `localhost` or `127.0.0.1` to point to the Nextcloud server IP or domain depending on where the reverse proxy is running. See the following options. + +
    + + On the same server without a container + + For this setup, the default sample configurations with `localhost:$APACHE_PORT` should work. + +
    + +
    + + On the same server in a Docker container + + The reverse-proxy container needs to be connected to the nextcloud containers. This can be achieved one of these 3 ways: + 1. Utilize host networking instead of docker bridge networking: Specify `--network host` option (or `network_mode: host` for docker-compose) as setting for the reverse proxy container to connect it to the host network. If you are using a firewall on the server, you need to open ports 80 and 443 for the reverse proxy manually. With this setup, the default sample configurations with reverse-proxy pointing to `localhost:$APACHE_PORT` should work directly. + 1. Connect nextcloud's external-facing containers to the reverse-proxy's docker network by specifying env variable APACHE_ADDITIONAL_NETWORK. With this setup, the reverse proxy can utilize Docker bridge network's DNS name resolution to access nextcloud at `http://nextcloud-aio-apache:$APACHE_PORT`. ⚠️⚠️⚠️ Note, the specified network must already exist before Nextcloud AIO is started. Otherwise it will fail to start the container because the network is not existing. + 1. Connect the reverse-proxy container to the `nextcloud-aio` network by specifying it as a secondary (external) network for the reverse proxy container. With this setup also, the reverse proxy can utilize Docker bridge network's DNS name resolution to access nextcloud at `http://nextcloud-aio-apache:$APACHE_PORT` . + +
    + +
    + + On a different server (in container or not) + + Use the private ip-address of the host that shall be running AIO. So e.g. `private.ip.address.of.aio.server:$APACHE_PORT` instead of `localhost:$APACHE_PORT`. + + If you are not sure how to retrieve that, you can run: `ip a | grep "scope global" | head -1 | awk '{print $2}' | sed 's|/.*||'` on the server that shall be running AIO (the commands only work on Linux). + +
    + +### Apache + +
    + +click here to expand + +**Disclaimer:** It might be possible that the config below is not working 100% correctly, yet. Improvements to it are very welcome! + +Add this as a new Apache site config: + +(The config below assumes that you are using certbot to get your certificates. You need to create them first in order to make it work.) + +``` + + ServerName + + RewriteEngine On + RewriteCond %{HTTPS} off + RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} + RewriteCond %{SERVER_NAME} = + RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] + + + + ServerName + + # Reverse proxy based on https://httpd.apache.org/docs/current/mod/mod_proxy_wstunnel.html + RewriteEngine On + ProxyPreserveHost On + RequestHeader set X-Real-IP %{REMOTE_ADDR}s + AllowEncodedSlashes NoDecode + + # Adjust the two lines below to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below + ProxyPass / http://localhost:11000/ nocanon + ProxyPassReverse / http://localhost:11000/ + + RewriteCond %{HTTP:Upgrade} websocket [NC] + RewriteCond %{HTTP:Connection} upgrade [NC] + RewriteCond %{THE_REQUEST} "^[a-zA-Z]+ /(.*) HTTP/\d+(\.\d+)?$" + RewriteRule .? "ws://localhost:11000/%1" [P,L,UnsafeAllow3F] # Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below + + # Enable h2, h2c and http1.1 + Protocols h2 h2c http/1.1 + + # Solves slow upload speeds caused by http2 + H2WindowSize 5242880 + + # TLS + SSLEngine on + SSLProtocol -all +TLSv1.2 +TLSv1.3 + SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 + SSLHonorCipherOrder off + SSLSessionTickets off + + # If running apache on a subdomain (eg. nextcloud.example.com) of a domain that already has an wildcard ssl certificate from certbot on this machine, + # the in the below lines should be replaced with just the domain (eg. example.com), not the subdomain. + # In this case the subdomain should already be secured without additional actions + SSLCertificateFile /etc/letsencrypt/live//fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live//privkey.pem + + # Disable HTTP TRACE method. + TraceEnable off + + Require all denied + + + # Support big file uploads + LimitRequestBody 0 + Timeout 86400 + ProxyTimeout 86400 + +``` + +⚠️ **Please note:** Look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +To make the config work you can run the following command: +`sudo a2enmod rewrite proxy proxy_http proxy_wstunnel ssl headers http2` + +
    + +### Caddy (recommended) + +
    + +click here to expand + +**Hint:** You may have a look at [this guide](https://github.com/nextcloud/all-in-one/discussions/575#discussion-4055615) for a more complete but possibly outdated example. + +Add this to your Caddyfile: + +``` +https://:443 { + reverse_proxy localhost:11000 # Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below +} +``` +The Caddyfile is a text file called `Caddyfile` (no extension) which – if you should be running Caddy inside a container – should usually be created in the same location as your `compose.yaml` file prior to starting the container. + +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +**Advice:** You may have a look at [this](https://github.com/nextcloud/all-in-one/discussions/575#discussion-4055615) for a more complete example. + +
    + +### Caddy with ACME DNS-challenge + +
    + +click here to expand + +You can get AIO running using the ACME DNS-challenge. Here is how to do it. + +1. Follow [this documentation](https://caddy.community/t/how-to-use-dns-provider-modules-in-caddy-2/8148) in order to get a Caddy build that is compatible with your domain provider's DNS challenge. +1. Add this to your Caddyfile: + ``` + https://:443 { + reverse_proxy localhost:11000 # Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below + tls { + dns + } + } + ``` + ⚠️ **Please note:** Look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + + You also need to adjust `` and `` to match your case. + +1. Now continue with [point 2](#2-use-this-startup-command) but additionally, add `--env SKIP_DOMAIN_VALIDATION=true` to the docker run command of the mastercontainer (but before the last line `ghcr.io/nextcloud-releases/all-in-one:latest`) which will disable the domain validation (because it is known that the domain validation will not work when using the DNS-challenge since no port is publicly opened). + +**Advice:** In order to make it work in your home network, you may add the internal ipv4-address of your reverse proxy as A DNS-record to your domain and disable the dns-rebind-protection in your router. Another way it to set up a local dns-server like a pi-hole and set up a custom dns-record for that domain that points to the internal ip-adddress of your reverse proxy (see https://github.com/nextcloud/all-in-one#how-can-i-access-nextcloud-locally). If both is not possible, you may add the domain to the hosts file which is needed then for any devices that shall use the server. + +
    + +### OpenLiteSpeed + +
    + +click here to expand + +You can find the OpenLiteSpeed reverse proxy guide by @MorrowShore here: https://github.com/nextcloud/all-in-one/discussions/6370 + +
    + +### Citrix ADC VPX / Citrix Netscaler + +
    + +click here to expand + +For a reverse proxy example guide for Citrix ADC VPX / Citrix Netscaler, see this guide by @esmith443: https://github.com/nextcloud/all-in-one/discussions/2452 + +
    + +### Cloudflare Tunnel + +
    + +click here to expand + + +**Hint:** You may have a look at [this guide](https://github.com/nextcloud/all-in-one/discussions/2845#discussioncomment-6423237) for a more complete but possibly outdated example. + +Although it does not seem like it is the case but from AIO perspective a Cloudflare Tunnel works like a reverse proxy. Please see the [caveats](https://github.com/nextcloud/all-in-one#notes-on-cloudflare-proxytunnel) before proceeding. Here is then how to make it work: + +1. Install the Cloudflare Tunnel on the same machine where AIO will be running on and point the Tunnel with the domain that you want to use for AIO to `http://localhost:11000`.
    +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. +1. Now continue with [point 2](#2-use-this-startup-command) but add `--env SKIP_DOMAIN_VALIDATION=true` to the docker run command - which will disable the domain validation (because it is known that the domain validation will not work behind a Cloudflare Tunnel). + +**Advice:** Make sure to [disable Cloudflare's Rocket Loader feature](https://help.nextcloud.com/t/login-page-not-working-solved/149417/8) as otherwise Nextcloud's login prompt will not be shown. + +
    + +### HaProxy + +
    + +click here to expand + +**Disclaimer:** It might be possible that the config below is not working 100% correctly, yet. Improvements to it are very welcome! + +Here is an example HaProxy config: + +``` +global + chroot /var/haproxy + log /var/run/log audit debug + lua-prepend-path /tmp/haproxy/lua/?.lua + ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 + ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets + + ssl-default-server-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305 + ssl-default-server-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256 + ssl-default-server-options ssl-min-ver TLSv1.2 no-tls-tickets + +defaults + log global + option redispatch -1 + retries 3 + default-server init-addr last,libc + +# Frontend: LetsEncrypt_443 () +frontend LetsEncrypt_443 + bind 0.0.0.0:443 name 0.0.0.0:443 ssl crt-list /tmp/haproxy/ssl/605f6609f106d1.17683543.certlist + mode http + option http-keep-alive + default_backend acme_challenge_backend + option forwardfor + # tuning options + timeout client 30s + + # logging options + # ACL: find_acme_challenge + acl acl_605f6d4b6453d2.03059920 path_beg -i /.well-known/acme-challenge/ + # ACL: Nextcloud + acl acl_60604e669c3ca4.13013327 hdr(host) -i + + # ACTION: redirect_acme_challenges + use_backend acme_challenge_backend if acl_605f6d4b6453d2.03059920 + # ACTION: Nextcloud + use_backend Nextcloud if acl_60604e669c3ca4.13013327 + + +# Frontend: LetsEncrypt_80 () +frontend LetsEncrypt_80 + bind 0.0.0.0:80 name 0.0.0.0:80 + mode tcp + default_backend acme_challenge_backend + # tuning options + timeout client 30s + + # logging options + # ACL: find_acme_challenge + acl acl_605f6d4b6453d2.03059920 path_beg -i /.well-known/acme-challenge/ + + # ACTION: redirect_acme_challenges + use_backend acme_challenge_backend if acl_605f6d4b6453d2.03059920 + +# Frontend (DISABLED): 1_HTTP_frontend () + +# Frontend (DISABLED): 1_HTTPS_frontend () + +# Frontend (DISABLED): 0_SNI_frontend () + +# Backend: acme_challenge_backend (Added by Let's Encrypt plugin) +backend acme_challenge_backend + # health checking is DISABLED + mode http + balance source + # stickiness + stick-table type ip size 50k expire 30m + stick on src + # tuning options + timeout connect 30s + timeout server 30s + http-reuse safe + server acme_challenge_host 127.0.0.1:43580 + +# Backend: Nextcloud () +backend Nextcloud + mode http + balance source + server Nextcloud localhost:11000 # Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below +``` + +⚠️ **Please note:** Look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### Nginx, Freenginx, Openresty, Angie + +
    + +click here to expand + +**Hint:** You may have a look at [this guide](https://github.com/nextcloud/all-in-one/discussions/588#discussioncomment-2811152) for a more complete but possibly outdated example. + +**Disclaimer:** This config was tested and should normally work on all modern Nginx versions. Improvements to the config are very welcome! + +Add the below template to your Nginx config. + +**Note:** please check your Nginx version by running: `nginx -v` and adjust the lines marked with version notes to fit your version. + +``` +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + listen [::]:80; # comment to disable IPv6 + + if ($scheme = "http") { + return 301 https://$host$request_uri; + } + if ($http_x_forwarded_proto = "http") { + return 301 https://$host$request_uri; + } + + listen 443 ssl http2; # for nginx versions below v1.25.1 + listen [::]:443 ssl http2; # for nginx versions below v1.25.1 - comment to disable IPv6 + + # listen 443 ssl; # for nginx v1.25.1+ + # listen [::]:443 ssl; # for nginx v1.25.1+ - keep comment to disable IPv6 + # http2 on; # uncomment to enable HTTP/2 - supported on nginx v1.25.1+ + + # listen 443 quic reuseport; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ - please remove "reuseport" if there is already another quic listener on port 443 with enabled reuseport + # listen [::]:443 quic reuseport; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ - please remove "reuseport" if there is already another quic listener on port 443 with enabled reuseport - keep comment to disable IPv6 + # http3 on; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ + # quic_gso on; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ + # quic_retry on; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ + # quic_bpf on; # improves HTTP/3 / QUIC - supported on nginx v1.25.0+, if nginx runs as a docker container you need to give it privileged permission to use this option + # add_header Alt-Svc 'h3=":443"; ma=86400'; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ + + proxy_buffering off; + proxy_request_buffering off; + + client_max_body_size 0; + client_body_buffer_size 512k; + # http3_stream_buffer_size 512k; # uncomment to enable HTTP/3 / QUIC - supported on nginx v1.25.0+ + proxy_read_timeout 86400s; + + server_name ; + + location / { + proxy_pass http://127.0.0.1:11000$request_uri; # Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Scheme $scheme; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $host; + proxy_set_header Early-Data $ssl_early_data; + + # Websocket + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + # If running nginx on a subdomain (eg. nextcloud.example.com) of a domain that already has an wildcard ssl certificate from certbot on this machine, + # the in the below lines should be replaced with just the domain (eg. example.com), not the subdomain. + # In this case the subdomain should already be secured without additional actions + ssl_certificate /etc/letsencrypt/live//fullchain.pem; # managed by certbot on host machine + ssl_certificate_key /etc/letsencrypt/live//privkey.pem; # managed by certbot on host machine + + ssl_dhparam /etc/dhparam; # curl -L https://ssl-config.mozilla.org/ffdhe2048.txt -o /etc/dhparam + + ssl_early_data on; + ssl_session_timeout 1d; + ssl_session_cache shared:SSL:10m; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ecdh_curve x25519:x448:secp521r1:secp384r1:secp256r1; + + ssl_prefer_server_ciphers on; + ssl_conf_command Options PrioritizeChaCha; + ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256; +} + +``` + +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### NPMplus (Fork of Nginx-Proxy-Manager - NPM) + +
    + +click here to expand + +⚠️ **Please note:** This is not needed when running NPMplus as a community container. + +First, make sure the environmental variables `PUID` and `PGID` in the `compose.yaml` file for NPM are either unset or set to `0`.
    +If you need to change the GID/PID then please add `net.ipv4.ip_unprivileged_port_start=0` at the end of `/etc/sysctl.conf`.
    +Note: this will cause that a non root user can bind privileged ports. + +Second, see these screenshots for a working config: + +![grafik](https://github.com/user-attachments/assets/c32c8fe8-7417-4f8f-9625-24b95651e630) + +![grafik](https://github.com/user-attachments/assets/f14bba5c-69ce-4514-a2ac-5e5d7fb97792) + + + +![grafik](https://github.com/user-attachments/assets/75d7f539-35d1-4a3e-8c51-43123f698893) + +![grafik](https://github.com/user-attachments/assets/e494edb5-8b70-4d45-bc9b-374219230041) + +`proxy_set_header Accept-Encoding $http_accept_encoding;` + +⚠️ **Please note:** Nextcloud will complain that X-XXS-Protection is set to the wrong value, this is intended by NPMplus.
    +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### Nginx-Proxy-Manager - NPM + +
    + +click here to expand + +**Hint:** You may have a look at [this guide](https://github.com/nextcloud/all-in-one/discussions/588#discussioncomment-3040493) for a more complete but possibly oudated example. + +First, make sure the environmental variables `PUID` and `PGID` in the `compose.yaml` file for NPM are either unset or set to `0`.
    +If you need to change the GID/PID then please add `net.ipv4.ip_unprivileged_port_start=0` at the end of `/etc/sysctl.conf`.
    +Note: this will cause that a non root user can bind privileged ports. + +Second, see these screenshots for a working config: + +![grafik](https://user-images.githubusercontent.com/75573284/213889707-b7841ca0-3ea7-4321-acf6-50e1c1649442.png) + +![grafik](https://user-images.githubusercontent.com/75573284/213889724-1ab32264-3e0c-4d83-b067-9fe9d1672fb2.png) + +![grafik](https://github.com/nextcloud/all-in-one/assets/24786786/fecbb5ef-d2f4-4e0f-bc4b-82207e2c2809) + +![grafik](https://user-images.githubusercontent.com/75573284/213889746-87dbe8c5-4d1f-492f-b251-bbf82f1510d0.png) + +``` +client_body_buffer_size 512k; +proxy_read_timeout 86400s; +client_max_body_size 0; +``` + +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. +Also change `@` to a mail address of yours. + +
    + +### Nginx-Proxy + +
    + +click here to expand + +Unfortunately, it is not possible to configure Nginx-proxy in a way that works because it completely relies on environmental variables of the docker containers itself. Providing these variables does not work as stated above. + +If you really want to use AIO, we recommend you to switch to caddy. It is simply amazing!
    + +Apart from that, there is a [manual-install](https://github.com/nextcloud/all-in-one/tree/main/manual-install). + +
    + +### Node.js with Express + +
    + +click here to expand + +**Disclaimer:** it might be possible that the config below is not working 100% correctly, yet. Improvements to it are very welcome! + +For Node.js, we will use the npm package `http-proxy`. WebSockets must be handled separately. + +This example only uses `http`, but if your Express server already uses a `https` server, then follow the same instructions for `https`. + +```js +const HttpProxy = require('http-proxy'); +const express = require('express'); +const http = require('http'); + +const app = express(); +const proxy = HttpProxy.createProxyServer({ + target: 'http://localhost:11000', // Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below + // Timeout can be changed to your liking. + timeout: 1000 * 60 * 3, + proxyTimeout: 1000 * 60 * 3, + // Not 100% certain whether autoRewrite is necessary, but enabling it SEEMS to make it behave more stably. + autoRewrite: true, + // Do not enable followRedirects. + followRedirects: false, +}); + +// Handle errors with proxy.web and proxy.ws +function onProxyError(err, req, res, target) { + // Handle errors however you like. Here's an example: + if (err.code === 'ECONNREFUSED') { + return res.status(503).send('Nextcloud server is currently not running. It may be down for temporary maintenance.'); + } + // other errors + else { + console.error(err); + return res.status(500).send(String(err)); + } +} + +app.use((req, res) => { + proxy.web(req, res, {}, onProxyError); +}); + +const httpServer = http.createServer(app); +httpServer.listen('80'); + +// Listen for an upgrade to a WebSocket connection. +httpServer.on('upgrade', (req, socket, head) => { + proxy.ws(req, socket, head, {}, onProxyError); +}); +``` + +If you are using the Express package `vhost` for your app, you can use `proxy.web` inside the vhosted express function (see the following code snippet), but `proxy.ws` still needs to be done "globally" on your http server. Nextcloud should automatically ignore websocket requests for other domains. + +```js +const HttpProxy = require('http-proxy'); +const express = require('express'); +const http = require('http'); + +const myNextcloudApp = express(); +const myOtherApp = express(); +const vhost = express(); + +// Definitions for proxy and onProxyError unchanged. (see above) + +myNextcloudApp.use((req, res) => { + proxy.web(req, res, {}, onProxyError); +}); + +vhost.use(vhostFunc('', myNextcloudApp)); + +const httpServer = http.createServer(app); +httpServer.listen('80'); + +// Listen for an upgrade to a WebSocket connection. +httpServer.on('upgrade', (req, socket, head) => { + proxy.ws(req, socket, head, {}, onProxyError); +}); +``` + +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### Synology Reverse Proxy + +
    + +click here to expand + +**Disclaimer:** it might be possible that the config below is not working 100% correctly, yet. Improvements to it are very welcome! + +See these screenshots for a working config: + +![image](https://user-images.githubusercontent.com/89748315/192525606-48cab54b-866e-4964-90a8-15e71bd362fb.png) + +![image](https://user-images.githubusercontent.com/70434961/213193789-fa936edc-e307-4e6a-9a53-ae26d1bf2f42.jpg) + +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### Traefik 2 + +
    + +click here to expand + +**Hint:** You may have a look at [this video](https://www.youtube.com/watch?v=VLPSRrLMDmA) for a more complete but possibly outdated example. + +**Disclaimer:** it might be possible that the config below is not working 100% correctly, yet. Improvements to it are very welcome! + +Traefik's building blocks (router, service, middlewares) need to be defined using dynamic configuration similar to [this](https://doc.traefik.io/traefik/providers/file/#configuration-examples) official Traefik configuration example. Using **docker labels _won't work_** because of the nature of the project. + +The examples below define the dynamic configuration in YAML files. If you rather prefer TOML, use a YAML to TOML converter. + +1. In Traefik's static configuration define a [file provider](https://doc.traefik.io/traefik/providers/file/) for dynamic providers: + + ```yml + # STATIC CONFIGURATION + + entryPoints: + https: + address: ":443" # Create an entrypoint called "https" that uses port 443 + transport: + respondingTimeouts: + readTimeout: 24h # Allows uploads > 100MB; prevents connection reset due to chunking (public upload-only links) + # If you want to enable HTTP/3 support, uncomment the line below + # http3: {} + + certificatesResolvers: + # Define "letsencrypt" certificate resolver + letsencrypt: + acme: + storage: /letsencrypt/acme.json # Defines the path where certificates should be stored + email: # Where LE sends notification about certificates expiring + tlschallenge: true + + providers: + file: + directory: "/path/to/dynamic/conf" # Adjust the path according your needs. + watch: true + + # Enable HTTP/3 feature by uncommenting the lines below. Don't forget to route 443 UDP to Traefik (Firewall\NAT\Traefik Container) + # experimental: + # http3: true + ``` + +1. Declare the router, service and middlewares for Nextcloud in `/path/to/dynamic/conf/nextcloud.yml`: + + ```yml + http: + routers: + nextcloud: + rule: "Host(``)" + entrypoints: + - "https" + service: nextcloud + middlewares: + - nextcloud-chain + tls: + certresolver: "letsencrypt" + + services: + nextcloud: + loadBalancer: + servers: + - url: "http://localhost:11000" # Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below + + middlewares: + nextcloud-secure-headers: + headers: + hostsProxyHeaders: + - "X-Forwarded-Host" + referrerPolicy: "same-origin" + + https-redirect: + redirectscheme: + scheme: https + + nextcloud-chain: + chain: + middlewares: + # - ... (e.g. rate limiting middleware) + - https-redirect + - nextcloud-secure-headers + ``` + +--- + +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### Traefik 3 + +
    + +click here to expand + +**Disclaimer:** it might be possible that the config below is not working 100% correctly, yet. Improvements to it are very welcome! + +Traefik's building blocks (router, service, middlewares) need to be defined using dynamic configuration similar to [this](https://doc.traefik.io/traefik/providers/file/#configuration-examples) official Traefik configuration example. Using **docker labels _won't work_** because of the nature of the project. + +The examples below define the dynamic configuration in YAML files. If you rather prefer TOML, use a YAML to TOML converter. + +1. In Traefik's static configuration define a [file provider](https://doc.traefik.io/traefik/providers/file/) for dynamic providers: + + ```yml + # STATIC CONFIGURATION + + entryPoints: + https: + address: ":443" # Create an entrypoint called "https" that uses port 443 + transport: + respondingTimeouts: + readTimeout: 24h # Allows uploads > 100MB; prevents connection reset due to chunking (public upload-only links) + # If you want to enable HTTP/3 support, uncomment the line below + # http3: {} + + certificatesResolvers: + # Define "letsencrypt" certificate resolver + letsencrypt: + acme: + storage: /letsencrypt/acme.json # Defines the path where certificates should be stored + email: # Where LE sends notification about certificates expiring + tlschallenge: true + + providers: + file: + directory: "/path/to/dynamic/conf" # Adjust the path according your needs. + watch: true + ``` + +2. Declare the router, service and middlewares for Nextcloud in `/path/to/dynamic/conf/nextcloud.yml`: + + ```yml + http: + routers: + nextcloud: + rule: "Host(``)" + entrypoints: + - "https" + service: nextcloud + middlewares: + - nextcloud-chain + tls: + certresolver: "letsencrypt" + + services: + nextcloud: + loadBalancer: + servers: + - url: "http://localhost:11000" # Adjust to match APACHE_PORT and APACHE_IP_BINDING. See https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#adapting-the-sample-web-server-configurations-below + + middlewares: + nextcloud-secure-headers: + headers: + hostsProxyHeaders: + - "X-Forwarded-Host" + referrerPolicy: "same-origin" + + https-redirect: + redirectscheme: + scheme: https + + nextcloud-chain: + chain: + middlewares: + # - ... (e.g. rate limiting middleware) + - https-redirect + - nextcloud-secure-headers + ``` + +--- + +⚠️ **Please note:** look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### IIS with ARR and URL Rewrite + +
    + +click here to expand + +**Disclaimer:** It might be possible that the config below is not working 100% correctly, yet. Improvements to it are very welcome! + +**Please note:** Using IIS as a reverse proxy comes with some limitations: +- A maximum upload size of 4GiB, in the example configuration below the limit is set to 2GiB. + + +#### Prerequisites +1. **Windows Server** with IIS installed. +2. [**Application Request Routing (ARR)**](https://www.iis.net/downloads/microsoft/application-request-routing) and [**URL Rewrite**](https://www.iis.net/downloads/microsoft/url-rewrite) modules installed. +3. [**WebSocket Protocol**](https://learn.microsoft.com/en-us/iis/configuration/system.webserver/websocket) feature enabled. + +For information on how to set up IIS as a reverse proxy please refer to [this](https://learn.microsoft.com/en-us/iis/extensions/url-rewrite-module/reverse-proxy-with-url-rewrite-v2-and-application-request-routing). +There are also information on how to use the IIS Manager [here](https://learn.microsoft.com/en-us/iis/). + +The following configuration example assumes the following: +* A site has been created that is configured with HTTPS support and the desired host name. +* A server farm named `nc-server-farm` has been created and is pointing to the Nextcloud server. +* No global Rewrite Rules has been created for the `nc-server-farm` server farm. + +Add the following `web.config` file to the root of the site you created as the reverse proxy. +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` +⚠️ **Please note:** Look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +
    + +### Tailscale + +
    + +click here to expand + +For a reverse proxy example guide for Tailscale, see this guide by [@Perseus333](https://github.com/Perseus333): https://github.com/nextcloud/all-in-one/discussions/6817 + +
    + + +### Others + +
    + +click here to expand + +Config examples for other reverse proxies are currently not documented. Pull requests are welcome! + +
    + +## 2. Use this startup command + +After adjusting your reverse proxy config, use the following command to start AIO:
    + +(For a `compose.yaml` example, see the example further [below](#inspiration-for-a-docker-compose-file).) + +``` +# For Linux: +sudo docker run \ +--init \ +--sig-proxy=false \ +--name nextcloud-aio-mastercontainer \ +--restart always \ +--publish 8080:8080 \ +--env APACHE_PORT=11000 \ +--env APACHE_IP_BINDING=0.0.0.0 \ +--env APACHE_ADDITIONAL_NETWORK="" \ +--env SKIP_DOMAIN_VALIDATION=false \ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config \ +--volume /var/run/docker.sock:/var/run/docker.sock:ro \ +ghcr.io/nextcloud-releases/all-in-one:latest +``` + +Note: you may be interested in adjusting Nextcloud’s datadir to store the files in a different location than the default docker volume. See [this documentation](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) on how to do it. + +You should also think about limiting the Apache container to listen only on localhost in case the reverse proxy is running on the same host and in the host network, by providing an additional environmental variable to this docker run command. See [point 3](#3-limit-the-access-to-the-apache-container). + +On macOS see https://github.com/nextcloud/all-in-one#how-to-run-aio-on-macos. + +
    + +Command for Windows + +On Windows, install [Docker Desktop](https://www.docker.com/products/docker-desktop/) (and don't forget to [enable ipv6](https://github.com/nextcloud/all-in-one/blob/main/docker-ipv6-support.md) if you should need that) and run the following command in the command prompt: + +``` +docker run ^ +--init ^ +--sig-proxy=false ^ +--name nextcloud-aio-mastercontainer ^ +--restart always ^ +--publish 8080:8080 ^ +--env APACHE_PORT=11000 ^ +--env APACHE_IP_BINDING=0.0.0.0 ^ +--env APACHE_ADDITIONAL_NETWORK="" ^ +--env SKIP_DOMAIN_VALIDATION=false ^ +--volume nextcloud_aio_mastercontainer:/mnt/docker-aio-config ^ +--volume //var/run/docker.sock:/var/run/docker.sock:ro ^ +ghcr.io/nextcloud-releases/all-in-one:latest +``` + +Also, you may be interested in adjusting Nextcloud's Datadir to store the files on the host system. See [this documentation](https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir) on how to do it. + +
    + +On Synology DSM see https://github.com/nextcloud/all-in-one#how-to-run-aio-on-synology-dsm + +### Inspiration for a docker-compose file + +Simply translate the docker run command into a docker-compose file. You can have a look at [this file](https://github.com/nextcloud/all-in-one/blob/main/compose.yaml) for some inspiration but you will need to modify it either way. You can find further examples here: https://github.com/nextcloud/all-in-one/discussions/588 + +## 3. Limit the access to the Apache container + +Use this environment variable during the initial startup of the mastercontainer to make the apache container only listen on localhost: `--env APACHE_IP_BINDING=127.0.0.1`. **Attention:** This is only recommended to be set if you use `localhost` in your reverse proxy config to connect to your AIO instance. If you use an ip-address instead of localhost, you should set it to `0.0.0.0`. + +## 4. Open the AIO interface + +After starting AIO, you should be able to access the AIO Interface via `https://ip.address.of.the.host:8080` and type in and validate the domain that you have configured.
    +⚠️ **Important:** do always use an ip-address if you access this port and not a domain as HSTS might block access to it later! (It is also expected that this port uses a self-signed certificate due to security concerns which you need to accept in your browser)
    +Enter your domain in the AIO interface that you've used in the reverse proxy config and you should be done. Please do not forget to open/forward port `3478/TCP` and `3478/UDP` in your firewall/router for the Talk container! + +## 5. Optional: Configure AIO for reverse proxies that connect to nextcloud using an ip-address and not localhost nor 127.0.0.1 +If your reverse proxy connects to nextcloud using an ip-address and not localhost or 127.0.0.1* you must make the following configuration changes + +*: The IP address it uses to connect to AIO is not in a private IP range such as these: `127.0.0.1/8,192.168.0.0/16,172.16.0.0/12,10.0.0.0/8,fd00::/8,::1` + +### Nextcloud trusted proxies +Add the IP it uses connect to AIO to the Nextcloud trusted_proxies like this: + +``` +sudo docker exec --user www-data -it nextcloud-aio-nextcloud php occ config:system:set trusted_proxies 2 --value=ip.address.of.proxy +``` + +### Collabora WOPI allow list +If your reverse proxy connects to Nextcloud with an IP address that is different from the one for your domain* and you are using the Collabora server then you must also add the IP to the WOPI request allow list via `Administration Settings > Administration > Office > Allow list for WOPI requests`. + +*: For example, the reverse proxy has a public globally routable IP and connects to your AIO instance via Tailscale with an IP in the `100.64.0.0/10` range, or you are using a Cloudflare tunnel ([cloudflare notes](https://github.com/nextcloud/all-in-one?tab=readme-ov-file#notes-on-cloudflare-proxytunnel): You must add all [Cloudflare IP-Ranges](https://www.cloudflare.com/ips/) to the WOPI allowlist.) + +### External reverse proxies connecting via VPN (e.g. Tailscale) + +If your reverse proxy is outside your LAN and connecting via VPN such as Tailscale, you may want to set `APACHE_IP_BINDING=AIO.VPN.host.IP` to ensure only traffic coming from the VPN can connect. + +## 6. Optional: get a valid certificate for the AIO interface + +If you want to also access your AIO interface publicly with a valid certificate, you can add e.g. the following config to your Caddyfile: + +``` +https://:8443 { + reverse_proxy https://localhost:8080 { + transport http { + tls_insecure_skip_verify + } + } +} +``` +⚠️ **Please note:** Look into [this](#adapting-the-sample-web-server-configurations-below) to adapt the above example configuration. + +Afterwards should the AIO interface be accessible via `https://ip.address.of.the.host:8443`. You can alternatively change the domain to a different subdomain by using `https://:443` instead of `https://:8443` in the Caddyfile and use that to access the AIO interface. + +## 7. How to debug things? + + + +If something does not work, follow the steps below: +1. Make sure to exactly follow the whole reverse proxy documentation step-for-step from top to bottom! +1. Make sure that you used the `docker run` command that is described in this reverse proxy documentation. **Hint:** make sure that you have set the `APACHE_PORT` via e.g. `--env APACHE_PORT=11000` during the docker run command! +1. Make sure to set the `APACHE_IP_BINDING` variable correctly. If in doubt, set it to `--env APACHE_IP_BINDING=0.0.0.0` +1. Make sure that all ports to which your reverse proxy is pointing match the chosen `APACHE_PORT`. +1. Make sure to follow [this](#adapting-the-sample-web-server-configurations-below) to adapt the example configurations to your specific setup! +1. Make sure that the mastercontainer is able to spawn other containers. You can do so by checking that the mastercontainer indeed has access to the Docker socket which might not be positioned in one of the suggested directories like `/var/run/docker.sock` but in a different directory, based on your OS and the way how you installed Docker. The mastercontainer logs should help figuring this out. You can have a look at them by running `sudo docker logs nextcloud-aio-mastercontainer` after the container is started the first time. +1. Check if after the mastercontainer was started, the reverse proxy if running inside a container, can reach the provided apache port. You can test this by running `nc -z localhost 11000; echo $?` from inside the reverse proxy container. If the output is `0`, everything works. Alternatively you can of course use instead of `localhost` the ip-address of the host here for the test. +1. Make sure that you are not behind CGNAT. If that is the case, you will not be able to open ports properly. In that case you might use a Cloudflare Tunnel! +1. If you use Cloudflare, you might need to skip the domain validation anyways since it is known that Cloudflare might block the validation attempts. In that case, see the last option below! +1. If your reverse proxy is configured to use the host network (as recommended in the above docs) or running on the host, make sure that you've configured your firewall to open port 443 (and 80)! +1. Check if you have a public IPv4- and public IPv6-address. If you only have a public IPv6-address (e.g. due to DS-Lite), make sure to enable IPv6 in Docker and your whole networking infrastructure (e.g. also by adding an AAAA DNS-entry to your domain)! +1. [Enable Hairpin NAT in your router](https://github.com/nextcloud/all-in-one/discussions/5849) or [set up a local DNS server and add a custom dns-record](https://github.com/nextcloud/all-in-one#how-can-i-access-nextcloud-locally) that allows the server to reach itself locally +1. Try to configure everything from scratch - if it still does not work by following https://github.com/nextcloud/all-in-one#how-to-properly-reset-the-instance. +1. As last resort, you may disable the domain validation by adding `--env SKIP_DOMAIN_VALIDATION=true` to the docker run command. But only use this if you are completely sure that you've correctly configured everything! + +## 8. Removing the reverse proxy +If you, at some point, want to remove the reverse proxy, here are some general steps: +1. Stop all running containers in the AIO Interface. +2. Stop and remove the mastercontainer. + ``` + sudo docker stop nextcloud-aio-mastercontainer + sudo docker rm nextcloud-aio-mastercontainer + ``` +3. Remove the software and configuration file that you used for the reverse proxy (see section 1). +4. Restart the mastercontainer with the [docker run command from the main readme](https://github.com/nextcloud/all-in-one?tab=readme-ov-file#how-to-use-this) but add the two options: + ``` + --env APACHE_IP_BINDING=0.0.0.0 \ + --env APACHE_PORT=443 \ + ``` + Do this *before* the last line of the run command! + + *The first command ensures that the Apache container is listening on all available network interfaces and the second command configures it to listen to port 443.* +5. Restart all other containers in the AIO interface. diff --git a/nextcloud-aio/tests/QA/001-initial-setup.md b/nextcloud-aio/tests/QA/001-initial-setup.md new file mode 100644 index 0000000..0595673 --- /dev/null +++ b/nextcloud-aio/tests/QA/001-initial-setup.md @@ -0,0 +1,10 @@ +# Initial setup + +- [ ] Verify that after starting the test container, you can access the AIO interface using https://internal.ip.address:8080 +- [ ] After clicking the self-signed-certificate warning away, it should show the setup page with an explanation what AIO is and the initial password and a button that contains a link to the AIO login page +- [ ] After copying the password and clicking on this button, it should open a new tab with the login page +- [ ] The login page should show an input field that allows to enter the AIO password and a `Log in` button +- [ ] After pasting the new password into the input field and clicking on this button button, you should be logged in +- [ ] You should now see the containers page and you should see three sections: one general section which explains what AIO is, one `New AIO instance` section and one section that allows to restore the whole AIO instance from backup. + +You can now continue with [002-new-instance.md](./002-new-instance.md) or [010-restore-instance.md](./010-restore-instance.md). diff --git a/nextcloud-aio/tests/QA/002-new-instance.md b/nextcloud-aio/tests/QA/002-new-instance.md new file mode 100644 index 0000000..f429869 --- /dev/null +++ b/nextcloud-aio/tests/QA/002-new-instance.md @@ -0,0 +1,31 @@ +# New instance + +For the below to work, it is important that you have a domain that you point onto your testserver and open port 443 in your router/firewall. + +- [ ] The `New AIO instance` section should show an input field that allows to enter a domain that will be used for Nextcloud later on as well as a short explanation regarding dynamic DNS +- [ ] Now test a few examples in the input box: + - [ ] Entering `djfslkklk` should report that DNS config is not set or the domain is not in a valid format + - [ ] Entering `https://sdjflkjk.cpm` should report that this is not a valid domain + - [ ] Entering `10.0.0.1` should report that ip-addresses are not supported + - [ ] Entering `nextcloud.com` should report that the domain does not point to this server + - [ ] Entering the domain that does point to your server e.g. `yourdomain.com` should finally redirect you to the next screen (if you did not configure your domain yet or did not open port 443, it should report that to you) +- [ ] Now you should see a button `Start containers` and an explanation which points out that clicking on the button will start the containers and that this can take a long time. +- [ ] Below that you should see a section `Optional addons` which shows a checkbox list with addons that can be enabled or disabled. + - [ ] Collabora and Nextcloud Talk should be enabled, the rest disabled + - [ ] Unchecking/Checking any of these should insert a button that allows to save the set config + - [ ] Checking OnlyOffice and Collabora at the same time should show a warning that this is not supported and should not saving the new config + - [ ] Recommended is to uncheck all options now + - [ ] Clicking on the save button should reload the page and activate the new config +- [ ] Clickig on the `Start containers` button should finally reveal a big spinning wheel that should block all elements on the side of being clicked. +- [ ] After waiting a few minutes, it should reload and show a new page + - [ ] On top of the page should be shown which channel you are running + - [ ] Below that, it should show that containers are currently starting + - [ ] Below that it should show a section with Containers: Apache, Database, Nextcloud and Redis and that your containers are up-to-date + - [ ] On the bottom should be the Optional addons section shown but with disabled checkboxes (not clickable) + - [ ] A automatic reload every 5s should happen until all Containers are started (as long as this window is focused) +- [ ] After waiting a bit longer it should instead of the advice that your containers are currently running show the initial Nextcloud credentials (username, password) and below that a button that allows to open the Nextcloud interface in a new tab +- [ ] Clicking on that button should open the Nextcloud interface in a new tab and you should be able to log in using the provided credentials +- [ ] Below the Containers section it should show a `Stop containers` button +- [ ] Below the Containers section and above the Optional Addons section, you should see a Backup and restore section and an AIO password change section + +You can now continue with [003-automatic-login.md](./003-automatic-login.md). \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/003-automatic-login.md b/nextcloud-aio/tests/QA/003-automatic-login.md new file mode 100644 index 0000000..7817e9e --- /dev/null +++ b/nextcloud-aio/tests/QA/003-automatic-login.md @@ -0,0 +1,8 @@ +# Automatic login + +- [ ] After you log in to Nextcloud using the provided initial credentials, open https://yourdomain.com/settings/admin/overview +- [ ] There you should see a Nextcloud AIO section and a button that allows to log into the AIO interface. +- [ ] Clicking on this button should open the AIO interface in a new tab and should automatically log you in +- [ ] All sessions in other tabs that are currently open should be closed (you can verify by reloading all other AIO tabs) + +You can now continue with [004-initial-backup.md](./004-initial-backup.md). \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/004-initial-backup.md b/nextcloud-aio/tests/QA/004-initial-backup.md new file mode 100644 index 0000000..75c6ac0 --- /dev/null +++ b/nextcloud-aio/tests/QA/004-initial-backup.md @@ -0,0 +1,18 @@ +# Initial backup + +- [ ] In the Backup and restore section, you should now see and input box where you should type in the path where the backup should get created and some explanation below + - [ ] Enter `/` which should send an error + - [ ] Enter `/mnt/` or `/media/` or `/host_mnt/` or `/var/backups/` should send an error as well + - [ ] Accepted should be `/mnt/backup`, `/media/backup`, `/host_mnt/c/backup` and `/var/backups`. + - [ ] The side should now reload +- [ ] The initial Nextcloud credentials on top of the page that are visible when the containers are running should now be hidden in a details tag +- [ ] In the Backup restore section you should now see a Backup information section with important info like the encryption password, the backup location and more. +- [ ] Also you should see a Backup cretion section that contains a `Create backup` button. +- [ ] Clicking on the `Create backup` button should open a window prompt that allows to cancel the operation. +- [ ] Canceling should return to the website, confirming should reveal the big spinner again which should block the website again. +- [ ] After a while you should see the information that Backup container is currently running +- [ ] Below the Containers section you should see the option to `Start containers` again. +- [ ] After a while and a few automatic reloads (as long as the side is focused), you should be redirected to the usual page and seen in the Backup and restore section that the last backup was successful. +- [ ] Below thhat you should see a details tag that allows to reveal all backup options + +You can now continue with [020-backup-and-restore.md](.//020-backup-and-restore.md) \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/010-restore-instance.md b/nextcloud-aio/tests/QA/010-restore-instance.md new file mode 100644 index 0000000..b85f0ab --- /dev/null +++ b/nextcloud-aio/tests/QA/010-restore-instance.md @@ -0,0 +1,19 @@ +# Restore instance + +For the below to work, you need a backup archive of an AIO instance and the location on the test machine and the password for the backup archive. You can get one here: [backup-archive](./assets/backup-archive/) + +- [ ] The section that allows to restore the whole AIO instance from backup should show two input fields: one that allows to enter a location where the backup archive is located and one that allows to enter password of the archive. It should also show a short explanation regarding the path requirements +- [ ] Entering an incorrect path and/or password should let you continue and test your settings in the next step + - [ ] Clicking on the test button should after a reload bring you back to the initial screen where it should say that the test was unsuccessful. Also you should be able to have a look at the backup container logs for investigation what exactly failed. + - [ ] You should also now see the input boxes again where you can change the path and password, confirm it and bring you again to the screen where you can test your settings. +- [ ] Entering the correct path to the backup archive and the correct password here should: + - [ ] Should reload and should hide all options except the option to test the path and password + - [ ] After the test you should see the options to check the integrity of the backup and a list of backup archives that you can choose from to restore your instance + - [ ] Clicking on either option should show a window prompt that lets you cancel the operation + - [ ] Clicking on the integrity check option should check the integrity and report that the backup integrity is good after a while which should then only show the option to choose the backup archive that should be restored + - [ ] Choosing the restore option should finally restore your files. + - [ ] After waiting a while it should reload the page and should show the usual container interface again with the state of your containers (stopped) and the option to start and update the containers again. +- [ ] Clicking on `Start and update containers` should show a window prompt that you should create a backup. Canceling should cancel the operation, confirming should reveal the big spinner again. +- [ ] After waiting a bit, all containers should be green and your instance should be fully functional again + +You can now continue with [020-backup-and-restore.md](./020-backup-and-restore.md) \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/020-backup-and-restore.md b/nextcloud-aio/tests/QA/020-backup-and-restore.md new file mode 100644 index 0000000..114d40f --- /dev/null +++ b/nextcloud-aio/tests/QA/020-backup-and-restore.md @@ -0,0 +1,12 @@ +# Backup and restore + +- [ ] Expanding all backup options in the Backup and restore sectioin should reveal a Backup information section, Backup creation section, Backup check section, Backup restore section and a Daily backup section. +- [ ] The backup restore section should list all available backup archives and list them from most recent to least recent. +- [ ] Clicking on either option of Create backup, Check backup integrity or Restore selected backup should run the corresponding action and report after a while in the last check, backup or restore was successful. +- [ ] Daily backup creatio should allow to enter a time in 24h format e.g. `04:00` should be accepted, `24:00` or `dfjlk` not. +- [ ] Submitting a time here should reload the page and reveal at the same place the option to delete the setting again. +- [ ] When the time of the automatic backup has come (you can test it by choosing a time that is e.g. only a minute away), it should automatically log you out (you can verify by reloading) and after you log in again you should see that the automatic backup is currently running. +- [ ] After a while you should see that your container are starting and in the Backup and restore section you should see that the backup was successful +- [ ] When entering additional backup directories, it should allow e.g. `/etc` and `nextcloud_aio_mastercontainer` but not `nextcloud/test`. Running a backup with this should back up these directories/volumes successfully. + +You can now continue with [030-aio-password-change.md](./030-aio-password-change.md) \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/030-aio-password-change.md b/nextcloud-aio/tests/QA/030-aio-password-change.md new file mode 100644 index 0000000..89c47f6 --- /dev/null +++ b/nextcloud-aio/tests/QA/030-aio-password-change.md @@ -0,0 +1,12 @@ +# AIO password change + +- [ ] In the AIO password change section you should see two input fields. And below the requirements for a new password +- [ ] When entering nothing it should report that you need to enter your current aio password +- [ ] When entering a false password, it should report that to you +- [ ] After entering your current password and leaving the new password empty it should report that you need to enter a new password +- [ ] After entering a new passwort shorter than 24 characters or not allowed characters, it should report that the password requirements are not met. +- [ ] `sdfjlksj` should not be accepted +- [ ] `jdsfklöjiroewoäsadjkfölk` should not be accepted +- [ ] `sdjlfj SDJFLK 32489 sdjklf` should which should reload the page + +You can now continue with [040-login-behavior.md](./040-login-behavior.md) \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/040-login-behavior.md b/nextcloud-aio/tests/QA/040-login-behavior.md new file mode 100644 index 0000000..174f3db --- /dev/null +++ b/nextcloud-aio/tests/QA/040-login-behavior.md @@ -0,0 +1,7 @@ +# Login behavior + +- [ ] When opening the AIO interface in a new tab while the apache container is running, it should report on the login page that Nextcloud is running and you should use the automatic login +- [ ] When the apache container is stopped, you should see here an input field that allows you to enter the AIO password which should log you in +- [ ] Starting and stopping the containers multiple times should every time produce a new token that is used in the admin overview in Nextcloud as link in the button to log you into the AIO interface. (see [003-automatic-login.md](./003-automatic-login.md)) + +You can now continue with [050-optional-addons.md](./050-optional-addons.md) \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/050-optional-addons.md b/nextcloud-aio/tests/QA/050-optional-addons.md new file mode 100644 index 0000000..e31481e --- /dev/null +++ b/nextcloud-aio/tests/QA/050-optional-addons.md @@ -0,0 +1,15 @@ +# Optional addons + +- [ ] Close to the bottom of the page in the AIO interface, you should see the optional addons section +- [ ] You should be able to change optional addons when containers are stopped and not change them when containers are running +- [ ] Enabling either of the options should start a new container with the same or comparable name and should also list them in the containers section +- [ ] After all containers are started with the new config active, you should verify that the options were automatically activated/deactivated. + - [ ] ClamAV by trying to upload a testvirus to Nextcloud https://www.eicar.org/?page_id=3950 + - [ ] Collabora by trying to open a .docx or .odt file in Nextcloud + - [ ] Nextcloud Talk by opening the Talk app in Nextcloud, creating a new chat and trying to join a call in this chat. Also verifying in the settings that the HPB and turn server work. + - [ ] Imaginary by having a look if when uploading a new picture in Nextcloud, it adds some log entries to the container + - [ ] Fulltextsearch by trying to search for a heading inside a file in Nextcloud + - [ ] Talk-recording by starting a call and trying to record something +- [ ] When Collabora is enabled, it should show below the Optional Addons section a section where you can change the dictionaries for collabora. `de_DE en_GB en_US es_ES fr_FR it nl pt_BR pt_PT ru` should be a valid setting. E.g. `de.De` not. If already set, it should show a button that allows to remove the setting again. + +You can now continue with [060-environmental-variables.md](./060-environmental-variables.md) \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/060-environmental-variables.md b/nextcloud-aio/tests/QA/060-environmental-variables.md new file mode 100644 index 0000000..a8022b0 --- /dev/null +++ b/nextcloud-aio/tests/QA/060-environmental-variables.md @@ -0,0 +1,28 @@ +# Environmental variables + +- [ ] When starting the mastercontainer with `--env APACHE_PORT=11000` on a clean instance, the domaincheck container should be started with that same port published. That makes sure that also the Apache container will use that port later on. Using a value here that is not a port will not allow the mastercontainer to start correctly. However `@INTERNAL` is also an allowed value which skips publishing the port on the host for internal usage inside a bridged network for example. +- [ ] When starting the mastercontainer with `--env APACHE_IP_BINDING=127.0.0.1` on a clean instance, the domaincheck container's apache port should only listen on localhost on the host. Using a value here that is not a number or dot will not allow the mastercontainer to start correctly. +- [ ] When starting the mastercontainer with `--env APACHE_ADDITIONAL_NETWORK=frontend_net` on a clean instance, the domaincheck and subsequently the apache containers should be connected to the specified `frontend_net` docker network, in addition to the default `nextcloud-aio` network. Specifying the network that doesn't already exist will not allow the mastercontainer to start correctly. +- [ ] When starting the mastercontainer with `--env TALK_PORT=3479` on a clean instance, the talk container should use this port later on. Using a value here that is not a port will not allow the mastercontainer to start correctly. Also it should stop if apache_port and talk_port are set to the same value. +- [ ] Make also sure that reverse proxies work by following https://github.com/nextcloud/all-in-one/blob/main/reverse-proxy.md#reverse-proxy-documentation and following [001-initial-setup.md](./001-initial-setup.md) and [002-new-instance.md](./002-new-instance.md) +- [ ] When starting the mastercontainer with `--env SKIP_DOMAIN_VALIDATION=true` on a clean instance, it should skip the domain verification. So it should accept any domain that you type in then. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_DATADIR="/mnt/testdata"` it should map that location from `/mnt/testdata` to `/mnt/ncdata` inside the Nextcloud container. Not having adjusted the permissions correctly before starting the Nextcloud container the first time will not allow the Nextcloud container to start correctly. See https://github.com/nextcloud/all-in-one#how-to-change-the-default-location-of-nextclouds-datadir for allowed values. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_MOUNT="/mnt/"` it should map `/mnt/` to `/mnt/` inside the Nextcloud container. See https://github.com/nextcloud/all-in-one#how-to-allow-the-nextcloud-container-to-access-directories-on-the-host for allowed values. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_UPLOAD_LIMIT=11G` it should change Nextclouds upload limit to 11G. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-upload-limit-for-nextcloud for allowed values. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_MEMORY_LIMIT=1024M` it should change Nextclouds PHP memory limit to 1024M. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-php-memory-limit-for-nextcloud for allowed values. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_MAX_TIME=4000` it should change Nextclouds upload max time 4000s. See https://github.com/nextcloud/all-in-one#how-to-adjust-the-max-execution-time-for-nextcloud for allowed values. +- [ ] When starting the mastercontainer with `--env BORG_RETENTION_POLICY="--keep-within=1d --keep-weekly=1 --keep-monthly=1"` it should change borgs retention policy to the defined one. This can be checked when creating a backup and looking at the logs. +- [ ] When starting the mastercontainer with `--env FULLTEXTSEARCH_JAVA_OPTIONS="-Xms1024M -Xmx1024M"` it should change Elasticsearchs `ES_JAVA_OPTS` options to the defined one. This can be checked by checking the `ES_JAVA_OPTS` variable for the nextcloud-aio-fulltextsearch container. +- [ ] When starting the mastercontainer with `--env WATCHTOWER_DOCKER_SOCKET_PATH="$XDG_RUNTIME_DIR/docker.sock"` it should map `$XDG_RUNTIME_DIR/docker.sock` to `/var/run/docker.sock` inside the watchtower container which allow to update the mastercontainer on docker rootless. +- [ ] When starting the mastercontainer with `--env AIO_DISABLE_BACKUP_SECTION=true` it should hide the backup section that gets shown after AIO is set up (everything of [020-backup-and-restore](./020-backup-and-restore.md)) and simply show that the backup section is disabled. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_TRUSTED_CACERTS_DIR=/path/to/my/cacerts`, the resulting nextcloud container should trust all the Certification Authorities, whose certificates are included in the directory `/path/to/my/cacerts` on the host. +See https://github.com/nextcloud/all-in-one#how-to-trust-user-defined-certification-authorities-ca +- [ ] When starting the mastercontainer with `--env COLLABORA_SECCOMP_DISABLED=true`, the resulting collabora container should have `--o:security.seccomp=false` applied to it. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_STARTUP_APPS=deck`, the resulting Nextcloud should have only installed the deck app and not the other apps that get installed by default. Default are `deck twofactor_totp tasks calendar contacts notes`. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_ADDITIONAL_APKS=zip`, the resulting Nextcloud container should have the zip package installed and not imagemagick. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_ADDITIONAL_PHP_EXTENSIONS=inotify`, the resulting Nextcloud container should have the inotify extension installed and not the imagick extension. +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_ENABLE_DRI_DEVICE=true`, the resulting Nextcloud container should have the /dev/dri device mounted into the container. (Only works if a `/dev/dri` device is present on the host) +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_ENABLE_NVIDIA_GPU=true`, the resulting Nextcloud container should have the nvidia gpu device mounted into the container. (Only works if a Nvidia GPU and runtime is installed on the host) +- [ ] When starting the mastercontainer with `--env NEXTCLOUD_KEEP_DISABLED_APPS=true` it should keep apps in Nextcloud that are disabled in the AIO interface. For example if Collabora is disabled in the AIO interface and you install the richdocuments app in Nextcloud, a restart should not uninstall the richdocuments app in Nextcloud anymore. + +You can now continue with [070-timezone-change.md](./070-timezone-change.md) diff --git a/nextcloud-aio/tests/QA/070-timezone-change.md b/nextcloud-aio/tests/QA/070-timezone-change.md new file mode 100644 index 0000000..7687411 --- /dev/null +++ b/nextcloud-aio/tests/QA/070-timezone-change.md @@ -0,0 +1,10 @@ +# Timezone change + +- [ ] At the very bottom of the page you should see the timezone change section +- [ ] When the containers are stopped, you should be able to change it and set/reset it +- [ ] If not already set, it should show an input field where you can enter a timezone +- [ ] `Europe/Berlin` should be accepted, e.g. `Europe Berlin` not +- [ ] When it is set, it should show that it is set to which timezone and display a button that allows to reset it again which does this on a press +- [ ] When it is set, running `date` inside Nextcloud related containers should return the correct timezone + +You can now continue with [080-daily-backup-script.md](./080-daily-backup-script.md) \ No newline at end of file diff --git a/nextcloud-aio/tests/QA/080-daily-backup-script.md b/nextcloud-aio/tests/QA/080-daily-backup-script.md new file mode 100644 index 0000000..19a3731 --- /dev/null +++ b/nextcloud-aio/tests/QA/080-daily-backup-script.md @@ -0,0 +1,5 @@ +# Daily backup script + +The script is delivered within the mastercontainer and allows to run a few things like daily backup and container updates from an external script. + +You can find the documentation on this here which needs to work as documented: https://github.com/nextcloud/all-in-one#how-to-stopstartupdate-containers-or-trigger-the-daily-backup-from-a-script-externally diff --git a/nextcloud-aio/tests/QA/assets/backup-archive/readme.md b/nextcloud-aio/tests/QA/assets/backup-archive/readme.md new file mode 100644 index 0000000..c215556 --- /dev/null +++ b/nextcloud-aio/tests/QA/assets/backup-archive/readme.md @@ -0,0 +1,4 @@ +# Backup archive + +The backup archive was moved here because of Git LFS limitations: +https://cloud.nextcloud.com/s/m5DF3AjRs72kWKY diff --git a/nextcloud-aio/tests/QA/readme.md b/nextcloud-aio/tests/QA/readme.md new file mode 100644 index 0000000..b1a9c6f --- /dev/null +++ b/nextcloud-aio/tests/QA/readme.md @@ -0,0 +1,7 @@ +# QA test plans + +In this folder are manual test plans for QA located that allow to manually step through certain features and make sure that everything works as expected. + +For a test instance, you should make sure that all potentially breaking changes are merged, build new containers by following https://github.com/nextcloud/all-in-one/blob/main/develop.md#how-to-build-new-containers, stop a potential old instance, remove it and delete all volumes. Afterwards start a new clean test instance by following https://github.com/nextcloud/all-in-one/blob/main/develop.md#developer-channel. + +Best is to start testing with [001-initial-setup.md](./001-initial-setup.md).