Compare commits

..

45 Commits

Author SHA1 Message Date
shoopea
3aa0a852a2 fix wildcard 2025-12-28 22:17:53 +01:00
shoopea
54d85767a7 test wildcard 2025-12-28 22:12:49 +01:00
shoopea
f47ddd1873 more debug 2025-12-28 22:06:06 +01:00
shoopea
ad88d9fe88 test wildcards 2025-12-28 21:58:06 +01:00
shoopea
fc81c38ffd api app source 2025-12-28 21:54:21 +01:00
shoopea
fd1c14831f more api call 2025-12-28 19:27:10 +01:00
shoopea
c8715679f8 sanity check when creating app 2025-12-28 16:41:51 +01:00
shoopea
ee94b077b4 really fix lock 2025-12-28 16:32:44 +01:00
shoopea
d92380cccb fix lock 2025-12-28 16:31:28 +01:00
shoopea
a1ed1035e9 fix cfg lock 2025-12-28 16:20:37 +01:00
shoopea
6a0c8006d6 app activate/deactivate 2025-12-28 16:15:25 +01:00
shoopea
1890050a30 app add/del 2025-12-27 22:00:19 +01:00
shoopea
3e867da45f Api App 2025-12-27 19:35:54 +01:00
shoopea
a1ba422429 add snapshots list api 2025-10-26 10:32:53 +01:00
shoopea
05054be795 add custom snapshot 2025-10-19 21:40:30 +02:00
shoopea
73f9551c8f update locks 2025-10-19 14:12:31 +02:00
shoopea
ace13b68a8 implement app run 2025-10-19 13:55:52 +02:00
shoopea
6ae863b60c app run skeleton 2025-10-19 13:33:22 +02:00
shoopea
2aca8b1ceb add app.boxes 2025-10-12 15:33:00 +02:00
shoopea
3c7838b10a add config app 2025-10-07 20:56:21 +02:00
shoopea
fd02cfdfc3 bump version 2025-10-01 22:30:51 +02:00
shoopea
6cf998997e simple api 2025-10-01 22:28:48 +02:00
shoopea
963fd34724 csrf token 2024-11-17 23:42:22 +01:00
shoopea
a31ff56055 fix 2024-11-17 23:40:48 +01:00
shoopea
060933aa27 test tokens 2024-11-17 23:37:42 +01:00
shoopea
3bd57a4e98 remove test 2024-11-17 19:21:17 +01:00
shoopea
b83b0e8c4f reverse 2024-11-17 18:48:52 +01:00
shoopea
6ed38972e7 test 2024-11-17 18:45:55 +01:00
shoopea
757b6b1627 bump ssh 2024-11-17 18:45:06 +01:00
shoopea
376da04727 test 2024-11-17 18:36:38 +01:00
shoopea
9a86d82460 ssh cmd debug 2024-11-17 18:16:35 +01:00
shoopea
ad1a8decc3 ssh setenv fail 2024-11-17 18:15:50 +01:00
shoopea
d217ba310e recover 2024-11-17 17:27:47 +01:00
shoopea
49417729ec fix home 2024-11-17 17:26:16 +01:00
shoopea
d8cfb3cf00 update 2024-11-17 17:02:38 +01:00
shoopea
cd94f48b8a simplify 2024-11-17 17:00:52 +01:00
shoopea
9a9db972a7 omitempty 2024-11-17 16:50:04 +01:00
shoopea
86da86684b prettier 2024-11-17 16:41:46 +01:00
shoopea
6fcf8b421c fix config save 2024-11-17 16:39:00 +01:00
shoopea
e7af7ce2fd add caller info 2024-11-17 16:34:13 +01:00
shoopea
3bcd6664a6 add saving cfg 2024-11-17 16:25:39 +01:00
shoopea
e1806fd27a cleanup and add a few stuff for http 2024-11-17 15:14:36 +01:00
shoopea
26c324c43c fix out of bounds miscomputation 2023-09-10 18:43:14 +02:00
shoopea
2ca0695a1f fix zfs send for expired snapshots 2023-09-10 18:38:53 +02:00
shoopea
3b1d0fc850 prepare indirect zfs transfer and multi-transfer 2023-08-22 13:23:48 +02:00
61 changed files with 1225 additions and 59342 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
config.json config.json
backup backup
backup.json

16
addr.go
View File

@@ -84,7 +84,7 @@ func (a Addr) ValidSnapshots() ([]*ZfsSnapshot, error) {
} }
} }
func (a Addr) SetManaged(val bool) error { func (a Addr) SetManaged(managed bool) error {
log.WithFields(log.Fields{"addr": a}).Debugf("starting") log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done") defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
@@ -97,11 +97,11 @@ func (a Addr) SetManaged(val bool) error {
log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("") log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("")
return err return err
} else { } else {
fs.mx.Lock() fs.Lock()
defer fs.mx.Unlock() defer fs.Unlock()
if fs.managed != val { if fs.managed != managed {
var cmd string var cmd string
if val { if managed {
cmd = fmt.Sprintf("zfs set %s=+ %s", zfsManagedPropertyName, a.Path()) cmd = fmt.Sprintf("zfs set %s=+ %s", zfsManagedPropertyName, a.Path())
} else { } else {
cmd = fmt.Sprintf("zfs set %s=- %s", zfsManagedPropertyName, a.Path()) cmd = fmt.Sprintf("zfs set %s=- %s", zfsManagedPropertyName, a.Path())
@@ -111,7 +111,7 @@ func (a Addr) SetManaged(val bool) error {
return err return err
} }
} }
fs.managed = val fs.managed = managed
return nil return nil
} }
} }
@@ -129,8 +129,8 @@ func (a Addr) SetBackedUp(val bool) error {
log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("") log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("")
return err return err
} else { } else {
fs.mx.Lock() fs.Lock()
defer fs.mx.Unlock() defer fs.Unlock()
fs.backedUp = val fs.backedUp = val
return nil return nil
} }

View File

