A => .gitignore +2 -0
@@ 1,2 @@
+autovdirsyncer
+autovdirsyncer.1
A => LICENCE +13 -0
@@ 1,13 @@
+Copyright (c) 2021, Hugo Osvaldo Barrera <hugo@barrera.io>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
A => Makefile +14 -0
@@ 1,14 @@
+DESTDIR?=/
+PREFIX=/usr
+
+build:
+ go build -ldflags '-s'
+ scdoc < autovdirsyncer.1.scd > autovdirsyncer.1
+
+install: build
+ @install -Dm755 autovdirsyncer \
+ ${DESTDIR}${PREFIX}/lib/autovdirsyncer
+ @install -Dm644 autovdirsyncer.service \
+ ${DESTDIR}${PREFIX}/lib/systemd/user/autovdirsyncer.service
+
+.PHONY: build install
A => README.md +31 -0
@@ 1,31 @@
+# autovdirsyncer
+
+**autovdirsyncer** is a wrapper to daemonise `vdirsyncer`.
+
+It monitor your [vdir collections][vdir] and runs vdirsyncer:
+
+- After two seconds if a file is changed. ("active" runs)
+- Every fifteen minutes if no file has changed. ("passive" runs)
+
+Active runs will be postponed by two seconds if you keep editing files. This is
+so that, if multiple events are happening at once (e.g.: you're copying a bunch
+of files), synchronisation will happen only after you're done.
+
+Feel free to join the IRC channel: #pimutils on irc.libera.chat.
+
+[vdir]: https://vdirsyncer.pimutils.org/en/stable/vdir.html
+
+# Caveats
+
+- Don't run `vdirsyncer` manually. It'll modify all your files, and
+ `autovdirsyncer` will trigger _another_ instance of `vdirsyncer`.
+- If you alter files continuously within two second intervals, `vdirsyncer`
+ will not run. It'll run after the continuous editing happens. This should not
+ be an issue for usual / desktop usage.
+- The location of collections is currently hardcoded to
+ `.local/share/calendars`. This tool should likely ask `vdirsyncer` to
+ enumerate its collections and use that as input.
+
+# Licence
+
+autovdirsyncer is licensed under the ISC licence. See LICENCE for details.
A => autovdirsyncer.1.scd +21 -0
@@ 1,21 @@
+autovdirsyncer(1)
+
+# NAME
+
+autovdirsyncer - wrapper to daemonise vdirsyncer
+
+# SYNOPSIS
+
+*grimshot*
+
+# DESCRIPTION
+
+*autovdirsyncer* is a wrapper to daemonise vdirsyncer and automatically run it
+in the background.
+
+If there are no file changes, *vdirsyncer* will run 15 minutes after the last
+run. If there are changes, *vdirsyncer* will run 2 seconds after the last change.
+
+# SEE ALSO
+
+*vdirsyncer*(1)
A => autovdirsyncer.service +9 -0
@@ 1,9 @@
+[Unit]
+Description=Synchronize calendars and contacts
+Documentation=man:autovdirsyncer(1)
+
+[Service]
+ExecStart=/usr/lib/autovdirsyncer
+
+[Install]
+WantedBy=default.target
A => go.mod +5 -0
@@ 1,5 @@
+module git.sr.ht/~whynothugo/autovdirsyncer
+
+go 1.16
+
+require github.com/fsnotify/fsnotify v1.4.9 // indirect
A => go.sum +4 -0
@@ 1,4 @@
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
A => main.go +135 -0
@@ 1,135 @@
+package main
+
+import (
+ "fmt"
+ "io/fs"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "sync"
+ "time"
+
+ "github.com/fsnotify/fsnotify"
+)
+
+var (
+ vdirsyncerIsRunning sync.Mutex
+ passiveInterval time.Duration // Period between automatic syncs if nothing happens.
+ activeDebounce time.Duration // Period to debounce when changes are happening.
+)
+
+func runSynchronization() {
+ vdirsyncerIsRunning.Lock()
+
+ cmd := exec.Command("vdirsyncer", "sync")
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+
+ err := cmd.Run()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error running vdirsyncer: %v.\n", err.Error())
+
+ if exitError, ok := err.(*exec.ExitError); ok {
+ fmt.Fprintf(
+ os.Stderr,
+ "Got exit code %d from vdirsyncer: %v.\n",
+ exitError.ExitCode(),
+ err.Error(),
+ )
+ }
+ }
+ fmt.Println("vdirsyncer ran successfully.")
+
+ vdirsyncerIsRunning.Unlock()
+}
+
+func scheduleSynchronizations(timer *time.Timer) {
+ for {
+ <-timer.C
+ runSynchronization()
+ timer.Reset(passiveInterval)
+ }
+}
+
+func listenForEvents(watcher *fsnotify.Watcher, timer *time.Timer) {
+ for {
+ select {
+ case event, ok := <-watcher.Events:
+ if !ok {
+ return
+ }
+ // fsnotify only sends write-events, so we don't need
+ // to check the event type (e.g. there are no "open" or
+ // "read" events).
+
+ fmt.Println("File altered:", event.Name)
+ // XXX: If changes happen continuously within two
+ // second intervals, sync never happens.
+ timer.Reset(activeDebounce)
+ case err, ok := <-watcher.Errors:
+ if !ok {
+ return
+ }
+ fmt.Println("error:", err)
+ }
+ }
+}
+
+func addDirsToWatcher(watcher *fsnotify.Watcher) error {
+ baseDir := "/home/hugo/.local/share/calendars/"
+
+ return filepath.WalkDir(baseDir, func(path string, d fs.DirEntry, err error) error {
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Failed to read path %q: %v.\n", path, err)
+ return err
+ }
+ if d.IsDir() {
+ fmt.Printf("Listening for changes on %+v \n", path)
+ err = watcher.Add(path)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ }
+ return nil
+ })
+}
+
+func main() {
+ var err error
+
+ // Frequency with which updates run.
+ passiveInterval, err = time.ParseDuration("15m")
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(2)
+ }
+
+ activeDebounce, err = time.ParseDuration("2s")
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(2)
+ }
+
+ // Watcher that monitors for local fs changes.
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(3)
+ }
+ defer watcher.Close()
+
+ err = addDirsToWatcher(watcher)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(3)
+ }
+
+ timer := time.NewTimer(passiveInterval)
+
+ go listenForEvents(watcher, timer)
+ go scheduleSynchronizations(timer)
+
+ // Never exit.
+ select {}
+}