323 lines
11 KiB
Go
323 lines
11 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"scout/internal/registry"
|
|
"scout/internal/users"
|
|
"scout/pkg/scoutpkg"
|
|
)
|
|
|
|
func (s *Server) packageList(writer http.ResponseWriter, request *http.Request) {
|
|
queryValues := request.URL.Query()
|
|
items := s.Registry.ListFor(
|
|
queryValues.Get("platform"),
|
|
queryValues.Get("architecture"),
|
|
queryValues.Get("channel"),
|
|
queryValues.Get("current_version"),
|
|
)
|
|
query := strings.ToLower(strings.TrimSpace(queryValues.Get("query")))
|
|
if query != "" {
|
|
filtered := make([]registry.Package, 0, len(items))
|
|
for _, item := range items {
|
|
if strings.Contains(strings.ToLower(item.Name), query) || strings.Contains(strings.ToLower(item.Description), query) {
|
|
filtered = append(filtered, item)
|
|
}
|
|
}
|
|
items = filtered
|
|
}
|
|
total := len(items)
|
|
page := positiveInt(queryValues.Get("page"), 1)
|
|
pageSize := positiveInt(queryValues.Get("page_size"), 50)
|
|
if pageSize > 100 {
|
|
pageSize = 100
|
|
}
|
|
start := (page - 1) * pageSize
|
|
if start > len(items) {
|
|
start = len(items)
|
|
}
|
|
end := start + pageSize
|
|
if end > len(items) {
|
|
end = len(items)
|
|
}
|
|
writeJSON(writer, http.StatusOK, map[string]any{
|
|
"packages": items[start:end],
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
})
|
|
}
|
|
|
|
func (s *Server) upload(writer http.ResponseWriter, request *http.Request) {
|
|
uploader, authorized := s.uploadUser(request)
|
|
if !authorized {
|
|
if _, loggedIn := s.authenticatedUser(request); loggedIn {
|
|
writeError(writer, http.StatusForbidden, "\u9700\u8981\u5f00\u53d1\u8005\u6216\u7ba1\u7406\u5458\u6743\u9650")
|
|
} else {
|
|
writeError(writer, http.StatusUnauthorized, "\u8bf7\u5148\u767b\u5f55\u5f00\u53d1\u8005\u8d26\u6237")
|
|
}
|
|
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 {
|
|
s.uploadError(writer, uploader, http.StatusBadRequest, "\u4e0a\u4f20\u8868\u5355\u65e0\u6548: "+err.Error())
|
|
return
|
|
}
|
|
file, header, err := request.FormFile("file")
|
|
if err != nil {
|
|
s.uploadError(writer, uploader, http.StatusBadRequest, "\u7f3a\u5c11\u5305\u6587\u4ef6\u5b57\u6bb5 file")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
fileName := filepath.Base(header.Filename)
|
|
if err := registry.ValidateFileName(fileName); err != nil {
|
|
s.uploadError(writer, uploader, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
input := registry.Package{
|
|
Name: strings.TrimSpace(request.FormValue("name")),
|
|
Version: strings.TrimSpace(request.FormValue("version")),
|
|
Description: strings.TrimSpace(request.FormValue("description")),
|
|
PackageType: strings.TrimSpace(request.FormValue("package_type")),
|
|
Channel: strings.TrimSpace(request.FormValue("channel")),
|
|
Status: strings.TrimSpace(request.FormValue("status")),
|
|
FileName: fileName,
|
|
UploaderID: uploader.ID,
|
|
UploaderEmail: uploader.Email,
|
|
}
|
|
|
|
var item registry.Package
|
|
if strings.EqualFold(filepath.Ext(fileName), ".scout") {
|
|
item, err = s.uploadScoutPackage(file, header.Size, input)
|
|
} else {
|
|
platform := strings.TrimSpace(request.FormValue("platform"))
|
|
architecture := strings.TrimSpace(request.FormValue("architecture"))
|
|
if input.Name == "" || input.Version == "" || platform == "" || architecture == "" {
|
|
s.uploadError(writer, uploader, http.StatusBadRequest, "\u666e\u901a\u5305\u5fc5\u987b\u6307\u5b9a\u5305\u540d\u3001\u7248\u672c\u3001\u5e73\u53f0\u548c\u67b6\u6784")
|
|
return
|
|
}
|
|
target := registry.Target{
|
|
Platform: platform,
|
|
Architecture: architecture,
|
|
PackageType: input.PackageType,
|
|
FileName: fileName,
|
|
UpgradeFrom: splitValues(request.FormValue("upgrade_from")),
|
|
}
|
|
item, err = s.Registry.AddArtifact(input, target, file)
|
|
}
|
|
if err != nil {
|
|
status := http.StatusBadRequest
|
|
if strings.Contains(err.Error(), "\u5df2\u7ecf\u5b58\u5728") {
|
|
status = http.StatusConflict
|
|
}
|
|
s.uploadError(writer, uploader, status, err.Error())
|
|
return
|
|
}
|
|
s.sendUserEmail(uploader, "Scout \u5305\u4e0a\u4f20\u6210\u529f", fmt.Sprintf("\u5305 %s %s \u5df2\u4e0a\u4f20\u6210\u529f\u3002\n\nSHA-256\uff1a%s\n\u5927\u5c0f\uff1a%d \u5b57\u8282", item.Name, item.Version, item.SHA256, item.Size))
|
|
writeJSON(writer, http.StatusCreated, item)
|
|
}
|
|
|
|
func (s *Server) uploadScoutPackage(file interface {
|
|
io.Reader
|
|
io.ReaderAt
|
|
io.Seeker
|
|
}, size int64, input registry.Package) (registry.Package, error) {
|
|
manifest, err := scoutpkg.Inspect(file, size)
|
|
if err != nil {
|
|
return registry.Package{}, err
|
|
}
|
|
if input.Name != "" && input.Name != manifest.Name {
|
|
return registry.Package{}, fmt.Errorf("\u8868\u5355\u5305\u540d\u4e0e Scout \u6e05\u5355\u4e0d\u4e00\u81f4")
|
|
}
|
|
if input.Version != "" && input.Version != manifest.Version {
|
|
return registry.Package{}, fmt.Errorf("\u8868\u5355\u7248\u672c\u4e0e Scout \u6e05\u5355\u4e0d\u4e00\u81f4")
|
|
}
|
|
if input.Channel != "" && manifest.Channel != "" && input.Channel != manifest.Channel {
|
|
return registry.Package{}, fmt.Errorf("\u8868\u5355\u53d1\u5e03\u901a\u9053\u4e0e Scout \u6e05\u5355\u4e0d\u4e00\u81f4")
|
|
}
|
|
if input.PackageType != "" && manifest.PackageType != "" && input.PackageType != manifest.PackageType {
|
|
return registry.Package{}, fmt.Errorf("\u8868\u5355\u5305\u7c7b\u578b\u4e0e Scout \u6e05\u5355\u4e0d\u4e00\u81f4")
|
|
}
|
|
input.Name = manifest.Name
|
|
input.Version = manifest.Version
|
|
if input.Description == "" {
|
|
input.Description = manifest.Description
|
|
}
|
|
if input.Channel == "" {
|
|
input.Channel = manifest.Channel
|
|
}
|
|
if input.PackageType == "" {
|
|
input.PackageType = manifest.PackageType
|
|
}
|
|
targets := make([]registry.Target, 0, len(manifest.Targets))
|
|
for _, target := range manifest.Targets {
|
|
targets = append(targets, registry.Target{
|
|
ID: target.ID,
|
|
Platform: target.Platform,
|
|
Architecture: target.Architecture,
|
|
PackageType: target.PackageType,
|
|
FileName: input.FileName,
|
|
Entrypoint: target.Entrypoint,
|
|
UpgradeFrom: append([]string(nil), target.UpgradeFrom...),
|
|
})
|
|
}
|
|
if _, err := file.Seek(0, io.SeekStart); err != nil {
|
|
return registry.Package{}, fmt.Errorf("\u91cd\u7f6e Scout \u5305\u8bfb\u53d6\u4f4d\u7f6e\u5931\u8d25: %w", err)
|
|
}
|
|
return s.Registry.AddScoutPackage(input, targets, file)
|
|
}
|
|
|
|
func (s *Server) uploadError(writer http.ResponseWriter, uploader users.User, status int, message string) {
|
|
s.notifyUploadFailure(uploader, message)
|
|
writeError(writer, status, message)
|
|
}
|
|
|
|
func (s *Server) packageRoute(writer http.ResponseWriter, request *http.Request) {
|
|
pathValue := strings.TrimPrefix(request.URL.Path, "/api/v1/packages/")
|
|
parts := strings.Split(strings.Trim(pathValue, "/"), "/")
|
|
query := request.URL.Query()
|
|
platform := query.Get("platform")
|
|
architecture := query.Get("architecture")
|
|
channel := query.Get("channel")
|
|
currentVersion := query.Get("current_version")
|
|
if len(parts) == 1 && parts[0] != "" {
|
|
item, ok := s.Registry.LatestFor(parts[0], platform, architecture, channel, currentVersion)
|
|
if !ok {
|
|
writeError(writer, http.StatusNotFound, "\u6ca1\u6709\u627e\u5230\u517c\u5bb9\u7684\u5305")
|
|
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.VersionsFor(
|
|
parts[0], platform, architecture, channel, currentVersion,
|
|
),
|
|
})
|
|
return
|
|
}
|
|
writeError(writer, http.StatusNotFound, "\u63a5\u53e3\u4e0d\u5b58\u5728")
|
|
return
|
|
}
|
|
if len(parts) == 3 && parts[2] == "download" {
|
|
s.download(writer, request, parts[0], parts[1])
|
|
return
|
|
}
|
|
writeError(writer, http.StatusNotFound, "\u63a5\u53e3\u4e0d\u5b58\u5728")
|
|
}
|
|
|
|
func (s *Server) download(writer http.ResponseWriter, request *http.Request, name, version string) {
|
|
query := request.URL.Query()
|
|
file, item, err := s.Registry.OpenPackageFor(
|
|
name,
|
|
version,
|
|
query.Get("platform"),
|
|
query.Get("architecture"),
|
|
query.Get("current_version"),
|
|
)
|
|
if err != nil || len(item.Targets) == 0 {
|
|
writeError(writer, http.StatusNotFound, "\u5305\u7248\u672c\u4e0d\u5b58\u5728\u6216\u4e0d\u517c\u5bb9")
|
|
return
|
|
}
|
|
defer file.Close()
|
|
target := item.Targets[0]
|
|
writer.Header().Set("Content-Type", "application/octet-stream")
|
|
writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, target.FileName))
|
|
writer.Header().Set("X-Scout-SHA256", target.SHA256)
|
|
writer.Header().Set("X-Scout-Platform", target.Platform)
|
|
writer.Header().Set("X-Scout-Architecture", target.Architecture)
|
|
writer.Header().Set("X-Scout-Package-Type", target.PackageType)
|
|
writer.Header().Set("Content-Length", strconv.FormatInt(target.Size, 10))
|
|
if request.Method == http.MethodHead {
|
|
return
|
|
}
|
|
_, _ = io.Copy(writer, file)
|
|
}
|
|
|
|
func (s *Server) stream(writer http.ResponseWriter, request *http.Request) {
|
|
flusher, ok := writer.(http.Flusher)
|
|
if !ok {
|
|
writeError(writer, http.StatusInternalServerError, "\u5f53\u524d\u670d\u52a1\u5668\u4e0d\u652f\u6301\u5b9e\u65f6\u901a\u77e5")
|
|
return
|
|
}
|
|
query := request.URL.Query()
|
|
wanted := make(map[string]bool)
|
|
for _, name := range splitValues(query.Get("packages")) {
|
|
wanted[name] = true
|
|
}
|
|
versions := parseVersionMap(query.Get("versions"))
|
|
platform := query.Get("platform")
|
|
architecture := query.Get("architecture")
|
|
channel := strings.ToLower(strings.TrimSpace(query.Get("channel")))
|
|
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, ": connected\n\n")
|
|
flusher.Flush()
|
|
for {
|
|
select {
|
|
case <-request.Context().Done():
|
|
return
|
|
case update, open := <-updates:
|
|
if !open {
|
|
return
|
|
}
|
|
if len(wanted) > 0 && !wanted[update.Package.Name] {
|
|
continue
|
|
}
|
|
if channel != "" && update.Package.Channel != channel {
|
|
continue
|
|
}
|
|
currentVersion := versions[update.Package.Name]
|
|
if currentVersion != "" && registry.CompareVersions(update.Package.Version, currentVersion) <= 0 {
|
|
continue
|
|
}
|
|
selected, compatible := registry.SelectTarget(update.Package, platform, architecture, currentVersion)
|
|
if !compatible {
|
|
continue
|
|
}
|
|
payload, _ := json.Marshal(registry.Update{Package: selected})
|
|
fmt.Fprintf(writer, "event: package_updated\ndata: %s\n\n", payload)
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
func splitValues(value string) []string {
|
|
parts := strings.Split(value, ",")
|
|
result := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
if part = strings.TrimSpace(part); part != "" {
|
|
result = append(result, part)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func parseVersionMap(value string) map[string]string {
|
|
result := make(map[string]string)
|
|
for _, entry := range splitValues(value) {
|
|
parts := strings.SplitN(entry, ":", 2)
|
|
if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
|
|
result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
|
|
}
|
|
}
|
|
return result
|
|
}
|