Files
BlueArchiveToolkit/internal/api/cdn.go
T
nyaKazuha 8d57a63697
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
feat(release):完成双 release 运维闭环
2026-09-12 11:08:04 +08:00

115 lines
2.9 KiB
Go

package api
import (
"fmt"
"mime"
"net/http"
"os"
"path/filepath"
"strings"
)
func (s *Server) serveCDN(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
_, rel, err := SplitCDNPath(r.URL.Path)
if err != nil {
http.NotFound(w, r)
return
}
var entry ResourceEntry
var hasEntry bool
resourceRoot := ""
explicitRelease := false
channel := r.URL.Query().Get("channel")
releaseID := r.URL.Query().Get("release_id")
if channel != "" || releaseID != "" {
explicitRelease = true
channel, releaseID, selectorErr := releaseSelector(r)
if selectorErr != nil {
http.Error(w, selectorErr.Error(), http.StatusBadRequest)
return
}
selected, selectErr := s.loadReleaseDistribution(r, channel, releaseID)
if selectErr != nil {
http.Error(w, selectErr.Error(), http.StatusServiceUnavailable)
return
}
if selected == nil || !selected.Available || selected.ResourceRoot == "" {
http.Error(w, "selected release is not distributable", http.StatusConflict)
return
}
resourceRoot = selected.ResourceRoot
entry, hasEntry = releaseDistributionEntry(selected, rel)
} else {
idx := s.index()
if idx == nil || idx.ResourceRoot == "" {
http.Error(w, "resource root not ready", http.StatusServiceUnavailable)
return
}
resourceRoot = idx.ResourceRoot
if s.cfg.RequireIndexed {
entry, hasEntry = idx.Lookup(rel)
}
}
if explicitRelease || s.cfg.RequireIndexed {
if !hasEntry || !entry.Present || !entry.SizeMatch {
http.NotFound(w, r)
return
}
}
abs, err := ResolveUnderRoot(resourceRoot, rel, true)
if err != nil {
http.NotFound(w, r)
return
}
info, err := os.Stat(abs)
if err != nil || !info.Mode().IsRegular() {
http.NotFound(w, r)
return
}
if (explicitRelease || s.cfg.RequireIndexed) && s.cfg.VerifySize {
if hasEntry && entry.Bytes > 0 && uint64(info.Size()) != entry.Bytes {
http.Error(w, "size mismatch with release index", http.StatusConflict)
return
}
}
file, err := os.Open(abs)
if err != nil {
http.NotFound(w, r)
return
}
defer file.Close()
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
w.Header().Set("ETag", cdnETag(entry, hasEntry, info))
w.Header().Set("Content-Type", cdnContentType(abs))
http.ServeContent(w, r, info.Name(), info.ModTime(), file)
}
func cdnETag(entry ResourceEntry, hasEntry bool, info os.FileInfo) string {
if hasEntry && entry.BLAKE3 != "" {
return fmt.Sprintf(`"blake3-%s"`, entry.BLAKE3)
}
return fmt.Sprintf(`W/"%d-%d"`, info.Size(), info.ModTime().UnixNano())
}
func cdnContentType(abs string) string {
ext := strings.ToLower(filepath.Ext(abs))
if ext == ".json" {
return "application/json; charset=utf-8"
}
if ext == ".hash" {
return "text/plain; charset=utf-8"
}
if detected := mime.TypeByExtension(ext); detected != "" {
return detected
}
return "application/octet-stream"
}