package httpapi import ( "data-terminal-cloud/internal/devicefleet" "data-terminal-cloud/internal/saas" "errors" "fmt" "github.com/jackc/pgx/v5" "io" "net/http" "time" ) func writeFleetError(w http.ResponseWriter, r *http.Request, err error) { if errors.Is(err, pgx.ErrNoRows) { WriteError(w, r, 404, "FIRMWARE_NOT_FOUND", "固件或设备不存在,请刷新后重试") return } var e *devicefleet.Error if errors.As(err, &e) { WriteError(w, r, 409, e.Code, e.Message) } else { writeSaaSError(w, r, err) } } func FleetFirmwareHandler(s *saas.Service, action string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, public, ok := opsFirmwarePrincipal(w, r) if !ok { return } tenant, err := s.FleetTenant(r.Context(), p, public, r.Method == "POST") if err != nil { writeFleetError(w, r, err) return } id := r.PathValue("id") if id != "" && !validUUID(id) { WriteError(w, r, 400, "INVALID_ID", "固件ID不正确") return } var out any switch action { case "list": var items []devicefleet.Release items, err = s.Fleet.Releases(r.Context(), tenant) out = map[string]any{"items": items} case "models": var items []string items, err = s.Fleet.Models(r.Context(), tenant) out = map[string]any{"items": items} case "prepare": var input devicefleet.ReleaseInput if !decodeWebJSON(w, r, &input, "固件信息不正确") { return } out, err = s.Fleet.Prepare(r.Context(), tenant, input) case "finalize": out, err = s.Fleet.Finalize(r.Context(), tenant, id) case "download": out, err = s.Fleet.Download(r.Context(), tenant, id) } if err != nil { writeFleetError(w, r, err) return } WriteJSON(w, r, 200, out) }) } func FleetDeviceHandler(s *saas.Service, upgrade bool) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p, ok := resourcePrincipal(w, r, "id") if !ok { return } var out any var err error if upgrade { var in struct { Code int64 `json:"version_code"` } if !decodeWebJSON(w, r, &in, "目标版本顺序号不正确") { return } out, err = s.FleetOTA(r.Context(), p, r.PathValue("id"), RequestID(r.Context()), in.Code) } else { tenant := r.Header.Get("X-Tenant-Id") if tenant != "" && !validUUID(tenant) { WriteError(w, r, 400, "INVALID_TENANT", "租户ID不正确") return } out, err = s.FleetState(r.Context(), p, r.PathValue("id"), tenant) } if err != nil { writeFleetError(w, r, err) return } WriteJSON(w, r, 200, out) }) } func FleetFirmwareDownloadHandler(s *devicefleet.Service) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { grant, err := s.ResolveDownload(r.Context(), r.PathValue("token")) if errors.Is(err, devicefleet.ErrDownloadTicketUnavailable) { http.NotFound(w, r) return } if err != nil { http.Error(w, "firmware download unavailable", http.StatusServiceUnavailable) return } object, err := s.OpenDownload(r.Context(), grant) if err != nil { http.Error(w, "firmware download unavailable", http.StatusServiceUnavailable) return } defer object.Close() serveFleetFirmwareContent(w, r, grant, object) }) } func serveFleetFirmwareContent(w http.ResponseWriter, r *http.Request, grant devicefleet.DownloadGrant, object io.ReadSeeker) { _ = http.NewResponseController(w).SetWriteDeadline(time.Now().Add(20 * time.Minute)) w.Header().Set("Accept-Ranges", "bytes") w.Header().Set("Cache-Control", "private, no-store") w.Header().Set("Content-Disposition", `attachment; filename="firmware.bin"`) w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("ETag", fmt.Sprintf(`"sha256-%s"`, grant.SHA256)) w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-Content-Type-Options", "nosniff") http.ServeContent(w, r, "firmware.bin", grant.CreatedAt, object) }