Compare commits
45 Commits
e7ed6cb2fd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aa0a852a2 | ||
|
|
54d85767a7 | ||
|
|
f47ddd1873 | ||
|
|
ad88d9fe88 | ||
|
|
fc81c38ffd | ||
|
|
fd1c14831f | ||
|
|
c8715679f8 | ||
|
|
ee94b077b4 | ||
|
|
d92380cccb | ||
|
|
a1ed1035e9 | ||
|
|
6a0c8006d6 | ||
|
|
1890050a30 | ||
|
|
3e867da45f | ||
|
|
a1ba422429 | ||
|
|
05054be795 | ||
|
|
73f9551c8f | ||
|
|
ace13b68a8 | ||
|
|
6ae863b60c | ||
|
|
2aca8b1ceb | ||
|
|
3c7838b10a | ||
|
|
fd02cfdfc3 | ||
|
|
6cf998997e | ||
|
|
963fd34724 | ||
|
|
a31ff56055 | ||
|
|
060933aa27 | ||
|
|
3bd57a4e98 | ||
|
|
b83b0e8c4f | ||
|
|
6ed38972e7 | ||
|
|
757b6b1627 | ||
|
|
376da04727 | ||
|
|
9a86d82460 | ||
|
|
ad1a8decc3 | ||
|
|
d217ba310e | ||
|
|
49417729ec | ||
|
|
d8cfb3cf00 | ||
|
|
cd94f48b8a | ||
|
|
9a9db972a7 | ||
|
|
86da86684b | ||
|
|
6fcf8b421c | ||
|
|
e7af7ce2fd | ||
|
|
3bcd6664a6 | ||
|
|
e1806fd27a | ||
|
|
26c324c43c | ||
|
|
2ca0695a1f | ||
|
|
3b1d0fc850 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
config.json
|
||||
backup
|
||||
backup.json
|
||||
|
||||
16
addr.go
16
addr.go
@@ -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
101
admin.go
@@ -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
636
api.go
Normal 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
283
app.go
@@ -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
|
||||
}
|
||||
|
||||
5002
assets/static/css/bootstrap-grid.css
vendored
5002
assets/static/css/bootstrap-grid.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.min.css
vendored
7
assets/static/css/bootstrap-grid.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
426
assets/static/css/bootstrap-reboot.css
vendored
426
assets/static/css/bootstrap-reboot.css
vendored
@@ -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
8
assets/static/css/bootstrap-reboot.min.css
vendored
8
assets/static/css/bootstrap-reboot.min.css
vendored
@@ -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
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
@@ -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
@@ -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
4752
assets/static/css/bootstrap-utilities.css
vendored
4752
assets/static/css/bootstrap-utilities.css
vendored
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
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
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
10837
assets/static/css/bootstrap.css
vendored
10837
assets/static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.min.css
vendored
7
assets/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10813
assets/static/css/bootstrap.rtl.css
vendored
10813
assets/static/css/bootstrap.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.rtl.min.css
vendored
7
assets/static/css/bootstrap.rtl.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6748
assets/static/js/bootstrap.bundle.js
vendored
6748
assets/static/js/bootstrap.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.bundle.min.js
vendored
7
assets/static/js/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4967
assets/static/js/bootstrap.esm.js
vendored
4967
assets/static/js/bootstrap.esm.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.esm.min.js
vendored
7
assets/static/js/bootstrap.esm.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5016
assets/static/js/bootstrap.js
vendored
5016
assets/static/js/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.min.js
vendored
7
assets/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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">© 2023</p>
|
||||
</form>
|
||||
<a href="/u/recover">Lost password</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
124
box.go
@@ -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
199
config.go
@@ -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
|
||||
}
|
||||
|
||||
14
email.go
14
email.go
@@ -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
43
go.mod
@@ -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
73
go.sum
@@ -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
67
http.go
@@ -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
6
ssh.go
@@ -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
76
user.go
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
10
version.go
10
version.go
@@ -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
51
zfs.go
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user