1108 lines
26 KiB
Go
1108 lines
26 KiB
Go
// backend\milestone_sync.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
type milestoneSyncResult struct {
|
|
TokenReceived bool `json:"tokenReceived"`
|
|
TokenType string `json:"tokenType"`
|
|
TokenExpiresAt time.Time `json:"tokenExpiresAt"`
|
|
RecordingServerID string `json:"recordingServerId"`
|
|
RecordingServerName string `json:"recordingServerName"`
|
|
RecordingServerVersion string `json:"recordingServerVersion"`
|
|
HardwareCount int `json:"hardwareCount"`
|
|
}
|
|
|
|
type milestoneSyncEvent struct {
|
|
Type string `json:"type"`
|
|
Message string `json:"message,omitempty"`
|
|
TokenType string `json:"tokenType,omitempty"`
|
|
TokenExpiresAt *time.Time `json:"tokenExpiresAt,omitempty"`
|
|
RecordingServerID string `json:"recordingServerId,omitempty"`
|
|
RecordingServerName string `json:"recordingServerName,omitempty"`
|
|
RecordingServerVersion string `json:"recordingServerVersion,omitempty"`
|
|
HardwareCount int `json:"hardwareCount,omitempty"`
|
|
Settings *milestoneSettingsResponse `json:"settings,omitempty"`
|
|
}
|
|
|
|
type milestoneRecordingServersResponse struct {
|
|
Array []milestoneRecordingServer `json:"array"`
|
|
}
|
|
|
|
type milestoneRecordingServer struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"displayName"`
|
|
Version string `json:"version"`
|
|
ProductVersion string `json:"productVersion"`
|
|
RecordingServerVersion string `json:"recordingServerVersion"`
|
|
}
|
|
|
|
type milestoneHardwareResponse struct {
|
|
Array []milestoneHardware `json:"array"`
|
|
}
|
|
|
|
type milestoneHardware struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
DisplayName string `json:"displayName"`
|
|
DeviceName string `json:"deviceName"`
|
|
Address string `json:"address"`
|
|
Enabled *bool `json:"enabled"`
|
|
Disabled *bool `json:"disabled"`
|
|
Relations milestoneRelations `json:"relations"`
|
|
}
|
|
|
|
type milestoneRelations struct {
|
|
Parent milestoneRelationRef `json:"parent"`
|
|
Self milestoneRelationRef `json:"self"`
|
|
}
|
|
|
|
type milestoneRelationRef struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
type milestoneHardwareDriverSettingsResponse struct {
|
|
Array []milestoneHardwareDriverSettingsItem `json:"array"`
|
|
}
|
|
|
|
type milestoneHardwareDriverSettingsItem struct {
|
|
DisplayName string `json:"displayName"`
|
|
HardwareDriverSettings milestoneHardwareDriverSettings `json:"hardwareDriverSettings"`
|
|
Relations milestoneRelations `json:"relations"`
|
|
}
|
|
|
|
type milestoneHardwareDriverSettings struct {
|
|
DetectedModelName string `json:"detectedModelName"`
|
|
ProductID string `json:"productID"`
|
|
MacAddress string `json:"macAddress"`
|
|
IPAddress string `json:"ipAddress"`
|
|
Address string `json:"address"`
|
|
HostName string `json:"hostName"`
|
|
DeviceModelName string `json:"deviceModelName"`
|
|
DeviceManufacturer string `json:"deviceManufacturer"`
|
|
FirmwareVersion string `json:"firmwareVersion"`
|
|
SerialNumber string `json:"serialNumber"`
|
|
}
|
|
|
|
type milestoneHardwareDevice struct {
|
|
HardwareID string
|
|
RecordingServerID string
|
|
DisplayName string
|
|
DeviceName string
|
|
Manufacturer string
|
|
MacAddress string
|
|
IPAddress string
|
|
Port string
|
|
DeviceModelName string
|
|
SerialNumber string
|
|
FirmwareVersion string
|
|
Enabled bool
|
|
Cameras []milestoneNamedDevice
|
|
Microphones []milestoneNamedDevice
|
|
}
|
|
|
|
func writeMilestoneSyncEvent(w http.ResponseWriter, event milestoneSyncEvent) {
|
|
if err := json.NewEncoder(w).Encode(event); err != nil {
|
|
log.Printf("Milestone sync stream event could not be written: %v", err)
|
|
return
|
|
}
|
|
|
|
if flusher, ok := w.(http.Flusher); ok {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
func emitMilestoneSyncEvent(
|
|
progress func(milestoneSyncEvent),
|
|
event milestoneSyncEvent,
|
|
) {
|
|
if progress != nil {
|
|
progress(event)
|
|
}
|
|
}
|
|
|
|
func (s *Server) refreshMilestoneTokenAndServerID(
|
|
ctx context.Context,
|
|
credentials milestoneCredentials,
|
|
) (milestoneSyncResult, error) {
|
|
return s.refreshMilestoneTokenAndServerIDWithProgress(ctx, credentials, nil)
|
|
}
|
|
|
|
func (s *Server) refreshMilestoneTokenAndServerIDWithProgress(
|
|
ctx context.Context,
|
|
credentials milestoneCredentials,
|
|
progress func(milestoneSyncEvent),
|
|
) (milestoneSyncResult, error) {
|
|
var syncResult milestoneSyncResult
|
|
|
|
tokenResponse, err := s.requestMilestoneToken(ctx, credentials)
|
|
if err != nil {
|
|
return syncResult, err
|
|
}
|
|
|
|
accessToken := strings.TrimSpace(tokenResponse.AccessToken)
|
|
if accessToken == "" {
|
|
return syncResult, errors.New("milestone returned empty access token")
|
|
}
|
|
|
|
tokenType := strings.TrimSpace(tokenResponse.TokenType)
|
|
if tokenType == "" {
|
|
tokenType = "Bearer"
|
|
}
|
|
|
|
expiresIn := tokenResponse.ExpiresIn
|
|
if expiresIn <= 0 {
|
|
expiresIn = 3600
|
|
}
|
|
|
|
tokenExpiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second)
|
|
|
|
syncResult.TokenReceived = true
|
|
syncResult.TokenType = tokenType
|
|
syncResult.TokenExpiresAt = tokenExpiresAt
|
|
|
|
emitMilestoneSyncEvent(progress, milestoneSyncEvent{
|
|
Type: "token",
|
|
TokenType: tokenType,
|
|
TokenExpiresAt: &tokenExpiresAt,
|
|
})
|
|
|
|
serverID, serverName, serverVersion, err := s.requestMilestoneServerID(
|
|
ctx,
|
|
credentials.Host,
|
|
tokenType,
|
|
accessToken,
|
|
credentials.SkipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return syncResult, err
|
|
}
|
|
|
|
syncResult.RecordingServerID = serverID
|
|
syncResult.RecordingServerName = serverName
|
|
syncResult.RecordingServerVersion = serverVersion
|
|
|
|
emitMilestoneSyncEvent(progress, milestoneSyncEvent{
|
|
Type: "recordingServer",
|
|
RecordingServerID: serverID,
|
|
RecordingServerName: serverName,
|
|
RecordingServerVersion: serverVersion,
|
|
})
|
|
|
|
hardwareDevices, err := s.requestMilestoneHardwareDevices(
|
|
ctx,
|
|
credentials.Host,
|
|
serverID,
|
|
accessToken,
|
|
credentials.SkipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return syncResult, err
|
|
}
|
|
|
|
tx, err := s.db.Begin(ctx)
|
|
if err != nil {
|
|
return syncResult, err
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
encryptionKey := s.milestoneEncryptionKey()
|
|
|
|
_, err = tx.Exec(
|
|
ctx,
|
|
`
|
|
UPDATE milestone_settings
|
|
SET
|
|
access_token_encrypted = pgp_sym_encrypt($1, $2),
|
|
token_type = $3,
|
|
token_expires_at = $4,
|
|
token_updated_at = now(),
|
|
server_id = $5,
|
|
server_name = $6,
|
|
server_version = $7,
|
|
server_updated_at = now(),
|
|
updated_at = now()
|
|
WHERE id = 1
|
|
`,
|
|
accessToken,
|
|
encryptionKey,
|
|
tokenType,
|
|
tokenExpiresAt,
|
|
serverID,
|
|
serverName,
|
|
serverVersion,
|
|
)
|
|
if err != nil {
|
|
return syncResult, err
|
|
}
|
|
|
|
syncedHardwareCount, err := syncMilestoneHardwareDevices(ctx, tx, hardwareDevices)
|
|
if err != nil {
|
|
return syncResult, err
|
|
}
|
|
|
|
syncResult.HardwareCount = syncedHardwareCount
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return syncResult, err
|
|
}
|
|
|
|
emitMilestoneSyncEvent(progress, milestoneSyncEvent{
|
|
Type: "hardware",
|
|
HardwareCount: syncResult.HardwareCount,
|
|
})
|
|
|
|
log.Printf(
|
|
"Milestone sync completed successfully: recordingServerId=%s hardwareDevices=%d",
|
|
serverID,
|
|
syncedHardwareCount,
|
|
)
|
|
|
|
return syncResult, nil
|
|
}
|
|
|
|
func (s *Server) requestMilestoneServerID(
|
|
ctx context.Context,
|
|
host string,
|
|
tokenType string,
|
|
accessToken string,
|
|
skipTLSVerify bool,
|
|
) (string, string, string, error) {
|
|
requestURL := strings.TrimRight(host, "/") + "/API/rest/v1/recordingServers"
|
|
|
|
log.Printf(
|
|
"Requesting Milestone recording server id: url=%s skipTLSVerify=%t",
|
|
requestURL,
|
|
skipTLSVerify,
|
|
)
|
|
|
|
request, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodGet,
|
|
requestURL,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
if tokenType == "" {
|
|
tokenType = "Bearer"
|
|
}
|
|
|
|
request.Header.Set("Authorization", tokenType+" "+accessToken)
|
|
request.Header.Set("Accept", "application/json")
|
|
|
|
client := milestoneHTTPClient(skipTLSVerify)
|
|
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return "", "", "", fmt.Errorf(
|
|
"milestone recording servers request failed: url=%s status=%d body=%s",
|
|
requestURL,
|
|
response.StatusCode,
|
|
truncateMilestoneLogBody(body),
|
|
)
|
|
}
|
|
|
|
var recordingServersResponse milestoneRecordingServersResponse
|
|
if err := json.Unmarshal(body, &recordingServersResponse); err != nil {
|
|
return "", "", "", fmt.Errorf(
|
|
"milestone recording servers response could not be parsed: body=%s error=%w",
|
|
truncateMilestoneLogBody(body),
|
|
err,
|
|
)
|
|
}
|
|
|
|
if len(recordingServersResponse.Array) == 0 {
|
|
return "", "", "", fmt.Errorf(
|
|
"milestone returned no recording servers: body=%s",
|
|
truncateMilestoneLogBody(body),
|
|
)
|
|
}
|
|
|
|
recordingServer := recordingServersResponse.Array[0]
|
|
|
|
recordingServerID := strings.TrimSpace(recordingServer.ID)
|
|
recordingServerName := strings.TrimSpace(recordingServer.DisplayName)
|
|
|
|
if recordingServerName == "" {
|
|
recordingServerName = strings.TrimSpace(recordingServer.Name)
|
|
}
|
|
|
|
recordingServerVersion := strings.TrimSpace(recordingServer.Version)
|
|
|
|
if recordingServerVersion == "" {
|
|
recordingServerVersion = strings.TrimSpace(recordingServer.ProductVersion)
|
|
}
|
|
|
|
if recordingServerVersion == "" {
|
|
recordingServerVersion = strings.TrimSpace(recordingServer.RecordingServerVersion)
|
|
}
|
|
|
|
if recordingServerID == "" {
|
|
return "", "", "", fmt.Errorf(
|
|
"milestone returned recording server without id: body=%s",
|
|
truncateMilestoneLogBody(body),
|
|
)
|
|
}
|
|
|
|
log.Printf(
|
|
"Milestone recording server received successfully: id=%s displayName=%q version=%q",
|
|
recordingServerID,
|
|
recordingServerName,
|
|
recordingServerVersion,
|
|
)
|
|
|
|
return recordingServerID, recordingServerName, recordingServerVersion, nil
|
|
}
|
|
|
|
func (s *Server) requestMilestoneHardwareDevices(
|
|
ctx context.Context,
|
|
host string,
|
|
recordingServerID string,
|
|
accessToken string,
|
|
skipTLSVerify bool,
|
|
) ([]milestoneHardwareDevice, error) {
|
|
hardwareItems, err := s.requestMilestoneHardware(
|
|
ctx,
|
|
host,
|
|
recordingServerID,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
result := make([]milestoneHardwareDevice, 0, len(hardwareItems))
|
|
|
|
for _, hardware := range hardwareItems {
|
|
hardwareID := strings.TrimSpace(hardware.ID)
|
|
if hardwareID == "" {
|
|
hardwareID = strings.TrimSpace(hardware.Relations.Self.ID)
|
|
}
|
|
|
|
if hardwareID == "" {
|
|
log.Printf("Milestone hardware skipped: missing hardware id")
|
|
continue
|
|
}
|
|
|
|
settings, err := s.requestMilestoneHardwareDriverSettings(
|
|
ctx,
|
|
host,
|
|
hardwareID,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hardwareDetails, err := s.requestMilestoneHardwareDetails(
|
|
ctx,
|
|
host,
|
|
hardwareID,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
displayName := strings.TrimSpace(hardware.DisplayName)
|
|
if displayName == "" {
|
|
displayName = strings.TrimSpace(hardware.Name)
|
|
}
|
|
|
|
model := strings.TrimSpace(settings.DeviceModelName)
|
|
if model == "" {
|
|
model = strings.TrimSpace(settings.DetectedModelName)
|
|
}
|
|
|
|
manufacturer := normalizeMilestoneManufacturer(
|
|
settings.DeviceManufacturer,
|
|
model,
|
|
)
|
|
|
|
if manufacturer == "" {
|
|
manufacturer = normalizeMilestoneManufacturer(
|
|
settings.ProductID,
|
|
model,
|
|
)
|
|
}
|
|
|
|
if manufacturer == "" {
|
|
manufacturer = extractMilestoneManufacturerFromName(model)
|
|
}
|
|
|
|
deviceName := strings.TrimSpace(hardware.DeviceName)
|
|
if deviceName == "" {
|
|
deviceName = displayName
|
|
}
|
|
|
|
enabled := true
|
|
|
|
if hardware.Enabled != nil {
|
|
enabled = *hardware.Enabled
|
|
}
|
|
|
|
if hardware.Disabled != nil {
|
|
enabled = !*hardware.Disabled
|
|
}
|
|
|
|
hardwareAddress := strings.TrimSpace(milestoneStringValue(hardwareDetails["address"]))
|
|
if hardwareAddress == "" {
|
|
hardwareAddress = hardware.Address
|
|
}
|
|
|
|
ipAddress, port := extractMilestoneHardwareAddressParts(hardwareAddress)
|
|
|
|
embeddedCameras := milestoneNamedDevicesFromHardwareCollection(
|
|
hardwareDetails,
|
|
"cameras",
|
|
)
|
|
|
|
embeddedMicrophones := milestoneNamedDevicesFromHardwareCollection(
|
|
hardwareDetails,
|
|
"microphones",
|
|
)
|
|
|
|
camerasWithDisabled, err := s.requestMilestoneNamedDevicesByHardware(
|
|
ctx,
|
|
host,
|
|
"cameras",
|
|
hardwareID,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
microphonesWithDisabled, err := s.requestMilestoneNamedDevicesByHardware(
|
|
ctx,
|
|
host,
|
|
"microphones",
|
|
hardwareID,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cameras := mergeMilestoneNamedDevices(
|
|
embeddedCameras,
|
|
camerasWithDisabled,
|
|
)
|
|
|
|
microphones := mergeMilestoneNamedDevices(
|
|
embeddedMicrophones,
|
|
microphonesWithDisabled,
|
|
)
|
|
|
|
result = append(result, milestoneHardwareDevice{
|
|
HardwareID: hardwareID,
|
|
RecordingServerID: recordingServerID,
|
|
DisplayName: displayName,
|
|
DeviceName: deviceName,
|
|
Manufacturer: manufacturer,
|
|
MacAddress: strings.TrimSpace(settings.MacAddress),
|
|
IPAddress: ipAddress,
|
|
Port: port,
|
|
DeviceModelName: model,
|
|
SerialNumber: strings.TrimSpace(settings.SerialNumber),
|
|
FirmwareVersion: strings.TrimSpace(settings.FirmwareVersion),
|
|
Enabled: enabled,
|
|
Cameras: cameras,
|
|
Microphones: microphones,
|
|
})
|
|
}
|
|
|
|
cameraCount := 0
|
|
microphoneCount := 0
|
|
|
|
for _, hardwareDevice := range result {
|
|
cameraCount += len(hardwareDevice.Cameras)
|
|
microphoneCount += len(hardwareDevice.Microphones)
|
|
}
|
|
|
|
log.Printf(
|
|
"Milestone hardware loaded successfully: recordingServerId=%s count=%d cameras=%d microphones=%d",
|
|
recordingServerID,
|
|
len(result),
|
|
cameraCount,
|
|
microphoneCount,
|
|
)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Server) requestMilestoneNamedDevicesByHardware(
|
|
ctx context.Context,
|
|
host string,
|
|
collection string,
|
|
hardwareID string,
|
|
accessToken string,
|
|
skipTLSVerify bool,
|
|
) ([]milestoneNamedDevice, error) {
|
|
switch collection {
|
|
case "cameras", "microphones":
|
|
default:
|
|
return nil, fmt.Errorf("unsupported milestone device collection: %s", collection)
|
|
}
|
|
|
|
requestURL := strings.TrimRight(host, "/") +
|
|
"/API/rest/v1/" +
|
|
collection +
|
|
"?disabled"
|
|
|
|
log.Printf(
|
|
"Requesting Milestone %s with disabled: url=%s hardwareId=%s skipTLSVerify=%t",
|
|
collection,
|
|
requestURL,
|
|
hardwareID,
|
|
skipTLSVerify,
|
|
)
|
|
|
|
body, err := requestMilestoneJSON(
|
|
ctx,
|
|
requestURL,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return milestoneNamedDevicesFromListResponse(body, collection, hardwareID)
|
|
}
|
|
|
|
func extractMilestoneHardwareIPAddress(value string) string {
|
|
host, _ := extractMilestoneHardwareAddressParts(value)
|
|
return host
|
|
}
|
|
|
|
func defaultMilestonePortForScheme(scheme string) string {
|
|
switch strings.ToLower(strings.TrimSpace(scheme)) {
|
|
case "https":
|
|
return "443"
|
|
case "http":
|
|
return "80"
|
|
default:
|
|
return "80"
|
|
}
|
|
}
|
|
|
|
func extractMilestoneHardwareAddressParts(value string) (string, string) {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return "", ""
|
|
}
|
|
|
|
parsedURL, err := url.Parse(value)
|
|
if err == nil && parsedURL.Hostname() != "" {
|
|
scheme := strings.ToLower(strings.TrimSpace(parsedURL.Scheme))
|
|
if scheme == "" {
|
|
scheme = "http"
|
|
}
|
|
|
|
port := strings.TrimSpace(parsedURL.Port())
|
|
if port == "" {
|
|
port = defaultMilestonePortForScheme(scheme)
|
|
}
|
|
|
|
return strings.TrimSpace(parsedURL.Hostname()), port
|
|
}
|
|
|
|
scheme := "http"
|
|
lowerValue := strings.ToLower(value)
|
|
|
|
if strings.HasPrefix(lowerValue, "https://") {
|
|
scheme = "https"
|
|
}
|
|
|
|
value = strings.TrimPrefix(value, "http://")
|
|
value = strings.TrimPrefix(value, "https://")
|
|
value = strings.TrimRight(value, "/")
|
|
|
|
if host, port, found := strings.Cut(value, ":"); found {
|
|
port = strings.TrimSpace(port)
|
|
|
|
if isOnlyDigits(port) {
|
|
return strings.TrimSpace(host), port
|
|
}
|
|
}
|
|
|
|
return strings.TrimSpace(value), defaultMilestonePortForScheme(scheme)
|
|
}
|
|
|
|
func extractMilestoneHardwarePort(value string) string {
|
|
_, port := extractMilestoneHardwareAddressParts(value)
|
|
return port
|
|
}
|
|
|
|
func (s *Server) requestMilestoneHardware(
|
|
ctx context.Context,
|
|
host string,
|
|
recordingServerID string,
|
|
accessToken string,
|
|
skipTLSVerify bool,
|
|
) ([]milestoneHardware, error) {
|
|
requestURL := strings.TrimRight(host, "/") +
|
|
"/API/rest/v1/recordingServers/" +
|
|
url.PathEscape(recordingServerID) +
|
|
"/hardware?disabled"
|
|
|
|
log.Printf(
|
|
"Requesting Milestone hardware: url=%s skipTLSVerify=%t",
|
|
requestURL,
|
|
skipTLSVerify,
|
|
)
|
|
|
|
body, err := requestMilestoneJSON(
|
|
ctx,
|
|
requestURL,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var hardwareResponse milestoneHardwareResponse
|
|
if err := json.Unmarshal(body, &hardwareResponse); err != nil {
|
|
return nil, fmt.Errorf(
|
|
"milestone hardware response could not be parsed: body=%s error=%w",
|
|
truncateMilestoneLogBody(body),
|
|
err,
|
|
)
|
|
}
|
|
|
|
return hardwareResponse.Array, nil
|
|
}
|
|
|
|
func (s *Server) requestMilestoneHardwareDetails(
|
|
ctx context.Context,
|
|
host string,
|
|
hardwareID string,
|
|
accessToken string,
|
|
skipTLSVerify bool,
|
|
) (map[string]any, error) {
|
|
requestURL := strings.TrimRight(host, "/") +
|
|
"/API/rest/v1/hardware/" +
|
|
url.PathEscape(hardwareID)
|
|
|
|
log.Printf(
|
|
"Requesting Milestone hardware details: url=%s skipTLSVerify=%t",
|
|
requestURL,
|
|
skipTLSVerify,
|
|
)
|
|
|
|
body, err := requestMilestoneJSON(
|
|
ctx,
|
|
requestURL,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var envelope milestoneHardwareUpdateEnvelope
|
|
if err := json.Unmarshal(body, &envelope); err == nil && envelope.Data != nil {
|
|
return envelope.Data, nil
|
|
}
|
|
|
|
var direct map[string]any
|
|
if err := json.Unmarshal(body, &direct); err != nil {
|
|
return nil, fmt.Errorf(
|
|
"milestone hardware details response could not be parsed: body=%s error=%w",
|
|
truncateMilestoneLogBody(body),
|
|
err,
|
|
)
|
|
}
|
|
|
|
if data, ok := direct["data"].(map[string]any); ok {
|
|
return data, nil
|
|
}
|
|
|
|
return direct, nil
|
|
}
|
|
|
|
func (s *Server) requestMilestoneHardwareDriverSettings(
|
|
ctx context.Context,
|
|
host string,
|
|
hardwareID string,
|
|
accessToken string,
|
|
skipTLSVerify bool,
|
|
) (milestoneHardwareDriverSettings, error) {
|
|
requestURL := strings.TrimRight(host, "/") +
|
|
"/API/rest/v1/hardware/" +
|
|
url.PathEscape(hardwareID) +
|
|
"/hardwareDriverSettings"
|
|
|
|
log.Printf(
|
|
"Requesting Milestone hardware driver settings: url=%s skipTLSVerify=%t",
|
|
requestURL,
|
|
skipTLSVerify,
|
|
)
|
|
|
|
body, err := requestMilestoneJSON(
|
|
ctx,
|
|
requestURL,
|
|
accessToken,
|
|
skipTLSVerify,
|
|
)
|
|
if err != nil {
|
|
return milestoneHardwareDriverSettings{}, err
|
|
}
|
|
|
|
var settingsResponse milestoneHardwareDriverSettingsResponse
|
|
if err := json.Unmarshal(body, &settingsResponse); err != nil {
|
|
return milestoneHardwareDriverSettings{}, fmt.Errorf(
|
|
"milestone hardware driver settings response could not be parsed: body=%s error=%w",
|
|
truncateMilestoneLogBody(body),
|
|
err,
|
|
)
|
|
}
|
|
|
|
if len(settingsResponse.Array) == 0 {
|
|
return milestoneHardwareDriverSettings{}, fmt.Errorf(
|
|
"milestone returned no hardware driver settings: hardwareId=%s body=%s",
|
|
hardwareID,
|
|
truncateMilestoneLogBody(body),
|
|
)
|
|
}
|
|
|
|
return settingsResponse.Array[0].HardwareDriverSettings, nil
|
|
}
|
|
|
|
func requestMilestoneJSON(
|
|
ctx context.Context,
|
|
requestURL string,
|
|
accessToken string,
|
|
skipTLSVerify bool,
|
|
) ([]byte, error) {
|
|
request, err := http.NewRequestWithContext(
|
|
ctx,
|
|
http.MethodGet,
|
|
requestURL,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
request.Header.Set("Authorization", "Bearer "+accessToken)
|
|
request.Header.Set("Accept", "application/json")
|
|
|
|
client := milestoneHTTPClient(skipTLSVerify)
|
|
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return nil, fmt.Errorf(
|
|
"milestone request failed: url=%s status=%d body=%s",
|
|
requestURL,
|
|
response.StatusCode,
|
|
truncateMilestoneLogBody(body),
|
|
)
|
|
}
|
|
|
|
return body, nil
|
|
}
|
|
|
|
func syncMilestoneHardwareDevices(
|
|
ctx context.Context,
|
|
tx pgx.Tx,
|
|
hardwareDevices []milestoneHardwareDevice,
|
|
) (int, error) {
|
|
syncedCount := 0
|
|
|
|
for _, hardwareDevice := range hardwareDevices {
|
|
hardwareDevice.HardwareID = strings.TrimSpace(hardwareDevice.HardwareID)
|
|
hardwareDevice.RecordingServerID = strings.TrimSpace(hardwareDevice.RecordingServerID)
|
|
hardwareDevice.DisplayName = strings.TrimSpace(hardwareDevice.DisplayName)
|
|
hardwareDevice.DeviceName = strings.TrimSpace(hardwareDevice.DeviceName)
|
|
hardwareDevice.Manufacturer = strings.TrimSpace(hardwareDevice.Manufacturer)
|
|
hardwareDevice.DeviceModelName = strings.TrimSpace(hardwareDevice.DeviceModelName)
|
|
hardwareDevice.SerialNumber = strings.TrimSpace(hardwareDevice.SerialNumber)
|
|
hardwareDevice.MacAddress = strings.TrimSpace(hardwareDevice.MacAddress)
|
|
hardwareDevice.IPAddress = strings.TrimSpace(hardwareDevice.IPAddress)
|
|
hardwareDevice.Port = strings.TrimSpace(hardwareDevice.Port)
|
|
hardwareDevice.FirmwareVersion = strings.TrimSpace(hardwareDevice.FirmwareVersion)
|
|
|
|
if hardwareDevice.HardwareID == "" {
|
|
continue
|
|
}
|
|
|
|
if hardwareDevice.Manufacturer != "" {
|
|
if err := ensureOptionalDeviceLookupValue(
|
|
ctx,
|
|
tx,
|
|
"device_manufacturers",
|
|
hardwareDevice.Manufacturer,
|
|
); err != nil {
|
|
return syncedCount, err
|
|
}
|
|
}
|
|
|
|
synced, err := createOrUpdateMilestoneHardwareDevice(ctx, tx, hardwareDevice)
|
|
if err != nil {
|
|
return syncedCount, err
|
|
}
|
|
|
|
if err := syncMilestoneHardwareChildDevices(ctx, tx, hardwareDevice); err != nil {
|
|
return syncedCount, err
|
|
}
|
|
|
|
if synced {
|
|
syncedCount++
|
|
}
|
|
}
|
|
|
|
return syncedCount, nil
|
|
}
|
|
|
|
func syncMilestoneHardwareChildDevices(
|
|
ctx context.Context,
|
|
tx pgx.Tx,
|
|
hardwareDevice milestoneHardwareDevice,
|
|
) error {
|
|
hardwareID := strings.TrimSpace(hardwareDevice.HardwareID)
|
|
if hardwareID == "" {
|
|
return nil
|
|
}
|
|
|
|
_, err := tx.Exec(
|
|
ctx,
|
|
`
|
|
DELETE FROM milestone_hardware_child_devices
|
|
WHERE milestone_hardware_id = $1
|
|
`,
|
|
hardwareID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, camera := range hardwareDevice.Cameras {
|
|
if err := upsertMilestoneHardwareChildDevice(
|
|
ctx,
|
|
tx,
|
|
hardwareID,
|
|
"camera",
|
|
camera,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, microphone := range hardwareDevice.Microphones {
|
|
if err := upsertMilestoneHardwareChildDevice(
|
|
ctx,
|
|
tx,
|
|
hardwareID,
|
|
"microphone",
|
|
microphone,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func upsertMilestoneHardwareChildDevice(
|
|
ctx context.Context,
|
|
tx pgx.Tx,
|
|
hardwareID string,
|
|
deviceType string,
|
|
childDevice milestoneNamedDevice,
|
|
) error {
|
|
milestoneDeviceID := strings.TrimSpace(childDevice.ID)
|
|
if milestoneDeviceID == "" {
|
|
return nil
|
|
}
|
|
|
|
name := strings.TrimSpace(childDevice.Name)
|
|
displayName := strings.TrimSpace(childDevice.DisplayName)
|
|
shortName := strings.TrimSpace(childDevice.ShortName)
|
|
|
|
if name == "" {
|
|
name = displayName
|
|
}
|
|
if name == "" {
|
|
name = shortName
|
|
}
|
|
if displayName == "" {
|
|
displayName = name
|
|
}
|
|
if shortName == "" {
|
|
shortName = name
|
|
}
|
|
|
|
_, err := tx.Exec(
|
|
ctx,
|
|
`
|
|
INSERT INTO milestone_hardware_child_devices (
|
|
milestone_hardware_id,
|
|
milestone_device_id,
|
|
device_type,
|
|
name,
|
|
display_name,
|
|
short_name,
|
|
enabled,
|
|
recording_enabled,
|
|
recording_storage_id
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
ON CONFLICT (milestone_hardware_id, milestone_device_id, device_type)
|
|
DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
display_name = EXCLUDED.display_name,
|
|
short_name = EXCLUDED.short_name,
|
|
enabled = EXCLUDED.enabled,
|
|
recording_enabled = EXCLUDED.recording_enabled,
|
|
recording_storage_id = EXCLUDED.recording_storage_id,
|
|
updated_at = now()
|
|
`,
|
|
hardwareID,
|
|
milestoneDeviceID,
|
|
deviceType,
|
|
name,
|
|
displayName,
|
|
shortName,
|
|
childDevice.Enabled,
|
|
childDevice.RecordingEnabled,
|
|
strings.TrimSpace(childDevice.RecordingStorageID),
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
func createOrUpdateMilestoneHardwareDevice(
|
|
ctx context.Context,
|
|
tx pgx.Tx,
|
|
hardwareDevice milestoneHardwareDevice,
|
|
) (bool, error) {
|
|
inventoryNumber, err := buildMilestoneInventoryNumber(ctx, tx, hardwareDevice)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
commandTag, err := tx.Exec(
|
|
ctx,
|
|
`
|
|
INSERT INTO devices (
|
|
inventory_number,
|
|
manufacturer,
|
|
model,
|
|
serial_number,
|
|
mac_address,
|
|
ip_address,
|
|
milestone_port,
|
|
location,
|
|
loan_status,
|
|
is_bao_device,
|
|
comment,
|
|
firmware_version,
|
|
milestone_recording_server_id,
|
|
milestone_hardware_id,
|
|
milestone_display_name,
|
|
milestone_enabled,
|
|
milestone_synced_at
|
|
)
|
|
VALUES (
|
|
$1,
|
|
$2,
|
|
$3,
|
|
$4,
|
|
$5,
|
|
$6,
|
|
$7,
|
|
'',
|
|
'Verfügbar',
|
|
false,
|
|
'',
|
|
$8,
|
|
$9,
|
|
$10,
|
|
$11,
|
|
$12,
|
|
now()
|
|
)
|
|
ON CONFLICT (milestone_hardware_id)
|
|
WHERE milestone_hardware_id <> ''
|
|
DO UPDATE SET
|
|
inventory_number = EXCLUDED.inventory_number,
|
|
manufacturer = EXCLUDED.manufacturer,
|
|
model = EXCLUDED.model,
|
|
serial_number = EXCLUDED.serial_number,
|
|
mac_address = EXCLUDED.mac_address,
|
|
ip_address = EXCLUDED.ip_address,
|
|
milestone_port = EXCLUDED.milestone_port,
|
|
firmware_version = EXCLUDED.firmware_version,
|
|
milestone_recording_server_id = EXCLUDED.milestone_recording_server_id,
|
|
milestone_display_name = EXCLUDED.milestone_display_name,
|
|
milestone_enabled = EXCLUDED.milestone_enabled,
|
|
milestone_synced_at = now(),
|
|
updated_at = now()
|
|
`,
|
|
inventoryNumber,
|
|
hardwareDevice.Manufacturer,
|
|
hardwareDevice.DeviceModelName,
|
|
hardwareDevice.SerialNumber,
|
|
hardwareDevice.MacAddress,
|
|
hardwareDevice.IPAddress,
|
|
hardwareDevice.Port,
|
|
hardwareDevice.FirmwareVersion,
|
|
hardwareDevice.RecordingServerID,
|
|
hardwareDevice.HardwareID,
|
|
hardwareDevice.DisplayName,
|
|
hardwareDevice.Enabled,
|
|
)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return commandTag.RowsAffected() > 0, nil
|
|
}
|