mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-10 23:23:31 +08:00
182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
package backup
|
||
|
||
import (
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// MaintenanceWindow 描述一个允许执行备份的时段。
|
||
// 格式语义:
|
||
// - Days 为 "0..6" 的字符串集合(0=周日,6=周六);空 = 每天
|
||
// - StartMinutes / EndMinutes 为"午夜起计算的分钟数",0 ≤ v < 1440
|
||
// - 跨午夜窗口:Start > End 表示跨夜(如 22:00-06:00)
|
||
//
|
||
// 多个窗口是 OR 语义:只要 now 落入任一窗口即允许执行。
|
||
type MaintenanceWindow struct {
|
||
Days map[int]bool
|
||
StartMinutes int
|
||
EndMinutes int
|
||
}
|
||
|
||
// ParseMaintenanceWindows 解析用户配置(CSV 每项形如 "days=mon,tue|time=22:00-06:00")。
|
||
// 简化语法:多个窗口以 ';' 分隔,每个窗口按 "[days=xxx;]time=HH:MM-HH:MM" 格式。
|
||
// Days 缺省 = 全周;若不合法,跳过该段而非抛错(让调用方尽力工作)。
|
||
// 示例:
|
||
//
|
||
// "time=01:00-05:00" 每天 1 点到 5 点
|
||
// "days=sat,sun;time=00:00-23:59" 仅周末全天
|
||
// "time=22:00-06:00" 每天跨夜
|
||
// "days=mon,tue,wed,thu,fri;time=22:00-06:00" 工作日跨夜
|
||
func ParseMaintenanceWindows(value string) []MaintenanceWindow {
|
||
v := strings.TrimSpace(value)
|
||
if v == "" {
|
||
return nil
|
||
}
|
||
segments := strings.Split(v, ";")
|
||
var windows []MaintenanceWindow
|
||
for _, segment := range segments {
|
||
segment = strings.TrimSpace(segment)
|
||
if segment == "" {
|
||
continue
|
||
}
|
||
window, ok := parseSingleWindow(segment)
|
||
if !ok {
|
||
continue
|
||
}
|
||
windows = append(windows, window)
|
||
}
|
||
return windows
|
||
}
|
||
|
||
func parseSingleWindow(segment string) (MaintenanceWindow, bool) {
|
||
// "days=xxx,time=HH:MM-HH:MM" 或 "time=..."
|
||
fields := strings.Split(segment, ",")
|
||
days := map[int]bool{}
|
||
var timeExpr string
|
||
for _, field := range fields {
|
||
field = strings.TrimSpace(field)
|
||
if field == "" {
|
||
continue
|
||
}
|
||
if strings.HasPrefix(field, "days=") {
|
||
daysPart := strings.TrimPrefix(field, "days=")
|
||
for _, day := range strings.Split(daysPart, "|") {
|
||
if idx := parseDayToken(strings.TrimSpace(day)); idx >= 0 {
|
||
days[idx] = true
|
||
}
|
||
}
|
||
} else if strings.HasPrefix(field, "time=") {
|
||
timeExpr = strings.TrimPrefix(field, "time=")
|
||
}
|
||
}
|
||
start, end, ok := parseTimeRange(strings.TrimSpace(timeExpr))
|
||
if !ok {
|
||
return MaintenanceWindow{}, false
|
||
}
|
||
return MaintenanceWindow{Days: days, StartMinutes: start, EndMinutes: end}, true
|
||
}
|
||
|
||
var dayTokens = map[string]int{
|
||
"sun": 0, "sunday": 0, "0": 0,
|
||
"mon": 1, "monday": 1, "1": 1,
|
||
"tue": 2, "tuesday": 2, "2": 2,
|
||
"wed": 3, "wednesday": 3, "3": 3,
|
||
"thu": 4, "thursday": 4, "4": 4,
|
||
"fri": 5, "friday": 5, "5": 5,
|
||
"sat": 6, "saturday": 6, "6": 6,
|
||
}
|
||
|
||
func parseDayToken(value string) int {
|
||
v := strings.ToLower(strings.TrimSpace(value))
|
||
if v == "" {
|
||
return -1
|
||
}
|
||
if idx, ok := dayTokens[v]; ok {
|
||
return idx
|
||
}
|
||
return -1
|
||
}
|
||
|
||
// parseTimeRange 解析 "HH:MM-HH:MM",返回起止分钟数。
|
||
func parseTimeRange(value string) (int, int, bool) {
|
||
parts := strings.SplitN(value, "-", 2)
|
||
if len(parts) != 2 {
|
||
return 0, 0, false
|
||
}
|
||
start, ok := parseHHMM(parts[0])
|
||
if !ok {
|
||
return 0, 0, false
|
||
}
|
||
end, ok := parseHHMM(parts[1])
|
||
if !ok {
|
||
return 0, 0, false
|
||
}
|
||
return start, end, true
|
||
}
|
||
|
||
func parseHHMM(value string) (int, bool) {
|
||
parts := strings.Split(strings.TrimSpace(value), ":")
|
||
if len(parts) != 2 {
|
||
return 0, false
|
||
}
|
||
h, err := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||
if err != nil || h < 0 || h > 23 {
|
||
return 0, false
|
||
}
|
||
m, err := strconv.Atoi(strings.TrimSpace(parts[1]))
|
||
if err != nil || m < 0 || m > 59 {
|
||
return 0, false
|
||
}
|
||
return h*60 + m, true
|
||
}
|
||
|
||
// IsWithinWindow 判断 t 是否落入任一窗口。windows 为空或 nil 时总是返回 true(不限制)。
|
||
func IsWithinWindow(t time.Time, windows []MaintenanceWindow) bool {
|
||
if len(windows) == 0 {
|
||
return true
|
||
}
|
||
minutes := t.Hour()*60 + t.Minute()
|
||
weekday := int(t.Weekday())
|
||
for _, w := range windows {
|
||
if len(w.Days) > 0 && !w.Days[weekday] {
|
||
continue
|
||
}
|
||
if w.StartMinutes == w.EndMinutes {
|
||
continue
|
||
}
|
||
if w.StartMinutes < w.EndMinutes {
|
||
// 同日窗口
|
||
if minutes >= w.StartMinutes && minutes < w.EndMinutes {
|
||
return true
|
||
}
|
||
} else {
|
||
// 跨午夜:[start, 1440) ∪ [0, end)
|
||
if minutes >= w.StartMinutes || minutes < w.EndMinutes {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// ValidateMaintenanceWindows 用户输入合法性校验(返回人可读的错误)。
|
||
func ValidateMaintenanceWindows(value string) error {
|
||
v := strings.TrimSpace(value)
|
||
if v == "" {
|
||
return nil
|
||
}
|
||
segments := strings.Split(v, ";")
|
||
for _, segment := range segments {
|
||
segment = strings.TrimSpace(segment)
|
||
if segment == "" {
|
||
continue
|
||
}
|
||
if _, ok := parseSingleWindow(segment); !ok {
|
||
return fmt.Errorf("无效的维护窗口配置: %q(期望格式如 time=22:00-06:00 或 days=sat,sun,time=00:00-23:59)", segment)
|
||
}
|
||
}
|
||
return nil
|
||
}
|