at 25.11-pre 20 kB view raw
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}