1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.nextcloud;
7 fpm = config.services.phpfpm.pools.nextcloud;
8
9 inherit (cfg) datadir;
10
11 phpPackage = cfg.phpPackage.buildEnv {
12 extensions = { enabled, all }:
13 (with all;
14 enabled
15 ++ optional cfg.enableImagemagick imagick
16 # Optionally enabled depending on caching settings
17 ++ optional cfg.caching.apcu apcu
18 ++ optional cfg.caching.redis redis
19 ++ optional cfg.caching.memcached memcached
20 )
21 ++ cfg.phpExtraExtensions all; # Enabled by user
22 extraConfig = toKeyValue phpOptions;
23 };
24
25 toKeyValue = generators.toKeyValue {
26 mkKeyValue = generators.mkKeyValueDefault {} " = ";
27 };
28
29 phpOptions = {
30 upload_max_filesize = cfg.maxUploadSize;
31 post_max_size = cfg.maxUploadSize;
32 memory_limit = cfg.maxUploadSize;
33 } // cfg.phpOptions
34 // optionalAttrs cfg.caching.apcu {
35 "apc.enable_cli" = "1";
36 };
37
38 occ = pkgs.writeScriptBin "nextcloud-occ" ''
39 #! ${pkgs.runtimeShell}
40 cd ${cfg.package}
41 sudo=exec
42 if [[ "$USER" != nextcloud ]]; then
43 sudo='exec /run/wrappers/bin/sudo -u nextcloud --preserve-env=NEXTCLOUD_CONFIG_DIR --preserve-env=OC_PASS'
44 fi
45 export NEXTCLOUD_CONFIG_DIR="${datadir}/config"
46 $sudo \
47 ${phpPackage}/bin/php \
48 occ "$@"
49 '';
50
51 inherit (config.system) stateVersion;
52
53in {
54
55 imports = [
56 (mkRemovedOptionModule [ "services" "nextcloud" "config" "adminpass" ] ''
57 Please use `services.nextcloud.config.adminpassFile' instead!
58 '')
59 (mkRemovedOptionModule [ "services" "nextcloud" "config" "dbpass" ] ''
60 Please use `services.nextcloud.config.dbpassFile' instead!
61 '')
62 (mkRemovedOptionModule [ "services" "nextcloud" "nginx" "enable" ] ''
63 The nextcloud module supports `nginx` as reverse-proxy by default and doesn't
64 support other reverse-proxies officially.
65
66 However it's possible to use an alternative reverse-proxy by
67
68 * disabling nginx
69 * setting `listen.owner` & `listen.group` in the phpfpm-pool to a different value
70
71 Further details about this can be found in the `Nextcloud`-section of the NixOS-manual
72 (which can be openend e.g. by running `nixos-help`).
73 '')
74 (mkRemovedOptionModule [ "services" "nextcloud" "disableImagemagick" ] ''
75 Use services.nextcloud.nginx.enableImagemagick instead.
76 '')
77 ];
78
79 options.services.nextcloud = {
80 enable = mkEnableOption "nextcloud";
81 hostName = mkOption {
82 type = types.str;
83 description = "FQDN for the nextcloud instance.";
84 };
85 home = mkOption {
86 type = types.str;
87 default = "/var/lib/nextcloud";
88 description = "Storage path of nextcloud.";
89 };
90 datadir = mkOption {
91 type = types.str;
92 defaultText = "config.services.nextcloud.home";
93 description = ''
94 Data storage path of nextcloud. Will be <xref linkend="opt-services.nextcloud.home" /> by default.
95 This folder will be populated with a config.php and data folder which contains the state of the instance (excl the database).";
96 '';
97 example = "/mnt/nextcloud-file";
98 };
99 extraApps = mkOption {
100 type = types.attrsOf types.package;
101 default = { };
102 description = ''
103 Extra apps to install. Should be an attrSet of appid to packages generated by fetchNextcloudApp.
104 The appid must be identical to the "id" value in the apps appinfo/info.xml.
105 Using this will disable the appstore to prevent Nextcloud from updating these apps (see <xref linkend="opt-services.nextcloud.appstoreEnable" />).
106 '';
107 example = literalExpression ''
108 {
109 maps = pkgs.fetchNextcloudApp {
110 name = "maps";
111 sha256 = "007y80idqg6b6zk6kjxg4vgw0z8fsxs9lajnv49vv1zjy6jx2i1i";
112 url = "https://github.com/nextcloud/maps/releases/download/v0.1.9/maps-0.1.9.tar.gz";
113 version = "0.1.9";
114 };
115 phonetrack = pkgs.fetchNextcloudApp {
116 name = "phonetrack";
117 sha256 = "0qf366vbahyl27p9mshfma1as4nvql6w75zy2zk5xwwbp343vsbc";
118 url = "https://gitlab.com/eneiluj/phonetrack-oc/-/wikis/uploads/931aaaf8dca24bf31a7e169a83c17235/phonetrack-0.6.9.tar.gz";
119 version = "0.6.9";
120 };
121 }
122 '';
123 };
124 extraAppsEnable = mkOption {
125 type = types.bool;
126 default = true;
127 description = ''
128 Automatically enable the apps in <xref linkend="opt-services.nextcloud.extraApps" /> every time nextcloud starts.
129 If set to false, apps need to be enabled in the Nextcloud user interface or with nextcloud-occ app:enable.
130 '';
131 };
132 appstoreEnable = mkOption {
133 type = types.nullOr types.bool;
134 default = null;
135 example = true;
136 description = ''
137 Allow the installation of apps and app updates from the store.
138 Enabled by default unless there are packages in <xref linkend="opt-services.nextcloud.extraApps" />.
139 Set to true to force enable the store even if <xref linkend="opt-services.nextcloud.extraApps" /> is used.
140 Set to false to disable the installation of apps from the global appstore. App management is always enabled regardless of this setting.
141 '';
142 };
143 logLevel = mkOption {
144 type = types.ints.between 0 4;
145 default = 2;
146 description = "Log level value between 0 (DEBUG) and 4 (FATAL).";
147 };
148 https = mkOption {
149 type = types.bool;
150 default = false;
151 description = "Use https for generated links.";
152 };
153 package = mkOption {
154 type = types.package;
155 description = "Which package to use for the Nextcloud instance.";
156 relatedPackages = [ "nextcloud21" "nextcloud22" ];
157 };
158 phpPackage = mkOption {
159 type = types.package;
160 relatedPackages = [ "php74" "php80" ];
161 defaultText = "pkgs.php";
162 description = ''
163 PHP package to use for Nextcloud.
164 '';
165 };
166
167 maxUploadSize = mkOption {
168 default = "512M";
169 type = types.str;
170 description = ''
171 Defines the upload limit for files. This changes the relevant options
172 in php.ini and nginx if enabled.
173 '';
174 };
175
176 skeletonDirectory = mkOption {
177 default = "";
178 type = types.str;
179 description = ''
180 The directory where the skeleton files are located. These files will be
181 copied to the data directory of new users. Leave empty to not copy any
182 skeleton files.
183 '';
184 };
185
186 webfinger = mkOption {
187 type = types.bool;
188 default = false;
189 description = ''
190 Enable this option if you plan on using the webfinger plugin.
191 The appropriate nginx rewrite rules will be added to your configuration.
192 '';
193 };
194
195 phpExtraExtensions = mkOption {
196 type = with types; functionTo (listOf package);
197 default = all: [];
198 defaultText = literalExpression "all: []";
199 description = ''
200 Additional PHP extensions to use for nextcloud.
201 By default, only extensions necessary for a vanilla nextcloud installation are enabled,
202 but you may choose from the list of available extensions and add further ones.
203 This is sometimes necessary to be able to install a certain nextcloud app that has additional requirements.
204 '';
205 example = literalExpression ''
206 all: [ all.pdlib all.bz2 ]
207 '';
208 };
209
210 phpOptions = mkOption {
211 type = types.attrsOf types.str;
212 default = {
213 short_open_tag = "Off";
214 expose_php = "Off";
215 error_reporting = "E_ALL & ~E_DEPRECATED & ~E_STRICT";
216 display_errors = "stderr";
217 "opcache.enable_cli" = "1";
218 "opcache.interned_strings_buffer" = "8";
219 "opcache.max_accelerated_files" = "10000";
220 "opcache.memory_consumption" = "128";
221 "opcache.revalidate_freq" = "1";
222 "opcache.fast_shutdown" = "1";
223 "openssl.cafile" = "/etc/ssl/certs/ca-certificates.crt";
224 catch_workers_output = "yes";
225 };
226 description = ''
227 Options for PHP's php.ini file for nextcloud.
228 '';
229 };
230
231 poolSettings = mkOption {
232 type = with types; attrsOf (oneOf [ str int bool ]);
233 default = {
234 "pm" = "dynamic";
235 "pm.max_children" = "32";
236 "pm.start_servers" = "2";
237 "pm.min_spare_servers" = "2";
238 "pm.max_spare_servers" = "4";
239 "pm.max_requests" = "500";
240 };
241 description = ''
242 Options for nextcloud's PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
243 '';
244 };
245
246 poolConfig = mkOption {
247 type = types.nullOr types.lines;
248 default = null;
249 description = ''
250 Options for nextcloud's PHP pool. See the documentation on <literal>php-fpm.conf</literal> for details on configuration directives.
251 '';
252 };
253
254 config = {
255 dbtype = mkOption {
256 type = types.enum [ "sqlite" "pgsql" "mysql" ];
257 default = "sqlite";
258 description = "Database type.";
259 };
260 dbname = mkOption {
261 type = types.nullOr types.str;
262 default = "nextcloud";
263 description = "Database name.";
264 };
265 dbuser = mkOption {
266 type = types.nullOr types.str;
267 default = "nextcloud";
268 description = "Database user.";
269 };
270 dbpassFile = mkOption {
271 type = types.nullOr types.str;
272 default = null;
273 description = ''
274 The full path to a file that contains the database password.
275 '';
276 };
277 dbhost = mkOption {
278 type = types.nullOr types.str;
279 default = "localhost";
280 description = ''
281 Database host.
282
283 Note: for using Unix authentication with PostgreSQL, this should be
284 set to <literal>/run/postgresql</literal>.
285 '';
286 };
287 dbport = mkOption {
288 type = with types; nullOr (either int str);
289 default = null;
290 description = "Database port.";
291 };
292 dbtableprefix = mkOption {
293 type = types.nullOr types.str;
294 default = null;
295 description = "Table prefix in Nextcloud database.";
296 };
297 adminuser = mkOption {
298 type = types.str;
299 default = "root";
300 description = "Admin username.";
301 };
302 adminpassFile = mkOption {
303 type = types.str;
304 description = ''
305 The full path to a file that contains the admin's password. Must be
306 readable by user <literal>nextcloud</literal>.
307 '';
308 };
309
310 extraTrustedDomains = mkOption {
311 type = types.listOf types.str;
312 default = [];
313 description = ''
314 Trusted domains, from which the nextcloud installation will be
315 acessible. You don't need to add
316 <literal>services.nextcloud.hostname</literal> here.
317 '';
318 };
319
320 trustedProxies = mkOption {
321 type = types.listOf types.str;
322 default = [];
323 description = ''
324 Trusted proxies, to provide if the nextcloud installation is being
325 proxied to secure against e.g. spoofing.
326 '';
327 };
328
329 overwriteProtocol = mkOption {
330 type = types.nullOr (types.enum [ "http" "https" ]);
331 default = null;
332 example = "https";
333
334 description = ''
335 Force Nextcloud to always use HTTPS i.e. for link generation. Nextcloud
336 uses the currently used protocol by default, but when behind a reverse-proxy,
337 it may use <literal>http</literal> for everything although Nextcloud
338 may be served via HTTPS.
339 '';
340 };
341
342 defaultPhoneRegion = mkOption {
343 default = null;
344 type = types.nullOr types.str;
345 example = "DE";
346 description = ''
347 <warning>
348 <para>This option exists since Nextcloud 21! If older versions are used,
349 this will throw an eval-error!</para>
350 </warning>
351
352 <link xlink:href="https://www.iso.org/iso-3166-country-codes.html">ISO 3611-1</link>
353 country codes for automatic phone-number detection without a country code.
354
355 With e.g. <literal>DE</literal> set, the <literal>+49</literal> can be omitted for
356 phone-numbers.
357 '';
358 };
359
360 objectstore = {
361 s3 = {
362 enable = mkEnableOption ''
363 S3 object storage as primary storage.
364
365 This mounts a bucket on an Amazon S3 object storage or compatible
366 implementation into the virtual filesystem.
367
368 Further details about this feature can be found in the
369 <link xlink:href="https://docs.nextcloud.com/server/22/admin_manual/configuration_files/primary_storage.html">upstream documentation</link>.
370 '';
371 bucket = mkOption {
372 type = types.str;
373 example = "nextcloud";
374 description = ''
375 The name of the S3 bucket.
376 '';
377 };
378 autocreate = mkOption {
379 type = types.bool;
380 description = ''
381 Create the objectstore if it does not exist.
382 '';
383 };
384 key = mkOption {
385 type = types.str;
386 example = "EJ39ITYZEUH5BGWDRUFY";
387 description = ''
388 The access key for the S3 bucket.
389 '';
390 };
391 secretFile = mkOption {
392 type = types.str;
393 example = "/var/nextcloud-objectstore-s3-secret";
394 description = ''
395 The full path to a file that contains the access secret. Must be
396 readable by user <literal>nextcloud</literal>.
397 '';
398 };
399 hostname = mkOption {
400 type = types.nullOr types.str;
401 default = null;
402 example = "example.com";
403 description = ''
404 Required for some non-Amazon implementations.
405 '';
406 };
407 port = mkOption {
408 type = types.nullOr types.port;
409 default = null;
410 description = ''
411 Required for some non-Amazon implementations.
412 '';
413 };
414 useSsl = mkOption {
415 type = types.bool;
416 default = true;
417 description = ''
418 Use SSL for objectstore access.
419 '';
420 };
421 region = mkOption {
422 type = types.nullOr types.str;
423 default = null;
424 example = "REGION";
425 description = ''
426 Required for some non-Amazon implementations.
427 '';
428 };
429 usePathStyle = mkOption {
430 type = types.bool;
431 default = false;
432 description = ''
433 Required for some non-Amazon S3 implementations.
434
435 Ordinarily, requests will be made with
436 <literal>http://bucket.hostname.domain/</literal>, but with path style
437 enabled requests are made with
438 <literal>http://hostname.domain/bucket</literal> instead.
439 '';
440 };
441 };
442 };
443 };
444
445 enableImagemagick = mkEnableOption ''
446 the ImageMagick module for PHP.
447 This is used by the theming app and for generating previews of certain images (e.g. SVG and HEIF).
448 You may want to disable it for increased security. In that case, previews will still be available
449 for some images (e.g. JPEG and PNG).
450 See <link xlink:href="https://github.com/nextcloud/server/issues/13099" />.
451 '' // {
452 default = true;
453 };
454
455 caching = {
456 apcu = mkOption {
457 type = types.bool;
458 default = true;
459 description = ''
460 Whether to load the APCu module into PHP.
461 '';
462 };
463 redis = mkOption {
464 type = types.bool;
465 default = false;
466 description = ''
467 Whether to load the Redis module into PHP.
468 You still need to enable Redis in your config.php.
469 See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
470 '';
471 };
472 memcached = mkOption {
473 type = types.bool;
474 default = false;
475 description = ''
476 Whether to load the Memcached module into PHP.
477 You still need to enable Memcached in your config.php.
478 See https://docs.nextcloud.com/server/14/admin_manual/configuration_server/caching_configuration.html
479 '';
480 };
481 };
482 autoUpdateApps = {
483 enable = mkOption {
484 type = types.bool;
485 default = false;
486 description = ''
487 Run regular auto update of all apps installed from the nextcloud app store.
488 '';
489 };
490 startAt = mkOption {
491 type = with types; either str (listOf str);
492 default = "05:00:00";
493 example = "Sun 14:00:00";
494 description = ''
495 When to run the update. See `systemd.services.<name>.startAt`.
496 '';
497 };
498 };
499 occ = mkOption {
500 type = types.package;
501 default = occ;
502 internal = true;
503 description = ''
504 The nextcloud-occ program preconfigured to target this Nextcloud instance.
505 '';
506 };
507 };
508
509 config = mkIf cfg.enable (mkMerge [
510 { warnings = let
511 latest = 22;
512 upgradeWarning = major: nixos:
513 ''
514 A legacy Nextcloud install (from before NixOS ${nixos}) may be installed.
515
516 After nextcloud${toString major} is installed successfully, you can safely upgrade
517 to ${toString (major + 1)}. The latest version available is nextcloud${toString latest}.
518
519 Please note that Nextcloud doesn't support upgrades across multiple major versions
520 (i.e. an upgrade from 16 is possible to 17, but not 16 to 18).
521
522 The package can be upgraded by explicitly declaring the service-option
523 `services.nextcloud.package`.
524 '';
525
526 # FIXME(@Ma27) remove as soon as nextcloud properly supports
527 # mariadb >=10.6.
528 isUnsupportedMariadb =
529 # All currently supported Nextcloud versions are affected.
530 (versionOlder cfg.package.version "23")
531 # This module uses mysql
532 && (cfg.config.dbtype == "mysql")
533 # MySQL is managed via NixOS
534 && config.services.mysql.enable
535 # We're using MariaDB
536 && (getName config.services.mysql.package) == "mariadb-server"
537 # MariaDB is at least 10.6 and thus not supported
538 && (versionAtLeast (getVersion config.services.mysql.package) "10.6");
539
540 in (optional (cfg.poolConfig != null) ''
541 Using config.services.nextcloud.poolConfig is deprecated and will become unsupported in a future release.
542 Please migrate your configuration to config.services.nextcloud.poolSettings.
543 '')
544 ++ (optional (versionOlder cfg.package.version "21") (upgradeWarning 20 "21.05"))
545 ++ (optional (versionOlder cfg.package.version "22") (upgradeWarning 21 "21.11"))
546 ++ (optional isUnsupportedMariadb ''
547 You seem to be using MariaDB at an unsupported version (i.e. at least 10.6)!
548 Please note that this isn't supported officially by Nextcloud. You can either
549
550 * Switch to `pkgs.mysql`
551 * Downgrade MariaDB to at least 10.5
552 * Work around Nextcloud's problems by specifying `innodb_read_only_compressed=0`
553
554 For further context, please read
555 https://help.nextcloud.com/t/update-to-next-cloud-21-0-2-has-get-an-error/117028/15
556 '');
557
558 services.nextcloud.package = with pkgs;
559 mkDefault (
560 if pkgs ? nextcloud
561 then throw ''
562 The `pkgs.nextcloud`-attribute has been removed. If it's supposed to be the default
563 nextcloud defined in an overlay, please set `services.nextcloud.package` to
564 `pkgs.nextcloud`.
565 ''
566 # 21.03 will not be an official release - it was instead 21.05.
567 # This versionOlder statement remains set to 21.03 for backwards compatibility.
568 # See https://github.com/NixOS/nixpkgs/pull/108899 and
569 # https://github.com/NixOS/rfcs/blob/master/rfcs/0080-nixos-release-schedule.md.
570 # FIXME(@Ma27) remove this else-if as soon as 21.05 is EOL! This is only here
571 # to ensure that users who are on Nextcloud 19 with a stateVersion <21.05 with
572 # no explicit services.nextcloud.package don't upgrade to v21 by accident (
573 # nextcloud20 throws an eval-error because it's dropped).
574 else if versionOlder stateVersion "21.03" then nextcloud20
575 else if versionOlder stateVersion "21.11" then nextcloud21
576 else nextcloud22
577 );
578
579 services.nextcloud.datadir = mkOptionDefault config.services.nextcloud.home;
580
581 services.nextcloud.phpPackage =
582 if versionOlder cfg.package.version "21" then pkgs.php74
583 else pkgs.php80;
584 }
585
586 { systemd.timers.nextcloud-cron = {
587 wantedBy = [ "timers.target" ];
588 timerConfig.OnBootSec = "5m";
589 timerConfig.OnUnitActiveSec = "5m";
590 timerConfig.Unit = "nextcloud-cron.service";
591 };
592
593 systemd.services = {
594 # When upgrading the Nextcloud package, Nextcloud can report errors such as
595 # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly"
596 # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround).
597 phpfpm-nextcloud.restartTriggers = [ cfg.package ];
598
599 nextcloud-setup = let
600 c = cfg.config;
601 writePhpArrary = a: "[${concatMapStringsSep "," (val: ''"${toString val}"'') a}]";
602 requiresReadSecretFunction = c.dbpassFile != null || c.objectstore.s3.enable;
603 objectstoreConfig = let s3 = c.objectstore.s3; in optionalString s3.enable ''
604 'objectstore' => [
605 'class' => '\\OC\\Files\\ObjectStore\\S3',
606 'arguments' => [
607 'bucket' => '${s3.bucket}',
608 'autocreate' => ${boolToString s3.autocreate},
609 'key' => '${s3.key}',
610 'secret' => nix_read_secret('${s3.secretFile}'),
611 ${optionalString (s3.hostname != null) "'hostname' => '${s3.hostname}',"}
612 ${optionalString (s3.port != null) "'port' => ${toString s3.port},"}
613 'use_ssl' => ${boolToString s3.useSsl},
614 ${optionalString (s3.region != null) "'region' => '${s3.region}',"}
615 'use_path_style' => ${boolToString s3.usePathStyle},
616 ],
617 ]
618 '';
619
620 showAppStoreSetting = cfg.appstoreEnable != null || cfg.extraApps != {};
621 renderedAppStoreSetting =
622 let
623 x = cfg.appstoreEnable;
624 in
625 if x == null then "false"
626 else boolToString x;
627
628 overrideConfig = pkgs.writeText "nextcloud-config.php" ''
629 <?php
630 ${optionalString requiresReadSecretFunction ''
631 function nix_read_secret($file) {
632 if (!file_exists($file)) {
633 throw new \RuntimeException(sprintf(
634 "Cannot start Nextcloud, secret file %s set by NixOS doesn't seem to "
635 . "exist! Please make sure that the file exists and has appropriate "
636 . "permissions for user & group 'nextcloud'!",
637 $file
638 ));
639 }
640
641 return trim(file_get_contents($file));
642 }
643 ''}
644 $CONFIG = [
645 'apps_paths' => [
646 ${optionalString (cfg.extraApps != { }) "[ 'path' => '${cfg.home}/nix-apps', 'url' => '/nix-apps', 'writable' => false ],"}
647 [ 'path' => '${cfg.home}/apps', 'url' => '/apps', 'writable' => false ],
648 [ 'path' => '${cfg.home}/store-apps', 'url' => '/store-apps', 'writable' => true ],
649 ],
650 ${optionalString (showAppStoreSetting) "'appstoreenabled' => ${renderedAppStoreSetting},"}
651 'datadirectory' => '${datadir}/data',
652 'skeletondirectory' => '${cfg.skeletonDirectory}',
653 ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
654 'log_type' => 'syslog',
655 'log_level' => '${builtins.toString cfg.logLevel}',
656 ${optionalString (c.overwriteProtocol != null) "'overwriteprotocol' => '${c.overwriteProtocol}',"}
657 ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
658 ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"}
659 ${optionalString (c.dbport != null) "'dbport' => '${toString c.dbport}',"}
660 ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"}
661 ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"}
662 ${optionalString (c.dbpassFile != null) "'dbpassword' => nix_read_secret('${c.dbpassFile}'),"}
663 'dbtype' => '${c.dbtype}',
664 'trusted_domains' => ${writePhpArrary ([ cfg.hostName ] ++ c.extraTrustedDomains)},
665 'trusted_proxies' => ${writePhpArrary (c.trustedProxies)},
666 ${optionalString (c.defaultPhoneRegion != null) "'default_phone_region' => '${c.defaultPhoneRegion}',"}
667 ${objectstoreConfig}
668 ];
669 '';
670 occInstallCmd = let
671 mkExport = { arg, value }: "export ${arg}=${value}";
672 dbpass = {
673 arg = "DBPASS";
674 value = if c.dbpassFile != null
675 then ''"$(<"${toString c.dbpassFile}")"''
676 else ''""'';
677 };
678 adminpass = {
679 arg = "ADMINPASS";
680 value = ''"$(<"${toString c.adminpassFile}")"'';
681 };
682 installFlags = concatStringsSep " \\\n "
683 (mapAttrsToList (k: v: "${k} ${toString v}") {
684 "--database" = ''"${c.dbtype}"'';
685 # The following attributes are optional depending on the type of
686 # database. Those that evaluate to null on the left hand side
687 # will be omitted.
688 ${if c.dbname != null then "--database-name" else null} = ''"${c.dbname}"'';
689 ${if c.dbhost != null then "--database-host" else null} = ''"${c.dbhost}"'';
690 ${if c.dbport != null then "--database-port" else null} = ''"${toString c.dbport}"'';
691 ${if c.dbuser != null then "--database-user" else null} = ''"${c.dbuser}"'';
692 "--database-pass" = "\$${dbpass.arg}";
693 "--admin-user" = ''"${c.adminuser}"'';
694 "--admin-pass" = "\$${adminpass.arg}";
695 "--data-dir" = ''"${datadir}/data"'';
696 });
697 in ''
698 ${mkExport dbpass}
699 ${mkExport adminpass}
700 ${occ}/bin/nextcloud-occ maintenance:install \
701 ${installFlags}
702 '';
703 occSetTrustedDomainsCmd = concatStringsSep "\n" (imap0
704 (i: v: ''
705 ${occ}/bin/nextcloud-occ config:system:set trusted_domains \
706 ${toString i} --value="${toString v}"
707 '') ([ cfg.hostName ] ++ cfg.config.extraTrustedDomains));
708
709 in {
710 wantedBy = [ "multi-user.target" ];
711 before = [ "phpfpm-nextcloud.service" ];
712 path = [ occ ];
713 script = ''
714 chmod og+x ${cfg.home}
715
716 ${optionalString (c.dbpassFile != null) ''
717 if [ ! -r "${c.dbpassFile}" ]; then
718 echo "dbpassFile ${c.dbpassFile} is not readable by nextcloud:nextcloud! Aborting..."
719 exit 1
720 fi
721 if [ -z "$(<${c.dbpassFile})" ]; then
722 echo "dbpassFile ${c.dbpassFile} is empty!"
723 exit 1
724 fi
725 ''}
726 if [ ! -r "${c.adminpassFile}" ]; then
727 echo "adminpassFile ${c.adminpassFile} is not readable by nextcloud:nextcloud! Aborting..."
728 exit 1
729 fi
730 if [ -z "$(<${c.adminpassFile})" ]; then
731 echo "adminpassFile ${c.adminpassFile} is empty!"
732 exit 1
733 fi
734
735 ln -sf ${cfg.package}/apps ${cfg.home}/
736
737 # Install extra apps
738 ln -sfT \
739 ${pkgs.linkFarm "nix-apps"
740 (mapAttrsToList (name: path: { inherit name path; }) cfg.extraApps)} \
741 ${cfg.home}/nix-apps
742
743 # create nextcloud directories.
744 # if the directories exist already with wrong permissions, we fix that
745 for dir in ${datadir}/config ${datadir}/data ${cfg.home}/store-apps ${cfg.home}/nix-apps; do
746 if [ ! -e $dir ]; then
747 install -o nextcloud -g nextcloud -d $dir
748 elif [ $(stat -c "%G" $dir) != "nextcloud" ]; then
749 chgrp -R nextcloud $dir
750 fi
751 done
752
753 ln -sf ${overrideConfig} ${datadir}/config/override.config.php
754
755 # Do not install if already installed
756 if [[ ! -e ${datadir}/config/config.php ]]; then
757 ${occInstallCmd}
758 fi
759
760 ${occ}/bin/nextcloud-occ upgrade
761
762 ${occ}/bin/nextcloud-occ config:system:delete trusted_domains
763
764 ${optionalString (cfg.extraAppsEnable && cfg.extraApps != { }) ''
765 # Try to enable apps (don't fail when one of them cannot be enabled , eg. due to incompatible version)
766 ${occ}/bin/nextcloud-occ app:enable ${concatStringsSep " " (attrNames cfg.extraApps)}
767 ''}
768
769 ${occSetTrustedDomainsCmd}
770 '';
771 serviceConfig.Type = "oneshot";
772 serviceConfig.User = "nextcloud";
773 };
774 nextcloud-cron = {
775 environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
776 serviceConfig.Type = "oneshot";
777 serviceConfig.User = "nextcloud";
778 serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${cfg.package}/cron.php";
779 };
780 nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable {
781 serviceConfig.Type = "oneshot";
782 serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all";
783 serviceConfig.User = "nextcloud";
784 startAt = cfg.autoUpdateApps.startAt;
785 };
786 };
787
788 services.phpfpm = {
789 pools.nextcloud = {
790 user = "nextcloud";
791 group = "nextcloud";
792 phpPackage = phpPackage;
793 phpEnv = {
794 NEXTCLOUD_CONFIG_DIR = "${datadir}/config";
795 PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
796 };
797 settings = mapAttrs (name: mkDefault) {
798 "listen.owner" = config.services.nginx.user;
799 "listen.group" = config.services.nginx.group;
800 } // cfg.poolSettings;
801 extraConfig = cfg.poolConfig;
802 };
803 };
804
805 users.users.nextcloud = {
806 home = "${cfg.home}";
807 group = "nextcloud";
808 createHome = true;
809 isSystemUser = true;
810 };
811 users.groups.nextcloud.members = [ "nextcloud" config.services.nginx.user ];
812
813 environment.systemPackages = [ occ ];
814
815 services.nginx.enable = mkDefault true;
816
817 services.nginx.virtualHosts.${cfg.hostName} = {
818 root = cfg.package;
819 locations = {
820 "= /robots.txt" = {
821 priority = 100;
822 extraConfig = ''
823 allow all;
824 log_not_found off;
825 access_log off;
826 '';
827 };
828 "= /" = {
829 priority = 100;
830 extraConfig = ''
831 if ( $http_user_agent ~ ^DavClnt ) {
832 return 302 /remote.php/webdav/$is_args$args;
833 }
834 '';
835 };
836 "/" = {
837 priority = 900;
838 extraConfig = "rewrite ^ /index.php;";
839 };
840 "~ ^/store-apps" = {
841 priority = 201;
842 extraConfig = "root ${cfg.home};";
843 };
844 "~ ^/nix-apps" = {
845 priority = 201;
846 extraConfig = "root ${cfg.home};";
847 };
848 "^~ /.well-known" = {
849 priority = 210;
850 extraConfig = ''
851 absolute_redirect off;
852 location = /.well-known/carddav {
853 return 301 /remote.php/dav;
854 }
855 location = /.well-known/caldav {
856 return 301 /remote.php/dav;
857 }
858 location ~ ^/\.well-known/(?!acme-challenge|pki-validation) {
859 return 301 /index.php$request_uri;
860 }
861 try_files $uri $uri/ =404;
862 '';
863 };
864 "~ ^/(?:build|tests|config|lib|3rdparty|templates|data)(?:$|/)".extraConfig = ''
865 return 404;
866 '';
867 "~ ^/(?:\\.(?!well-known)|autotest|occ|issue|indie|db_|console)".extraConfig = ''
868 return 404;
869 '';
870 "~ ^\\/(?:index|remote|public|cron|core\\/ajax\\/update|status|ocs\\/v[12]|updater\\/.+|oc[ms]-provider\\/.+|.+\\/richdocumentscode\\/proxy)\\.php(?:$|\\/)" = {
871 priority = 500;
872 extraConfig = ''
873 include ${config.services.nginx.package}/conf/fastcgi.conf;
874 fastcgi_split_path_info ^(.+?\.php)(\\/.*)$;
875 set $path_info $fastcgi_path_info;
876 try_files $fastcgi_script_name =404;
877 fastcgi_param PATH_INFO $path_info;
878 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
879 fastcgi_param HTTPS ${if cfg.https then "on" else "off"};
880 fastcgi_param modHeadersAvailable true;
881 fastcgi_param front_controller_active true;
882 fastcgi_pass unix:${fpm.socket};
883 fastcgi_intercept_errors on;
884 fastcgi_request_buffering off;
885 fastcgi_read_timeout 120s;
886 '';
887 };
888 "~ \\.(?:css|js|woff2?|svg|gif|map)$".extraConfig = ''
889 try_files $uri /index.php$request_uri;
890 expires 6M;
891 access_log off;
892 '';
893 "~ ^\\/(?:updater|ocs-provider|ocm-provider)(?:$|\\/)".extraConfig = ''
894 try_files $uri/ =404;
895 index index.php;
896 '';
897 "~ \\.(?:png|html|ttf|ico|jpg|jpeg|bcmap|mp4|webm)$".extraConfig = ''
898 try_files $uri /index.php$request_uri;
899 access_log off;
900 '';
901 };
902 extraConfig = ''
903 index index.php index.html /index.php$request_uri;
904 add_header X-Content-Type-Options nosniff;
905 add_header X-XSS-Protection "1; mode=block";
906 add_header X-Robots-Tag none;
907 add_header X-Download-Options noopen;
908 add_header X-Permitted-Cross-Domain-Policies none;
909 add_header X-Frame-Options sameorigin;
910 add_header Referrer-Policy no-referrer;
911 add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
912 client_max_body_size ${cfg.maxUploadSize};
913 fastcgi_buffers 64 4K;
914 fastcgi_hide_header X-Powered-By;
915 gzip on;
916 gzip_vary on;
917 gzip_comp_level 4;
918 gzip_min_length 256;
919 gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
920 gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;
921
922 ${optionalString cfg.webfinger ''
923 rewrite ^/.well-known/host-meta /public.php?service=host-meta last;
924 rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;
925 ''}
926 '';
927 };
928 }
929 ]);
930
931 meta.doc = ./nextcloud.xml;
932}