intial commit
This commit is contained in:
commit
eab296a3ca
7 changed files with 255 additions and 0 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/test/
|
||||||
|
/test.yaml
|
||||||
|
/gcal-sync
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/test/
|
||||||
|
/test.yaml
|
||||||
|
/gcal-sync
|
||||||
|
vendor/
|
26
Dockerfile
Normal file
26
Dockerfile
Normal 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
104
config/config.go
Normal 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
11
go.mod
Normal 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
13
go.sum
Normal 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
94
main.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue