[bug#75959] services: syncthing: Added support for config file serialization.

Message ID 87frkqhg8w.fsf@zacchae.us
State New
Headers
Series [bug#75959] services: syncthing: Added support for config file serialization. |

Commit Message

Zacchaeus Scheffer Feb. 6, 2025, 10:15 p.m. UTC
  From 7ef311e85b1198c752b2eec57caa0256227e079c Mon Sep 17 00:00:00 2001
From: Zacchaeus <eikcaz@zacchae.us>
Date: Sun, 21 Jul 2024 00:54:25 -0700
Subject: [PATCH] services: syncthing: Added support for config file
 serialization.

* gnu/services/syncthing.scm: (syncthing-config-file) (syncthing-folder)
(syncthing-device) (syncthing-folder-device): New records;
(syncthing-service-type): added special-files-service-type extension for the
config file; (syncthing-files-service): service to create config file
* gnu/home/services/syncthing.scm: (home-syncthing-service-type): extended
home-files-services-type and re-exported more things from
gnu/services/syncthing.scm
* doc/guix.texi: (syncthing-service-type): document additions

Change-Id: I87eeba1ee1fdada8f29c2ee881fbc6bc4113dde9
---
Fixed a bug caused by running system service (home service was fine).
Also changed syncthing-config-file field of syncthing-configuration to
config-file (syncthing-config-file record name unchanged).  Hence,
setting the config file looks something like:

(syncthing-configuration (config-file (syncthing-config-file ...)))

This matches the pattern observed in other services like:

(connman-configuration (general-configuration (connman-general-configuration ...)))


 doc/guix.texi                   | 288 ++++++++++++++++++++
 gnu/home/services/syncthing.scm |  17 +-
 gnu/services/syncthing.scm      | 466 +++++++++++++++++++++++++++++++-
 3 files changed, 768 insertions(+), 3 deletions(-)
  

Comments

Leo Famulari Feb. 7, 2025, 12:21 a.m. UTC | #1
On Thu, Feb 06, 2025 at 05:15:27PM -0500, Zacchaeus wrote:
> From 7ef311e85b1198c752b2eec57caa0256227e079c Mon Sep 17 00:00:00 2001
> From: Zacchaeus <eikcaz@zacchae.us>
> Date: Sun, 21 Jul 2024 00:54:25 -0700
> Subject: [PATCH] services: syncthing: Added support for config file
>  serialization.

Thanks for the updated patch! By the way, when you generate the patches,
please use the Git option --reroll-count, so that we can keep track of
the revisions.

I applied it to the current master branch, and created a VM image based
on the lightweight-desktop template in our repo (attached).

Then, I copied the image out of the store, made it writable, and booted
it with QEMU.

------
$ ./pre-inst-env guix system image --image-type=qcow2 --no-grafts doc/os-config-lightweight-desktop.texi-syncthing --fallback --max-jobs=1 --cores=12 --keep-going --image-size=10G -v3 
/gnu/store/86zz6i1x55irgg1r74riil8avgl8hlp9-image.qcow2
$ cp /gnu/store/86zz6i1x55irgg1r74riil8avgl8hlp9-image.qcow2 ~/tmp/guix.qcow2 && chmod 600 ~/tmp/guix.qcow2
$ qemu-system-x86_64 -nic user,model=virtio-net-pci -enable-kvm -m 1024 /home/leo/tmp/guix.qcow2
------

However, when I have included an instance of syncthing-service-type in
the OS declaration, my user's home directory is owned by root, and I'm
unable to log in as my user. When I remove syncthing-service-type, that
problem does not exist.

Any ideas?
;; -*- mode: scheme; -*-
;; This is an operating system configuration template
;; for a "desktop" setup without full-blown desktop
;; environments.

(use-modules (gnu) (gnu system nss))
(use-service-modules desktop syncthing)
(use-package-modules bootloaders emacs emacs-xyz ratpoison suckless wm
                     web-browsers
                     web
                     xdisorg
                     xorg)

(operating-system
  (host-name "antelope")
  (timezone "Europe/Paris")
  (locale "en_US.utf8")

  (kernel-arguments (list "console=ttyS0,115200"))

  ;; Use the UEFI variant of GRUB with the EFI System
  ;; Partition mounted on /boot/efi.
  (bootloader (bootloader-configuration
                (bootloader grub-bootloader)
                (targets '("/dev/sdX"))))

  ;; Assume the target root file system is labelled "my-root",
  ;; and the EFI System Partition has UUID 1234-ABCD.
  (file-systems (cons
                 (file-system
                         (device (file-system-label "my-root"))
                         (mount-point "/")
                         (type "ext4"))
                 %base-file-systems))

  (users (cons* (user-account
                 (name "leo")
                 (password "")
                 (comment "leo")
                 (group "users")
                 (supplementary-groups '("wheel" "netdev"
                                         "audio" "video")))
                %base-user-accounts))

  ;; Add a bunch of window managers; we can choose one at
  ;; the log-in screen with F1.
  (packages (append (list
                     ;; terminal emulator
                     dillo
                     netsurf
                     rxvt-unicode
                     xterm)
                    %base-packages))

  ;; Use the "desktop" services, which include the X11
  ;; log-in service, networking with NetworkManager, and more.
  (services
    (cons* (service xfce-desktop-service-type)
           (service syncthing-service-type
                    (let ((laptop (syncthing-device (id "M...")))
                          (desktop (syncthing-device (id "X...")
                                                     (addresses '("tcp://foo")))))
                      (syncthing-configuration
                       (user "leo")
                       (config-file (syncthing-config-file
                        (folders (list (syncthing-folder
                                        (label "some-files")
                                        (path "~/data")
                                        (devices (list desktop laptop)))
                                       (syncthing-folder
                                        (label "critical-files")
                                        (path "~/secrets")
                                        (devices
                                         (list desktop
                                               laptop
                                               (syncthing-folder-device
                                                (device desktop)
                                                (encryptionPassword "mypassword"))))))))))))
            %desktop-services))

  ;; Allow resolution of '.local' host names with mDNS.
  (name-service-switch %mdns-host-lookup-nss))
  

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index b1b6d98e74..2a4829a6a6 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -136,6 +136,7 @@  Copyright @copyright{} 2024 Troy Figiel@*
 Copyright @copyright{} 2024 Sharlatan Hellseher@*
 Copyright @copyright{} 2024 45mg@*
 Copyright @copyright{} 2025 Sören Tempel@*
+Copyright @copyright{} 2025 Zacchaeus@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -22669,9 +22670,296 @@  This assumes that the specified group exists.
 Common configuration and data directory.  The default configuration
 directory is @file{$HOME} of the specified Syncthing @code{user}.
 
+@item @code{config-file} (default: @var{#f})
+Either a file-like object that resolves to a syncthing configuration xml
+file, or a syncthing-config-file record (see below).  If set to #f, Guix
+will not try to generate a config file, and the syncthing will generate
+a default one which will not be touched on reconfigure.
+
+@end table
+@end deftp
+
+In the below, only details specific to Guix, or related to how your
+device will ``ping'' others, are presented.  Otherwise, you should
+consult @uref{https://docs.syncthing.net/users/config.html, Syncthing
+config documentation}.  Camelcase is preserved below only as to be
+consistent with its appearance in Syncthing code/documentation.  If you
+would like to migrate to Guix-powered Syncthing configuration, the
+generated config adds newlines/whitespace to the produced config such
+that your old config can be diff'ed with the new one.  You can still
+modify Syncthing from the GUI or through ``introducer'' and
+``autoAcceptFolders'' mechanisms, but such changes will be reset on
+reconfigure.
+
+@deftp {Data Type} syncthing-config-file
+Data type representing the configuration file read by the syncthing
+daemon.
+
+@table @asis
+@item @code{folders} (default: @var{(list (syncthing-folder (id "default") (label "Default Folder") (path "~/Sync")))}
+The default here is the same as Syncthing's default.  The value should
+be a list of @code{syncthing-folder}s.
+
+@item @code{devices} (default: @var{'()}
+This should be a list of @code{syncthing-device}s.  Guix will
+automatically add any devices specified in any `folders' to this list.
+There are instances when you want to connect to a device despite not
+(initially) sharing any folders (such as a device with
+autoAcceptFolders).  In such instances, you should specify those devices
+here.  If multiple versions of the same device (same id) are discovered,
+the one in this list is prioritized.  Otherwise, the first instance in
+the first folder is used.
+
+@item @code{gui-enabled} (default: @var{"true"})
+By default, any user on the computer can access the GUI and make changes
+to Syncthing.  If you leave this enabled, you should probably set
+gui-user and gui-password (see below).
+
+@item @code{gui-tls} (default: @var{"false"})
+@item @code{gui-debugging} (default: @var{"false"})
+@item @code{gui-sendBasicAuthPrompt} (default: @var{"false"})
+@item @code{gui-address} (default: @var{"127.0.0.1:8384"})
+@item @code{gui-user} (default: @var{#f})
+@item @code{gui-password} (default: @var{#f})
+@item @code{gui-apikey} (default: @var{"Vuky3VHVseQEoSk9YgxhSkNTnjQmqYK9"})
+@item @code{gui-theme} (default: @var{"default"})
+@item @code{ldap-enabled} (default: @var{#f})
+@item @code{ldap-address} (default: @var{""})
+@item @code{ldap-bindDN} (default: @var{""})
+@item @code{ldap-transport} (default: @var{""})
+@item @code{ldap-insecureSkipVerify} (default: @var{""})
+@item @code{ldap-searchBaseDN} (default: @var{""})
+@item @code{ldap-searchFilter} (default: @var{""})
+@item @code{listenAddress} (default: @var{"default"})
+@item @code{globalAnnounceServer} (default: @var{"default"})
+@item @code{globalAnnounceEnabled} (default: @var{"true"})
+Global discovery servers can be used to help connect devices at unknown
+IP addresses by storing the last known IP address.
+
+@item @code{localAnnounceEnabled} (default: @var{"true"})
+This makes devices find each other very easily on the same LAN.  Often,
+this will allow you to just plug an Ethernet between two devices, or
+connect one device to the other's hotspot and start syncing.
+
+@item @code{localAnnouncePort} (default: @var{"21027"})
+@item @code{localAnnounceMCAddr} (default: @var{"[ff12::8384]:21027"})
+@item @code{maxSendKbps} (default: @var{"0"})
+@item @code{maxRecvKbps} (default: @var{"0"})
+@item @code{reconnectionIntervalS} (default: @var{"60"})
+@item @code{relaysEnabled} (default: @var{"true"})
+This option allows your Syncthing instance to coordinate with a global
+network of relays to enable syncing between devices when all other
+methods fail.
+
+@item @code{relayReconnectIntervalM} (default: @var{"10"})
+@item @code{startBrowser} (default: @var{"true"})
+@item @code{natEnabled} (default: @var{"true"})
+@item @code{natLeaseMinutes} (default: @var{"60"})
+@item @code{natRenewalMinutes} (default: @var{"30"})
+@item @code{natTimeoutSeconds} (default: @var{"10"})
+@item @code{urAccepted} (default: @var{"0"})
+ur* options control usage reporting.  Set to -1 to disable, or positive
+to enable.  The default (0) has reporting disabled, but you will be
+asked to decide in the GUI.
+
+@item @code{urSeen} (default: @var{"0"})
+@item @code{urUniqueID} (default: @var{""})
+@item @code{urURL} (default: @var{"https://data.syncthing.net/newdata"})
+@item @code{urPostInsecurely} (default: @var{"false"})
+@item @code{urInitialDelayS} (default: @var{"1800"})
+@item @code{autoUpgradeIntervalH} (default: @var{"12"})
+@item @code{upgradeToPreReleases} (default: @var{"false"})
+@item @code{keepTemporariesH} (default: @var{"24"})
+@item @code{cacheIgnoredFiles} (default: @var{"false"})
+@item @code{progressUpdateIntervalS} (default: @var{"5"})
+@item @code{limitBandwidthInLan} (default: @var{"false"})
+@item @code{minHomeDiskFree-unit} (default: @var{"%"})
+@item @code{minHomeDiskFree} (default: @var{"1"})
+@item @code{releasesURL} (default: @var{"https://upgrades.syncthing.net/meta.json"})
+@item @code{overwriteRemoteDeviceNamesOnConnect} (default: @var{"false"})
+@item @code{tempIndexMinBlocks} (default: @var{"10"})
+@item @code{unackedNotificationID} (default: @var{"authenticationUserAndPassword"})
+@item @code{trafficClass} (default: @var{"0"})
+@item @code{setLowPriority} (default: @var{"true"})
+@item @code{maxFolderConcurrency} (default: @var{"0"})
+@item @code{crashReportingURL} (default: @var{"https://crash.syncthing.net/newcrash"})
+@item @code{crashReportingEnabled} (default: @var{"true"})
+@item @code{stunKeepaliveStartS} (default: @var{"180"})
+@item @code{stunKeepaliveMinS} (default: @var{"20"})
+@item @code{stunServer} (default: @var{"default"})
+@item @code{databaseTuning} (default: @var{"auto"})
+@item @code{maxConcurrentIncomingRequestKiB} (default: @var{"0"})
+@item @code{announceLANAddresses} (default: @var{"true"})
+@item @code{sendFullIndexOnUpgrade} (default: @var{"false"})
+@item @code{connectionLimitEnough} (default: @var{"0"})
+@item @code{connectionLimitMax} (default: @var{"0"})
+@item @code{insecureAllowOldTLSVersions} (default: @var{"false"})
+@item @code{connectionPriorityTcpLan} (default: @var{"10"})
+@item @code{connectionPriorityQuicLan} (default: @var{"20"})
+@item @code{connectionPriorityTcpWan} (default: @var{"30"})
+@item @code{connectionPriorityQuicWan} (default: @var{"40"})
+@item @code{connectionPriorityRelay} (default: @var{"50"})
+@item @code{connectionPriorityUpgradeThreshold} (default: @var{"0"})
+@item @code{default-folder} (default: @var{(syncthing-folder (label ""))})
+@item @code{default-device} (default: @var{(syncthing-device (id ""))})
+@item @code{default-ignores} (default: @var{"")})
+The default-* above do not affect folders and devices added by the Guix
+interface.  They will, however, affect folders and devices that are
+added through the GUI, by an ``introducer'', or a device with
+``autoAcceptFolders''.
+@end table
+@end deftp
+
+@deftp {Data Type} syncthing-device
+Data type representing a device to sync with.
+
+@table @asis
+@item @code{id}
+A long hash tied to the keys generated by Syncthing on the first launch.
+You can obtain this from the Syncthing GUI or by inspecting an existing
+Syncthing configuration file.
+
+@item @code{name} (default: @var{""})
+Human readable device name for viewing in the GUI or in scheme.
+
+@item @code{compression} (default: @var{"metadata"})
+@item @code{introducer} (default: @var{"false"})
+@item @code{skipIntroductionRemovals} (default: @var{"false"})
+@item @code{introducedBy} (default: @var{""})
+@item @code{addresses} (default: @var{'("dynamic")})
+List of addresses at which to search for this device.  The special value
+``dynamic'' will have syncthing use several means to find the device.
+
+@item @code{paused} (default: @var{"false"})
+@item @code{autoAcceptFolders} (default: @var{"false"})
+@item @code{maxSendKbps} (default: @var{"0"})
+@item @code{maxRecvKbps} (default: @var{"0"})
+@item @code{maxRequestKiB} (default: @var{"0"})
+@item @code{untrusted} (default: @var{"false"})
+@item @code{remoteGUIPort} (default: @var{"0"})
+@item @code{numConnections} (default: @var{"0")})
+
+@end table
+@end deftp
+
+@deftp {Data Type} syncthing-folder
+Data type representing a folder to be synced.
+
+@table @asis
+@item @code{id} (default: @var{#f})
+This id cannot match the id of any other folder on this device.  If left
+unspecified, it will default to the label (see below).
+
+@item @code{label}
+Human readable label for the folder.
+
+@item @code{path}
+The path at which to store this folder.
+
+@item @code{type} (default: @var{"sendreceive"})
+@item @code{rescanIntervalS} (default: @var{"3600"})
+@item @code{fsWatcherEnabled} (default: @var{"true"})
+@item @code{fsWatcherDelayS} (default: @var{"10"})
+@item @code{ignorePerms} (default: @var{"false"})
+@item @code{autoNormalize} (default: @var{"true"})
+@item @code{devices} (default: @var{'()})
+Devices should be a list of other Syncthing devices.  You do not need to
+specify the current device.  Each device can be listed as a a
+@code{syncthing-device} record or a @code{syncthing-folder-device}
+record if you want files to be encrypted on disk.
+
+@item @code{filesystemType} (default: @var{"basic"})
+@item @code{minDiskFree-unit} (default: @var{"%"})
+@item @code{minDiskFree} (default: @var{"1"})
+@item @code{versioning-type} (default: @var{#f})
+@item @code{versioning-fsPath} (default: @var{""})
+@item @code{versioning-fsType} (default: @var{"basic"})
+@item @code{versioning-cleanupIntervalS} (default: @var{"3600"})
+@item @code{versioning-cleanoutDays} (default: @var{#f})
+@item @code{versioning-keep} (default: @var{#f})
+@item @code{versioning-maxAge} (default: @var{#f})
+@item @code{versioning-command} (default: @var{#f})
+@item @code{copiers} (default: @var{"0"})
+@item @code{pullerMaxPendingKiB} (default: @var{"0"})
+@item @code{hashers} (default: @var{"0"})
+@item @code{order} (default: @var{"random"})
+@item @code{ignoreDelete} (default: @var{"false"})
+@item @code{scanProgressIntervalS} (default: @var{"0"})
+@item @code{pullerPauseS} (default: @var{"0"})
+@item @code{maxConflicts} (default: @var{"10"})
+@item @code{disableSparseFiles} (default: @var{"false"})
+@item @code{disableTempIndexes} (default: @var{"false"})
+@item @code{paused} (default: @var{"false"})
+@item @code{weakHashThresholdPct} (default: @var{"25"})
+@item @code{markerName} (default: @var{".stfolder"})
+@item @code{copyOwnershipFromParent} (default: @var{"false"})
+@item @code{modTimeWindowS} (default: @var{"0"})
+@item @code{maxConcurrentWrites} (default: @var{"2"})
+@item @code{disableFsync} (default: @var{"false"})
+@item @code{blockPullOrder} (default: @var{"standard"})
+@item @code{copyRangeMethod} (default: @var{"standard"})
+@item @code{caseSensitiveFS} (default: @var{"false"})
+@item @code{junctionsAsDirs} (default: @var{"false"})
+@item @code{syncOwnership} (default: @var{"false"})
+@item @code{sendOwnership} (default: @var{"false"})
+@item @code{syncXattrs} (default: @var{"false"})
+@item @code{sendXattrs} (default: @var{"false"})
+@item @code{xattrFilter-maxSingleEntrySize} (default: @var{"1024"})
+@item @code{xattrFilter-maxTotalSize} (default: @var{"4096")})
+@end table
+@end deftp
+
+@deftp {Data Type} syncthing-folder-device
+There is some configuration which is specific to the relationship
+between a specific folder and a specific device.  If you are fine
+leaving these as their default, then you can simply specify a
+syncthing-device instead of a @code{syncthing-folder-device} in
+@code{syncthing-folder}s.
+
+@table @asis
+@item @code{device}
+device should be a @code{syncthing-device} for which this configuration
+applies.
+
+@item @code{introducedBy} (default: @var{""})
+@item @code{encryptionPassword} (default: @var{""})
+if encryptionPassword is non-empty, then it will be used as a password
+to encrypt file chunks as they are synced to that device.  For more info
+on syncing to devices you don't totally trust, see
+@uref{https://docs.syncthing.net/users/untrusted.html, Syncthing Documentation Untrusted}.
+Note that file transfers are always end-to-end encrypted, regardless of
+this setting.
+
 @end table
 @end deftp
 
+Here is a more complex example configuration for illustrative purposes:
+@lisp
+(service syncthing-service-type
+         (let ((laptop (syncthing-device (id "VHOD2D6-...-7XRMDEN")))
+               (desktop (syncthing-device (id "64SAZ37-...-FZJ5GUA")
+                                          (addresses '("mydomain.example"))))
+               (bob-desktop (syncthing-device (id "KYIMEGO-...-FT77EAO"))))
+           (syncthing-configuration
+            (user "alice")
+            (config-file
+             (syncthing-config-file
+               (folders (list (syncthing-folder
+                               (label "some-files")
+                               (path "~/data")
+                               (devices (list desktop laptop)))
+                              (syncthing-folder
+                               (label "critical-files")
+                               (path "~/secrets")
+                               (devices
+                                (list desktop
+                                      laptop
+                                      (syncthing-folder-device
+                                       (device bob-desktop)
+                                       (encryptionPassword "mypassword"))))))))))))
+@end lisp
+
+
 Furthermore, @code{(gnu services ssh)} provides the following services.
 @cindex SSH
 @cindex SSH server
diff --git a/gnu/home/services/syncthing.scm b/gnu/home/services/syncthing.scm
index 8d66a167ce..dd6c752ee4 100644
--- a/gnu/home/services/syncthing.scm
+++ b/gnu/home/services/syncthing.scm
@@ -1,5 +1,6 @@ 
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2023 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2025 Zacchaeus <eikcaz@zacchae.us>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -24,9 +25,23 @@  (define-module (gnu home services syncthing)
   #:use-module (gnu home services shepherd)
   #:export (home-syncthing-service-type)
   #:re-export (syncthing-configuration
-               syncthing-configuration?))
+               syncthing-configuration?
+               syncthing-config-file
+               syncthing-config-file?
+               syncthing-device
+               syncthing-device?
+               syncthing-folder
+               syncthing-folder?
+               syncthing-folder-device
+               syncthing-folder-device?))
 
 (define home-syncthing-service-type
   (service-type
    (inherit (system->home-service-type syncthing-service-type))
+   ;; system->home-service-type does not convert special-files-service-type to
+   ;; home-files-service-type, so redefine extensios
+   (extensions (list (service-extension home-files-service-type
+                                        syncthing-files-service)
+                     (service-extension home-shepherd-service-type
+                                        syncthing-shepherd-service)))
    (default-value (for-home (syncthing-configuration)))))
diff --git a/gnu/services/syncthing.scm b/gnu/services/syncthing.scm
index a7a9c6aadd..31e3dbe75f 100644
--- a/gnu/services/syncthing.scm
+++ b/gnu/services/syncthing.scm
@@ -1,6 +1,7 @@ 
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2021 Oleg Pykhalov <go.wigust@gmail.com>
 ;;; Copyright © 2023 Justin Veilleux <terramorpha@cock.li>
+;;; Copyright © 2025 Zacchaeus <eikcaz@zacchae.us>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -25,9 +26,20 @@  (define-module (gnu services syncthing)
   #:use-module (guix records)
   #:use-module (ice-9 match)
   #:use-module (srfi srfi-1)
+  #:use-module (sxml simple)
   #:export (syncthing-configuration
             syncthing-configuration?
-            syncthing-service-type))
+            syncthing-device
+            syncthing-device?
+            syncthing-config-file
+            syncthing-config-file?
+            syncthing-folder-device
+            syncthing-folder-device?
+            syncthing-folder
+            syncthing-folder?
+            syncthing-service-type
+            syncthing-shepherd-service
+            syncthing-files-service))
 
 ;;; Commentary:
 ;;;
@@ -35,6 +47,438 @@  (define-module (gnu services syncthing)
 ;;;
 ;;; Code:
 
+(define-record-type* <syncthing-device>
+  syncthing-device make-syncthing-device
+  syncthing-device?
+  (id syncthing-device-id)
+  (name syncthing-device-name (default ""))
+  (compression syncthing-device-compression (default "metadata"))
+  (introducer syncthing-device-introducer (default "false"))
+  (skipIntroductionRemovals syncthing-device-skipIntroductionRemovals (default "false"))
+  (introducedBy syncthing-device-introducedBy (default ""))
+  (addresses syncthing-device-addresses (default '("dynamic")))
+  (paused syncthing-device-paused (default "false"))
+  (autoAcceptFolders syncthing-device-autoAcceptFolders (default "false"))
+  (maxSendKbps syncthing-device-maxSendKbps (default "0"))
+  (maxRecvKbps syncthing-device-maxRecvKbps (default "0"))
+  (maxRequestKiB syncthing-device-maxRequestKiB (default "0"))
+  (untrusted syncthing-device-untrusted (default "false"))
+  (remoteGUIPort syncthing-device-remoteGUIPort (default "0"))
+  (numConnections syncthing-device-numConnections (default "0")))
+
+(define syncthing-device->sxml
+  (match-record-lambda <syncthing-device>
+      (id name compression introducer skipIntroductionRemovals introducedBy addresses paused autoAcceptFolders maxSendKbps maxRecvKbps maxRequestKiB untrusted remoteGUIPort numConnections)
+    `(device (@ (id ,id)
+                (name ,name)
+                (compression ,compression)
+                (introducer ,introducer)
+                (skipIntroductionRemovals ,skipIntroductionRemovals)
+                (introducedBy ,introducedBy))
+             ,@(map (lambda (address) `(address ,address)) addresses)
+             (paused ,paused)
+             (autoAcceptFolders ,autoAcceptFolders)
+             (maxSendKbps ,maxSendKbps)
+             (maxRecvKbps ,maxRecvKbps)
+             (maxRequestKiB ,maxRequestKiB)
+             (untrusted ,untrusted)
+             (remoteGUIPort ,remoteGUIPort)
+             (numConnections ,numConnections))))
+
+(define-record-type* <syncthing-folder-device>
+  syncthing-folder-device make-syncthing-folder-device
+  syncthing-folder-device?
+  (device syncthing-folder-device-device)
+  (introducedBy syncthing-folder-device-introducedBy (default (syncthing-device (id ""))))
+  (encryptionPassword syncthing-folder-device-encryptionPassword (default "")))
+
+(define syncthing-folder-device->sxml
+  (match-record-lambda <syncthing-folder-device>
+      (device introducedBy encryptionPassword)
+    `(device (@ (id ,(syncthing-device-id device))
+                (introducedBy ,(syncthing-device-id introducedBy)))
+             (encryptionPassword ,encryptionPassword))))
+
+(define-record-type* <syncthing-folder>
+  syncthing-folder make-syncthing-folder
+  syncthing-folder?
+  (id syncthing-folder-id (default #f))
+  (label syncthing-folder-label)
+  (path syncthing-folder-path)
+  (type syncthing-folder-type (default "sendreceive"))
+  (rescanIntervalS syncthing-folder-rescanIntervalS (default "3600"))
+  (fsWatcherEnabled syncthing-folder-fsWatcherEnabled (default "true"))
+  (fsWatcherDelayS syncthing-folder-fsWatcherDelayS (default "10"))
+  (fsWatcherTimeoutS syncthing-folder-fsWatcherTimeoutS (default "0"))
+  (ignorePerms syncthing-folder-ignorePerms (default "false"))
+  (autoNormalize syncthing-folder-autoNormalize (default "true"))
+  (devices syncthing-folder-devices (default '())
+           (sanitize (lambda (folder-device-list)
+                       (map (lambda (device)
+                              (if (syncthing-folder-device? device)
+                                  device
+                                  (syncthing-folder-device (device device))))
+                            folder-device-list))))
+  (filesystemType syncthing-folder-filesystemType (default "basic"))
+  (minDiskFree-unit syncthing-folder-minDiskFree-unit (default "%"))
+  (minDiskFree syncthing-folder-minDiskFree (default "1"))
+  (versioning-type syncthing-folder-versioning-type (default #f))
+  (versioning-fsPath syncthing-folder-versioning-fsPath (default ""))
+  (versioning-fsType syncthing-folder-versioning-fsType (default "basic"))
+  (versioning-cleanupIntervalS syncthing-folder-versioning-cleanupIntervalS (default "3600"))
+  (versioning-cleanoutDays syncthing-folder-versioning-cleanoutDays (default #f))
+  (versioning-keep syncthing-folder-versioning-keep (default #f))
+  (versioning-maxAge syncthing-folder-versioning-maxAge (default #f))
+  (versioning-command syncthing-folder-versioning-command (default #f))
+  (copiers syncthing-folder-copiers (default "0"))
+  (pullerMaxPendingKiB syncthing-folder-pullerMaxPendingKiB (default "0"))
+  (hashers syncthing-folder-hashers (default "0"))
+  (order syncthing-folder-order (default "random"))
+  (ignoreDelete syncthing-folder-ignoreDelete (default "false"))
+  (scanProgressIntervalS syncthing-folder-scanProgressIntervalS (default "0"))
+  (pullerPauseS syncthing-folder-pullerPauseS (default "0"))
+  (maxConflicts syncthing-folder-maxConflicts (default "10"))
+  (disableSparseFiles syncthing-folder-disableSparseFiles (default "false"))
+  (disableTempIndexes syncthing-folder-disableTempIndexes (default "false"))
+  (paused syncthing-folder-paused (default "false"))
+  (weakHashThresholdPct syncthing-folder-weakHashThresholdPct (default "25"))
+  (markerName syncthing-folder-markerName (default ".stfolder"))
+  (copyOwnershipFromParent syncthing-folder-copyOwnershipFromParent (default "false"))
+  (modTimeWindowS syncthing-folder-modTimeWindowS (default "0"))
+  (maxConcurrentWrites syncthing-folder-maxConcurrentWrites (default "2"))
+  (disableFsync syncthing-folder-disableFsync (default "false"))
+  (blockPullOrder syncthing-folder-blockPullOrder (default "standard"))
+  (copyRangeMethod syncthing-folder-copyRangeMethod (default "standard"))
+  (caseSensitiveFS syncthing-folder-caseSensitiveFS (default "false"))
+  (junctionsAsDirs syncthing-folder-junctionsAsDirs (default "false"))
+  (syncOwnership syncthing-folder-syncOwnership (default "false"))
+  (sendOwnership syncthing-folder-sendOwnership (default "false"))
+  (syncXattrs syncthing-folder-syncXattrs (default "false"))
+  (sendXattrs syncthing-folder-sendXattrs (default "false"))
+  (xattrFilter-maxSingleEntrySize syncthing-folder-xattrFilter-maxSingleEntrySize (default "1024"))
+  (xattrFilter-maxTotalSize syncthing-folder-xattrFilter-maxTotalSize (default "4096")))
+
+;; Some parameters, when empty, are fully omitted from the config file.  It is
+;; unknown if this causes a functional difference, but stick to the normal
+;; program's behavior to be safe.
+(define (maybe-param symbol value)
+  (if value `((param (@ (key ,(symbol->string symbol)) (val ,value)) "")) '()))
+
+(define syncthing-folder->sxml
+  (match-record-lambda <syncthing-folder>
+      (id
+       label path type rescanIntervalS fsWatcherEnabled fsWatcherDelayS
+       fsWatcherTimeoutS ignorePerms autoNormalize devices filesystemType
+       minDiskFree-unit minDiskFree versioning-type versioning-fsPath
+       versioning-fsType versioning-cleanupIntervalS versioning-cleanoutDays
+       versioning-keep versioning-maxAge versioning-command copiers
+       pullerMaxPendingKiB hashers order ignoreDelete scanProgressIntervalS
+       pullerPauseS maxConflicts disableSparseFiles disableTempIndexes paused
+       weakHashThresholdPct markerName copyOwnershipFromParent modTimeWindowS
+       maxConcurrentWrites disableFsync blockPullOrder copyRangeMethod
+       caseSensitiveFS junctionsAsDirs syncOwnership sendOwnership syncXattrs
+       sendXattrs xattrFilter-maxSingleEntrySize xattrFilter-maxTotalSize)
+    `(folder (@ (id ,(if id id label))
+                (label ,label)
+                (path ,path)
+                (type ,type)
+                (rescanIntervalS ,rescanIntervalS)
+                (fsWatcherEnabled ,fsWatcherEnabled)
+                (fsWatcherDelayS ,fsWatcherDelayS)
+                (fsWatcherTimeoutS ,fsWatcherTimeoutS)
+                (ignorePerms ,ignorePerms)
+                (autoNormalize ,autoNormalize))
+             (filesystemType ,filesystemType)
+             ,@(map syncthing-folder-device->sxml
+                    devices)
+             (minDiskFree (@ (unit ,minDiskFree-unit))
+                          ,minDiskFree)
+             (versioning ,@(if versioning-type
+                               `((@ (type ,versioning-type)))
+                               '())
+                         ,@(maybe-param 'cleanoutDays versioning-cleanoutDays)
+                         ,@(maybe-param 'keep versioning-keep)
+                         ,@(maybe-param 'maxAge versioning-maxAge)
+                         ,@(maybe-param 'command versioning-command)
+                         (cleanupIntervalS ,versioning-cleanupIntervalS)
+                         (fsPath ,versioning-fsPath)
+                         (fsType ,versioning-fsType))
+             (copiers ,copiers)
+             (pullerMaxPendingKiB ,pullerMaxPendingKiB)
+             (hashers ,hashers)
+             (order ,order)
+             (ignoreDelete ,ignoreDelete)
+             (scanProgressIntervalS ,scanProgressIntervalS)
+             (pullerPauseS ,pullerPauseS)
+             (maxConflicts ,maxConflicts)
+             (disableSparseFiles ,disableSparseFiles)
+             (disableTempIndexes ,disableTempIndexes)
+             (paused ,paused)
+             (weakHashThresholdPct ,weakHashThresholdPct)
+             (markerName ,markerName)
+             (copyOwnershipFromParent ,copyOwnershipFromParent)
+             (modTimeWindowS ,modTimeWindowS)
+             (maxConcurrentWrites ,maxConcurrentWrites)
+             (disableFsync ,disableFsync)
+             (blockPullOrder ,blockPullOrder)
+             (copyRangeMethod ,copyRangeMethod)
+             (caseSensitiveFS ,caseSensitiveFS)
+             (junctionsAsDirs ,junctionsAsDirs)
+             (syncOwnership ,syncOwnership)
+             (sendOwnership ,sendOwnership)
+             (syncXattrs ,syncXattrs)
+             (sendXattrs ,sendXattrs)
+             (xattrFilter (maxSingleEntrySize ,xattrFilter-maxSingleEntrySize)
+                          (maxTotalSize ,xattrFilter-maxTotalSize)))))
+
+(define-record-type* <syncthing-config-file>
+  syncthing-config-file make-syncthing-config-file
+  syncthing-config-file?
+  (folders syncthing-config-folders
+           ; this matches syncthing's default
+           (default (list (syncthing-folder (id "default")
+                                            (label "Default Folder")
+                                            (path "~/Sync")))))
+  (devices syncthing-config-devices
+           (default '()))
+  (gui-enabled syncthing-config-gui-enabled (default "true"))
+  (gui-tls syncthing-config-gui-tls (default "false"))
+  (gui-debugging syncthing-config-gui-debugging (default "false"))
+  (gui-sendBasicAuthPrompt syncthing-config-gui-sendBasicAuthPrompt (default "false"))
+  (gui-address syncthing-config-gui-address (default "127.0.0.1:8384"))
+  (gui-user syncthing-config-gui-user (default #f))
+  (gui-password syncthing-config-gui-password (default #f))
+  (gui-apikey syncthing-config-gui-apikey (default "Vuky3VHVseQEoSk9YgxhSkNTnjQmqYK9"))
+  (gui-theme syncthing-config-gui-theme (default "default"))
+  (ldap-enabled syncthing-config-ldap-enabled (default #f))
+  (ldap-address syncthing-config-ldap-address (default ""))
+  (ldap-bindDN syncthing-config-ldap-bindDN (default ""))
+  (ldap-transport syncthing-config-ldap-transport (default ""))
+  (ldap-insecureSkipVerify syncthing-config-ldap-insecureSkipVerify (default ""))
+  (ldap-searchBaseDN syncthing-config-ldap-searchBaseDN (default ""))
+  (ldap-searchFilter syncthing-config-ldap-searchFilter (default ""))
+  (listenAddress syncthing-config-listenAddress (default "default"))
+  (globalAnnounceServer syncthing-config-globalAnnounceServer (default "default"))
+  (globalAnnounceEnabled syncthing-config-globalAnnounceEnabled (default "true"))
+  (localAnnounceEnabled syncthing-config-localAnnounceEnabled (default "true"))
+  (localAnnouncePort syncthing-config-localAnnouncePort (default "21027"))
+  (localAnnounceMCAddr syncthing-config-localAnnounceMCAddr (default "[ff12::8384]:21027"))
+  (maxSendKbps syncthing-config-maxSendKbps (default "0"))
+  (maxRecvKbps syncthing-config-maxRecvKbps (default "0"))
+  (reconnectionIntervalS syncthing-config-reconnectionIntervalS (default "60"))
+  (relaysEnabled syncthing-config-relaysEnabled (default "true"))
+  (relayReconnectIntervalM syncthing-config-relayReconnectIntervalM (default "10"))
+  (startBrowser syncthing-config-startBrowser (default "true"))
+  (natEnabled syncthing-config-natEnabled (default "true"))
+  (natLeaseMinutes syncthing-config-natLeaseMinutes (default "60"))
+  (natRenewalMinutes syncthing-config-natRenewalMinutes (default "30"))
+  (natTimeoutSeconds syncthing-config-natTimeoutSeconds (default "10"))
+  (urAccepted syncthing-config-urAccepted (default "0"))
+  (urSeen syncthing-config-urSeen (default "0"))
+  (urUniqueID syncthing-config-urUniqueID (default ""))
+  (urURL syncthing-config-urURL (default "https://data.syncthing.net/newdata"))
+  (urPostInsecurely syncthing-config-urPostInsecurely (default "false"))
+  (urInitialDelayS syncthing-config-urInitialDelayS (default "1800"))
+  (autoUpgradeIntervalH syncthing-config-autoUpgradeIntervalH (default "12"))
+  (upgradeToPreReleases syncthing-config-upgradeToPreReleases (default "false"))
+  (keepTemporariesH syncthing-config-keepTemporariesH (default "24"))
+  (cacheIgnoredFiles syncthing-config-cacheIgnoredFiles (default "false"))
+  (progressUpdateIntervalS syncthing-config-progressUpdateIntervalS (default "5"))
+  (limitBandwidthInLan syncthing-config-limitBandwidthInLan (default "false"))
+  (minHomeDiskFree-unit syncthing-config-minHomeDiskFree-unit (default "%"))
+  (minHomeDiskFree syncthing-config-minHomeDiskFree (default "1"))
+  (releasesURL syncthing-config-releasesURL (default "https://upgrades.syncthing.net/meta.json"))
+  (overwriteRemoteDeviceNamesOnConnect syncthing-config-overwriteRemoteDeviceNamesOnConnect (default "false"))
+  (tempIndexMinBlocks syncthing-config-tempIndexMinBlocks (default "10"))
+  (unackedNotificationID syncthing-config-unackedNotificationID (default "authenticationUserAndPassword"))
+  (trafficClass syncthing-config-trafficClass (default "0"))
+  (setLowPriority syncthing-config-setLowPriority (default "true"))
+  (maxFolderConcurrency syncthing-config-maxFolderConcurrency (default "0"))
+  (crashReportingURL syncthing-config-crashReportingURL (default "https://crash.syncthing.net/newcrash"))
+  (crashReportingEnabled syncthing-config-crashReportingEnabled (default "true"))
+  (stunKeepaliveStartS syncthing-config-stunKeepaliveStartS (default "180"))
+  (stunKeepaliveMinS syncthing-config-stunKeepaliveMinS (default "20"))
+  (stunServer syncthing-config-stunServer (default "default"))
+  (databaseTuning syncthing-config-databaseTuning (default "auto"))
+  (maxConcurrentIncomingRequestKiB syncthing-config-maxConcurrentIncomingRequestKiB (default "0"))
+  (announceLANAddresses syncthing-config-announceLANAddresses (default "true"))
+  (sendFullIndexOnUpgrade syncthing-config-sendFullIndexOnUpgrade (default "false"))
+  (connectionLimitEnough syncthing-config-connectionLimitEnough (default "0"))
+  (connectionLimitMax syncthing-config-connectionLimitMax (default "0"))
+  (insecureAllowOldTLSVersions syncthing-config-insecureAllowOldTLSVersions (default "false"))
+  (connectionPriorityTcpLan syncthing-config-connectionPriorityTcpLan (default "10"))
+  (connectionPriorityQuicLan syncthing-config-connectionPriorityQuicLan (default "20"))
+  (connectionPriorityTcpWan syncthing-config-connectionPriorityTcpWan (default "30"))
+  (connectionPriorityQuicWan syncthing-config-connectionPriorityQuicWan (default "40"))
+  (connectionPriorityRelay syncthing-config-connectionPriorityRelay (default "50"))
+  (connectionPriorityUpgradeThreshold syncthing-config-connectionPriorityUpgradeThreshold (default "0"))
+  (default-folder syncthing-config-defaultFolder
+    (default (syncthing-folder (label "") (path "~"))))
+  (default-device syncthing-config-defaultDevice
+    (default (syncthing-device (id ""))))
+  (default-ignores syncthing-config-defaultIgnores (default "")))
+
+(define syncthing-config-file->sxml
+  (match-record-lambda <syncthing-config-file>
+      (folders
+       devices gui-enabled gui-tls gui-debugging gui-sendBasicAuthPrompt
+       gui-address gui-user gui-password gui-apikey gui-theme ldap-enabled
+       ldap-address ldap-bindDN ldap-transport ldap-insecureSkipVerify
+       ldap-searchBaseDN ldap-searchFilter listenAddress globalAnnounceServer
+       globalAnnounceEnabled localAnnounceEnabled localAnnouncePort
+       localAnnounceMCAddr maxSendKbps maxRecvKbps reconnectionIntervalS
+       relaysEnabled relayReconnectIntervalM startBrowser natEnabled
+       natLeaseMinutes natRenewalMinutes natTimeoutSeconds urAccepted
+       urSeen urUniqueID urURL urPostInsecurely urInitialDelayS
+       autoUpgradeIntervalH upgradeToPreReleases keepTemporariesH
+       cacheIgnoredFiles progressUpdateIntervalS limitBandwidthInLan
+       minHomeDiskFree-unit minHomeDiskFree releasesURL
+       overwriteRemoteDeviceNamesOnConnect tempIndexMinBlocks
+       unackedNotificationID trafficClass setLowPriority maxFolderConcurrency
+       crashReportingURL crashReportingEnabled stunKeepaliveStartS
+       stunKeepaliveMinS stunServer databaseTuning
+       maxConcurrentIncomingRequestKiB announceLANAddresses
+       sendFullIndexOnUpgrade connectionLimitEnough connectionLimitMax
+       insecureAllowOldTLSVersions connectionPriorityTcpLan
+       connectionPriorityQuicLan connectionPriorityTcpWan
+       connectionPriorityQuicWan connectionPriorityRelay
+       connectionPriorityUpgradeThreshold default-folder default-device
+       default-ignores)
+    `(configuration (@ (version "37"))
+                    ,@(map syncthing-folder->sxml
+                           folders)
+                    ;; collect any devices in any folders, as well as any
+                    ;; devices explicitly added.
+                    ,@(map syncthing-device->sxml
+                           (delete-duplicates
+                            (append devices
+                                    (apply append
+                                           (map (lambda (folder)
+                                                  (map syncthing-folder-device-device
+                                                       (syncthing-folder-devices folder)))
+                                                folders)))
+                            ;; devices are the same if their id's are equal
+                            (lambda (device1 device2)
+                              (string= (syncthing-device-id device1)
+                                       (syncthing-device-id device2)))))
+                    (gui (@ (enabled ,gui-enabled)
+                            (tls ,gui-tls)
+                            (debugging ,gui-debugging)
+                            (sendBasicAuthPrompt ,gui-sendBasicAuthPrompt))
+                         (address ,gui-address)
+                         ,@(if gui-user `((user ,gui-user)) '())
+                         ,@(if gui-password `((password ,gui-password)) '())
+                         (apikey ,gui-apikey)
+                         (theme ,gui-theme))
+                    (ldap ,(if ldap-enabled
+                               `((address ,ldap-address)
+                                 (bindDN ,ldap-bindDN)
+                                 ,@(if ldap-transport
+                                       `((transport ,ldap-transport))
+                                       '())
+                                 ,@(if ldap-insecureSkipVerify
+                                       `((insecureSkipVerify ,ldap-insecureSkipVerify))
+                                       '())
+                                 ,@(if ldap-searchBaseDN
+                                       `((searchBaseDN ,ldap-searchBaseDN))
+                                       '())
+                                 ,@(if ldap-searchFilter
+                                       `((searchFilter ,ldap-searchFilter))
+                                       '()))
+                               ""))
+                    (options (listenAddress ,listenAddress)
+                             (globalAnnounceServer ,globalAnnounceServer)
+                             (globalAnnounceEnabled ,globalAnnounceEnabled)
+                             (localAnnounceEnabled ,localAnnounceEnabled)
+                             (localAnnouncePort ,localAnnouncePort)
+                             (localAnnounceMCAddr ,localAnnounceMCAddr)
+                             (maxSendKbps ,maxSendKbps)
+                             (maxRecvKbps ,maxRecvKbps)
+                             (reconnectionIntervalS ,reconnectionIntervalS)
+                             (relaysEnabled ,relaysEnabled)
+                             (relayReconnectIntervalM ,relayReconnectIntervalM)
+                             (startBrowser ,startBrowser)
+                             (natEnabled ,natEnabled)
+                             (natLeaseMinutes ,natLeaseMinutes)
+                             (natRenewalMinutes ,natRenewalMinutes)
+                             (natTimeoutSeconds ,natTimeoutSeconds)
+                             (urAccepted ,urAccepted)
+                             (urSeen ,urSeen)
+                             (urUniqueID ,urUniqueID)
+                             (urURL ,urURL)
+                             (urPostInsecurely ,urPostInsecurely)
+                             (urInitialDelayS ,urInitialDelayS)
+                             (autoUpgradeIntervalH ,autoUpgradeIntervalH)
+                             (upgradeToPreReleases ,upgradeToPreReleases)
+                             (keepTemporariesH ,keepTemporariesH)
+                             (cacheIgnoredFiles ,cacheIgnoredFiles)
+                             (progressUpdateIntervalS ,progressUpdateIntervalS)
+                             (limitBandwidthInLan ,limitBandwidthInLan)
+                             (minHomeDiskFree (@ (unit ,minHomeDiskFree-unit))
+                                              ,minHomeDiskFree)
+                             (releasesURL ,releasesURL)
+                             (overwriteRemoteDeviceNamesOnConnect ,overwriteRemoteDeviceNamesOnConnect)
+                             (tempIndexMinBlocks ,tempIndexMinBlocks)
+                             (unackedNotificationID ,unackedNotificationID)
+                             (trafficClass ,trafficClass)
+                             (setLowPriority ,setLowPriority)
+                             (maxFolderConcurrency ,maxFolderConcurrency)
+                             (crashReportingURL ,crashReportingURL)
+                             (crashReportingEnabled ,crashReportingEnabled)
+                             (stunKeepaliveStartS ,stunKeepaliveStartS)
+                             (stunKeepaliveMinS ,stunKeepaliveMinS)
+                             (stunServer ,stunServer)
+                             (databaseTuning ,databaseTuning)
+                             (maxConcurrentIncomingRequestKiB ,maxConcurrentIncomingRequestKiB)
+                             (announceLANAddresses ,announceLANAddresses)
+                             (sendFullIndexOnUpgrade ,sendFullIndexOnUpgrade)
+                             (connectionLimitEnough ,connectionLimitEnough)
+                             (connectionLimitMax ,connectionLimitMax)
+                             (insecureAllowOldTLSVersions ,insecureAllowOldTLSVersions)
+                             (connectionPriorityTcpLan ,connectionPriorityTcpLan)
+                             (connectionPriorityQuicLan ,connectionPriorityQuicLan)
+                             (connectionPriorityTcpWan ,connectionPriorityTcpWan)
+                             (connectionPriorityQuicWan ,connectionPriorityQuicWan)
+                             (connectionPriorityRelay ,connectionPriorityRelay)
+                             (connectionPriorityUpgradeThreshold ,connectionPriorityUpgradeThreshold))
+                    (defaults
+                      ,(syncthing-folder->sxml default-folder)
+                      ,(syncthing-device->sxml default-device)
+                      (ignores ,default-ignores)))))
+
+;; It is useful to be able to view the xml output by Guix, and to be able to
+;; diff it with a user's previous config, especially when migrating one's
+;; config to Guix.  This function adds whitespace that matches the whitespace
+;; of config files managed by Syncthing for easy diffing.
+(define (indent-sxml sxml indent-increment current-indent)
+  (match sxml
+    (((tag ('@ properties ...) (subtags ..1) ..1) sibling-tags ...)
+     `(,current-indent (,tag (@ ,@properties) "\n"
+                             ,@(indent-sxml subtags indent-increment
+                                            (string-append indent-increment current-indent))
+                             ,current-indent) "\n"
+                       ,@(indent-sxml sibling-tags indent-increment current-indent)))
+    (((tag ('@ properties ...) primitive ...) sibling-tags ...)
+     `(,current-indent (,tag (@ ,@properties) ,@primitive) "\n"
+                       ,@(indent-sxml sibling-tags indent-increment current-indent)))
+    (((tag (subtags ..1) ..1) sibling-tags ...)
+     `(,current-indent (,tag "\n"
+                             ,@(indent-sxml subtags indent-increment
+                                            (string-append indent-increment current-indent))
+                             ,current-indent) "\n"
+                       ,@(indent-sxml sibling-tags indent-increment current-indent)))
+    (((tag primitive ...) sibling-tags ...)
+     `(,current-indent (,tag ,@primitive) "\n"
+                       ,@(indent-sxml sibling-tags indent-increment current-indent)))
+    (() '())))
+
+(define (serialize-syncthing-config-file config)
+  (with-output-to-string
+    (lambda ()
+      (sxml->xml (cons '*TOP* (indent-sxml (list (syncthing-config-file->sxml config))
+                                           "    "
+                                           ""))))))
+
 (define-record-type* <syncthing-configuration>
   syncthing-configuration make-syncthing-configuration
   syncthing-configuration?
@@ -50,6 +494,8 @@  (define-record-type* <syncthing-configuration>
              (default "users"))
   (home      syncthing-configuration-home      ;string
              (default #f))
+  (config-file syncthing-configuration-config-file
+                         (default #f))         ; syncthing-config-file or file-like
   (home-service? syncthing-configuration-home-service?
                  (default for-home?) (innate)))
 
@@ -93,10 +539,26 @@  (define syncthing-shepherd-service
       (respawn? #f)
       (stop #~(make-kill-destructor))))))
 
+
+(define syncthing-files-service
+  (match-record-lambda <syncthing-configuration> (config-file user home home-service?)
+    (if config-file
+        `((,(if home-service?
+                ".config/syncthing/config.xml"
+                (string-append (or home (passwd:dir (getpw user)))
+                               "/.config/syncthing/config.xml"))
+           ,(if (file-like? config-file)
+                config-file
+                (plain-file "syncthin-config.xml" (serialize-syncthing-config-file
+                                                   config-file)))))
+        '())))
+
 (define syncthing-service-type
   (service-type (name 'syncthing)
                 (extensions (list (service-extension shepherd-root-service-type
-                                                     syncthing-shepherd-service)))
+                                                     syncthing-shepherd-service)
+                                  (service-extension special-files-service-type
+                                                     syncthing-files-service)))
                 (description
                  "Run @uref{https://github.com/syncthing/syncthing, Syncthing}
 decentralized continuous file system synchronization.")))