1120 lines
31 KiB
Go
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")
|
|
}
|
|
}
|