mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 07:24:55 +08:00
121 lines
3.3 KiB
Go
121 lines
3.3 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, rel)
|
|
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
|
|
}
|
|
summary := idx.Summary()
|
|
if !summary.Ready {
|
|
// The cached health fact covers the whole current release. A
|
|
// locally present target is not enough to serve it as a healthy
|
|
// immutable artifact.
|
|
http.Error(w, "current release is not distributable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
resourceRoot = idx.ResourceRoot
|
|
entry, hasEntry = idx.Lookup(rel)
|
|
}
|
|
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 && hasEntry && uint64(info.Size()) != entry.Bytes {
|
|
http.Error(w, "size mismatch with release index", http.StatusConflict)
|
|
return
|
|
}
|
|
if !explicitRelease && 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 func() { _ = 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"
|
|
}
|