Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add backup cleanup codez #789

Merged
merged 4 commits into from
Jul 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ linters:
- dupl # Maybe one day we can reduce the duplicate code with generics.
- nlreturn # Not needed because wsl is good enough; better actually.
- godot # Does not work with annotations.
- musttag # Broken in 1.59: https://github.com/go-simpler/musttag/issues/98
- depguard # Not even sure why this is useful. We have too many deps to care.
run:
timeout: 5m
Expand Down
1 change: 1 addition & 0 deletions pkg/client/handlers_gui.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ func (c *Client) mergeAndValidateNewConfig(config *configfile.Config, request *h
config.Commands = nil
config.Service = nil
config.Snapshot.Plugins.MySQL = nil
config.Snapshot.Plugins.Nvidia = nil

// for k, v := range request.PostForm {
// c.Errorf("Config Post: %s = %+v", k, v)
Expand Down
2 changes: 1 addition & 1 deletion pkg/logs/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const (
// LogConfig allows sending logs to rotating files.
// Setting an AppName will force log creation even if LogFile and HTTPLog are empty.
type LogConfig struct {
AppName string `json:"-"`
AppName string `json:"-" toml:"-" xml:"-" yaml:"-"`
LogFile string `json:"logFile" toml:"log_file" xml:"log_file" yaml:"logFile"`
DebugLog string `json:"debugLog" toml:"debug_log" xml:"debug_log" yaml:"debugLog"`
HTTPLog string `json:"httpLog" toml:"http_log" xml:"http_log" yaml:"httpLog"`
Expand Down
14 changes: 7 additions & 7 deletions pkg/snapshot/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import (

// MySQLConfig allows us to gather a process list for the snapshot.
type MySQLConfig struct {
Name string `toml:"name" xml:"name"`
Host string `toml:"host" xml:"host"`
User string `toml:"user" xml:"user"`
Pass string `toml:"pass" xml:"pass"`
Timeout cnfg.Duration `toml:"timeout" xml:"timeout"`
// only used by service checks, snapshot interval is used for mysql.
Interval cnfg.Duration `toml:"interval" xml:"interval"`
Name string `json:"name" toml:"name" xml:"name"`
Host string `json:"host" toml:"host" xml:"host"`
User string `json:"-" toml:"user" xml:"user"`
Pass string `json:"-" toml:"pass" xml:"pass"`
Timeout cnfg.Duration `json:"timeout" toml:"timeout" xml:"timeout"`
// Only used by service checks, snapshot interval is used for mysql.
Interval cnfg.Duration `json:"interval" toml:"interval" xml:"interval"`
}

// MySQLProcesses allows us to manipulate our list with methods.
Expand Down
4 changes: 1 addition & 3 deletions pkg/snapshot/nvidia.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ func (s *Snapshot) GetNvidia(ctx context.Context, config *NvidiaConfig) error {
return fmt.Errorf("unable to locate nvidia-smi at provided path '%s': %w", cmdPath, err)
}
} else if cmdPath, err = exec.LookPath(nvidiaSMIname()); err != nil {
// do not throw an error if nvidia-smi is missing.
// return fmt.Errorf("nvidia-smi missing! %w", err)
return nil
return fmt.Errorf("nvidia-smi missing from PATH! %w", err)
}

cmd := exec.CommandContext(ctx, cmdPath, "--format=csv,noheader", "--query-gpu="+
Expand Down
1 change: 0 additions & 1 deletion pkg/snapshot/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ type Config struct {
IPMI bool `json:"ipmi" toml:"ipmi" xml:"ipmi"` // get ipmi sensor info.
IPMISudo bool `json:"ipmiSudo" toml:"ipmiSudo" xml:"ipmiSudo"` // use sudo to get ipmi sensor info.
Plugins
// Debug bool `toml:"debug" xml:"debug" json:"debug"`
}

// Plugins is optional configuration for "plugins".
Expand Down
2 changes: 1 addition & 1 deletion pkg/triggers/autoupdate/autoupdate.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func (c *cmd) updateNow(ctx context.Context, u *update.Update, msg website.Event

cmd := &update.Command{
URL: u.CurrURL,
Logger: c.Logger.DebugLog,
Logger: c.Logger,
Args: []string{"--restart", "--config", c.ConfigFile},
Path: os.Args[0],
}
Expand Down
66 changes: 66 additions & 0 deletions pkg/update/cleanup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package update

import (
"os"
"path/filepath"
"sort"
"strings"
"time"

"github.com/Notifiarr/notifiarr/pkg/mnd"
)

const keepBackups = 3

type file struct {
name string
when time.Time
}

type fileList []file

func (f fileList) Len() int {
return len(f)
}

func (f fileList) Less(i, j int) bool {
return f[i].when.UnixMicro() < f[j].when.UnixMicro()
}

func (f fileList) Swap(i, j int) {
f[i], f[j] = f[j], f[i]
}

func (u *Command) cleanOldBackups() {
dir := filepath.Dir(u.Path)
pfx := strings.TrimSuffix(filepath.Base(u.Path), dotExe) + ".backup."

entries, err := os.ReadDir(dir)
if err != nil {
return
}

consider := fileList{}

for _, entry := range entries {
if !strings.HasPrefix(entry.Name(), pfx) || len(entry.Name()) < len(pfx)+len(backupTimeFormat) {
continue
}

date := strings.TrimSuffix(strings.TrimPrefix(entry.Name(), pfx), dotExe)
if when, err := time.Parse(backupTimeFormat, date); err == nil {
consider = append(consider, file{name: entry.Name(), when: when})
}
}

sort.Sort(consider)

for idx, file := range consider {
if len(consider)-idx <= keepBackups {
return
}

err := os.Remove(filepath.Join(dir, file.name))
u.Printf("[UPDATE] Deleted old backup file: %s%s (error: %v)", file.name, mnd.DurationAge(file.when), err)
}
}
47 changes: 24 additions & 23 deletions pkg/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
Expand All @@ -25,14 +24,18 @@ import (
"github.com/Notifiarr/notifiarr/pkg/mnd"
)

const downloadTimeout = 5 * time.Minute
const (
downloadTimeout = 5 * time.Minute
backupTimeFormat = "060102T150405"
dotExe = ".exe"
)

// Command is the input data to perform an in-place update.
type Command struct {
URL string // file to download.
Path string // file to be updated.
Args []string // optional, but non-nil will crash.
*log.Logger // debug logs.
URL string // file to download.
Path string // file to be updated.
Args []string // optional, but non-nil will crash.
mnd.Logger // debug logs.
}

// Errors. Don't trigger these.
Expand All @@ -42,11 +45,13 @@ var (
)

// Restart is meant to be called from a special flag that reloads the app after an upgrade.
func Restart(u *Command) error {
func Restart(cmd *Command) error {
// A small pause to give the parent time to exit.
time.Sleep(time.Second)
// A small pause to give the new app time to fork.
defer time.Sleep(time.Second)

if err := exec.Command(u.Path, u.Args...).Start(); err != nil { //nolint:gosec
if err := exec.Command(cmd.Path, cmd.Args...).Start(); err != nil { //nolint:gosec
return fmt.Errorf("executing command %w", err)
}

Expand Down Expand Up @@ -109,30 +114,26 @@ func (u *Command) replaceFile(ctx context.Context) (string, error) {
}

suff := ""
if strings.HasSuffix(u.Path, ".exe") {
suff = ".exe"
if strings.HasSuffix(u.Path, dotExe) {
suff = dotExe
}

backupFile := strings.TrimSuffix(u.Path, ".exe")
backupFile += ".backup." + time.Now().Format("060102T150405") + suff
u.Printf("[UPDATE] Renaming %s => %s", u.Path, backupFile)
backupFile := strings.TrimSuffix(u.Path, dotExe)
backupFile += ".backup." + time.Now().Format(backupTimeFormat) + suff
u.Debugf("[UPDATE] Renaming %s => %s", u.Path, backupFile)

if err := os.Rename(u.Path, backupFile); err != nil {
return backupFile, fmt.Errorf("renaming original file: %w", err)
}

u.Printf("[UPDATE] Renaming %s => %s", tempFile, u.Path)
u.Debugf("[UPDATE] Renaming %s => %s", tempFile, u.Path)

u.cleanOldBackups()

if err := os.Rename(tempFile, u.Path); err != nil {
return backupFile, fmt.Errorf("renaming downloaded file: %w", err)
}
/* // Hack used for testing.
u.Printf("[UPDATE] Renaming [HACK] %s => %s", backupFile, u.Path)

if err := os.Rename(backupFile, u.Path); err != nil {
return backupFile, fmt.Errorf("renaming downloaded file %w", err)
}
/**/
return backupFile, nil
}

Expand All @@ -143,7 +144,7 @@ func (u *Command) writeFile(ctx context.Context, folderPath string) (string, err
}
defer tempFile.Close()

u.Printf("[UPDATE] Primed Temp File: %s", tempFile.Name())
u.Debugf("[UPDATE] Primed Temp File: %s", tempFile.Name())

req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.URL, nil)
if err != nil {
Expand Down Expand Up @@ -211,7 +212,7 @@ func (u *Command) writeZipFile(tempFile *os.File, body []byte, size int64) error

// Find the exe file and write that.
for _, zipFile := range zipReader.File {
if runtime.GOOS == mnd.Windows && strings.HasSuffix(zipFile.Name, ".exe") {
if runtime.GOOS == mnd.Windows && strings.HasSuffix(zipFile.Name, dotExe) {
zipOpen, err := zipFile.Open()
if err != nil {
return fmt.Errorf("reading zipped exe file: %w", err)
Expand All @@ -226,7 +227,7 @@ func (u *Command) writeZipFile(tempFile *os.File, body []byte, size int64) error
}
}

u.Println("[UPDATE] exe file not found in zip file")
u.Errorf("[UPDATE] exe file not found in zip file")

return nil
}