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