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