1{ config, lib, pkgs, ... }:
2with lib;
3let
4 cfg = config.services.seafile;
5 settingsFormat = pkgs.formats.ini { };
6
7 ccnetConf = settingsFormat.generate "ccnet.conf" cfg.ccnetSettings;
8
9 seafileConf = settingsFormat.generate "seafile.conf" cfg.seafileSettings;
10
11 seahubSettings = pkgs.writeText "seahub_settings.py" ''
12 FILE_SERVER_ROOT = '${cfg.ccnetSettings.General.SERVICE_URL}/seafhttp'
13 DATABASES = {
14 'default': {
15 'ENGINE': 'django.db.backends.sqlite3',
16 'NAME': '${seahubDir}/seahub.db',
17 }
18 }
19 MEDIA_ROOT = '${seahubDir}/media/'
20 THUMBNAIL_ROOT = '${seahubDir}/thumbnail/'
21
22 SERVICE_URL = '${cfg.ccnetSettings.General.SERVICE_URL}'
23
24 with open('${seafRoot}/.seahubSecret') as f:
25 SECRET_KEY = f.readline().rstrip()
26
27 ${cfg.seahubExtraConf}
28 '';
29
30 seafRoot = "/var/lib/seafile"; # hardcode it due to dynamicuser
31 ccnetDir = "${seafRoot}/ccnet";
32 dataDir = "${seafRoot}/data";
33 seahubDir = "${seafRoot}/seahub";
34
35in {
36
37 ###### Interface
38
39 options.services.seafile = {
40 enable = mkEnableOption (lib.mdDoc "Seafile server");
41
42 ccnetSettings = mkOption {
43 type = types.submodule {
44 freeformType = settingsFormat.type;
45
46 options = {
47 General = {
48 SERVICE_URL = mkOption {
49 type = types.str;
50 example = "https://www.example.com";
51 description = lib.mdDoc ''
52 Seahub public URL.
53 '';
54 };
55 };
56 };
57 };
58 default = { };
59 description = lib.mdDoc ''
60 Configuration for ccnet, see
61 <https://manual.seafile.com/config/ccnet-conf/>
62 for supported values.
63 '';
64 };
65
66 seafileSettings = mkOption {
67 type = types.submodule {
68 freeformType = settingsFormat.type;
69
70 options = {
71 fileserver = {
72 port = mkOption {
73 type = types.port;
74 default = 8082;
75 description = lib.mdDoc ''
76 The tcp port used by seafile fileserver.
77 '';
78 };
79 host = mkOption {
80 type = types.str;
81 default = "127.0.0.1";
82 example = "0.0.0.0";
83 description = lib.mdDoc ''
84 The binding address used by seafile fileserver.
85 '';
86 };
87 };
88 };
89 };
90 default = { };
91 description = lib.mdDoc ''
92 Configuration for seafile-server, see
93 <https://manual.seafile.com/config/seafile-conf/>
94 for supported values.
95 '';
96 };
97
98 workers = mkOption {
99 type = types.int;
100 default = 4;
101 example = 10;
102 description = lib.mdDoc ''
103 The number of gunicorn worker processes for handling requests.
104 '';
105 };
106
107 adminEmail = mkOption {
108 example = "john@example.com";
109 type = types.str;
110 description = lib.mdDoc ''
111 Seafile Seahub Admin Account Email.
112 '';
113 };
114
115 initialAdminPassword = mkOption {
116 example = "someStrongPass";
117 type = types.str;
118 description = lib.mdDoc ''
119 Seafile Seahub Admin Account initial password.
120 Should be change via Seahub web front-end.
121 '';
122 };
123
124 seafilePackage = mkOption {
125 type = types.package;
126 description = lib.mdDoc "Which package to use for the seafile server.";
127 default = pkgs.seafile-server;
128 defaultText = literalExpression "pkgs.seafile-server";
129 };
130
131 seahubExtraConf = mkOption {
132 default = "";
133 type = types.lines;
134 description = lib.mdDoc ''
135 Extra config to append to `seahub_settings.py` file.
136 Refer to <https://manual.seafile.com/config/seahub_settings_py/>
137 for all available options.
138 '';
139 };
140 };
141
142 ###### Implementation
143
144 config = mkIf cfg.enable {
145
146 environment.etc."seafile/ccnet.conf".source = ccnetConf;
147 environment.etc."seafile/seafile.conf".source = seafileConf;
148 environment.etc."seafile/seahub_settings.py".source = seahubSettings;
149
150 systemd.targets.seafile = {
151 wantedBy = [ "multi-user.target" ];
152 description = "Seafile components";
153 };
154
155 systemd.services = let
156 securityOptions = {
157 ProtectHome = true;
158 PrivateUsers = true;
159 PrivateDevices = true;
160 ProtectClock = true;
161 ProtectHostname = true;
162 ProtectProc = "invisible";
163 ProtectKernelModules = true;
164 ProtectKernelTunables = true;
165 ProtectKernelLogs = true;
166 ProtectControlGroups = true;
167 RestrictNamespaces = true;
168 LockPersonality = true;
169 RestrictRealtime = true;
170 RestrictSUIDSGID = true;
171 MemoryDenyWriteExecute = true;
172 SystemCallArchitectures = "native";
173 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" ];
174 };
175 in {
176 seaf-server = {
177 description = "Seafile server";
178 partOf = [ "seafile.target" ];
179 after = [ "network.target" ];
180 wantedBy = [ "seafile.target" ];
181 restartTriggers = [ ccnetConf seafileConf ];
182 path = [ pkgs.sqlite ];
183 serviceConfig = securityOptions // {
184 User = "seafile";
185 Group = "seafile";
186 DynamicUser = true;
187 StateDirectory = "seafile";
188 RuntimeDirectory = "seafile";
189 LogsDirectory = "seafile";
190 ConfigurationDirectory = "seafile";
191 ExecStart = ''
192 ${cfg.seafilePackage}/bin/seaf-server \
193 --foreground \
194 -F /etc/seafile \
195 -c ${ccnetDir} \
196 -d ${dataDir} \
197 -l /var/log/seafile/server.log \
198 -P /run/seafile/server.pid \
199 -p /run/seafile
200 '';
201 };
202 preStart = ''
203 if [ ! -f "${seafRoot}/server-setup" ]; then
204 mkdir -p ${dataDir}/library-template
205 mkdir -p ${ccnetDir}/{GroupMgr,misc,OrgMgr,PeerMgr}
206 sqlite3 ${ccnetDir}/GroupMgr/groupmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/groupmgr.sql"
207 sqlite3 ${ccnetDir}/misc/config.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/config.sql"
208 sqlite3 ${ccnetDir}/OrgMgr/orgmgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/org.sql"
209 sqlite3 ${ccnetDir}/PeerMgr/usermgr.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/user.sql"
210 sqlite3 ${dataDir}/seafile.db ".read ${cfg.seafilePackage}/share/seafile/sql/sqlite/seafile.sql"
211 echo "${cfg.seafilePackage.version}-sqlite" > "${seafRoot}"/server-setup
212 fi
213 # checking for upgrades and handling them
214 # WARNING: needs to be extended to actually handle major version migrations
215 installedMajor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f1)
216 installedMinor=$(cat "${seafRoot}/server-setup" | cut -d"-" -f1 | cut -d"." -f2)
217 pkgMajor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f1)
218 pkgMinor=$(echo "${cfg.seafilePackage.version}" | cut -d"." -f2)
219
220 if [[ $installedMajor == $pkgMajor && $installedMinor == $pkgMinor ]]; then
221 :
222 elif [[ $installedMajor == 8 && $installedMinor == 0 && $pkgMajor == 9 && $pkgMinor == 0 ]]; then
223 # Upgrade from 8.0 to 9.0
224 sqlite3 ${dataDir}/seafile.db ".read ${pkgs.seahub}/scripts/upgrade/sql/9.0.0/sqlite3/seafile.sql"
225 echo "${cfg.seafilePackage.version}-sqlite" > "${seafRoot}"/server-setup
226 else
227 echo "Unsupported upgrade" >&2
228 exit 1
229 fi
230 '';
231 };
232
233 seahub = {
234 description = "Seafile Server Web Frontend";
235 wantedBy = [ "seafile.target" ];
236 partOf = [ "seafile.target" ];
237 after = [ "network.target" "seaf-server.service" ];
238 requires = [ "seaf-server.service" ];
239 restartTriggers = [ seahubSettings ];
240 environment = {
241 PYTHONPATH = "${pkgs.seahub.pythonPath}:${pkgs.seahub}/thirdpart:${pkgs.seahub}";
242 DJANGO_SETTINGS_MODULE = "seahub.settings";
243 CCNET_CONF_DIR = ccnetDir;
244 SEAFILE_CONF_DIR = dataDir;
245 SEAFILE_CENTRAL_CONF_DIR = "/etc/seafile";
246 SEAFILE_RPC_PIPE_PATH = "/run/seafile";
247 SEAHUB_LOG_DIR = "/var/log/seafile";
248 };
249 serviceConfig = securityOptions // {
250 User = "seafile";
251 Group = "seafile";
252 DynamicUser = true;
253 RuntimeDirectory = "seahub";
254 StateDirectory = "seafile";
255 LogsDirectory = "seafile";
256 ConfigurationDirectory = "seafile";
257 ExecStart = ''
258 ${pkgs.seahub.python.pkgs.gunicorn}/bin/gunicorn seahub.wsgi:application \
259 --name seahub \
260 --workers ${toString cfg.workers} \
261 --log-level=info \
262 --preload \
263 --timeout=1200 \
264 --limit-request-line=8190 \
265 --bind unix:/run/seahub/gunicorn.sock
266 '';
267 };
268 preStart = ''
269 mkdir -p ${seahubDir}/media
270 # Link all media except avatars
271 for m in `find ${pkgs.seahub}/media/ -maxdepth 1 -not -name "avatars"`; do
272 ln -sf $m ${seahubDir}/media/
273 done
274 if [ ! -e "${seafRoot}/.seahubSecret" ]; then
275 ${pkgs.seahub.python}/bin/python ${pkgs.seahub}/tools/secret_key_generator.py > ${seafRoot}/.seahubSecret
276 chmod 400 ${seafRoot}/.seahubSecret
277 fi
278 if [ ! -f "${seafRoot}/seahub-setup" ]; then
279 # avatars directory should be writable
280 install -D -t ${seahubDir}/media/avatars/ ${pkgs.seahub}/media/avatars/default.png
281 install -D -t ${seahubDir}/media/avatars/groups ${pkgs.seahub}/media/avatars/groups/default.png
282 # init database
283 ${pkgs.seahub}/manage.py migrate
284 # create admin account
285 ${pkgs.expect}/bin/expect -c 'spawn ${pkgs.seahub}/manage.py createsuperuser --email=${cfg.adminEmail}; expect "Password: "; send "${cfg.initialAdminPassword}\r"; expect "Password (again): "; send "${cfg.initialAdminPassword}\r"; expect "Superuser created successfully."'
286 echo "${pkgs.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
287 fi
288 if [ $(cat "${seafRoot}/seahub-setup" | cut -d"-" -f1) != "${pkgs.seahub.version}" ]; then
289 # update database
290 ${pkgs.seahub}/manage.py migrate
291 echo "${pkgs.seahub.version}-sqlite" > "${seafRoot}/seahub-setup"
292 fi
293 '';
294 };
295 };
296 };
297}