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