Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fca1030e1a | ||
|
|
2f7ff28c9a | ||
|
|
82e601fd7f | ||
|
|
f5bcc7c381 |
@@ -1,6 +1,7 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
env_files = [".env"]
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
|
||||
@@ -1,22 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
_ "net/http/pprof"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/pkg/config"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
server := server.NewServer()
|
||||
go func() {
|
||||
http.ListenAndServe("localhost:6060", nil)
|
||||
}()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
github.com/a-h/templ v0.3.977
|
||||
github.com/alicebob/miniredis/v2 v2.36.1
|
||||
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab
|
||||
github.com/sethvargo/go-envconfig v1.3.0
|
||||
github.com/studio-b12/gowebdav v0.12.0
|
||||
github.com/valkey-io/valkey-go v1.0.72
|
||||
golang.org/x/image v0.36.0
|
||||
|
||||
@@ -31,6 +31,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=
|
||||
|
||||
@@ -10,12 +10,13 @@ import (
|
||||
"hash/fnv"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"github.com/valkey-io/valkey-go"
|
||||
)
|
||||
|
||||
@@ -59,6 +60,13 @@ func (s *Service) getImage(uid string) (file []byte, mimeType string, err error)
|
||||
}
|
||||
|
||||
func (s *Service) GetImage(ctx context.Context, uid string, options Options) (img io.Reader, mimeType string, err error) {
|
||||
if err = s.seph.Acquire(ctx, 1); err != nil {
|
||||
err = fmt.Errorf("could not Acquire semaphore: %w", err)
|
||||
return
|
||||
|
||||
}
|
||||
defer s.seph.Release(1)
|
||||
|
||||
rawImage, mimeType, err := s.getImage(uid)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/valkey-io/valkey-go"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
@@ -15,12 +16,14 @@ type Options struct {
|
||||
type Service struct {
|
||||
fs fileSystem
|
||||
cache valkey.Client
|
||||
seph *semaphore.Weighted
|
||||
}
|
||||
|
||||
func New(fs fileSystem, cache valkey.Client) *Service {
|
||||
func New(fs fileSystem, cache valkey.Client, maxConcurency int64) *Service {
|
||||
return &Service{
|
||||
fs: fs,
|
||||
cache: cache,
|
||||
seph: semaphore.NewWeighted(maxConcurency),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type captureWriter struct {
|
||||
http.ResponseWriter
|
||||
code int
|
||||
}
|
||||
|
||||
func (c *captureWriter) WriteHeader(statusCode int) {
|
||||
c.code = statusCode
|
||||
c.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
cw := &captureWriter{
|
||||
ResponseWriter: w,
|
||||
code: 200,
|
||||
}
|
||||
next.ServeHTTP(cw, r)
|
||||
slog.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"duration", time.Since(start),
|
||||
"status", cw.code,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// cacheMiddleware adds Cache-Control headers to the response.
|
||||
func cacheMiddleware(maxAge time.Duration) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", maxAge.Seconds()))
|
||||
w.Header().Set("Vary", "HX-Request,Accept-Language,Accept-Encoding")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
+10
-27
@@ -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)
|
||||
@@ -55,7 +50,7 @@ func registerOther() (h http.Handler) {
|
||||
}
|
||||
|
||||
_ = resthome.New(mux, page.New(fs))
|
||||
_ = restimage.New(mux, images.New(fs, valkeyClient))
|
||||
_ = restimage.New(mux, images.New(fs, valkeyClient, 4))
|
||||
|
||||
mux.Handle("/", http.FileServer(http.FS(web.GetStaticFS())))
|
||||
|
||||
@@ -63,18 +58,6 @@ 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
|
||||
}
|
||||
|
||||
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
slog.Info("request",
|
||||
"method", r.Method,
|
||||
"path", r.URL.Path,
|
||||
"duration", time.Since(start),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -27,7 +26,7 @@ func registerStatic(mux *http.ServeMux) {
|
||||
|
||||
path = "/" + path
|
||||
|
||||
slog.Info("registering file", "path", path)
|
||||
slog.Debug("registering file", "path", path)
|
||||
|
||||
mux.Handle(path, fileServer)
|
||||
|
||||
@@ -38,14 +37,3 @@ func registerStatic(mux *http.ServeMux) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// cacheMiddleware adds Cache-Control headers to the response.
|
||||
func cacheMiddleware(maxAge time.Duration) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%.0f", maxAge.Seconds()))
|
||||
w.Header().Set("Vary", "HX-Request,Accept-Language,Accept-Encoding")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
+1976
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 112 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 439 KiB |
@@ -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) {
|
||||
<!DOCTYPE html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user