Files
move-data-nas/internal/syncengine/parser.go
T
2026-07-19 19:49:00 -04:00

172 lines
4.2 KiB
Go

package syncengine
import (
"regexp"
"strconv"
"strings"
)
type RsyncStats struct {
SentBytes int64
ReceivedBytes int64
TotalSize int64
Speedup float64
FilesSent int
FilesTotal int
}
type ProgressLine struct {
Phase string
Percent float64
Files int
Total int
SentBytes int64
XferedBytes int64
}
type ProgressFields struct {
FileBytes int64
Pct int
SpeedBps int64
EtaSeconds int
XfrDone int
XfrTotal int
}
var (
progressRegex = regexp.MustCompile(`\s*([\d,]+)\s+([\d,]+)\s+([\d%]+)\s*`)
sentRegex = regexp.MustCompile(`sent\s+([\d,]+)\s+bytes`)
recvRegex = regexp.MustCompile(`received\s+([\d,]+)\s+bytes`)
totalRegex = regexp.MustCompile(`total size is\s+([\d,]+)`)
filesRegex = regexp.MustCompile(`Number of files: ([\d,]+)`)
)
var perFileProgressRegex = regexp.MustCompile(
`^\s*(\d{1,3}(?:,\d{3})+)\s+(\d+)%\s+(\d+\.\d+)([kMG])B/s\s+(\d+:\d{2}:\d{2})(.*)`,
)
var xfrRegex = regexp.MustCompile(`xfr#(\d+).*to-chk=(\d+)/(\d+)`)
func parseXfrSuffix(suffix string) (done, total int) {
m := xfrRegex.FindStringSubmatch(suffix)
if m == nil {
return 0, 0
}
done, _ = strconv.Atoi(m[1])
t, _ := strconv.Atoi(m[2])
_ = t
total, _ = strconv.Atoi(m[3])
return done, total
}
func ParseProgressLine(line string) *ProgressLine {
if strings.Contains(line, "files to consider") || strings.Contains(line, "files...") {
return &ProgressLine{Phase: "scanning"}
}
if strings.Contains(line, "building file list") {
return &ProgressLine{Phase: "listing"}
}
if strings.Contains(line, "sent") && strings.Contains(line, "bytes") {
return &ProgressLine{Phase: "stats"}
}
return nil
}
func ParseStatsLine(line string) (int64, bool) {
m := sentRegex.FindStringSubmatch(line)
if len(m) >= 2 {
n, _ := strconv.ParseInt(strings.ReplaceAll(m[1], ",", ""), 10, 64)
return n, true
}
return 0, false
}
func ParseFinalStats(output string) *RsyncStats {
stats := &RsyncStats{}
lines := strings.Split(output, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if m := sentRegex.FindStringSubmatch(line); len(m) >= 2 {
stats.SentBytes, _ = strconv.ParseInt(strings.ReplaceAll(m[1], ",", ""), 10, 64)
}
if m := recvRegex.FindStringSubmatch(line); len(m) >= 2 {
stats.ReceivedBytes, _ = strconv.ParseInt(strings.ReplaceAll(m[1], ",", ""), 10, 64)
}
if m := totalRegex.FindStringSubmatch(line); len(m) >= 2 {
stats.TotalSize, _ = strconv.ParseInt(strings.ReplaceAll(m[1], ",", ""), 10, 64)
}
if m := filesRegex.FindStringSubmatch(line); len(m) >= 2 {
stats.FilesTotal, _ = strconv.Atoi(strings.ReplaceAll(m[1], ",", ""))
}
}
return stats
}
func isProgressOnlyLine(line string) bool {
return perFileProgressRegex.MatchString(line)
}
func parseProgressFields(line string) *ProgressFields {
m := perFileProgressRegex.FindStringSubmatch(line)
if m == nil {
return nil
}
bytes, _ := strconv.ParseInt(strings.ReplaceAll(m[1], ",", ""), 10, 64)
pct, _ := strconv.Atoi(m[2])
speed, _ := strconv.ParseFloat(m[3], 64)
unit := m[4]
eta := m[5]
suffix := m[6]
speedBps := int64(speed * 1e6)
switch unit {
case "k", "K":
speedBps = int64(speed * 1e3)
case "m", "M":
speedBps = int64(speed * 1e6)
case "g", "G":
speedBps = int64(speed * 1e9)
}
etaSecs := 0
parts := strings.Split(eta, ":")
if len(parts) == 3 {
h, _ := strconv.Atoi(parts[0])
m, _ := strconv.Atoi(parts[1])
s, _ := strconv.Atoi(parts[2])
etaSecs = h*3600 + m*60 + s
}
pf := &ProgressFields{
FileBytes: bytes,
Pct: pct,
SpeedBps: speedBps,
EtaSeconds: etaSecs,
}
if suffix != "" {
done, total := parseXfrSuffix(suffix)
pf.XfrDone = done
pf.XfrTotal = total
}
return pf
}
func isFileNameLine(line string) bool {
if line == "" || strings.TrimSpace(line) == "" {
return false
}
if strings.Contains(line, "sending incremental file list") ||
strings.Contains(line, "building file list") ||
strings.Contains(line, "cannot open") ||
strings.Contains(line, "skipping non-regular") ||
strings.HasPrefix(line, "sent ") ||
strings.HasPrefix(line, "total ") ||
strings.HasPrefix(line, "Number of files:") ||
strings.Contains(line, "bytes received") {
return false
}
return !isProgressOnlyLine(line)
}