-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
rclone: add serve options #8986
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,52 @@ let | |||||||||
| replaceIllegalChars = builtins.replaceStrings [ "/" " " "$" ] [ "." "_" "" ]; | ||||||||||
| isUsingSecretProvisioner = name: config ? "${name}" && config."${name}".secrets != { }; | ||||||||||
|
|
||||||||||
| # options shared between mounts/serve | ||||||||||
| mountServeOptions = { | ||||||||||
| logLevel = lib.mkOption { | ||||||||||
| type = lib.types.nullOr ( | ||||||||||
| lib.types.enum [ | ||||||||||
| "ERROR" | ||||||||||
| "NOTICE" | ||||||||||
| "INFO" | ||||||||||
| "DEBUG" | ||||||||||
| ] | ||||||||||
| ); | ||||||||||
| default = null; | ||||||||||
| example = "INFO"; | ||||||||||
| description = '' | ||||||||||
| Set the log-level. | ||||||||||
| See: https://rclone.org/docs/#logging | ||||||||||
|
Comment on lines
+29
to
+30
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| ''; | ||||||||||
| }; | ||||||||||
| options = lib.mkOption { | ||||||||||
| type = | ||||||||||
| with lib.types; | ||||||||||
| attrsOf ( | ||||||||||
| nullOr (oneOf [ | ||||||||||
| bool | ||||||||||
| int | ||||||||||
| float | ||||||||||
| str | ||||||||||
| ]) | ||||||||||
| ); | ||||||||||
| default = { }; | ||||||||||
| apply = lib.mergeAttrs { | ||||||||||
| vfs-cache-mode = "full"; | ||||||||||
| cache-dir = "%C/rclone"; | ||||||||||
| }; | ||||||||||
| description = '' | ||||||||||
| An attribute set of option values passed to the command. | ||||||||||
| To set a boolean option, assign it `true` or `false`. See | ||||||||||
| <https://nixos.org/manual/nixpkgs/stable/#function-library-lib.cli.toCommandLineShellGNU> | ||||||||||
| for more details on the format. | ||||||||||
|
|
||||||||||
| Some caching options are set by default, namely `vfs-cache-mode = "full"` | ||||||||||
| and `cache-dir`. These can be overridden if desired. | ||||||||||
| ''; | ||||||||||
| }; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| in | ||||||||||
| { | ||||||||||
| meta.maintainers = with lib.maintainers; [ jess ]; | ||||||||||
|
|
@@ -117,23 +163,6 @@ in | |||||||||
| default = true; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| logLevel = lib.mkOption { | ||||||||||
| type = lib.types.nullOr ( | ||||||||||
| lib.types.enum [ | ||||||||||
| "ERROR" | ||||||||||
| "NOTICE" | ||||||||||
| "INFO" | ||||||||||
| "DEBUG" | ||||||||||
| ] | ||||||||||
| ); | ||||||||||
| default = null; | ||||||||||
| example = "INFO"; | ||||||||||
| description = '' | ||||||||||
| Set the log-level. | ||||||||||
| See: https://rclone.org/docs/#logging | ||||||||||
| ''; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| mountPoint = lib.mkOption { | ||||||||||
| type = lib.types.str; | ||||||||||
| default = null; | ||||||||||
|
|
@@ -142,34 +171,8 @@ in | |||||||||
| ''; | ||||||||||
| example = "/home/alice/my-remote"; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| options = lib.mkOption { | ||||||||||
| type = | ||||||||||
| with lib.types; | ||||||||||
| attrsOf ( | ||||||||||
| nullOr (oneOf [ | ||||||||||
| bool | ||||||||||
| int | ||||||||||
| float | ||||||||||
| str | ||||||||||
| ]) | ||||||||||
| ); | ||||||||||
| default = { }; | ||||||||||
| apply = lib.mergeAttrs { | ||||||||||
| vfs-cache-mode = "full"; | ||||||||||
| cache-dir = "%C/rclone"; | ||||||||||
| }; | ||||||||||
| description = '' | ||||||||||
| An attribute set of option values passed to `rclone mount`. To set | ||||||||||
| a boolean option, assign it `true` or `false`. See | ||||||||||
| <https://nixos.org/manual/nixpkgs/stable/#function-library-lib.cli.toCommandLineShellGNU> | ||||||||||
| for more details on the format. | ||||||||||
|
|
||||||||||
| Some caching options are set by default, namely `vfs-cache-mode = "full"` | ||||||||||
| and `cache-dir`. These can be overridden if desired. | ||||||||||
| ''; | ||||||||||
| }; | ||||||||||
| }; | ||||||||||
| } | ||||||||||
| // mountServeOptions; | ||||||||||
| } | ||||||||||
| ); | ||||||||||
| default = { }; | ||||||||||
|
|
@@ -199,6 +202,67 @@ in | |||||||||
| ''; | ||||||||||
|
|
||||||||||
| }; | ||||||||||
|
|
||||||||||
| serve = lib.mkOption { | ||||||||||
| type = | ||||||||||
| with lib.types; | ||||||||||
| attrsOf ( | ||||||||||
| lib.types.submodule { | ||||||||||
| options = { | ||||||||||
| enable = lib.mkEnableOption "serving this path"; | ||||||||||
|
|
||||||||||
| protocol = lib.mkOption { | ||||||||||
| type = lib.types.enum [ | ||||||||||
| "dlna" | ||||||||||
| "docker" | ||||||||||
| "ftp" | ||||||||||
| "http" | ||||||||||
| "nfs" | ||||||||||
| "restic" | ||||||||||
| "s3" | ||||||||||
| "sftp" | ||||||||||
| "webdav" | ||||||||||
| ]; | ||||||||||
| description = '' | ||||||||||
| The protocol to serve this path using. | ||||||||||
| See: https://rclone.org/commands/rclone_serve | ||||||||||
|
Comment on lines
+227
to
+228
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| ''; | ||||||||||
| example = "http"; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| autoServe = lib.mkEnableOption "automatic serving" // { | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also the description is pretty unclear. Should be expanded a bit to make it clear it will cause the service to start on login. |
||||||||||
| default = true; | ||||||||||
| }; | ||||||||||
| } | ||||||||||
| // mountServeOptions; | ||||||||||
| } | ||||||||||
| ); | ||||||||||
| default = { }; | ||||||||||
| description = '' | ||||||||||
| An attribute set mapping remote file paths to their corresponding serve configurations. | ||||||||||
|
|
||||||||||
| For each entry, to perform the equivalent of | ||||||||||
| `rclone serve protocol remote:path/to/files` — as described in the | ||||||||||
| rclone documentation <https://rclone.org/commands/rclone_serve/> — we create | ||||||||||
| a key-value pair like this: | ||||||||||
| `"path/to/files/on/remote" = { ... }`. | ||||||||||
| ''; | ||||||||||
| example = lib.literalExpression '' | ||||||||||
| { | ||||||||||
| "path/to/files" = { | ||||||||||
| enable = true; | ||||||||||
| protocol = "http"; | ||||||||||
| options = { | ||||||||||
| addr = "127.0.0.1:3000"; | ||||||||||
| dir-cache-time = "5000h"; | ||||||||||
| poll-interval = "10s"; | ||||||||||
| umask = "002"; | ||||||||||
| user-agent = "Laptop"; | ||||||||||
| }; | ||||||||||
| }; | ||||||||||
| } | ||||||||||
| ''; | ||||||||||
| }; | ||||||||||
| }; | ||||||||||
| } | ||||||||||
| ); | ||||||||||
|
|
@@ -399,12 +463,62 @@ in | |||||||||
| ] | ||||||||||
| ) | ||||||||||
| ); | ||||||||||
|
|
||||||||||
| serveServices = lib.listToAttrs ( | ||||||||||
| lib.concatMap | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code is completely unclear and definitely needs to be refactored into some more understandable form. |
||||||||||
| ( | ||||||||||
| { name, value }: | ||||||||||
| let | ||||||||||
| remote-name = name; | ||||||||||
| remote = value; | ||||||||||
| in | ||||||||||
| lib.concatMap ( | ||||||||||
| { name, value }: | ||||||||||
| let | ||||||||||
| serve-path = name; | ||||||||||
| serve = value; | ||||||||||
| in | ||||||||||
| lib.optional serve.enable ( | ||||||||||
| lib.nameValuePair "rclone-serve:${replaceIllegalChars serve-path}@${remote-name}" { | ||||||||||
| Unit = { | ||||||||||
| Description = "Rclone protocol serving for ${remote-name}:${serve-path}"; | ||||||||||
| Requires = [ "rclone-config.service" ]; | ||||||||||
| After = [ "rclone-config.service" ]; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| Service = { | ||||||||||
| Type = "notify"; | ||||||||||
| Environment = lib.optional (serve.logLevel != null) "RCLONE_LOG_LEVEL=${serve.logLevel}"; | ||||||||||
|
|
||||||||||
| ExecStart = lib.concatStringsSep " " [ | ||||||||||
| (lib.getExe cfg.package) | ||||||||||
| "serve" | ||||||||||
| (lib.escapeShellArg serve.protocol) | ||||||||||
| (lib.cli.toCommandLineShellGNU { } serve.options) | ||||||||||
| (lib.escapeShellArg "${remote-name}:${serve-path}") | ||||||||||
| ]; | ||||||||||
| Restart = "on-failure"; | ||||||||||
| }; | ||||||||||
|
|
||||||||||
| Install.WantedBy = lib.optional serve.autoServe "default.target"; | ||||||||||
| } | ||||||||||
| ) | ||||||||||
| ) (lib.attrsToList remote.serve) | ||||||||||
| ) | ||||||||||
| ( | ||||||||||
| lib.pipe cfg.remotes [ | ||||||||||
| lib.attrsToList | ||||||||||
| (lib.filter (rem: rem.value ? serve)) | ||||||||||
| ] | ||||||||||
| ) | ||||||||||
| ); | ||||||||||
| in | ||||||||||
| lib.mkIf cfg.enable { | ||||||||||
| home.packages = [ cfg.package ]; | ||||||||||
| systemd.user.services = lib.mkMerge [ | ||||||||||
| rcloneConfigService | ||||||||||
| mountServices | ||||||||||
| serveServices | ||||||||||
| ]; | ||||||||||
| }; | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| { pkgs, lib, ... }: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While it is very nice you added an integration test I would suggest to also add a unit test that verifies that the expected service files are created. Since integration tests are not typically run in CI we would otherwise easily miss regressions. |
||
| let | ||
| sshKeys = import "${pkgs.path}/nixos/tests/ssh-keys.nix" pkgs; | ||
|
|
||
| # https://rclone.org/sftp/#ssh-authentication | ||
| keyPem = lib.pipe sshKeys.snakeOilEd25519PrivateKey.text [ | ||
| lib.trim | ||
| (lib.replaceStrings [ "\n" ] [ "\\\\n" ]) | ||
| ]; | ||
|
|
||
| module = pkgs.writeText "serve-module" '' | ||
| { pkgs, lib, ... }: { | ||
| programs.rclone.remotes = { | ||
| alices-sftp-remote = { | ||
| config = { | ||
| type = "sftp"; | ||
| host = "remote"; | ||
| user = "alice"; | ||
| key_pem = "${keyPem}"; | ||
| known_hosts = "${sshKeys.snakeOilEd25519PublicKey}"; | ||
| }; | ||
| serve = { | ||
| "/home/alice/files" = { | ||
| enable = true; | ||
| protocol = "http"; | ||
| options.addr = "localhost:8080"; | ||
| }; | ||
| }; | ||
| }; | ||
| }; | ||
| } | ||
| ''; | ||
| in | ||
| { | ||
| nodes.remote = { | ||
| services.openssh.enable = true; | ||
|
|
||
| users.users.alice.openssh.authorizedKeys.keys = [ | ||
| sshKeys.snakeOilEd25519PublicKey | ||
| ]; | ||
| }; | ||
|
|
||
| script = '' | ||
| remote.wait_for_unit("network.target") | ||
| remote.wait_for_unit("multi-user.target") | ||
|
|
||
| succeed_as_alice( | ||
| "mkdir -p /home/alice/.ssh", | ||
| "install -m644 ${module} /home/alice/.config/home-manager/test-remote.nix" | ||
| ) | ||
|
|
||
| actual = succeed_as_alice("home-manager switch") | ||
| expected = "rclone-config.service" | ||
| assert "Starting units: " in actual and expected in actual, \ | ||
| f"expected home-manager switch to contain {expected}, but got {actual}" | ||
|
|
||
| with subtest("Serve a remote over HTTP (sftp)"): | ||
| # create files on remote | ||
| succeed_as_alice( | ||
| "mkdir /home/alice/files", | ||
| "touch /home/alice/files/other_file" | ||
| "echo serving > /home/alice/files/test.txt", | ||
| box=remote | ||
| ) | ||
|
|
||
| # fetch file from server | ||
| output = succeed_as_alice( | ||
| "curl -s http://localhost:8080/test.txt" | ||
| ) | ||
| expected = "serving" | ||
| assert expected in output, \ | ||
| f"HTTP server response does not contain expected content. Got: {output}" | ||
|
|
||
| # verify file listing | ||
| output = succeed_as_alice( | ||
| "curl -s http://localhost:8080/" | ||
| ) | ||
| assert "other_file" in output, \ | ||
| f"HTTP directory listing does not contain other_file. Got: {output}" | ||
| ''; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for
nullOr: