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");
+ '';
+ };
+
}