package main import ( _ "embed" "encoding/json" "errors" "fmt" "os" "regexp" "sync" "time" log "github.com/sirupsen/logrus" "github.com/tailscale/hujson" ) type Config struct { ScheduleDuration map[string]string `json:"schedule"` Box map[string]BoxConfig `json:"box"` Email EmailConfig `json:"email"` Apps []AppConfig `json:"apps"` Timezone string `json:"timezone"` Admin *AdminConfig `json:"admin"` Debug bool `json:"debug"` box map[string]*Box `json:"-"` apps map[string]*App `json:"-"` timezone *time.Location `json:"-"` } var ( cfgMx sync.Mutex cfgRun bool ) //go:embed assets/backup.sample.json var sampleCfg []byte type BoxConfig struct { Addr string `json:"addr"` User string `json:"user"` Key string `json:"key"` AllowDirectConnect bool `json:"allow_direct_connect"` } type AppConfig struct { Name string `json:"name"` Schedule []string `json:"schedule"` Sources []string `json:"src"` Destinations []string `json:"dest"` Before map[string]string `json:"before"` After map[string]string `json:"after"` Active bool `json:"active"` } // Load config from file func LoadConfigFile(path string) (*Config, error) { log.WithFields(log.Fields{"path": path}).Debugf("starting") defer log.WithFields(log.Fields{"path": path}).Debugf("done") b, err := os.ReadFile(path) if err != nil { log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("") return nil, err } return LoadConfigByte(b) } // Load config from string func LoadConfigByte(conf []byte) (*Config, error) { log.WithFields(log.Fields{}).Debugf("starting") defer log.WithFields(log.Fields{}).Debugf("done") c := &Config{} if err := json.Unmarshal(sampleCfg, c); err != nil { log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal", "attr": "sampleCfg"}).Errorf("") return nil, err } b, err := hujson.Standardize(conf) if err != nil { log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("") return nil, err } if err := json.Unmarshal(b, c); err != nil { log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("") return nil, err } c.timezone, err = time.LoadLocation(c.Timezone) if err != nil { log.WithFields(log.Fields{"error": err, "call": "time.LoadLocation", "attr": cfg.Timezone}).Errorf("") return nil, err } if c.Email.Active { if len(c.Email.SmtpHost) == 0 { err := fmt.Errorf("no smtp") log.WithFields(log.Fields{"error": err}).Errorf("") return nil, err } if len(c.Email.FromEmail) == 0 { err := fmt.Errorf("no email from") log.WithFields(log.Fields{"error": err}).Errorf("") return nil, err } if len(c.Email.ToEmail) == 0 { err := fmt.Errorf("no email to") log.WithFields(log.Fields{"error": err}).Errorf("") return nil, err } } for k, v := range c.ScheduleDuration { switch k { case "hourly": case "daily": case "weekly": case "monthly": case "yearly": if _, err := Expiration(time.Now(), v); err != nil { log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("") return nil, err } default: err := errors.New("invalid schedule") log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("") return nil, err } } c.box = make(map[string]*Box) for k, v := range c.Box { if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil { log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("") return nil, err } else { if _, ok := c.box[k]; ok { err := errors.New("already exists") log.WithFields(log.Fields{"attr": k, "error": err}).Errorf("") return nil, err } c.box[k] = b } } c.apps = make(map[string]*App) for _, v := range c.Apps { if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil { log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("") return nil, err } else { if _, ok := c.apps[v.Name]; ok { err := errors.New("app already exists") log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("") return nil, err } c.apps[v.Name] = a for k := range a.schedule { if dur, ok := c.ScheduleDuration[k]; ok { re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`) if !re.MatchString(dur) { err := errors.New("incorrect schedule duration") log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("") return nil, err } } else { err := errors.New("undefined schedule duration") log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("") return nil, err } } } } return c, nil } // Run config func (c *Config) Run() { log.WithFields(log.Fields{}).Debugf("starting") defer log.WithFields(log.Fields{}).Debugf("done") if cfgRun { return } cfgMx.Lock() defer cfgMx.Unlock() cfgRun = true defer func() { cfgRun = false }() e := NewEmail(time.Now()) var wg sync.WaitGroup // Setup boxes for _, b := range c.box { wg.Add(1) go func(box *Box) { defer wg.Done() if err := box.Open(); err != nil { log.WithFields(log.Fields{"name": box.name, "call": "Open", "error": err}).Errorf("") e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name)) return } }(b) } wg.Wait() // Run each app for _, a := range cfg.apps { wg.Add(1) go func(app *App) { if sched, err := app.Run(e.startTime); err != nil { e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err)) } else if *debug { if sched != "" { e.AddItem(fmt.Sprintf(" - App : Success backing up %s (%s)", app.name, sched)) } else { e.AddItem(fmt.Sprintf(" - App : No backup for %s", app.name)) } } wg.Done() }(a) } wg.Wait() // Cleanup for _, a := range cfg.apps { for _, src := range a.sources { if b, ok := c.box[src.Box()]; ok { if fs, ok := b.zfs.filesystems[src.Path()]; ok { fs.srcApps = append(fs.srcApps, a) } } for _, dest := range a.destinations { if b, ok := c.box[dest.Box()]; ok { dest2 := dest.Append("/" + src.Box() + "/" + src.Path()) if fs, ok := b.zfs.filesystems[dest2.Path()]; ok { fs.destApps = append(fs.destApps, a) } else { e.AddItem(fmt.Sprintf(" - Dest : No folder (%s)", dest2.String())) } } } } } for _, b := range cfg.box { if b.online { for _, fs := range b.zfs.filesystems { if len(fs.srcApps) > 0 && !fs.backedUp { log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("not backed up") e.AddItem(fmt.Sprintf(" - Src : Folder not backed up (%s)", b.name+":"+fs.path)) } if len(fs.destApps) == 0 && !fs.backedUp && fs.managed { log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("managed") e.AddItem(fmt.Sprintf(" - Dest : Folder managed (%s)", b.name+":"+fs.path)) } } } } // Stop for _, b := range c.box { if err := b.Close(); err != nil { log.WithFields(log.Fields{"name": b.name, "call": "Close", "error": err}).Errorf("") } } if len(e.items) > 0 { if err := e.Send(cfg.Email.SmtpHost, cfg.Email.FromEmail, cfg.Email.ToEmail); err != nil { log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("") } } return }