8e08c73f60
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
77 lines
2.0 KiB
Go
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
|
|
}
|