teg/backend/milestone_storage.go
2026-06-09 14:26:35 +02:00

1120 lines
31 KiB
Go

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