8 cfg = config.services.seafile;
9 settingsFormat = pkgs.formats.ini { };
11 ccnetConf = settingsFormat.generate "ccnet.conf" (
12 lib.attrsets.recursiveUpdate {
15 UNIX_SOCKET = "/var/run/mysqld/mysqld.sock";
17 CONNECTION_CHARSET = "utf8";
22 seafileConf = settingsFormat.generate "seafile.conf" (
23 lib.attrsets.recursiveUpdate {
26 unix_socket = "/var/run/mysqld/mysqld.sock";
27 db_name = "seafile_db";
28 connection_charset = "utf8";
33 seahubSettings = pkgs.writeText "seahub_settings.py" ''
34 FILE_SERVER_ROOT = '${cfg.ccnetSettings.General.SERVICE_URL}/seafhttp'
37 'ENGINE': 'django.db.backends.mysql',
39 'HOST' : '/var/run/mysqld/mysqld.sock',
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}
55 seafRoot = "/var/lib/seafile";
56 ccnetDir = "${seafRoot}/ccnet";
57 seahubDir = "${seafRoot}/seahub";
58 defaultUser = "seafile";
65 options.services.seafile = with lib; {
66 enable = mkEnableOption "Seafile server";
68 ccnetSettings = mkOption {
69 type = types.submodule {
70 freeformType = settingsFormat.type;
74 SERVICE_URL = mkOption {
75 type = types.singleLineStr;
76 example = "https://www.example.com";
86 Configuration for ccnet, see
87 <https://manual.seafile.com/config/ccnet-conf/>
92 seafileSettings = mkOption {
93 type = types.submodule {
94 freeformType = settingsFormat.type;
102 The tcp port used by seafile fileserver.
106 type = types.singleLineStr;
107 default = "ipv4:127.0.0.1";
108 example = "unix:/run/seafile/server.sock";
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.
124 Configuration for seafile-server, see
125 <https://manual.seafile.com/config/seafile-conf/>
126 for supported values.
130 seahubAddress = mkOption {
131 type = types.singleLineStr;
132 default = "unix:/run/seahub/gunicorn.sock";
133 example = "[::1]:8083";
135 Which address to bind the seahub server to, of the form:
139 IPv6 HOSTs must be wrapped in brackets.
148 The number of gunicorn worker processes for handling requests.
152 adminEmail = mkOption {
153 example = "john@example.com";
154 type = types.singleLineStr;
156 Seafile Seahub Admin Account Email.
160 initialAdminPassword = mkOption {
161 example = "someStrongPass";
162 type = types.singleLineStr;
164 Seafile Seahub Admin Account initial password.
165 Should be changed via Seahub web front-end.
169 seahubPackage = mkPackageOption pkgs "seahub" { };
172 type = types.singleLineStr;
173 default = defaultUser;
174 description = "User account under which seafile runs.";
178 type = types.singleLineStr;
179 default = defaultUser;
180 description = "Group under which seafile runs.";
185 default = "${seafRoot}/data";
186 description = "Path in which to store user data";
190 enable = mkEnableOption "automatic garbage collection on stored data blocks";
193 type = types.listOf types.singleLineStr;
194 default = [ "Sun 03:00:00" ];
196 When to run garbage collection on stored data blocks.
197 The time format is described in {manpage}`systemd.time(7)`.
201 randomizedDelaySec = mkOption {
203 type = types.singleLineStr;
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)`
213 persistent = mkOption {
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.
230 seahubExtraConf = mkOption {
233 CSRF_TRUSTED_ORIGINS = ["https://example.com"]
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.
244 ###### Implementation
246 config = lib.mkIf cfg.enable {
249 package = lib.mkDefault pkgs.mariadb;
258 ensurePermissions = {
259 "ccnet_db.*" = "ALL PRIVILEGES";
260 "seafile_db.*" = "ALL PRIVILEGES";
261 "seahub_db.*" = "ALL PRIVILEGES";
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) {
278 users.groups = lib.optionalAttrs (cfg.group == defaultUser) { "${defaultUser}" = { }; };
280 systemd.targets.seafile = {
281 wantedBy = [ "multi-user.target" ];
282 description = "Seafile components";
290 PrivateDevices = true;
292 ProtectSystem = "strict";
294 ProtectHostname = true;
295 ProtectProc = "invisible";
296 ProtectKernelModules = true;
297 ProtectKernelTunables = true;
298 ProtectKernelLogs = true;
299 ProtectControlGroups = true;
300 RestrictNamespaces = true;
302 LockPersonality = true;
303 RestrictRealtime = true;
304 RestrictSUIDSGID = true;
305 NoNewPrivileges = true;
306 MemoryDenyWriteExecute = true;
307 SystemCallArchitectures = "native";
308 RestrictAddressFamilies = [
315 StateDirectory = "seafile";
316 RuntimeDirectory = "seafile";
317 LogsDirectory = "seafile";
318 ConfigurationDirectory = "seafile";
319 ReadWritePaths = lib.lists.optional (cfg.dataDir != "${seafRoot}/data") cfg.dataDir;
324 description = "Seafile server";
325 partOf = [ "seafile.target" ];
326 unitConfig.RequiresMountsFor = lib.lists.optional (cfg.dataDir != "${seafRoot}/data") cfg.dataDir;
327 requires = [ "mysql.service" ];
332 wantedBy = [ "seafile.target" ];
337 serviceConfig = serviceOptions // {
339 ${lib.getExe cfg.seahubPackage.seafile-server} \
344 -l /var/log/seafile/server.log \
345 -P /run/seafile/server.pid \
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
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
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
403 echo "Unsupported upgrade: $installedMajor.$installedMinor to $pkgMajor.$pkgMinor" >&2
408 # Fix unix socket permissions
410 lib.strings.optionalString (lib.strings.hasPrefix "unix:" cfg.seafileSettings.fileserver.host) ''
411 while [[ ! -S "${lib.strings.removePrefix "unix:" cfg.seafileSettings.fileserver.host}" ]]; do
414 chmod 666 "${lib.strings.removePrefix "unix:" cfg.seafileSettings.fileserver.host}"
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;
426 "seaf-server.service"
431 "seaf-server.service"
433 restartTriggers = [ seahubSettings ];
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";
443 serviceConfig = serviceOptions // {
444 RuntimeDirectory = "seahub";
446 ${lib.getExe cfg.seahubPackage.python3.pkgs.gunicorn} seahub.wsgi:application \
448 --workers ${toString cfg.workers} \
452 --limit-request-line=8190 \
453 --bind ${cfg.seahubAddress}
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/
462 if [ ! -e "${seafRoot}/.seahubSecret" ]; then
465 ${lib.getExe cfg.seahubPackage.python3} ${cfg.seahubPackage}/tools/secret_key_generator.py > ${seafRoot}/.seahubSecret
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
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"
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"
487 description = "Seafile storage garbage collection";
489 "seaf-server.service"
493 "seaf-server.service"
496 unitConfig.RequiresMountsFor = lib.lists.optional (cfg.dataDir != "${seafRoot}/data") cfg.dataDir;
498 "seaf-server.service"
502 "seaf-server.service"
505 startAt = lib.lists.optionals cfg.gc.enable cfg.gc.dates;
506 serviceConfig = serviceOptions // {
510 if [ ! -f "${seafRoot}/server-setup" ]; then
511 echo "Server not setup yet, GC not needed" >&2
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
526 # Clean up user-deleted blocks and libraries
527 ${cfg.seahubPackage.seafile-server}/bin/seafserv-gc \
536 systemd.timers.seaf-gc = lib.mkIf cfg.gc.enable {
538 RandomizedDelaySec = cfg.gc.randomizedDelaySec;
539 Persistent = cfg.gc.persistent;
544 meta.maintainers = with lib.maintainers; [