up
This commit is contained in:
30
flake.lock
generated
30
flake.lock
generated
@@ -100,11 +100,11 @@
|
|||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1754974548,
|
"lastModified": 1755107032,
|
||||||
"narHash": "sha256-XMjUjKD/QRPcqUnmSDczSYdw46SilnG0+wkho654DFM=",
|
"narHash": "sha256-ckb/RX9rJ/FslBA3K4hYAXgVW/7JdQ50Z+28XZT96zg=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "home-manager",
|
"repo": "home-manager",
|
||||||
"rev": "27a26be51ff0162a8f67660239f9407dba68d7c5",
|
"rev": "4b6dd06c6a92308c06da5e0e55f2c505237725c9",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -132,11 +132,11 @@
|
|||||||
"homebrew-cask": {
|
"homebrew-cask": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755061279,
|
"lastModified": 1755104931,
|
||||||
"narHash": "sha256-DRGrgAAcMPM83bfrp3/pMIw7D3JxdBPj/wjGfB2whs0=",
|
"narHash": "sha256-jtXcymAnYH/hCvEGaUlt5vpFwqx00/r5Wly9UCqy7vQ=",
|
||||||
"owner": "homebrew",
|
"owner": "homebrew",
|
||||||
"repo": "homebrew-cask",
|
"repo": "homebrew-cask",
|
||||||
"rev": "bfac3b4fa44cedc18dbcc2bb1efd52e7a129fd8a",
|
"rev": "53dc289ce38d2561d853b482d0f03914a7f2f985",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -148,11 +148,11 @@
|
|||||||
"homebrew-core": {
|
"homebrew-core": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755063843,
|
"lastModified": 1755104716,
|
||||||
"narHash": "sha256-h+EckB4MLcQFVgM9sgHAP+fuvmzJOhKAvHcE8Wt2dyg=",
|
"narHash": "sha256-6jp9InEQfaVJR4kSdwb0+D6603DanSUEFF3uYYPIQVM=",
|
||||||
"owner": "homebrew",
|
"owner": "homebrew",
|
||||||
"repo": "homebrew-core",
|
"repo": "homebrew-core",
|
||||||
"rev": "d70d9dc733ea84f025ee1d40d24a40b861454887",
|
"rev": "8aa4cd22422bcb03de07c08c24773257d928e988",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -240,11 +240,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755064407,
|
"lastModified": 1755109924,
|
||||||
"narHash": "sha256-kx0aQ9wtm7966jTZXkFYMk+fcr3kZ+gjdULvRiIVxRQ=",
|
"narHash": "sha256-2xroOWuRFMLvqknLQJ8gmpdXuvg9t5qDUJ3KQKq5/GE=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "ecaec3fd41536abe3ee2a68f3ae0958036ce92cc",
|
"rev": "b7f46264dd0a1891d52c9a8b919c2eebbc527638",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -278,11 +278,11 @@
|
|||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1755043294,
|
"lastModified": 1755095763,
|
||||||
"narHash": "sha256-X5q/ztJ0PHScZGF88nD1EtY/x2Ob9Dtzj/PleOtjmQg=",
|
"narHash": "sha256-cFwtMaONA4uKYk/rBrmFvIAQieZxZytoprzIblTn1HA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixvim",
|
"repo": "nixvim",
|
||||||
"rev": "1d4816820c8efb731a8b967581db916728fbd9e2",
|
"rev": "ecc7880e00a2a735074243d8a664a931d73beace",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
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
|
||||||
|
|||||||
386
modules/services/syncthing-darwin.nix
Normal file
386
modules/services/syncthing-darwin.nix
Normal 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
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,20 +1,39 @@
|
|||||||
{user, ...}: {
|
{
|
||||||
|
user,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
services.syncthing = {
|
services.syncthing = {
|
||||||
enable = true;
|
enable = true;
|
||||||
openDefaultPorts = true;
|
openDefaultPorts = pkgs.stdenv.isLinux;
|
||||||
dataDir = "/home/${user}/.local/share/syncthing";
|
dataDir =
|
||||||
configDir = "/home/${user}/.config/syncthing";
|
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}";
|
user = "${user}";
|
||||||
group = "users";
|
group =
|
||||||
|
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;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
devices = {};
|
devices = {
|
||||||
|
"jason" = {id = "42II2VO-QYPJG26-ZS3MB2I-AOPVZ67-JJNSE76-U54CO5Y-634A5OG-ECU4YQA";};
|
||||||
|
"tahani" = {id = "6B7OZZF-TEAMUGO-FBOELXP-Z4OY7EU-5ZHLB5T-V6Z3UDB-Q2DYR43-QBYW6QM";};
|
||||||
|
};
|
||||||
folders = {
|
folders = {
|
||||||
"Projects" = {
|
"Projects" = {
|
||||||
path = "/home/${user}/Projects";
|
path =
|
||||||
|
if pkgs.stdenv.isDarwin
|
||||||
|
then "/Users/${user}/Projects"
|
||||||
|
else "/home/${user}/Projects";
|
||||||
devices = [];
|
devices = [];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
383
overlays/syncthing-darwin.nix
Normal file
383
overlays/syncthing-darwin.nix
Normal 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
|
||||||
|
''}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user