1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7with lib;
8let
9 cfg = config.services.rustus;
10in
11{
12 meta.maintainers = with maintainers; [ happysalada ];
13
14 options.services.rustus = {
15
16 enable = mkEnableOption "TUS protocol implementation in Rust";
17
18 host = mkOption {
19 type = types.str;
20 description = ''
21 The host that rustus will connect to.
22 '';
23 default = "127.0.0.1";
24 example = "127.0.0.1";
25 };
26
27 port = mkOption {
28 type = types.port;
29 description = ''
30 The port that rustus will connect to.
31 '';
32 default = 1081;
33 example = 1081;
34 };
35
36 log_level = mkOption {
37 type = types.enum [
38 "DEBUG"
39 "INFO"
40 "ERROR"
41 ];
42 description = ''
43 Desired log level
44 '';
45 default = "INFO";
46 example = "ERROR";
47 };
48
49 max_body_size = mkOption {
50 type = types.str;
51 description = ''
52 Maximum body size in bytes
53 '';
54 default = "10000000"; # 10 mb
55 example = "100000000";
56 };
57
58 url = mkOption {
59 type = types.str;
60 description = ''
61 url path for uploads
62 '';
63 default = "/files";
64 };
65
66 disable_health_access_logs = mkOption {
67 type = types.bool;
68 description = ''
69 disable access log for /health endpoint
70 '';
71 default = false;
72 };
73
74 cors = mkOption {
75 type = types.listOf types.str;
76 description = ''
77 list of origins allowed to upload
78 '';
79 default = [ "*" ];
80 example = [
81 "*.staging.domain"
82 "*.prod.domain"
83 ];
84 };
85
86 tus_extensions = mkOption {
87 type = types.listOf (
88 types.enum [
89 "getting"
90 "creation"
91 "termination"
92 "creation-with-upload"
93 "creation-defer-length"
94 "concatenation"
95 "checksum"
96 ]
97 );
98 description = ''
99 Since TUS protocol offers extensibility you can turn off some protocol extensions.
100 '';
101 default = [
102 "getting"
103 "creation"
104 "termination"
105 "creation-with-upload"
106 "creation-defer-length"
107 "concatenation"
108 "checksum"
109 ];
110 };
111
112 remove_parts = mkOption {
113 type = types.bool;
114 description = ''
115 remove parts files after successful concatenation
116 '';
117 default = true;
118 example = false;
119 };
120
121 storage = lib.mkOption {
122 description = ''
123 Storages are used to actually store your files. You can configure where you want to store files.
124 '';
125 default = { };
126 example = lib.literalExpression ''
127 {
128 type = "hybrid-s3"
129 s3_access_key_file = konfig.age.secrets.R2_ACCESS_KEY.path;
130 s3_secret_key_file = konfig.age.secrets.R2_SECRET_KEY.path;
131 s3_bucket = "my_bucket";
132 s3_url = "https://s3.example.com";
133 }
134 '';
135 type = lib.types.submodule {
136 options = {
137 type = lib.mkOption {
138 type = lib.types.enum [
139 "file-storage"
140 "hybrid-s3"
141 ];
142 description = "Type of storage to use";
143 };
144 s3_access_key_file = lib.mkOption {
145 type = lib.types.str;
146 description = "File path that contains the S3 access key.";
147 };
148 s3_secret_key_file = lib.mkOption {
149 type = lib.types.path;
150 description = "File path that contains the S3 secret key.";
151 };
152 s3_region = lib.mkOption {
153 type = lib.types.str;
154 default = "us-east-1";
155 description = "S3 region name.";
156 };
157 s3_bucket = lib.mkOption {
158 type = lib.types.str;
159 description = "S3 bucket.";
160 };
161 s3_url = lib.mkOption {
162 type = lib.types.str;
163 description = "S3 url.";
164 };
165
166 force_sync = lib.mkOption {
167 type = lib.types.bool;
168 description = "calls fsync system call after every write to disk in local storage";
169 default = true;
170 };
171 data_dir = lib.mkOption {
172 type = lib.types.str;
173 description = "path to the local directory where all files are stored";
174 default = "/var/lib/rustus";
175 };
176 dir_structure = lib.mkOption {
177 type = lib.types.str;
178 description = "pattern of a directory structure locally and on s3";
179 default = "{year}/{month}/{day}";
180 };
181 };
182 };
183 };
184
185 info_storage = lib.mkOption {
186 description = ''
187 Info storages are used to store information about file uploads. These storages must be persistent, because every time chunk is uploaded rustus updates information about upload. And when someone wants to download file, information about it requested from storage to get actual path of an upload.
188 '';
189 default = { };
190 type = lib.types.submodule {
191 options = {
192 type = lib.mkOption {
193 type = lib.types.enum [ "file-info-storage" ];
194 description = "Type of info storage to use";
195 default = "file-info-storage";
196 };
197 dir = lib.mkOption {
198 type = lib.types.str;
199 description = "directory to store info about uploads";
200 default = "/var/lib/rustus";
201 };
202 };
203 };
204 };
205 };
206
207 config = lib.mkIf cfg.enable {
208
209 systemd.services.rustus =
210 let
211 isHybridS3 = cfg.storage.type == "hybrid-s3";
212 in
213 {
214 description = "Rustus server";
215 documentation = [ "https://s3rius.github.io/rustus/" ];
216
217 wantedBy = [ "multi-user.target" ];
218 after = [ "network.target" ];
219
220 environment = {
221 RUSTUS_SERVER_HOST = cfg.host;
222 RUSTUS_SERVER_PORT = toString cfg.port;
223 RUSTUS_LOG_LEVEL = cfg.log_level;
224 RUSTUS_MAX_BODY_SIZE = cfg.max_body_size;
225 RUSTUS_URL = cfg.url;
226 RUSTUS_DISABLE_HEALTH_ACCESS_LOG = lib.mkIf cfg.disable_health_access_logs "true";
227 RUSTUS_CORS = lib.concatStringsSep "," cfg.cors;
228 RUSTUS_TUS_EXTENSIONS = lib.concatStringsSep "," cfg.tus_extensions;
229 RUSTUS_REMOVE_PARTS = if cfg.remove_parts then "true" else "false";
230 RUSTUS_STORAGE = cfg.storage.type;
231 RUSTUS_DATA_DIR = cfg.storage.data_dir;
232 RUSTUS_DIR_STRUCTURE = cfg.storage.dir_structure;
233 RUSTUS_FORCE_FSYNC = if cfg.storage.force_sync then "true" else "false";
234 RUSTUS_S3_URL = mkIf isHybridS3 cfg.storage.s3_url;
235 RUSTUS_S3_BUCKET = mkIf isHybridS3 cfg.storage.s3_bucket;
236 RUSTUS_S3_REGION = mkIf isHybridS3 cfg.storage.s3_region;
237 RUSTUS_S3_ACCESS_KEY_PATH = mkIf isHybridS3 "%d/S3_ACCESS_KEY_PATH";
238 RUSTUS_S3_SECRET_KEY_PATH = mkIf isHybridS3 "%d/S3_SECRET_KEY_PATH";
239 RUSTUS_INFO_STORAGE = cfg.info_storage.type;
240 RUSTUS_INFO_DIR = cfg.info_storage.dir;
241 };
242
243 serviceConfig = {
244 ExecStart = "${pkgs.rustus}/bin/rustus";
245 StateDirectory = "rustus";
246 # User name is defined here to enable restoring a backup for example
247 # You will run the backup restore command as sudo -u rustus in order
248 # to have write permissions to /var/lib
249 User = "rustus";
250 DynamicUser = true;
251 LoadCredential = lib.optionals isHybridS3 [
252 "S3_ACCESS_KEY_PATH:${cfg.storage.s3_access_key_file}"
253 "S3_SECRET_KEY_PATH:${cfg.storage.s3_secret_key_file}"
254 ];
255 # hardening
256 RestrictRealtime = true;
257 RestrictNamespaces = true;
258 LockPersonality = true;
259 ProtectKernelModules = true;
260 ProtectKernelTunables = true;
261 ProtectKernelLogs = true;
262 ProtectControlGroups = true;
263 ProtectHostUserNamespaces = true;
264 ProtectClock = true;
265 RestrictSUIDSGID = true;
266 SystemCallArchitectures = "native";
267 CapabilityBoundingSet = "";
268 ProtectProc = "invisible";
269 # TODO consider SystemCallFilter LimitAS ProcSubset
270 };
271 };
272 };
273}