~whynothugo/autovdirsyncer

bd9451a301d600786b69a3099bbdcd8dba3ef832 — Hugo Osvaldo Barrera 2 years ago
Initial commit
9 files changed, 234 insertions(+), 0 deletions(-)

A .gitignore
A LICENCE
A Makefile
A README.md
A autovdirsyncer.1.scd
A autovdirsyncer.service
A go.mod
A go.sum
A main.go
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 {}
}