intial commit

This commit is contained in:
Seán C McCord 2024-10-01 13:44:45 -04:00
commit eab296a3ca
Signed by: scm
GPG key ID: 07AE1E7F9CC5165B
7 changed files with 255 additions and 0 deletions

3
.dockerignore Normal file
View file

@ -0,0 +1,3 @@
/test/
/test.yaml
/gcal-sync

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/test/
/test.yaml
/gcal-sync
vendor/

26
Dockerfile Normal file
View file

@ -0,0 +1,26 @@
FROM stagex/busybox:sx2024.09.0@sha256:d34bfa56566aa72d605d6cbdc154de8330cf426cfea1bc4ba8013abcac594395 AS busybox
FROM stagex/ca-certificates:sx2024.09.0@sha256:33787f1feb634be4232a6dfe77578c1a9b890ad82a2cf18c11dd44507b358803 AS ca-certificates
FROM stagex/go:sx2024.09.0@sha256:56f511d92dcc6c1910115d8b19dd984b6cd682d1d3e9b72456e3b95f7b141fee AS go
FROM scratch AS builder
ENV CGO_ENABLED=0
COPY --from=busybox . .
COPY --from=go . .
COPY --from=ca-certificates . .
ADD . /src/gcal-sync
WORKDIR /src/gcal-sync
RUN go build -o /app
FROM scratch
WORKDIR /
COPY --from=ca-certificates . .
COPY --from=builder /app /app
ENTRYPOINT ["/app"]

104
config/config.go Normal file
View file

@ -0,0 +1,104 @@
package config
import (
"context"
"fmt"
"net/http"
"os"
"github.com/emersion/go-ical"
"github.com/emersion/go-webdav"
"github.com/emersion/go-webdav/caldav"
"gopkg.in/yaml.v3"
)
type Config struct {
Server string `yaml:"server"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Calendars []CalendarSync `yaml:"calendars"`
}
type CalendarSync struct {
Name string `yaml:"name"`
Destination string `yaml:"destination"`
URL string `yaml:"url"`
}
func (item *CalendarSync) Sync(ctx context.Context, cfg *Config) error {
webdavClient := webdav.HTTPClientWithBasicAuth(http.DefaultClient, cfg.Username, cfg.Password)
cc, err := caldav.NewClient(webdavClient, cfg.Server)
if err != nil {
return fmt.Errorf("failed to construct a CalDAV client: %w", err)
}
calPath, err := CalendarPath(ctx, cc, cfg.Username, item.Destination)
if err != nil {
return fmt.Errorf("failed to find destination calendar path: %w", err)
}
resp, err := http.Get(item.URL)
if err != nil {
return fmt.Errorf("failed to sync %s: %w", item.Name, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("source returned non-200 status code: %s", resp.Status)
}
calendarData, err := ical.NewDecoder(resp.Body).Decode()
if err != nil {
return fmt.Errorf("failed to decode source calendar data: %w", err)
}
// if err := ical.NewEncoder(os.Stdout).Encode(calendarData); err == nil {
// os.Exit(0)
// }
if _, err := cc.PutCalendarObject(ctx, calPath, calendarData); err != nil {
return fmt.Errorf("failed to push calendar data to destination: %w", err)
}
return nil
}
func CalendarPath(ctx context.Context, cc *caldav.Client, username, calendarName string) (string, error) {
homeSet, err := cc.FindCalendarHomeSet(ctx, username)
if err != nil {
return "", fmt.Errorf("failed to find home set for %s: %w", username, err)
}
calendars, err := cc.FindCalendars(ctx, homeSet)
if err != nil {
return "", fmt.Errorf("failed to retrieve calendars: %w", err)
}
for _, cal := range calendars {
if cal.Name == calendarName {
return cal.Path, nil
}
}
return "", fmt.Errorf("calendar with name %s not found", calendarName)
}
// Load a configuration file
func Load(configFile string) (*Config, error) {
cfg := new(Config)
f, err := os.Open(configFile)
if err != nil {
return nil, fmt.Errorf("failed to open config file %s: %w", configFile, err)
}
defer f.Close()
if err = yaml.NewDecoder(f).Decode(cfg); err != nil {
return nil, fmt.Errorf("failed to decode config file %s: %w", configFile, err)
}
return cfg, nil
}

11
go.mod Normal file
View file

@ -0,0 +1,11 @@
module git.cycore.io/scm/gcal-sync
go 1.22.7
require (
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6
github.com/emersion/go-webdav v0.5.0
gopkg.in/yaml.v3 v3.0.1
)
require github.com/teambition/rrule-go v1.8.2 // indirect

13
go.sum Normal file
View file

@ -0,0 +1,13 @@
github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6 h1:kHoSgklT8weIDl6R6xFpBJ5IioRdBU1v2X2aCZRVCcM=
github.com/emersion/go-ical v0.0.0-20240127095438-fc1c9d8fb2b6/go.mod h1:BEksegNspIkjCQfmzWgsgbu6KdeJ/4LwUZs7DMBzjzw=
github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.5.0 h1:Ak/BQLgAihJt/UxJbCsEXDPxS5Uw4nZzgIMOq3rkKjc=
github.com/emersion/go-webdav v0.5.0/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno=
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
github.com/teambition/rrule-go v1.8.2 h1:lIjpjvWTj9fFUZCmuoVDrKVOtdiyzbzc93qTmRVe/J8=
github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

94
main.go Normal file
View file

@ -0,0 +1,94 @@
package main
import (
"context"
"errors"
"flag"
"log"
"log/slog"
"time"
"git.cycore.io/scm/gcal-sync/config"
)
var configFile string
var debug bool
var interval time.Duration
func init() {
flag.StringVar(&configFile, "config", "/etc/gcal-sync/config.yaml", "name of the file from which to load the configuration")
flag.BoolVar(&debug, "debug", false, "enable debug logging")
flag.DurationVar(&interval, "interval", 5*time.Minute, "refresh frequency")
}
func main() {
ctx := context.Background()
flag.Parse()
if debug {
slog.SetLogLoggerLevel(slog.LevelDebug)
} else {
slog.SetLogLoggerLevel(slog.LevelInfo)
}
cfg, err := config.Load(configFile)
if err != nil {
log.Fatalf("failed to load configuration file %s: %s", configFile, err.Error())
}
var failures error
for _, item := range cfg.Calendars {
if err := item.Sync(ctx, cfg); err != nil {
slog.Error("failed to sync calendar",
slog.String("name", item.Name),
slog.String("error", err.Error()),
)
failures = errors.Join(failures, err)
continue
}
slog.Debug("posted updated calendar data",
slog.String("name", item.Name),
)
}
for {
if err := Sync(ctx, cfg); err != nil {
slog.Warn("failed to sync all calendars")
} else {
slog.Info("synchronized all calendar data",
slog.Int("count", len(cfg.Calendars)),
)
}
time.Sleep(interval)
}
}
func Sync(ctx context.Context, cfg *config.Config) error {
var failures error
for _, item := range cfg.Calendars {
if err := item.Sync(ctx, cfg); err != nil {
slog.Error("failed to sync calendar",
slog.String("name", item.Name),
slog.String("url", item.URL),
slog.String("error", err.Error()),
)
failures = errors.Join(failures, err)
continue
}
slog.Debug("posted updated calendar data",
slog.String("name", item.Name),
)
}
return failures
}