{ config, pkgs, lib, ...}: with lib; let cfg = config.services.seafile-server; seafileConfigFile = pkgs.writeText "seafile.conf" (generators.toINI {} cfg.seafileSettings); ccnetConfigFile = pkgs.writeText "ccnet.conf" (generators.toINI {} cfg.ccnetSettings); gunicornConfigFile = pkgs.writeText "gunicorn.conf.py" '' import os daemon = True workers = 5 # default localhost:8000 bind = "127.0.0.1:8000" # Pid pids_dir = '${cfg.storagePath}/pids' pidfile = os.path.join(pids_dir, 'seahub.pid') # for file upload, we need a longer timeout value (default is only 30s, too short) timeout = 1200 limit_request_line = 8190 ''; seahubConfigFile = pkgs.writeText "seahub_settings.py" '' SECRET_KEY = #seckey# DATABASES = { 'default': { 'ENGINE': 'django.db.backends.${if cfg.db.type == "mysql" then "mysql" else abort "invalid db type"}', 'NAME': '${cfg.db.dbnameSeahub}', 'USER': '${cfg.db.user}', 'PASSWORD': '#dbpass#', 'HOST': '${cfg.db.host}', 'PORT': '${toString cfg.db.port}' } } ''; # fix permissions at start in { options.services.seafile-server = { enable = mkEnableOption "Seafile server"; storagePath = mkOption { type = types.path; default = "/srv/seafile"; description = "where to store uploaded file data"; }; ccnetSettings = mkOption { type = with types; attrsOf (attrsOf (oneOf [ bool int str ])); default = {}; description = '' all possible ccnet.conf settings ''; }; seafileSettings = mkOption { type = with types; attrsOf (attrsOf (oneOf [ bool int str ])); default = {}; description = '' all possible seafile.conf settings ''; }; autorun = mkOption { type = types.bool; default = true; description = "enable the seafile-server service to get started automatically"; }; db = { type = mkOption { type = types.enum ["sqlite" "mysql"]; default = "sqlite"; description = "database backend type"; }; user = mkOption { type = types.nullOr types.str; default = "seafile"; description = "Database user name. Not required for sqlite."; }; dbnameSeafile = mkOption { type = types.nullOr types.str; default = "seafile"; description = "Database name for Seafile server. Not required for sqlite."; }; dbnameCcnet = mkOption { type = types.nullOr types.str; default = "seafile"; description = "Database name for Ccnet server. Not required for sqlite."; }; dbnameSeahub = mkOption { type = types.nullOr types.str; default = "seafile"; description = "Database name for Seahub web interface. Not required for sqlite."; }; passwordFile = mkOption { type = types.nullOr types.str; default = null; description = '' The full path to a file that contains the database password. Not required for sqlite. ''; }; host = mkOption { type = types.nullOr types.str; default = "localhost"; description = "Database host."; }; port = mkOption { type = with types; nullOr (either int str); default = 3306; description = "Database port. Not required for sqlite."; }; }; user = mkOption { type = types.str; default = "seafile"; description = "User account under which the Seafile server runs."; }; group = mkOption { type = types.str; default = "seafile"; description = "Group account under which the Seafile server runs."; }; name = mkOption { type = types.str; default = "Seafile"; description = "name of the Seafile instance, will show up in client and web interface"; }; domainName = mkOption { type = types.str; description = "full domain name of the seafile instance"; }; # ccnetPort = mkOption { # type = types.int; # default = 10001; # description = "listening port for ccnet server"; # }; seafilePort = mkOption { type = types.int; default = 12001; description = "listening port for Seafile server"; }; fileserverPort = mkOption { type = types.int; default = 8082; description = "listening port for Seafile's fileserver component"; }; seahubPort = mkOption { type = types.int; default = 443; description = "listening http port for Seahub web interface"; }; externalPort = mkOption { type = types.int; default = 443; description = "external port under which Seahub is reachable from the outside. Possibly the external port of a reverse proxy like nginx."; }; enableTLS = mkEnableOption { default = true; description = "whether to use TLS and HTTPS for communication"; }; openFirewall = mkEnableOption { default = true; description = "whether to open up the firewall ports for ccnet, seafile-server and seahub"; }; defaultQuota = mkOption { type = types.nullOr types.int; default = null; description = "default quota to be set per user, in GB"; }; trashExpirationTime = mkOption { type = types.int; default = 30; description = "default time for automatic library trash cleanup"; }; fileRevisionHistoryDays = mkOption { type = types.nullOr types.int; default = null; description = "default history length of file revisions to keep in days. null means keeping all revisions."; }; fileserverBindAddress = mkOption { type = types.str; default = "0.0.0.0"; description = "bind address for fileserver, binds to all addresses by default"; }; fileserverWorkers = mkOption { type = types.int; default = 10; description = "number of worker threads to server http requests"; }; fileserverIndexers = mkOption { type = types.int; default = 1; description = "number of threads used to sequentially divide uploaded files into blocks for storage"; }; fileserverBlockSize = mkOption { type = types.int; default = 1; description = "size of blocks that uploaded files are divided into, in MB"; }; }; config = let directoriesToManage = [ cfg.storagePath ]; in mkIf cfg.enable { services.seafile-server.ccnetSettings = { # TODO: ID and NAME might be required General.SERVICE_URL="http${if cfg.enableTLS then "s" else ""}://${cfg.domainName}:${toString cfg.externalPort}/"; Database = mkMerge [ { ENGINE = cfg.db.type; } (mkIf (cfg.db.type == "mysql") { HOST = cfg.db.host; PORT = cfg.db.port; USER = cfg.db.user; CONNECTION_CHARSET = "utf8"; DB = cfg.db.dbnameCcnet; password = "#dbpass#"; }) ]; }; services.seafile-server.seafileSettings = { library_trash.expire_days = cfg.trashExpirationTime; fileserver = { host = cfg.fileserverBindAddress; port = cfg.fileserverPort; worker_threads = cfg.fileserverWorkers; max_indexing_threads = cfg.fileserverIndexers; fixed_block_size = cfg.fileserverBlockSize; }; quota = mkIf (! isNull cfg.defaultQuota) { default = cfg.defaultQuota; }; history = mkIf (! isNull cfg.fileRevisionHistoryDays) { keep_days = cfg.fileRevisionHistoryDays; }; database = mkMerge [ { type = cfg.db.type; } # while just using the cfg.db set directly might be possible and # save lines of code, I prefer hand-picking options (mkIf (cfg.db.type == "mysql") { host = cfg.db.host; port = cfg.db.port; user = cfg.db.user; connection_charset = "utf8"; db_name = cfg.db.dbnameSeafile; max_connections = 100; password = "#dbpass#"; }) ]; }; systemd = { # state directory permissions managed by systemd tmpfiles.rules = [ "d ${cfg.storagePath} 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.storagePath}/conf 0700 ${cfg.user} ${cfg.group} -" "d ${cfg.storagePath}/pids 0710 ${cfg.user} ${cfg.group} -" ]; services.seafile-server = { path = with pkgs; [ seafile-server.seafile-server-core ]; script = '' ./seafile-server/seafile-server-latest/bin/seafile-admin start ''; serviceConfig = { ExecStartPre = [ ("+${pkgs.writeScript "seafile-server-preStart-privileged" '' #!${pkgs.runtimeShell} # stuff run as root ''}") ("${pkgs.writeShellScript "seafile-server-preStart-unprivileged" '' # stuff run as seafile user set -ex # seafile.conf generation # move config templates from nix store cp ${ccnetConfigFile} ./conf/ccnet.conf cp ${seafileConfigFile} ./conf/seafile.conf cp ${gunicornConfigFile} ./conf/gunicorn.conf.py cp ${seahubConfigFile} ./conf/seahub_settings.py # seahub secret key if [ ! -e .seahubSecret ]; then ${pkgs.seafile-server.pythonEnv}/bin/python ${pkgs.seafile-server}/seahub/tools/secret_key_generator.py > .seahubSecret chmod 400 .seahubSecret fi SEAHUB_SECRET="$(head -n1 .seahubSecret)" # TODO: check for special characters needing to be escaped sed -e "s,#seckey#,$SEAHUB_SECRET,g" -i ./conf/seahub_settings.py # replace placeholder secrets with real secret read from file #TODO: unset -x to prevent DBPASS from being leaked in journal ${if !(isNull cfg.db.passwordFile) then '' DBPASS="$(head -n1 ${toString cfg.db.passwordFile})" sed -e "s,#dbpass#,$DBPASS,g" -i ./conf/seafile.conf ./conf/ccnet.conf ./conf/seahub_settings.py '' else "" } # initialise db and other things needed at first run if [ -e .initialised ]; then #TODO: db initialisation touch .initialised fi ln -nsf ${pkgs.seafile-server} seafile-server # for determining update version mismatches cp ${pkgs.seafile-server}/installed_version . ''}") ]; User = cfg.user; Group = cfg.group; Type = "oneshot"; WorkingDirectory = cfg.storagePath; }; enable = cfg.autorun; wantedBy = [ "multi-user.target" ]; }; }; users.users.${cfg.user} = { home = "${cfg.storagePath}/home"; group = cfg.group; # don't make NixOS create the home directory as otherwise the permissions for /srv might be 0700, # making it impossible to cd into the storagePath createHome = false; isNormalUser = false; }; users.groups.${cfg.group}.members = [ cfg.user ]; # ToDo: make sure ccnet is reachable networking.firewall.allowedTCPPorts = with cfg; if openFirewall then [ seafilePort fileserverPort ] else []; }; }