1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.seafile;
9 settingsFormat = pkgs.formats.ini { };
10
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 );
21
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 );
32
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/'
44
45 SERVICE_URL = '${cfg.ccnetSettings.General.SERVICE_URL}'
46
47 CSRF_TRUSTED_ORIGINS = ["${cfg.ccnetSettings.General.SERVICE_URL}"]
48
49 with open('${seafRoot}/.seahubSecret') as f:
50 SECRET_KEY = f.readline().rstrip()
51
52 ${cfg.seahubExtraConf}
53 '';
54
55 seafRoot = "/var/lib/seafile";
56 ccnetDir = "${seafRoot}/ccnet";
57 seahubDir = "${seafRoot}/seahub";
58 defaultUser = "seafile";
59
60in
61{
62
63 ###### Interface
64
65 options.services.seafile = with lib; {
66 enable = mkEnableOption "Seafile server";
67
68 ccnetSettings = mkOption {
69 type = types.submodule {
70 freeformType = settingsFormat.type;
71
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 };
91
92 seafileSettings = mkOption {
93 type = types.submodule {
94 freeformType = settingsFormat.type;
95
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.
111
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 };
129
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 };
142
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 };
151
152 adminEmail = mkOption {
153 example = "john@example.com";
154 type = types.singleLineStr;
155 description = ''
156 Seafile Seahub Admin Account Email.
157 '';
158 };
159
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 };
168
169 seahubPackage = mkPackageOption pkgs "seahub" { };
170
171 user = mkOption {
172 type = types.singleLineStr;
173 default = defaultUser;
174 description = "User account under which seafile runs.";
175 };
176
177 group = mkOption {
178 type = types.singleLineStr;
179 default = defaultUser;
180 description = "Group under which seafile runs.";
181 };
182
183 dataDir = mkOption {
184 type = types.path;
185 default = "${seafRoot}/data";
186 description = "Path in which to store user data";
187 };
188
189 gc = {
190 enable = mkEnableOption "automatic garbage collection on stored data blocks";
191
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 };
200
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 };
212
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 };
229
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 };
243
244 ###### Implementation
245
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 };
266
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;
270
271 users.users = lib.optionalAttrs (cfg.user == defaultUser) {
272 "${defaultUser}" = {
273 group = cfg.group;
274 isSystemUser = true;
275 };
276 };
277
278 users.groups = lib.optionalAttrs (cfg.group == defaultUser) { "${defaultUser}" = { }; };
279
280 systemd.targets.seafile = {
281 wantedBy = [ "multi-user.target" ];
282 description = "Seafile components";
283 };
284
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 ];
312
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)
363
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
369
370 # From https://github.com/haiwen/seahub/blob/e12f941bfef7191795d8c72a7d339c01062964b2/scripts/sqlite2mysql.sh
371
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
380
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
387
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
399
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 '';
407
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 };
418
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 };
485
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
514
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)
520
521 if [[ $installedMajor != $pkgMajor || $installedMinor != $pkgMinor ]]; then
522 echo "Server not upgraded yet" >&2
523 exit
524 fi
525
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 };
535
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 };
543
544 meta.maintainers = with lib.maintainers; [
545 greizgh
546 schmittlauch
547 ];
548}