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