// s3staticsites: servers http requests from an S3 bucket // Copyright (C) 2024 Finn Herzfeld // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package main import ( "context" "io" "net/http" "strings" minio "github.com/minio/minio-go/v7" "golang.org/x/exp/slog" ) func ListenAndServe(minioClient *minio.Client) error { slog.Info("starting http server", "bind", config.Bind) err := http.ListenAndServe(config.Bind, handler{minio: minioClient}) if err != nil { return err } return nil } type handler struct { minio *minio.Client } func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { domain := r.Host path := r.URL.Path if path == "" || path == "/" { path = "index.html" } object, stat, err := h.get(r.Context(), domain, path) if err != nil { resp := minio.ToErrorResponse(err) if resp.StatusCode == http.StatusNotFound { // TODO: custom 404 page w.WriteHeader(http.StatusNotFound) w.Write([]byte("404 not found")) slog.Info("served 404", "domain", domain, "path", path) return } slog.Error("error getting object from storage", "bucket", domain, "object", path, "err", err) http.Error(w, "internal server error", http.StatusInternalServerError) return } w.Header().Add("Content-Type", stat.ContentType) w.Header().Add("ETag", stat.ETag) w.Header().Add("Access-Control-Allow-Origin", "*") n, err := io.Copy(w, object) if err != nil { slog.Warn("error writting response", "error", err, "domain", domain, "path", path) return } slog.Info("served request", "domain", domain, "path", path, "size_bytes", n) } func (h handler) get(ctx context.Context, bucket string, path string) (*minio.Object, *minio.ObjectInfo, error) { object, err := h.minio.GetObject(ctx, bucket, path, minio.GetObjectOptions{}) if err != nil { return nil, nil, err } stat, err := object.Stat() if err != nil { resp := minio.ToErrorResponse(err) if resp.StatusCode == http.StatusNotFound { if strings.HasSuffix(path, "/index.html") { return nil, nil, err } if !strings.HasSuffix(path, "/") { path = path + "/" } path = path + "index.html" return h.get(ctx, bucket, path) } return nil, nil, err } return object, &stat, nil }