diff --git a/backive.go b/backive.go new file mode 100644 index 0000000..4ee9ffa --- /dev/null +++ b/backive.go @@ -0,0 +1,11 @@ +package backive + +var config Configuration +var runs Runs +var database Database + +func Init(cfg Configuration, db Database) { + config = cfg + database = db + runs.Load(database) +} diff --git a/backive.yml b/backive.yml index 99e6288..864a7cf 100644 --- a/backive.yml +++ b/backive.yml @@ -14,10 +14,11 @@ backups: dev_test_backup: user: qwc targetDevice: dev_test - frequency: 0 + frequency: 2 scriptPath: /home/qwc/source/backive/testbackup.sh sourcePath: /home/qwc/web/worktime targetPath: worktime + label: "Development test backup" scanner_usbstick_test: user: qwc targetDevice: scanner-usbstick diff --git a/backup.go b/backup.go new file mode 100644 index 0000000..8bb24fc --- /dev/null +++ b/backup.go @@ -0,0 +1,203 @@ +package backive + +import ( + "encoding/json" + "fmt" + "io" + "log" + "os" + "os/exec" + "path" + "strings" + "time" +) + +// Backup contains all necessary information for executing a configured backup. +type Backup struct { + Name string `mapstructure:",omitempty"` + TargetDevice string `mapstructure:"targetDevice"` + TargetPath string `mapstructure:"targetPath"` + SourcePath string `mapstructure:"sourcePath"` + ScriptPath string `mapstructure:"scriptPath"` + Frequency int `mapstructure:"frequency"` + ExeUser string `mapstructure:"user,omitempty"` + Label string `mapstructure:"label,omitempty"` + logger *log.Logger +} + +// Backups is nothing else than a name to Backup type mapping +type Backups map[string]*Backup + +// findBackupsForDevice only finds the first backup which is configured for a given device. +func (bs *Backups) FindBackupsForDevice(d Device) ([]*Backup, bool) { + var backups []*Backup = []*Backup{} + for _, b := range *bs { + if d.Name == b.TargetDevice { + backups = append(backups, b) + } + } + var ret bool = len(backups) > 0 + return backups, ret +} + +func (b *Backup) CanRun() error { + // target path MUST exist + if b.TargetPath == "" { + return fmt.Errorf("The setting targetPath MUST exist within a backup configuration.") + } + // script must exist, having only script means this is handled in the script + if b.ScriptPath == "" { + return fmt.Errorf("The setting scriptPath must exist within a backup configuration.") + } + if !b.ShouldRun() { + return fmt.Errorf("Frequency (days inbetween) not reached.") + } + return nil +} + +func (b *Backup) PrepareRun() error { + backupPath := path.Join( + config.Settings.SystemMountPoint, + b.TargetDevice, + b.TargetPath, + ) + CreateDirectoryIfNotExists(backupPath) + // configure extra logger + logname := "/var/log/backive/backive.log" + logdir, _ := path.Split(logname) + CreateDirectoryIfNotExists(logdir) + logname = path.Join(logdir, b.Name) + ".log" + logfile, err := os.OpenFile(logname, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + log.Println("Error creating logfile!") + return err + } + writer := io.MultiWriter(logfile) + b.logger = log.New(writer, b.Name, log.LstdFlags) + cmd := exec.Command("chown", "-R", b.ExeUser, backupPath) + err = cmd.Run() + if err != nil { + b.logger.Printf("chown for backup directory failed: %s", err) + return err + } + return nil +} + +// Run runs the backup script with appropriate rights. +func (b *Backup) Run() error { + log.Printf("Running backup '%s'.", b.Name) + dev, ok := config.Devices[b.TargetDevice] + if ok { + log.Printf("Device found: %s (%s).", dev.Name, dev.UUID) + } else { + log.Printf("Device %s not found", b.TargetDevice) + } + if ok && dev.IsMounted() { + if !strings.ContainsAny(b.ScriptPath, "/") || strings.HasPrefix(b.ScriptPath, ".") { + //The scriptPath is a relative path, from the place of the config, so use the config as base + log.Printf("ERROR: Script path is relative, aborting.") + return fmt.Errorf("Script path is relative, aborting.") + } + cmd := exec.Command("/usr/bin/sh", b.ScriptPath) + if b.ExeUser != "" { + // setup script environment including user to use + cmd = exec.Command("sudo", "-E", "-u", b.ExeUser, "/usr/bin/sh", b.ScriptPath) + } + b.logger.Printf("Running backup script of '%s'", b.Name) + b.logger.Printf("Script is: %s", b.ScriptPath) + b.logger.Printf("Full command is: %s", cmd.String()) + cmd.Stdout = b.logger.Writer() + cmd.Stderr = b.logger.Writer() + cmd.Env = []string{ + fmt.Sprintf("BACKIVE_MOUNT=%s", config.Settings.SystemMountPoint), + fmt.Sprintf("BACKIVE_TO=%s", + path.Join(config.Settings.SystemMountPoint, dev.Name, b.TargetPath), + ), + fmt.Sprintf("BACKIVE_FROM=%s", b.SourcePath), + } + log.Printf("Environment for process: %s", cmd.Env) + cmd.Dir = path.Join(config.Settings.SystemMountPoint, dev.Name) + + log.Printf("About to run: %s", cmd.String()) + // run script + err := cmd.Run() + if err != nil { + log.Printf("Backup '%s' run failed", b.Name) + return err + } + runs.RegisterRun(b) + return nil + } + // quit with error that the device is not available. + return fmt.Errorf("The device is not mounted") +} + +// Runs contains the Data for the scheduler: mapping from backups to a list of timestamps of the last 10 backups +type Runs struct { + data map[string][]time.Time +} + +// Load loads the data from the json database +func (r *Runs) Load(db Database) { + data := db.data["runs"] + if data != "" { + runerr := json.Unmarshal([]byte(db.data["runs"]), &r.data) + if runerr != nil { + panic(runerr) + } + } +} + +// Save saves the data into the json database +func (r *Runs) Save(db Database) { + str, err := json.Marshal(r.data) + if err != nil { + panic(err) + } + db.data["runs"] = string(str) +} + +// ShouldRun Takes a backup key and returns a bool if a backup should run now. +func (b *Backup) ShouldRun() bool { + freq := b.Frequency + // calculate time difference from last run, return true if no run has taken place + lr, ok := runs.LastRun(b) + if ok == nil { + dur := time.Since(lr) + days := dur.Hours() / 24 + if days >= float64(freq) { + return true + } + } + if freq == 0 { + return true + } + return false +} + +// RegisterRun saves a date of a backup run into the internal storage +func (r *Runs) RegisterRun(b *Backup) { + if r.data == nil { + r.data = map[string][]time.Time{} + } + nbl, ok := r.data[b.Name] + if !ok { + nbl = make([]time.Time, 1) + } + nbl = append([]time.Time{time.Now()}, nbl...) + r.data[b.Name] = nbl + r.Save(database) +} + +// LastRun returns the time.Time of the last run of the backup given. +func (r *Runs) LastRun(b *Backup) (time.Time, error) { + _, ok := r.data[b.Name] + if ok { + slice := r.data[b.Name] + if len(slice) > 0 { + var t = time.Time(slice[0]) + return t, nil + } + } + return time.Unix(0, 0), fmt.Errorf("Backup name not found and therefore has never run") +} diff --git a/backup/backup.go b/backup/backup.go deleted file mode 100644 index 28342a5..0000000 --- a/backup/backup.go +++ /dev/null @@ -1,12 +0,0 @@ -package backup - -// Backup contains all necessary information for executing a configured backup. -type Backup struct { - Name string `mapstructure:",omitempty"` - TargetDevice string `mapstructure:"targetDevice"` - TargetDir string `mapstructure:"targetDir"` - SourceDir string `mapstructure:"sourceDir"` - ScriptPath string `mapstructure:"scriptPath"` - Frequency int `mapstructure:"frequency"` - ExeUser string `mapstructure:"user,omitempty"` -} diff --git a/cmd/backive/main.go b/cmd/backive/main.go index 0104806..4122bc5 100644 --- a/cmd/backive/main.go +++ b/cmd/backive/main.go @@ -1,39 +1,22 @@ package main import ( - "bytes" - "encoding/json" "fmt" - "io" "log" - "net" "os" - "os/exec" "os/signal" "path" - "strings" "syscall" - "time" - "github.com/spf13/viper" + "github.com/qwc/backive" ) var logfile os.File -func createDirectoryIfNotExists(dir string) { - if _, err := os.Stat(dir); err == nil { - //ignore - } else if os.IsNotExist(err) { - os.MkdirAll(dir, 0755) - } else { - log.Fatal(err) - } -} - func setupLogging() { logname := "/var/log/backive/backive.log" logdir, _ := path.Split(logname) - createDirectoryIfNotExists(logdir) + backive.CreateDirectoryIfNotExists(logdir) logfile, err := os.OpenFile(logname, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) if err != nil { fmt.Println("Error creating logfile!") @@ -44,433 +27,14 @@ func setupLogging() { // Global variables for backive var ( - database Database - config Configuration - runs Runs - events EventHandler + config backive.Configuration + database backive.Database + events backive.EventHandler ) -var devsByUuid string = "/dev/disk/by-uuid" -var dbPath string = "/var/lib/backive/data.json" - -// Database is a simple string to string mapping, where arbitrary strings can be stored and safed to disk or loaded -type Database struct { - data map[string]string -} - -// Save saves the database -func (d *Database) Save() { - jsonstr, merr := json.Marshal(d.data) - if merr != nil { - panic(merr) - } - log.Printf("Writing database output to file: %s", jsonstr) - saveDir, _ := path.Split(dbPath) - createDirectoryIfNotExists(saveDir) - err := os.WriteFile(dbPath, []byte(jsonstr), 0644) - if err != nil { - panic(err) - } -} - -// LoadDb loads the database -func (d *Database) Load() { - if _, err := os.Stat(dbPath); err == nil { - data, rferr := os.ReadFile(dbPath) - if rferr != nil { - panic(rferr) - } - json.Unmarshal(data, &d.data) - } else if os.IsNotExist(err) { - // no data - - } -} - -// Device represents a device, with a name easy to remember and the UUID to identify it, optionally an owner. -type Device struct { - Name string `mapstructure:",omitempty"` - UUID string `mapstructure:"uuid"` - OwnerUser string `mapstructure:"owner,omitempty"` - isMounted bool -} - -// Mount will mount a device -func (d *Device) Mount() error { - log.Printf("Mounting device %s, creating directory if it does not exist.\n", d.Name) - createDirectoryIfNotExists( - path.Join(config.Settings.SystemMountPoint, d.Name), - ) - //time.Sleep(3000 * time.Millisecond) - log.Printf("Executing mount command for %s", d.Name) - cmd := exec.Command( - "mount", - path.Join(devsByUuid, d.UUID), - path.Join(config.Settings.SystemMountPoint, d.Name), - ) - cmd.Stdout = log.Writer() - cmd.Stderr = log.Writer() - log.Printf("Command to execute: %s", cmd.String()) - err := cmd.Run() - if err != nil { - log.Printf("Mounting failed with error %v", err) - return err - } - d.isMounted = true - return nil -} - -// Unmount will unmount a device -func (d *Device) Unmount() error { - if d.isMounted { - log.Printf("Unmounting %s", d.Name) - sync := exec.Command("sync") - syncErr := sync.Run() - if syncErr != nil { - log.Println(syncErr) - return syncErr - } - cmd := exec.Command( - "umount", - path.Join(config.Settings.SystemMountPoint, d.Name), - ) - log.Printf("About to run: %s", cmd.String()) - err := cmd.Run() - if err != nil { - log.Println(err) - return err - } - d.isMounted = false - } - return nil -} - -func (d *Device) IsMounted() bool { - return d.isMounted -} - -// Backup contains all necessary information for executing a configured backup. -type Backup struct { - Name string `mapstructure:",omitempty"` - TargetDevice string `mapstructure:"targetDevice"` - TargetPath string `mapstructure:"targetPath"` - SourcePath string `mapstructure:"sourcePath"` - ScriptPath string `mapstructure:"scriptPath"` - Frequency int `mapstructure:"frequency"` - ExeUser string `mapstructure:"user,omitempty"` - logger *log.Logger -} - -// Configuration struct holding the settings and config items of devices and backups -type Configuration struct { - Settings Settings `mapstructure:"settings"` - Devices Devices `mapstructure:"devices"` - Backups Backups `mapstructure:"backups"` - Vconfig *viper.Viper -} - -// Settings struct holds the global configuration items -type Settings struct { - SystemMountPoint string `mapstructure:"systemMountPoint"` - UserMountPoint string `mapstructure:"userMountPoint"` - UnixSocketLocation string `mapstructure:"unixSocketLocation"` - LogLocation string `mapstructure:"logLocation"` - DbLocation string `mapstructure:"dbLocation"` -} - -// Devices is nothing else than a name to Device type mapping -type Devices map[string]*Device - -// Backups is nothing else than a name to Backup type mapping -type Backups map[string]*Backup - -// findBackupsForDevice only finds the first backup which is configured for a given device. -func (bs *Backups) findBackupsForDevice(d Device) ([]*Backup, bool) { - var backups []*Backup = []*Backup{} - for _, b := range *bs { - if d.Name == b.TargetDevice { - backups = append(backups, b) - } - } - var ret bool = len(backups) > 0 - return backups, ret -} - -// CreateViper creates a viper instance for usage later -func (c *Configuration) CreateViper() { - vconfig := viper.New() - // vconfig.Debug() - vconfig.SetConfigName("backive") - vconfig.SetConfigFile("backive.yml") - //vconfig.SetConfigFile("backive.yaml") - vconfig.SetConfigType("yaml") - vconfig.AddConfigPath("/etc/backive/") // system config - //vconfig.AddConfigPath("$HOME/.backive/") - vconfig.AddConfigPath(".") - c.Vconfig = vconfig -} - -// Load loads the configuration from the disk -func (c *Configuration) Load() { - c.CreateViper() - vc := c.Vconfig - if err := vc.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - panic(fmt.Errorf("Fatal: No config file could be found: %w", err)) - } - panic(fmt.Errorf("Fatal error config file: %w ", err)) - } - log.Printf("Configuration file used: %s", vc.ConfigFileUsed()) - - //Unmarshal all into Configuration type - err := vc.Unmarshal(c) - if err != nil { - fmt.Printf("Error occured when loading config: %v\n", err) - panic("No configuration available!") - } - for k, v := range c.Backups { - log.Printf("Initializing backup '%s'\n", k) - v.Name = k - log.Printf("Initialized backup '%s'\n", v.Name) - } - for k, v := range c.Devices { - log.Printf("Initializing device '%s'\n", k) - v.Name = k - log.Printf("Initialized device '%s'\n", v.Name) - } -} - -type EventHandler struct { - ls net.Listener - done <-chan struct{} - callbacks []func(map[string]string) -} - -// Init initializes the unix socket. -func (eh *EventHandler) Init(socketPath string) { - log.Println("Initializing EventHandler...") - var err error - dir, _ := path.Split(socketPath) - createDirectoryIfNotExists(dir) - eh.ls, err = net.Listen("unix", socketPath) - if err != nil { - panic(err) - } - eh.callbacks = make([]func(map[string]string), 3) -} - -// Listen starts the event loop. -func (eh *EventHandler) Listen() { - log.Println("Running eventloop") - func() { - for { - eh.process() - } - }() -} - -// RegisterCallback adds a function to the list of callback functions for processing of events. -func (eh *EventHandler) RegisterCallback(cb func(map[string]string)) { - eh.callbacks = append(eh.callbacks, cb) -} - -// process processes each and every unix socket event, Unmarshals the json data and calls the list of callbacks. -func (eh *EventHandler) process() { - client, err := eh.ls.Accept() - log.Println("Accepted client") - if err != nil { - log.Fatal(err) - } - data := make([]byte, 2048) - for { - buf := make([]byte, 512) - nr, err := client.Read(buf) - if err != nil && err != io.EOF { - log.Fatal(err) - } - data = append(data, buf[0:nr]...) - if err == io.EOF { - break - } - } - sdata := string(bytes.Trim(data, "\x00")) - //log.Println(sdata) - env := map[string]string{} - errjson := json.Unmarshal([]byte(sdata), &env) - if errjson != nil { - log.Fatal(errjson) - } - for _, v := range eh.callbacks { - if v != nil { - v(env) - } - } -} - -func (b *Backup) CanRun() error { - // target path MUST exist - if b.TargetPath == "" { - return fmt.Errorf("The setting targetPath MUST exist within a backup configuration.") - } - // script must exist, having only script means this is handled in the script - if b.ScriptPath == "" { - return fmt.Errorf("The setting scriptPath must exist within a backup configuration.") - } - if !b.ShouldRun() { - return fmt.Errorf("Frequency (days inbetween) not reached.") - } - return nil -} - -func (b *Backup) PrepareRun() error { - backupPath := path.Join( - config.Settings.SystemMountPoint, - b.TargetDevice, - b.TargetPath, - ) - createDirectoryIfNotExists(backupPath) - // configure extra logger - logname := "/var/log/backive/backive.log" - logdir, _ := path.Split(logname) - createDirectoryIfNotExists(logdir) - logname = path.Join(logdir, b.Name) + ".log" - logfile, err := os.OpenFile(logname, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) - if err != nil { - log.Println("Error creating logfile!") - return err - } - writer := io.MultiWriter(logfile) - b.logger = log.New(writer, b.Name, log.LstdFlags) - cmd := exec.Command("chown", "-R", b.ExeUser, backupPath) - err = cmd.Run() - if err != nil { - b.logger.Printf("chown for backup directory failed: %s", err) - return err - } - return nil -} - -// Run runs the backup script with appropriate rights. -func (b *Backup) Run() error { - log.Printf("Running backup '%s'.", b.Name) - dev, ok := config.Devices[b.TargetDevice] - if ok { - log.Printf("Device found: %s (%s).", dev.Name, dev.UUID) - } else { - log.Printf("Device %s not found", b.TargetDevice) - } - if ok && dev.IsMounted() { - if !strings.ContainsAny(b.ScriptPath, "/") || strings.HasPrefix(b.ScriptPath, ".") { - //The scriptPath is a relative path, from the place of the config, so use the config as base - log.Printf("ERROR: Script path is relative, aborting.") - return fmt.Errorf("Script path is relative, aborting.") - } - cmd := exec.Command("/usr/bin/sh", b.ScriptPath) - if b.ExeUser != "" { - // setup script environment including user to use - cmd = exec.Command("sudo", "-E", "-u", b.ExeUser, "/usr/bin/sh", b.ScriptPath) - } - b.logger.Printf("Running backup script of '%s'", b.Name) - b.logger.Printf("Script is: %s", b.ScriptPath) - b.logger.Printf("Full command is: %s", cmd.String()) - cmd.Stdout = b.logger.Writer() - cmd.Stderr = b.logger.Writer() - cmd.Env = []string{ - fmt.Sprintf("BACKIVE_MOUNT=%s", config.Settings.SystemMountPoint), - fmt.Sprintf("BACKIVE_TO=%s", - path.Join(config.Settings.SystemMountPoint, dev.Name, b.TargetPath), - ), - fmt.Sprintf("BACKIVE_FROM=%s", b.SourcePath), - } - log.Printf("Environment for process: %s", cmd.Env) - cmd.Dir = path.Join(config.Settings.SystemMountPoint, dev.Name) - - log.Printf("About to run: %s", cmd.String()) - // run script - err := cmd.Run() - if err != nil { - log.Printf("Backup '%s' run failed", b.Name) - return err - } - runs.RegisterRun(b) - return nil - } - // quit with error that the device is not available. - return fmt.Errorf("The device is not mounted") -} - -// Runs contains the Data for the scheduler: mapping from backups to a list of timestamps of the last 10 backups -type Runs struct { - data map[string][]time.Time -} - -// Load loads the data from the json database -func (r *Runs) Load(db Database) { - data := db.data["runs"] - if data != "" { - runerr := json.Unmarshal([]byte(db.data["runs"]), &r.data) - if runerr != nil { - panic(runerr) - } - } -} - -// Save saves the data into the json database -func (r *Runs) Save(db Database) { - str, err := json.Marshal(r.data) - if err != nil { - panic(err) - } - db.data["runs"] = string(str) -} - -// ShouldRun Takes a backup key and returns a bool if a backup should run now. -func (b *Backup) ShouldRun() bool { - freq := b.Frequency - // calculate time difference from last run, return true if no run has taken place - lr, ok := runs.LastRun(*b) - if ok == nil { - dur := time.Since(lr) - days := dur.Hours() / 24 - if days >= float64(freq) { - return true - } - } - if freq == 0 { - return true - } - return false -} - -// RegisterRun saves a date of a backup run into the internal storage -func (r *Runs) RegisterRun(b *Backup) { - if r.data == nil { - r.data = map[string][]time.Time{} - } - nbl, ok := r.data[b.Name] - if !ok { - nbl = make([]time.Time, 1) - } - nbl = append([]time.Time{time.Now()}, nbl...) - r.data[b.Name] = nbl - r.Save(database) -} - -// LastRun returns the time.Time of the last run of the backup given. -func (r *Runs) LastRun(b Backup) (time.Time, error) { - _, ok := r.data[b.Name] - if ok { - slice := r.data[b.Name] - if len(slice) > 0 { - var t = time.Time(slice[0]) - return t, nil - } - } - return time.Unix(0, 0), fmt.Errorf("Backup name not found and therefore has never run") -} func defaultCallback(envMap map[string]string) { if action, ok := envMap["ACTION"]; ok && action == "add" { - var dev *Device + var dev *backive.Device var uuid string if fs_uuid, ok := envMap["ID_FS_UUID"]; !ok { log.Println("ID_FS_UUID not available ?!") @@ -490,7 +54,7 @@ func defaultCallback(envMap map[string]string) { if uuidFound { log.Println("Device recognized.") log.Printf("Device: Name: %s, UUID: %s", dev.Name, dev.UUID) - backups, found := config.Backups.findBackupsForDevice(*dev) + backups, found := config.Backups.FindBackupsForDevice(*dev) log.Println("Searching configured backups...") if found { for _, backup := range backups { @@ -569,7 +133,7 @@ func main() { // find and load config database.Load() config.Load() - runs.Load(database) + backive.Init(config, database) // init scheduler and check for next needed runs? // start event loop diff --git a/config.go b/config.go new file mode 100644 index 0000000..a395d84 --- /dev/null +++ b/config.go @@ -0,0 +1,69 @@ +package backive + +import ( + "fmt" + "log" + + "github.com/spf13/viper" +) + +// Configuration struct holding the settings and config items of devices and backups +type Configuration struct { + Settings Settings `mapstructure:"settings"` + Devices Devices `mapstructure:"devices"` + Backups Backups `mapstructure:"backups"` + Vconfig *viper.Viper +} + +// Settings struct holds the global configuration items +type Settings struct { + SystemMountPoint string `mapstructure:"systemMountPoint"` + UserMountPoint string `mapstructure:"userMountPoint"` + UnixSocketLocation string `mapstructure:"unixSocketLocation"` + LogLocation string `mapstructure:"logLocation"` + DbLocation string `mapstructure:"dbLocation"` +} + +// CreateViper creates a viper instance for usage later +func (c *Configuration) CreateViper() { + vconfig := viper.New() + // vconfig.Debug() + vconfig.SetConfigName("backive") + vconfig.SetConfigFile("backive.yml") + //vconfig.SetConfigFile("backive.yaml") + vconfig.SetConfigType("yaml") + vconfig.AddConfigPath("/etc/backive/") // system config + //vconfig.AddConfigPath("$HOME/.backive/") + vconfig.AddConfigPath(".") + c.Vconfig = vconfig +} + +// Load loads the configuration from the disk +func (c *Configuration) Load() { + c.CreateViper() + vc := c.Vconfig + if err := vc.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + panic(fmt.Errorf("Fatal: No config file could be found: %w", err)) + } + panic(fmt.Errorf("Fatal error config file: %w ", err)) + } + log.Printf("Configuration file used: %s", vc.ConfigFileUsed()) + + //Unmarshal all into Configuration type + err := vc.Unmarshal(c) + if err != nil { + fmt.Printf("Error occured when loading config: %v\n", err) + panic("No configuration available!") + } + for k, v := range c.Backups { + log.Printf("Initializing backup '%s'\n", k) + v.Name = k + log.Printf("Initialized backup '%s'\n", v.Name) + } + for k, v := range c.Devices { + log.Printf("Initializing device '%s'\n", k) + v.Name = k + log.Printf("Initialized device '%s'\n", v.Name) + } +} diff --git a/config/config.go b/config/config.go deleted file mode 100644 index 2143202..0000000 --- a/config/config.go +++ /dev/null @@ -1,82 +0,0 @@ -package config - -import ( - "fmt" - - "github.com/qwc/backive/backup" - "github.com/qwc/backive/device" - - "github.com/spf13/viper" -) - -var ( - config *Configuration - vconfig *viper.Viper -) - -// Configuration struct holding the settings and config items of devices and backups -type Configuration struct { - Settings Settings `mapstructure:"settings"` - Devices Devices `mapstructure:"devices"` - Backups Backups `mapstructure:"backups"` -} - -// Settings struct holds the global configuration items -type Settings struct { - SystemMountPoint string `mapstructure:"systemMountPoint"` - UserMountPoint string `mapstructure:"userMountPoint"` -} - -// Devices is nothing else than a name to Device type mapping -type Devices map[string]device.Device - -// Backups is nothing else than a name to Backup type mapping -type Backups map[string]backup.Backup - -// CreateViper creates a viper instance for usage later -func CreateViper() *viper.Viper { - vconfig := viper.New() - vconfig.SetConfigName("backive") - vconfig.SetConfigType("yaml") - vconfig.AddConfigPath("/etc/backive/") // system config - vconfig.AddConfigPath("$HOME/.backive/") - vconfig.AddConfigPath(".") - return vconfig -} - -// Load loads the configuration from the disk -func Load() *Configuration { - vconfig := CreateViper() - if err := vconfig.ReadInConfig(); err != nil { - if _, ok := err.(viper.ConfigFileNotFoundError); ok { - panic(fmt.Errorf("Fatal: No config file could be found")) - } - panic(fmt.Errorf("Fatal error config file: %w ", err)) - } - - var cfg *Configuration - - //Unmarshal all into Configuration type - err := vconfig.Unmarshal(cfg) - if err != nil { - fmt.Printf("Error occured when loading config: %v\n", err) - panic("No configuration available!") - } - for k, v := range cfg.Backups { - v.Name = k - } - for k, v := range cfg.Devices { - v.Name = k - } - return cfg -} - -// Init Initializes the configuration -func Init() { - config = Load() -} - -// Get returns the Configuration global variable -func Get() *Configuration { - return config -} diff --git a/config/config_test.go b/config/config_test.go deleted file mode 100644 index 47a1ef4..0000000 --- a/config/config_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package config - -// test package for config - -import ( - "bytes" - "fmt" - "testing" - - "github.com/spf13/viper" -) - -func TestDummyConfig(t *testing.T) { - - v := viper.New() - v.SetConfigType("yaml") - var yamlExample = []byte(` -settings: - systemMountPoint: /media/backive - userMountPoint: $HOME/.backive/mounts -devices: - my_device: - uuid: 98237459872398745987 - owner: -backups: - my_backup: - targetDevice: my_device - targetDir: backive_backup - sourceDir: /home/user123/stuff - scriptPath: /path/to/script - frequency: 7 #weekly -`) - v.ReadConfig(bytes.NewBuffer(yamlExample)) - var theConfig Configuration - err := v.Unmarshal(&theConfig) - if err != nil { - fmt.Printf("Unable to decode into struct, %v \n", err) - panic("Failed!") - } - fmt.Printf("systemMountpoint is %v \n", theConfig) - - fmt.Printf("System Mountpoint is %v\n", v.Get("settings.systemmountpoint")) -} diff --git a/database.go b/database.go new file mode 100644 index 0000000..e5b78d1 --- /dev/null +++ b/database.go @@ -0,0 +1,44 @@ +package backive + +import ( + "encoding/json" + "log" + "os" + "path" +) + +// Database is a simple string to string mapping, where arbitrary strings can be stored and safed to disk or loaded +type Database struct { + data map[string]string +} + +var dbPath string = "/var/lib/backive/data.json" + +// Save saves the database +func (d *Database) Save() { + jsonstr, merr := json.Marshal(d.data) + if merr != nil { + panic(merr) + } + log.Printf("Writing database output to file: %s", jsonstr) + saveDir, _ := path.Split(dbPath) + CreateDirectoryIfNotExists(saveDir) + err := os.WriteFile(dbPath, []byte(jsonstr), 0644) + if err != nil { + panic(err) + } +} + +// LoadDb loads the database +func (d *Database) Load() { + if _, err := os.Stat(dbPath); err == nil { + data, rferr := os.ReadFile(dbPath) + if rferr != nil { + panic(rferr) + } + json.Unmarshal(data, &d.data) + } else if os.IsNotExist(err) { + // no data + + } +} diff --git a/db/db.go b/db/db.go deleted file mode 100644 index c79c137..0000000 --- a/db/db.go +++ /dev/null @@ -1,32 +0,0 @@ -package db - -import ( - "encoding/json" - "os" -) - -// Database is a simple string to string mapping, where arbitrary strings can be stored and safed to disk or loaded -var Database map[string]string -var path string = "/var/lib/backive/data.json" - -// Save saves the database -func Save() { - jsonstr, merr := json.Marshal(Database) - if merr != nil { - panic(merr) - } - - err := os.WriteFile(path, []byte(jsonstr), 0644) - if err != nil { - panic(err) - } -} - -// Load loads the database -func Load() { - data, err := os.ReadFile(path) - if err != nil { - panic(err) - } - json.Unmarshal(data, &Database) -} diff --git a/device.go b/device.go new file mode 100644 index 0000000..97472e8 --- /dev/null +++ b/device.go @@ -0,0 +1,74 @@ +package backive + +import ( + "log" + "os/exec" + "path" +) + +var devsByUuid string = "/dev/disk/by-uuid" + +// Devices is nothing else than a name to Device type mapping +type Devices map[string]*Device + +// Device represents a device, with a name easy to remember and the UUID to identify it, optionally an owner. +type Device struct { + Name string `mapstructure:",omitempty"` + UUID string `mapstructure:"uuid"` + OwnerUser string `mapstructure:"owner,omitempty"` + isMounted bool +} + +// Mount will mount a device +func (d *Device) Mount() error { + log.Printf("Mounting device %s, creating directory if it does not exist.\n", d.Name) + CreateDirectoryIfNotExists( + path.Join(config.Settings.SystemMountPoint, d.Name), + ) + //time.Sleep(3000 * time.Millisecond) + log.Printf("Executing mount command for %s", d.Name) + cmd := exec.Command( + "mount", + path.Join(devsByUuid, d.UUID), + path.Join(config.Settings.SystemMountPoint, d.Name), + ) + cmd.Stdout = log.Writer() + cmd.Stderr = log.Writer() + log.Printf("Command to execute: %s", cmd.String()) + err := cmd.Run() + if err != nil { + log.Printf("Mounting failed with error %v", err) + return err + } + d.isMounted = true + return nil +} + +// Unmount will unmount a device +func (d *Device) Unmount() error { + if d.isMounted { + log.Printf("Unmounting %s", d.Name) + sync := exec.Command("sync") + syncErr := sync.Run() + if syncErr != nil { + log.Println(syncErr) + return syncErr + } + cmd := exec.Command( + "umount", + path.Join(config.Settings.SystemMountPoint, d.Name), + ) + log.Printf("About to run: %s", cmd.String()) + err := cmd.Run() + if err != nil { + log.Println(err) + return err + } + d.isMounted = false + } + return nil +} + +func (d *Device) IsMounted() bool { + return d.isMounted +} diff --git a/device/device.go b/device/device.go deleted file mode 100644 index 2bd6ac4..0000000 --- a/device/device.go +++ /dev/null @@ -1,25 +0,0 @@ -package device - -// Device represents a device, with a name easy to remember and the UUID to identify it, optionally an owner. -type Device struct { - Name string `mapstructure:",omitempty"` - UUID string `mapstructure:"uuid"` - OwnerUser string `mapstructure:"owner,omitempty"` - isMounted bool -} - -// Mount will mount a device -func (d Device) Mount() { - - d.isMounted = true -} - -// Unmount will unmount a device -func (d Device) Unmount() { - - d.isMounted = false -} - -func (d Device) IsMounted() bool { - return d.isMounted -} diff --git a/events.go b/events.go new file mode 100644 index 0000000..e779b57 --- /dev/null +++ b/events.go @@ -0,0 +1,77 @@ +package backive + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net" + "path" +) + +type EventHandler struct { + ls net.Listener + done <-chan struct{} + callbacks []func(map[string]string) +} + +// Init initializes the unix socket. +func (eh *EventHandler) Init(socketPath string) { + log.Println("Initializing EventHandler...") + var err error + dir, _ := path.Split(socketPath) + CreateDirectoryIfNotExists(dir) + eh.ls, err = net.Listen("unix", socketPath) + if err != nil { + panic(err) + } + eh.callbacks = make([]func(map[string]string), 3) +} + +// Listen starts the event loop. +func (eh *EventHandler) Listen() { + log.Println("Running eventloop") + func() { + for { + eh.process() + } + }() +} + +// RegisterCallback adds a function to the list of callback functions for processing of events. +func (eh *EventHandler) RegisterCallback(cb func(map[string]string)) { + eh.callbacks = append(eh.callbacks, cb) +} + +// process processes each and every unix socket event, Unmarshals the json data and calls the list of callbacks. +func (eh *EventHandler) process() { + client, err := eh.ls.Accept() + log.Println("Accepted client") + if err != nil { + log.Fatal(err) + } + data := make([]byte, 2048) + for { + buf := make([]byte, 512) + nr, err := client.Read(buf) + if err != nil && err != io.EOF { + log.Fatal(err) + } + data = append(data, buf[0:nr]...) + if err == io.EOF { + break + } + } + sdata := string(bytes.Trim(data, "\x00")) + //log.Println(sdata) + env := map[string]string{} + errjson := json.Unmarshal([]byte(sdata), &env) + if errjson != nil { + log.Fatal(errjson) + } + for _, v := range eh.callbacks { + if v != nil { + v(env) + } + } +} diff --git a/events/events.go b/events/events.go deleted file mode 100644 index 6c3d92a..0000000 --- a/events/events.go +++ /dev/null @@ -1,63 +0,0 @@ -package events - -import ( - "encoding/json" - "io" - "net" -) - -var ls net.Listener -var done <-chan struct{} -var callbacks []func(map[string]string) - -// Init initializes the unix socket. -func Init(socketPath string) { - var err error - ls, err = net.Listen("unix", socketPath) - if err != nil { - panic(err) - } - callbacks = make([]func(map[string]string), 3) -} - -// Listen starts the event loop. -func Listen() { - for { - go func() { - process() - }() - } -} - -// RegisterCallback adds a function to the list of callback functions for processing of events. -func RegisterCallback(cb func(map[string]string)) { - callbacks = append(callbacks, cb) -} - -// process processes each and every unix socket event, Unmarshals the json data and calls the list of callbacks. -func process() { - client, err := ls.Accept() - if err != nil { - panic(err) - } - data := make([]byte, 2048) - for { - buf := make([]byte, 512) - nr, err := client.Read(buf) - if err != nil && err != io.EOF { - panic(err) - } - data = append(data, buf[0:nr]...) - if err == io.EOF { - break - } - } - env := map[string]string{} - errjson := json.Unmarshal(data, &env) - if errjson != nil { - panic(errjson) - } - for _, v := range callbacks { - v(env) - } -} diff --git a/go.mod b/go.mod index fd1777b..8e81220 100644 --- a/go.mod +++ b/go.mod @@ -22,3 +22,5 @@ require ( gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/qwc/backive => ./ diff --git a/runner/runner.go b/runner/runner.go deleted file mode 100644 index 767ebfe..0000000 --- a/runner/runner.go +++ /dev/null @@ -1,42 +0,0 @@ -package runner - -import ( - "fmt" - "os" - - "github.com/qwc/backive/backup" - "github.com/qwc/backive/config" -) - -// Run runs the backup script with appropriate rights. -func Run(b backup.Backup) error { - cfg := config.Get() - if cfg.Devices[b.Name].IsMounted() { - checkExistence := func(path string, name string) error { - if _, err := os.Stat(path); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("%s does not exist", name) - } else { - return fmt.Errorf("Error when checking %s: %w", name, err) - } - } - return nil - } - // Check for existence of target dir - if err := checkExistence(b.TargetDir, "target directory"); err != nil { - return err - } - // check for existence of source dir - if err := checkExistence(b.SourceDir, "source directory"); err != nil { - return err - } - // check for existence of script path - if err := checkExistence(b.ScriptPath, "script path"); err != nil { - return err - } - // setup script environment including user to use - // run script - } - // quit with error that the device is not available. - return fmt.Errorf("The device is not mounted") -} diff --git a/scheduler/scheduler.go b/scheduler/scheduler.go deleted file mode 100644 index 38b66d5..0000000 --- a/scheduler/scheduler.go +++ /dev/null @@ -1,76 +0,0 @@ -package scheduler - -import ( - "container/list" - "encoding/json" - "fmt" - "time" - - "github.com/qwc/backive/config" - "github.com/qwc/backive/db" -) - -type backupRuns struct { - runlist *list.List -} - -// Runs contains the Data for the scheduler: mapping from backups to a list of timestamps of the last 10 backups -type Runs map[string]backupRuns - -var runs Runs - -// Load loads the data from the json database -func Load() { - runerr := json.Unmarshal([]byte(db.Database["runs"]), &runs) - if runerr != nil { - panic(runerr) - } -} - -// Save saves the data into the json database -func Save() { - str, err := json.Marshal(runs) - if err != nil { - panic(err) - } - db.Database["runs"] = string(str) -} - -// ShouldRun Takes a backup key and returns a bool if a backup should run now. -func ShouldRun(backup string) bool { - backupdata := config.Get().Backups[backup] - freq := backupdata.Frequency - // calculate time difference from last run, return true if no run has taken place - lr, ok := LastRun(backup) - if ok == nil { - dur := time.Since(lr) - days := dur.Hours() / 24 - if days > float64(freq) { - return true - } - } - if freq == 0 { - return true - } - return false -} - -// RegisterRun saves a date of a backup run into the internal storage -func RegisterRun(backup string) { - nbl, ok := runs[backup] - if !ok { - nbl.runlist = list.New() - runs[backup] = nbl - } - nbl.runlist.PushFront(time.Now()) -} - -// LastRun returns the time.Time of the last run of the backup given. -func LastRun(backup string) (time.Time, error) { - _, ok := runs[backup] - if ok { - var t = time.Time(runs[backup].runlist.Front().Value.(time.Time)) - return t, nil - } - return time.Unix(0, 0), fmt.Errorf("Backup name not found and therefore has never run") -} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..34909e4 --- /dev/null +++ b/utils.go @@ -0,0 +1,16 @@ +package backive + +import ( + "log" + "os" +) + +func CreateDirectoryIfNotExists(dir string) { + if _, err := os.Stat(dir); err == nil { + //ignore + } else if os.IsNotExist(err) { + os.MkdirAll(dir, 0755) + } else { + log.Fatal(err) + } +}