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
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user