1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7
8let
9 cfg = config.services.szurubooru;
10 inherit (lib)
11 mkOption
12 mkEnableOption
13 mkIf
14 mkPackageOption
15 types
16 ;
17 format = pkgs.formats.yaml { };
18 python = pkgs.python3;
19in
20
21{
22 options = {
23 services.szurubooru = {
24 enable = mkEnableOption "Szurubooru, an image board engine dedicated for small and medium communities";
25
26 user = mkOption {
27 type = types.str;
28 default = "szurubooru";
29 description = ''
30 User account under which Szurubooru runs.
31 '';
32 };
33
34 group = mkOption {
35 type = types.str;
36 default = "szurubooru";
37 description = ''
38 Group under which Szurubooru runs.
39 '';
40 };
41
42 dataDir = mkOption {
43 type = types.path;
44 default = "/var/lib/szurubooru";
45 example = "/var/lib/szuru";
46 description = ''
47 The path to the data directory in which Szurubooru will store its data.
48 '';
49 };
50
51 openFirewall = mkOption {
52 type = types.bool;
53 default = false;
54 example = true;
55 description = ''
56 Whether to open the firewall for the port in {option}`services.szurubooru.server.port`.
57 '';
58 };
59
60 server = {
61 package = mkPackageOption pkgs [
62 "szurubooru"
63 "server"
64 ] { };
65
66 port = mkOption {
67 type = types.port;
68 default = 8080;
69 example = 9000;
70 description = ''
71 Port to expose HTTP service.
72 '';
73 };
74
75 threads = mkOption {
76 type = types.int;
77 default = 4;
78 example = 6;
79 description = ''Number of waitress threads to start.'';
80 };
81
82 settings = mkOption {
83 type = types.submodule {
84 freeformType = format.type;
85 options = {
86 name = mkOption {
87 type = types.str;
88 default = "szurubooru";
89 example = "Szuru";
90 description = ''Name shown in the website title and on the front page.'';
91 };
92
93 domain = mkOption {
94 type = types.str;
95 example = "http://example.com";
96 description = ''Full URL to the homepage of this szurubooru site (with no trailing slash).'';
97 };
98
99 # NOTE: this is not a real upstream option
100 secretFile = mkOption {
101 type = types.path;
102 example = "/run/secrets/szurubooru-server-secret";
103 description = ''
104 File containing a secret used to salt the users' password hashes and generate filenames for static content.
105 '';
106 };
107
108 delete_source_files = mkOption {
109 type = types.enum [
110 "yes"
111 "no"
112 ];
113 default = "no";
114 example = "yes";
115 description = ''Whether to delete thumbnails and source files on post delete.'';
116 };
117
118 smtp = {
119 host = mkOption {
120 type = types.nullOr types.str;
121 default = null;
122 example = "localhost";
123 description = ''Host of the SMTP server used to send reset password.'';
124 };
125
126 port = mkOption {
127 type = types.nullOr types.port;
128 default = null;
129 example = 25;
130 description = ''Port of the SMTP server.'';
131 };
132
133 user = mkOption {
134 type = types.nullOr types.str;
135 default = null;
136 example = "bot";
137 description = ''User to connect to the SMTP server.'';
138 };
139
140 # NOTE: this is not a real upstream option
141 passFile = mkOption {
142 type = types.nullOr types.path;
143 default = null;
144 example = "/run/secrets/szurubooru-smtp-pass";
145 description = ''File containing the password associated to the given user for the SMTP server.'';
146 };
147 };
148
149 data_url = mkOption {
150 type = types.str;
151 default = "${cfg.server.settings.domain}/data/";
152 defaultText = lib.literalExpression ''"''${services.szurubooru.server.settings.domain}/data/"'';
153 example = "http://example.com/content/";
154 description = ''Full URL to the data endpoint.'';
155 };
156
157 data_dir = mkOption {
158 type = types.path;
159 default = "${cfg.dataDir}/data";
160 defaultText = lib.literalExpression ''"''${services.szurubooru.dataDir}/data"'';
161 example = "/srv/szurubooru/data";
162 description = ''Path to the static files.'';
163 };
164
165 debug = mkOption {
166 type = types.int;
167 default = 0;
168 example = 1;
169 description = ''Whether to generate server logs.'';
170 };
171
172 show_sql = mkOption {
173 type = types.int;
174 default = 0;
175 example = 1;
176 description = ''Whether to show SQL in server logs.'';
177 };
178 };
179 };
180 description = ''
181 Configuration to write to {file}`config.yaml`.
182 See <https://github.com/rr-/szurubooru/blob/master/server/config.yaml.dist> for more information.
183 '';
184 };
185 };
186
187 client = {
188 package = mkPackageOption pkgs [
189 "szurubooru"
190 "client"
191 ] { };
192 };
193
194 database = {
195 host = mkOption {
196 type = types.str;
197 default = "localhost";
198 example = "192.168.1.2";
199 description = ''Host on which the PostgreSQL database runs.'';
200 };
201
202 port = mkOption {
203 type = types.port;
204 default = 5432;
205 description = ''The port under which PostgreSQL listens to.'';
206 };
207
208 name = mkOption {
209 type = types.str;
210 default = cfg.database.user;
211 defaultText = lib.literalExpression "szurubooru.database.name";
212 example = "szuru";
213 description = ''Name of the PostgreSQL database.'';
214 };
215
216 user = mkOption {
217 type = types.str;
218 default = "szurubooru";
219 example = "szuru";
220 description = ''PostgreSQL user.'';
221 };
222
223 passwordFile = mkOption {
224 type = types.path;
225 example = "/run/secrets/szurubooru-db-password";
226 description = ''A file containing the password for the PostgreSQL user.'';
227 };
228 };
229 };
230 };
231
232 config = mkIf cfg.enable {
233
234 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.server.port ];
235
236 users.groups = mkIf (cfg.group == "szurubooru") {
237 szurubooru = { };
238 };
239
240 users.users = mkIf (cfg.user == "szurubooru") {
241 szurubooru = {
242 group = cfg.group;
243 description = "Szurubooru Daemon user";
244 isSystemUser = true;
245 };
246 };
247
248 systemd.services.szurubooru =
249 let
250 configFile = format.generate "config.yaml" (
251 lib.pipe cfg.server.settings [
252 (
253 settings:
254 lib.recursiveUpdate settings {
255 secretFile = null;
256 secret = "$SZURUBOORU_SECRET";
257
258 smtp.pass = if settings.smtp.passFile != null then "$SZURUBOORU_SMTP_PASS" else null;
259 smtp.passFile = null;
260 smtp.enable = null;
261
262 database = "postgresql://${cfg.database.user}:$SZURUBOORU_DATABASE_PASSWORD@${cfg.database.host}:${toString cfg.database.port}/${cfg.database.name}";
263 }
264 )
265 (lib.filterAttrsRecursive (_: x: x != null))
266 ]
267 );
268 pyenv = python.buildEnv.override {
269 extraLibs = [ (python.pkgs.toPythonModule cfg.server.package) ];
270 };
271 in
272 {
273 description = "Server of Szurubooru, an image board engine dedicated for small and medium communities";
274
275 wantedBy = [
276 "multi-user.target"
277 "szurubooru-client.service"
278 ];
279 before = [ "szurubooru-client.service" ];
280 after = [
281 "network.target"
282 "network-online.target"
283 ];
284 wants = [ "network-online.target" ];
285
286 environment = {
287 PYTHONPATH = "${pyenv}/${pyenv.sitePackages}/";
288 };
289
290 path =
291 with pkgs;
292 [
293 envsubst
294 ffmpeg_4-full
295 ]
296 ++ (with python.pkgs; [
297 alembic
298 waitress
299 ]);
300
301 script = ''
302 export SZURUBOORU_SECRET="$(<${cfg.server.settings.secretFile})"
303 export SZURUBOORU_DATABASE_PASSWORD="$(<${cfg.database.passwordFile})"
304 ${lib.optionalString (cfg.server.settings.smtp.passFile != null) ''
305 export SZURUBOORU_SMTP_PASS=$(<${cfg.server.settings.smtp.passFile})
306 ''}
307 install -m0640 ${cfg.server.package.src}/config.yaml.dist ${cfg.dataDir}/config.yaml.dist
308 envsubst -i ${configFile} -o ${cfg.dataDir}/config.yaml
309 sed 's|script_location = |script_location = ${cfg.server.package.src}/|' ${cfg.server.package.src}/alembic.ini > ${cfg.dataDir}/alembic.ini
310 alembic upgrade head
311 waitress-serve --port ${toString cfg.server.port} --threads ${toString cfg.server.threads} szurubooru.facade:app
312 '';
313
314 serviceConfig = {
315 User = cfg.user;
316 Group = cfg.group;
317
318 Type = "simple";
319 Restart = "on-failure";
320
321 StateDirectory = mkIf (cfg.dataDir == "/var/lib/szurubooru") "szurubooru";
322 WorkingDirectory = cfg.dataDir;
323 };
324 };
325 };
326
327 meta = {
328 maintainers = with lib.maintainers; [ ratcornu ];
329 doc = ./szurubooru.md;
330 };
331}