@@ -3,8 +3,6 @@ package main
import ( import (
"context" "context"
"embed" "embed"
"html/template"
"io/fs"
"net/http" "net/http"
"os/signal" "os/signal"
"syscall" "syscall"
@@ -12,25 +10,13 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"github.com/sethvargo/go-password/password"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type AdminConfig struct { type AdminConfig struct {
Users []*User `json:"users"`
Secrets *SecretsConfig `json:"secrets"`
Addr string `json:"addr"` Addr string `json:"addr"`
} }
type SecretsConfig struct {
PasswordPepper string `json:"password_pepper"`
ContextKey string `json:"context_key"`
ContextExpiration int `json:"context_expiration"`
ScryptN int `json:"scrypt_n"`
ScryptR int `json:"scrypt_r"`
ScryptP int `json:"scrypt_p"`
}
//go:embed assets //go:embed assets
var assets embed.FS var assets embed.FS
@@ -40,45 +26,11 @@ func NewAdmin() *AdminConfig {
a := &AdminConfig{ a := &AdminConfig{
Addr: "0.0.0.0:8080", Addr: "0.0.0.0:8080",
Secrets: NewSecrets(),
Users: make([]*User, 0),
} }
return a return a
} }
func NewSecrets() *SecretsConfig {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
pepper, _ := password.Generate(20, 5, 0, false, false)
ctx, _ := password.Generate(20, 5, 0, false, false)
return &SecretsConfig{
PasswordPepper: pepper,
ContextKey: ctx,
ContextExpiration: 3600,
ScryptN: 32768,
ScryptR: 8,
ScryptP: 1,
}
}
func (a *AdminConfig) NewAdminUser() {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
p, _ := password.Generate(20, 5, 0, false, false)
u, _ := NewUser("admin", p)
a.Users = append(a.Users, u)
log.WithFields(log.Fields{}).Warnf("Admin user password : %s", p)
return
}
func (a *AdminConfig) Run() { func (a *AdminConfig) Run() {
log.WithFields(log.Fields{}).Debugf("starting") log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done") defer log.WithFields(log.Fields{}).Debugf("done")
@@ -93,38 +45,33 @@ func (a *AdminConfig) Run() {
r := gin.Default() r := gin.Default()
if t, err := template.ParseFS(assets, "assets/templates/*.html"); err != nil { r.GET("/run", ApiRun)
log.WithFields(log.Fields{"call": "template.ParseFS", "error": err}).Errorf("") r.GET("/run/:app", ApiRunApp)
return
} else {
r.SetHTMLTemplate(t)
}
r.GET("/", HttpAnyIndex) r.GET("/snapshot/add/:app/:schedule", ApiSnapshotAdd)
r.POST("/", HttpAnyIndex) r.GET("/snapshot/del/:app/:name", ApiSnapshotDel)
r.GET("/snapshot/list/:app", ApiSnapshotList)
r.GET("/ping", func(c *gin.Context) { r.GET("/schedule/list", ApiScheduleList)
c.JSON(http.StatusOK, gin.H{ r.GET("/schedule/add/:name/:duration", ApiScheduleAdd)
"message": "pong", r.GET("/schedule/del/:name", ApiScheduleDel)
})
})
r.GET("/run", func(c *gin.Context) { r.GET("/save", ApiSave)
cfg.Run()
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
})
fsys, _ := fs.Sub(assets, "assets/static") r.GET("/config", ApiConfig)
r.StaticFS("/assets", http.FS(fsys)) r.GET("/config/:app", ApiConfigApp)
protected := r.Group("p", HttpAuth()) r.GET("/app/list", ApiAppList)
protected.GET("test", HttpAnyHome) r.GET("/app/list/schedule/:schedule", ApiListSchedule)
r.GET("/app/add/:app", ApiAppAdd)
r.GET("/app/del/:app", ApiAppDel)
unprotected := r.Group("u", HttpNoAuth()) r.GET("/app/activate/:app", ApiAppActivate)
unprotected.GET("signin", HttpAnySignIn) r.GET("/app/deactivate/:app", ApiAppDeactivate)
unprotected.POST("signin", HttpAnySignIn)
r.GET("/app/:app/source/list", ApiAppSourceList)
r.GET("/app/:app/source/add/*src", ApiAppSourceAdd)
r.GET("/app/:app/source/del/*src", ApiAppSourceDel)
srv := &http.Server{ srv := &http.Server{
Addr: a.Addr, Addr: a.Addr,
@@ -138,7 +85,7 @@ func (a *AdminConfig) Run() {
}() }()
c := cron.New(cron.WithLocation(time.UTC)) c := cron.New(cron.WithLocation(time.UTC))
if _, err := c.AddFunc("0 * * * *", func() { cfg.Run() }); err != nil { if _, err := c.AddFunc("0 * * * *", func() { cfg.Run(true) }); err != nil {
log.WithFields(log.Fields{"call": "cron.AddFunc", "error": err}).Errorf("") log.WithFields(log.Fields{"call": "cron.AddFunc", "error": err}).Errorf("")
} }
c.Start() c.Start()

636
api.go Normal file
View File

@@ -0,0 +1,636 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"slices"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/tidwall/pretty"
)
func ApiRun(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
cfg.Run(true)
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
func ApiSnapshotAdd(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
if app, ok := cfg.apps[c.Param("app")]; ok {
schedule := c.Param("schedule")
if _, ok := app.schedule[schedule]; ok {
if err := app.RunStandaloneSchedule(schedule); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
} else {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "no schedule found",
})
}
} else {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "no app found",
})
}
}
func ApiSnapshotDel(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "not implemented",
})
}
func ApiSnapshotList(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
if app, ok := cfg.apps[c.Param("app")]; ok {
if snapshots, err := app.Snapshots(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "done",
"snapshots": snapshots,
})
}
} else {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "no app found",
})
}
}
// FIXME
func ApiScheduleAdd(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "not implemented",
})
}
// FIXME
func ApiScheduleDel(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "not implemented",
})
}
// FIXME
func ApiScheduleList(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "not implemented",
})
}
func ApiRunApp(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
if app, ok := cfg.apps[c.Param("app")]; ok {
if err := app.RunStandaloneTime(time.Now()); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
} else {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "no app found",
})
}
}
func ApiSave(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
if err := cfg.Save(true); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
} else {
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
}
func ApiConfig(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
if b, err := cfg.Pretty(true); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
} else {
c.Data(http.StatusOK, "application/json", b)
}
}
func ApiConfigApp(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
name := c.Param("app")
found := false
CfgLock()
defer CfgUnlock()
for _, app := range cfg.Apps {
if app.Name == name {
found = true
if b, err := app.Pretty(false); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
} else {
c.Data(http.StatusOK, "application/json", b)
}
}
}
if !found {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "no app found",
})
}
}
func ApiAppList(c *gin.Context) {
log.WithFields(log.Fields{}).Debugf("starting")
log.WithFields(log.Fields{}).Debugf("done")
list := make([]string, 0)
CfgLock()
defer CfgUnlock()
for _, app := range cfg.Apps {
list = append(list, app.Name)
}
slices.Sort(list)
b, err := json.Marshal(list)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.Data(http.StatusOK, "application/json", pretty.PrettyOptions(b, &pretty.Options{Indent: " "}))
}
func ApiListSchedule(c *gin.Context) {
log.WithFields(log.Fields{"schedule": c.Param("schedule")}).Debugf("starting")
log.WithFields(log.Fields{"schedule": c.Param("schedule")}).Debugf("done")
name := c.Param("schedule")
list := make([]string, 0)
CfgLock()
defer CfgUnlock()
for _, app := range cfg.Apps {
for _, sched := range app.Schedule {
if sched == name && !slices.Contains(list, app.Name) {
list = append(list, app.Name)
}
}
}
slices.Sort(list)
b, err := json.Marshal(list)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.Data(http.StatusOK, "application/json", pretty.PrettyOptions(b, &pretty.Options{Indent: " "}))
}
func ApiAppAdd(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
name := c.Param("app")
found := false
CfgLock()
defer CfgUnlock()
for _, app := range cfg.Apps {
if app.Name == name {
found = true
}
}
if found {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "app already exist",
})
return
}
app := &AppConfig{
Name: name,
Active: false,
}
cfg.Apps = append(cfg.Apps, app)
err := cfg.Save(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
func ApiAppDel(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
name := c.Param("app")
found := false
CfgLock()
defer CfgUnlock()
for id, app := range cfg.Apps {
if app.Name == name {
if app.Active {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "app still active",
})
return
}
found = true
cfg.Apps[id] = cfg.Apps[len(cfg.Apps)-1]
cfg.Apps = cfg.Apps[:len(cfg.Apps)-1]
}
}
if !found {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "no app found",
})
return
}
err := cfg.Save(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
func ApiAppActivate(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
name := c.Param("app")
CfgLock()
defer CfgUnlock()
for _, app := range cfg.Apps {
if app.Name == name {
if app.Active {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "app already active",
})
return
}
a, err := cfg.NewApp(app.Name, app.Sources, app.Destinations, app.Schedule, app.Before, app.After)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
cfg.apps[app.Name] = a
}
}
err := cfg.Save(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
func ApiAppDeactivate(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
name := c.Param("app")
CfgLock()
defer CfgUnlock()
if _, ok := cfg.apps[name]; ok {
delete(cfg.apps, name)
} else {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "app is not active",
})
return
}
for _, app := range cfg.Apps {
if app.Name == name {
app.Active = false
}
}
err := cfg.Save(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
func ApiAppSourceList(c *gin.Context) {
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("starting")
log.WithFields(log.Fields{"app": c.Param("app")}).Debugf("done")
name := c.Param("app")
found := false
app := &AppConfig{}
CfgLock()
defer CfgUnlock()
for _, a := range cfg.Apps {
if a.Name == name {
found = true
app = a
}
}
if !found {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "app does not exist",
})
return
}
b, err := json.Marshal(app.Sources)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.Data(http.StatusOK, "application/json", pretty.PrettyOptions(b, &pretty.Options{Indent: " "}))
}
func ApiAppSourceAdd(c *gin.Context) {
app := c.Param("app")
src := c.Param("src")
src = src[1:] // cut first char from wildcard match
log.WithFields(log.Fields{"app": app, "src": src}).Debugf("starting")
log.WithFields(log.Fields{"app": app, "src": src}).Debugf("done")
found := false
ac := &AppConfig{}
CfgLock()
defer CfgUnlock()
for _, a := range cfg.Apps {
if a.Name == app {
found = true
ac = a
}
}
if !found {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "app does not exist",
})
return
}
s := Addr(src)
if s.Box() == "" {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "source box incorrect",
})
return
}
if _, ok := cfg.box[s.Box()]; !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "source box doesn't exist",
})
return
}
if s.Path() == "" {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "source path incorrect",
})
return
}
if slices.Contains(ac.Sources, s.String()) {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "source already exists",
})
return
}
ac.Sources = append(ac.Sources, s.String())
if ac.Active {
a, err := cfg.NewApp(ac.Name, ac.Sources, ac.Destinations, ac.Schedule, ac.Before, ac.After)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
cfg.apps[a.name] = a
}
err := cfg.Save(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}
func ApiAppSourceDel(c *gin.Context) {
app := c.Param("app")
src := c.Param("src")
src = src[1:] // cut first char from wildcard match
log.WithFields(log.Fields{"app": app, "src": src}).Debugf("starting")
log.WithFields(log.Fields{"app": app, "src": src}).Debugf("done")
found := false
ac := &AppConfig{}
CfgLock()
defer CfgUnlock()
for _, a := range cfg.Apps {
if a.Name == app {
found = true
ac = a
}
}
if !found {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "app does not exist",
})
return
}
if !slices.Contains(ac.Sources, src) {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": "source does not exist",
})
return
}
for id, s := range ac.Sources {
if s == src {
ac.Sources[id] = ac.Sources[len(ac.Sources)-1]
ac.Sources = ac.Sources[:len(ac.Sources)-1]
}
}
if ac.Active {
a, err := cfg.NewApp(ac.Name, ac.Sources, ac.Destinations, ac.Schedule, ac.Before, ac.After)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
cfg.apps[a.name] = a
}
err := cfg.Save(false)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"message": "error",
"error": fmt.Sprint(err),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
}

