diff --git a/mod-seafile-server.nix b/mod-seafile-server.nix index 68697ae..f5ae079 100644 --- a/mod-seafile-server.nix +++ b/mod-seafile-server.nix @@ -2,6 +2,40 @@ 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 { @@ -12,6 +46,20 @@ in 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; @@ -28,38 +76,40 @@ in default = "seafile"; description = "Database user name. Not required for sqlite."; }; - dbname = mkOption { + dbnameSeafile = mkOption { type = types.nullOr types.str; default = "seafile"; - description = "Database name. Not required for sqlite."; + description = "Database name for Seafile server. Not required for sqlite."; }; - password = mkOption { + 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 = '' - 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."; - }; + 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; @@ -73,11 +123,11 @@ in 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"; -# }; + 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; @@ -172,16 +222,68 @@ in 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 0750 ${cfg.user} ${cfg.group} -" - "d ${cfg.storagePath}/home 0710 ${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.ccnet-server seafile-server.seafile-server-core ]; + path = with pkgs; [ seafile-server.seafile-server-core ]; script = '' ./seafile-server/seafile-server-latest/bin/seafile-admin start ''; @@ -193,66 +295,46 @@ in ''}") ("${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 -H 'TEMPLATEHOST' - 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 - # outside URL - SERVICE_URL = http${if cfg.enableTLS then "s" else ""}://${cfg.domainName}:${toString cfg.externalPort} + set -ex # seafile.conf generation - echo '[library_trash] - expire_days ${toString cfg.trashExpirationTime} + # move config templates from nix store + ${pkgs.coreutils}/bin/install ${ccnetConfigFile} ./conf/ccnet.conf + ${pkgs.coreutils}/bin/install ${seafileConfigFile} ./conf/seafile.conf + ${pkgs.coreutils}/bin/install ${gunicornConfigFile} ./conf/gunicorn.conf.py + ${pkgs.coreutils}/bin/install ${seahubConfigFile} ./conf/seahub_settings.py - [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 + # 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 - if [ ${toString (! isNull cfg.defaultQuota)} ]; then - echo '[quota]' >> ./conf/seafile.conf - echo 'default = ${toString cfg.defaultQuota}' >> ./conf/seafile.conf + # 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 - 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 + ln -nsf ${pkgs.seafile-server} seafile-server + + # for determining update version mismatches + ${pkgs.coreutils}/bin/install ${pkgs.seafile-server}/installed_version . ''}") ]; User = cfg.user; diff --git a/seafile-nixos-tests.nix b/seafile-nixos-tests.nix index d1e404a..ed1122c 100644 --- a/seafile-nixos-tests.nix +++ b/seafile-nixos-tests.nix @@ -15,7 +15,7 @@ import () { (import ./default.nix) ]; - i18n.consoleKeyMap = "de"; + console.keyMap = "de"; users.mutableUsers = false; users.users.test = { isNormalUser = true; diff --git a/seafile-server/default.nix b/seafile-server/default.nix index 02868fb..2f4d32b 100644 --- a/seafile-server/default.nix +++ b/seafile-server/default.nix @@ -28,7 +28,7 @@ , python3Packages }: let - version = "8.0.0"; + version = "8.0.3"; python = python3; pythonPackages = python3Packages; django = pythonPackages.django; @@ -49,6 +49,8 @@ let pycryptodome ] ++ map (p: p.override { inherit django; }) djangoModules; # build django modules with required version + # defining them here to be able to expose them in a python environment as well + pythonEnvDeps = seahubPythonDependencies ++ [ libsearpc ]; seafile-server-core = stdenv.mkDerivation rec { name = "seafile-server-core"; inherit version; @@ -56,7 +58,7 @@ let owner = "haiwen"; repo = "seafile-server"; rev = "v${version}-server"; - sha256 = "0pd1zjsw6lkpxd54ln0dz5r9zx9585nib10kvpl1vgzp61g4d223"; + sha256 = "1wmbx4smf342b5pars1zm9af2i0yaq7kjj7ry0gr337gdpa4qn3b"; }; # patch to work with latest, non-vulnerable libevhtp patches = [ @@ -66,7 +68,14 @@ let # `which` is called directly from python during buildPhase, so we need the binary nativeBuildInputs = [ autoconf automake libtool pkgconfig vala autoreconfHook which pythonPackages.wrapPython ]; buildInputs = [ sqlite glib python libuuid openssl oniguruma fuse libarchive libevent libevhtp ]; - propagatedBuildInputs = [ libsearpc ] ++ seahubPythonDependencies; + propagatedBuildInputs = pythonEnvDeps; + # copy manual to required location + postInstall = '' + mkdir $out/doc + cp ${src}/doc/*.doc $out/doc/ + ''; + # prevent doc directory from being moved to share in fixupPhase + forceShare = [ "man" "info" ]; postFixup = '' buildPythonPath $propagatedBuildInputs wrapPythonProgramsIn "$out/bin" "$out $pythonPath" @@ -85,7 +94,7 @@ let owner = "haiwen"; repo = "seahub"; rev = "v${version}-server"; - sha256 = "0j7g43j7w1zb00pg4aaacdv5ycva3qf561hj9pbwh4709mbiykip"; + sha256 = "0vfkiavsmpjm6wjr5rcnmnpnb3rxr3svwk8fsh5c76zg87ckdz4d"; }; phases = [ "unpackPhase" "installPhase" "fixupPhase" "distPhase" ]; buildInputs = [ python pythonPackages.wrapPython ]; @@ -120,19 +129,40 @@ stdenv.mkDerivation { name = "seafile-server"; inherit version; + nativeBuildInputs = [ python3Packages.wrapPython ]; buildInputs = [ seahub seafile-server-core libsearpc ] ++ lib.optional withMysql libmysqlclient; phases = [ "installPhase" "fixupPhase" "distPhase" ]; - # todo: create data directory in /srv in activation script + # create required directory structure + # Which files need to be copied is specified in the function `copy_scripts_and_libs` + # of ${seafile-server-core.src}/scripts/build/build-server.py + # The install script below has been hand crafted from that list of files and needs to be updated on new releases. installPhase = '' mkdir "$out" cd "$out" ln -s ${seahub} seahub - ln -s ${seafile-server-core} seafile-server-latest + ln -s ${seafile-server-core} seafile-server + # copy general scripts + cp ${seafile-server-core.src}/scripts/{setup-seafile.sh,setup-seafile-mysql.sh,setup-seafile-mysql.py,seafile.sh,seahub.sh,reset-admin.sh,seaf-fuse.sh,check_init_admin.py,seaf-gc.sh,seaf-fsck.sh} . + # copy update scripts (and their sql) + cp -r ${seafile-server-core.src}/scripts/upgrade . + cp -r ${seafile-server-core.src}/scripts/sql . + # copy_user_manual is already done in the postInstall hook of seafile-server-core + # python admin scripts need to be made executable and patched with python path + chmod ugo+x *.py + buildPythonPath $propagatedBuildInputs + wrapPythonProgramsIn "$out/*.py" "$out $pythonPath" + + echo -n "${version}" > installed_version ''; meta = with lib; { maintainers = with maintainers; [ schmittlauch ]; license = licenses.free; # components with different free software licenses are combined }; inherit seafile-server-core seahub;# for using the path in the NixOS module + + pythonEnv = python3.buildEnv.override { + extraLibs = pythonEnvDeps; + ignoreCollisions = true; + }; } diff --git a/seafile-test.nix b/seafile-test.nix index cf624c2..62aeded 100644 --- a/seafile-test.nix +++ b/seafile-test.nix @@ -10,13 +10,13 @@ (import ./default.nix) ]; - i18n.consoleKeyMap = "de"; + console.keyMap = "de"; users.mutableUsers = false; users.users.test = { isNormalUser = true; extraGroups = [ "wheel" ]; #hashedPassword = "$6$SZCzE/xB$Hr9sfsJ7xAcBCoptG39cxxQk8RZfldDjjGpSngOvn9Ufex5dHBEbdncXRZnfrGATsGcYPvLi7m4wIu.f8tY9B."; - password = ""; + password = "test"; home = "/home/test"; createHome = true; }; @@ -25,7 +25,33 @@ services.seafile-server = { enable = true; #autorun = false; - domainName = "localhost"; + domainName = "seaf.local"; + db = { + type = "mysql"; + passwordFile = toString (pkgs.writeText "testPW" "test"); + }; }; + # db backend + services.mysql = + { + enable = true; + package = pkgs.mariadb; + ensureDatabases = [ "ccnet" "seafile" "seahub" ]; + ensureUsers = [ + rec { + name = config.services.seafile-server.db.user; + ensurePermissions = { + "ccnet.*" = "ALL PRIVILEGES"; + "seafile.*" = "ALL PRIVILEGES"; + "seahub.*" = "ALL PRIVILEGES"; + }; + } + ]; + # set a password for the seafile user + initialScript = pkgs.writeText "mariadb-init.sql" '' + CREATE USER ${config.services.seafile-server.db.user}@localhost IDENTIFIED VIA mysql_native_password USING PASSWORD("test"); + ''; + }; + }