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
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")
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("")
return err
} else {
fs.mx.Lock()
defer fs.mx.Unlock()
if fs.managed != val {
fs.Lock()
defer fs.Unlock()
if fs.managed != managed {
var cmd string
if val {
if managed {
cmd = fmt.Sprintf("zfs set %s=+ %s", zfsManagedPropertyName, a.Path())
} else {
cmd = fmt.Sprintf("zfs set %s=- %s", zfsManagedPropertyName, a.Path())
@@ -111,7 +111,7 @@ func (a Addr) SetManaged(val bool) error {
return err
}
}
fs.managed = val
fs.managed = managed
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("")
return err
} else {
fs.mx.Lock()
defer fs.mx.Unlock()
fs.Lock()
defer fs.Unlock()
fs.backedUp = val
return nil
}

101
admin.go
View File

@@ -3,8 +3,6 @@ package main
import (
"context"
"embed"
"html/template"
"io/fs"
"net/http"
"os/signal"
"syscall"
@@ -12,23 +10,11 @@ import (
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
"github.com/sethvargo/go-password/password"
log "github.com/sirupsen/logrus"
)
type AdminConfig struct {
Users []*User `json:"users"`
Secrets *SecretsConfig `json:"secrets"`
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"`
Addr string `json:"addr"`
}
//go:embed assets
@@ -39,46 +25,12 @@ func NewAdmin() *AdminConfig {
defer log.WithFields(log.Fields{}).Debugf("done")
a := &AdminConfig{
Addr: "0.0.0.0:8080",
Secrets: NewSecrets(),
Users: make([]*User, 0),
Addr: "0.0.0.0:8080",
}
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() {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
@@ -93,38 +45,33 @@ func (a *AdminConfig) Run() {
r := gin.Default()
if t, err := template.ParseFS(assets, "assets/templates/*.html"); err != nil {
log.WithFields(log.Fields{"call": "template.ParseFS", "error": err}).Errorf("")
return
} else {
r.SetHTMLTemplate(t)
}
r.GET("/run", ApiRun)
r.GET("/run/:app", ApiRunApp)
r.GET("/", HttpAnyIndex)
r.POST("/", HttpAnyIndex)
r.GET("/snapshot/add/:app/:schedule", ApiSnapshotAdd)
r.GET("/snapshot/del/:app/:name", ApiSnapshotDel)
r.GET("/snapshot/list/:app", ApiSnapshotList)
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.GET("/schedule/list", ApiScheduleList)
r.GET("/schedule/add/:name/:duration", ApiScheduleAdd)
r.GET("/schedule/del/:name", ApiScheduleDel)
r.GET("/run", func(c *gin.Context) {
cfg.Run()
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
})
r.GET("/save", ApiSave)
fsys, _ := fs.Sub(assets, "assets/static")
r.StaticFS("/assets", http.FS(fsys))
r.GET("/config", ApiConfig)
r.GET("/config/:app", ApiConfigApp)
protected := r.Group("p", HttpAuth())
protected.GET("test", HttpAnyHome)
r.GET("/app/list", ApiAppList)
r.GET("/app/list/schedule/:schedule", ApiListSchedule)
r.GET("/app/add/:app", ApiAppAdd)
r.GET("/app/del/:app", ApiAppDel)
unprotected := r.Group("u", HttpNoAuth())
unprotected.GET("signin", HttpAnySignIn)
unprotected.POST("signin", HttpAnySignIn)
r.GET("/app/activate/:app", ApiAppActivate)
r.GET("/app/deactivate/:app", ApiAppDeactivate)
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{
Addr: a.Addr,
@@ -138,7 +85,7 @@ func (a *AdminConfig) Run() {
}()
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("")
}
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"
"regexp"
"strings"
"sync"
"time"
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")
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{
name: name,
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")
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)
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("")
}
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
}
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")
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)
if err != nil {
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")
if schedule != "" {
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
return schedule, a.RunSchedule(schedule, now)
}
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")
for _, src := range a.sources {
backedUp := false
dests := make([]Addr, 0)
for _, dest := range a.destinations {
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
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("")
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 {
log.WithFields(log.Fields{"app": a.name, "call": "SetManaged", "src": src, "dest": dest, "error": err}).Errorf("")
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 {
log.WithFields(log.Fields{"app": a.name, "call": "SetBackedUp", "src": src, "error": err}).Errorf("")
return err
@@ -508,3 +525,193 @@ func (a *App) Transfer() error {
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 {
cfg = c
} else {
log.Debugf("loading default config")
cfg, _ = LoadConfigByte(sampleCfg)
}
@@ -56,15 +57,9 @@ func main() {
if cfg.Admin == nil {
cfg.Admin = NewAdmin()
}
if cfg.Admin.Secrets == nil {
cfg.Admin.Secrets = NewSecrets()
}
if len(cfg.Admin.Users) == 0 {
cfg.Admin.NewAdminUser()
}
cfg.Admin.Run()
} else {
cfg.Run()
cfg.Run(true)
os.Exit(0)
}

124
box.go
View File

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

199
config.go
View File

@@ -13,19 +13,19 @@ import (
log "github.com/sirupsen/logrus"
"github.com/tailscale/hujson"
"github.com/tidwall/pretty"
)
type Config struct {
ScheduleDuration map[string]string `json:"schedule"`
Box map[string]BoxConfig `json:"box"`
Email EmailConfig `json:"email"`
Apps []AppConfig `json:"apps"`
Timezone string `json:"timezone"`
Admin *AdminConfig `json:"admin"`
Debug bool `json:"debug"`
box map[string]*Box `json:"-"`
apps map[string]*App `json:"-"`
timezone *time.Location `json:"-"`
ScheduleDuration map[string]string `json:"schedule,omitempty"`
Box map[string]*BoxConfig `json:"box,omitempty"`
Email *EmailConfig `json:"email,omitempty"`
Apps []*AppConfig `json:"apps,omitempty"`
Timezone string `json:"timezone,omitempty"`
Admin *AdminConfig `json:"admin,omitempty"`
box map[string]*Box `json:"-"`
apps map[string]*App `json:"-"`
timezone *time.Location `json:"-"`
}
var (
@@ -37,10 +37,9 @@ var (
var sampleCfg []byte
type BoxConfig struct {
Addr string `json:"addr"`
User string `json:"user"`
Key string `json:"key"`
AllowDirectConnect bool `json:"allow_direct_connect"`
Addr string `json:"addr"`
User string `json:"user"`
Key string `json:"key"`
}
type AppConfig struct {
@@ -48,9 +47,27 @@ type AppConfig struct {
Schedule []string `json:"schedule"`
Sources []string `json:"src"`
Destinations []string `json:"dest"`
Before map[string]string `json:"before"`
After map[string]string `json:"after"`
Active bool `json:"active"`
Before map[string]string `json:"before,omitempty"`
After map[string]string `json:"after,omitempty"`
Active bool `json:"active,omitempty"`
}
type EmailConfig struct {
Active bool `json:"active"`
SmtpHost string `json:"smtp,omitempty"`
FromEmail string `json:"email_from,omitempty"`
ToEmail []string `json:"email_to,omitempty"`
}
func CfgLock() {
log.WithFields(log.Fields{}).Debugf("starting")
cfgMx.Lock()
}
func CfgUnlock() {
log.WithFields(log.Fields{}).Debugf("starting")
cfgMx.Unlock()
}
// Load config from file
@@ -96,23 +113,25 @@ func LoadConfigByte(conf []byte) (*Config, error) {
return nil, err
}
if c.Email.Active {
if len(c.Email.SmtpHost) == 0 {
err := fmt.Errorf("no smtp")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if c.Email != nil {
if c.Email.Active {
if len(c.Email.SmtpHost) == 0 {
err := errors.New("no smtp")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.FromEmail) == 0 {
err := fmt.Errorf("no email from")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.FromEmail) == 0 {
err := errors.New("no email from")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.ToEmail) == 0 {
err := fmt.Errorf("no email to")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
if len(c.Email.ToEmail) == 0 {
err := errors.New("no email to")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
}
}
@@ -136,7 +155,7 @@ func LoadConfigByte(conf []byte) (*Config, error) {
c.box = make(map[string]*Box)
for k, v := range c.Box {
if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil {
if b, err := c.NewBox(k, v.Addr, v.User, v.Key); err != nil {
log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
return nil, err
} else {
@@ -151,28 +170,30 @@ func LoadConfigByte(conf []byte) (*Config, error) {
c.apps = make(map[string]*App)
for _, v := range c.Apps {
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
return nil, err
} else {
if _, ok := c.apps[v.Name]; ok {
err := errors.New("app already exists")
log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("")
if v.Active {
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
return nil, err
}
c.apps[v.Name] = a
for k := range a.schedule {
if dur, ok := c.ScheduleDuration[k]; ok {
re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
if !re.MatchString(dur) {
err := errors.New("incorrect schedule duration")
} else {
if _, ok := c.apps[v.Name]; ok {
err := errors.New("app already exists")
log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("")
return nil, err
}
c.apps[v.Name] = a
for k := range a.schedule {
if dur, ok := c.ScheduleDuration[k]; ok {
re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
if !re.MatchString(dur) {
err := errors.New("incorrect schedule duration")
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
return nil, err
}
} else {
err := errors.New("undefined schedule duration")
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
return nil, err
}
} else {
err := errors.New("undefined schedule duration")
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
return nil, err
}
}
}
@@ -181,8 +202,72 @@ func LoadConfigByte(conf []byte) (*Config, error) {
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
func (c *Config) Run() {
func (c *Config) Run(lock bool) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
@@ -190,8 +275,10 @@ func (c *Config) Run() {
return
}
cfgMx.Lock()
defer cfgMx.Unlock()
if lock {
CfgLock()
defer CfgUnlock()
}
cfgRun = true
defer func() { cfgRun = false }()
@@ -219,7 +306,7 @@ func (c *Config) Run() {
for _, a := range cfg.apps {
wg.Add(1)
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))
} else if *debug {
if sched != "" {
@@ -282,6 +369,4 @@ func (c *Config) Run() {
log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("")
}
}
return
}

View File

@@ -6,7 +6,6 @@ import (
"net/smtp"
"sort"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
@@ -15,14 +14,6 @@ import (
type Email struct {
startTime time.Time
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 {
@@ -35,8 +26,9 @@ func NewEmail(now time.Time) *Email {
func (e *Email) AddItem(item string) {
log.WithFields(log.Fields{"item": item}).Debugf("starting")
defer log.WithFields(log.Fields{"item": item}).Debugf("done")
e.items = append(e.items, item)
if cfg.Email.Active {
e.items = append(e.items, item)
}
}
func (e *Email) Send(addr, from string, to []string) error {

43
go.mod
View File

@@ -1,13 +1,40 @@
module git.siteop.biz/shoopea/backup
go 1.16
go 1.21
require (
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sethvargo/go-password v0.2.0 // indirect
github.com/silenceper/pool v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
golang.org/x/crypto v0.9.0
github.com/gin-gonic/gin v1.9.1
github.com/robfig/cron/v3 v3.0.1
github.com/sethvargo/go-password v0.2.0
github.com/silenceper/pool v1.0.0
github.com/sirupsen/logrus v1.9.3
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
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/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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
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/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
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/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.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
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/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.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
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/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=
@@ -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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/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/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU=
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.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
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.1/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/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/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/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/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.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
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/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
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-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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
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/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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
if s.session.Setenv("TZ", cfg.Timezone); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "error": err}).Errorf("")
if err = s.session.Setenv("TZ", cfg.Timezone); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "attr": cfg.Timezone, "error": err}).Errorf("")
return err
}
@@ -161,7 +161,7 @@ func (s *Ssh) ExecPipe(cmd string) error {
}
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()
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.
package main
var githash = "1a1713e"
var branch = "v2"
var buildstamp = "2023-08-21_12:35:47"
var commits = "83"
var version = "1a1713e-b83 - 2023-08-21_12:35:47"
var githash = "54d8576"
var branch = "master"
var buildstamp = "2025-12-28_21:17:45"
var commits = "129"
var version = "54d8576-b129 - 2025-12-28_21:17:45"

51
zfs.go
View File

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