99 lines
2.1 KiB
Go
99 lines
2.1 KiB
Go
package staticimages
|
|
|
|
import (
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
const cacheControl = "public, no-cache"
|
|
const svgContentSecurityPolicy = "default-src 'none'; style-src 'unsafe-inline'; sandbox"
|
|
const fallbackContentType = "application/octet-stream"
|
|
|
|
var contentTypeExtensions = map[string]string{
|
|
".png": "image/png",
|
|
".jpeg": "image/jpeg",
|
|
".webp": "image/webp",
|
|
".svg": "image/svg+xml",
|
|
}
|
|
|
|
type Handler struct {
|
|
uploadDir fs.FS
|
|
modTime time.Time
|
|
}
|
|
|
|
func NewHandler(uploadDir string) *Handler {
|
|
return &Handler{
|
|
uploadDir: os.DirFS(uploadDir),
|
|
modTime: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
|
|
cleaned := cleanPath(requestedPath(r))
|
|
file, stat, ok := h.openFile(cleaned)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
content, ok := file.(io.ReadSeeker)
|
|
if !ok {
|
|
http.Error(w, "File not seekable", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
contentType := contentTypeFor(cleaned)
|
|
w.Header().Set("Content-Type", contentType)
|
|
w.Header().Set("Cache-Control", cacheControl)
|
|
if contentType == "image/svg+xml" {
|
|
w.Header().Set("Content-Security-Policy", svgContentSecurityPolicy)
|
|
}
|
|
|
|
http.ServeContent(w, r, cleaned, stat.ModTime(), content)
|
|
}
|
|
|
|
func requestedPath(r *http.Request) string {
|
|
if rctx := chi.RouteContext(r.Context()); rctx != nil && rctx.RoutePath != "" {
|
|
return rctx.RoutePath
|
|
}
|
|
return r.URL.Path
|
|
}
|
|
|
|
func cleanPath(p string) string {
|
|
return strings.TrimPrefix(path.Clean("/"+p), "/")
|
|
}
|
|
|
|
func (h *Handler) openFile(p string) (fs.File, fs.FileInfo, bool) {
|
|
if p == "" || p == "." {
|
|
return nil, nil, false
|
|
}
|
|
file, err := h.uploadDir.Open(p)
|
|
if err != nil {
|
|
return nil, nil, false
|
|
}
|
|
stat, err := file.Stat()
|
|
if err != nil || stat.IsDir() {
|
|
file.Close()
|
|
return nil, nil, false
|
|
}
|
|
return file, stat, true
|
|
}
|
|
|
|
func contentTypeFor(p string) string {
|
|
ext := path.Ext(p)
|
|
if contentType, ok := contentTypeExtensions[ext]; ok {
|
|
return contentType
|
|
}
|
|
return fallbackContentType
|
|
}
|