{ config, pkgs, lib, ...}: with lib; let cfg = config.services.seafile-server; # 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"; }; 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."; }; dbname = mkOption { type = types.nullOr types.str; default = "seafile"; description = "Database name. Not required for sqlite."; }; password = mkOption { type = types.nullOr types.str; default = null; description = '' Database password. Use passwordFile to avoid this being world-readable in the /nix/store. 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. ''; }; host = mkOption { type = types.nullOr types.str; default = "localhost"; description = "Database host."; }; dbport = 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 { systemd = { # state directory permissions managed by systemd tmpfiles.rules = [ "d ${cfg.storagePath} 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.storagePath}/conf 0750 ${cfg.user} ${cfg.group} -" "d ${cfg.storagePath}/home 0710 ${cfg.user} ${cfg.group} -" ]; services.seafile-server = { path = with pkgs; [ seafile-server.ccnet-server 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 # ccnet-init must only be run once per installation, as it also generates stateful key and ID # solution: invoke it once, use result as template if [ ! -e ./ccnet/mykey.peer ]; then ${pkgs.seafile-server.ccnet-server}/bin/ccnet-init -c ./ccnet -n 'TEMPLATENAME' -H 'TEMPLATEHOST' -P 'TEMPLATEPORT' mv ./ccnet/ccnet.conf{,.template} fi # generate actual ccnet config file echo "[General]" > ./conf/ccnet.conf grep "^ID =" ./ccnet/ccnet.conf.template >> ./conf/ccnet.conf echo 'USER_NAME = ${cfg.name} NAME = ${cfg.name} # outside URL SERVICE_URL = http${if cfg.enableTLS then "s" else ""}://${cfg.domainName}:${toString cfg.externalPort} [Network] Port = ${toString cfg.ccnetPort}' >> ./conf/ccnet.conf # seafile.conf generation echo '[library_trash] expire_days ${toString cfg.trashExpirationTime} [fileserver] host = ${cfg.fileserverBindAddress} port = ${toString cfg.fileserverPort} worker_threads = ${toString cfg.fileserverWorkers} max_indexing_threads = ${toString cfg.fileserverIndexers} fixed_block_size = ${toString cfg.fileserverIndexers}' > ./conf/seafile.conf if [ ${toString (! isNull cfg.defaultQuota)} ]; then echo '[quota]' >> ./conf/seafile.conf echo 'default = ${toString cfg.defaultQuota}' >> ./conf/seafile.conf fi if [ ${toString (! isNull cfg.fileRevisionHistoryDays)} ]; then echo '[history]' >> ./conf/seafile.conf echo 'keep_days = ${toString cfg.defaultQuota}' >> ./conf/seafile.conf fi # seafile database settings if [ ${cfg.db.type} = "mysql" ]; then echo '[database] type = mysql host = ${cfg.db.host} port = ${toString cfg.db.dbport} user = ${cfg.db.user} connection_charset = utf8 db_name = ${cfg.db.dbname} max_connections = 100' >> ./conf/seafile.conf if [ ${toString (! isNull cfg.db.password)}; then echo 'password = ${toString cfg.db.password}' >> ./conf/seafile.conf else echo "password = $(cat ${toString cfg.db.passwordFile})" >> ./conf/seafile.conf fi else echo '[database] type = sqlite' >> ./conf/seafile.conf fi ln -s ${pkgs.seafile-server} seafile-server ./seafile-server/seafile-server-latest/bin/seafile-admin setup ''}") ]; 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 ]; networking.firewall.allowedTCPPorts = with cfg; if openFirewall then [ ccnetPort seafilePort fileserverPort ] else []; }; }