add web gui
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
/cli
|
||||
/server
|
||||
/web
|
||||
/env/
|
||||
/build/
|
||||
*.exe
|
||||
/config.json
|
||||
22
build.sh
22
build.sh
@@ -55,6 +55,28 @@ for platform in "${platforms[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
# WEB
|
||||
|
||||
platforms=("linux/amd64" "linux/arm64" "linux/riscv64" "linux/ppc64le")
|
||||
|
||||
for platform in "${platforms[@]}"; do
|
||||
echo "* Compiling web server for $platform..."
|
||||
platform_split=(${platform//\// })
|
||||
|
||||
EXT=""
|
||||
if [ "${platform_split[0]}" == "windows" ]; then
|
||||
EXT=.exe
|
||||
fi
|
||||
|
||||
if [ "$MAKE_PACKAGE" == "true" ]; then
|
||||
CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_web$EXT -a ./cmd/web
|
||||
tar -czf build/server_${platform_split[0]}_${platform_split[1]}.tar.gz build/cloudsave_web$EXT
|
||||
rm build/cloudsave_web$EXT
|
||||
else
|
||||
CGO_ENABLED=0 GOOS=${platform_split[0]} GOARCH=${platform_split[1]} go build -o build/cloudsave_web_${platform_split[0]}_${platform_split[1]}$EXT -a ./cmd/web
|
||||
fi
|
||||
done
|
||||
|
||||
## CLIENT
|
||||
|
||||
platforms=("windows/amd64" "windows/arm64" "darwin/amd64" "darwin/arm64" "linux/amd64" "linux/arm64")
|
||||
|
||||
8
cmd/web/config.template.json
Normal file
8
cmd/web/config.template.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"server": {
|
||||
"port": 8181
|
||||
},
|
||||
"remote": {
|
||||
"url": "http://localhost:8080"
|
||||
}
|
||||
}
|
||||
40
cmd/web/server/config/config.go
Normal file
40
cmd/web/server/config/config.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type (
|
||||
Configuration struct {
|
||||
Server ServerConfiguration `json:"server"`
|
||||
Remote RemoteConfiguration `json:"remote"`
|
||||
}
|
||||
|
||||
ServerConfiguration struct {
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
RemoteConfiguration struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
)
|
||||
|
||||
func Load(path string) (Configuration, error) {
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return Configuration{}, fmt.Errorf("failed to open configuration file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
d := json.NewDecoder(f)
|
||||
|
||||
var c Configuration
|
||||
err = d.Decode(&c)
|
||||
if err != nil {
|
||||
return Configuration{}, fmt.Errorf("failed to parse configuration file (%s): %w", path, err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
63
cmd/web/server/middlewares.go
Normal file
63
cmd/web/server/middlewares.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func recoverMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err != nil {
|
||||
internalServerError(w, r)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// BasicAuth implements a simple middleware handler for adding basic http auth to a route.
|
||||
func BasicAuth(realm string, creds map[string]string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
basicAuthFailed(w, r, realm)
|
||||
return
|
||||
}
|
||||
|
||||
credPass := creds[user]
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(credPass), []byte(pass)); err != nil {
|
||||
basicAuthFailed(w, r, realm)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func basicAuthFailed(w http.ResponseWriter, r *http.Request, realm string) {
|
||||
unauthorized(realm, w, r)
|
||||
}
|
||||
|
||||
func unauthorized(realm string, w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, err := w.Write([]byte(UnauthorizedErrorHTMLPage))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func internalServerError(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := w.Write([]byte(InternalServerErrorHTMLPage))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
248
cmd/web/server/server.go
Normal file
248
cmd/web/server/server.go
Normal file
@@ -0,0 +1,248 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"cloudsave/cmd/web/server/config"
|
||||
"cloudsave/pkg/constants"
|
||||
"cloudsave/pkg/remote/client"
|
||||
"cloudsave/pkg/repository"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"slices"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
type (
|
||||
HTTPServer struct {
|
||||
Server *http.Server
|
||||
Config config.Configuration
|
||||
Templates Templates
|
||||
}
|
||||
|
||||
Templates struct {
|
||||
Dashboard *template.Template
|
||||
Detailled *template.Template
|
||||
System *template.Template
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
DetaillePayload struct {
|
||||
Version string
|
||||
Save repository.Metadata
|
||||
BackupMetadata []repository.Backup
|
||||
Hash string
|
||||
}
|
||||
|
||||
DashboardPayload struct {
|
||||
Version string
|
||||
Saves []repository.Metadata
|
||||
}
|
||||
|
||||
SystemPayload struct {
|
||||
Version string
|
||||
Client client.Information
|
||||
Server client.Information
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed templates/500.html
|
||||
InternalServerErrorHTMLPage string
|
||||
|
||||
//go:embed templates/401.html
|
||||
UnauthorizedErrorHTMLPage string
|
||||
|
||||
//go:embed templates/dashboard.html
|
||||
DashboardHTMLPage string
|
||||
|
||||
//go:embed templates/detailled.html
|
||||
DetailledHTMLPage string
|
||||
|
||||
//go:embed templates/information.html
|
||||
SystemHTMLPage string
|
||||
)
|
||||
|
||||
// NewServer start the http server
|
||||
func NewServer(c config.Configuration) *HTTPServer {
|
||||
dashboardTemplate := template.New("dashboard")
|
||||
dashboardTemplate.Parse(DashboardHTMLPage)
|
||||
|
||||
detailledTemplate := template.New("detailled")
|
||||
detailledTemplate.Parse(DetailledHTMLPage)
|
||||
|
||||
systemTemplate := template.New("system")
|
||||
systemTemplate.Parse(SystemHTMLPage)
|
||||
|
||||
s := &HTTPServer{
|
||||
Config: c,
|
||||
Templates: Templates{
|
||||
Dashboard: dashboardTemplate,
|
||||
Detailled: detailledTemplate,
|
||||
System: systemTemplate,
|
||||
},
|
||||
}
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.Logger)
|
||||
router.Use(recoverMiddleware)
|
||||
router.Route("/web", func(routerAPI chi.Router) {
|
||||
routerAPI.Get("/", s.dashboard)
|
||||
routerAPI.Get("/{id}", s.detailled)
|
||||
routerAPI.Get("/system", s.system)
|
||||
})
|
||||
s.Server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", c.Server.Port),
|
||||
Handler: router,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *HTTPServer) dashboard(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
basicAuthFailed(w, r, "realm")
|
||||
return
|
||||
}
|
||||
|
||||
cli := client.New(s.Config.Remote.URL, user, pass)
|
||||
|
||||
if err := cli.Ping(); err != nil {
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
saves, err := cli.All()
|
||||
if err != nil {
|
||||
if errors.Is(err, client.ErrUnauthorized) {
|
||||
unauthorized("Unable to access resources", w, r)
|
||||
return
|
||||
}
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
slices.SortFunc(saves, func(a, b repository.Metadata) int {
|
||||
return a.Date.Compare(b.Date)
|
||||
})
|
||||
|
||||
slices.Reverse(saves)
|
||||
|
||||
payload := DashboardPayload{
|
||||
Version: constants.Version,
|
||||
Saves: saves,
|
||||
}
|
||||
|
||||
if err := s.Templates.Dashboard.Execute(w, payload); err != nil {
|
||||
slog.Error("failed to render the html pages", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) detailled(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
basicAuthFailed(w, r, "realm")
|
||||
return
|
||||
}
|
||||
|
||||
id := chi.URLParam(r, "id")
|
||||
cli := client.New(s.Config.Remote.URL, user, pass)
|
||||
|
||||
if err := cli.Ping(); err != nil {
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
save, err := cli.Metadata(id)
|
||||
if err != nil {
|
||||
if errors.Is(err, client.ErrUnauthorized) {
|
||||
unauthorized("Unable to access resources", w, r)
|
||||
return
|
||||
}
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
h, err := cli.Hash(id)
|
||||
if err != nil {
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
ids, err := cli.ListArchives(id)
|
||||
if err != nil {
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
var bm []repository.Backup
|
||||
for _, i := range ids {
|
||||
b, err := cli.ArchiveInfo(id, i)
|
||||
if err != nil {
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
bm = append(bm, b)
|
||||
}
|
||||
|
||||
payload := DetaillePayload{
|
||||
Save: save,
|
||||
Hash: h,
|
||||
BackupMetadata: bm,
|
||||
Version: constants.Version,
|
||||
}
|
||||
|
||||
if err := s.Templates.Detailled.Execute(w, payload); err != nil {
|
||||
slog.Error("failed to render the html pages", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) system(w http.ResponseWriter, r *http.Request) {
|
||||
user, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
basicAuthFailed(w, r, "realm")
|
||||
return
|
||||
}
|
||||
cli := client.New(s.Config.Remote.URL, user, pass)
|
||||
|
||||
if err := cli.Ping(); err != nil {
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
clientInfo := client.Information{
|
||||
Version: constants.Version,
|
||||
APIVersion: constants.ApiVersion,
|
||||
GoVersion: runtime.Version(),
|
||||
OSName: runtime.GOOS,
|
||||
OSArchitecture: runtime.GOARCH,
|
||||
}
|
||||
serverInfo, err := cli.Version()
|
||||
if err != nil {
|
||||
if errors.Is(err, client.ErrUnauthorized) {
|
||||
unauthorized("Unable to access resources", w, r)
|
||||
return
|
||||
}
|
||||
slog.Error("unable to connect to the remote", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
payload := SystemPayload{
|
||||
Version: constants.Version,
|
||||
Client: clientInfo,
|
||||
Server: serverInfo,
|
||||
}
|
||||
|
||||
if err := s.Templates.System.Execute(w, payload); err != nil {
|
||||
slog.Error("failed to render the html pages", "err", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
9
cmd/web/server/templates/401.html
Normal file
9
cmd/web/server/templates/401.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>You are not allowed</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>401 Unauthorized</h1>
|
||||
</body>
|
||||
</html>
|
||||
9
cmd/web/server/templates/500.html
Normal file
9
cmd/web/server/templates/500.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>An error occured</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>500 Internal Server Error</h1>
|
||||
</body>
|
||||
</html>
|
||||
38
cmd/web/server/templates/dashboard.html
Normal file
38
cmd/web/server/templates/dashboard.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Dashboard</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">CloudSave</span>
|
||||
<a class="muted" href="/web/system">v{{.Version}}</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container" style="margin-top: 1rem;">
|
||||
<div class="list-group">
|
||||
{{range .Saves}}
|
||||
<a href="/web/{{.ID}}" class="list-group-item list-group-item-action">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
<h5 class="mb-1">{{.Name}}</h5>
|
||||
<small>{{.Date}}</small>
|
||||
</div>
|
||||
<p class="mb-1">Version: {{.Version}}</p>
|
||||
<small>{{.ID}}</small>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
</html>
|
||||
54
cmd/web/server/templates/detailled.html
Normal file
54
cmd/web/server/templates/detailled.html
Normal file
@@ -0,0 +1,54 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{.Save.Name}}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">CloudSave</span>
|
||||
<a class="muted" href="/web/system">v{{.Version}}</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container" style="margin-top: 1rem;">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/web">Home</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{.Save.ID}}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="list-group">
|
||||
<h1>{{.Save.Name}} <span class="badge text-bg-success">Version {{.Save.Version}}</span></h1>
|
||||
<hr />
|
||||
<h3>Details</h3>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">UUID: {{.Save.ID}}</li>
|
||||
<li class="list-group-item">Last Upload: {{.Save.Date}}</li>
|
||||
<li class="list-group-item">Hash (MD5): {{.Hash}}</li>
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
<h3>Backup</h3>
|
||||
{{ range .BackupMetadata}}
|
||||
<div class="card" style="margin-top: 1rem;">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{.CreatedAt}}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-body-secondary">{{.UUID}}</h6>
|
||||
<p class="card-text">MD5: {{.MD5}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
</html>
|
||||
52
cmd/web/server/templates/information.html
Normal file
52
cmd/web/server/templates/information.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>System Information</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">CloudSave</span>
|
||||
<span class="muted">v{{.Version}}</span>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container" style="margin-top: 1rem;">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/web">Home</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">System</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="list-group">
|
||||
<h3>Client</h3>
|
||||
<hr />
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">Version: {{.Client.Version}}</li>
|
||||
<li class="list-group-item">API Version: {{.Client.APIVersion}}</li>
|
||||
<li class="list-group-item">Go Version: {{.Client.GoVersion}}</li>
|
||||
<li class="list-group-item">OS: {{.Client.OSName}}/{{.Client.OSArchitecture}}</li>
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
<h3>Server</h3>
|
||||
<hr />
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">Version: {{.Server.Version}}</li>
|
||||
<li class="list-group-item">API Version: {{.Server.APIVersion}}</li>
|
||||
<li class="list-group-item">Go Version: {{.Server.GoVersion}}</li>
|
||||
<li class="list-group-item">OS: {{.Server.OSName}}/{{.Server.OSArchitecture}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q"
|
||||
crossorigin="anonymous"></script>
|
||||
|
||||
</html>
|
||||
34
cmd/web/web.go
Normal file
34
cmd/web/web.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cloudsave/cmd/web/server"
|
||||
"cloudsave/cmd/web/server/config"
|
||||
"cloudsave/pkg/constants"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Printf("CloudSave web -- v%s.%s.%s\n\n", constants.Version, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
var configPath string
|
||||
flag.StringVar(&configPath, "config", "/var/lib/cloudsave/config.json", "Define the path to the configuration file")
|
||||
flag.Parse()
|
||||
|
||||
c, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "failed to load configuration:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
s := server.NewServer(c)
|
||||
|
||||
fmt.Println("starting server at :" + strconv.Itoa(c.Server.Port))
|
||||
if err := s.Server.ListenAndServe(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "failed to start web server:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package constants
|
||||
|
||||
const Version = "0.0.2"
|
||||
const Version = "0.0.3"
|
||||
|
||||
const ApiVersion = 1
|
||||
|
||||
@@ -37,6 +37,7 @@ type (
|
||||
|
||||
var (
|
||||
ErrNotFound error = errors.New("not found")
|
||||
ErrUnauthorized error = errors.New("unauthorized (HTTP Error 401)")
|
||||
)
|
||||
|
||||
func New(baseURL, username, password string) *Client {
|
||||
@@ -382,6 +383,10 @@ func (c *Client) get(url string) (obj.HTTPObject, error) {
|
||||
return obj.HTTPObject{}, ErrNotFound
|
||||
}
|
||||
|
||||
if res.StatusCode == 401 {
|
||||
return obj.HTTPObject{}, ErrUnauthorized
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return obj.HTTPObject{}, fmt.Errorf("server returns an unexpected status code: %d %s (expected 200)", res.StatusCode, res.Status)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user