diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
index cbcc3cb7cfcd..842bb8737632 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
@@ -357,6 +357,14 @@
services.multipath.
+
+
+ seafile,
+ an open source file syncing & sharing software. Available
+ as
+ services.seafile.
+
+
diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md
index 982f87daecdc..b0a9e2e34ba3 100644
--- a/nixos/doc/manual/release-notes/rl-2111.section.md
+++ b/nixos/doc/manual/release-notes/rl-2111.section.md
@@ -110,6 +110,8 @@ In addition to numerous new and upgraded packages, this release has the followin
- [multipath](https://github.com/opensvc/multipath-tools), the device mapper multipath (DM-MP) daemon. Available as [services.multipath](#opt-services.multipath.enable).
+- [seafile](https://www.seafile.com/en/home/), an open source file syncing & sharing software. Available as [services.seafile](options.html#opt-services.seafile.enable).
+
## Backward Incompatibilities {#sec-release-21.11-incompatibilities}
- The `services.wakeonlan` option was removed, and replaced with `networking.interfaces..wakeOnLan`.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index df35a57d047f..e4805ad5d6fa 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -836,6 +836,7 @@
./services/networking/rpcbind.nix
./services/networking/rxe.nix
./services/networking/sabnzbd.nix
+ ./services/networking/seafile.nix
./services/networking/searx.nix
./services/networking/skydns.nix
./services/networking/shadowsocks.nix
diff --git a/nixos/modules/services/networking/seafile.nix b/nixos/modules/services/networking/seafile.nix
new file mode 100644
index 000000000000..856797b6b020
--- /dev/null
+++ b/nixos/modules/services/networking/seafile.nix
@@ -0,0 +1,290 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+ python = pkgs.python3Packages.python;
+ cfg = config.services.seafile;
+ settingsFormat = pkgs.formats.ini { };
+
+ ccnetConf = settingsFormat.generate "ccnet.conf" cfg.ccnetSettings;
+
+ seafileConf = settingsFormat.generate "seafile.conf" cfg.seafileSettings;
+
+ seahubSettings = pkgs.writeText "seahub_settings.py" ''
+ FILE_SERVER_ROOT = '${cfg.ccnetSettings.General.SERVICE_URL}/seafhttp'
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': '${seahubDir}/seahub.db',
+ }
+ }
+ MEDIA_ROOT = '${seahubDir}/media/'
+ THUMBNAIL_ROOT = '${seahubDir}/thumbnail/'
+
+ with open('${seafRoot}/.seahubSecret') as f:
+ SECRET_KEY = f.readline().rstrip()
+
+ ${cfg.seahubExtraConf}
+ '';
+
+ seafRoot = "/var/lib/seafile"; # hardcode it due to dynamicuser
+ ccnetDir = "${seafRoot}/ccnet";
+ dataDir = "${seafRoot}/data";
+ seahubDir = "${seafRoot}/seahub";
+
+in {
+
+ ###### Interface
+
+ options.services.seafile = {
+ enable = mkEnableOption "Seafile server";
+
+ ccnetSettings = mkOption {
+ type = types.submodule {
+ freeformType = settingsFormat.type;
+
+ options = {
+ General = {
+ SERVICE_URL = mkOption {
+ type = types.str;
+ example = "https://www.example.com";
+ description = ''
+ Seahub public URL.
+ '';
+ };
+ };
+ };
+ };
+ default = { };
+ description = ''
+ Configuration for ccnet, see
+
+ for supported values.
+ '';
+ };
+
+ seafileSettings = mkOption {
+ type = types.submodule {
+ freeformType = settingsFormat.type;
+
+ options = {
+ fileserver = {
+ port = mkOption {
+ type = types.port;
+ default = 8082;
+ description = ''
+ The tcp port used by seafile fileserver.
+ '';
+ };
+ host = mkOption {
+ type = types.str;
+ default = "127.0.0.1";
+ example = "0.0.0.0";
+ description = ''
+ The binding address used by seafile fileserver.
+ '';
+ };
+ };
+ };
+ };
+ default = { };
+ description = ''
+ Configuration for seafile-server, see
+
+ for supported values.
+ '';
+ };
+
+ workers = mkOption {
+ type = types.int;
+ default = 4;
+ example = 10;
+ description = ''
+ The number of gunicorn worker processes for handling requests.
+ '';
+ };
+
+ adminEmail = mkOption {
+ example = "john@example.com";
+ type = types.str;
+ description = ''
+ Seafile Seahub Admin Account Email.
+ '';
+ };
+
+ initialAdminPassword = mkOption {
+ example = "someStrongPass";
+ type = types.str;
+ description = ''
+ Seafile Seahub Admin Account initial password.
+ Should be change via Seahub web front-end.
+ '';
+ };
+
+ seafilePackage = mkOption {
+ type = types.package;
+ description = "Which package to use for the seafile server.";
+ default = pkgs.seafile-server;
+ };
+
+ seahubExtraConf = mkOption {
+ default = "";
+ type = types.lines;
+ description = ''
+ Extra config to append to `seahub_settings.py` file.
+ Refer to
+ for all available options.
+ '';
+ };
+ };
+
+ ###### Implementation
+
+ config = mkIf cfg.enable {
+
+ environment.etc."seafile/ccnet.conf".source = ccnetConf;
+ environment.etc."seafile/seafile.conf".source = seafileConf;
+ environment.etc."seafile/seahub_settings.py".source = seahubSettings;
+
+ systemd.targets.seafile = {
+ wantedBy = [ "multi-user.target" ];
+ description = "Seafile components";
+ };
+
+ systemd.services = let
+ securityOptions = {
+ ProtectHome = true;
+ PrivateUsers = true;
+ PrivateDevices = true;
+ ProtectClock = true;
+ ProtectHostname = true;
+ ProtectProc = "invisible";
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectKernelLogs = true;
+ ProtectControlGroups = true;
+ RestrictNamespaces = true;
+ LockPersonality = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ MemoryDenyWriteExecute = true;
+ SystemCallArchitectures = "native";
+ RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" ];
+ };
+ in {
+ seaf-server = {
+ description = "Seafile server";
+ partOf = [ "seafile.target" ];
+ after = [ "network.target" ];
+ wantedBy = [ "seafile.target" ];
+ restartTriggers = [ ccnetConf seafileConf ];
+ serviceConfig = securityOptions // {
+ User = "seafile";
+ Group = "seafile";
+ DynamicUser = true;
+ StateDirectory = "seafile";
+ RuntimeDirectory = "seafile";
+ LogsDirectory = "seafile";
+ ConfigurationDirectory = "seafile";
+ ExecStart = ''
+ ${cfg.seafilePackage}/bin/seaf-server \
+ --foreground \
+ -F /etc/seafile \
+ -c ${ccnetDir} \
+ -d ${dataDir} \
+ -l /var/log/seafile/server.log \
+ -P /run/seafile/server.pid \
+ -p /run/seafile
+ '';
+ };
+ preStart = ''
+ if [ ! -f "${seafRoot}/server-setup" ]; then
+ mkdir -p ${dataDir}/library-template
+ mkdir -p ${ccnetDir}/{GroupMgr,misc,OrgMgr,PeerMgr}
+ ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/GroupMgr/groupmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/groupmgr.sql"
+ ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/misc/config.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/config.sql"
+ ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/OrgMgr/orgmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/org.sql"
+ ${pkgs.sqlite}/bin/sqlite3 ${ccnetDir}/PeerMgr/usermgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/user.sql"
+ ${pkgs.sqlite}/bin/sqlite3 ${dataDir}/seafile.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/seafile.sql"
+ echo "${cfg.seafilePackage.version}-sqlite" > "${seafRoot}"/server-setup
+ fi
+ # checking for upgrades and handling them
+ # WARNING: needs to be extended to actually handle major version migrations
+ installedMajor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f1)
+ installedMinor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f2)
+ pkgMajor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f1)
+ pkgMinor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f2)
+ if [ $installedMajor != $pkgMajor ] || [ $installedMinor != $pkgMinor ]; then
+ echo "Unsupported upgrade" >&2
+ exit 1
+ fi
+ '';
+ };
+
+ seahub = let
+ penv = (pkgs.python3.withPackages (ps: with ps; [ gunicorn seahub ]));
+ in {
+ description = "Seafile Server Web Frontend";
+ wantedBy = [ "seafile.target" ];
+ partOf = [ "seafile.target" ];
+ after = [ "network.target" "seaf-server.service" ];
+ requires = [ "seaf-server.service" ];
+ restartTriggers = [ seahubSettings ];
+ environment = {
+ PYTHONPATH =
+ "${pkgs.python3Packages.seahub}/thirdpart:${pkgs.python3Packages.seahub}:${penv}/${python.sitePackages}";
+ DJANGO_SETTINGS_MODULE = "seahub.settings";
+ CCNET_CONF_DIR = ccnetDir;
+ SEAFILE_CONF_DIR = dataDir;
+ SEAFILE_CENTRAL_CONF_DIR = "/etc/seafile";
+ SEAFILE_RPC_PIPE_PATH = "/run/seafile";
+ SEAHUB_LOG_DIR = "/var/log/seafile";
+ };
+ serviceConfig = securityOptions // {
+ User = "seafile";
+ Group = "seafile";
+ DynamicUser = true;
+ RuntimeDirectory = "seahub";
+ StateDirectory = "seafile";
+ LogsDirectory = "seafile";
+ ConfigurationDirectory = "seafile";
+ ExecStart = ''
+ ${penv}/bin/gunicorn seahub.wsgi:application \
+ --name seahub \
+ --workers ${toString cfg.workers} \
+ --log-level=info \
+ --preload \
+ --timeout=1200 \
+ --limit-request-line=8190 \
+ --bind unix:/run/seahub/gunicorn.sock
+ '';
+ };
+ preStart = ''
+ mkdir -p ${seahubDir}/media
+ # Link all media except avatars
+ for m in `find ${pkgs.python3Packages.seahub}/media/ -maxdepth 1 -not -name "avatars"`; do
+ ln -sf $m ${seahubDir}/media/
+ done
+ if [ ! -e "${seafRoot}/.seahubSecret" ]; then
+ ${penv}/bin/python ${pkgs.python3Packages.seahub}/tools/secret_key_generator.py > ${seafRoot}/.seahubSecret
+ chmod 400 ${seafRoot}/.seahubSecret
+ fi
+ if [ ! -f "${seafRoot}/seahub-setup" ]; then
+ # avatars directory should be writable
+ install -D -t ${seahubDir}/media/avatars/ ${pkgs.python3Packages.seahub}/media/avatars/default.png
+ install -D -t ${seahubDir}/media/avatars/groups ${pkgs.python3Packages.seahub}/media/avatars/groups/default.png
+ # init database
+ ${pkgs.python3Packages.seahub}/manage.py migrate
+ # create admin account
+ ${pkgs.expect}/bin/expect -c 'spawn ${pkgs.python3Packages.seahub}/manage.py createsuperuser --email=${cfg.adminEmail}; expect "Password: "; send "${cfg.initialAdminPassword}\r"; expect "Password (again): "; send "${cfg.initialAdminPassword}\r"; expect "Superuser created successfully."'
+ echo "${pkgs.python3Packages.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
+ fi
+ if [ $(cat "${seafRoot}/seahub-setup" | cut -d"-" -f1) != "${pkgs.python3Packages.seahub.version}" ]; then
+ # update database
+ ${pkgs.python3Packages.seahub}/manage.py migrate
+ echo "${pkgs.python3Packages.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
+ fi
+ '';
+ };
+ };
+ };
+}
diff --git a/nixos/tests/seafile.nix b/nixos/tests/seafile.nix
new file mode 100644
index 000000000000..17862cff189e
--- /dev/null
+++ b/nixos/tests/seafile.nix
@@ -0,0 +1,123 @@
+import ./make-test-python.nix ({ pkgs, ... }:
+ let
+ client = { config, pkgs, ... }: {
+ virtualisation.memorySize = 256;
+ environment.systemPackages = [ pkgs.seafile-shared pkgs.curl ];
+ };
+ in {
+ name = "seafile";
+ meta = with pkgs.stdenv.lib.maintainers; {
+ maintainers = [ kampfschlaefer schmittlauch ];
+ };
+
+ nodes = {
+ server = { config, pkgs, ... }: {
+ virtualisation.memorySize = 512;
+ services.seafile = {
+ enable = true;
+ ccnetSettings.General.SERVICE_URL = "http://server";
+ adminEmail = "admin@example.com";
+ initialAdminPassword = "seafile_password";
+ };
+ services.nginx = {
+ enable = true;
+ virtualHosts."server" = {
+ locations."/".proxyPass = "http://unix:/run/seahub/gunicorn.sock";
+ locations."/seafhttp" = {
+ proxyPass = "http://127.0.0.1:8082";
+ extraConfig = ''
+ rewrite ^/seafhttp(.*)$ $1 break;
+ client_max_body_size 0;
+ proxy_connect_timeout 36000s;
+ proxy_read_timeout 36000s;
+ proxy_send_timeout 36000s;
+ send_timeout 36000s;
+ proxy_http_version 1.1;
+ '';
+ };
+ };
+ };
+ networking.firewall = { allowedTCPPorts = [ 80 ]; };
+ };
+ client1 = client pkgs;
+ client2 = client pkgs;
+ };
+
+ testScript = ''
+ start_all()
+
+ with subtest("start seaf-server"):
+ server.wait_for_unit("seaf-server.service")
+ server.wait_for_file("/run/seafile/seafile.sock")
+
+ with subtest("start seahub"):
+ server.wait_for_unit("seahub.service")
+ server.wait_for_unit("nginx.service")
+ server.wait_for_file("/run/seahub/gunicorn.sock")
+
+ with subtest("client1 fetch seahub page"):
+ client1.succeed("curl -L http://server | grep 'Log In' >&2")
+
+ with subtest("client1 connect"):
+ client1.wait_for_unit("default.target")
+ client1.succeed("seaf-cli init -d . >&2")
+ client1.succeed("seaf-cli start >&2")
+ client1.succeed(
+ "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password >&2"
+ )
+
+ libid = client1.succeed(
+ 'seaf-cli create -s http://server -n test01 -u admin\@example.com -p seafile_password -t "first test library"'
+ ).strip()
+
+ client1.succeed(
+ "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test01"
+ )
+ client1.fail(
+ "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test02"
+ )
+
+ client1.succeed(
+ f"seaf-cli download -l {libid} -s http://server -u admin\@example.com -p seafile_password -d . >&2"
+ )
+
+ client1.sleep(3)
+
+ client1.succeed("seaf-cli status |grep synchronized >&2")
+
+ client1.succeed("ls -la >&2")
+ client1.succeed("ls -la test01 >&2")
+
+ client1.execute("echo bla > test01/first_file")
+
+ client1.sleep(2)
+
+ client1.succeed("seaf-cli status |grep synchronized >&2")
+
+ with subtest("client2 sync"):
+ client2.wait_for_unit("default.target")
+
+ client2.succeed("seaf-cli init -d . >&2")
+ client2.succeed("seaf-cli start >&2")
+
+ client2.succeed(
+ "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password >&2"
+ )
+
+ libid = client2.succeed(
+ "seaf-cli list-remote -s http://server -u admin\@example.com -p seafile_password |grep test01 |cut -d' ' -f 2"
+ ).strip()
+
+ client2.succeed(
+ f"seaf-cli download -l {libid} -s http://server -u admin\@example.com -p seafile_password -d . >&2"
+ )
+
+ client2.sleep(3)
+
+ client2.succeed("seaf-cli status |grep synchronized >&2")
+
+ client2.succeed("ls -la test01 >&2")
+
+ client2.succeed('[ `cat test01/first_file` = "bla" ]')
+ '';
+ })