188 lines
5.3 KiB
Go
188 lines
5.3 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"scout/internal/registry"
|
|
)
|
|
|
|
type Server struct {
|
|
Registry *registry.Registry
|
|
DeveloperToken string
|
|
MaxUploadBytes int64
|
|
}
|
|
|
|
func (s *Server) Handler() http.Handler {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("GET /healthz", s.health)
|
|
mux.HandleFunc("POST /api/v1/packages/upload", s.upload)
|
|
mux.HandleFunc("GET /api/v1/updates/stream", s.stream)
|
|
mux.HandleFunc("GET /api/v1/packages/", s.packageRoute)
|
|
return logging(mux)
|
|
}
|
|
|
|
func (s *Server) health(writer http.ResponseWriter, _ *http.Request) {
|
|
writeJSON(writer, http.StatusOK, map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func (s *Server) upload(writer http.ResponseWriter, request *http.Request) {
|
|
if !s.authorized(request) {
|
|
writeError(writer, http.StatusUnauthorized, "开发者令牌无效")
|
|
return
|
|
}
|
|
maxBytes := s.MaxUploadBytes
|
|
if maxBytes <= 0 {
|
|
maxBytes = 512 << 20
|
|
}
|
|
request.Body = http.MaxBytesReader(writer, request.Body, maxBytes)
|
|
if err := request.ParseMultipartForm(32 << 20); err != nil {
|
|
writeError(writer, http.StatusBadRequest, "上传表单无效: "+err.Error())
|
|
return
|
|
}
|
|
file, _, err := request.FormFile("file")
|
|
if err != nil {
|
|
writeError(writer, http.StatusBadRequest, "缺少包文件字段 file")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
|
|
item, err := s.Registry.AddPackage(registry.Package{
|
|
Name: request.FormValue("name"),
|
|
Version: request.FormValue("version"),
|
|
Description: request.FormValue("description"),
|
|
Platform: request.FormValue("platform"),
|
|
Architecture: request.FormValue("architecture"),
|
|
}, file)
|
|
if err != nil {
|
|
status := http.StatusBadRequest
|
|
if strings.Contains(err.Error(), "已经存在") {
|
|
status = http.StatusConflict
|
|
}
|
|
writeError(writer, status, err.Error())
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusCreated, item)
|
|
}
|
|
|
|
func (s *Server) packageRoute(writer http.ResponseWriter, request *http.Request) {
|
|
path := strings.TrimPrefix(request.URL.Path, "/api/v1/packages/")
|
|
parts := strings.Split(strings.Trim(path, "/"), "/")
|
|
if len(parts) == 1 && parts[0] != "" {
|
|
item, ok := s.Registry.Latest(parts[0])
|
|
if !ok {
|
|
writeError(writer, http.StatusNotFound, "包不存在")
|
|
return
|
|
}
|
|
writeJSON(writer, http.StatusOK, item)
|
|
return
|
|
}
|
|
if len(parts) == 2 && parts[0] != "" {
|
|
if parts[1] == "versions" {
|
|
writeJSON(writer, http.StatusOK, map[string]any{"name": parts[0], "versions": s.Registry.Versions(parts[0])})
|
|
return
|
|
}
|
|
writeError(writer, http.StatusNotFound, "接口不存在")
|
|
return
|
|
}
|
|
if len(parts) == 3 && parts[2] == "download" {
|
|
s.download(writer, request, parts[0], parts[1])
|
|
return
|
|
}
|
|
writeError(writer, http.StatusNotFound, "接口不存在")
|
|
}
|
|
|
|
func (s *Server) download(writer http.ResponseWriter, request *http.Request, name, version string) {
|
|
file, item, err := s.Registry.OpenPackage(name, version)
|
|
if err != nil {
|
|
writeError(writer, http.StatusNotFound, "包版本不存在")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
|
writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-%s.pkg"`, name, version))
|
|
writer.Header().Set("X-Scout-SHA256", item.SHA256)
|
|
writer.Header().Set("Content-Length", strconv.FormatInt(item.Size, 10))
|
|
if request.Method == http.MethodHead {
|
|
return
|
|
}
|
|
if _, err := io.Copy(writer, file); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (s *Server) stream(writer http.ResponseWriter, request *http.Request) {
|
|
flusher, ok := writer.(http.Flusher)
|
|
if !ok {
|
|
writeError(writer, http.StatusInternalServerError, "当前服务器不支持实时通知")
|
|
return
|
|
}
|
|
wanted := make(map[string]bool)
|
|
for _, name := range strings.Split(request.URL.Query().Get("packages"), ",") {
|
|
if name = strings.TrimSpace(name); name != "" {
|
|
wanted[name] = true
|
|
}
|
|
}
|
|
updates, unsubscribe := s.Registry.Subscribe()
|
|
defer unsubscribe()
|
|
writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
|
|
writer.Header().Set("Cache-Control", "no-cache")
|
|
writer.Header().Set("Connection", "keep-alive")
|
|
fmt.Fprint(writer, ": 已连接\n\n")
|
|
flusher.Flush()
|
|
for {
|
|
select {
|
|
case <-request.Context().Done():
|
|
return
|
|
case update, ok := <-updates:
|
|
if !ok {
|
|
return
|
|
}
|
|
if len(wanted) > 0 && !wanted[update.Package.Name] {
|
|
continue
|
|
}
|
|
payload, _ := json.Marshal(update)
|
|
fmt.Fprintf(writer, "event: package_updated\ndata: %s\n\n", payload)
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) authorized(request *http.Request) bool {
|
|
if s.DeveloperToken == "" {
|
|
return false
|
|
}
|
|
value := request.Header.Get("Authorization")
|
|
if strings.HasPrefix(value, "Bearer ") {
|
|
return strings.TrimPrefix(value, "Bearer ") == s.DeveloperToken
|
|
}
|
|
return request.Header.Get("X-Developer-Token") == s.DeveloperToken
|
|
}
|
|
|
|
func writeJSON(writer http.ResponseWriter, status int, value any) {
|
|
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
writer.WriteHeader(status)
|
|
_ = json.NewEncoder(writer).Encode(value)
|
|
}
|
|
|
|
func writeError(writer http.ResponseWriter, status int, message string) {
|
|
writeJSON(writer, status, map[string]string{"error": message})
|
|
}
|
|
|
|
func logging(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.Method == http.MethodOptions {
|
|
writer.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
next.ServeHTTP(writer, request)
|
|
})
|
|
}
|
|
|
|
var _ = errors.New
|