365 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			365 lines
		
	
	
		
			9.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	_ "embed"
 | 
						|
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"os"
 | 
						|
	"regexp"
 | 
						|
	"sync"
 | 
						|
	"time"
 | 
						|
 | 
						|
	log "github.com/sirupsen/logrus"
 | 
						|
	"github.com/tailscale/hujson"
 | 
						|
	"github.com/tidwall/pretty"
 | 
						|
)
 | 
						|
 | 
						|
type Config struct {
 | 
						|
	ScheduleDuration map[string]string     `json:"schedule,omitempty"`
 | 
						|
	Box              map[string]*BoxConfig `json:"box,omitempty"`
 | 
						|
	Email            *EmailConfig          `json:"email,omitempty"`
 | 
						|
	Apps             []*AppConfig          `json:"apps,omitempty"`
 | 
						|
	Timezone         string                `json:"timezone,omitempty"`
 | 
						|
	Admin            *AdminConfig          `json:"admin,omitempty"`
 | 
						|
	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"`
 | 
						|
}
 | 
						|
 | 
						|
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,omitempty"`
 | 
						|
	After        map[string]string `json:"after,omitempty"`
 | 
						|
	Active       bool              `json:"active,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
type EmailConfig struct {
 | 
						|
	Active    bool     `json:"active"`
 | 
						|
	SmtpHost  string   `json:"smtp,omitempty"`
 | 
						|
	FromEmail string   `json:"email_from,omitempty"`
 | 
						|
	ToEmail   []string `json:"email_to,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
func CfgLock() {
 | 
						|
	log.WithFields(log.Fields{}).Debugf("starting")
 | 
						|
	cfgMx.Lock()
 | 
						|
}
 | 
						|
 | 
						|
func CfgUnlock() {
 | 
						|
	log.WithFields(log.Fields{}).Debugf("starting")
 | 
						|
	cfgMx.Unlock()
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
// 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 != nil {
 | 
						|
		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); 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
 | 
						|
}
 | 
						|
 | 
						|
// Pretty config
 | 
						|
func (c *Config) Pretty() ([]byte, error) {
 | 
						|
	log.WithFields(log.Fields{}).Debugf("starting")
 | 
						|
	defer log.WithFields(log.Fields{}).Debugf("done")
 | 
						|
 | 
						|
	CfgLock()
 | 
						|
	defer CfgUnlock()
 | 
						|
 | 
						|
	b, err := json.Marshal(cfg)
 | 
						|
	if err != nil {
 | 
						|
		log.WithFields(log.Fields{"error": err, "call": "json.Marshal"}).Errorf("")
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return pretty.PrettyOptions(b, &pretty.Options{Indent: "        "}), nil
 | 
						|
}
 | 
						|
 | 
						|
// Pretty App Config
 | 
						|
func (a *AppConfig) Pretty() ([]byte, error) {
 | 
						|
	log.WithFields(log.Fields{}).Debugf("starting")
 | 
						|
	defer log.WithFields(log.Fields{}).Debugf("done")
 | 
						|
 | 
						|
	CfgLock()
 | 
						|
	defer CfgUnlock()
 | 
						|
 | 
						|
	b, err := json.Marshal(a)
 | 
						|
	if err != nil {
 | 
						|
		log.WithFields(log.Fields{"error": err, "call": "json.Marshal"}).Errorf("")
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return pretty.PrettyOptions(b, &pretty.Options{Indent: "        "}), nil
 | 
						|
}
 | 
						|
 | 
						|
// Save config
 | 
						|
func (c *Config) Save() error {
 | 
						|
	log.WithFields(log.Fields{}).Debugf("starting")
 | 
						|
	defer log.WithFields(log.Fields{}).Debugf("done")
 | 
						|
 | 
						|
	r, err := cfg.Pretty()
 | 
						|
	if err != nil {
 | 
						|
		log.WithFields(log.Fields{"error": err, "call": "cfg.Pretty"}).Errorf("")
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	f, err := os.Create(*cfgFile)
 | 
						|
	if err != nil {
 | 
						|
		log.WithFields(log.Fields{"error": err, "call": "os.Open"}).Errorf("")
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	if _, err := f.Write(r); err != nil {
 | 
						|
		log.WithFields(log.Fields{"error": err, "call": "File.Write"}).Errorf("")
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
// Run config
 | 
						|
func (c *Config) Run() {
 | 
						|
	log.WithFields(log.Fields{}).Debugf("starting")
 | 
						|
	defer log.WithFields(log.Fields{}).Debugf("done")
 | 
						|
 | 
						|
	if cfgRun {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	CfgLock()
 | 
						|
	defer CfgUnlock()
 | 
						|
 | 
						|
	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.RunTime(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("")
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 |