3001 lines
79 KiB
Go
3001 lines
79 KiB
Go
// backend\milestone_storage.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultCreateStorageMaxSizeMB = 30 * 1024
|
|
defaultCreateStorageRetainMinutes = 60
|
|
defaultCreateArchiveMaxSizeMB = 3.6 * 1024 * 1024
|
|
defaultCreateArchiveRetainMinutes = 365 * 24 * 60
|
|
)
|
|
|
|
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"`
|
|
IsDefault bool `json:"isDefault"`
|
|
}
|
|
|
|
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"`
|
|
IsDefault bool `json:"isDefault"`
|
|
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 updateMilestoneStorageRequest struct {
|
|
DisplayName *string `json:"displayName"`
|
|
Name *string `json:"name"`
|
|
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 float64 `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",
|
|
response.StatusCode,
|
|
)
|
|
}
|
|
|
|
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",
|
|
response.StatusCode,
|
|
)
|
|
}
|
|
|
|
archives, err := decodeMilestoneArchiveStorages(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if archives == nil {
|
|
return []milestoneArchiveStorage{}, nil
|
|
}
|
|
|
|
return archives, nil
|
|
}
|
|
|
|
func getMilestoneArchiveStorage(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
archiveStorageID string,
|
|
) (*milestoneArchiveStorage, error) {
|
|
archiveStorageID = strings.TrimSpace(archiveStorageID)
|
|
if archiveStorageID == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/archiveStorages/" + url.PathEscape(archiveStorageID)
|
|
|
|
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 storage get failed: status=%d body=%s",
|
|
response.StatusCode,
|
|
string(body),
|
|
)
|
|
}
|
|
|
|
return decodeMilestoneArchiveStorage(body)
|
|
}
|
|
|
|
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",
|
|
response.StatusCode,
|
|
)
|
|
}
|
|
|
|
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 getMilestoneRecordingServerDefaultStorageID(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
) string {
|
|
recordingServerID := strings.TrimSpace(config.ServerID)
|
|
if recordingServerID == "" {
|
|
return ""
|
|
}
|
|
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/recordingServers/" + url.PathEscape(recordingServerID)
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
setMilestoneRequestHeaders(request, config, false)
|
|
|
|
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
body, err := io.ReadAll(response.Body)
|
|
if err != nil || response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return ""
|
|
}
|
|
|
|
var envelope struct {
|
|
Data map[string]any `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &envelope); err != nil || envelope.Data == nil {
|
|
return ""
|
|
}
|
|
|
|
// Check direct fields (object reference or plain string ID)
|
|
for _, key := range []string{"storage", "defaultStorage", "defaultRecordingStorage"} {
|
|
switch v := envelope.Data[key].(type) {
|
|
case map[string]any:
|
|
if id := strings.TrimSpace(milestoneStringValue(v["id"])); id != "" {
|
|
return id
|
|
}
|
|
case string:
|
|
if id := strings.TrimSpace(v); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check inside relations
|
|
if relations, ok := envelope.Data["relations"].(map[string]any); ok {
|
|
for _, key := range []string{"storage", "defaultStorage", "defaultRecordingStorage"} {
|
|
if ref, ok := relations[key].(map[string]any); ok {
|
|
if id := strings.TrimSpace(milestoneStringValue(ref["id"])); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Log the recording server response keys to help identify the correct field
|
|
keys := make([]string, 0, len(envelope.Data))
|
|
for k := range envelope.Data {
|
|
keys = append(keys, k)
|
|
}
|
|
log.Printf("Milestone recording server response keys (default storage not found): %v", keys)
|
|
|
|
return ""
|
|
}
|
|
|
|
func getMilestoneStoragesWithArchives(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
) ([]milestoneStorageWithArchives, error) {
|
|
storages, err := getMilestoneStorages(ctx, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Prefer isDefault flag returned directly in the storage list
|
|
defaultStorageID := ""
|
|
for _, s := range storages {
|
|
if s.IsDefault {
|
|
defaultStorageID = s.ID
|
|
break
|
|
}
|
|
}
|
|
// Fall back to recording server endpoint
|
|
if defaultStorageID == "" {
|
|
defaultStorageID = getMilestoneRecordingServerDefaultStorageID(ctx, config)
|
|
if defaultStorageID != "" {
|
|
log.Printf("Milestone default storage from recording server: id=%s", defaultStorageID)
|
|
} else {
|
|
log.Printf("Milestone default storage could not be determined (storages=%d)", len(storages))
|
|
}
|
|
}
|
|
|
|
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,
|
|
IsDefault: defaultStorageID != "" && storage.ID == defaultStorageID,
|
|
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 name == "" {
|
|
name = displayName
|
|
}
|
|
if displayName == "" {
|
|
displayName = name
|
|
}
|
|
|
|
diskPath = normalizeMilestoneCreateDiskPath(diskPath, name, displayName)
|
|
|
|
if name == "" {
|
|
writeError(w, http.StatusBadRequest, "Name fehlt")
|
|
return
|
|
}
|
|
if diskPath == "" {
|
|
writeError(w, http.StatusBadRequest, "Speicherpfad fehlt")
|
|
return
|
|
}
|
|
|
|
maxSize := input.MaxSize
|
|
if maxSize < 0 {
|
|
maxSize = defaultCreateStorageMaxSizeMB
|
|
}
|
|
|
|
retainMinutes := input.RetainMinutes
|
|
if retainMinutes <= 0 {
|
|
retainMinutes = defaultCreateStorageRetainMinutes
|
|
}
|
|
|
|
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": maxSize,
|
|
"retainMinutes": retainMinutes,
|
|
"signing": input.Signing,
|
|
}
|
|
|
|
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)
|
|
}
|
|
archiveDiskPath = normalizeMilestoneCreateDiskPath(
|
|
archiveDiskPath,
|
|
archiveFolderName,
|
|
name,
|
|
displayName,
|
|
)
|
|
if archiveDiskPath == "" {
|
|
writeError(w, http.StatusBadRequest, "Archivpfad fehlt")
|
|
return
|
|
}
|
|
|
|
archiveDisplayName := strings.TrimSpace(input.ArchiveDisplayName)
|
|
archiveName := strings.TrimSpace(input.ArchiveName)
|
|
|
|
if archiveName == "" {
|
|
archiveName = archiveDisplayName
|
|
}
|
|
if archiveDisplayName == "" {
|
|
archiveDisplayName = archiveName
|
|
}
|
|
if archiveName == "" {
|
|
archiveName = formatArchiveName(storage.Name, storage.ID)
|
|
}
|
|
if archiveDisplayName == "" {
|
|
archiveDisplayName = formatArchiveDisplayName(storage.DisplayName, storage.Name, storage.ID)
|
|
}
|
|
|
|
archiveMaxSize := input.ArchiveMaxSize
|
|
if archiveMaxSize < 0 {
|
|
archiveMaxSize = defaultCreateArchiveMaxSizeMB
|
|
}
|
|
|
|
archiveRetainMinutes := input.ArchiveRetainMinutes
|
|
if archiveRetainMinutes <= 0 {
|
|
archiveRetainMinutes = defaultCreateArchiveRetainMinutes
|
|
}
|
|
|
|
archivePayload := map[string]any{
|
|
"displayName": archiveDisplayName,
|
|
"name": archiveName,
|
|
"description": strings.TrimSpace(input.ArchiveDescription),
|
|
"diskPath": archiveDiskPath,
|
|
"maxSize": archiveMaxSize,
|
|
"retainMinutes": 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
|
|
}
|
|
|
|
currentArchiveStorage, err := getMilestoneArchiveStorage(r.Context(), config, archiveStorageID)
|
|
if err != nil {
|
|
log.Printf("Milestone archive storage load before update failed: archiveStorageId=%s error=%v", archiveStorageID, err)
|
|
writeError(w, http.StatusBadGateway, "Archive-Storage konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
payload := map[string]any{}
|
|
|
|
var folderRename milestoneFolderRenameResult
|
|
if input.Name != nil && input.DiskPath == nil && currentArchiveStorage != nil {
|
|
nextName := strings.TrimSpace(*input.Name)
|
|
|
|
if nextName != "" && !strings.EqualFold(strings.TrimSpace(currentArchiveStorage.Name), nextName) {
|
|
folderRename, err = s.renameMilestoneStorageFolderIfNeeded(
|
|
r.Context(),
|
|
"archive",
|
|
currentArchiveStorage.DiskPath,
|
|
currentArchiveStorage.Name,
|
|
currentArchiveStorage.DisplayName,
|
|
nextName,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Milestone archive folder rename failed: archiveStorageId=%s error=%v", archiveStorageID, err)
|
|
writeError(w, http.StatusBadGateway, "Archiv-Ordner konnte nicht umbenannt werden")
|
|
return
|
|
}
|
|
|
|
if folderRename.Renamed {
|
|
payload["diskPath"] = folderRename.NewPath
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
s.rollbackMilestoneFolderRename(r.Context(), "archive", folderRename)
|
|
|
|
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,
|
|
"renamedPath": folderRename.NewPath,
|
|
})
|
|
}
|
|
|
|
func patchMilestoneStorage(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
storageID string,
|
|
payload map[string]any,
|
|
) error {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/storages/" + url.PathEscape(storageID)
|
|
|
|
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 storage patch failed: status=%d body=%s",
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleUpdateMilestoneStorage(w http.ResponseWriter, r *http.Request) {
|
|
storageID := strings.TrimSpace(r.PathValue("id"))
|
|
if storageID == "" {
|
|
writeError(w, http.StatusBadRequest, "Storage-ID fehlt")
|
|
return
|
|
}
|
|
|
|
var input updateMilestoneStorageRequest
|
|
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
|
|
}
|
|
|
|
currentStorage, err := getMilestoneStorage(r.Context(), config, storageID)
|
|
if err != nil {
|
|
log.Printf("Milestone storage load before update failed: storageId=%s error=%v", storageID, err)
|
|
writeError(w, http.StatusBadGateway, "Speicher konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
payload := map[string]any{}
|
|
|
|
var folderRename milestoneFolderRenameResult
|
|
if input.Name != nil && currentStorage != nil {
|
|
nextName := strings.TrimSpace(*input.Name)
|
|
|
|
if nextName != "" && !strings.EqualFold(strings.TrimSpace(currentStorage.Name), nextName) {
|
|
folderRename, err = s.renameMilestoneStorageFolderIfNeeded(
|
|
r.Context(),
|
|
"storage",
|
|
currentStorage.DiskPath,
|
|
currentStorage.Name,
|
|
currentStorage.DisplayName,
|
|
nextName,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Milestone storage folder rename failed: storageId=%s error=%v", storageID, err)
|
|
writeError(w, http.StatusBadGateway, "Speicher-Ordner konnte nicht umbenannt werden")
|
|
return
|
|
}
|
|
|
|
if folderRename.Renamed {
|
|
payload["diskPath"] = folderRename.NewPath
|
|
}
|
|
}
|
|
}
|
|
|
|
if input.DisplayName != nil {
|
|
payload["displayName"] = strings.TrimSpace(*input.DisplayName)
|
|
}
|
|
if input.Name != nil {
|
|
payload["name"] = strings.TrimSpace(*input.Name)
|
|
}
|
|
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 := patchMilestoneStorage(r.Context(), config, storageID, payload); err != nil {
|
|
s.rollbackMilestoneFolderRename(r.Context(), "storage", folderRename)
|
|
|
|
log.Printf("Milestone storage update failed: storageId=%s error=%v", storageID, err)
|
|
writeError(w, http.StatusBadGateway, "Speicher konnte nicht aktualisiert werden")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
"renamedPath": folderRename.NewPath,
|
|
})
|
|
}
|
|
|
|
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",
|
|
response.StatusCode,
|
|
)
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
type milestoneStorageDeviceBasicInfo struct {
|
|
ID string
|
|
ResourceType string
|
|
DisplayName string
|
|
Name string
|
|
RecordingStorageID string
|
|
}
|
|
|
|
func milestoneStringValue(value any) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
|
|
if stringValue, ok := value.(string); ok {
|
|
return stringValue
|
|
}
|
|
|
|
return fmt.Sprint(value)
|
|
}
|
|
|
|
func milestoneObjectID(value any) string {
|
|
if objectValue, ok := value.(map[string]any); ok {
|
|
return strings.TrimSpace(milestoneStringValue(objectValue["id"]))
|
|
}
|
|
|
|
return strings.TrimSpace(milestoneStringValue(value))
|
|
}
|
|
|
|
func milestoneDeviceID(item map[string]any) string {
|
|
if id := strings.TrimSpace(milestoneStringValue(item["id"])); id != "" {
|
|
return id
|
|
}
|
|
|
|
if relations, ok := item["relations"].(map[string]any); ok {
|
|
if self, ok := relations["self"].(map[string]any); ok {
|
|
if id := strings.TrimSpace(milestoneStringValue(self["id"])); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func milestoneDeviceRecordingStorageID(item map[string]any) string {
|
|
if id := milestoneObjectID(item["recordingStorage"]); id != "" {
|
|
return id
|
|
}
|
|
|
|
if relations, ok := item["relations"].(map[string]any); ok {
|
|
if id := milestoneObjectID(relations["recordingStorage"]); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func milestoneDeviceHardwareID(item map[string]any) string {
|
|
if relations, ok := item["relations"].(map[string]any); ok {
|
|
if parent, ok := relations["parent"].(map[string]any); ok {
|
|
parentType := strings.TrimSpace(milestoneStringValue(parent["type"]))
|
|
if parentType == "" || strings.EqualFold(parentType, "hardware") {
|
|
return strings.TrimSpace(milestoneStringValue(parent["id"]))
|
|
}
|
|
}
|
|
|
|
if id := milestoneObjectID(relations["hardware"]); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func milestoneDeviceDisplayName(item map[string]any) string {
|
|
if value := strings.TrimSpace(milestoneStringValue(item["displayName"])); value != "" {
|
|
return value
|
|
}
|
|
|
|
if value := strings.TrimSpace(milestoneStringValue(item["name"])); value != "" {
|
|
return value
|
|
}
|
|
|
|
return milestoneDeviceID(item)
|
|
}
|
|
|
|
func decodeMilestoneObjectItems(body []byte) ([]map[string]any, error) {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var rawItems []any
|
|
|
|
if array, ok := decoded["array"].([]any); ok {
|
|
rawItems = array
|
|
} else if dataArray, ok := decoded["data"].([]any); ok {
|
|
rawItems = dataArray
|
|
} else if data, ok := decoded["data"].(map[string]any); ok {
|
|
if array, ok := data["array"].([]any); ok {
|
|
rawItems = array
|
|
}
|
|
}
|
|
|
|
items := make([]map[string]any, 0, len(rawItems))
|
|
for _, rawItem := range rawItems {
|
|
if item, ok := rawItem.(map[string]any); ok {
|
|
items = append(items, item)
|
|
}
|
|
}
|
|
|
|
return items, nil
|
|
}
|
|
|
|
func decodeMilestoneObject(body []byte) (map[string]any, error) {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if data, ok := decoded["data"].(map[string]any); ok {
|
|
return data, nil
|
|
}
|
|
|
|
return decoded, nil
|
|
}
|
|
|
|
func getMilestoneDevicesByStorage(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
resourceType string,
|
|
storageID string,
|
|
) ([]milestoneStorageDeviceBasicInfo, error) {
|
|
storageID = strings.TrimSpace(storageID)
|
|
if storageID == "" {
|
|
return []milestoneStorageDeviceBasicInfo{}, nil
|
|
}
|
|
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/" + strings.Trim(resourceType, "/") + "?disabled"
|
|
|
|
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 %s get failed: status=%d body=%s",
|
|
resourceType,
|
|
response.StatusCode,
|
|
string(body),
|
|
)
|
|
}
|
|
|
|
items, err := decodeMilestoneObjectItems(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
devices := make([]milestoneStorageDeviceBasicInfo, 0)
|
|
for _, item := range items {
|
|
id := milestoneDeviceID(item)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
|
|
recordingStorageID := milestoneDeviceRecordingStorageID(item)
|
|
if !strings.EqualFold(recordingStorageID, storageID) {
|
|
continue
|
|
}
|
|
|
|
devices = append(devices, milestoneStorageDeviceBasicInfo{
|
|
ID: id,
|
|
ResourceType: resourceType,
|
|
DisplayName: milestoneDeviceDisplayName(item),
|
|
Name: strings.TrimSpace(milestoneStringValue(item["name"])),
|
|
RecordingStorageID: recordingStorageID,
|
|
})
|
|
}
|
|
|
|
return devices, nil
|
|
}
|
|
|
|
type milestoneStorageDeleteProgressReporter func(event string, payload map[string]any)
|
|
|
|
type milestoneTaskResult struct {
|
|
Progress string `json:"progress"`
|
|
ErrorCode string `json:"errorCode"`
|
|
ErrorText string `json:"errorText"`
|
|
State string `json:"state"`
|
|
Path string `json:"path"`
|
|
TaskType string `json:"taskType"`
|
|
}
|
|
|
|
func postMilestoneChangeDeviceRecordingStorageTask(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
resourceType string,
|
|
deviceID string,
|
|
targetStorage milestoneStorage,
|
|
report milestoneStorageDeleteProgressReporter,
|
|
progressStart int,
|
|
progressEnd int,
|
|
) error {
|
|
resourceType = strings.Trim(strings.TrimSpace(resourceType), "/")
|
|
deviceID = strings.TrimSpace(deviceID)
|
|
targetStorageID := strings.TrimSpace(targetStorage.ID)
|
|
|
|
if resourceType == "" {
|
|
return fmt.Errorf("resource type is missing")
|
|
}
|
|
if deviceID == "" {
|
|
return fmt.Errorf("device id is missing")
|
|
}
|
|
if targetStorageID == "" {
|
|
return fmt.Errorf("target storage id is missing")
|
|
}
|
|
|
|
endpoint := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/" +
|
|
resourceType +
|
|
"/" +
|
|
url.PathEscape(deviceID) +
|
|
"?task=ChangeDeviceRecordingStorage"
|
|
|
|
payload := map[string]any{
|
|
"itemSelection": map[string]any{
|
|
"type": "storages",
|
|
"id": targetStorageID,
|
|
},
|
|
"moveData": true,
|
|
}
|
|
|
|
responseBody, err := postMilestoneTask(ctx, config, endpoint, payload)
|
|
if err != nil {
|
|
log.Printf(
|
|
"Milestone ChangeDeviceRecordingStorage task failed: resourceType=%s deviceId=%s targetStorageId=%s targetStorageName=%s endpoint=%s payload=%s error=%v",
|
|
resourceType,
|
|
deviceID,
|
|
targetStorageID,
|
|
formatStorageTitleForLog(targetStorage),
|
|
endpoint,
|
|
milestonePayloadForLog(payload),
|
|
err,
|
|
)
|
|
|
|
return fmt.Errorf(
|
|
"milestone ChangeDeviceRecordingStorage task failed for %s id=%s: endpoint=%s payload=%s error=%w",
|
|
resourceType,
|
|
deviceID,
|
|
endpoint,
|
|
milestonePayloadForLog(payload),
|
|
err,
|
|
)
|
|
}
|
|
|
|
log.Printf(
|
|
"Milestone ChangeDeviceRecordingStorage task started: resourceType=%s deviceId=%s targetStorageId=%s targetStorageName=%s endpoint=%s payload=%s response=%s",
|
|
resourceType,
|
|
deviceID,
|
|
targetStorageID,
|
|
formatStorageTitleForLog(targetStorage),
|
|
endpoint,
|
|
milestonePayloadForLog(payload),
|
|
string(responseBody),
|
|
)
|
|
|
|
if result, ok := milestoneTaskResultFromBody(responseBody); ok {
|
|
reportMilestoneTaskResult(report, progressStart, progressEnd, result)
|
|
if milestoneTaskResultIsError(result) {
|
|
return fmt.Errorf(
|
|
"milestone ChangeDeviceRecordingStorage task returned error state=%s errorCode=%s errorText=%s",
|
|
result.State,
|
|
result.ErrorCode,
|
|
result.ErrorText,
|
|
)
|
|
}
|
|
if milestoneTaskResultIsComplete(result) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
taskID := milestoneTaskIDFromBody(responseBody)
|
|
if taskID == "" {
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": progressEnd,
|
|
"message": "Milestone hat keinen Task-ID zurückgegeben. Der ChangeDeviceRecordingStorage-Aufruf wurde angenommen.",
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": progressStart,
|
|
"message": "ChangeDeviceRecordingStorage-Task gestartet.",
|
|
"taskId": taskID,
|
|
})
|
|
}
|
|
|
|
return pollMilestoneTaskProgress(
|
|
ctx,
|
|
config,
|
|
taskID,
|
|
report,
|
|
progressStart,
|
|
progressEnd,
|
|
)
|
|
}
|
|
|
|
func postMilestoneTask(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
requestURL string,
|
|
payload map[string]any,
|
|
) ([]byte, error) {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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, _ := io.ReadAll(response.Body)
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return responseBody, fmt.Errorf(
|
|
"status=%d body=%s",
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return responseBody, nil
|
|
}
|
|
|
|
func milestonePayloadForLog(payload map[string]any) string {
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Sprintf("%v", payload)
|
|
}
|
|
|
|
return string(body)
|
|
}
|
|
|
|
func milestoneTaskIDFromBody(body []byte) string {
|
|
var decoded any
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
return ""
|
|
}
|
|
|
|
return firstMilestoneStringByKeys(decoded, []string{
|
|
"taskId",
|
|
"taskID",
|
|
"id",
|
|
})
|
|
}
|
|
|
|
func firstMilestoneStringByKeys(value any, keys []string) string {
|
|
object, ok := value.(map[string]any)
|
|
if !ok {
|
|
if values, ok := value.([]any); ok {
|
|
for _, item := range values {
|
|
if result := firstMilestoneStringByKeys(item, keys); result != "" {
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
for _, key := range keys {
|
|
if stringValue, ok := object[key].(string); ok && strings.TrimSpace(stringValue) != "" {
|
|
return strings.TrimSpace(stringValue)
|
|
}
|
|
}
|
|
|
|
if relations, ok := object["relations"].(map[string]any); ok {
|
|
if self, ok := relations["self"].(map[string]any); ok {
|
|
if id := strings.TrimSpace(milestoneStringValue(self["id"])); id != "" {
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, nested := range object {
|
|
if result := firstMilestoneStringByKeys(nested, keys); result != "" {
|
|
return result
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func milestoneTaskResultFromBody(body []byte) (milestoneTaskResult, bool) {
|
|
var decoded map[string]any
|
|
if err := json.Unmarshal(body, &decoded); err != nil {
|
|
return milestoneTaskResult{}, false
|
|
}
|
|
|
|
if result, ok := milestoneTaskResultFromValue(decoded["result"]); ok {
|
|
return result, true
|
|
}
|
|
|
|
if data, ok := decoded["data"].(map[string]any); ok {
|
|
if result, ok := milestoneTaskResultFromValue(data["result"]); ok {
|
|
return result, true
|
|
}
|
|
}
|
|
|
|
return milestoneTaskResult{}, false
|
|
}
|
|
|
|
func milestoneTaskResultFromValue(value any) (milestoneTaskResult, bool) {
|
|
object, ok := value.(map[string]any)
|
|
if !ok {
|
|
return milestoneTaskResult{}, false
|
|
}
|
|
|
|
result := milestoneTaskResult{
|
|
Progress: strings.TrimSpace(milestoneStringValue(object["progress"])),
|
|
ErrorCode: strings.TrimSpace(milestoneStringValue(object["errorCode"])),
|
|
ErrorText: strings.TrimSpace(milestoneStringValue(object["errorText"])),
|
|
State: strings.TrimSpace(milestoneStringValue(object["state"])),
|
|
Path: strings.TrimSpace(milestoneStringValue(object["path"])),
|
|
TaskType: strings.TrimSpace(milestoneStringValue(object["taskType"])),
|
|
}
|
|
|
|
return result, result.Progress != "" ||
|
|
result.ErrorCode != "" ||
|
|
result.ErrorText != "" ||
|
|
result.State != "" ||
|
|
result.Path != "" ||
|
|
result.TaskType != ""
|
|
}
|
|
|
|
func milestoneTaskResultProgress(result milestoneTaskResult, start int, end int) int {
|
|
if result.Progress == "" {
|
|
return end
|
|
}
|
|
|
|
progress, err := strconv.Atoi(result.Progress)
|
|
if err != nil {
|
|
return end
|
|
}
|
|
|
|
if progress < 0 {
|
|
progress = 0
|
|
}
|
|
if progress > 100 {
|
|
progress = 100
|
|
}
|
|
|
|
if end < start {
|
|
return progress
|
|
}
|
|
|
|
return start + ((end - start) * progress / 100)
|
|
}
|
|
|
|
func milestoneTaskResultIsComplete(result milestoneTaskResult) bool {
|
|
state := strings.TrimSpace(strings.ToLower(result.State))
|
|
|
|
return state == "success" ||
|
|
state == "completed" ||
|
|
state == "complete" ||
|
|
state == "finished"
|
|
}
|
|
|
|
func milestoneTaskResultIsError(result milestoneTaskResult) bool {
|
|
state := strings.TrimSpace(strings.ToLower(result.State))
|
|
|
|
return state == "error" ||
|
|
state == "failed" ||
|
|
state == "failure" ||
|
|
state == "faulted"
|
|
}
|
|
|
|
func reportMilestoneTaskResult(
|
|
report milestoneStorageDeleteProgressReporter,
|
|
start int,
|
|
end int,
|
|
result milestoneTaskResult,
|
|
) {
|
|
if report == nil {
|
|
return
|
|
}
|
|
|
|
message := "Milestone-Task läuft."
|
|
if result.State != "" {
|
|
message = "Milestone-Task Status: " + result.State
|
|
}
|
|
if result.ErrorText != "" {
|
|
message = result.ErrorText
|
|
}
|
|
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": milestoneTaskResultProgress(result, start, end),
|
|
"message": message,
|
|
"taskState": result.State,
|
|
"taskType": result.TaskType,
|
|
"taskPath": result.Path,
|
|
"errorCode": result.ErrorCode,
|
|
"errorText": result.ErrorText,
|
|
})
|
|
}
|
|
|
|
func getMilestoneTaskResult(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
taskID string,
|
|
) (milestoneTaskResult, bool, error) {
|
|
taskID = strings.TrimSpace(taskID)
|
|
if taskID == "" {
|
|
return milestoneTaskResult{}, false, fmt.Errorf("task id is missing")
|
|
}
|
|
|
|
requestURLs := []string{
|
|
strings.TrimRight(config.Host, "/") + "/API/rest/v1/tasks/" + url.PathEscape(taskID),
|
|
}
|
|
|
|
recordingServerID := strings.TrimSpace(config.ServerID)
|
|
if recordingServerID != "" {
|
|
requestURLs = append(
|
|
requestURLs,
|
|
strings.TrimRight(config.Host, "/")+
|
|
"/API/rest/v1/recordingServers/"+
|
|
url.PathEscape(recordingServerID)+
|
|
"/tasks/"+
|
|
url.PathEscape(taskID),
|
|
)
|
|
}
|
|
|
|
var lastErr error
|
|
|
|
for _, requestURL := range requestURLs {
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return milestoneTaskResult{}, false, err
|
|
}
|
|
|
|
setMilestoneRequestHeaders(request, config, false)
|
|
|
|
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
body, readErr := io.ReadAll(response.Body)
|
|
response.Body.Close()
|
|
|
|
if readErr != nil {
|
|
return milestoneTaskResult{}, false, readErr
|
|
}
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
lastErr = fmt.Errorf(
|
|
"milestone task get failed: url=%s status=%d body=%s",
|
|
requestURL,
|
|
response.StatusCode,
|
|
string(body),
|
|
)
|
|
continue
|
|
}
|
|
|
|
result, ok := milestoneTaskResultFromBody(body)
|
|
return result, ok, nil
|
|
}
|
|
|
|
if lastErr != nil {
|
|
return milestoneTaskResult{}, false, lastErr
|
|
}
|
|
|
|
return milestoneTaskResult{}, false, nil
|
|
}
|
|
|
|
func pollMilestoneTaskProgress(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
taskID string,
|
|
report milestoneStorageDeleteProgressReporter,
|
|
progressStart int,
|
|
progressEnd int,
|
|
) error {
|
|
deadline := time.NewTimer(5 * time.Minute)
|
|
defer deadline.Stop()
|
|
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
result, ok, err := getMilestoneTaskResult(ctx, config, taskID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ok {
|
|
reportMilestoneTaskResult(report, progressStart, progressEnd, result)
|
|
|
|
if milestoneTaskResultIsError(result) {
|
|
return fmt.Errorf(
|
|
"milestone task %s failed: state=%s errorCode=%s errorText=%s",
|
|
taskID,
|
|
result.State,
|
|
result.ErrorCode,
|
|
result.ErrorText,
|
|
)
|
|
}
|
|
|
|
if milestoneTaskResultIsComplete(result) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-deadline.C:
|
|
return fmt.Errorf("milestone task %s progress polling timed out", taskID)
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
}
|
|
|
|
func writeMilestoneStorageSSE(
|
|
w http.ResponseWriter,
|
|
flusher http.Flusher,
|
|
event string,
|
|
payload map[string]any,
|
|
) {
|
|
if payload == nil {
|
|
payload = map[string]any{}
|
|
}
|
|
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
body = []byte(`{"message":"SSE payload konnte nicht serialisiert werden"}`)
|
|
}
|
|
|
|
fmt.Fprintf(w, "event: %s\n", event)
|
|
fmt.Fprintf(w, "data: %s\n\n", body)
|
|
flusher.Flush()
|
|
}
|
|
|
|
type milestoneStorageDeviceMigrationResult struct {
|
|
Cameras int `json:"cameras"`
|
|
Microphones int `json:"microphones"`
|
|
}
|
|
|
|
func migrateMilestoneStorageDevicesToDefaultStorage(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
sourceStorageID string,
|
|
targetStorage milestoneStorage,
|
|
report milestoneStorageDeleteProgressReporter,
|
|
) (milestoneStorageDeviceMigrationResult, error) {
|
|
result := milestoneStorageDeviceMigrationResult{}
|
|
|
|
targetStorageID := strings.TrimSpace(targetStorage.ID)
|
|
if targetStorageID == "" {
|
|
return result, fmt.Errorf("default storage id is missing")
|
|
}
|
|
|
|
cameras, err := getMilestoneDevicesByStorage(ctx, config, "cameras", sourceStorageID)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
microphones, err := getMilestoneDevicesByStorage(ctx, config, "microphones", sourceStorageID)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
totalDevices := len(cameras) + len(microphones)
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": 5,
|
|
"message": fmt.Sprintf("%d Kamera(s) und %d Mikrofon(e) werden auf %s umgestellt.", len(cameras), len(microphones), formatStorageTitleForLog(targetStorage)),
|
|
"cameras": len(cameras),
|
|
"microphones": len(microphones),
|
|
"defaultStorageId": targetStorageID,
|
|
})
|
|
}
|
|
|
|
completedDevices := 0
|
|
|
|
nextProgressRange := func() (int, int) {
|
|
if totalDevices <= 0 {
|
|
return 80, 90
|
|
}
|
|
|
|
start := 10 + (completedDevices * 80 / totalDevices)
|
|
end := 10 + ((completedDevices + 1) * 80 / totalDevices)
|
|
|
|
return start, end
|
|
}
|
|
|
|
for _, camera := range cameras {
|
|
progressStart, progressEnd := nextProgressRange()
|
|
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": progressStart,
|
|
"message": fmt.Sprintf("Kamera %q wird auf den Standardspeicher umgestellt.", camera.DisplayName),
|
|
"deviceType": "camera",
|
|
"deviceId": camera.ID,
|
|
"deviceName": camera.DisplayName,
|
|
})
|
|
}
|
|
|
|
if err := postMilestoneChangeDeviceRecordingStorageTask(
|
|
ctx,
|
|
config,
|
|
"cameras",
|
|
camera.ID,
|
|
targetStorage,
|
|
report,
|
|
progressStart,
|
|
progressEnd,
|
|
); err != nil {
|
|
return result, fmt.Errorf(
|
|
"Kamera %q konnte nicht per ChangeDeviceRecordingStorage auf den Standardspeicher %q umgestellt werden: %w",
|
|
camera.DisplayName,
|
|
formatStorageTitleForLog(targetStorage),
|
|
err,
|
|
)
|
|
}
|
|
|
|
completedDevices++
|
|
result.Cameras++
|
|
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": progressEnd,
|
|
"message": fmt.Sprintf("Kamera %q wurde umgestellt.", camera.DisplayName),
|
|
"deviceType": "camera",
|
|
"deviceId": camera.ID,
|
|
"deviceName": camera.DisplayName,
|
|
})
|
|
}
|
|
}
|
|
|
|
for _, microphone := range microphones {
|
|
progressStart, progressEnd := nextProgressRange()
|
|
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": progressStart,
|
|
"message": fmt.Sprintf("Mikrofon %q wird auf den Standardspeicher umgestellt.", microphone.DisplayName),
|
|
"deviceType": "microphone",
|
|
"deviceId": microphone.ID,
|
|
"deviceName": microphone.DisplayName,
|
|
})
|
|
}
|
|
|
|
if err := postMilestoneChangeDeviceRecordingStorageTask(
|
|
ctx,
|
|
config,
|
|
"microphones",
|
|
microphone.ID,
|
|
targetStorage,
|
|
report,
|
|
progressStart,
|
|
progressEnd,
|
|
); err != nil {
|
|
return result, fmt.Errorf(
|
|
"Mikrofon %q konnte nicht per ChangeDeviceRecordingStorage auf den Standardspeicher %q umgestellt werden: %w",
|
|
microphone.DisplayName,
|
|
formatStorageTitleForLog(targetStorage),
|
|
err,
|
|
)
|
|
}
|
|
|
|
completedDevices++
|
|
result.Microphones++
|
|
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": progressEnd,
|
|
"message": fmt.Sprintf("Mikrofon %q wurde umgestellt.", microphone.DisplayName),
|
|
"deviceType": "microphone",
|
|
"deviceId": microphone.ID,
|
|
"deviceName": microphone.DisplayName,
|
|
})
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func formatStorageTitleForLog(storage milestoneStorage) string {
|
|
for _, value := range []string{storage.DisplayName, storage.Name, storage.ID} {
|
|
if strings.TrimSpace(value) != "" {
|
|
return strings.TrimSpace(value)
|
|
}
|
|
}
|
|
|
|
return "Standardspeicher"
|
|
}
|
|
|
|
func findMilestoneStorageByID(storages []milestoneStorage, storageID string) *milestoneStorage {
|
|
for index := range storages {
|
|
if strings.EqualFold(storages[index].ID, storageID) {
|
|
return &storages[index]
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func storageLooksLikeLocalDefault(storage milestoneStorage) bool {
|
|
for _, value := range []string{storage.DisplayName, storage.Name} {
|
|
if strings.EqualFold(strings.TrimSpace(value), "Local storage") {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func findMilestoneDefaultStorageForDelete(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
storages []milestoneStorage,
|
|
sourceStorageID string,
|
|
) (*milestoneStorage, error) {
|
|
for index := range storages {
|
|
if storages[index].IsDefault {
|
|
if strings.EqualFold(storages[index].ID, sourceStorageID) {
|
|
return nil, fmt.Errorf("Der Standardspeicher kann nicht gelöscht werden")
|
|
}
|
|
|
|
return &storages[index], nil
|
|
}
|
|
}
|
|
|
|
defaultStorageID := getMilestoneRecordingServerDefaultStorageID(ctx, config)
|
|
if defaultStorageID != "" {
|
|
if strings.EqualFold(defaultStorageID, sourceStorageID) {
|
|
return nil, fmt.Errorf("Der Standardspeicher kann nicht gelöscht werden")
|
|
}
|
|
|
|
if storage := findMilestoneStorageByID(storages, defaultStorageID); storage != nil {
|
|
return storage, nil
|
|
}
|
|
|
|
return &milestoneStorage{
|
|
ID: defaultStorageID,
|
|
DisplayName: "Standardspeicher",
|
|
Name: "Standardspeicher",
|
|
}, nil
|
|
}
|
|
|
|
for index := range storages {
|
|
if !strings.EqualFold(storages[index].ID, sourceStorageID) &&
|
|
storageLooksLikeLocalDefault(storages[index]) {
|
|
return &storages[index], nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("Standardspeicher \"Local storage\" wurde nicht gefunden")
|
|
}
|
|
|
|
type milestoneFolderRenameResult struct {
|
|
OldPath string
|
|
NewPath string
|
|
Renamed bool
|
|
}
|
|
|
|
func milestoneDiskPathBaseName(pathValue string) string {
|
|
pathValue = strings.TrimSpace(pathValue)
|
|
pathValue = strings.TrimRight(pathValue, `\/`)
|
|
|
|
if pathValue == "" {
|
|
return ""
|
|
}
|
|
|
|
separatorIndex := strings.LastIndexAny(pathValue, `\/`)
|
|
if separatorIndex < 0 {
|
|
return pathValue
|
|
}
|
|
|
|
return pathValue[separatorIndex+1:]
|
|
}
|
|
|
|
func milestoneDiskPathParent(pathValue string) string {
|
|
pathValue = strings.TrimSpace(pathValue)
|
|
pathValue = strings.TrimRight(pathValue, `\/`)
|
|
|
|
if pathValue == "" {
|
|
return ""
|
|
}
|
|
|
|
separatorIndex := strings.LastIndexAny(pathValue, `\/`)
|
|
if separatorIndex < 0 {
|
|
return ""
|
|
}
|
|
|
|
return pathValue[:separatorIndex]
|
|
}
|
|
|
|
func normalizeMilestoneCreateDiskPath(pathValue string, names ...string) string {
|
|
pathValue = strings.TrimSpace(pathValue)
|
|
if pathValue == "" {
|
|
return ""
|
|
}
|
|
|
|
if milestoneFolderNameMatches(pathValue, names...) {
|
|
parentPath := milestoneDiskPathParent(pathValue)
|
|
if parentPath != "" {
|
|
return parentPath
|
|
}
|
|
}
|
|
|
|
return pathValue
|
|
}
|
|
|
|
type milestoneFolderDeleteCandidate struct {
|
|
RootType string
|
|
Path string
|
|
Label string
|
|
}
|
|
|
|
func appendMilestoneFolderDeleteCandidate(
|
|
candidates []milestoneFolderDeleteCandidate,
|
|
rootType string,
|
|
pathValue string,
|
|
label string,
|
|
) []milestoneFolderDeleteCandidate {
|
|
pathValue = strings.TrimSpace(pathValue)
|
|
if pathValue == "" {
|
|
return candidates
|
|
}
|
|
|
|
normalizedPath := strings.ToLower(strings.TrimRight(pathValue, `\/`))
|
|
for _, candidate := range candidates {
|
|
if strings.ToLower(strings.TrimRight(candidate.Path, `\/`)) == normalizedPath {
|
|
return candidates
|
|
}
|
|
}
|
|
|
|
return append(candidates, milestoneFolderDeleteCandidate{
|
|
RootType: rootType,
|
|
Path: pathValue,
|
|
Label: label,
|
|
})
|
|
}
|
|
|
|
func milestoneStorageFolderDeleteCandidates(storage milestoneStorage) []milestoneFolderDeleteCandidate {
|
|
candidates := []milestoneFolderDeleteCandidate{}
|
|
diskPath := strings.TrimSpace(storage.DiskPath)
|
|
|
|
if diskPath == "" {
|
|
return candidates
|
|
}
|
|
|
|
storageID := strings.TrimSpace(storage.ID)
|
|
if storageID != "" && !strings.EqualFold(milestoneDiskPathBaseName(diskPath), storageID) {
|
|
candidates = appendMilestoneFolderDeleteCandidate(
|
|
candidates,
|
|
"storage",
|
|
joinMilestoneDiskPath(diskPath, storageID),
|
|
"Basis-Storage-Datenordner",
|
|
)
|
|
}
|
|
|
|
if strings.EqualFold(milestoneDiskPathBaseName(diskPath), storageID) ||
|
|
milestoneFolderNameMatches(diskPath, storage.Name, storage.DisplayName) {
|
|
candidates = appendMilestoneFolderDeleteCandidate(
|
|
candidates,
|
|
"storage",
|
|
diskPath,
|
|
"Basis-Ordner",
|
|
)
|
|
}
|
|
|
|
return candidates
|
|
}
|
|
|
|
func milestoneArchiveFolderDeleteCandidates(archive milestoneArchiveStorage) []milestoneFolderDeleteCandidate {
|
|
candidates := []milestoneFolderDeleteCandidate{}
|
|
diskPath := strings.TrimSpace(archive.DiskPath)
|
|
|
|
if diskPath == "" {
|
|
return candidates
|
|
}
|
|
|
|
archiveID := strings.TrimSpace(archive.ID)
|
|
if archiveID != "" && !strings.EqualFold(milestoneDiskPathBaseName(diskPath), archiveID) {
|
|
candidates = appendMilestoneFolderDeleteCandidate(
|
|
candidates,
|
|
"archive",
|
|
joinMilestoneDiskPath(diskPath, archiveID),
|
|
"Archiv-Storage-Datenordner",
|
|
)
|
|
}
|
|
|
|
if strings.EqualFold(milestoneDiskPathBaseName(diskPath), archiveID) ||
|
|
milestoneFolderNameMatches(diskPath, archive.Name, archive.DisplayName) {
|
|
candidates = appendMilestoneFolderDeleteCandidate(
|
|
candidates,
|
|
"archive",
|
|
diskPath,
|
|
"Archiv-Ordner",
|
|
)
|
|
}
|
|
|
|
return candidates
|
|
}
|
|
|
|
func milestoneFolderNameMatches(pathValue string, names ...string) bool {
|
|
baseName := milestoneDiskPathBaseName(pathValue)
|
|
if baseName == "" {
|
|
return false
|
|
}
|
|
|
|
for _, name := range names {
|
|
if strings.EqualFold(strings.TrimSpace(name), baseName) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func canUseMilestoneNameAsFolderName(value string) bool {
|
|
value = strings.TrimSpace(value)
|
|
|
|
if value == "" || value == "." || value == ".." {
|
|
return false
|
|
}
|
|
|
|
return !strings.ContainsAny(value, `<>:"/\|?*`)
|
|
}
|
|
|
|
func (s *Server) renameMilestoneStorageFolderIfNeeded(
|
|
ctx context.Context,
|
|
rootType string,
|
|
currentDiskPath string,
|
|
currentName string,
|
|
currentDisplayName string,
|
|
nextName string,
|
|
) (milestoneFolderRenameResult, error) {
|
|
result := milestoneFolderRenameResult{
|
|
OldPath: strings.TrimSpace(currentDiskPath),
|
|
}
|
|
|
|
nextName = strings.TrimSpace(nextName)
|
|
if result.OldPath == "" || nextName == "" {
|
|
return result, nil
|
|
}
|
|
|
|
if !canUseMilestoneNameAsFolderName(nextName) {
|
|
return result, fmt.Errorf("Neuer Ordnername %q ist kein gültiger Windows-Ordnername", nextName)
|
|
}
|
|
|
|
if strings.EqualFold(milestoneDiskPathBaseName(result.OldPath), nextName) {
|
|
result.NewPath = result.OldPath
|
|
return result, nil
|
|
}
|
|
|
|
if !milestoneFolderNameMatches(result.OldPath, currentName, currentDisplayName) {
|
|
return result, nil
|
|
}
|
|
|
|
newPath, err := s.renameServerFolderPath(ctx, rootType, result.OldPath, nextName)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
result.NewPath = newPath
|
|
result.Renamed = true
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s *Server) rollbackMilestoneFolderRename(
|
|
ctx context.Context,
|
|
rootType string,
|
|
renameResult milestoneFolderRenameResult,
|
|
) {
|
|
if !renameResult.Renamed ||
|
|
strings.TrimSpace(renameResult.OldPath) == "" ||
|
|
strings.TrimSpace(renameResult.NewPath) == "" {
|
|
return
|
|
}
|
|
|
|
oldName := milestoneDiskPathBaseName(renameResult.OldPath)
|
|
if oldName == "" {
|
|
return
|
|
}
|
|
|
|
if _, err := s.renameServerFolderPath(ctx, rootType, renameResult.NewPath, oldName); err != nil {
|
|
log.Printf(
|
|
"Milestone folder rename rollback failed: rootType=%s newPath=%s oldPath=%s error=%v",
|
|
rootType,
|
|
renameResult.NewPath,
|
|
renameResult.OldPath,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func (s *Server) deleteMilestoneStorageDiskFolders(
|
|
ctx context.Context,
|
|
storage milestoneStorage,
|
|
archiveStorages []milestoneArchiveStorage,
|
|
report milestoneStorageDeleteProgressReporter,
|
|
) []string {
|
|
errors := []string{}
|
|
|
|
candidates := []milestoneFolderDeleteCandidate{}
|
|
for _, archive := range archiveStorages {
|
|
candidates = append(candidates, milestoneArchiveFolderDeleteCandidates(archive)...)
|
|
}
|
|
candidates = append(candidates, milestoneStorageFolderDeleteCandidates(storage)...)
|
|
|
|
for _, candidate := range candidates {
|
|
if strings.TrimSpace(candidate.Path) == "" {
|
|
continue
|
|
}
|
|
|
|
message := fmt.Sprintf("%s %q wird gelöscht.", candidate.Label, candidate.Path)
|
|
if report != nil {
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": 97,
|
|
"message": message,
|
|
"folder": candidate.Path,
|
|
"rootType": candidate.RootType,
|
|
})
|
|
}
|
|
|
|
if err := s.deleteServerFolderPath(ctx, candidate.RootType, candidate.Path); err != nil {
|
|
log.Printf(
|
|
"Milestone folder delete failed: rootType=%s label=%s path=%s error=%v",
|
|
candidate.RootType,
|
|
candidate.Label,
|
|
candidate.Path,
|
|
err,
|
|
)
|
|
errors = append(errors, fmt.Sprintf("%s %s: %v", candidate.Label, candidate.Path, err))
|
|
}
|
|
}
|
|
|
|
return errors
|
|
}
|
|
|
|
func deleteMilestoneStorageFromAPI(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
storageID string,
|
|
) error {
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/storages/" + url.PathEscape(storageID)
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodDelete, requestURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
setMilestoneRequestHeaders(request, config, false)
|
|
|
|
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, _ := io.ReadAll(response.Body)
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return fmt.Errorf(
|
|
"milestone storage delete failed: status=%d body=%s",
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func deleteMilestoneArchiveStorageFromAPI(
|
|
ctx context.Context,
|
|
config milestoneRuntimeConfig,
|
|
archiveStorageID string,
|
|
) error {
|
|
requestURL := strings.TrimRight(config.Host, "/") +
|
|
"/API/rest/v1/archiveStorages/" + url.PathEscape(archiveStorageID)
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodDelete, requestURL, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
setMilestoneRequestHeaders(request, config, false)
|
|
|
|
response, err := milestoneHTTPClient(config.SkipTLSVerify).Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
responseBody, _ := io.ReadAll(response.Body)
|
|
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
|
return fmt.Errorf(
|
|
"milestone archive storage delete failed: status=%d body=%s",
|
|
response.StatusCode,
|
|
string(responseBody),
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) handleDeleteMilestoneStorage(w http.ResponseWriter, r *http.Request) {
|
|
storageID := strings.TrimSpace(r.PathValue("id"))
|
|
if storageID == "" {
|
|
writeError(w, http.StatusBadRequest, "Storage-ID fehlt")
|
|
return
|
|
}
|
|
|
|
config, err := s.getMilestoneRuntimeConfig(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
storages, err := getMilestoneStorages(r.Context(), config)
|
|
if err != nil {
|
|
log.Printf("Milestone storages load failed for delete: %v", err)
|
|
writeError(w, http.StatusBadGateway, "Milestone-Speicher konnten nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
storageToDelete := findMilestoneStorageByID(storages, storageID)
|
|
if storageToDelete == nil {
|
|
writeError(w, http.StatusNotFound, "Milestone-Speicher wurde nicht gefunden")
|
|
return
|
|
}
|
|
|
|
archiveStoragesToDelete, err := getMilestoneArchiveStorages(r.Context(), config, storageID)
|
|
if err != nil {
|
|
log.Printf("Milestone archive storages load before storage delete failed: storageId=%s error=%v", storageID, err)
|
|
writeError(w, http.StatusBadGateway, "Milestone-Archivspeicher konnten vor dem Löschen nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
defaultStorage, err := findMilestoneDefaultStorageForDelete(
|
|
r.Context(),
|
|
config,
|
|
storages,
|
|
storageID,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Milestone default storage lookup failed for delete: storageId=%s error=%v", storageID, err)
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
migration, err := migrateMilestoneStorageDevicesToDefaultStorage(
|
|
r.Context(),
|
|
config,
|
|
storageID,
|
|
*defaultStorage,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
log.Printf(
|
|
"Milestone storage device migration failed: storageId=%s defaultStorageId=%s error=%v",
|
|
storageID,
|
|
defaultStorage.ID,
|
|
err,
|
|
)
|
|
writeError(w, http.StatusBadGateway, "Kameras und Mikrofone konnten nicht auf den Standardspeicher umgestellt werden")
|
|
return
|
|
}
|
|
|
|
if err := deleteMilestoneStorageFromAPI(r.Context(), config, storageID); err != nil {
|
|
log.Printf("Milestone storage delete failed: storageId=%s error=%v", storageID, err)
|
|
writeError(w, http.StatusBadGateway, "Milestone-Speicher konnte nicht gelöscht werden")
|
|
return
|
|
}
|
|
|
|
folderDeleteErrors := s.deleteMilestoneStorageDiskFolders(
|
|
r.Context(),
|
|
*storageToDelete,
|
|
archiveStoragesToDelete,
|
|
nil,
|
|
)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
"deletedStorage": map[string]any{
|
|
"id": storageToDelete.ID,
|
|
"displayName": storageToDelete.DisplayName,
|
|
"name": storageToDelete.Name,
|
|
},
|
|
"defaultStorage": map[string]any{
|
|
"id": defaultStorage.ID,
|
|
"displayName": defaultStorage.DisplayName,
|
|
"name": defaultStorage.Name,
|
|
},
|
|
"migratedDevices": migration,
|
|
"folderDeleteErrors": folderDeleteErrors,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleDeleteMilestoneStorageStream(w http.ResponseWriter, r *http.Request) {
|
|
flusher, ok := w.(http.Flusher)
|
|
if !ok {
|
|
writeError(w, http.StatusInternalServerError, "SSE wird von diesem Server nicht unterstützt")
|
|
return
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Cache-Control", "no-cache")
|
|
w.Header().Set("Connection", "keep-alive")
|
|
w.Header().Set("X-Accel-Buffering", "no")
|
|
|
|
report := func(event string, payload map[string]any) {
|
|
writeMilestoneStorageSSE(w, flusher, event, payload)
|
|
}
|
|
|
|
fail := func(status int, message string, err error) {
|
|
payload := map[string]any{
|
|
"progress": 100,
|
|
"message": message,
|
|
"status": status,
|
|
}
|
|
if err != nil {
|
|
payload["error"] = err.Error()
|
|
}
|
|
report("storage-delete-error", payload)
|
|
}
|
|
|
|
storageID := strings.TrimSpace(r.PathValue("id"))
|
|
if storageID == "" {
|
|
fail(http.StatusBadRequest, "Storage-ID fehlt", nil)
|
|
return
|
|
}
|
|
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": 0,
|
|
"message": "Löschvorgang wird vorbereitet.",
|
|
})
|
|
|
|
config, err := s.getMilestoneRuntimeConfig(r.Context())
|
|
if err != nil {
|
|
fail(http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden", err)
|
|
return
|
|
}
|
|
|
|
storages, err := getMilestoneStorages(r.Context(), config)
|
|
if err != nil {
|
|
log.Printf("Milestone storages load failed for delete stream: %v", err)
|
|
fail(http.StatusBadGateway, "Milestone-Speicher konnten nicht geladen werden", err)
|
|
return
|
|
}
|
|
|
|
storageToDelete := findMilestoneStorageByID(storages, storageID)
|
|
if storageToDelete == nil {
|
|
fail(http.StatusNotFound, "Milestone-Speicher wurde nicht gefunden", nil)
|
|
return
|
|
}
|
|
|
|
archiveStoragesToDelete, err := getMilestoneArchiveStorages(r.Context(), config, storageID)
|
|
if err != nil {
|
|
log.Printf("Milestone archive storages load before stream storage delete failed: storageId=%s error=%v", storageID, err)
|
|
fail(http.StatusBadGateway, "Milestone-Archivspeicher konnten vor dem Löschen nicht geladen werden", err)
|
|
return
|
|
}
|
|
|
|
defaultStorage, err := findMilestoneDefaultStorageForDelete(
|
|
r.Context(),
|
|
config,
|
|
storages,
|
|
storageID,
|
|
)
|
|
if err != nil {
|
|
log.Printf("Milestone default storage lookup failed for delete stream: storageId=%s error=%v", storageID, err)
|
|
fail(http.StatusBadRequest, err.Error(), err)
|
|
return
|
|
}
|
|
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": 3,
|
|
"message": fmt.Sprintf("Standardspeicher %q wurde gefunden.", formatStorageTitleForLog(*defaultStorage)),
|
|
"defaultStorage": map[string]any{
|
|
"id": defaultStorage.ID,
|
|
"displayName": defaultStorage.DisplayName,
|
|
"name": defaultStorage.Name,
|
|
},
|
|
})
|
|
|
|
migration, err := migrateMilestoneStorageDevicesToDefaultStorage(
|
|
r.Context(),
|
|
config,
|
|
storageID,
|
|
*defaultStorage,
|
|
report,
|
|
)
|
|
if err != nil {
|
|
log.Printf(
|
|
"Milestone storage device migration failed during stream delete: storageId=%s defaultStorageId=%s error=%v",
|
|
storageID,
|
|
defaultStorage.ID,
|
|
err,
|
|
)
|
|
fail(http.StatusBadGateway, "Kameras und Mikrofone konnten nicht auf den Standardspeicher umgestellt werden", err)
|
|
return
|
|
}
|
|
|
|
report("storage-delete-progress", map[string]any{
|
|
"progress": 95,
|
|
"message": "Basis-Speicher wird gelöscht.",
|
|
"migratedDevices": migration,
|
|
})
|
|
|
|
if err := deleteMilestoneStorageFromAPI(r.Context(), config, storageID); err != nil {
|
|
log.Printf("Milestone storage delete failed during stream delete: storageId=%s error=%v", storageID, err)
|
|
fail(http.StatusBadGateway, "Milestone-Speicher konnte nicht gelöscht werden", err)
|
|
return
|
|
}
|
|
|
|
folderDeleteErrors := s.deleteMilestoneStorageDiskFolders(
|
|
r.Context(),
|
|
*storageToDelete,
|
|
archiveStoragesToDelete,
|
|
report,
|
|
)
|
|
|
|
completeMessage := "Basis-Speicher wurde gelöscht."
|
|
if len(folderDeleteErrors) > 0 {
|
|
completeMessage = "Basis-Speicher wurde gelöscht, aber nicht alle Ordner konnten entfernt werden."
|
|
}
|
|
|
|
report("storage-delete-complete", map[string]any{
|
|
"progress": 100,
|
|
"message": completeMessage,
|
|
"deletedStorage": map[string]any{
|
|
"id": storageToDelete.ID,
|
|
"displayName": storageToDelete.DisplayName,
|
|
"name": storageToDelete.Name,
|
|
},
|
|
"defaultStorage": map[string]any{
|
|
"id": defaultStorage.ID,
|
|
"displayName": defaultStorage.DisplayName,
|
|
"name": defaultStorage.Name,
|
|
},
|
|
"migratedDevices": migration,
|
|
"folderDeleteErrors": folderDeleteErrors,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleDeleteMilestoneArchiveStorage(w http.ResponseWriter, r *http.Request) {
|
|
archiveStorageID := strings.TrimSpace(r.PathValue("id"))
|
|
if archiveStorageID == "" {
|
|
writeError(w, http.StatusBadRequest, "Archive-Storage-ID fehlt")
|
|
return
|
|
}
|
|
|
|
config, err := s.getMilestoneRuntimeConfig(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusBadGateway, "Milestone-Konfiguration konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
archiveStorageToDelete, err := getMilestoneArchiveStorage(r.Context(), config, archiveStorageID)
|
|
if err != nil {
|
|
log.Printf("Milestone archive storage load before delete failed: archiveStorageId=%s error=%v", archiveStorageID, err)
|
|
writeError(w, http.StatusBadGateway, "Milestone-Archivspeicher konnte nicht geladen werden")
|
|
return
|
|
}
|
|
|
|
if err := deleteMilestoneArchiveStorageFromAPI(r.Context(), config, archiveStorageID); err != nil {
|
|
log.Printf("Milestone archive storage delete failed: archiveStorageId=%s error=%v", archiveStorageID, err)
|
|
writeError(w, http.StatusBadGateway, "Milestone-Archivspeicher konnte nicht gelöscht werden")
|
|
return
|
|
}
|
|
|
|
folderDeleteErrors := []string{}
|
|
if archiveStorageToDelete != nil {
|
|
for _, candidate := range milestoneArchiveFolderDeleteCandidates(*archiveStorageToDelete) {
|
|
if err := s.deleteServerFolderPath(r.Context(), candidate.RootType, candidate.Path); err != nil {
|
|
log.Printf(
|
|
"Milestone archive folder delete failed after archive delete: archiveStorageId=%s path=%s error=%v",
|
|
archiveStorageID,
|
|
candidate.Path,
|
|
err,
|
|
)
|
|
folderDeleteErrors = append(folderDeleteErrors, fmt.Sprintf("%s %s: %v", candidate.Label, candidate.Path, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ok": true,
|
|
"folderDeleteErrors": folderDeleteErrors,
|
|
})
|
|
}
|