Files
move-data-nas/internal/syncengine/parser.go
T
darroyo 8e08c73f60 feat: complete SyncServer implementation
Full-stack Go monolith with embedded React frontend for orchestrating
rsync-over-SSH file synchronization with Wake-on-LAN support.

Features:
- JWT auth (HS256) with bcrypt password hashing
- CRUD for machines (with WoL config) and sync_pairs
- Ed25519 SSH key generation and known_hosts management
- WoL magic packet sender + TCP-connect waiter with backoff
- Sync engine: rsync subprocess, per-pair job queue, progress parsing
- Homebrew cron parser for scheduled syncs
- SSE stream for live job status (queued/waking_up/running/success/failed)
- React+TS+Vite+Tailwind SPA embedded via embed.FS
- Debian packaging with systemd unit, postinst/prerm/postrm

Tech stack:
- Go 1.22+ (CGO_ENABLED=0, pure SQLite via modernc.org/sqlite)
- chi router for HTTP API
- TypeScript + React 18 + Tailwind CSS frontend
- Cross-compiled to Linux amd64 for Proxmox LXC deployment

Tests: wol (MAC parsing, magic packet), syncengine/queue, scheduler/cron
2026-07-07 15:03:22 -04:00

77 lines
2.0 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
}
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,]+)`)
)
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
}