diff --git a/.air.toml b/.air.toml index 459b17a..1873118 100644 --- a/.air.toml +++ b/.air.toml @@ -1,6 +1,7 @@ root = "." testdata_dir = "testdata" tmp_dir = "tmp" +env_files = [".env"] [build] args_bin = [] diff --git a/cmd/schreifuchs-ch/main.go b/cmd/schreifuchs-ch/main.go index 55afa10..5ff9ba9 100644 --- a/cmd/schreifuchs-ch/main.go +++ b/cmd/schreifuchs-ch/main.go @@ -1,22 +1,38 @@ package main import ( + "context" "log/slog" "os" + "os/signal" + "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/server" ) func main() { - server := server.NewServer() + ctx := context.Background() + ctx, cancle := context.WithCancel(ctx) + defer cancle() + + go func() { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt) + <-sigint + + cancle() + }() slog.SetLogLoggerLevel(slog.LevelDebug) - slog.Info("Server started", "addr", server.Addr) - err := server.ListenAndServe() + cfg, err := config.Read(ctx) if err != nil { - slog.Error("Server failed to start", "error", err) - os.Exit(1) + slog.Error("could not read configuaration", "err", err) + } + + err = server.Start(ctx, cfg) + if err != nil { + slog.Error("error while serving", "err", err) + return } } - diff --git a/go.mod b/go.mod index a3845f6..7beb18e 100644 --- a/go.mod +++ b/go.mod @@ -20,9 +20,11 @@ require ( github.com/cli/browser v1.3.0 // indirect github.com/fatih/color v1.16.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/natefinch/atomic v1.0.1 // indirect + github.com/sethvargo/go-envconfig v1.3.0 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect golang.org/x/mod v0.26.0 // indirect golang.org/x/net v0.48.0 // indirect diff --git a/go.sum b/go.sum index e285c29..1c2d705 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IR github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -31,6 +33,8 @@ github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= 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 v1.3.0 h1:gJs+Fuv8+f05omTpwWIu6KmuseFAXKrIaOZSh8RMt0U= +github.com/sethvargo/go-envconfig v1.3.0/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/studio-b12/gowebdav v0.12.0 h1:kFRtQECt8jmVAvA6RHBz3geXUGJHUZA6/IKpOVUs5kM= diff --git a/internal/components/page/page.go b/internal/components/page/page.go index d7315db..d9a7aa8 100644 --- a/internal/components/page/page.go +++ b/internal/components/page/page.go @@ -10,7 +10,7 @@ import ( "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate" - "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem" + "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem" "github.com/studio-b12/gowebdav" ) diff --git a/internal/components/page/resource.go b/internal/components/page/resource.go index 9c9780e..3457071 100644 --- a/internal/components/page/resource.go +++ b/internal/components/page/resource.go @@ -1,7 +1,7 @@ package page import ( - "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem" + "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem" ) type Service struct { diff --git a/internal/pkg/config/resource.go b/internal/pkg/config/resource.go new file mode 100644 index 0000000..e7d1dd3 --- /dev/null +++ b/internal/pkg/config/resource.go @@ -0,0 +1,49 @@ +package config + +import ( + "context" + + "github.com/sethvargo/go-envconfig" +) + +type Cfg struct { + FileSystem *WebdavConfig `env:", prefix=FILESYSTEM_"` + Cache *ValkeyConfig `env:", prefix=CACHE_"` +} + +type WebdavConfig struct { + URL string `env:"URL"` + User string `env:"USERNAME"` + Password string `env:"PASSWORD"` +} + +type ValkeyConfig struct { + ClientName string `env:"CLIENTNAME"` + Username string `env:"USERNAME"` + Password string `env:"PASSWORD"` + + // InitAddress point to valkey nodes. + // Valkey will connect to them one by one and issue a CLUSTER SLOT command to initialize the cluster client until success. + // If len(InitAddress) == 1 and the address is not running in cluster mode, valkey will fall back to the single client mode. + // If ClientOption.Sentinel.MasterSet is set, then InitAddress will be used to connect sentinels + // You can bypass this behavior by using ClientOption.ForceSingleClient. + InitAddress []string `env:"ADDRESS"` +} + +func Read(ctx context.Context) (cfg Cfg, err error) { + err = envconfig.ProcessWith(ctx, &envconfig.Config{ + Target: &cfg, + + // All fields will use a ";" delimiter and "@" separator, unless locally + // overridden. + DefaultDelimiter: ";", + DefaultSeparator: "@", + + Lookuper: envconfig.MultiLookuper(secretLookuper(), envconfig.OsLookuper()), + }) + if err != nil { + return + } + + return +} diff --git a/internal/pkg/config/secrets.go b/internal/pkg/config/secrets.go new file mode 100644 index 0000000..268dd4f --- /dev/null +++ b/internal/pkg/config/secrets.go @@ -0,0 +1,36 @@ +package config + +import ( + "io" + "log/slog" + "os" + "strings" + + "github.com/sethvargo/go-envconfig" +) + +func secretLookuper() envconfig.LookuperFunc { + return func(key string) (string, bool) { + fileName := os.Getenv(key + "_FILE") + if fileName == "" { + return "", false + } + + file, err := os.Open(fileName) + if err != nil { + slog.Error("could not read secret file", "path", fileName) + return "", false + } + + defer file.Close() + + sb := &strings.Builder{} + + _, err = io.Copy(sb, file) + if err != nil { + return "", false + } + + return sb.String(), true + } +} diff --git a/internal/filesystem/cache.go b/internal/pkg/filesystem/cache.go similarity index 100% rename from internal/filesystem/cache.go rename to internal/pkg/filesystem/cache.go diff --git a/internal/filesystem/cache_test.go b/internal/pkg/filesystem/cache_test.go similarity index 100% rename from internal/filesystem/cache_test.go rename to internal/pkg/filesystem/cache_test.go diff --git a/internal/filesystem/cached_client_test.go b/internal/pkg/filesystem/cached_client_test.go similarity index 100% rename from internal/filesystem/cached_client_test.go rename to internal/pkg/filesystem/cached_client_test.go diff --git a/internal/filesystem/model.go b/internal/pkg/filesystem/model.go similarity index 100% rename from internal/filesystem/model.go rename to internal/pkg/filesystem/model.go diff --git a/internal/filesystem/read.go b/internal/pkg/filesystem/read.go similarity index 100% rename from internal/filesystem/read.go rename to internal/pkg/filesystem/read.go diff --git a/internal/filesystem/readdir.go b/internal/pkg/filesystem/readdir.go similarity index 100% rename from internal/filesystem/readdir.go rename to internal/pkg/filesystem/readdir.go diff --git a/internal/filesystem/stat.go b/internal/pkg/filesystem/stat.go similarity index 100% rename from internal/filesystem/stat.go rename to internal/pkg/filesystem/stat.go diff --git a/internal/templer/middleware.go b/internal/pkg/templer/middleware.go similarity index 100% rename from internal/templer/middleware.go rename to internal/pkg/templer/middleware.go diff --git a/internal/server/routes.go b/internal/server/routes.go index 390f353..ba2b1a9 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -3,16 +3,15 @@ package server import ( "log/slog" "net/http" - "os" "time" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/images" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate" - "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/filesystem" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/resthome" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/restimage" - "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer" + "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/filesystem" + "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/templer" "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web" "github.com/studio-b12/gowebdav" "github.com/valkey-io/valkey-go" @@ -22,7 +21,7 @@ func (s *Server) RegisterRoutes() (h http.Handler) { mux := http.NewServeMux() registerStatic(mux) - mux.Handle("/", registerOther()) + mux.Handle("/", s.registerOther()) h = mux h = templer.Middleware(h) @@ -30,23 +29,19 @@ func (s *Server) RegisterRoutes() (h http.Handler) { return h } -func registerOther() (h http.Handler) { +func (s *Server) registerOther() (h http.Handler) { mux := http.NewServeMux() - webdavClient := gowebdav.NewClient("https://cloud.schreifuchs.ch/remote.php/dav/files/test/website", "test", os.Getenv("TEST_PW")) + webdavClient := gowebdav.NewClient(s.cfg.FileSystem.URL, s.cfg.FileSystem.User, s.cfg.FileSystem.Password) if err := webdavClient.Connect(); err != nil { - slog.Error("could not connect webdavClient", "err", err, "pw", os.Getenv("TEST_PW")) + slog.Error("could not connect webdavClient", "err", err) } var fs filesystem.FS = webdavClient - valkeyAddr := os.Getenv("VALKEY_ADDR") - if valkeyAddr == "" { - valkeyAddr = "127.0.0.1:6379" - } valkeyClient, err := valkey.NewClient(valkey.ClientOption{ - InitAddress: []string{valkeyAddr}, - Password: os.Getenv("VALKEY_PASSWORD"), + InitAddress: s.cfg.Cache.InitAddress, + Password: s.cfg.Cache.Password, }) if err != nil { slog.Error("failed to create valkey client", "err", err) @@ -63,7 +58,7 @@ func registerOther() (h http.Handler) { h = mux h = translator.Middleware(h) - h = cacheMiddleware(time.Second * 45)(h) + h = cacheMiddleware(time.Minute * 5)(h) return h } diff --git a/internal/server/server.go b/internal/server/server.go index b2db09b..6d54344 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1,34 +1,61 @@ package server import ( + "context" "fmt" + "log/slog" "net/http" "os" "strconv" "time" + + "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config" ) type Server struct { port int + cfg config.Cfg } -func NewServer() *http.Server { +func Start(ctx context.Context, cfg config.Cfg) (err error) { port, _ := strconv.Atoi(os.Getenv("PORT")) if port == 0 { port = 8080 } s := &Server{ port: port, + cfg: cfg, } // Declare Server config - server := &http.Server{ + srv := &http.Server{ Addr: fmt.Sprintf(":%d", s.port), Handler: s.RegisterRoutes(), IdleTimeout: time.Minute, ReadTimeout: 10 * time.Second, WriteTimeout: 30 * time.Second, } + idleConnsClosed := make(chan struct{}) - return server + go func() { + <-ctx.Done() + + slog.Info("shutting down server") + + // Shutdown server when ctx done + if err = srv.Shutdown(context.Background()); err != nil { + // Error from closing listeners, or context timeout: + err = fmt.Errorf("error whilse shutting down server: %w", err) + } + + close(idleConnsClosed) + }() + + slog.Info("starting server", "address", srv.Addr) + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + return fmt.Errorf("error while server ListenAndServe: %w", err) + } + + <-idleConnsClosed + return err } diff --git a/internal/server/static.go b/internal/server/static.go index e9b48db..efdeccc 100644 --- a/internal/server/static.go +++ b/internal/server/static.go @@ -27,7 +27,7 @@ func registerStatic(mux *http.ServeMux) { path = "/" + path - slog.Info("registering file", "path", path) + slog.Debug("registering file", "path", path) mux.Handle(path, fileServer) diff --git a/web/layouts/base.templ b/web/layouts/base.templ index 21ca704..59593fd 100644 --- a/web/layouts/base.templ +++ b/web/layouts/base.templ @@ -2,7 +2,7 @@ package layouts import "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate" import "strings" -import "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer" +import "git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/templer" templ Base(title string) {