up
This commit is contained in:
@@ -44,6 +44,10 @@
|
|||||||
|
|
||||||
flake.darwinConfigurations = inputs.nixpkgs.lib.genAttrs darwinHosts (
|
flake.darwinConfigurations = inputs.nixpkgs.lib.genAttrs darwinHosts (
|
||||||
hostname:
|
hostname:
|
||||||
|
let
|
||||||
|
syncthingOverlay = import ./overlays/syncthing-darwin.nix;
|
||||||
|
syncthingModule = (syncthingOverlay null { darwin = {}; }).darwinSyncthingModule;
|
||||||
|
in
|
||||||
inputs.darwin.lib.darwinSystem {
|
inputs.darwin.lib.darwinSystem {
|
||||||
system = "aarch64-darwin";
|
system = "aarch64-darwin";
|
||||||
specialArgs =
|
specialArgs =
|
||||||
@@ -54,8 +58,11 @@
|
|||||||
modules = [
|
modules = [
|
||||||
inputs.home-manager.darwinModules.home-manager
|
inputs.home-manager.darwinModules.home-manager
|
||||||
inputs.nix-homebrew.darwinModules.nix-homebrew
|
inputs.nix-homebrew.darwinModules.nix-homebrew
|
||||||
|
syncthingModule
|
||||||
|
|
||||||
{
|
{
|
||||||
|
nixpkgs.overlays = [ syncthingOverlay ];
|
||||||
|
|
||||||
nix-homebrew = {
|
nix-homebrew = {
|
||||||
inherit user;
|
inherit user;
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
imports = [
|
imports = [
|
||||||
../../core
|
../../core
|
||||||
../../networking/tailscale.nix
|
../../networking/tailscale.nix
|
||||||
../../services/syncthing-darwin.nix
|
|
||||||
../../services/syncthing.nix
|
../../services/syncthing.nix
|
||||||
./dock
|
./dock
|
||||||
./homebrew.nix
|
./homebrew.nix
|
||||||
|
|||||||
@@ -1,386 +0,0 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.services.syncthing;
|
|
||||||
defaultUser = "syncthing";
|
|
||||||
defaultGroup = defaultUser;
|
|
||||||
settingsFormat = pkgs.formats.json { };
|
|
||||||
cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != { })) cfg.settings;
|
|
||||||
|
|
||||||
isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/";
|
|
||||||
|
|
||||||
curlAddressArgs = path:
|
|
||||||
if isUnixGui
|
|
||||||
then "--unix-socket ${cfg.guiAddress} http://.${path}"
|
|
||||||
else "${cfg.guiAddress}${path}";
|
|
||||||
|
|
||||||
devices = mapAttrsToList (_: device: device // { deviceID = device.id; }) cfg.settings.devices;
|
|
||||||
anyAutoAccept = builtins.any (dev: dev.autoAcceptFolders) devices;
|
|
||||||
|
|
||||||
folders = mapAttrsToList (_: folder: folder // {
|
|
||||||
devices = let
|
|
||||||
folderDevices = folder.devices;
|
|
||||||
in
|
|
||||||
map (device:
|
|
||||||
if builtins.isString device then
|
|
||||||
{ deviceId = cfg.settings.devices.${device}.id; }
|
|
||||||
else if builtins.isAttrs device then
|
|
||||||
{ deviceId = cfg.settings.devices.${device.name}.id; } // device
|
|
||||||
else
|
|
||||||
throw "Invalid type for devices in folder; expected list or attrset."
|
|
||||||
) folderDevices;
|
|
||||||
}) (filterAttrs (_: folder: folder.enable) cfg.settings.folders);
|
|
||||||
|
|
||||||
jq = "${pkgs.jq}/bin/jq";
|
|
||||||
updateConfig = pkgs.writers.writeBash "merge-syncthing-config" (
|
|
||||||
''
|
|
||||||
set -efu
|
|
||||||
umask 0077
|
|
||||||
|
|
||||||
curl() {
|
|
||||||
while
|
|
||||||
! ${pkgs.libxml2}/bin/xmllint \
|
|
||||||
--xpath 'string(configuration/gui/apikey)' \
|
|
||||||
${cfg.configDir}/config.xml \
|
|
||||||
>"$TMPDIR/api_key"
|
|
||||||
do sleep 1; done
|
|
||||||
(printf "X-API-Key: "; cat "$TMPDIR/api_key") >"$TMPDIR/headers"
|
|
||||||
${pkgs.curl}/bin/curl -sSLk -H "@$TMPDIR/headers" \
|
|
||||||
--retry 1000 --retry-delay 1 --retry-all-errors \
|
|
||||||
"$@"
|
|
||||||
}
|
|
||||||
''
|
|
||||||
+ (lib.pipe {
|
|
||||||
devs = {
|
|
||||||
new_conf_IDs = map (v: v.id) devices;
|
|
||||||
GET_IdAttrName = "deviceID";
|
|
||||||
override = cfg.overrideDevices;
|
|
||||||
conf = devices;
|
|
||||||
baseAddress = curlAddressArgs "/rest/config/devices";
|
|
||||||
};
|
|
||||||
dirs = {
|
|
||||||
new_conf_IDs = map (v: v.id) folders;
|
|
||||||
GET_IdAttrName = "id";
|
|
||||||
override = cfg.overrideFolders;
|
|
||||||
conf = folders;
|
|
||||||
baseAddress = curlAddressArgs "/rest/config/folders";
|
|
||||||
};
|
|
||||||
} [
|
|
||||||
(mapAttrs (conf_type: s:
|
|
||||||
lib.pipe s.conf [
|
|
||||||
(map (new_cfg:
|
|
||||||
let
|
|
||||||
jsonPreSecretsFile = pkgs.writeTextFile {
|
|
||||||
name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json";
|
|
||||||
text = builtins.toJSON new_cfg;
|
|
||||||
};
|
|
||||||
injectSecretsJqCmd = {
|
|
||||||
"devs" = "${jq} .";
|
|
||||||
"dirs" = let
|
|
||||||
folder = new_cfg;
|
|
||||||
devicesWithSecrets = lib.pipe folder.devices [
|
|
||||||
(lib.filter (device: (builtins.isAttrs device) && device ? encryptionPasswordFile))
|
|
||||||
(map (device: {
|
|
||||||
deviceId = device.deviceId;
|
|
||||||
variableName = "secret_${builtins.hashString "sha256" device.encryptionPasswordFile}";
|
|
||||||
secretPath = device.encryptionPasswordFile;
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
jqUpdates = map (device: ''
|
|
||||||
.devices[] |= (
|
|
||||||
if .deviceId == "${device.deviceId}" then
|
|
||||||
del(.encryptionPasswordFile) |
|
|
||||||
.encryptionPassword = ''$${device.variableName}
|
|
||||||
else
|
|
||||||
.
|
|
||||||
end
|
|
||||||
)
|
|
||||||
'') devicesWithSecrets;
|
|
||||||
jqRawFiles = map (device: "--rawfile ${device.variableName} ${lib.escapeShellArg device.secretPath}") devicesWithSecrets;
|
|
||||||
in
|
|
||||||
"${jq} ${lib.concatStringsSep " " jqRawFiles} ${lib.escapeShellArg (lib.concatStringsSep "|" ([ "." ] ++ jqUpdates))}";
|
|
||||||
}.${conf_type};
|
|
||||||
in
|
|
||||||
''
|
|
||||||
${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress}
|
|
||||||
''
|
|
||||||
))
|
|
||||||
(lib.concatStringsSep "\n")
|
|
||||||
]
|
|
||||||
+ lib.optionalString s.override ''
|
|
||||||
stale_${conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} \
|
|
||||||
--argjson new_ids ${lib.escapeShellArg (builtins.toJSON s.new_conf_IDs)} \
|
|
||||||
--raw-output \
|
|
||||||
'[.[].${s.GET_IdAttrName}] - $new_ids | .[]'
|
|
||||||
)"
|
|
||||||
for id in ''${stale_${conf_type}_ids}; do
|
|
||||||
>&2 echo "Deleting stale device: $id"
|
|
||||||
curl -X DELETE ${s.baseAddress}/$id
|
|
||||||
done
|
|
||||||
''
|
|
||||||
))
|
|
||||||
builtins.attrValues
|
|
||||||
(lib.concatStringsSep "\n")
|
|
||||||
])
|
|
||||||
+ (lib.pipe cleanedConfig [
|
|
||||||
builtins.attrNames
|
|
||||||
(lib.subtractLists [ "folders" "devices" ])
|
|
||||||
(map (subOption: ''
|
|
||||||
curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} ${curlAddressArgs "/rest/config/${subOption}"}
|
|
||||||
''))
|
|
||||||
(lib.concatStringsSep "\n")
|
|
||||||
])
|
|
||||||
+ ''
|
|
||||||
if curl ${curlAddressArgs "/rest/config/restart-required"} |
|
|
||||||
${jq} -e .requiresRestart > /dev/null; then
|
|
||||||
curl -X POST ${curlAddressArgs "/rest/system/restart"}
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
services.syncthing = {
|
|
||||||
enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
|
|
||||||
|
|
||||||
cert = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Path to the cert.pem file, which will be copied into Syncthing's configDir.";
|
|
||||||
};
|
|
||||||
|
|
||||||
key = mkOption {
|
|
||||||
type = types.nullOr types.str;
|
|
||||||
default = null;
|
|
||||||
description = "Path to the key.pem file, which will be copied into Syncthing's configDir.";
|
|
||||||
};
|
|
||||||
|
|
||||||
overrideDevices = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Whether to delete the devices which are not configured via the devices option.";
|
|
||||||
};
|
|
||||||
|
|
||||||
overrideFolders = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = !anyAutoAccept;
|
|
||||||
description = "Whether to delete the folders which are not configured via the folders option.";
|
|
||||||
};
|
|
||||||
|
|
||||||
settings = mkOption {
|
|
||||||
type = types.submodule {
|
|
||||||
freeformType = settingsFormat.type;
|
|
||||||
options = {
|
|
||||||
options = mkOption {
|
|
||||||
default = { };
|
|
||||||
description = "The options element contains all other global configuration options";
|
|
||||||
type = types.submodule {
|
|
||||||
freeformType = settingsFormat.type;
|
|
||||||
options = {
|
|
||||||
localAnnounceEnabled = mkOption {
|
|
||||||
type = types.nullOr types.bool;
|
|
||||||
default = null;
|
|
||||||
description = "Whether to send announcements to the local LAN.";
|
|
||||||
};
|
|
||||||
globalAnnounceEnabled = mkOption {
|
|
||||||
type = types.nullOr types.bool;
|
|
||||||
default = null;
|
|
||||||
description = "Whether to send announcements to the global discovery servers.";
|
|
||||||
};
|
|
||||||
relaysEnabled = mkOption {
|
|
||||||
type = types.nullOr types.bool;
|
|
||||||
default = null;
|
|
||||||
description = "When true, relays will be connected to and potentially used for device to device connections.";
|
|
||||||
};
|
|
||||||
urAccepted = mkOption {
|
|
||||||
type = types.nullOr types.int;
|
|
||||||
default = null;
|
|
||||||
description = "Whether the user has accepted to submit anonymous usage data.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
devices = mkOption {
|
|
||||||
default = { };
|
|
||||||
description = "Peers/devices which Syncthing should communicate with.";
|
|
||||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
|
||||||
freeformType = settingsFormat.type;
|
|
||||||
options = {
|
|
||||||
name = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = name;
|
|
||||||
description = "The name of the device.";
|
|
||||||
};
|
|
||||||
id = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
description = "The device ID.";
|
|
||||||
};
|
|
||||||
autoAcceptFolders = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Automatically create or share folders that this device advertises at the default path.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
folders = mkOption {
|
|
||||||
default = { };
|
|
||||||
description = "Folders which should be shared by Syncthing.";
|
|
||||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
|
||||||
freeformType = settingsFormat.type;
|
|
||||||
options = {
|
|
||||||
enable = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
description = "Whether to share this folder.";
|
|
||||||
};
|
|
||||||
path = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = name;
|
|
||||||
description = "The path to the folder which should be shared.";
|
|
||||||
};
|
|
||||||
id = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = name;
|
|
||||||
description = "The ID of the folder. Must be the same on all devices.";
|
|
||||||
};
|
|
||||||
label = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = name;
|
|
||||||
description = "The label of the folder.";
|
|
||||||
};
|
|
||||||
type = mkOption {
|
|
||||||
type = types.enum [ "sendreceive" "sendonly" "receiveonly" "receiveencrypted" ];
|
|
||||||
default = "sendreceive";
|
|
||||||
description = "Controls how the folder is handled by Syncthing.";
|
|
||||||
};
|
|
||||||
devices = mkOption {
|
|
||||||
type = types.listOf (types.oneOf [
|
|
||||||
types.str
|
|
||||||
(types.submodule {
|
|
||||||
freeformType = settingsFormat.type;
|
|
||||||
options = {
|
|
||||||
name = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
description = "The name of a device defined in the devices option.";
|
|
||||||
};
|
|
||||||
encryptionPasswordFile = mkOption {
|
|
||||||
type = types.nullOr types.path;
|
|
||||||
default = null;
|
|
||||||
description = "Path to encryption password file.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
default = [ ];
|
|
||||||
description = "The devices this folder should be shared with.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
default = { };
|
|
||||||
description = "Extra configuration options for Syncthing.";
|
|
||||||
};
|
|
||||||
|
|
||||||
guiAddress = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = "127.0.0.1:8384";
|
|
||||||
description = "The address to serve the web interface at.";
|
|
||||||
};
|
|
||||||
|
|
||||||
user = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = defaultUser;
|
|
||||||
description = "The user to run Syncthing as.";
|
|
||||||
};
|
|
||||||
|
|
||||||
group = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = defaultGroup;
|
|
||||||
description = "The group to run Syncthing under.";
|
|
||||||
};
|
|
||||||
|
|
||||||
dataDir = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
default = "/var/lib/syncthing";
|
|
||||||
description = "The path where synchronised directories will exist.";
|
|
||||||
};
|
|
||||||
|
|
||||||
configDir = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
default = cfg.dataDir + "/.config/syncthing";
|
|
||||||
description = "The path where the settings and keys will exist.";
|
|
||||||
};
|
|
||||||
|
|
||||||
openDefaultPorts = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Whether to open the default ports in the firewall (not applicable on Darwin).";
|
|
||||||
};
|
|
||||||
|
|
||||||
package = mkPackageOption pkgs "syncthing" { };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = !(cfg.overrideFolders && anyAutoAccept);
|
|
||||||
message = "services.syncthing.overrideFolders will delete auto-accepted folders from the configuration, creating path conflicts.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ cfg.package ];
|
|
||||||
|
|
||||||
launchd.user.agents.syncthing = {
|
|
||||||
serviceConfig = {
|
|
||||||
ProgramArguments = [
|
|
||||||
"${cfg.package}/bin/syncthing"
|
|
||||||
"-no-browser"
|
|
||||||
"-gui-address=${if isUnixGui then "unix://" else ""}${cfg.guiAddress}"
|
|
||||||
"-config=${cfg.configDir}"
|
|
||||||
"-data=${cfg.configDir}"
|
|
||||||
];
|
|
||||||
EnvironmentVariables = {
|
|
||||||
STNORESTART = "yes";
|
|
||||||
STNOUPGRADE = "yes";
|
|
||||||
};
|
|
||||||
KeepAlive = true;
|
|
||||||
RunAtLoad = true;
|
|
||||||
ProcessType = "Background";
|
|
||||||
StandardOutPath = "${cfg.configDir}/syncthing.log";
|
|
||||||
StandardErrorPath = "${cfg.configDir}/syncthing.log";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
launchd.user.agents.syncthing-init = mkIf (cleanedConfig != { }) {
|
|
||||||
serviceConfig = {
|
|
||||||
ProgramArguments = [ "${updateConfig}" ];
|
|
||||||
RunAtLoad = true;
|
|
||||||
KeepAlive = false;
|
|
||||||
ProcessType = "Background";
|
|
||||||
StandardOutPath = "${cfg.configDir}/syncthing-init.log";
|
|
||||||
StandardErrorPath = "${cfg.configDir}/syncthing-init.log";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
system.activationScripts.syncthing = mkIf (cfg.cert != null || cfg.key != null) ''
|
|
||||||
echo "Setting up Syncthing certificates..."
|
|
||||||
mkdir -p ${cfg.configDir}
|
|
||||||
${optionalString (cfg.cert != null) ''
|
|
||||||
cp ${toString cfg.cert} ${cfg.configDir}/cert.pem
|
|
||||||
chmod 644 ${cfg.configDir}/cert.pem
|
|
||||||
''}
|
|
||||||
${optionalString (cfg.key != null) ''
|
|
||||||
cp ${toString cfg.key} ${cfg.configDir}/key.pem
|
|
||||||
chmod 600 ${cfg.configDir}/key.pem
|
|
||||||
''}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,20 @@
|
|||||||
{
|
{
|
||||||
user,
|
user,
|
||||||
pkgs,
|
pkgs,
|
||||||
|
lib,
|
||||||
...
|
...
|
||||||
}: {
|
}: let
|
||||||
|
isDarwin = pkgs.stdenv.isDarwin;
|
||||||
|
isLinux = pkgs.stdenv.isLinux;
|
||||||
|
homeDir = if isDarwin then "/Users/${user}" else "/home/${user}";
|
||||||
|
in {
|
||||||
services.syncthing = {
|
services.syncthing = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openDefaultPorts = pkgs.stdenv.isLinux;
|
openDefaultPorts = isLinux;
|
||||||
dataDir =
|
dataDir = "${homeDir}/.local/share/syncthing";
|
||||||
if pkgs.stdenv.isDarwin
|
configDir = "${homeDir}/.config/syncthing";
|
||||||
then "/Users/${user}/.local/share/syncthing"
|
|
||||||
else "/home/${user}/.local/share/syncthing";
|
|
||||||
configDir =
|
|
||||||
if pkgs.stdenv.isDarwin
|
|
||||||
then "/Users/${user}/.config/syncthing"
|
|
||||||
else "/home/${user}/.config/syncthing";
|
|
||||||
user = "${user}";
|
user = "${user}";
|
||||||
group =
|
group = if isDarwin then "staff" else "users";
|
||||||
if pkgs.stdenv.isDarwin
|
|
||||||
then "staff"
|
|
||||||
else "users";
|
|
||||||
guiAddress = "0.0.0.0:8384";
|
guiAddress = "0.0.0.0:8384";
|
||||||
overrideFolders = true;
|
overrideFolders = true;
|
||||||
overrideDevices = true;
|
overrideDevices = true;
|
||||||
@@ -30,10 +26,7 @@
|
|||||||
};
|
};
|
||||||
folders = {
|
folders = {
|
||||||
"Projects" = {
|
"Projects" = {
|
||||||
path =
|
path = "${homeDir}/Projects";
|
||||||
if pkgs.stdenv.isDarwin
|
|
||||||
then "/Users/${user}/Projects"
|
|
||||||
else "/home/${user}/Projects";
|
|
||||||
devices = ["tahani" "jason"];
|
devices = ["tahani" "jason"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# Overlays
|
|
||||||
|
|
||||||
Files in this directory run automatically as part of each build.
|
|
||||||
@@ -1,383 +1,386 @@
|
|||||||
final: prev: {
|
final: prev: {
|
||||||
# Create a Darwin-compatible Syncthing service module
|
darwinSyncthingModule = { config, lib, pkgs, ... }:
|
||||||
darwinModules = prev.darwinModules or {} // {
|
with lib;
|
||||||
syncthing = { config, lib, pkgs, ... }:
|
let
|
||||||
with lib;
|
cfg = config.services.syncthing;
|
||||||
let
|
defaultUser = "syncthing";
|
||||||
cfg = config.services.syncthing;
|
defaultGroup = defaultUser;
|
||||||
defaultUser = "syncthing";
|
settingsFormat = pkgs.formats.json { };
|
||||||
defaultGroup = defaultUser;
|
cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != { })) cfg.settings;
|
||||||
settingsFormat = pkgs.formats.json { };
|
|
||||||
cleanedConfig = converge (filterAttrsRecursive (_: v: v != null && v != { })) cfg.settings;
|
|
||||||
|
|
||||||
isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/";
|
isUnixGui = (builtins.substring 0 1 cfg.guiAddress) == "/";
|
||||||
|
|
||||||
curlAddressArgs = path:
|
curlAddressArgs = path:
|
||||||
if isUnixGui
|
if isUnixGui
|
||||||
then "--unix-socket ${cfg.guiAddress} http://.${path}"
|
then "--unix-socket ${cfg.guiAddress} http://.${path}"
|
||||||
else "${cfg.guiAddress}${path}";
|
else "${cfg.guiAddress}${path}";
|
||||||
|
|
||||||
devices = mapAttrsToList (_: device: device // { deviceID = device.id; }) cfg.settings.devices;
|
devices = mapAttrsToList (_: device: device // { deviceID = device.id; }) cfg.settings.devices;
|
||||||
anyAutoAccept = builtins.any (dev: dev.autoAcceptFolders) devices;
|
anyAutoAccept = builtins.any (dev: dev.autoAcceptFolders) devices;
|
||||||
|
|
||||||
folders = mapAttrsToList (_: folder: folder // {
|
folders = mapAttrsToList (_: folder: folder // {
|
||||||
devices = let
|
devices = let
|
||||||
folderDevices = folder.devices;
|
folderDevices = folder.devices;
|
||||||
in
|
in
|
||||||
map (device:
|
map (device:
|
||||||
if builtins.isString device then
|
if builtins.isString device then
|
||||||
{ deviceId = cfg.settings.devices.${device}.id; }
|
{ deviceId = cfg.settings.devices.${device}.id; }
|
||||||
else if builtins.isAttrs device then
|
else if builtins.isAttrs device then
|
||||||
{ deviceId = cfg.settings.devices.${device.name}.id; } // device
|
{ deviceId = cfg.settings.devices.${device.name}.id; } // device
|
||||||
else
|
else
|
||||||
throw "Invalid type for devices in folder; expected list or attrset."
|
throw "Invalid type for devices in folder; expected list or attrset."
|
||||||
) folderDevices;
|
) folderDevices;
|
||||||
}) (filterAttrs (_: folder: folder.enable) cfg.settings.folders);
|
}) (filterAttrs (_: folder: folder.enable) cfg.settings.folders);
|
||||||
|
|
||||||
jq = "${pkgs.jq}/bin/jq";
|
jq = "${pkgs.jq}/bin/jq";
|
||||||
updateConfig = pkgs.writers.writeBash "merge-syncthing-config" (
|
updateConfig = pkgs.writers.writeBash "merge-syncthing-config" (
|
||||||
''
|
''
|
||||||
set -efu
|
set -efu
|
||||||
umask 0077
|
umask 0077
|
||||||
|
|
||||||
curl() {
|
curl() {
|
||||||
while
|
while
|
||||||
! ${pkgs.libxml2}/bin/xmllint \
|
! ${pkgs.libxml2}/bin/xmllint \
|
||||||
--xpath 'string(configuration/gui/apikey)' \
|
--xpath 'string(configuration/gui/apikey)' \
|
||||||
${cfg.configDir}/config.xml \
|
${cfg.configDir}/config.xml \
|
||||||
>"$TMPDIR/api_key"
|
>"$TMPDIR/api_key"
|
||||||
do sleep 1; done
|
do sleep 1; done
|
||||||
(printf "X-API-Key: "; cat "$TMPDIR/api_key") >"$TMPDIR/headers"
|
(printf "X-API-Key: "; cat "$TMPDIR/api_key") >"$TMPDIR/headers"
|
||||||
${pkgs.curl}/bin/curl -sSLk -H "@$TMPDIR/headers" \
|
${pkgs.curl}/bin/curl -sSLk -H "@$TMPDIR/headers" \
|
||||||
--retry 1000 --retry-delay 1 --retry-all-errors \
|
--retry 1000 --retry-delay 1 --retry-all-errors \
|
||||||
"$@"
|
"$@"
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
+ (lib.pipe {
|
+ (lib.pipe {
|
||||||
devs = {
|
devs = {
|
||||||
new_conf_IDs = map (v: v.id) devices;
|
new_conf_IDs = map (v: v.id) devices;
|
||||||
GET_IdAttrName = "deviceID";
|
GET_IdAttrName = "deviceID";
|
||||||
override = cfg.overrideDevices;
|
override = cfg.overrideDevices;
|
||||||
conf = devices;
|
conf = devices;
|
||||||
baseAddress = curlAddressArgs "/rest/config/devices";
|
baseAddress = curlAddressArgs "/rest/config/devices";
|
||||||
|
};
|
||||||
|
dirs = {
|
||||||
|
new_conf_IDs = map (v: v.id) folders;
|
||||||
|
GET_IdAttrName = "id";
|
||||||
|
override = cfg.overrideFolders;
|
||||||
|
conf = folders;
|
||||||
|
baseAddress = curlAddressArgs "/rest/config/folders";
|
||||||
|
};
|
||||||
|
} [
|
||||||
|
(mapAttrs (conf_type: s:
|
||||||
|
lib.pipe s.conf [
|
||||||
|
(map (new_cfg:
|
||||||
|
let
|
||||||
|
jsonPreSecretsFile = pkgs.writeTextFile {
|
||||||
|
name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json";
|
||||||
|
text = builtins.toJSON new_cfg;
|
||||||
|
};
|
||||||
|
injectSecretsJqCmd = {
|
||||||
|
"devs" = "${jq} .";
|
||||||
|
"dirs" = let
|
||||||
|
folder = new_cfg;
|
||||||
|
devicesWithSecrets = lib.pipe folder.devices [
|
||||||
|
(lib.filter (device: (builtins.isAttrs device) && device ? encryptionPasswordFile))
|
||||||
|
(map (device: {
|
||||||
|
deviceId = device.deviceId;
|
||||||
|
variableName = "secret_${builtins.hashString "sha256" device.encryptionPasswordFile}";
|
||||||
|
secretPath = device.encryptionPasswordFile;
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
jqUpdates = map (device: ''
|
||||||
|
.devices[] |= (
|
||||||
|
if .deviceId == "${device.deviceId}" then
|
||||||
|
del(.encryptionPasswordFile) |
|
||||||
|
.encryptionPassword = ''$${device.variableName}
|
||||||
|
else
|
||||||
|
.
|
||||||
|
end
|
||||||
|
)
|
||||||
|
'') devicesWithSecrets;
|
||||||
|
jqRawFiles = map (device: "--rawfile ${device.variableName} ${lib.escapeShellArg device.secretPath}") devicesWithSecrets;
|
||||||
|
in
|
||||||
|
"${jq} ${lib.concatStringsSep " " jqRawFiles} ${lib.escapeShellArg (lib.concatStringsSep "|" ([ "." ] ++ jqUpdates))}";
|
||||||
|
}.${conf_type};
|
||||||
|
in
|
||||||
|
''
|
||||||
|
${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress}
|
||||||
|
''
|
||||||
|
))
|
||||||
|
(lib.concatStringsSep "\n")
|
||||||
|
]
|
||||||
|
+ lib.optionalString s.override ''
|
||||||
|
stale_${conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} \
|
||||||
|
--argjson new_ids ${lib.escapeShellArg (builtins.toJSON s.new_conf_IDs)} \
|
||||||
|
--raw-output \
|
||||||
|
'[.[].${s.GET_IdAttrName}] - $new_ids | .[]'
|
||||||
|
)"
|
||||||
|
for id in ''${stale_${conf_type}_ids}; do
|
||||||
|
>&2 echo "Deleting stale device: $id"
|
||||||
|
curl -X DELETE ${s.baseAddress}/$id
|
||||||
|
done
|
||||||
|
''
|
||||||
|
))
|
||||||
|
builtins.attrValues
|
||||||
|
(lib.concatStringsSep "\n")
|
||||||
|
])
|
||||||
|
+ (lib.pipe cleanedConfig [
|
||||||
|
builtins.attrNames
|
||||||
|
(lib.subtractLists [ "folders" "devices" ])
|
||||||
|
(map (subOption: ''
|
||||||
|
curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} ${curlAddressArgs "/rest/config/${subOption}"}
|
||||||
|
''))
|
||||||
|
(lib.concatStringsSep "\n")
|
||||||
|
])
|
||||||
|
+ ''
|
||||||
|
if curl ${curlAddressArgs "/rest/config/restart-required"} |
|
||||||
|
${jq} -e .requiresRestart > /dev/null; then
|
||||||
|
curl -X POST ${curlAddressArgs "/rest/system/restart"}
|
||||||
|
fi
|
||||||
|
''
|
||||||
|
);
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
services.syncthing = {
|
||||||
|
enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
|
||||||
|
|
||||||
|
cert = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
description = "Path to the cert.pem file, which will be copied into Syncthing's configDir.";
|
||||||
};
|
};
|
||||||
dirs = {
|
|
||||||
new_conf_IDs = map (v: v.id) folders;
|
key = mkOption {
|
||||||
GET_IdAttrName = "id";
|
type = types.nullOr types.str;
|
||||||
override = cfg.overrideFolders;
|
default = null;
|
||||||
conf = folders;
|
description = "Path to the key.pem file, which will be copied into Syncthing's configDir.";
|
||||||
baseAddress = curlAddressArgs "/rest/config/folders";
|
|
||||||
};
|
};
|
||||||
} [
|
|
||||||
(mapAttrs (conf_type: s:
|
|
||||||
lib.pipe s.conf [
|
|
||||||
(map (new_cfg:
|
|
||||||
let
|
|
||||||
jsonPreSecretsFile = pkgs.writeTextFile {
|
|
||||||
name = "${conf_type}-${new_cfg.id}-conf-pre-secrets.json";
|
|
||||||
text = builtins.toJSON new_cfg;
|
|
||||||
};
|
|
||||||
injectSecretsJqCmd = {
|
|
||||||
"devs" = "${jq} .";
|
|
||||||
"dirs" = let
|
|
||||||
folder = new_cfg;
|
|
||||||
devicesWithSecrets = lib.pipe folder.devices [
|
|
||||||
(lib.filter (device: (builtins.isAttrs device) && device ? encryptionPasswordFile))
|
|
||||||
(map (device: {
|
|
||||||
deviceId = device.deviceId;
|
|
||||||
variableName = "secret_${builtins.hashString "sha256" device.encryptionPasswordFile}";
|
|
||||||
secretPath = device.encryptionPasswordFile;
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
jqUpdates = map (device: ''
|
|
||||||
.devices[] |= (
|
|
||||||
if .deviceId == "${device.deviceId}" then
|
|
||||||
del(.encryptionPasswordFile) |
|
|
||||||
.encryptionPassword = ''$${device.variableName}
|
|
||||||
else
|
|
||||||
.
|
|
||||||
end
|
|
||||||
)
|
|
||||||
'') devicesWithSecrets;
|
|
||||||
jqRawFiles = map (device: "--rawfile ${device.variableName} ${lib.escapeShellArg device.secretPath}") devicesWithSecrets;
|
|
||||||
in
|
|
||||||
"${jq} ${lib.concatStringsSep " " jqRawFiles} ${lib.escapeShellArg (lib.concatStringsSep "|" ([ "." ] ++ jqUpdates))}";
|
|
||||||
}.${conf_type};
|
|
||||||
in
|
|
||||||
''
|
|
||||||
${injectSecretsJqCmd} ${jsonPreSecretsFile} | curl --json @- -X POST ${s.baseAddress}
|
|
||||||
''
|
|
||||||
))
|
|
||||||
(lib.concatStringsSep "\n")
|
|
||||||
]
|
|
||||||
+ lib.optionalString s.override ''
|
|
||||||
stale_${conf_type}_ids="$(curl -X GET ${s.baseAddress} | ${jq} \
|
|
||||||
--argjson new_ids ${lib.escapeShellArg (builtins.toJSON s.new_conf_IDs)} \
|
|
||||||
--raw-output \
|
|
||||||
'[.[].${s.GET_IdAttrName}] - $new_ids | .[]'
|
|
||||||
)"
|
|
||||||
for id in ''${stale_${conf_type}_ids}; do
|
|
||||||
>&2 echo "Deleting stale device: $id"
|
|
||||||
curl -X DELETE ${s.baseAddress}/$id
|
|
||||||
done
|
|
||||||
''
|
|
||||||
))
|
|
||||||
builtins.attrValues
|
|
||||||
(lib.concatStringsSep "\n")
|
|
||||||
])
|
|
||||||
+ (lib.pipe cleanedConfig [
|
|
||||||
builtins.attrNames
|
|
||||||
(lib.subtractLists [ "folders" "devices" ])
|
|
||||||
(map (subOption: ''
|
|
||||||
curl -X PUT -d ${lib.escapeShellArg (builtins.toJSON cleanedConfig.${subOption})} ${curlAddressArgs "/rest/config/${subOption}"}
|
|
||||||
''))
|
|
||||||
(lib.concatStringsSep "\n")
|
|
||||||
])
|
|
||||||
+ ''
|
|
||||||
if curl ${curlAddressArgs "/rest/config/restart-required"} |
|
|
||||||
${jq} -e .requiresRestart > /dev/null; then
|
|
||||||
curl -X POST ${curlAddressArgs "/rest/system/restart"}
|
|
||||||
fi
|
|
||||||
''
|
|
||||||
);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options = {
|
|
||||||
services.syncthing = {
|
|
||||||
enable = mkEnableOption "Syncthing, a self-hosted open-source alternative to Dropbox and Bittorrent Sync";
|
|
||||||
|
|
||||||
cert = mkOption {
|
overrideDevices = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.bool;
|
||||||
default = null;
|
default = true;
|
||||||
description = "Path to the cert.pem file, which will be copied into Syncthing's configDir.";
|
description = "Whether to delete the devices which are not configured via the devices option.";
|
||||||
};
|
};
|
||||||
|
|
||||||
key = mkOption {
|
overrideFolders = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.bool;
|
||||||
default = null;
|
default = !anyAutoAccept;
|
||||||
description = "Path to the key.pem file, which will be copied into Syncthing's configDir.";
|
description = "Whether to delete the folders which are not configured via the folders option.";
|
||||||
};
|
};
|
||||||
|
|
||||||
overrideDevices = mkOption {
|
settings = mkOption {
|
||||||
type = types.bool;
|
type = types.submodule {
|
||||||
default = true;
|
freeformType = settingsFormat.type;
|
||||||
description = "Whether to delete the devices which are not configured via the devices option.";
|
options = {
|
||||||
};
|
options = mkOption {
|
||||||
|
default = { };
|
||||||
overrideFolders = mkOption {
|
description = "The options element contains all other global configuration options";
|
||||||
type = types.bool;
|
type = types.submodule {
|
||||||
default = !anyAutoAccept;
|
freeformType = settingsFormat.type;
|
||||||
description = "Whether to delete the folders which are not configured via the folders option.";
|
options = {
|
||||||
};
|
localAnnounceEnabled = mkOption {
|
||||||
|
type = types.nullOr types.bool;
|
||||||
settings = mkOption {
|
default = null;
|
||||||
type = types.submodule {
|
description = "Whether to send announcements to the local LAN.";
|
||||||
freeformType = settingsFormat.type;
|
};
|
||||||
options = {
|
globalAnnounceEnabled = mkOption {
|
||||||
options = mkOption {
|
type = types.nullOr types.bool;
|
||||||
default = { };
|
default = null;
|
||||||
description = "The options element contains all other global configuration options";
|
description = "Whether to send announcements to the global discovery servers.";
|
||||||
type = types.submodule {
|
};
|
||||||
freeformType = settingsFormat.type;
|
relaysEnabled = mkOption {
|
||||||
options = {
|
type = types.nullOr types.bool;
|
||||||
localAnnounceEnabled = mkOption {
|
default = null;
|
||||||
type = types.nullOr types.bool;
|
description = "When true, relays will be connected to and potentially used for device to device connections.";
|
||||||
default = null;
|
};
|
||||||
description = "Whether to send announcements to the local LAN.";
|
urAccepted = mkOption {
|
||||||
};
|
type = types.nullOr types.int;
|
||||||
globalAnnounceEnabled = mkOption {
|
default = null;
|
||||||
type = types.nullOr types.bool;
|
description = "Whether the user has accepted to submit anonymous usage data.";
|
||||||
default = null;
|
};
|
||||||
description = "Whether to send announcements to the global discovery servers.";
|
|
||||||
};
|
|
||||||
relaysEnabled = mkOption {
|
|
||||||
type = types.nullOr types.bool;
|
|
||||||
default = null;
|
|
||||||
description = "When true, relays will be connected to and potentially used for device to device connections.";
|
|
||||||
};
|
|
||||||
urAccepted = mkOption {
|
|
||||||
type = types.nullOr types.int;
|
|
||||||
default = null;
|
|
||||||
description = "Whether the user has accepted to submit anonymous usage data.";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
devices = mkOption {
|
devices = mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
description = "Peers/devices which Syncthing should communicate with.";
|
description = "Peers/devices which Syncthing should communicate with.";
|
||||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
type = types.attrsOf (types.submodule ({ name, ... }: {
|
||||||
freeformType = settingsFormat.type;
|
freeformType = settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
name = mkOption {
|
name = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = name;
|
default = name;
|
||||||
description = "The name of the device.";
|
description = "The name of the device.";
|
||||||
|
};
|
||||||
|
id = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "The device ID.";
|
||||||
|
};
|
||||||
|
autoAcceptFolders = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Automatically create or share folders that this device advertises at the default path.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
id = mkOption {
|
}));
|
||||||
type = types.str;
|
};
|
||||||
description = "The device ID.";
|
|
||||||
};
|
|
||||||
autoAcceptFolders = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Automatically create or share folders that this device advertises at the default path.";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
folders = mkOption {
|
folders = mkOption {
|
||||||
default = { };
|
default = { };
|
||||||
description = "Folders which should be shared by Syncthing.";
|
description = "Folders which should be shared by Syncthing.";
|
||||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
type = types.attrsOf (types.submodule ({ name, ... }: {
|
||||||
freeformType = settingsFormat.type;
|
freeformType = settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
enable = mkOption {
|
enable = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
description = "Whether to share this folder.";
|
description = "Whether to share this folder.";
|
||||||
};
|
};
|
||||||
path = mkOption {
|
path = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = name;
|
default = name;
|
||||||
description = "The path to the folder which should be shared.";
|
description = "The path to the folder which should be shared.";
|
||||||
};
|
};
|
||||||
id = mkOption {
|
id = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = name;
|
default = name;
|
||||||
description = "The ID of the folder. Must be the same on all devices.";
|
description = "The ID of the folder. Must be the same on all devices.";
|
||||||
};
|
};
|
||||||
label = mkOption {
|
label = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = name;
|
default = name;
|
||||||
description = "The label of the folder.";
|
description = "The label of the folder.";
|
||||||
};
|
};
|
||||||
type = mkOption {
|
type = mkOption {
|
||||||
type = types.enum [ "sendreceive" "sendonly" "receiveonly" "receiveencrypted" ];
|
type = types.enum [ "sendreceive" "sendonly" "receiveonly" "receiveencrypted" ];
|
||||||
default = "sendreceive";
|
default = "sendreceive";
|
||||||
description = "Controls how the folder is handled by Syncthing.";
|
description = "Controls how the folder is handled by Syncthing.";
|
||||||
};
|
};
|
||||||
devices = mkOption {
|
devices = mkOption {
|
||||||
type = types.listOf (types.oneOf [
|
type = types.listOf (types.oneOf [
|
||||||
types.str
|
types.str
|
||||||
(types.submodule {
|
(types.submodule {
|
||||||
freeformType = settingsFormat.type;
|
freeformType = settingsFormat.type;
|
||||||
options = {
|
options = {
|
||||||
name = mkOption {
|
name = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
description = "The name of a device defined in the devices option.";
|
description = "The name of a device defined in the devices option.";
|
||||||
|
};
|
||||||
|
encryptionPasswordFile = mkOption {
|
||||||
|
type = types.nullOr types.path;
|
||||||
|
default = null;
|
||||||
|
description = "Path to encryption password file.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
encryptionPasswordFile = mkOption {
|
})
|
||||||
type = types.nullOr types.path;
|
]);
|
||||||
default = null;
|
default = [ ];
|
||||||
description = "Path to encryption password file.";
|
description = "The devices this folder should be shared with.";
|
||||||
};
|
};
|
||||||
};
|
|
||||||
})
|
|
||||||
]);
|
|
||||||
default = [ ];
|
|
||||||
description = "The devices this folder should be shared with.";
|
|
||||||
};
|
};
|
||||||
};
|
}));
|
||||||
}));
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
default = { };
|
||||||
|
description = "Extra configuration options for Syncthing.";
|
||||||
|
};
|
||||||
|
|
||||||
|
guiAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1:8384";
|
||||||
|
description = "The address to serve the web interface at.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaultUser;
|
||||||
|
description = "The user to run Syncthing as.";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaultGroup;
|
||||||
|
description = "The group to run Syncthing under.";
|
||||||
|
};
|
||||||
|
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/syncthing";
|
||||||
|
description = "The path where synchronised directories will exist.";
|
||||||
|
};
|
||||||
|
|
||||||
|
configDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = cfg.dataDir + "/.config/syncthing";
|
||||||
|
description = "The path where the settings and keys will exist.";
|
||||||
|
};
|
||||||
|
|
||||||
|
openDefaultPorts = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to open the default ports in the firewall (not applicable on Darwin).";
|
||||||
|
};
|
||||||
|
|
||||||
|
package = mkPackageOption pkgs "syncthing" { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = !(cfg.overrideFolders && anyAutoAccept);
|
||||||
|
message = "services.syncthing.overrideFolders will delete auto-accepted folders from the configuration, creating path conflicts.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = [ cfg.package ];
|
||||||
|
|
||||||
|
launchd.user.agents.syncthing = {
|
||||||
|
serviceConfig = {
|
||||||
|
ProgramArguments = [
|
||||||
|
"${cfg.package}/bin/syncthing"
|
||||||
|
"-no-browser"
|
||||||
|
"-gui-address=${if isUnixGui then "unix://" else ""}${cfg.guiAddress}"
|
||||||
|
"-config=${cfg.configDir}"
|
||||||
|
"-data=${cfg.configDir}"
|
||||||
|
];
|
||||||
|
EnvironmentVariables = {
|
||||||
|
STNORESTART = "yes";
|
||||||
|
STNOUPGRADE = "yes";
|
||||||
|
};
|
||||||
|
KeepAlive = true;
|
||||||
|
RunAtLoad = true;
|
||||||
|
ProcessType = "Background";
|
||||||
|
StandardOutPath = "${cfg.configDir}/syncthing.log";
|
||||||
|
StandardErrorPath = "${cfg.configDir}/syncthing.log";
|
||||||
};
|
};
|
||||||
default = { };
|
|
||||||
description = "Extra configuration options for Syncthing.";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
guiAddress = mkOption {
|
launchd.user.agents.syncthing-init = mkIf (cleanedConfig != { }) {
|
||||||
type = types.str;
|
serviceConfig = {
|
||||||
default = "127.0.0.1:8384";
|
ProgramArguments = [ "${updateConfig}" ];
|
||||||
description = "The address to serve the web interface at.";
|
RunAtLoad = true;
|
||||||
|
KeepAlive = false;
|
||||||
|
ProcessType = "Background";
|
||||||
|
StandardOutPath = "${cfg.configDir}/syncthing-init.log";
|
||||||
|
StandardErrorPath = "${cfg.configDir}/syncthing-init.log";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
user = mkOption {
|
system.activationScripts.syncthing = mkIf (cfg.cert != null || cfg.key != null) ''
|
||||||
type = types.str;
|
echo "Setting up Syncthing certificates..."
|
||||||
default = defaultUser;
|
mkdir -p ${cfg.configDir}
|
||||||
description = "The user to run Syncthing as.";
|
${optionalString (cfg.cert != null) ''
|
||||||
};
|
cp ${toString cfg.cert} ${cfg.configDir}/cert.pem
|
||||||
|
chmod 644 ${cfg.configDir}/cert.pem
|
||||||
group = mkOption {
|
''}
|
||||||
type = types.str;
|
${optionalString (cfg.key != null) ''
|
||||||
default = defaultGroup;
|
cp ${toString cfg.key} ${cfg.configDir}/key.pem
|
||||||
description = "The group to run Syncthing under.";
|
chmod 600 ${cfg.configDir}/key.pem
|
||||||
};
|
''}
|
||||||
|
'';
|
||||||
dataDir = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
default = "/var/lib/syncthing";
|
|
||||||
description = "The path where synchronised directories will exist.";
|
|
||||||
};
|
|
||||||
|
|
||||||
configDir = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
default = cfg.dataDir + "/.config/syncthing";
|
|
||||||
description = "The path where the settings and keys will exist.";
|
|
||||||
};
|
|
||||||
|
|
||||||
package = mkPackageOption pkgs "syncthing" { };
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = !(cfg.overrideFolders && anyAutoAccept);
|
|
||||||
message = "services.syncthing.overrideFolders will delete auto-accepted folders from the configuration, creating path conflicts.";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = [ cfg.package ];
|
|
||||||
|
|
||||||
launchd.user.agents.syncthing = {
|
|
||||||
serviceConfig = {
|
|
||||||
ProgramArguments = [
|
|
||||||
"${cfg.package}/bin/syncthing"
|
|
||||||
"-no-browser"
|
|
||||||
"-gui-address=${if isUnixGui then "unix://" else ""}${cfg.guiAddress}"
|
|
||||||
"-config=${cfg.configDir}"
|
|
||||||
"-data=${cfg.configDir}"
|
|
||||||
];
|
|
||||||
EnvironmentVariables = {
|
|
||||||
STNORESTART = "yes";
|
|
||||||
STNOUPGRADE = "yes";
|
|
||||||
};
|
|
||||||
KeepAlive = true;
|
|
||||||
RunAtLoad = true;
|
|
||||||
ProcessType = "Background";
|
|
||||||
StandardOutPath = "${cfg.configDir}/syncthing.log";
|
|
||||||
StandardErrorPath = "${cfg.configDir}/syncthing.log";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
launchd.user.agents.syncthing-init = mkIf (cleanedConfig != { }) {
|
|
||||||
serviceConfig = {
|
|
||||||
ProgramArguments = [ "${updateConfig}" ];
|
|
||||||
RunAtLoad = true;
|
|
||||||
KeepAlive = false;
|
|
||||||
ProcessType = "Background";
|
|
||||||
StandardOutPath = "${cfg.configDir}/syncthing-init.log";
|
|
||||||
StandardErrorPath = "${cfg.configDir}/syncthing-init.log";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
system.activationScripts.syncthing = mkIf (cfg.cert != null || cfg.key != null) ''
|
|
||||||
echo "Setting up Syncthing certificates..."
|
|
||||||
mkdir -p ${cfg.configDir}
|
|
||||||
${optionalString (cfg.cert != null) ''
|
|
||||||
cp ${toString cfg.cert} ${cfg.configDir}/cert.pem
|
|
||||||
chmod 644 ${cfg.configDir}/cert.pem
|
|
||||||
''}
|
|
||||||
${optionalString (cfg.key != null) ''
|
|
||||||
cp ${toString cfg.key} ${cfg.configDir}/key.pem
|
|
||||||
chmod 600 ${cfg.configDir}/key.pem
|
|
||||||
''}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user