feat: make storage tier thresholds configurable in system settings

Add StorageGreenThresholdPercent (default 30%) and StorageRedThresholdPercent
(default 10%) to both SystemSettingsDto and the settings UI.

StorageGuardService now reads thresholds from settings instead of hardcoding.

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
This commit is contained in:
2026-06-05 11:55:08 +08:00
co-authored by Claude Opus 4.8 noreply@anthropic.com
parent f5ad1dec00
commit 0831b33ea0
5 changed files with 81 additions and 6 deletions
+2
View File
@@ -421,6 +421,8 @@ export interface SystemSettings {
enableStorageGuard: boolean; enableStorageGuard: boolean;
pauseRecordingWhenFreeSpaceBelowMegabytes: number; pauseRecordingWhenFreeSpaceBelowMegabytes: number;
resumeRecordingWhenFreeSpaceAboveMegabytes: number; resumeRecordingWhenFreeSpaceAboveMegabytes: number;
storageGreenThresholdPercent: number;
storageRedThresholdPercent: number;
enableRetentionCleanup: boolean; enableRetentionCleanup: boolean;
retentionDays: number; retentionDays: number;
retentionDeleteFiles: boolean; retentionDeleteFiles: boolean;
+30 -1
View File
@@ -106,6 +106,8 @@ const form = reactive<SettingsFormModel>({
enableStorageGuard: true, enableStorageGuard: true,
pauseRecordingWhenFreeSpaceBelowMegabytes: 1024, pauseRecordingWhenFreeSpaceBelowMegabytes: 1024,
resumeRecordingWhenFreeSpaceAboveMegabytes: 4096, resumeRecordingWhenFreeSpaceAboveMegabytes: 4096,
storageGreenThresholdPercent: 30,
storageRedThresholdPercent: 10,
enableRetentionCleanup: false, enableRetentionCleanup: false,
retentionDays: 30, retentionDays: 30,
retentionDeleteFiles: false, retentionDeleteFiles: false,
@@ -1097,10 +1099,37 @@ watch(
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="16" style="margin-top: 16px">
<el-col :span="12">
<el-form-item label="绿色水位线:剩余空间高于 (%)">
<el-input-number
v-model="form.storageGreenThresholdPercent"
:min="5"
:max="90"
:step="5"
:disabled="!form.enableStorageGuard"
/>
<div class="field-hint">高于此比例时正常录制低于时拒绝新录制</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="红色水位线:剩余空间低于 (%)">
<el-input-number
v-model="form.storageRedThresholdPercent"
:min="1"
:max="85"
:step="5"
:disabled="!form.enableStorageGuard"
/>
<div class="field-hint">低于此比例时暂停所有录制和转码仅保留上传</div>
</el-form-item>
</el-col>
</el-row>
</el-form> </el-form>
<div class="helper-panel"> <div class="helper-panel">
恢复阈值建议高于暂停阈值避免磁盘空间在临界值附近反复抖动MP4 转码会额外占用中间 TS 文件空间 恢复阈值建议高于暂停阈值避免磁盘空间在临界值附近反复抖动MP4 转码会额外占用中间 TS 文件空间<br/>
绿色/红色水位线控制三级存储保护<b>绿色</b>(正常录制) <b>黄色</b>(拒绝新录制现有继续转码上传) <b>红色</b>(暂停所有录制转码仅上传清盘)
</div> </div>
</el-card> </el-card>
@@ -112,6 +112,12 @@ public sealed class SystemSettingsDto
public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096; public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096;
/// <summary>Disk free percentage above which storage is considered healthy (Green tier). Default 30%.</summary>
public double StorageGreenThresholdPercent { get; set; } = 30;
/// <summary>Disk free percentage below which storage is critical (Red tier). Default 10%.</summary>
public double StorageRedThresholdPercent { get; set; } = 10;
public bool EnableAutoReconnect { get; set; } = true; public bool EnableAutoReconnect { get; set; } = true;
public int ReconnectDelayMaxSeconds { get; set; } = 5; public int ReconnectDelayMaxSeconds { get; set; } = 5;
@@ -385,6 +391,12 @@ public sealed class UpdateSystemSettingsRequest
public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096; public int ResumeRecordingWhenFreeSpaceAboveMegabytes { get; set; } = 4096;
/// <summary>Disk free percentage above which storage is considered healthy (Green tier). Default 30%.</summary>
public double StorageGreenThresholdPercent { get; set; } = 30;
/// <summary>Disk free percentage below which storage is critical (Red tier). Default 10%.</summary>
public double StorageRedThresholdPercent { get; set; } = 10;
public bool EnableAutoReconnect { get; set; } = true; public bool EnableAutoReconnect { get; set; } = true;
public int ReconnectDelayMaxSeconds { get; set; } = 5; public int ReconnectDelayMaxSeconds { get; set; } = 5;
@@ -5,6 +5,7 @@ using LiveRecorder.Application.Models.Cleanup;
using LiveRecorder.Application.Models.Settings; using LiveRecorder.Application.Models.Settings;
using LiveRecorder.Domain.Entities; using LiveRecorder.Domain.Entities;
using LiveRecorder.Domain.Enums; using LiveRecorder.Domain.Enums;
using System.Globalization;
using System.Text.Json; using System.Text.Json;
namespace LiveRecorder.Application.Services; namespace LiveRecorder.Application.Services;
@@ -25,6 +26,8 @@ public sealed class SystemSettingsService : ISystemSettingsService
private const string EnableStorageGuardKey = "storage.guard.enabled"; private const string EnableStorageGuardKey = "storage.guard.enabled";
private const string PauseRecordingWhenFreeSpaceBelowMegabytesKey = "storage.guard.pause_recording_below_mb"; private const string PauseRecordingWhenFreeSpaceBelowMegabytesKey = "storage.guard.pause_recording_below_mb";
private const string ResumeRecordingWhenFreeSpaceAboveMegabytesKey = "storage.guard.resume_recording_above_mb"; private const string ResumeRecordingWhenFreeSpaceAboveMegabytesKey = "storage.guard.resume_recording_above_mb";
private const string StorageGreenThresholdPercentKey = "storage.guard.green_threshold_percent";
private const string StorageRedThresholdPercentKey = "storage.guard.red_threshold_percent";
private const string EnableReconnectKey = "recording.enable_auto_reconnect"; private const string EnableReconnectKey = "recording.enable_auto_reconnect";
private const string ReconnectDelayMaxSecondsKey = "recording.reconnect_delay_max_seconds"; private const string ReconnectDelayMaxSecondsKey = "recording.reconnect_delay_max_seconds";
private const string ReadWriteTimeoutMillisecondsKey = "recording.read_write_timeout_milliseconds"; private const string ReadWriteTimeoutMillisecondsKey = "recording.read_write_timeout_milliseconds";
@@ -140,6 +143,8 @@ public sealed class SystemSettingsService : ISystemSettingsService
EnableStorageGuard = bool.TryParse(GetValue(lookup, EnableStorageGuardKey, "true"), out var enableStorageGuard) && enableStorageGuard, EnableStorageGuard = bool.TryParse(GetValue(lookup, EnableStorageGuardKey, "true"), out var enableStorageGuard) && enableStorageGuard,
PauseRecordingWhenFreeSpaceBelowMegabytes = GetIntValue(lookup, PauseRecordingWhenFreeSpaceBelowMegabytesKey, 1024, 0, 1048576), PauseRecordingWhenFreeSpaceBelowMegabytes = GetIntValue(lookup, PauseRecordingWhenFreeSpaceBelowMegabytesKey, 1024, 0, 1048576),
ResumeRecordingWhenFreeSpaceAboveMegabytes = GetIntValue(lookup, ResumeRecordingWhenFreeSpaceAboveMegabytesKey, 4096, 0, 1048576), ResumeRecordingWhenFreeSpaceAboveMegabytes = GetIntValue(lookup, ResumeRecordingWhenFreeSpaceAboveMegabytesKey, 4096, 0, 1048576),
StorageGreenThresholdPercent = GetDoubleValue(lookup, StorageGreenThresholdPercentKey, 30, 5, 90),
StorageRedThresholdPercent = GetDoubleValue(lookup, StorageRedThresholdPercentKey, 10, 1, 85),
EnableAutoReconnect = bool.TryParse(GetValue(lookup, EnableReconnectKey, "true"), out var enableReconnect) && enableReconnect, EnableAutoReconnect = bool.TryParse(GetValue(lookup, EnableReconnectKey, "true"), out var enableReconnect) && enableReconnect,
ReconnectDelayMaxSeconds = GetIntValue(lookup, ReconnectDelayMaxSecondsKey, 5, 1, 300), ReconnectDelayMaxSeconds = GetIntValue(lookup, ReconnectDelayMaxSecondsKey, 5, 1, 300),
ReadWriteTimeoutMilliseconds = GetIntValue(lookup, ReadWriteTimeoutMillisecondsKey, 15000000, 1000, 60000000), ReadWriteTimeoutMilliseconds = GetIntValue(lookup, ReadWriteTimeoutMillisecondsKey, 15000000, 1000, 60000000),
@@ -307,6 +312,16 @@ public sealed class SystemSettingsService : ISystemSettingsService
Math.Clamp(request.ResumeRecordingWhenFreeSpaceAboveMegabytes, 0, 1048576).ToString(), Math.Clamp(request.ResumeRecordingWhenFreeSpaceAboveMegabytes, 0, 1048576).ToString(),
now, now,
cancellationToken); cancellationToken);
await UpsertAsync(
StorageGreenThresholdPercentKey,
Math.Clamp(request.StorageGreenThresholdPercent, 5, 90).ToString("F1", CultureInfo.InvariantCulture),
now,
cancellationToken);
await UpsertAsync(
StorageRedThresholdPercentKey,
Math.Clamp(request.StorageRedThresholdPercent, 1, 85).ToString("F1", CultureInfo.InvariantCulture),
now,
cancellationToken);
await UpsertAsync(EnableReconnectKey, request.EnableAutoReconnect.ToString(), now, cancellationToken); await UpsertAsync(EnableReconnectKey, request.EnableAutoReconnect.ToString(), now, cancellationToken);
await UpsertAsync(ReconnectDelayMaxSecondsKey, request.ReconnectDelayMaxSeconds.ToString(), now, cancellationToken); await UpsertAsync(ReconnectDelayMaxSecondsKey, request.ReconnectDelayMaxSeconds.ToString(), now, cancellationToken);
await UpsertAsync(ReadWriteTimeoutMillisecondsKey, request.ReadWriteTimeoutMilliseconds.ToString(), now, cancellationToken); await UpsertAsync(ReadWriteTimeoutMillisecondsKey, request.ReadWriteTimeoutMilliseconds.ToString(), now, cancellationToken);
@@ -466,6 +481,22 @@ public sealed class SystemSettingsService : ISystemSettingsService
return Math.Clamp(parsedValue, minimum, maximum); return Math.Clamp(parsedValue, minimum, maximum);
} }
private static double GetDoubleValue(
IReadOnlyDictionary<string, string> lookup,
string key,
double fallback,
double minimum,
double maximum)
{
var raw = GetValue(lookup, key, fallback.ToString(CultureInfo.InvariantCulture));
if (!double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsedValue))
{
return fallback;
}
return Math.Clamp(parsedValue, minimum, maximum);
}
private static IReadOnlyList<int> GetIntListValue(IReadOnlyDictionary<string, string> lookup, string key) private static IReadOnlyList<int> GetIntListValue(IReadOnlyDictionary<string, string> lookup, string key)
{ {
if (!lookup.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw)) if (!lookup.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
@@ -7,8 +7,6 @@ namespace LiveRecorder.Infrastructure.Services;
public sealed class StorageGuardService : IStorageGuardService public sealed class StorageGuardService : IStorageGuardService
{ {
private const long Megabyte = 1024L * 1024L; private const long Megabyte = 1024L * 1024L;
private const double GreenThresholdPercent = 30.0;
private const double YellowThresholdPercent = 10.0;
private readonly ILogger<StorageGuardService> _logger; private readonly ILogger<StorageGuardService> _logger;
public StorageGuardService(ILogger<StorageGuardService> logger) public StorageGuardService(ILogger<StorageGuardService> logger)
@@ -46,13 +44,16 @@ public sealed class StorageGuardService : IStorageGuardService
var usagePercent = totalBytes > 0 ? (double)usedBytes / totalBytes * 100.0 : 0; var usagePercent = totalBytes > 0 ? (double)usedBytes / totalBytes * 100.0 : 0;
var freePercent = 100.0 - usagePercent; var freePercent = 100.0 - usagePercent;
// Determine tier // Determine tier using configurable thresholds
var greenThreshold = Math.Clamp(settings.StorageGreenThresholdPercent, 5, 90);
var redThreshold = Math.Clamp(settings.StorageRedThresholdPercent, 1, greenThreshold - 1);
StorageTier tier; StorageTier tier;
if (freePercent >= GreenThresholdPercent) if (freePercent >= greenThreshold)
{ {
tier = StorageTier.Green; tier = StorageTier.Green;
} }
else if (freePercent >= YellowThresholdPercent) else if (freePercent >= redThreshold)
{ {
tier = StorageTier.Yellow; tier = StorageTier.Yellow;
} }