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