// backend\milestone_storage.go package main import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "strings" ) type milestoneArchiveStorage struct { ID string `json:"id"` DisplayName string `json:"displayName"` Name string `json:"name"` Description string `json:"description,omitempty"` DiskPath string `json:"diskPath"` MaxSize int `json:"maxSize,omitempty"` RetainMinutes int `json:"retainMinutes,omitempty"` Mounted bool `json:"mounted"` } type milestoneArchiveStoragesResponse struct { Array []milestoneArchiveStorage `json:"array"` } type milestoneStorage struct { ID string `json:"id"` DisplayName string `json:"displayName"` Name string `json:"name"` Description string `json:"description,omitempty"` DiskPath string `json:"diskPath"` MaxSize int `json:"maxSize,omitempty"` RetainMinutes int `json:"retainMinutes,omitempty"` Mounted bool `json:"mounted"` } type milestoneStorageResponse struct { Data milestoneStorage `json:"data"` } type milestoneStorageDetails struct { ID string `json:"id"` DisplayName string `json:"displayName"` Name string `json:"name"` Description string `json:"description,omitempty"` DiskPath string `json:"diskPath"` MaxSize int `json:"maxSize,omitempty"` RetainMinutes int `json:"retainMinutes,omitempty"` Mounted bool `json:"mounted"` ArchiveStorages []milestoneArchiveStorage `json:"archiveStorages"` } type milestoneStorageDetailsResponse struct { Data milestoneStorageDetails `json:"data"` } type milestoneStoragesResponse struct { Array []milestoneStorage `json:"array"` } type milestoneStorageWithArchives struct { ID string `json:"id"` DisplayName string `json:"displayName"` Name string `json:"name"` Description string `json:"description,omitempty"` DiskPath string `json:"diskPath"` MaxSize int `json:"maxSize,omitempty"` RetainMinutes int `json:"retainMinutes,omitempty"` Mounted bool `json:"mounted"` StorageInformation *milestoneStorageInformation `json:"storageInformation,omitempty"` ArchiveStorages []milestoneArchiveStorage `json:"archiveStorages"` } type createMilestoneArchiveStorageRequest struct { DisplayName string `json:"displayName"` Name string `json:"name"` Description string `json:"description"` DiskPath string `json:"diskPath"` MaxSize int `json:"maxSize"` RetainMinutes int `json:"retainMinutes"` } type updateMilestoneArchiveStorageRequest struct { DisplayName *string `json:"displayName"` Name *string `json:"name"` Description *string `json:"description"` DiskPath *string `json:"diskPath"` MaxSize *int `json:"maxSize"` RetainMinutes *int `json:"retainMinutes"` } type createMilestoneAutoArchiveStorageRequest struct { ArchiveFolderName string `json:"archiveFolderName"` DisplayName string `json:"displayName"` Name string `json:"name"` Description string `json:"description"` MaxSize int `json:"maxSize"` RetainMinutes int `json:"retainMinutes"` } type createMilestoneStorageWithArchiveRequest struct { DisplayName string `json:"displayName"` Name string `json:"name"` Description string `json:"description"` DiskPath string `json:"diskPath"` MaxSize int `json:"maxSize"` RetainMinutes int `json:"retainMinutes"` Signing bool `json:"signing"` ArchiveDisplayName string `json:"archiveDisplayName"` ArchiveName string `json:"archiveName"` ArchiveDescription string `json:"archiveDescription"` ArchiveDiskPath string `json:"archiveDiskPath"` ArchiveFolderName string `json:"archiveFolderName"` ArchiveMaxSize int `json:"archiveMaxSize"` ArchiveRetainMinutes int `json:"archiveRetainMinutes"` } type milestoneStorageInformationRelationRef struct { Type string `json:"type"` ID string `json:"id"` } type milestoneStorageInformationRelations struct { Self milestoneStorageInformationRelationRef `json:"self"` Parent milestoneStorageInformationRelationRef `json:"parent"` } type milestoneStorageInformation struct { AnyTablesWithData string `json:"anyTablesWithData"` DisplayName string `json:"displayName"` IsAvailable bool `json:"isAvailable"` IsMounted bool `json:"isMounted"` LastUpdated string `json:"lastUpdated"` LockedUsedSpace int64 `json:"lockedUsedSpace"` UsedSpace int64 `json:"usedSpace"` Relations milestoneStorageInformationRelations `json:"relations"` } type milestoneStorageInformationTask struct { Name string `json:"name"` DisplayName string `json:"displayName"` } type milestoneStorageInformationResource struct { Type string `json:"type"` DisplayName string `json:"displayName"` } type milestoneStorageInformationResponse struct { Data milestoneStorageInformation `json:"data"` Tasks []milestoneStorageInformationTask `json:"tasks"` Resources []milestoneStorageInformationResource `json:"resources"` } func getMilestoneStorageInformation( ctx context.Context, config milestoneRuntimeConfig, storageInformationID string, ) (*milestoneStorageInformationResponse, error) { storageInformationID = strings.TrimSpace(storageInformationID) if storageInformationID == "" { return nil, nil } requestURL := strings.TrimRight(config.Host, "/") + "/API/rest/v1/storageInformation/" + url.PathEscape(storageInformationID) request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) if err != nil { return nil, err } setMilestoneRequestHeaders(request, config, false) response, err := milestoneHTTPClient(config.SkipTLSVerify).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 storage information get failed: status=%d body=%s", response.StatusCode, string(body), ) } var storageInformation milestoneStorageInformationResponse if err := json.Unmarshal(body, &storageInformation); err != nil { return nil, err } return &storageInformation, nil } func (s *Server) handleGetMilestoneStorageInformation(w http.ResponseWriter, r *http.Request) { storageInformationID := strings.TrimSpace(r.PathValue("id")) if storageInformationID == "" { writeError(w, http.StatusBadRequest, "Storage-Information-ID fehlt") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } storageInformation, err := getMilestoneStorageInformation( r.Context(), config, storageInformationID, ) if err != nil { log.Printf("Milestone storage information load failed: %v", err) writeError(w, http.StatusBadGateway, "Milestone-Storage-Information konnte nicht geladen werden") return } if storageInformation == nil { writeError(w, http.StatusNotFound, "Milestone-Storage-Information wurde nicht gefunden") return } writeJSON(w, http.StatusOK, storageInformation) } func getMilestoneArchiveStorages( ctx context.Context, config milestoneRuntimeConfig, storageID string, ) ([]milestoneArchiveStorage, error) { storageID = strings.TrimSpace(storageID) if storageID == "" { return []milestoneArchiveStorage{}, nil } requestURL := strings.TrimRight(config.Host, "/") + "/API/rest/v1/storages/" + url.PathEscape(storageID) + "/archiveStorages" request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) if err != nil { return nil, err } setMilestoneRequestHeaders(request, config, false) response, err := milestoneHTTPClient(config.SkipTLSVerify).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 archive storages get failed: status=%d body=%s", response.StatusCode, string(body), ) } archives, err := decodeMilestoneArchiveStorages(body) if err != nil { return nil, err } if archives == nil { return []milestoneArchiveStorage{}, nil } return archives, nil } func decodeMilestoneArchiveStorages(body []byte) ([]milestoneArchiveStorage, error) { var arrayResponse milestoneArchiveStoragesResponse if err := json.Unmarshal(body, &arrayResponse); err == nil && arrayResponse.Array != nil { return arrayResponse.Array, nil } var directArray []milestoneArchiveStorage if err := json.Unmarshal(body, &directArray); err == nil && directArray != nil { return directArray, nil } var detailsResponse milestoneStorageDetailsResponse if err := json.Unmarshal(body, &detailsResponse); err == nil && detailsResponse.Data.ArchiveStorages != nil { return detailsResponse.Data.ArchiveStorages, nil } var generic map[string]json.RawMessage if err := json.Unmarshal(body, &generic); err != nil { return nil, err } if rawArray, ok := generic["archiveStorages"]; ok { var archives []milestoneArchiveStorage if err := json.Unmarshal(rawArray, &archives); err != nil { return nil, err } return archives, nil } if rawData, ok := generic["data"]; ok { var data map[string]json.RawMessage if err := json.Unmarshal(rawData, &data); err != nil { return nil, err } if rawArray, ok := data["archiveStorages"]; ok { var archives []milestoneArchiveStorage if err := json.Unmarshal(rawArray, &archives); err != nil { return nil, err } return archives, nil } } return []milestoneArchiveStorage{}, nil } func getMilestoneStorages( ctx context.Context, config milestoneRuntimeConfig, ) ([]milestoneStorage, error) { recordingServerID := strings.TrimSpace(config.ServerID) if recordingServerID == "" { return nil, fmt.Errorf("milestone recording server id is missing") } requestURL := strings.TrimRight(config.Host, "/") + "/API/rest/v1/recordingServers/" + url.PathEscape(recordingServerID) + "/storages" request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) if err != nil { return nil, err } setMilestoneRequestHeaders(request, config, false) response, err := milestoneHTTPClient(config.SkipTLSVerify).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 storages get failed: status=%d body=%s", response.StatusCode, string(body), ) } var storagesResponse milestoneStoragesResponse if err := json.Unmarshal(body, &storagesResponse); err == nil && storagesResponse.Array != nil { return storagesResponse.Array, nil } var directArray []milestoneStorage if err := json.Unmarshal(body, &directArray); err == nil && directArray != nil { return directArray, nil } return []milestoneStorage{}, nil } func getMilestoneStoragesWithArchives( ctx context.Context, config milestoneRuntimeConfig, ) ([]milestoneStorageWithArchives, error) { storages, err := getMilestoneStorages(ctx, config) if err != nil { return nil, err } result := make([]milestoneStorageWithArchives, 0, len(storages)) for _, storage := range storages { archives, err := getMilestoneArchiveStorages(ctx, config, storage.ID) if err != nil { return nil, err } if archives == nil { archives = []milestoneArchiveStorage{} } var storageInformation *milestoneStorageInformation storageInformationResponse, err := getMilestoneStorageInformation( ctx, config, storage.ID, ) if err != nil { log.Printf( "Milestone storage information could not be loaded: storageId=%s error=%v", storage.ID, err, ) } else if storageInformationResponse != nil { storageInformation = &storageInformationResponse.Data } result = append(result, milestoneStorageWithArchives{ ID: storage.ID, DisplayName: storage.DisplayName, Name: storage.Name, Description: storage.Description, DiskPath: storage.DiskPath, MaxSize: storage.MaxSize, RetainMinutes: storage.RetainMinutes, Mounted: storage.Mounted, StorageInformation: storageInformation, ArchiveStorages: archives, }) } return result, nil } func (s *Server) handleListMilestoneStorages(w http.ResponseWriter, r *http.Request) { config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { log.Printf("Milestone storages load failed: runtime config error: %v", err) writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } storages, err := getMilestoneStoragesWithArchives(r.Context(), config) if err != nil { log.Printf("Milestone storages load failed: %v", err) writeError(w, http.StatusBadGateway, "Milestone-Speicher konnten nicht geladen werden") return } writeJSON(w, http.StatusOK, map[string]any{ "storages": storages, }) } func (s *Server) handleCreateMilestoneStorageWithArchive(w http.ResponseWriter, r *http.Request) { var input createMilestoneStorageWithArchiveRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } displayName := strings.TrimSpace(input.DisplayName) name := strings.TrimSpace(input.Name) diskPath := strings.TrimSpace(input.DiskPath) if displayName == "" { writeError(w, http.StatusBadRequest, "Anzeigename fehlt") return } if name == "" { writeError(w, http.StatusBadRequest, "Name fehlt") return } if diskPath == "" { writeError(w, http.StatusBadRequest, "Speicherpfad fehlt") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } storagePayload := map[string]any{ "displayName": displayName, "name": name, "description": strings.TrimSpace(input.Description), "diskPath": diskPath, "maxSize": input.MaxSize, "signing": input.Signing, } if input.RetainMinutes > 0 { storagePayload["retainMinutes"] = input.RetainMinutes } storage, err := postMilestoneStorage(r.Context(), config, storagePayload) if err != nil { log.Printf("Milestone storage create failed: error=%v", err) writeError(w, http.StatusBadGateway, "Milestone-Speicher konnte nicht angelegt werden") return } if storage == nil || strings.TrimSpace(storage.ID) == "" { storage, err = findMilestoneStorageAfterCreate(r.Context(), config, input) if err != nil { log.Printf("Milestone storage lookup after create failed: error=%v", err) writeError(w, http.StatusBadGateway, "Milestone-Speicher wurde angelegt, konnte aber nicht wiedergefunden werden") return } } if storage == nil || strings.TrimSpace(storage.ID) == "" { writeError(w, http.StatusBadGateway, "Milestone hat keine Storage-ID zurückgegeben") return } archiveFolderName := strings.TrimSpace(input.ArchiveFolderName) if archiveFolderName == "" { archiveFolderName = "Archive" } archiveDiskPath := strings.TrimSpace(input.ArchiveDiskPath) if archiveDiskPath == "" { archiveDiskPath = joinMilestoneDiskPath(storage.DiskPath, archiveFolderName) } if archiveDiskPath == "" { writeError(w, http.StatusBadRequest, "Archivpfad fehlt") return } archiveDisplayName := strings.TrimSpace(input.ArchiveDisplayName) if archiveDisplayName == "" { archiveDisplayName = formatArchiveDisplayName(storage.DisplayName, storage.Name, storage.ID) } archiveName := strings.TrimSpace(input.ArchiveName) if archiveName == "" { archiveName = formatArchiveName(storage.Name, storage.ID) } archivePayload := map[string]any{ "displayName": archiveDisplayName, "name": archiveName, "description": strings.TrimSpace(input.ArchiveDescription), "diskPath": archiveDiskPath, "maxSize": input.ArchiveMaxSize, } if input.ArchiveRetainMinutes > 0 { archivePayload["retainMinutes"] = input.ArchiveRetainMinutes } archiveStorage, err := postMilestoneArchiveStorage(r.Context(), config, storage.ID, archivePayload) if err != nil { log.Printf("Milestone archive storage create after storage create failed: storageId=%s error=%v", storage.ID, err) writeError(w, http.StatusBadGateway, "Milestone-Speicher wurde angelegt, aber Archivspeicher konnte nicht angelegt werden") return } writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "storage": storage, "archiveStorage": archiveStorage, }) } func (s *Server) handleCreateMilestoneArchiveStorage(w http.ResponseWriter, r *http.Request) { storageID := strings.TrimSpace(r.PathValue("id")) if storageID == "" { writeError(w, http.StatusBadRequest, "Storage-ID fehlt") return } var input createMilestoneArchiveStorageRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } payload := map[string]any{ "displayName": strings.TrimSpace(input.DisplayName), "name": strings.TrimSpace(input.Name), "description": strings.TrimSpace(input.Description), "diskPath": strings.TrimSpace(input.DiskPath), "maxSize": input.MaxSize, } if input.RetainMinutes > 0 { payload["retainMinutes"] = input.RetainMinutes } archiveStorage, err := postMilestoneArchiveStorage(r.Context(), config, storageID, payload) if err != nil { log.Printf("Milestone archive storage create failed: storageId=%s error=%v", storageID, err) writeError(w, http.StatusBadGateway, "Archive-Storage konnte nicht angelegt werden") return } writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "archiveStorage": archiveStorage, }) } func (s *Server) handleCreateMilestoneAutoArchiveStorage(w http.ResponseWriter, r *http.Request) { storageID := strings.TrimSpace(r.PathValue("id")) if storageID == "" { writeError(w, http.StatusBadRequest, "Storage-ID fehlt") return } var input createMilestoneAutoArchiveStorageRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } storage, err := getMilestoneStorage(r.Context(), config, storageID) if err != nil { log.Printf("Milestone storage load failed: storageId=%s error=%v", storageID, err) writeError(w, http.StatusBadGateway, "Milestone-Speicher konnte nicht geladen werden") return } if storage == nil { writeError(w, http.StatusNotFound, "Milestone-Speicher wurde nicht gefunden") return } archiveFolderName := strings.TrimSpace(input.ArchiveFolderName) if archiveFolderName == "" { archiveFolderName = "Archive" } archiveDiskPath := joinMilestoneDiskPath(storage.DiskPath, archiveFolderName) if archiveDiskPath == "" { writeError(w, http.StatusBadRequest, "Archivpfad konnte nicht aus dem Speicherpfad gebildet werden") return } displayName := strings.TrimSpace(input.DisplayName) if displayName == "" { displayName = formatArchiveDisplayName(storage.DisplayName, storage.Name, storage.ID) } name := strings.TrimSpace(input.Name) if name == "" { name = formatArchiveName(storage.Name, storage.ID) } payload := map[string]any{ "displayName": displayName, "name": name, "description": strings.TrimSpace(input.Description), "diskPath": archiveDiskPath, "maxSize": input.MaxSize, } if input.RetainMinutes > 0 { payload["retainMinutes"] = input.RetainMinutes } archiveStorage, err := postMilestoneArchiveStorage(r.Context(), config, storageID, payload) if err != nil { log.Printf("Milestone auto archive storage create failed: storageId=%s error=%v", storageID, err) writeError(w, http.StatusBadGateway, "Automatischer Archivspeicher konnte nicht angelegt werden") return } writeJSON(w, http.StatusOK, map[string]any{ "ok": true, "diskPath": archiveDiskPath, "archiveStorage": archiveStorage, }) } func joinMilestoneDiskPath(basePath string, childPath string) string { basePath = strings.TrimSpace(basePath) childPath = strings.TrimSpace(childPath) basePath = strings.TrimRight(basePath, `\/`) childPath = strings.TrimLeft(childPath, `\/`) if basePath == "" { return childPath } if childPath == "" { return basePath } separator := `\` if strings.Contains(basePath, "/") && !strings.Contains(basePath, `\`) { separator = "/" } return basePath + separator + childPath } func formatArchiveDisplayName(displayName string, name string, id string) string { base := strings.TrimSpace(displayName) if base == "" { base = strings.TrimSpace(name) } if base == "" { base = strings.TrimSpace(id) } if base == "" { base = "Storage" } return base + " Archiv" } func formatArchiveName(name string, id string) string { base := strings.TrimSpace(name) if base == "" { base = strings.TrimSpace(id) } if base == "" { base = "storage" } base = strings.ReplaceAll(base, " ", "_") base = strings.ReplaceAll(base, `\`, "_") base = strings.ReplaceAll(base, "/", "_") return base + "_archive" } func postMilestoneStorage( ctx context.Context, config milestoneRuntimeConfig, payload map[string]any, ) (*milestoneStorage, error) { recordingServerID := strings.TrimSpace(config.ServerID) if recordingServerID == "" { return nil, fmt.Errorf("milestone recording server id is missing") } body, err := json.Marshal(payload) if err != nil { return nil, err } requestURL := strings.TrimRight(config.Host, "/") + "/API/rest/v1/recordingServers/" + url.PathEscape(recordingServerID) + "/storages" request, err := http.NewRequestWithContext( ctx, http.MethodPost, requestURL, strings.NewReader(string(body)), ) if err != nil { return nil, err } setMilestoneRequestHeaders(request, config, true) response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request) if err != nil { return nil, err } defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) if err != nil { return nil, err } if response.StatusCode < 200 || response.StatusCode >= 300 { return nil, fmt.Errorf( "milestone storage post failed: status=%d body=%s", response.StatusCode, string(responseBody), ) } storage, _ := decodeMilestoneStorage(responseBody) return storage, nil } func findMilestoneStorageAfterCreate( ctx context.Context, config milestoneRuntimeConfig, input createMilestoneStorageWithArchiveRequest, ) (*milestoneStorage, error) { storages, err := getMilestoneStorages(ctx, config) if err != nil { return nil, err } name := strings.TrimSpace(input.Name) displayName := strings.TrimSpace(input.DisplayName) diskPath := strings.TrimSpace(input.DiskPath) for _, storage := range storages { if name != "" && strings.EqualFold(strings.TrimSpace(storage.Name), name) { matched := storage return &matched, nil } } for _, storage := range storages { if diskPath != "" && strings.EqualFold(strings.TrimSpace(storage.DiskPath), diskPath) { matched := storage return &matched, nil } } for _, storage := range storages { if displayName != "" && strings.EqualFold(strings.TrimSpace(storage.DisplayName), displayName) { matched := storage return &matched, nil } } return nil, nil } func postMilestoneArchiveStorage( ctx context.Context, config milestoneRuntimeConfig, storageID string, payload map[string]any, ) (*milestoneArchiveStorage, error) { body, err := json.Marshal(payload) if err != nil { return nil, err } requestURL := strings.TrimRight(config.Host, "/") + "/API/rest/v1/storages/" + url.PathEscape(storageID) + "/archiveStorages" request, err := http.NewRequestWithContext( ctx, http.MethodPost, requestURL, strings.NewReader(string(body)), ) if err != nil { return nil, err } setMilestoneRequestHeaders(request, config, true) response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request) if err != nil { return nil, err } defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) if err != nil { return nil, err } if response.StatusCode < 200 || response.StatusCode >= 300 { return nil, fmt.Errorf( "milestone archive storage post failed: status=%d body=%s", response.StatusCode, string(responseBody), ) } archiveStorage, _ := decodeMilestoneArchiveStorage(responseBody) return archiveStorage, nil } func patchMilestoneArchiveStorage( ctx context.Context, config milestoneRuntimeConfig, archiveStorageID string, payload map[string]any, ) error { body, err := json.Marshal(payload) if err != nil { return err } requestURL := strings.TrimRight(config.Host, "/") + "/API/rest/v1/archiveStorages/" + url.PathEscape(archiveStorageID) request, err := http.NewRequestWithContext( ctx, http.MethodPatch, requestURL, strings.NewReader(string(body)), ) if err != nil { return err } setMilestoneRequestHeaders(request, config, true) response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request) if err != nil { return err } defer response.Body.Close() responseBody, err := io.ReadAll(response.Body) if err != nil { return err } if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf( "milestone archive storage patch failed: status=%d body=%s", response.StatusCode, string(responseBody), ) } return nil } func (s *Server) handleUpdateMilestoneArchiveStorage(w http.ResponseWriter, r *http.Request) { archiveStorageID := strings.TrimSpace(r.PathValue("id")) if archiveStorageID == "" { writeError(w, http.StatusBadRequest, "Archive-Storage-ID fehlt") return } var input updateMilestoneArchiveStorageRequest if err := readJSON(r, &input); err != nil { writeError(w, http.StatusBadRequest, "Invalid JSON") return } config, err := s.getMilestoneRuntimeConfig(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden") return } payload := map[string]any{} if input.DisplayName != nil { payload["displayName"] = strings.TrimSpace(*input.DisplayName) } if input.Name != nil { payload["name"] = strings.TrimSpace(*input.Name) } if input.Description != nil { payload["description"] = strings.TrimSpace(*input.Description) } if input.DiskPath != nil { payload["diskPath"] = strings.TrimSpace(*input.DiskPath) } if input.MaxSize != nil { payload["maxSize"] = *input.MaxSize } if input.RetainMinutes != nil { payload["retainMinutes"] = *input.RetainMinutes } if len(payload) == 0 { writeJSON(w, http.StatusOK, map[string]any{"ok": true}) return } if err := patchMilestoneArchiveStorage(r.Context(), config, archiveStorageID, payload); err != nil { log.Printf("Milestone archive storage update failed: archiveStorageId=%s error=%v", archiveStorageID, err) writeError(w, http.StatusBadGateway, "Archive-Storage konnte nicht aktualisiert werden") return } writeJSON(w, http.StatusOK, map[string]any{ "ok": true, }) } func getMilestoneStorage( ctx context.Context, config milestoneRuntimeConfig, storageID string, ) (*milestoneStorage, error) { storageID = strings.TrimSpace(storageID) if storageID == "" { return nil, nil } requestURL := strings.TrimRight(config.Host, "/") + "/API/rest/v1/storages/" + url.PathEscape(storageID) request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) if err != nil { return nil, err } setMilestoneRequestHeaders(request, config, false) response, err := milestoneHTTPClient(config.SkipTLSVerify).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 storage get failed: status=%d body=%s", response.StatusCode, string(body), ) } var storageResponse milestoneStorageResponse if err := json.Unmarshal(body, &storageResponse); err == nil && strings.TrimSpace(storageResponse.Data.ID) != "" { return &storageResponse.Data, nil } var direct milestoneStorage if err := json.Unmarshal(body, &direct); err != nil { return nil, err } if strings.TrimSpace(direct.ID) == "" { return nil, nil } return &direct, nil } func decodeMilestoneStorage(body []byte) (*milestoneStorage, error) { if len(strings.TrimSpace(string(body))) == 0 { return nil, nil } var envelope milestoneStorageResponse if err := json.Unmarshal(body, &envelope); err == nil && strings.TrimSpace(envelope.Data.ID) != "" { return &envelope.Data, nil } var direct milestoneStorage if err := json.Unmarshal(body, &direct); err == nil && strings.TrimSpace(direct.ID) != "" { return &direct, nil } var generic map[string]json.RawMessage if err := json.Unmarshal(body, &generic); err != nil { return nil, err } if rawData, ok := generic["data"]; ok { var storage milestoneStorage if err := json.Unmarshal(rawData, &storage); err == nil && strings.TrimSpace(storage.ID) != "" { return &storage, nil } } return nil, nil } func decodeMilestoneArchiveStorage(body []byte) (*milestoneArchiveStorage, error) { if len(strings.TrimSpace(string(body))) == 0 { return nil, nil } var envelope struct { Data milestoneArchiveStorage `json:"data"` } if err := json.Unmarshal(body, &envelope); err == nil && strings.TrimSpace(envelope.Data.ID) != "" { return &envelope.Data, nil } var direct milestoneArchiveStorage if err := json.Unmarshal(body, &direct); err == nil && strings.TrimSpace(direct.ID) != "" { return &direct, nil } return nil, nil } func setMilestoneRequestHeaders(request *http.Request, config milestoneRuntimeConfig, hasBody bool) { tokenType := strings.TrimSpace(config.TokenType) if tokenType == "" { tokenType = "Bearer" } request.Header.Set("Authorization", tokenType+" "+config.AccessToken) request.Header.Set("Accept", "application/json") if hasBody { request.Header.Set("Content-Type", "application/json") } }