diff --git a/cmd/notion2ical/main.go b/cmd/notion2ical/main.go index 444e0c9..1695c66 100644 --- a/cmd/notion2ical/main.go +++ b/cmd/notion2ical/main.go @@ -1,7 +1,35 @@ package main -import "fmt" +import ( + "context" + + "github.com/fmartingr/notion2ical/internal/server" + "go.uber.org/zap" +) func main() { - fmt.Println("I come in peace.") + ctx := context.Background() + logger, err := zap.NewProduction() + if err != nil { + panic(err) + } + + // TODO: set log level + + defer func() { + if err := logger.Sync(); err != nil { + panic(err) + } + }() + + server := server.NewServer( + logger, + server.ParseServerConfiguration(ctx, logger), + ) + + if err := server.Start(ctx); err != nil { + logger.Panic("error starting server", zap.Error(err)) + } + + server.WaitStop() } diff --git a/go.mod b/go.mod index b38056d..86b0bca 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,20 @@ module github.com/fmartingr/notion2ical go 1.19 + +require ( + github.com/gofiber/fiber/v2 v2.36.0 + github.com/sethvargo/go-envconfig v0.8.2 + go.uber.org/zap v1.22.0 +) + +require ( + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/klauspost/compress v1.15.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.38.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b8bca7 --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gofiber/fiber/v2 v2.36.0 h1:1qLMe5rhXFLPa2SjK10Wz7WFgLwYi4TYg7XrjztJHqA= +github.com/gofiber/fiber/v2 v2.36.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-envconfig v0.8.2 h1:DDUVuG21RMgeB/bn4leclUI/837y6cQCD4w8hb5797k= +github.com/sethvargo/go-envconfig v0.8.2/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.38.0 h1:yTjSSNjuDi2PPvXY2836bIwLmiTS2T4T9p1coQshpco= +github.com/valyala/fasthttp v1.38.0/go.mod h1:t/G+3rLek+CyY9bnIE+YlMRddxVAAGjhxndDB4i4C0I= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.22.0 h1:Zcye5DUgBloQ9BaT4qc9BnjOFog5TvBSAGkJ3Nf70c0= +go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9 h1:nhht2DYV/Sn3qOayu8lM+cU1ii9sTLUeBQwQQfUHtrs= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/models/server.go b/internal/models/server.go new file mode 100644 index 0000000..dc8f8a7 --- /dev/null +++ b/internal/models/server.go @@ -0,0 +1,8 @@ +package models + +import "context" + +type Server interface { + Start(ctx context.Context) error + Stop(ctx context.Context) error +} diff --git a/internal/server/config.go b/internal/server/config.go new file mode 100644 index 0000000..21e8bdf --- /dev/null +++ b/internal/server/config.go @@ -0,0 +1,65 @@ +package server + +import ( + "bufio" + "context" + "os" + "strings" + + "github.com/sethvargo/go-envconfig" + "go.uber.org/zap" +) + +// readDotEnv reads the configuration from variables in a .env file (only for contributing) +func readDotEnv(logger *zap.Logger) map[string]string { + file, err := os.Open(".env") + if err != nil { + return nil + } + defer file.Close() + + result := make(map[string]string) + + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") { + continue + } + + keyval := strings.SplitN(line, "=", 2) + result[keyval[0]] = keyval[1] + } + + if err := scanner.Err(); err != nil { + logger.Fatal("error reading dotenv", zap.Error(err)) + } + + return result +} + +type ServerConfig struct { + Hostname string `env:"HOSTNAME,required"` + Http struct { + Enabled bool `env:"HTTP_ENABLED,default=True"` + Port int `env:"HTTP_PORT,default=8080"` + } + LogLevel string `env:"LOG_LEVEL,default=info"` +} + +func ParseServerConfiguration(ctx context.Context, logger *zap.Logger) *ServerConfig { + var cfg ServerConfig + + lookuper := envconfig.MultiLookuper( + envconfig.MapLookuper(map[string]string{"HOSTNAME": os.Getenv("HOSTNAME")}), + envconfig.MapLookuper(readDotEnv(logger)), + envconfig.PrefixLookuper("NOTION2ICAL_", envconfig.OsLookuper()), + envconfig.OsLookuper(), + ) + if err := envconfig.ProcessWith(ctx, &cfg, lookuper); err != nil { + logger.Fatal("Error parsing configuration: %s", zap.Error(err)) + } + + return &cfg +} diff --git a/internal/server/http.go b/internal/server/http.go new file mode 100644 index 0000000..700f81f --- /dev/null +++ b/internal/server/http.go @@ -0,0 +1,62 @@ +package server + +import ( + "context" + "fmt" + + "github.com/gofiber/fiber/v2" + "go.uber.org/zap" +) + +type httpServer struct { + http *fiber.App + addr string + + logger *zap.Logger +} + +func (s *httpServer) Start(_ context.Context) error { + s.http. + Static("/", "./public"). + Get("/calendar", s.calendarHandler). + Get("/liveness", s.livenessHandler). + Use(s.notFound) + + s.logger.Info("starting http server", zap.String("addr", s.addr)) + return s.http.Listen(s.addr) +} + +func (s *httpServer) Stop(ctx context.Context) error { + s.logger.Info("stoppping http server") + return s.http.Shutdown() +} + +func (s *httpServer) notFound(c *fiber.Ctx) error { + return c.SendStatus(404) +} + +func (s *httpServer) livenessHandler(c *fiber.Ctx) error { + return c.SendStatus(200) +} + +func (s *httpServer) calendarHandler(c *fiber.Ctx) error { + return c.SendStatus(501) +} + +func NewHttpServer(logger *zap.Logger, port int) *httpServer { + return &httpServer{ + logger: logger, + addr: fmt.Sprintf(":%d", port), + http: fiber.New(fiber.Config{ + ErrorHandler: func(c *fiber.Ctx, err error) error { + logger.Error( + "handler error", + zap.String("method", c.Method()), + zap.String("path", c.Path()), + zap.Error(err), + ) + return c.SendStatus(500) + }, + }), + } +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..8503ddc --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,72 @@ +package server + +import ( + "context" + "errors" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + internalModels "github.com/fmartingr/notion2ical/internal/models" + "go.uber.org/zap" +) + +type Server struct { + Http internalModels.Server + config *ServerConfig + logger *zap.Logger + + cancel context.CancelFunc +} + +func (s *Server) Start(ctx context.Context) error { + ctx, cancel := context.WithCancel(ctx) + s.cancel = cancel + + if s.config.Http.Enabled { + go func() { + if err := s.Http.Start(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Fatal("error starting server", zap.Error(err)) + } + }() + } + + return nil +} + +func (s *Server) WaitStop() { + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + + sig := <-signals + s.logger.Info("signal received, shutting down", zap.String("signal", sig.String())) + + s.Stop() +} + +func (s *Server) Stop() { + s.cancel() + + shuwdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if s.config.Http.Enabled { + if err := s.Http.Stop(shuwdownContext); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Fatal("error shutting down http server", zap.Error(err)) + } + } +} + +func NewServer(logger *zap.Logger, conf *ServerConfig) *Server { + server := &Server{ + logger: logger, + config: conf, + } + if conf.Http.Enabled { + server.Http = NewHttpServer(logger, conf.Http.Port) + } + + return server +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..0855308 --- /dev/null +++ b/public/index.html @@ -0,0 +1,15 @@ + + + + + + + + Notion to iCal + + + + Notion to ical + + +