283
app.go
View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"regexp" "regexp"
"strings" "strings"
"sync"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -22,6 +23,30 @@ func (c *Config) NewApp(name string, sources, destinations, schedule []string, b
log.WithFields(log.Fields{"name": name}).Debugf("starting") log.WithFields(log.Fields{"name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name}).Debugf("done") defer log.WithFields(log.Fields{"name": name}).Debugf("done")
if _, ok := c.apps[name]; ok {
err := errors.New("app already exist")
log.WithFields(log.Fields{"app": name, "error": err}).Errorf("")
return nil, err
}
if len(sources) == 0 {
err := errors.New("no sources")
log.WithFields(log.Fields{"app": name, "error": err}).Errorf("")
return nil, err
}
if len(destinations) == 0 {
err := errors.New("no destinations")
log.WithFields(log.Fields{"app": name, "error": err}).Errorf("")
return nil, err
}
if len(schedule) == 0 {
err := errors.New("no schedule")
log.WithFields(log.Fields{"app": name, "error": err}).Errorf("")
return nil, err
}
a := &App{ a := &App{
name: name, name: name,
sources: make([]Addr, 0), sources: make([]Addr, 0),
@@ -258,6 +283,11 @@ func (a *App) RunSchedule(schedule string, now time.Time) error {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("starting") log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("done") defer log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now}).Debugf("done")
if err := a.SanityCheck(); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "call": "SanityCheck", "error": err}).Errorf("")
return err
}
snapshotName := SnapshotName(schedule, now) snapshotName := SnapshotName(schedule, now)
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "snapshot": snapshotName}).Debugf("snapshot name") log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "snapshot": snapshotName}).Debugf("snapshot name")
@@ -277,19 +307,31 @@ func (a *App) RunSchedule(schedule string, now time.Time) error {
log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "RunAfter", "attr": schedule, "error": err}).Errorf("") log.WithFields(log.Fields{"app": a.name, "schedule": schedule, "now": now, "call": "RunAfter", "attr": schedule, "error": err}).Errorf("")
} }
for _, src := range a.sources {
if err := src.SetManaged(true); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "src.SetManaged", "error": err}).Errorf("")
return err
}
}
if err := a.Transfer(); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "Transfer", "error": err}).Errorf("")
return err
}
if err := a.Cleanup(now); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "Cleanup", "error": err}).Errorf("")
return err
}
return nil return nil
} }
func (a *App) Run(now time.Time) (string, error) { func (a *App) RunTime(now time.Time) (string, error) {
log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("starting") log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("done") defer log.WithFields(log.Fields{"app": a.name, "now": now}).Debugf("done")
if err := a.SanityCheck(); err != nil {
log.WithFields(log.Fields{"app": a.name, "now": now, "call": "SanityCheck", "error": err}).Errorf("")
return "", err
}
schedule, err := a.NextSchedule(now) schedule, err := a.NextSchedule(now)
if err != nil { if err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "NextSchedule", "error": err}).Errorf("") log.WithFields(log.Fields{"app": a.name, "call": "NextSchedule", "error": err}).Errorf("")
@@ -297,31 +339,7 @@ func (a *App) Run(now time.Time) (string, error) {
} }
log.WithFields(log.Fields{"app": a.name, "now": now, "schedule": schedule}).Debugf("schedule") log.WithFields(log.Fields{"app": a.name, "now": now, "schedule": schedule}).Debugf("schedule")
if schedule != "" { return schedule, a.RunSchedule(schedule, now)
if err := a.RunSchedule(schedule, now); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "NextSchedule", "error": err}).Errorf("")
return "", err
}
}
for _, src := range a.sources {
if err := src.SetManaged(true); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "src.SetManaged", "error": err}).Errorf("")
return "", err
}
}
if err := a.Transfer(); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "Transfer", "error": err}).Errorf("")
return "", err
}
if err := a.Cleanup(now); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "Cleanup", "error": err}).Errorf("")
return "", err
}
return schedule, nil
} }
func (a *App) NextSchedule(now time.Time) (string, error) { func (a *App) NextSchedule(now time.Time) (string, error) {
@@ -478,7 +496,7 @@ func (a *App) Transfer() error {
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done") defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
for _, src := range a.sources { for _, src := range a.sources {
backedUp := false dests := make([]Addr, 0)
for _, dest := range a.destinations { for _, dest := range a.destinations {
dest2 := dest.Append("/" + src.Box() + "/" + src.Path()) dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
if dest2.Online() { if dest2.Online() {
@@ -486,18 +504,17 @@ func (a *App) Transfer() error {
log.WithFields(log.Fields{"app": a.name, "call": "Mkdir", "attr": dest, "error": err}).Errorf("") log.WithFields(log.Fields{"app": a.name, "call": "Mkdir", "attr": dest, "error": err}).Errorf("")
return err return err
} }
if err := TransferZfs(src, dest2); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "TransferZfs", "src": src, "dest": dest, "error": err}).Errorf("")
return err
}
if err := dest2.SetManaged(true); err != nil { if err := dest2.SetManaged(true); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "SetManaged", "src": src, "dest": dest, "error": err}).Errorf("") log.WithFields(log.Fields{"app": a.name, "call": "SetManaged", "src": src, "dest": dest, "error": err}).Errorf("")
return err return err
} }
backedUp = true dests = append(dests, dest2)
} }
} }
if backedUp { if n, err := TransferZfs(src, dests); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "TransferZfs", "src": src, "dests": dests, "error": err}).Errorf("")
return err
} else if n > 0 {
if err := src.SetBackedUp(true); err != nil { if err := src.SetBackedUp(true); err != nil {
log.WithFields(log.Fields{"app": a.name, "call": "SetBackedUp", "src": src, "error": err}).Errorf("") log.WithFields(log.Fields{"app": a.name, "call": "SetBackedUp", "src": src, "error": err}).Errorf("")
return err return err
@@ -508,3 +525,193 @@ func (a *App) Transfer() error {
return nil return nil
} }
func (a *App) AllBoxes() []*Box {
log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
bm := make(map[string]struct{})
for _, s := range a.sources {
bm[s.Box()] = struct{}{}
}
for _, d := range a.destinations {
bm[d.Box()] = struct{}{}
}
for _, b := range a.before {
bm[b.Box()] = struct{}{}
}
for _, af := range a.after {
bm[af.Box()] = struct{}{}
}
bx := make([]*Box, 0)
for n := range bm {
bx = append(bx, cfg.box[n])
}
return bx
}
func (a *App) SourceBoxes() []*Box {
log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
bm := make(map[string]struct{})
for _, s := range a.sources {
bm[s.Box()] = struct{}{}
}
for _, b := range a.before {
bm[b.Box()] = struct{}{}
}
for _, af := range a.after {
bm[af.Box()] = struct{}{}
}
bx := make([]*Box, 0)
for n := range bm {
bx = append(bx, cfg.box[n])
}
return bx
}
func (a *App) RunStandaloneTime(now time.Time) error {
log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
if cfgRun {
err := errors.New("backup already running")
log.WithFields(log.Fields{"app": a.name, "error": err}).Errorf("")
return err
}
CfgLock()
defer CfgUnlock()
cfgRun = true
defer func() { cfgRun = false }()
boxes := a.AllBoxes()
var wg sync.WaitGroup
// Open boxes
for _, b := range boxes {
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("")
return
}
}(b)
defer b.Close()
}
wg.Wait()
if sched, err := a.RunTime(now); err != nil {
log.WithFields(log.Fields{"call": "Run", "error": err}).Errorf("")
return err
} else {
if sched == "" {
err := errors.New("no backup needed")
log.WithFields(log.Fields{"app": a.name, "error": err}).Errorf("")
return err
}
}
return nil
}
func (a *App) RunStandaloneSchedule(name string) error {
log.WithFields(log.Fields{"app": a.name, "name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name, "name": name}).Debugf("done")
if cfgRun {
err := errors.New("backup already running")
log.WithFields(log.Fields{"app": a.name, "error": err}).Errorf("")
return err
}
CfgLock()
defer CfgUnlock()
cfgRun = true
defer func() { cfgRun = false }()
boxes := a.AllBoxes()
var wg sync.WaitGroup
// Open boxes
for _, b := range boxes {
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("")
return
}
}(b)
defer b.Close()
}
wg.Wait()
return a.RunSchedule(name, time.Now())
}
func (a *App) Snapshots() ([]string, error) {
log.WithFields(log.Fields{"app": a.name}).Debugf("starting")
defer log.WithFields(log.Fields{"app": a.name}).Debugf("done")
CfgLock()
defer CfgUnlock()
cfgRun = true
defer func() { cfgRun = false }()
boxes := a.SourceBoxes()
var wg sync.WaitGroup
// Open boxes
for _, b := range boxes {
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("")
return
}
}(b)
defer b.Close()
}
wg.Wait()
names := make([]string, 0)
for _, src := range a.sources {
if snapshots, err := src.ValidSnapshots(); err != nil {
log.WithFields(log.Fields{"call": "ValidSnapshots", "attr": src, "error": err}).Errorf("")
return names, err
} else {
for _, snapshot := range snapshots {
names = append(names, snapshot.name)
}
}
}
return names, nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,426 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -1,423 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,29 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Login :: zBackup</title>
<!-- Bootstrap core CSS -->
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="container">
<form class="form-signin" method="POST" action="/u/submit">
<h1 class="h3 mb-3 font-weight-normal">Sign in</h1>
{{if (ne .Error "")}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
<div class="form-group">
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
</div>
<div class="form-group">
<input type="password" class="form-control" id="pwd" placeholder="Password" name="password" required>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >Sign in</button>
<p class="mt-5 mb-3 text-muted">&copy; 2023</p>
</form>
<a href="/u/recover">Lost password</a>
</div>
</body>
</html>

View File

@@ -49,6 +49,7 @@ func main() {
} else if c, err := LoadConfigFile("backup.json"); err == nil { } else if c, err := LoadConfigFile("backup.json"); err == nil {
cfg = c cfg = c
} else { } else {
log.Debugf("loading default config")
cfg, _ = LoadConfigByte(sampleCfg) cfg, _ = LoadConfigByte(sampleCfg)
} }
@@ -56,15 +57,9 @@ func main() {
if cfg.Admin == nil { if cfg.Admin == nil {
cfg.Admin = NewAdmin() cfg.Admin = NewAdmin()
} }
if cfg.Admin.Secrets == nil {
cfg.Admin.Secrets = NewSecrets()
}
if len(cfg.Admin.Users) == 0 {
cfg.Admin.NewAdminUser()
}
cfg.Admin.Run() cfg.Admin.Run()
} else { } else {
cfg.Run() cfg.Run(true)
os.Exit(0) os.Exit(0)
} }

