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{ Project: s.projectScope, 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("channel"), 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] fileName := filepath.Base(strings.TrimSpace(target.FileName)) if err := registry.ValidateFileName(fileName); err != nil { fileName = fmt.Sprintf("%s-%s.pkg", name, version) } writer.Header().Set("Content-Type", "application/octet-stream") writer.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, 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 }