1{ config, pkgs, lib, ... }:
2
3with lib;
4
5let
6 cfg = config.services.etebase-server;
7
8 iniFmt = pkgs.formats.ini {};
9
10 configIni = iniFmt.generate "etebase-server.ini" cfg.settings;
11
12 defaultUser = "etebase-server";
13in
14{
15 imports = [
16 (mkRemovedOptionModule
17 [ "services" "etebase-server" "customIni" ]
18 "Set the option `services.etebase-server.settings' instead.")
19 (mkRemovedOptionModule
20 [ "services" "etebase-server" "database" ]
21 "Set the option `services.etebase-server.settings.database' instead.")
22 (mkRenamedOptionModule
23 [ "services" "etebase-server" "secretFile" ]
24 [ "services" "etebase-server" "settings" "secret_file" ])
25 (mkRenamedOptionModule
26 [ "services" "etebase-server" "host" ]
27 [ "services" "etebase-server" "settings" "allowed_hosts" "allowed_host1" ])
28 ];
29
30 options = {
31 services.etebase-server = {
32 enable = mkOption {
33 type = types.bool;
34 default = false;
35 example = true;
36 description = ''
37 Whether to enable the Etebase server.
38
39 Once enabled you need to create an admin user by invoking the
40 shell command `etebase-server createsuperuser` with
41 the user specified by the `user` option or a superuser.
42 Then you can login and create accounts on your-etebase-server.com/admin
43 '';
44 };
45
46 package = mkOption {
47 type = types.package;
48 default = pkgs.python3.pkgs.etebase-server;
49 defaultText = literalExpression "pkgs.python3.pkgs.etebase-server";
50 description = "etebase-server package to use.";
51 };
52
53 dataDir = mkOption {
54 type = types.str;
55 default = "/var/lib/etebase-server";
56 description = "Directory to store the Etebase server data.";
57 };
58
59 port = mkOption {
60 type = with types; nullOr port;
61 default = 8001;
62 description = "Port to listen on.";
63 };
64
65 openFirewall = mkOption {
66 type = types.bool;
67 default = false;
68 description = ''
69 Whether to open ports in the firewall for the server.
70 '';
71 };
72
73 unixSocket = mkOption {
74 type = with types; nullOr str;
75 default = null;
76 description = "The path to the socket to bind to.";
77 example = "/run/etebase-server/etebase-server.sock";
78 };
79
80 settings = mkOption {
81 type = lib.types.submodule {
82 freeformType = iniFmt.type;
83
84 options = {
85 global = {
86 debug = mkOption {
87 type = types.bool;
88 default = false;
89 description = ''
90 Whether to set django's DEBUG flag.
91 '';
92 };
93 secret_file = mkOption {
94 type = with types; nullOr str;
95 default = null;
96 description = ''
97 The path to a file containing the secret
98 used as django's SECRET_KEY.
99 '';
100 };
101 static_root = mkOption {
102 type = types.str;
103 default = "${cfg.dataDir}/static";
104 defaultText = literalExpression ''"''${config.services.etebase-server.dataDir}/static"'';
105 description = "The directory for static files.";
106 };
107 media_root = mkOption {
108 type = types.str;
109 default = "${cfg.dataDir}/media";
110 defaultText = literalExpression ''"''${config.services.etebase-server.dataDir}/media"'';
111 description = "The media directory.";
112 };
113 };
114 allowed_hosts = {
115 allowed_host1 = mkOption {
116 type = types.str;
117 default = "0.0.0.0";
118 example = "localhost";
119 description = ''
120 The main host that is allowed access.
121 '';
122 };
123 };
124 database = {
125 engine = mkOption {
126 type = types.enum [ "django.db.backends.sqlite3" "django.db.backends.postgresql" ];
127 default = "django.db.backends.sqlite3";
128 description = "The database engine to use.";
129 };
130 name = mkOption {
131 type = types.str;
132 default = "${cfg.dataDir}/db.sqlite3";
133 defaultText = literalExpression ''"''${config.services.etebase-server.dataDir}/db.sqlite3"'';
134 description = "The database name.";
135 };
136 };
137 };
138 };
139 default = {};
140 description = ''
141 Configuration for `etebase-server`. Refer to
142 <https://github.com/etesync/server/blob/master/etebase-server.ini.example>
143 and <https://github.com/etesync/server/wiki>
144 for details on supported values.
145 '';
146 example = {
147 global = {
148 debug = true;
149 media_root = "/path/to/media";
150 };
151 allowed_hosts = {
152 allowed_host2 = "localhost";
153 };
154 };
155 };
156
157 user = mkOption {
158 type = types.str;
159 default = defaultUser;
160 description = "User under which Etebase server runs.";
161 };
162 };
163 };
164
165 config = mkIf cfg.enable {
166
167 environment.systemPackages = with pkgs; [
168 (runCommand "etebase-server" {
169 nativeBuildInputs = [ makeWrapper ];
170 } ''
171 makeWrapper ${cfg.package}/bin/etebase-server \
172 $out/bin/etebase-server \
173 --chdir ${escapeShellArg cfg.dataDir} \
174 --prefix ETEBASE_EASY_CONFIG_PATH : "${configIni}"
175 '')
176 ];
177
178 systemd.tmpfiles.rules = [
179 "d '${cfg.dataDir}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
180 ] ++ lib.optionals (cfg.unixSocket != null) [
181 "d '${builtins.dirOf cfg.unixSocket}' - ${cfg.user} ${config.users.users.${cfg.user}.group} - -"
182 ];
183
184 systemd.services.etebase-server = {
185 description = "An Etebase (EteSync 2.0) server";
186 after = [ "network.target" "systemd-tmpfiles-setup.service" ];
187 path = [ cfg.package ];
188 wantedBy = [ "multi-user.target" ];
189 serviceConfig = {
190 User = cfg.user;
191 Restart = "always";
192 WorkingDirectory = cfg.dataDir;
193 };
194 environment = {
195 ETEBASE_EASY_CONFIG_PATH = configIni;
196 PYTHONPATH = cfg.package.pythonPath;
197 };
198 preStart = ''
199 # Auto-migrate on first run or if the package has changed
200 versionFile="${cfg.dataDir}/src-version"
201 if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
202 etebase-server migrate --no-input
203 etebase-server collectstatic --no-input --clear
204 echo ${cfg.package} > "$versionFile"
205 fi
206 '';
207 script =
208 let
209 python = cfg.package.python;
210 networking = if cfg.unixSocket != null
211 then "--uds ${cfg.unixSocket}"
212 else "--host 0.0.0.0 --port ${toString cfg.port}";
213 in ''
214 ${python.pkgs.uvicorn}/bin/uvicorn ${networking} \
215 --app-dir ${cfg.package}/${cfg.package.python.sitePackages} \
216 etebase_server.asgi:application
217 '';
218 };
219
220 users = optionalAttrs (cfg.user == defaultUser) {
221 users.${defaultUser} = {
222 isSystemUser = true;
223 group = defaultUser;
224 home = cfg.dataDir;
225 };
226
227 groups.${defaultUser} = {};
228 };
229
230 networking.firewall = mkIf cfg.openFirewall {
231 allowedTCPPorts = [ cfg.port ];
232 };
233 };
234}