Files
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

182 lines
4.7 KiB
Go

package models
import (
"database/sql"
"strings"
"time"
)
type SyncPair struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
SourceMachineID *int64 `db:"source_machine_id" json:"source_machine_id"`
SourcePath string `db:"source_path" json:"source_path"`
DestMachineID *int64 `db:"dest_machine_id" json:"dest_machine_id"`
DestPath string `db:"dest_path" json:"dest_path"`
Direction string `db:"direction" json:"direction"`
RsyncFlags string `db:"rsync_flags" json:"rsync_flags"`
ExcludePatterns string `db:"exclude_patterns" json:"exclude_patterns"`
Enabled bool `db:"enabled" json:"enabled"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
type SyncPairRepository struct {
db *sql.DB
}
func NewSyncPairRepository(db *sql.DB) *SyncPairRepository {
return &SyncPairRepository{db: db}
}
func (r *SyncPairRepository) Create(sp *SyncPair) (int64, error) {
res, err := r.db.Exec(`
INSERT INTO sync_pairs (name, source_machine_id, source_path, dest_machine_id,
dest_path, direction, rsync_flags, exclude_patterns, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
sp.Name, sp.SourceMachineID, sp.SourcePath, sp.DestMachineID,
sp.DestPath, sp.Direction, sp.RsyncFlags, sp.ExcludePatterns, boolToInt(sp.Enabled),
)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
func (r *SyncPairRepository) GetAll() ([]SyncPair, error) {
rows, err := r.db.Query(`
SELECT id, name, source_machine_id, source_path, dest_machine_id, dest_path,
direction, rsync_flags, exclude_patterns, enabled, created_at
FROM sync_pairs ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var pairs []SyncPair
for rows.Next() {
var p SyncPair
var srcID, dstID sql.NullInt64
if err := rows.Scan(&p.ID, &p.Name, &srcID, &p.SourcePath, &dstID,
&p.DestPath, &p.Direction, &p.RsyncFlags, &p.ExcludePatterns,
&p.Enabled, &p.CreatedAt); err != nil {
return nil, err
}
if srcID.Valid {
v := srcID.Int64
p.SourceMachineID = &v
}
if dstID.Valid {
v := dstID.Int64
p.DestMachineID = &v
}
p.Enabled = intToBool(intToInt(p.Enabled))
pairs = append(pairs, p)
}
return pairs, rows.Err()
}
func intToInt(v interface{}) int {
switch x := v.(type) {
case int:
return x
case int64:
return int(x)
case bool:
if x {
return 1
}
return 0
default:
return 0
}
}
func (r *SyncPairRepository) GetByID(id int64) (*SyncPair, error) {
var p SyncPair
var srcID, dstID sql.NullInt64
err := r.db.QueryRow(`
SELECT id, name, source_machine_id, source_path, dest_machine_id, dest_path,
direction, rsync_flags, exclude_patterns, enabled, created_at
FROM sync_pairs WHERE id = ?`, id).Scan(
&p.ID, &p.Name, &srcID, &p.SourcePath, &dstID,
&p.DestPath, &p.Direction, &p.RsyncFlags, &p.ExcludePatterns,
&p.Enabled, &p.CreatedAt)
if err != nil {
return nil, err
}
if srcID.Valid {
v := srcID.Int64
p.SourceMachineID = &v
}
if dstID.Valid {
v := dstID.Int64
p.DestMachineID = &v
}
p.Enabled = intToBool(intToInt(p.Enabled))
return &p, nil
}
func (r *SyncPairRepository) Update(sp *SyncPair) error {
_, err := r.db.Exec(`
UPDATE sync_pairs SET name=?, source_machine_id=?, source_path=?, dest_machine_id=?,
dest_path=?, direction=?, rsync_flags=?, exclude_patterns=?, enabled=?
WHERE id=?`,
sp.Name, sp.SourceMachineID, sp.SourcePath, sp.DestMachineID,
sp.DestPath, sp.Direction, sp.RsyncFlags, sp.ExcludePatterns,
boolToInt(sp.Enabled), sp.ID,
)
return err
}
func (r *SyncPairRepository) Delete(id int64) error {
_, err := r.db.Exec("DELETE FROM sync_pairs WHERE id = ?", id)
return err
}
func (r *SyncPairRepository) GetEnabled() ([]SyncPair, error) {
rows, err := r.db.Query(`
SELECT id, name, source_machine_id, source_path, dest_machine_id, dest_path,
direction, rsync_flags, exclude_patterns, enabled, created_at
FROM sync_pairs WHERE enabled = 1`)
if err != nil {
return nil, err
}
defer rows.Close()
var pairs []SyncPair
for rows.Next() {
var p SyncPair
var srcID, dstID sql.NullInt64
if err := rows.Scan(&p.ID, &p.Name, &srcID, &p.SourcePath, &dstID,
&p.DestPath, &p.Direction, &p.RsyncFlags, &p.ExcludePatterns,
&p.Enabled, &p.CreatedAt); err != nil {
return nil, err
}
if srcID.Valid {
v := srcID.Int64
p.SourceMachineID = &v
}
if dstID.Valid {
v := dstID.Int64
p.DestMachineID = &v
}
p.Enabled = true
pairs = append(pairs, p)
}
return pairs, rows.Err()
}
func (sp *SyncPair) ExcludePatternsList() []string {
if sp.ExcludePatterns == "" {
return nil
}
var patterns []string
for _, p := range strings.Split(sp.ExcludePatterns, "\n") {
p = strings.TrimSpace(p)
if p != "" {
patterns = append(patterns, p)
}
}
return patterns
}