diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3fe14d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/borgmatic-monitor +/env.sh +/example.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aba8e65 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM alpine + +COPY ./build/bgm-server /usr/bin/ + +ENTRYPOINT ["/usr/bin/bgm-server"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3660e20 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +swagger: + swag init -d "cmd/server/,pkg/web/" -g main.go + +watch-server: swagger + MYSQL_DATABASE=borgmaticmon gowatch -p ./cmd/server/ + +watch-client: + gowatch -p ./cmd/client/ -args="-e,http://localhost:1323/api/v1/borgmatic/infos,-h,apps,-c,env.sh" + +.PHONY: +bins: + mkdir -p build/ + CGO_ENABLED=0 go build -o ./build/bgm-server ./cmd/server + CGO_ENABLED=0 go build -o ./build/bgm-client ./cmd/client + +.PHONY: +docker: bins + docker buildx build -t gitnet.fr/deblan/bgm-server --push . diff --git a/build/bgm-client b/build/bgm-client new file mode 100755 index 0000000..4e875a6 Binary files /dev/null and b/build/bgm-client differ diff --git a/build/bgm-server b/build/bgm-server new file mode 100755 index 0000000..36284d3 Binary files /dev/null and b/build/bgm-server differ diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..960bf30 --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,92 @@ +package main + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "log" + "net/http" + "os" + "os/exec" + + "github.com/urfave/cli/v3" + "gitnet.fr/deblan/borgmatic-monitor/pkg/web/api" +) + +func main() { + cmd := &cli.Command{ + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "endpoint", + Aliases: []string{"e"}, + Value: "", + Usage: "Endpoint of the monitor", + }, + &cli.StringFlag{ + Name: "hostname", + Aliases: []string{"h"}, + Value: "", + Usage: "Hostname", + }, + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Value: "", + Usage: "Borgmatic config file", + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + var buff bytes.Buffer + + c := exec.Command( + "borgmatic", + "info", + "-c", cmd.String("config"), + "--json", + ) + + c.Stderr = os.Stderr + c.Stdout = &buff + + err := c.Run() + + if err != nil { + return err + } + + input := api.BorgmaticInfos{ + Hostname: cmd.String("hostname"), + Infos: buff.String(), + } + + j, err := json.Marshal(input) + + if err != nil { + return err + } + + req, err := http.NewRequest("POST", cmd.String("endpoint"), bytes.NewBuffer(j)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + + if err != nil { + return err + } + + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return errors.New("Invalid response code") + } + + return nil + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..bab49e7 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "github.com/labstack/echo/v5" + "github.com/labstack/echo/v5/middleware" + echoSwagger "github.com/swaggo/echo-swagger" + "gitnet.fr/deblan/borgmatic-monitor/docs" + "gitnet.fr/deblan/borgmatic-monitor/pkg/database" + "gitnet.fr/deblan/borgmatic-monitor/pkg/database/model" + "gitnet.fr/deblan/borgmatic-monitor/pkg/web" +) + +func main() { + database.Init() + + db := database.GetDb() + + db.AutoMigrate(model.Host{}) + db.AutoMigrate(model.Info{}) + + e := echo.New() + e.Use(middleware.RequestLogger()) + + e.GET("/", web.Hosts) + e.GET("/host/:id", web.Host) + e.POST("/api/v1/borgmatic/infos", web.ApiBorgmaticInfos) + + docs.SwaggerInfo.Title = "Borgmatic monitor" + docs.SwaggerInfo.Description = "API" + docs.SwaggerInfo.Version = "1.0" + docs.SwaggerInfo.BasePath = "/" + docs.SwaggerInfo.Schemes = []string{"http", "https"} + e.GET("/swagger/*", echoSwagger.WrapHandler) + + if err := e.Start(":1323"); err != nil { + e.Logger.Error("failed to start server", "error", err) + } +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..f2c9fcc --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,80 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/borgmatic/infos": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create a borgmatic infos", + "parameters": [ + { + "description": "Object", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.BorgmaticInfos" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + } + }, + "definitions": { + "api.BorgmaticInfos": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "infos": { + "type": "string" + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..16a65b0 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,51 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/api/v1/borgmatic/infos": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "summary": "Create a borgmatic infos", + "parameters": [ + { + "description": "Object", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/api.BorgmaticInfos" + } + } + ], + "responses": { + "200": { + "description": "OK" + }, + "400": { + "description": "Bad Request" + } + } + } + } + }, + "definitions": { + "api.BorgmaticInfos": { + "type": "object", + "properties": { + "hostname": { + "type": "string" + }, + "infos": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..4d78b49 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,31 @@ +definitions: + api.BorgmaticInfos: + properties: + hostname: + type: string + infos: + type: string + type: object +info: + contact: {} +paths: + /api/v1/borgmatic/infos: + post: + consumes: + - application/json + parameters: + - description: Object + in: body + name: request + required: true + schema: + $ref: '#/definitions/api.BorgmaticInfos' + produces: + - application/json + responses: + "200": + description: OK + "400": + description: Bad Request + summary: Create a borgmatic infos +swagger: "2.0" diff --git a/go.mod b/go.mod index 1cc579a..ab88385 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,54 @@ module gitnet.fr/deblan/borgmatic-monitor go 1.25.1 + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/spec v0.22.3 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/labstack/echo/v5 v5.0.4 // indirect + github.com/mailru/easyjson v0.9.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/sv-tools/openapi v0.4.0 // indirect + github.com/swaggo/echo-swagger v1.5.0 // indirect + github.com/swaggo/files/v2 v2.0.2 // indirect + github.com/swaggo/swag v1.16.6 // indirect + github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect + github.com/urfave/cli/v2 v2.27.5 // indirect + github.com/urfave/cli/v3 v3.6.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/text v0.34.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.42.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.6.0 // indirect + gorm.io/gorm v1.31.1 // indirect + maragu.dev/gomponents v1.2.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6b74a68 --- /dev/null +++ b/go.sum @@ -0,0 +1,150 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v5 v5.0.4 h1:ll3I/O8BifjMztj9dD1vx/peZQv8cR2CTUdQK6QxGGc= +github.com/labstack/echo/v5 v5.0.4/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= +github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4= +github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE= +github.com/swaggo/echo-swagger v1.5.0 h1:nkHxOaBy0SkbJMtMeXZC64KHSa0mJdZFQhVqwEcMres= +github.com/swaggo/echo-swagger v1.5.0/go.mod h1:TzO363X1ZG/MSbjrG2IX6m65Yd3/zpqh5KM6lPctAhk= +github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= +github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk= +github.com/swaggo/swag/v2 v2.0.0-rc5/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= +github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= +gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc= +maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/borgmatic/archive.go b/pkg/borgmatic/archive.go new file mode 100644 index 0000000..1843fe3 --- /dev/null +++ b/pkg/borgmatic/archive.go @@ -0,0 +1,15 @@ +package borgmatic + +type Archive struct { + CommandLine []string `json:"command_line"` + Comment string `json:"comment"` + Duration float64 `json:"duration"` + Start *Time `json:"start"` + End *Time `json:"end"` + Hostname string `json:"hostname"` + Id string `json:"id"` + Limits ArchiveLimits `json:"limits"` + Stats ArchiveStats `json:"stats"` + Name string `json:"name"` + Username string `json:"username"` +} diff --git a/pkg/borgmatic/archive_limits.go b/pkg/borgmatic/archive_limits.go new file mode 100644 index 0000000..d083ec3 --- /dev/null +++ b/pkg/borgmatic/archive_limits.go @@ -0,0 +1,5 @@ +package borgmatic + +type ArchiveLimits struct { + MaxArchiveSize float64 `json:"max_archive_size"` +} diff --git a/pkg/borgmatic/archive_stats.go b/pkg/borgmatic/archive_stats.go new file mode 100644 index 0000000..39fab04 --- /dev/null +++ b/pkg/borgmatic/archive_stats.go @@ -0,0 +1,8 @@ +package borgmatic + +type ArchiveStats struct { + CompressedSize int64 `json:"compressed_size"` + DeduplicatedSize int64 `json:"deduplicated_size"` + Nfiles int64 `json:"nfiles"` + OriginalSize int64 `json:"original_size"` +} diff --git a/pkg/borgmatic/cache.go b/pkg/borgmatic/cache.go new file mode 100644 index 0000000..a412943 --- /dev/null +++ b/pkg/borgmatic/cache.go @@ -0,0 +1,6 @@ +package borgmatic + +type Cache struct { + Path string `json:"path"` + Stats CacheStats `json:"stats"` +} diff --git a/pkg/borgmatic/cache_stats.go b/pkg/borgmatic/cache_stats.go new file mode 100644 index 0000000..1972655 --- /dev/null +++ b/pkg/borgmatic/cache_stats.go @@ -0,0 +1,10 @@ +package borgmatic + +type CacheStats struct { + TotalChunks float64 `json:"total_chunks"` + TotalCsize float64 `json:"total_csize"` + TotalSize float64 `json:"total_size"` + TotalUniqueChunks float64 `json:"total_unique_chunks"` + UniqueCsize float64 `json:"unique_csize"` + UniqueSize float64 `json:"unique_size"` +} diff --git a/pkg/borgmatic/encryption.go b/pkg/borgmatic/encryption.go new file mode 100644 index 0000000..8ae6985 --- /dev/null +++ b/pkg/borgmatic/encryption.go @@ -0,0 +1,5 @@ +package borgmatic + +type Encryption struct { + Mode string `json:"mode"` +} diff --git a/pkg/borgmatic/info.go b/pkg/borgmatic/info.go new file mode 100644 index 0000000..dcba3d8 --- /dev/null +++ b/pkg/borgmatic/info.go @@ -0,0 +1,8 @@ +package borgmatic + +type Info struct { + Archives []Archive `json:"archives"` + Cache Cache `json:"cache"` + Encryption Encryption `json:"encryption"` + Repository Repository `json:"repository"` +} diff --git a/pkg/borgmatic/infos.go b/pkg/borgmatic/infos.go new file mode 100644 index 0000000..4f44229 --- /dev/null +++ b/pkg/borgmatic/infos.go @@ -0,0 +1,3 @@ +package borgmatic + +type Infos []Info diff --git a/pkg/borgmatic/repository.go b/pkg/borgmatic/repository.go new file mode 100644 index 0000000..bcfd9d3 --- /dev/null +++ b/pkg/borgmatic/repository.go @@ -0,0 +1,8 @@ +package borgmatic + +type Repository struct { + Id string `json:"id"` + LastModified *Time `json:"last_modified"` + Location string `json:"location"` + Label string `json:"label"` +} diff --git a/pkg/borgmatic/time.go b/pkg/borgmatic/time.go new file mode 100644 index 0000000..f322255 --- /dev/null +++ b/pkg/borgmatic/time.go @@ -0,0 +1,24 @@ +package borgmatic + +import ( + "strings" + "time" +) + +type Time struct { + time.Time +} + +func (t *Time) UnmarshalJSON(b []byte) (err error) { + s := strings.Trim(string(b), "\"") + + if s == "null" { + t.Time = time.Time{} + + return + } + + t.Time, err = time.Parse("2006-01-02T15:04:05.000000", s) + + return +} diff --git a/pkg/database/database.go b/pkg/database/database.go new file mode 100644 index 0000000..11851f5 --- /dev/null +++ b/pkg/database/database.go @@ -0,0 +1,68 @@ +package database + +import ( + "fmt" + "os" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +var ( + db *gorm.DB + config mysql.Config +) + +func Init() { + config = mysql.Config{ + DSN: fmt.Sprintf( + "%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", + fallback(os.Getenv("MYSQL_USER"), "root"), + fallback(os.Getenv("MYSQL_PASSWORD"), "root"), + fallback(os.Getenv("MYSQL_HOST"), "127.0.0.1"), + fallback(os.Getenv("MYSQL_PORT"), "3306"), + fallback(os.Getenv("MYSQL_DATABASE"), "app"), + ), + DefaultStringSize: 256, + DisableDatetimePrecision: true, + DontSupportRenameIndex: true, + DontSupportRenameColumn: true, + SkipInitializeWithVersion: false, + } +} + +func GetDb() *gorm.DB { + var err error + + if db == nil { + db, err = gorm.Open(mysql.New(config)) + + if err != nil { + panic(err) + } + } + + gdb, err := db.DB() + + if err != nil { + db = nil + + return GetDb() + } + + if err = gdb.Ping(); err != nil { + db = nil + + return GetDb() + } + + return db +} + +func fallback(value string, fb string) string { + if value == "" { + return fb + } + + return value +} diff --git a/pkg/database/host.go b/pkg/database/host.go new file mode 100644 index 0000000..d8866e6 --- /dev/null +++ b/pkg/database/host.go @@ -0,0 +1,80 @@ +package database + +import ( + "math" + + "gitnet.fr/deblan/borgmatic-monitor/pkg/database/model" + "gorm.io/gorm" +) + +func Hosts() []model.Host { + var hosts []model.Host + + GetDb(). + Model(model.Host{}). + Preload("Infos", func(db *gorm.DB) *gorm.DB { + return db.Order("infos.id DESC") + }). + Order("name ASC"). + Find(&hosts) + + return hosts +} + +func Host(id int) *model.Host { + var host model.Host + + GetDb(). + Model(model.Host{}). + Order("name ASC"). + Where("id = ?", id). + First(&host) + + if host.ID > 0 { + return &host + } + + return nil +} + +func HostByName(label string) *model.Host { + var host model.Host + + GetDb(). + Model(model.Host{}). + Order("name ASC"). + Where("name = ?", label). + First(&host) + + if host.ID > 0 { + return &host + } + + return nil +} + +func HostInfos(host *model.Host, page, itemPerPage int) *[]model.Info { + var infos []model.Info + + GetDb(). + Model(model.Info{}). + Order("infos.id DESC"). + Offset((page-1)*itemPerPage). + Limit(itemPerPage). + Where("host_id = ?", host.ID). + Find(&infos) + + return &infos +} + +func HostInfosMaxPages(host *model.Host, itemPerPage int) int { + var count int64 + + GetDb(). + Model(model.Info{}). + Order("infos.id DESC"). + Where("host_id = ?", host.ID). + Count(&count) + + return int(math.Ceil(float64(count) / float64(itemPerPage))) +} diff --git a/pkg/database/model/host.go b/pkg/database/model/host.go new file mode 100644 index 0000000..dba65dc --- /dev/null +++ b/pkg/database/model/host.go @@ -0,0 +1,35 @@ +package model + +import ( + "time" + + "gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic" + "gorm.io/gorm" +) + +type Host struct { + gorm.Model + + Name string `gorm:"not null"` + Token string `gorm:"not null"` + Infos []Info `gorm:"one2many:info;not null"` +} + +func (h *Host) LastArchive() *borgmatic.Archive { + var last *borgmatic.Archive + var lastDate *time.Time + + for _, item := range h.Infos { + if infos := *item.Infos(); infos != nil { + for _, info := range infos { + for _, a := range info.Archives { + if lastDate == nil || a.End.After(*lastDate) { + last = &a + } + } + } + } + } + + return last +} diff --git a/pkg/database/model/info.go b/pkg/database/model/info.go new file mode 100644 index 0000000..bfddba0 --- /dev/null +++ b/pkg/database/model/info.go @@ -0,0 +1,28 @@ +package model + +import ( + "encoding/json" + + "gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic" + "gorm.io/gorm" +) + +type Info struct { + gorm.Model + + HostID uint + Host Host `gorm:"many2one:info"` + Data string `gorm:"type:text;not null"` +} + +func (i *Info) Infos() *borgmatic.Infos { + var infos borgmatic.Infos + + err := json.Unmarshal([]byte(i.Data), &infos) + + if err != nil { + return nil + } + + return &infos +} diff --git a/pkg/web/api/borgmatic_infos.go b/pkg/web/api/borgmatic_infos.go new file mode 100644 index 0000000..5ec50df --- /dev/null +++ b/pkg/web/api/borgmatic_infos.go @@ -0,0 +1,6 @@ +package api + +type BorgmaticInfos struct { + Hostname string `json:"hostname"` + Infos string `json:"infos"` +} diff --git a/pkg/web/api_borgmatic_infos.go b/pkg/web/api_borgmatic_infos.go new file mode 100644 index 0000000..f8ca241 --- /dev/null +++ b/pkg/web/api_borgmatic_infos.go @@ -0,0 +1,49 @@ +package web + +import ( + "encoding/json" + "net/http" + + "github.com/labstack/echo/v5" + "gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic" + "gitnet.fr/deblan/borgmatic-monitor/pkg/database" + "gitnet.fr/deblan/borgmatic-monitor/pkg/database/model" + "gitnet.fr/deblan/borgmatic-monitor/pkg/web/api" +) + +// @Summary Create a borgmatic infos +// @Accept json +// @Produce json +// @Success 200 +// @Success 400 +// @Router /api/v1/borgmatic/infos [post] +// @Param request body api.BorgmaticInfos true "Object" +func ApiBorgmaticInfos(c *echo.Context) error { + value := new(api.BorgmaticInfos) + + if err := c.Bind(value); err != nil { + return c.JSON(http.StatusBadRequest, err.Error()) + } + + host := database.HostByName(value.Hostname) + + if host == nil { + return c.JSON(http.StatusBadRequest, "Host not found") + } + + var infos borgmatic.Infos + + err := json.Unmarshal([]byte(value.Infos), &infos) + + if err != nil { + return c.JSON(http.StatusBadRequest, err.Error()) + } + + info := new(model.Info) + info.HostID = host.ID + info.Data = value.Infos + + database.GetDb().Save(info) + + return c.JSON(200, "ok") +} diff --git a/pkg/web/host.go b/pkg/web/host.go new file mode 100644 index 0000000..040af15 --- /dev/null +++ b/pkg/web/host.go @@ -0,0 +1,83 @@ +package web + +import ( + "fmt" + "math" + "net/http" + + "github.com/labstack/echo/v5" + "github.com/spf13/cast" + "gitnet.fr/deblan/borgmatic-monitor/pkg/database" + "gitnet.fr/deblan/borgmatic-monitor/pkg/web/partial" + + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func Host(c *echo.Context) error { + id := cast.ToInt(c.Param("id")) + + if id == 0 { + return c.HTML(http.StatusNotFound, "Not found.") + } + + host := database.Host(id) + + if host == nil { + return c.HTML(http.StatusNotFound, "Not found.") + } + + type params struct { + Page float64 `query:"page"` + } + + var p params + + maxPerPages := 2 + maxPages := database.HostInfosMaxPages(host, maxPerPages) + + if err := c.Bind(&p); err != nil { + p.Page = 1 + } + + page := int(math.Min(math.Max(p.Page, 1), float64(maxPages))) + infos := database.HostInfos(host, page, maxPerPages) + + var nodes []Node + + if len(*infos) == 0 { + nodes = append( + nodes, + Div( + Class("text-muted"), + Text("Nothing yet!"), + ), + ) + } else { + for _, item := range *infos { + if infos := *item.Infos(); infos != nil { + for i := len(infos) - 1; i >= 0; i-- { + nodes = append(nodes, partial.BorgmaticInfo(infos[i])) + } + } + } + } + + return c.HTML(http.StatusOK, Page( + fmt.Sprintf("host_%d", host.ID), + host.Name, + Div( + Class("container-fluid"), + Div( + Class("d-flex justify-content-end"), + partial.Paginate(fmt.Sprintf("/host/%d?page=%%page%%", host.ID), page, maxPages), + ), + Div( + Class("row"), + Map(nodes, func(node Node) Node { + return node + }), + ), + ), + )) +} diff --git a/pkg/web/hosts.go b/pkg/web/hosts.go new file mode 100644 index 0000000..2f40c12 --- /dev/null +++ b/pkg/web/hosts.go @@ -0,0 +1,31 @@ +package web + +import ( + "net/http" + + "github.com/labstack/echo/v5" + "gitnet.fr/deblan/borgmatic-monitor/pkg/database" + "gitnet.fr/deblan/borgmatic-monitor/pkg/web/partial" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func Hosts(c *echo.Context) error { + var nodes []Node + + for _, host := range database.Hosts() { + nodes = append(nodes, partial.Host(host)) + } + + return c.HTML(http.StatusOK, Page( + "home", + "Hosts", + Div( + Class("container-fluid"), + Div( + Class("row"), + Group(nodes), + ), + ), + )) +} diff --git a/pkg/web/page.go b/pkg/web/page.go new file mode 100644 index 0000000..51c986a --- /dev/null +++ b/pkg/web/page.go @@ -0,0 +1,50 @@ +package web + +import ( + "bytes" + + "gitnet.fr/deblan/borgmatic-monitor/pkg/web/partial" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/components" + . "maragu.dev/gomponents/html" +) + +func Page(currentNode string, title string, children ...Node) string { + page := HTML5(HTML5Props{ + Title: title, + Language: "en", + Head: []Node{ + Link( + Href("https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"), + Rel("stylesheet"), + Integrity("sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"), + CrossOrigin("anonymous"), + ), + Link( + Href("https://cdn.jsdelivr.net/npm/remixicon@4.9.0/fonts/remixicon.css"), + Rel("stylesheet"), + ), + }, + Body: []Node{ + Main( + Class("d-flex flex-nowrap"), + partial.NavMenu(currentNode), + Div( + Class("flex-fill p-3"), + Style("max-height: 100vh; overflow: auto"), + Group(children), + ), + ), + Script( + Src("https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"), + Integrity("sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"), + ), + }, + }) + + var buf bytes.Buffer + + page.Render(&buf) + + return buf.String() +} diff --git a/pkg/web/partial/archive.go b/pkg/web/partial/archive.go new file mode 100644 index 0000000..0249401 --- /dev/null +++ b/pkg/web/partial/archive.go @@ -0,0 +1,47 @@ +package partial + +import ( + "fmt" + + "github.com/dustin/go-humanize" + "gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func Archive(archive borgmatic.Archive) Node { + return Div( + Class("row"), + Div( + Class("col-12 col-md-4"), + I(Class("ri-store-3-fill me-1")), + Text(archive.Name), + ), + Div( + Class("text-end col-12 col-md-4"), + Text(fmt.Sprintf("%2.fs", archive.Duration)), + I(Class("ri-time-fill mx-2")), + Text(fmt.Sprintf("%d", archive.Stats.Nfiles)), + I(Class("ri-file-fill ms-2")), + ), + Div( + Class("text-end col-12 col-md-4"), + Div( + Class("row"), + Div( + Class("col-4 text-secondary"), + Text(humanize.Bytes(uint64(archive.Stats.OriginalSize))), + ), + Div( + Class("col-4 text-primary"), + Text(humanize.Bytes(uint64(archive.Stats.DeduplicatedSize))), + ), + Div( + Class("col-4 text-success"), + Text(humanize.Bytes(uint64(archive.Stats.CompressedSize))), + I(Class("ri-file-zip-fill ms-2")), + ), + ), + ), + ) +} diff --git a/pkg/web/partial/borgmatic_info.go b/pkg/web/partial/borgmatic_info.go new file mode 100644 index 0000000..4d6e3ac --- /dev/null +++ b/pkg/web/partial/borgmatic_info.go @@ -0,0 +1,39 @@ +package partial + +import ( + "gitnet.fr/deblan/borgmatic-monitor/pkg/borgmatic" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func BorgmaticInfo(info borgmatic.Info) Node { + return Div( + Class("col-12"), + Div( + Class("card mb-3"), + Div( + Class("card-header"), + Text(info.Repository.LastModified.Format("2006-01-02 15:04:05")), + ), + Div( + Class("card-body"), + Div( + I(Class("ri-price-tag-3-line me-1")), + Text(info.Repository.Label), + ), + Div( + I(Class("ri-map-pin-line me-1")), + Text(info.Repository.Location), + ), + Div( + Class("mb-3"), + I(Class("ri-lock-line me-1")), + Text(info.Encryption.Mode), + ), + Div( + Map(info.Archives, Archive), + ), + ), + ), + ) +} diff --git a/pkg/web/partial/host.go b/pkg/web/partial/host.go new file mode 100644 index 0000000..94776f7 --- /dev/null +++ b/pkg/web/partial/host.go @@ -0,0 +1,64 @@ +package partial + +import ( + "fmt" + + "gitnet.fr/deblan/borgmatic-monitor/pkg/database/model" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func Host(host model.Host) Node { + lastArchive := host.LastArchive() + + var body Node + + if lastArchive != nil { + pushes := len(host.Infos) + + body = Div( + Div( + Class("fw-bold"), + I(Class("ri-archive-stack-line me-1")), + If(pushes <= 1, Text(fmt.Sprintf("%d push", pushes))), + If(pushes > 1, Text(fmt.Sprintf("%d pushes", pushes))), + ), + Div( + I(Class("ri-calendar-line me-1")), + Text(fmt.Sprintf( + "Last update on %s", + lastArchive.End.Format("2006-01-02 15:04:05"), + )), + ), + Div( + Class("mt-3"), + A( + Class("btn btn-primary"), + Href(fmt.Sprintf("/host/%d", host.ID)), + Text("Show"), + ), + ), + ) + } else { + body = Div( + Class("text-danger"), + I(Class("ri-error-warning-line me-1")), + Text("Nothing yet!"), + ) + } + + return Div( + Class("col-6"), + Div( + Class("card mb-3"), + Div( + Class("card-header"), + Text(host.Name), + ), + Div( + Class("card-body"), + body, + ), + ), + ) +} diff --git a/pkg/web/partial/page.go b/pkg/web/partial/page.go new file mode 100644 index 0000000..fcd3bba --- /dev/null +++ b/pkg/web/partial/page.go @@ -0,0 +1,56 @@ +package partial + +import ( + "fmt" + + "gitnet.fr/deblan/borgmatic-monitor/pkg/database" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func NavMenu(currentNode string) Node { + items := [][3]string{ + [3]string{"home", "/", "All"}, + } + + for _, host := range database.Hosts() { + items = append( + items, + [3]string{ + fmt.Sprintf("host_%d", host.ID), + fmt.Sprintf("/host/%d", host.ID), + host.Name, + }, + ) + } + + return Div( + Class("d-flex flex-column flex-shrink-0 p-3 text-bg-dark"), + Style("width: 280px; height: 100vh"), + Ul( + Class("nav nav-pills flex-column mb-auto"), + Map(items, func(item [3]string) Node { + return NavMenuItem(item, currentNode == item[0]) + }), + ), + ) +} + +func NavMenuItem(item [3]string, isActive bool) Node { + class := "nav-link" + + if isActive { + class += " active" + } else { + class += " text-white" + } + + return Li( + Class("nav-item"), + A( + Class(class), + Href(item[1]), + Text(item[2]), + ), + ) +} diff --git a/pkg/web/partial/pager.go b/pkg/web/partial/pager.go new file mode 100644 index 0000000..e39f6ba --- /dev/null +++ b/pkg/web/partial/pager.go @@ -0,0 +1,42 @@ +package partial + +import ( + "fmt" + "strings" + + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func Paginate(path string, currentPage int, pages int) Node { + return Nav( + Attr("arial-label", "Pagination"), + Ul( + Class("pagination"), + If( + currentPage > 1, + Li( + Class("page-item"), + A( + Class("page-link"), + Attr("arial-label", "Previous"), + Href(strings.Replace(path, "%page%", fmt.Sprintf("%d", currentPage-1), -1)), + Span(Attr("arial-hidden", "true"), Raw("«")), + ), + ), + ), + If( + currentPage < pages, + Li( + Class("page-item"), + A( + Class("page-link"), + Attr("arial-label", "Next"), + Href(strings.Replace(path, "%page%", fmt.Sprintf("%d", currentPage+1), -1)), + Span(Attr("arial-hidden", "true"), Raw("»")), + ), + ), + ), + ), + ) +}