zfs_unstable: 2.3.0-rc3 -> 2.3.0-rc4 (#365045)
[NixPkgs.git] / nixos / modules / services / networking / seafile.nix
blobca8d41492e3d42cb5fdffe00ceb862b1a7d1c8d1
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 let
8   cfg = config.services.seafile;
9   settingsFormat = pkgs.formats.ini { };
11   ccnetConf = settingsFormat.generate "ccnet.conf" (
12     lib.attrsets.recursiveUpdate {
13       Database = {
14         ENGINE = "mysql";
15         UNIX_SOCKET = "/var/run/mysqld/mysqld.sock";
16         DB = "ccnet_db";
17         CONNECTION_CHARSET = "utf8";
18       };
19     } cfg.ccnetSettings
20   );
22   seafileConf = settingsFormat.generate "seafile.conf" (
23     lib.attrsets.recursiveUpdate {
24       database = {
25         type = "mysql";
26         unix_socket = "/var/run/mysqld/mysqld.sock";
27         db_name = "seafile_db";
28         connection_charset = "utf8";
29       };
30     } cfg.seafileSettings
31   );
33   seahubSettings = pkgs.writeText "seahub_settings.py" ''
34     FILE_SERVER_ROOT = '${cfg.ccnetSettings.General.SERVICE_URL}/seafhttp'
35     DATABASES = {
36         'default': {
37             'ENGINE': 'django.db.backends.mysql',
38             'NAME' : 'seahub_db',
39             'HOST' : '/var/run/mysqld/mysqld.sock',
40         }
41     }
42     MEDIA_ROOT = '${seahubDir}/media/'
43     THUMBNAIL_ROOT = '${seahubDir}/thumbnail/'
45     SERVICE_URL = '${cfg.ccnetSettings.General.SERVICE_URL}'
47     CSRF_TRUSTED_ORIGINS = ["${cfg.ccnetSettings.General.SERVICE_URL}"]
49     with open('${seafRoot}/.seahubSecret') as f:
50         SECRET_KEY = f.readline().rstrip()
52     ${cfg.seahubExtraConf}
53   '';
55   seafRoot = "/var/lib/seafile";
56   ccnetDir = "${seafRoot}/ccnet";
57   seahubDir = "${seafRoot}/seahub";
58   defaultUser = "seafile";
63   ###### Interface
65   options.services.seafile = with lib; {
66     enable = mkEnableOption "Seafile server";
68     ccnetSettings = mkOption {
69       type = types.submodule {
70         freeformType = settingsFormat.type;
72         options = {
73           General = {
74             SERVICE_URL = mkOption {
75               type = types.singleLineStr;
76               example = "https://www.example.com";
77               description = ''
78                 Seahub public URL.
79               '';
80             };
81           };
82         };
83       };
84       default = { };
85       description = ''
86         Configuration for ccnet, see
87         <https://manual.seafile.com/config/ccnet-conf/>
88         for supported values.
89       '';
90     };
92     seafileSettings = mkOption {
93       type = types.submodule {
94         freeformType = settingsFormat.type;
96         options = {
97           fileserver = {
98             port = mkOption {
99               type = types.port;
100               default = 8082;
101               description = ''
102                 The tcp port used by seafile fileserver.
103               '';
104             };
105             host = mkOption {
106               type = types.singleLineStr;
107               default = "ipv4:127.0.0.1";
108               example = "unix:/run/seafile/server.sock";
109               description = ''
110                 The bind address used by seafile fileserver.
112                 The addr can be defined as one of the following:
113                 - ipv6:<ipv6addr> for binding to an IPv6 address.
114                 - unix:<named pipe> for binding to a unix named socket
115                 - ipv4:<ipv4addr> for binding to an ipv4 address
116                 Otherwise the addr is assumed to be ipv4.
117               '';
118             };
119           };
120         };
121       };
122       default = { };
123       description = ''
124         Configuration for seafile-server, see
125         <https://manual.seafile.com/config/seafile-conf/>
126         for supported values.
127       '';
128     };
130     seahubAddress = mkOption {
131       type = types.singleLineStr;
132       default = "unix:/run/seahub/gunicorn.sock";
133       example = "[::1]:8083";
134       description = ''
135         Which address to bind the seahub server to, of the form:
136         - HOST
137         - HOST:PORT
138         - unix:PATH.
139         IPv6 HOSTs must be wrapped in brackets.
140       '';
141     };
143     workers = mkOption {
144       type = types.int;
145       default = 4;
146       example = 10;
147       description = ''
148         The number of gunicorn worker processes for handling requests.
149       '';
150     };
152     adminEmail = mkOption {
153       example = "john@example.com";
154       type = types.singleLineStr;
155       description = ''
156         Seafile Seahub Admin Account Email.
157       '';
158     };
160     initialAdminPassword = mkOption {
161       example = "someStrongPass";
162       type = types.singleLineStr;
163       description = ''
164         Seafile Seahub Admin Account initial password.
165         Should be changed via Seahub web front-end.
166       '';
167     };
169     seahubPackage = mkPackageOption pkgs "seahub" { };
171     user = mkOption {
172       type = types.singleLineStr;
173       default = defaultUser;
174       description = "User account under which seafile runs.";
175     };
177     group = mkOption {
178       type = types.singleLineStr;
179       default = defaultUser;
180       description = "Group under which seafile runs.";
181     };
183     dataDir = mkOption {
184       type = types.path;
185       default = "${seafRoot}/data";
186       description = "Path in which to store user data";
187     };
189     gc = {
190       enable = mkEnableOption "automatic garbage collection on stored data blocks";
192       dates = mkOption {
193         type = types.listOf types.singleLineStr;
194         default = [ "Sun 03:00:00" ];
195         description = ''
196           When to run garbage collection on stored data blocks.
197           The time format is described in {manpage}`systemd.time(7)`.
198         '';
199       };
201       randomizedDelaySec = mkOption {
202         default = "0";
203         type = types.singleLineStr;
204         example = "45min";
205         description = ''
206           Add a randomized delay before each garbage collection.
207           The delay will be chosen between zero and this value.
208           This value must be a time span in the format specified by
209           {manpage}`systemd.time(7)`
210         '';
211       };
213       persistent = mkOption {
214         default = true;
215         type = types.bool;
216         example = false;
217         description = ''
218           Takes a boolean argument. If true, the time when the service
219           unit was last triggered is stored on disk. When the timer is
220           activated, the service unit is triggered immediately if it
221           would have been triggered at least once during the time when
222           the timer was inactive. Such triggering is nonetheless
223           subject to the delay imposed by RandomizedDelaySec=. This is
224           useful to catch up on missed runs of the service when the
225           system was powered down.
226         '';
227       };
228     };
230     seahubExtraConf = mkOption {
231       default = "";
232       example = ''
233         CSRF_TRUSTED_ORIGINS = ["https://example.com"]
234       '';
235       type = types.lines;
236       description = ''
237         Extra config to append to `seahub_settings.py` file.
238         Refer to <https://manual.seafile.com/config/seahub_settings_py/>
239         for all available options.
240       '';
241     };
242   };
244   ###### Implementation
246   config = lib.mkIf cfg.enable {
247     services.mysql = {
248       enable = true;
249       package = lib.mkDefault pkgs.mariadb;
250       ensureDatabases = [
251         "ccnet_db"
252         "seafile_db"
253         "seahub_db"
254       ];
255       ensureUsers = [
256         {
257           name = cfg.user;
258           ensurePermissions = {
259             "ccnet_db.*" = "ALL PRIVILEGES";
260             "seafile_db.*" = "ALL PRIVILEGES";
261             "seahub_db.*" = "ALL PRIVILEGES";
262           };
263         }
264       ];
265     };
267     environment.etc."seafile/ccnet.conf".source = ccnetConf;
268     environment.etc."seafile/seafile.conf".source = seafileConf;
269     environment.etc."seafile/seahub_settings.py".source = seahubSettings;
271     users.users = lib.optionalAttrs (cfg.user == defaultUser) {
272       "${defaultUser}" = {
273         group = cfg.group;
274         isSystemUser = true;
275       };
276     };
278     users.groups = lib.optionalAttrs (cfg.group == defaultUser) { "${defaultUser}" = { }; };
280     systemd.targets.seafile = {
281       wantedBy = [ "multi-user.target" ];
282       description = "Seafile components";
283     };
285     systemd.services =
286       let
287         serviceOptions = {
288           ProtectHome = true;
289           PrivateUsers = true;
290           PrivateDevices = true;
291           PrivateTmp = true;
292           ProtectSystem = "strict";
293           ProtectClock = true;
294           ProtectHostname = true;
295           ProtectProc = "invisible";
296           ProtectKernelModules = true;
297           ProtectKernelTunables = true;
298           ProtectKernelLogs = true;
299           ProtectControlGroups = true;
300           RestrictNamespaces = true;
301           RemoveIPC = true;
302           LockPersonality = true;
303           RestrictRealtime = true;
304           RestrictSUIDSGID = true;
305           NoNewPrivileges = true;
306           MemoryDenyWriteExecute = true;
307           SystemCallArchitectures = "native";
308           RestrictAddressFamilies = [
309             "AF_UNIX"
310             "AF_INET"
311           ];
313           User = cfg.user;
314           Group = cfg.group;
315           StateDirectory = "seafile";
316           RuntimeDirectory = "seafile";
317           LogsDirectory = "seafile";
318           ConfigurationDirectory = "seafile";
319           ReadWritePaths = lib.lists.optional (cfg.dataDir != "${seafRoot}/data") cfg.dataDir;
320         };
321       in
322       {
323         seaf-server = {
324           description = "Seafile server";
325           partOf = [ "seafile.target" ];
326           unitConfig.RequiresMountsFor = lib.lists.optional (cfg.dataDir != "${seafRoot}/data") cfg.dataDir;
327           requires = [ "mysql.service" ];
328           after = [
329             "network.target"
330             "mysql.service"
331           ];
332           wantedBy = [ "seafile.target" ];
333           restartTriggers = [
334             ccnetConf
335             seafileConf
336           ];
337           serviceConfig = serviceOptions // {
338             ExecStart = ''
339               ${lib.getExe cfg.seahubPackage.seafile-server} \
340               --foreground \
341               -F /etc/seafile \
342               -c ${ccnetDir} \
343               -d ${cfg.dataDir} \
344               -l /var/log/seafile/server.log \
345               -P /run/seafile/server.pid \
346               -p /run/seafile
347             '';
348           };
349           preStart = ''
350             if [ ! -f "${seafRoot}/server-setup" ]; then
351                 mkdir -p ${cfg.dataDir}/library-template
352                 # Load schema on first install
353                 ${pkgs.mariadb.client}/bin/mysql --database=ccnet_db < ${cfg.seahubPackage.seafile-server}/share/seafile/sql/mysql/ccnet.sql
354                 ${pkgs.mariadb.client}/bin/mysql --database=seafile_db < ${cfg.seahubPackage.seafile-server}/share/seafile/sql/mysql/seafile.sql
355                 echo "${cfg.seahubPackage.seafile-server.version}-mysql" > "${seafRoot}"/server-setup
356                 echo Loaded MySQL schemas for first install
357             fi
358             # checking for upgrades and handling them
359             installedMajor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f1)
360             installedMinor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f2)
361             pkgMajor=$(echo "${cfg.seahubPackage.seafile-server.version}" | cut -d"." -f1)
362             pkgMinor=$(echo "${cfg.seahubPackage.seafile-server.version}" | cut -d"." -f2)
364             if [[ $installedMajor == $pkgMajor && $installedMinor == $pkgMinor ]]; then
365                :
366             elif [[ $installedMajor == 10 && $installedMinor == 0 && $pkgMajor == 11 && $pkgMinor == 0 ]]; then
367                 # Upgrade from 10.0 to 11.0: migrate to mysql
368                 echo Migrating from version 10 to 11
370                 # From https://github.com/haiwen/seahub/blob/e12f941bfef7191795d8c72a7d339c01062964b2/scripts/sqlite2mysql.sh
372                 echo Migrating ccnet database to MySQL
373                 ${lib.getExe pkgs.sqlite} ${ccnetDir}/PeerMgr/usermgr.db ".dump" | \
374                   ${lib.getExe cfg.seahubPackage.python3} ${cfg.seahubPackage}/scripts/sqlite2mysql.py > ${ccnetDir}/ccnet.sql
375                 ${lib.getExe pkgs.sqlite} ${ccnetDir}/GroupMgr/groupmgr.db ".dump" | \
376                   ${lib.getExe cfg.seahubPackage.python3} ${cfg.seahubPackage}/scripts/sqlite2mysql.py >> ${ccnetDir}/ccnet.sql
377                 sed 's/ctime INTEGER/ctime BIGINT/g' -i ${ccnetDir}/ccnet.sql
378                 sed 's/email TEXT, role TEXT/email VARCHAR(255), role TEXT/g' -i ${ccnetDir}/ccnet.sql
379                 ${pkgs.mariadb.client}/bin/mysql --database=ccnet_db < ${ccnetDir}/ccnet.sql
381                 echo Migrating seafile database to MySQL
382                 ${lib.getExe pkgs.sqlite} ${cfg.dataDir}/seafile.db ".dump" | \
383                   ${lib.getExe cfg.seahubPackage.python3} ${cfg.seahubPackage}/scripts/sqlite2mysql.py > ${cfg.dataDir}/seafile.sql
384                 sed 's/owner_id TEXT/owner_id VARCHAR(255)/g' -i ${cfg.dataDir}/seafile.sql
385                 sed 's/user_name TEXT/user_name VARCHAR(255)/g' -i ${cfg.dataDir}/seafile.sql
386                 ${pkgs.mariadb.client}/bin/mysql --database=seafile_db < ${cfg.dataDir}/seafile.sql
388                 echo Migrating seahub database to MySQL
389                 echo 'SET FOREIGN_KEY_CHECKS=0;' > ${seahubDir}/seahub.sql
390                 ${lib.getExe pkgs.sqlite} ${seahubDir}/seahub.db ".dump" | \
391                   ${lib.getExe cfg.seahubPackage.python3} ${cfg.seahubPackage}/scripts/sqlite2mysql.py >> ${seahubDir}/seahub.sql
392                 sed 's/`permission` , `reporter` text NOT NULL/`permission` longtext NOT NULL/g' -i ${seahubDir}/seahub.sql
393                 sed 's/varchar(256) NOT NULL UNIQUE/varchar(255) NOT NULL UNIQUE/g' -i ${seahubDir}/seahub.sql
394                 sed 's/,    UNIQUE (`user_email`, `contact_email`)//g' -i ${seahubDir}/seahub.sql
395                 sed '/INSERT INTO `base_dirfileslastmodifiedinfo`/d' -i ${seahubDir}/seahub.sql
396                 sed '/INSERT INTO `notifications_usernotification`/d' -i ${seahubDir}/seahub.sql
397                 sed 's/DEFERRABLE INITIALLY DEFERRED//g' -i ${seahubDir}/seahub.sql
398                 ${pkgs.mariadb.client}/bin/mysql --database=seahub_db < ${seahubDir}/seahub.sql
400                 echo "${cfg.seahubPackage.seafile-server.version}-mysql" > "${seafRoot}"/server-setup
401                 echo Migration complete
402             else
403                 echo "Unsupported upgrade: $installedMajor.$installedMinor to $pkgMajor.$pkgMinor" >&2
404                 exit 1
405             fi
406           '';
408           # Fix unix socket permissions
409           postStart = (
410             lib.strings.optionalString (lib.strings.hasPrefix "unix:" cfg.seafileSettings.fileserver.host) ''
411               while [[ ! -S "${lib.strings.removePrefix "unix:" cfg.seafileSettings.fileserver.host}" ]]; do
412                 sleep 1
413               done
414               chmod 666 "${lib.strings.removePrefix "unix:" cfg.seafileSettings.fileserver.host}"
415             ''
416           );
417         };
419         seahub = {
420           description = "Seafile Server Web Frontend";
421           wantedBy = [ "seafile.target" ];
422           partOf = [ "seafile.target" ];
423           unitConfig.RequiresMountsFor = lib.lists.optional (cfg.dataDir != "${seafRoot}/data") cfg.dataDir;
424           requires = [
425             "mysql.service"
426             "seaf-server.service"
427           ];
428           after = [
429             "network.target"
430             "mysql.service"
431             "seaf-server.service"
432           ];
433           restartTriggers = [ seahubSettings ];
434           environment = {
435             PYTHONPATH = "${cfg.seahubPackage.pythonPath}:${cfg.seahubPackage}/thirdpart:${cfg.seahubPackage}";
436             DJANGO_SETTINGS_MODULE = "seahub.settings";
437             CCNET_CONF_DIR = ccnetDir;
438             SEAFILE_CONF_DIR = cfg.dataDir;
439             SEAFILE_CENTRAL_CONF_DIR = "/etc/seafile";
440             SEAFILE_RPC_PIPE_PATH = "/run/seafile";
441             SEAHUB_LOG_DIR = "/var/log/seafile";
442           };
443           serviceConfig = serviceOptions // {
444             RuntimeDirectory = "seahub";
445             ExecStart = ''
446               ${lib.getExe cfg.seahubPackage.python3.pkgs.gunicorn} seahub.wsgi:application \
447               --name seahub \
448               --workers ${toString cfg.workers} \
449               --log-level=info \
450               --preload \
451               --timeout=1200 \
452               --limit-request-line=8190 \
453               --bind ${cfg.seahubAddress}
454             '';
455           };
456           preStart = ''
457             mkdir -p ${seahubDir}/media
458             # Link all media except avatars
459             for m in `find ${cfg.seahubPackage}/media/ -maxdepth 1 -not -name "avatars"`; do
460               ln -sf $m ${seahubDir}/media/
461             done
462             if [ ! -e "${seafRoot}/.seahubSecret" ]; then
463                 (
464                   umask 377 &&
465                   ${lib.getExe cfg.seahubPackage.python3} ${cfg.seahubPackage}/tools/secret_key_generator.py > ${seafRoot}/.seahubSecret
466                 )
467             fi
468             if [ ! -f "${seafRoot}/seahub-setup" ]; then
469                 # avatars directory should be writable
470                 install -D -t ${seahubDir}/media/avatars/ ${cfg.seahubPackage}/media/avatars/default.png
471                 install -D -t ${seahubDir}/media/avatars/groups ${cfg.seahubPackage}/media/avatars/groups/default.png
472                 # init database
473                 ${cfg.seahubPackage}/manage.py migrate
474                 # create admin account
475                 ${lib.getExe pkgs.expect} -c 'spawn ${cfg.seahubPackage}/manage.py createsuperuser --email=${cfg.adminEmail}; expect "Password: "; send "${cfg.initialAdminPassword}\r"; expect "Password (again): "; send "${cfg.initialAdminPassword}\r"; expect "Superuser created successfully."'
476                 echo "${cfg.seahubPackage.version}-mysql" > "${seafRoot}/seahub-setup"
477             fi
478             if [ $(cat "${seafRoot}/seahub-setup" | cut -d"-" -f1) != "${pkgs.seahub.version}" ]; then
479                 # run django migrations
480                 ${cfg.seahubPackage}/manage.py migrate
481                 echo "${cfg.seahubPackage.version}-mysql" > "${seafRoot}/seahub-setup"
482             fi
483           '';
484         };
486         seaf-gc = {
487           description = "Seafile storage garbage collection";
488           conflicts = [
489             "seaf-server.service"
490             "seahub.service"
491           ];
492           after = [
493             "seaf-server.service"
494             "seahub.service"
495           ];
496           unitConfig.RequiresMountsFor = lib.lists.optional (cfg.dataDir != "${seafRoot}/data") cfg.dataDir;
497           onSuccess = [
498             "seaf-server.service"
499             "seahub.service"
500           ];
501           onFailure = [
502             "seaf-server.service"
503             "seahub.service"
504           ];
505           startAt = lib.lists.optionals cfg.gc.enable cfg.gc.dates;
506           serviceConfig = serviceOptions // {
507             Type = "oneshot";
508           };
509           script = ''
510             if [ ! -f "${seafRoot}/server-setup" ]; then
511                 echo "Server not setup yet, GC not needed" >&2
512                 exit
513             fi
515             # checking for pending upgrades
516             installedMajor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f1)
517             installedMinor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f2)
518             pkgMajor=$(echo "${cfg.seahubPackage.seafile-server.version}" | cut -d"." -f1)
519             pkgMinor=$(echo "${cfg.seahubPackage.seafile-server.version}" | cut -d"." -f2)
521             if [[ $installedMajor != $pkgMajor || $installedMinor != $pkgMinor ]]; then
522                 echo "Server not upgraded yet" >&2
523                 exit
524             fi
526             # Clean up user-deleted blocks and libraries
527             ${cfg.seahubPackage.seafile-server}/bin/seafserv-gc \
528               -F /etc/seafile \
529               -c ${ccnetDir} \
530               -d ${cfg.dataDir} \
531               --rm-fs
532           '';
533         };
534       };
536     systemd.timers.seaf-gc = lib.mkIf cfg.gc.enable {
537       timerConfig = {
538         RandomizedDelaySec = cfg.gc.randomizedDelaySec;
539         Persistent = cfg.gc.persistent;
540       };
541     };
542   };
544   meta.maintainers = with lib.maintainers; [
545     greizgh
546     schmittlauch
547   ];