82
box.go
View File

@@ -7,7 +7,6 @@ import (
"github.com/silenceper/pool" "github.com/silenceper/pool"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
) )
type Box struct { type Box struct {
@@ -19,19 +18,20 @@ type Box struct {
sshPool pool.Pool sshPool pool.Pool
created bool created bool
online bool online bool
allowDirectConnect bool
mx sync.Mutex mx sync.Mutex
} }
type BoxSshPool struct { func (b *Box) Lock() {
signer ssh.Signer log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
config *ssh.ClientConfig b.mx.Lock()
client *ssh.Client
logged bool
mx sync.Mutex
} }
func (c *Config) NewBox(name, addr, user, key string, direct bool) (b *Box, err error) { func (b *Box) Unlock() {
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
b.mx.Unlock()
}
func (c *Config) NewBox(name, addr, user, key string) (b *Box, err error) {
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting") log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done") defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
@@ -59,7 +59,6 @@ func (c *Config) NewBox(name, addr, user, key string, direct bool) (b *Box, err
sshPool: p, sshPool: p,
online: false, online: false,
created: true, created: true,
allowDirectConnect: true, //FIXME use direct
} }
b.zfs.box = b b.zfs.box = b
@@ -71,8 +70,8 @@ func (b *Box) Open() error {
log.WithFields(log.Fields{"name": b.name}).Debugf("starting") log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done") defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
b.mx.Lock() b.Lock()
defer b.mx.Unlock() defer b.Unlock()
if b.online { if b.online {
return nil return nil
@@ -100,8 +99,8 @@ func (b *Box) Close() error {
log.WithFields(log.Fields{"name": b.name}).Debugf("starting") log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done") defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
b.mx.Lock() b.Lock()
defer b.mx.Unlock() defer b.Unlock()
if !b.online { if !b.online {
return nil return nil
@@ -139,22 +138,34 @@ func (b *Box) Exec(cmd string) (r string, err error) {
return s.Exec(cmd) return s.Exec(cmd)
} }
func TransferZfs(from, to Addr) error { func TransferZfs(from Addr, to []Addr) (int, error) {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
count := 0
for _, dest := range to {
if err := TransferDirectZfs(from, dest); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "TransferDirectZfs", "attr": dest, "error": err}).Errorf("")
return count, err
} else {
count++
}
}
return count, nil
}
func TransferDirectZfs(from, to Addr) error {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting") log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done") defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
var ( var (
err error err error
fromSnapshots, toSnapshots []*ZfsSnapshot fromSnapshots, toSnapshots []*ZfsSnapshot
directTransfer bool
) )
if cfg.box[from.Box()].allowDirectConnect && cfg.box[to.Box()].allowDirectConnect {
directTransfer = true
} else {
directTransfer = false
}
if fromSnapshots, err = from.ValidSnapshots(); err != nil { if fromSnapshots, err = from.ValidSnapshots(); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": from, "error": err}).Errorf("") log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": from, "error": err}).Errorf("")
return err return err
@@ -171,7 +182,6 @@ func TransferZfs(from, to Addr) error {
if len(toSnapshots) == 0 { if len(toSnapshots) == 0 {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("initiating destination") log.WithFields(log.Fields{"from": from, "to": to}).Debugf("initiating destination")
if directTransfer {
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send " + fromSnapshots[0].String() + " | zfs recv -F " + to.Path()); err != nil { if _, err := to.BoxExec("ssh " + from.Box() + " zfs send " + fromSnapshots[0].String() + " | zfs recv -F " + to.Path()); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("") log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
return err return err
@@ -179,21 +189,23 @@ func TransferZfs(from, to Addr) error {
newToSnapshot := &ZfsSnapshot{name: fromSnapshots[0].name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]} newToSnapshot := &ZfsSnapshot{name: fromSnapshots[0].name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]}
toSnapshots = append(toSnapshots, newToSnapshot) toSnapshots = append(toSnapshots, newToSnapshot)
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(newToSnapshot) cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(newToSnapshot)
} else {
//handle indirect transfer
}
} }
fromFromSnapshotId := -1 fromFromSnapshotId := len(fromSnapshots) - 1
lastToSnapshot := toSnapshots[len(toSnapshots)-1] fromToSnapshotId := -1
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("searching last snapshot %s", lastToSnapshot.String()) for fromFromSnapshotId >= 0 {
for id, v := range fromSnapshots { fromToSnapshotId = len(toSnapshots) - 1
if v.name == lastToSnapshot.name { for fromToSnapshotId >= 0 {
fromFromSnapshotId = id if fromSnapshots[fromFromSnapshotId].name == toSnapshots[fromToSnapshotId].name {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("found %s", v.String())
break break
} }
fromToSnapshotId = fromToSnapshotId - 1
}
if fromToSnapshotId >= 0 {
break
}
fromFromSnapshotId = fromFromSnapshotId - 1
} }
if fromFromSnapshotId == -1 { if fromFromSnapshotId == -1 {
@@ -204,14 +216,10 @@ func TransferZfs(from, to Addr) error {
if fromFromSnapshotId < len(fromSnapshots)-1 { if fromFromSnapshotId < len(fromSnapshots)-1 {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("transfering from %s to %s", fromSnapshots[fromFromSnapshotId].name, fromSnapshots[len(fromSnapshots)-1].name) log.WithFields(log.Fields{"from": from, "to": to}).Debugf("transfering from %s to %s", fromSnapshots[fromFromSnapshotId].name, fromSnapshots[len(fromSnapshots)-1].name)
if directTransfer {
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send -I " + fromSnapshots[fromFromSnapshotId].String() + " " + fromSnapshots[len(fromSnapshots)-1].String() + " | zfs recv -F " + to.Path()); err != nil { if _, err := to.BoxExec("ssh " + from.Box() + " zfs send -I " + fromSnapshots[fromFromSnapshotId].String() + " " + fromSnapshots[len(fromSnapshots)-1].String() + " | zfs recv -F " + to.Path()); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("") log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
return err return err
} }
} else {
// handle indirect transfer
}
for _, v := range fromSnapshots[fromFromSnapshotId+1:] { for _, v := range fromSnapshots[fromFromSnapshotId+1:] {
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(&ZfsSnapshot{name: v.name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]}) cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(&ZfsSnapshot{name: v.name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]})

125
config.go
View File

@@ -13,16 +13,16 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/tailscale/hujson" "github.com/tailscale/hujson"
"github.com/tidwall/pretty"
) )
type Config struct { type Config struct {
ScheduleDuration map[string]string `json:"schedule"` ScheduleDuration map[string]string `json:"schedule,omitempty"`
Box map[string]BoxConfig `json:"box"` Box map[string]*BoxConfig `json:"box,omitempty"`
Email EmailConfig `json:"email"` Email *EmailConfig `json:"email,omitempty"`
Apps []AppConfig `json:"apps"` Apps []*AppConfig `json:"apps,omitempty"`
Timezone string `json:"timezone"` Timezone string `json:"timezone,omitempty"`
Admin *AdminConfig `json:"admin"` Admin *AdminConfig `json:"admin,omitempty"`
Debug bool `json:"debug"`
box map[string]*Box `json:"-"` box map[string]*Box `json:"-"`
apps map[string]*App `json:"-"` apps map[string]*App `json:"-"`
timezone *time.Location `json:"-"` timezone *time.Location `json:"-"`
@@ -40,7 +40,6 @@ type BoxConfig struct {
Addr string `json:"addr"` Addr string `json:"addr"`
User string `json:"user"` User string `json:"user"`
Key string `json:"key"` Key string `json:"key"`
AllowDirectConnect bool `json:"allow_direct_connect"`
} }
type AppConfig struct { type AppConfig struct {
@@ -48,9 +47,27 @@ type AppConfig struct {
Schedule []string `json:"schedule"` Schedule []string `json:"schedule"`
Sources []string `json:"src"` Sources []string `json:"src"`
Destinations []string `json:"dest"` Destinations []string `json:"dest"`
Before map[string]string `json:"before"` Before map[string]string `json:"before,omitempty"`
After map[string]string `json:"after"` After map[string]string `json:"after,omitempty"`
Active bool `json:"active,omitempty"`
}
type EmailConfig struct {
Active bool `json:"active"` 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 // Load config from file
@@ -96,25 +113,27 @@ func LoadConfigByte(conf []byte) (*Config, error) {
return nil, err return nil, err
} }
if c.Email != nil {
if c.Email.Active { if c.Email.Active {
if len(c.Email.SmtpHost) == 0 { if len(c.Email.SmtpHost) == 0 {
err := fmt.Errorf("no smtp") err := errors.New("no smtp")
log.WithFields(log.Fields{"error": err}).Errorf("") log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err return nil, err
} }
if len(c.Email.FromEmail) == 0 { if len(c.Email.FromEmail) == 0 {
err := fmt.Errorf("no email from") err := errors.New("no email from")
log.WithFields(log.Fields{"error": err}).Errorf("") log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err return nil, err
} }
if len(c.Email.ToEmail) == 0 { if len(c.Email.ToEmail) == 0 {
err := fmt.Errorf("no email to") err := errors.New("no email to")
log.WithFields(log.Fields{"error": err}).Errorf("") log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err return nil, err
} }
} }
}
for k, v := range c.ScheduleDuration { for k, v := range c.ScheduleDuration {
switch k { switch k {
@@ -136,7 +155,7 @@ func LoadConfigByte(conf []byte) (*Config, error) {
c.box = make(map[string]*Box) c.box = make(map[string]*Box)
for k, v := range c.Box { for k, v := range c.Box {
if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil { 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("") log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
return nil, err return nil, err
} else { } else {
@@ -151,6 +170,7 @@ func LoadConfigByte(conf []byte) (*Config, error) {
c.apps = make(map[string]*App) c.apps = make(map[string]*App)
for _, v := range c.Apps { for _, v := range c.Apps {
if v.Active {
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil { 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("") log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
return nil, err return nil, err
@@ -177,12 +197,77 @@ func LoadConfigByte(conf []byte) (*Config, error) {
} }
} }
} }
}
return c, nil return c, nil
} }
// Pretty config
func (c *Config) Pretty(lock bool) ([]byte, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if lock {
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(lock bool) ([]byte, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if lock {
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(lock bool) error {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
r, err := cfg.Pretty(lock)
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 // Run config
func (c *Config) Run() { func (c *Config) Run(lock bool) {
log.WithFields(log.Fields{}).Debugf("starting") log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done") defer log.WithFields(log.Fields{}).Debugf("done")
@@ -190,8 +275,10 @@ func (c *Config) Run() {
return return
} }
cfgMx.Lock() if lock {
defer cfgMx.Unlock() CfgLock()
defer CfgUnlock()
}
cfgRun = true cfgRun = true
defer func() { cfgRun = false }() defer func() { cfgRun = false }()
@@ -219,7 +306,7 @@ func (c *Config) Run() {
for _, a := range cfg.apps { for _, a := range cfg.apps {
wg.Add(1) wg.Add(1)
go func(app *App) { go func(app *App) {
if sched, err := app.Run(e.startTime); err != nil { if sched, err := app.RunTime(e.startTime); err != nil {
e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err)) e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err))
} else if *debug { } else if *debug {
if sched != "" { if sched != "" {
@@ -282,6 +369,4 @@ func (c *Config) Run() {
log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("") log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("")
} }
} }
return
} }

View File

@@ -6,7 +6,6 @@ import (
"net/smtp" "net/smtp"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -15,14 +14,6 @@ import (
type Email struct { type Email struct {
startTime time.Time startTime time.Time
items []string items []string
mx sync.Mutex
}
type EmailConfig struct {
Active bool `json:"active"`
SmtpHost string `json:"smtp"`
FromEmail string `json:"email_from"`
ToEmail []string `json:"email_to"`
} }
func NewEmail(now time.Time) *Email { func NewEmail(now time.Time) *Email {
@@ -35,9 +26,10 @@ func NewEmail(now time.Time) *Email {
func (e *Email) AddItem(item string) { func (e *Email) AddItem(item string) {
log.WithFields(log.Fields{"item": item}).Debugf("starting") log.WithFields(log.Fields{"item": item}).Debugf("starting")
defer log.WithFields(log.Fields{"item": item}).Debugf("done") defer log.WithFields(log.Fields{"item": item}).Debugf("done")
if cfg.Email.Active {
e.items = append(e.items, item) e.items = append(e.items, item)
} }
}
func (e *Email) Send(addr, from string, to []string) error { func (e *Email) Send(addr, from string, to []string) error {
log.WithFields(log.Fields{}).Debugf("starting") log.WithFields(log.Fields{}).Debugf("starting")

43
go.mod
View File

@@ -1,13 +1,40 @@
module git.siteop.biz/shoopea/backup module git.siteop.biz/shoopea/backup
go 1.16 go 1.21
require ( require (
github.com/gin-gonic/gin v1.9.1 // indirect github.com/gin-gonic/gin v1.9.1
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1
github.com/sethvargo/go-password v0.2.0 // indirect github.com/sethvargo/go-password v0.2.0
github.com/silenceper/pool v1.0.0 // indirect github.com/silenceper/pool v1.0.0
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
golang.org/x/crypto v0.9.0 github.com/tidwall/pretty v1.2.1
golang.org/x/crypto v0.29.0
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

73
go.sum
View File

@@ -5,6 +5,7 @@ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
@@ -12,6 +13,7 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
@@ -23,6 +25,7 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -30,7 +33,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
@@ -43,6 +45,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
@@ -50,7 +53,6 @@ github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE= github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
github.com/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU= github.com/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU=
github.com/silenceper/pool v1.0.0/go.mod h1:3DN13bqAbq86Lmzf6iUXWEPIWFPOSYVfaoceFvilKKI= github.com/silenceper/pool v1.0.0/go.mod h1:3DN13bqAbq86Lmzf6iUXWEPIWFPOSYVfaoceFvilKKI=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -65,77 +67,38 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

67
http.go
View File

@@ -1,67 +0,0 @@
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func HttpAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// Search signed-in userID
userID := 0
if userID == 0 {
// Return 404 and abort handlers chain.
c.String(http.StatusNotFound, "404 page not found")
c.AbortWithStatus(http.StatusNotFound)
return
}
}
}
func HttpNoAuth() gin.HandlerFunc {
return func(c *gin.Context) {
return
}
}
func HttpAnySignIn(c *gin.Context) {
if GetWebSessionUserID(c) > 0 {
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
} else {
SetCSRFToken(c)
warning, _ := c.Cookie("warning")
c.SetCookie("warning", "", -1, "/", cfg.Admin.Addr, false, true)
c.HTML(http.StatusOK, "page-signin.html", gin.H{
"Error": warning,
})
}
return
}
func HttpAnyIndex(c *gin.Context) {
if GetWebSessionUserID(c) > 0 {
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
} else {
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
}
return
}
func HttpAnyHome(c *gin.Context) {
if GetWebSessionUserID(c) == 0 {
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
} else {
SetCSRFToken(c)
c.HTML(http.StatusOK, "page-home.html", gin.H{})
}
return
}
func GetWebSessionUserID(c *gin.Context) int64 {
return 0
}
func SetCSRFToken(c *gin.Context) {
return
}

6
ssh.go
View File

@@ -137,8 +137,8 @@ func (s *Ssh) ExecPipe(cmd string) error {
} }
s.session = session s.session = session
if s.session.Setenv("TZ", cfg.Timezone); err != nil { if err = s.session.Setenv("TZ", cfg.Timezone); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "error": err}).Errorf("") log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "attr": cfg.Timezone, "error": err}).Errorf("")
return err return err
} }
@@ -161,7 +161,7 @@ func (s *Ssh) ExecPipe(cmd string) error {
} }
if err = s.session.Start(cmd); err != nil { if err = s.session.Start(cmd); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Start", "error": err}).Errorf("") log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Start", "attr": cmd, "error": err}).Errorf("")
s.session.Close() s.session.Close()
return err return err
} }

76
user.go
View File

@@ -1,76 +0,0 @@
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/scrypt"
)
type User struct {
Username string `json:"username"`
Salt string `json:"salt"`
Passwd string `json:"passwd"`
}
func NewUser(name, passwd string) (*User, error) {
log.WithFields(log.Fields{"name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name}).Debugf("done")
for _, v := range cfg.Admin.Users {
if v.Username == name {
err := errors.New("user already exists")
log.WithFields(log.Fields{"name": name, "error": err}).Errorf("")
return nil, err
}
}
u := &User{
Username: name,
}
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
log.WithFields(log.Fields{"name": name, "call": "rand.Read", "error": err}).Errorf("")
return nil, err
}
u.Salt = hex.EncodeToString(salt)
if pass, err := u.HashPassword(passwd); err != nil {
log.WithFields(log.Fields{"name": name, "call": "HashPassword", "error": err}).Errorf("")
return nil, err
} else {
u.Passwd = hex.EncodeToString(pass)
}
return u, nil
}
func (u *User) HashPassword(passwd string) ([]byte, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
//peppering the pass
hash := hmac.New(sha256.New, []byte(cfg.Admin.Secrets.PasswordPepper))
hash.Write([]byte(passwd))
hashPass := hash.Sum(nil)
//salting the hash
salt := make([]byte, 32)
if _, err := hex.Decode(salt, []byte(u.Salt)); err != nil {
log.WithFields(log.Fields{"call": "hex.Decode", "error": err}).Errorf("")
return nil, err
}
if h, err := scrypt.Key(hashPass, salt, cfg.Admin.Secrets.ScryptN, cfg.Admin.Secrets.ScryptR, cfg.Admin.Secrets.ScryptP, 32); err != nil {
log.WithFields(log.Fields{"call": "scrypt.Key", "error": err}).Errorf("")
return h, err
} else {
return h, nil
}
}

View File

@@ -1,7 +1,7 @@
// Code generated by version.sh (@generated) DO NOT EDIT. // Code generated by version.sh (@generated) DO NOT EDIT.
package main package main
var githash = "1a1713e" var githash = "54d8576"
var branch = "v2" var branch = "master"
var buildstamp = "2023-08-21_12:35:47" var buildstamp = "2025-12-28_21:17:45"
var commits = "83" var commits = "129"
var version = "1a1713e-b83 - 2023-08-21_12:35:47" var version = "54d8576-b129 - 2025-12-28_21:17:45"

51
zfs.go
View File

@@ -36,12 +36,32 @@ type ZfsSnapshot struct {
fs *ZfsFs fs *ZfsFs
} }
func (z *BoxZfs) Lock() {
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
z.mx.Lock()
}
func (z *BoxZfs) Unlock() {
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
z.mx.Unlock()
}
func (fs *ZfsFs) Lock() {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path}).Debugf("starting")
fs.mx.Lock()
}
func (fs *ZfsFs) Unlock() {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path}).Debugf("starting")
fs.mx.Unlock()
}
func (z *BoxZfs) Open() error { func (z *BoxZfs) Open() error {
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting") log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done") defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
z.mx.Lock() z.Lock()
defer z.mx.Unlock() defer z.Unlock()
if z.online { if z.online {
return nil return nil
@@ -146,12 +166,12 @@ func (z *BoxZfs) Close() error {
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting") log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done") defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
z.mx.Lock() z.Lock()
defer z.mx.Unlock() defer z.Unlock()
for _, fs := range z.filesystems { for _, fs := range z.filesystems {
fs.mx.Lock() fs.Lock()
defer fs.mx.Unlock() defer fs.Unlock()
} }
z.online = false z.online = false
@@ -169,8 +189,8 @@ func (z *BoxZfs) Mkdir(path string) error {
return err return err
} }
z.mx.Lock() z.Lock()
defer z.mx.Unlock() defer z.Unlock()
b := z.box b := z.box
if !b.online { if !b.online {
@@ -230,8 +250,8 @@ func (fs *ZfsFs) TakeSnapshot(name string) (*ZfsSnapshot, error) {
return nil, err return nil, err
} }
fs.mx.Lock() fs.Lock()
defer fs.mx.Unlock() defer fs.Unlock()
if _, ok := fs.snapshots[name]; ok { if _, ok := fs.snapshots[name]; ok {
err := errors.New("already exists") err := errors.New("already exists")
@@ -264,8 +284,8 @@ func (fs *ZfsFs) DelSnapshot(name string) error {
return err return err
} }
fs.mx.Lock() fs.Lock()
defer fs.mx.Unlock() defer fs.Unlock()
if _, ok := fs.snapshots[name]; !ok { if _, ok := fs.snapshots[name]; !ok {
err := errors.New("doesn't exist") err := errors.New("doesn't exist")
@@ -296,8 +316,8 @@ func (fs *ZfsFs) AddSnapshot(s *ZfsSnapshot) error {
return err return err
} }
fs.mx.Lock() fs.Lock()
defer fs.mx.Unlock() defer fs.Unlock()
if _, ok := fs.snapshots[s.name]; ok { if _, ok := fs.snapshots[s.name]; ok {
err := errors.New("already exist") err := errors.New("already exist")
@@ -311,6 +331,9 @@ func (fs *ZfsFs) AddSnapshot(s *ZfsSnapshot) error {
} }
func (fs *ZfsFs) ValidSnapshots() []*ZfsSnapshot { func (fs *ZfsFs) ValidSnapshots() []*ZfsSnapshot {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path}).Debugf("starting")
defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path}).Debugf("done")
tab := make([]*ZfsSnapshot, 0) tab := make([]*ZfsSnapshot, 0)
for _, s := range fs.snapshots { for _, s := range fs.snapshots {