feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
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.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user