This commit is contained in:
2025-08-13 20:50:46 +02:00
parent 7b5bcdb8b3
commit 69f6374d6d
5 changed files with 811 additions and 22 deletions

30
flake.lock generated
View File

@@ -100,11 +100,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1754974548,
"narHash": "sha256-XMjUjKD/QRPcqUnmSDczSYdw46SilnG0+wkho654DFM=",
"lastModified": 1755107032,
"narHash": "sha256-ckb/RX9rJ/FslBA3K4hYAXgVW/7JdQ50Z+28XZT96zg=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "27a26be51ff0162a8f67660239f9407dba68d7c5",
"rev": "4b6dd06c6a92308c06da5e0e55f2c505237725c9",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"homebrew-cask": {
"flake": false,
"locked": {
"lastModified": 1755061279,
"narHash": "sha256-DRGrgAAcMPM83bfrp3/pMIw7D3JxdBPj/wjGfB2whs0=",
"lastModified": 1755104931,
"narHash": "sha256-jtXcymAnYH/hCvEGaUlt5vpFwqx00/r5Wly9UCqy7vQ=",
"owner": "homebrew",
"repo": "homebrew-cask",
"rev": "bfac3b4fa44cedc18dbcc2bb1efd52e7a129fd8a",
"rev": "53dc289ce38d2561d853b482d0f03914a7f2f985",
"type": "github"
},
"original": {
@@ -148,11 +148,11 @@
"homebrew-core": {
"flake": false,
"locked": {
"lastModified": 1755063843,
"narHash": "sha256-h+EckB4MLcQFVgM9sgHAP+fuvmzJOhKAvHcE8Wt2dyg=",
"lastModified": 1755104716,
"narHash": "sha256-6jp9InEQfaVJR4kSdwb0+D6603DanSUEFF3uYYPIQVM=",
"owner": "homebrew",
"repo": "homebrew-core",
"rev": "d70d9dc733ea84f025ee1d40d24a40b861454887",
"rev": "8aa4cd22422bcb03de07c08c24773257d928e988",
"type": "github"
},
"original": {
@@ -240,11 +240,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1755064407,
"narHash": "sha256-kx0aQ9wtm7966jTZXkFYMk+fcr3kZ+gjdULvRiIVxRQ=",
"lastModified": 1755109924,
"narHash": "sha256-2xroOWuRFMLvqknLQJ8gmpdXuvg9t5qDUJ3KQKq5/GE=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ecaec3fd41536abe3ee2a68f3ae0958036ce92cc",
"rev": "b7f46264dd0a1891d52c9a8b919c2eebbc527638",
"type": "github"
},
"original": {
@@ -278,11 +278,11 @@
"systems": "systems_2"
},
"locked": {
"lastModified": 1755043294,
"narHash": "sha256-X5q/ztJ0PHScZGF88nD1EtY/x2Ob9Dtzj/PleOtjmQg=",
"lastModified": 1755095763,
"narHash": "sha256-cFwtMaONA4uKYk/rBrmFvIAQieZxZytoprzIblTn1HA=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "1d4816820c8efb731a8b967581db916728fbd9e2",
"rev": "ecc7880e00a2a735074243d8a664a931d73beace",
"type": "github"
},
"original": {

View File

@@ -10,6 +10,7 @@
imports = [
../../core
../../networking/tailscale.nix
../../services/syncthing-darwin.nix
../../services/syncthing.nix
./dock
./homebrew.nix

View File

@@ -0,0 +1,386 @@
{ 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
''}
'';
};
}

View File

@@ -1,20 +1,39 @@
{user, ...}: {
{
user,
pkgs,
...
}: {
services.syncthing = {
enable = true;
openDefaultPorts = true;
dataDir = "/home/${user}/.local/share/syncthing";
configDir = "/home/${user}/.config/syncthing";
openDefaultPorts = pkgs.stdenv.isLinux;
dataDir =
if pkgs.stdenv.isDarwin
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}";
group = "users";
group =
if pkgs.stdenv.isDarwin
then "staff"
else "users";
guiAddress = "0.0.0.0:8384";
overrideFolders = true;
overrideDevices = true;
settings = {
devices = {};
devices = {
"jason" = {id = "42II2VO-QYPJG26-ZS3MB2I-AOPVZ67-JJNSE76-U54CO5Y-634A5OG-ECU4YQA";};
"tahani" = {id = "6B7OZZF-TEAMUGO-FBOELXP-Z4OY7EU-5ZHLB5T-V6Z3UDB-Q2DYR43-QBYW6QM";};
};
folders = {
"Projects" = {
path = "/home/${user}/Projects";
path =
if pkgs.stdenv.isDarwin
then "/Users/${user}/Projects"
else "/home/${user}/Projects";
devices = [];
};
};

View File

@@ -0,0 +1,383 @@
final: prev: {
# Create a Darwin-compatible Syncthing service module
darwinModules = prev.darwinModules or {} // {
syncthing = { 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.";
};
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
''}
'';
};
};
};
}