Files
story/internal/httpapi/fleet_handlers.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

134 lines
3.9 KiB
Go

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