Files
story/internal/httpapi/fleet_handlers.go
T

134 lines
3.9 KiB
Go
Raw Normal View History

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)
}