Compare commits
5 Commits
v0.0.2
...
f699fcd7f6
| Author | SHA1 | Date | |
|---|---|---|---|
| f699fcd7f6 | |||
| 898012a30d | |||
| 2f777c72ee | |||
| d479004217 | |||
| cf96815d0f |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,7 @@
|
|||||||
/cli
|
/cli
|
||||||
/server
|
/server
|
||||||
|
/web
|
||||||
/env/
|
/env/
|
||||||
/build/
|
/build/
|
||||||
|
*.exe
|
||||||
|
/config.json
|
||||||
24
build.sh
24
build.sh
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
MAKE_PACKAGE=false
|
MAKE_PACKAGE=false
|
||||||
VERSION=0.0.2
|
VERSION=0.0.3
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
echo "Usage: $0 [OPTIONS]"
|
echo "Usage: $0 [OPTIONS]"
|
||||||
@@ -55,6 +55,28 @@ for platform in "${platforms[@]}"; do
|
|||||||
fi
|
fi
|
||||||
done
|
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
|
## CLIENT
|
||||||
|
|
||||||
platforms=("windows/amd64" "windows/arm64" "darwin/amd64" "darwin/arm64" "linux/amd64" "linux/arm64")
|
platforms=("windows/amd64" "windows/arm64" "darwin/amd64" "darwin/arm64" "linux/amd64" "linux/arm64")
|
||||||
|
|||||||
@@ -1,32 +1,39 @@
|
|||||||
package add
|
package add
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cloudsave/pkg/remote"
|
||||||
"cloudsave/pkg/repository"
|
"cloudsave/pkg/repository"
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/google/subcommands"
|
"github.com/google/subcommands"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
AddCmd struct {
|
AddCmd struct {
|
||||||
name string
|
name string
|
||||||
|
remote string
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (*AddCmd) Name() string { return "add" }
|
func (*AddCmd) Name() string { return "add" }
|
||||||
func (*AddCmd) Synopsis() string { return "Add a folder to the sync list" }
|
func (*AddCmd) Synopsis() string { return "add a folder to the sync list" }
|
||||||
func (*AddCmd) Usage() string {
|
func (*AddCmd) Usage() string {
|
||||||
return `add:
|
return `Usage: cloudsave add [-name] [-remote] <PATH>
|
||||||
Add a folder to the sync list
|
|
||||||
|
Add a folder to the track list
|
||||||
|
|
||||||
|
Options:
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AddCmd) SetFlags(f *flag.FlagSet) {
|
func (p *AddCmd) SetFlags(f *flag.FlagSet) {
|
||||||
f.StringVar(&p.name, "name", "", "Override the name of the game")
|
f.StringVar(&p.name, "name", "", "Override the name of the game")
|
||||||
|
f.StringVar(&p.remote, "remote", "", "Defines a remote server to sync with")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||||
@@ -50,6 +57,10 @@ func (p *AddCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) s
|
|||||||
return subcommands.ExitFailure
|
return subcommands.ExitFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(strings.TrimSpace(p.remote)) > 0 {
|
||||||
|
remote.Set(m.ID, p.remote)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println(m.ID)
|
fmt.Println(m.ID)
|
||||||
|
|
||||||
return subcommands.ExitSuccess
|
return subcommands.ExitSuccess
|
||||||
|
|||||||
@@ -20,8 +20,9 @@ type (
|
|||||||
func (*ListCmd) Name() string { return "apply" }
|
func (*ListCmd) Name() string { return "apply" }
|
||||||
func (*ListCmd) Synopsis() string { return "apply a backup" }
|
func (*ListCmd) Synopsis() string { return "apply a backup" }
|
||||||
func (*ListCmd) Usage() string {
|
func (*ListCmd) Usage() string {
|
||||||
return `apply:
|
return `Usage: cloudsave apply <GAME_ID> <BACKUP_ID>
|
||||||
Apply a backup
|
|
||||||
|
Apply a backup
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,11 @@ type (
|
|||||||
func (*ListCmd) Name() string { return "list" }
|
func (*ListCmd) Name() string { return "list" }
|
||||||
func (*ListCmd) Synopsis() string { return "list all game registered" }
|
func (*ListCmd) Synopsis() string { return "list all game registered" }
|
||||||
func (*ListCmd) Usage() string {
|
func (*ListCmd) Usage() string {
|
||||||
return `list:
|
return `Usage: cloudsave list [-include-backup] [-a]
|
||||||
List all game registered
|
|
||||||
|
List all game registered
|
||||||
|
|
||||||
|
Options:
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package pull
|
package pull
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cloudsave/cmd/cli/tools/prompt/credentials"
|
||||||
"cloudsave/pkg/remote/client"
|
"cloudsave/pkg/remote/client"
|
||||||
"cloudsave/pkg/repository"
|
"cloudsave/pkg/repository"
|
||||||
"cloudsave/pkg/tools/archive"
|
"cloudsave/pkg/tools/archive"
|
||||||
"cloudsave/cmd/cli/tools/prompt/credentials"
|
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -22,8 +22,9 @@ type (
|
|||||||
func (*PullCmd) Name() string { return "pull" }
|
func (*PullCmd) Name() string { return "pull" }
|
||||||
func (*PullCmd) Synopsis() string { return "pull a game save from the remote" }
|
func (*PullCmd) Synopsis() string { return "pull a game save from the remote" }
|
||||||
func (*PullCmd) Usage() string {
|
func (*PullCmd) Usage() string {
|
||||||
return `list:
|
return `Usage: cloudsave pull <GAME_ID>
|
||||||
Pull a game save from the remote
|
|
||||||
|
Pull a game save from the remote
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,17 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (*RemoteCmd) Name() string { return "remote" }
|
func (*RemoteCmd) Name() string { return "remote" }
|
||||||
func (*RemoteCmd) Synopsis() string { return "manage remote" }
|
func (*RemoteCmd) Synopsis() string { return "add or update the remote url" }
|
||||||
func (*RemoteCmd) Usage() string {
|
func (*RemoteCmd) Usage() string {
|
||||||
return `remote:
|
return `Usage: cloudsave remote <-set|-list>
|
||||||
manage remove
|
|
||||||
|
The -list argument lists all remotes for each registered game.
|
||||||
|
This command performs a connection test.
|
||||||
|
|
||||||
|
The -set argument allow you to set (create or update)
|
||||||
|
the URL to the remote for a game
|
||||||
|
|
||||||
|
Options
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ type (
|
|||||||
func (*RemoveCmd) Name() string { return "remove" }
|
func (*RemoveCmd) Name() string { return "remove" }
|
||||||
func (*RemoveCmd) Synopsis() string { return "unregister a game" }
|
func (*RemoveCmd) Synopsis() string { return "unregister a game" }
|
||||||
func (*RemoveCmd) Usage() string {
|
func (*RemoveCmd) Usage() string {
|
||||||
return `remove:
|
return `Usage: cloudsave remove <GAME_ID>
|
||||||
Unregister a game
|
|
||||||
|
Unregister a game
|
||||||
|
Caution: all the backup are deleted
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,14 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (*RunCmd) Name() string { return "run" }
|
func (*RunCmd) Name() string { return "scan" }
|
||||||
func (*RunCmd) Synopsis() string { return "Check and process all the folder" }
|
func (*RunCmd) Synopsis() string { return "check and process all the folder" }
|
||||||
func (*RunCmd) Usage() string {
|
func (*RunCmd) Usage() string {
|
||||||
return `run:
|
return `Usage: cloudsave scan
|
||||||
Check and process all the folder
|
|
||||||
|
Check if the files have been modified. If so,
|
||||||
|
the current archive is moved to the backup list
|
||||||
|
and a new archive is created with a new version number.
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ type (
|
|||||||
func (*SyncCmd) Name() string { return "sync" }
|
func (*SyncCmd) Name() string { return "sync" }
|
||||||
func (*SyncCmd) Synopsis() string { return "list all game registered" }
|
func (*SyncCmd) Synopsis() string { return "list all game registered" }
|
||||||
func (*SyncCmd) Usage() string {
|
func (*SyncCmd) Usage() string {
|
||||||
return `add:
|
return `Usage: cloudsave sync
|
||||||
List all game registered
|
|
||||||
|
Synchronize the archives with the server defined for each game.
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +48,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
|
|||||||
r, err := remote.One(g.ID)
|
r, err := remote.One(g.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, remote.ErrNoRemote) {
|
if errors.Is(err, remote.ErrNoRemote) {
|
||||||
|
fmt.Println(g.Name + ": no remote configured")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
|
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
|
||||||
@@ -85,6 +87,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
|
|||||||
destroyPg()
|
destroyPg()
|
||||||
slog.Warn("failed to push backup files", "err", err)
|
slog.Warn("failed to push backup files", "err", err)
|
||||||
}
|
}
|
||||||
|
fmt.Println(g.Name + ": pushed")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +139,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Println("already up-to-date")
|
fmt.Println(g.Name + ": already up-to-date")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +151,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
|
|||||||
return subcommands.ExitFailure
|
return subcommands.ExitFailure
|
||||||
}
|
}
|
||||||
destroyPg()
|
destroyPg()
|
||||||
|
fmt.Println(g.Name + ": pushed")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +172,7 @@ func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{})
|
|||||||
fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err)
|
fmt.Fprintln(os.Stderr, "error: failed to synchronize date:", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fmt.Println(g.Name + ": pulled")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package version
|
package version
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cloudsave/cmd/cli/tools/prompt/credentials"
|
||||||
"cloudsave/pkg/constants"
|
"cloudsave/pkg/constants"
|
||||||
"cloudsave/pkg/remote/client"
|
"cloudsave/pkg/remote/client"
|
||||||
"cloudsave/cmd/cli/tools/prompt/credentials"
|
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -23,8 +23,11 @@ type (
|
|||||||
func (*VersionCmd) Name() string { return "version" }
|
func (*VersionCmd) Name() string { return "version" }
|
||||||
func (*VersionCmd) Synopsis() string { return "show version and system information" }
|
func (*VersionCmd) Synopsis() string { return "show version and system information" }
|
||||||
func (*VersionCmd) Usage() string {
|
func (*VersionCmd) Usage() string {
|
||||||
return `add:
|
return `Usage: cloudsave version [-a]
|
||||||
Show version and system information
|
|
||||||
|
Print the version of the software
|
||||||
|
|
||||||
|
Options:
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,50 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
cache map[string]cachedInfo
|
||||||
|
|
||||||
|
cachedInfo struct {
|
||||||
|
MD5 string
|
||||||
|
Version int
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrBackupNotExists error = errors.New("backup not found")
|
ErrBackupNotExists error = errors.New("backup not found")
|
||||||
|
|
||||||
|
// singleton
|
||||||
|
hashCacheMu sync.RWMutex
|
||||||
|
hashCache cache = make(map[string]cachedInfo)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (c cache) Get(gameID string) (cachedInfo, bool) {
|
||||||
|
hashCacheMu.RLock()
|
||||||
|
defer hashCacheMu.RUnlock()
|
||||||
|
|
||||||
|
if v, ok := c[gameID]; ok {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
return cachedInfo{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cache) Register(gameID string, v cachedInfo) {
|
||||||
|
hashCacheMu.Lock()
|
||||||
|
defer hashCacheMu.Unlock()
|
||||||
|
|
||||||
|
c[gameID] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c cache) Remove(gameID string) {
|
||||||
|
hashCacheMu.Lock()
|
||||||
|
defer hashCacheMu.Unlock()
|
||||||
|
|
||||||
|
delete(c, gameID)
|
||||||
|
}
|
||||||
|
|
||||||
func Write(gameID, documentRoot string, r io.Reader) error {
|
func Write(gameID, documentRoot string, r io.Reader) error {
|
||||||
dataFolderPath := filepath.Join(documentRoot, "data", gameID)
|
dataFolderPath := filepath.Join(documentRoot, "data", gameID)
|
||||||
partPath := filepath.Join(dataFolderPath, "data.tar.gz.part")
|
partPath := filepath.Join(dataFolderPath, "data.tar.gz.part")
|
||||||
@@ -42,6 +80,7 @@ func Write(gameID, documentRoot string, r io.Reader) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashCache.Remove(gameID)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +136,7 @@ func UpdateMetadata(gameID, documentRoot string, m repository.Metadata) error {
|
|||||||
|
|
||||||
func ArchiveInfo(gameID, documentRoot, uuid string) (repository.Backup, error) {
|
func ArchiveInfo(gameID, documentRoot, uuid string) (repository.Backup, error) {
|
||||||
dataFolderPath := filepath.Join(documentRoot, "data", gameID, "hist", uuid, "data.tar.gz")
|
dataFolderPath := filepath.Join(documentRoot, "data", gameID, "hist", uuid, "data.tar.gz")
|
||||||
|
cacheID := gameID + ":" + uuid
|
||||||
|
|
||||||
finfo, err := os.Stat(dataFolderPath)
|
finfo, err := os.Stat(dataFolderPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -106,11 +146,23 @@ func ArchiveInfo(gameID, documentRoot, uuid string) (repository.Backup, error) {
|
|||||||
return repository.Backup{}, err
|
return repository.Backup{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m, ok := hashCache.Get(cacheID); ok {
|
||||||
|
return repository.Backup{
|
||||||
|
CreatedAt: finfo.ModTime(),
|
||||||
|
UUID: uuid,
|
||||||
|
MD5: m.MD5,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
h, err := hash.FileMD5(dataFolderPath)
|
h, err := hash.FileMD5(dataFolderPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return repository.Backup{}, fmt.Errorf("failed to calculate file md5: %w", err)
|
return repository.Backup{}, fmt.Errorf("failed to calculate file md5: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hashCache.Register(cacheID, cachedInfo{
|
||||||
|
MD5: h,
|
||||||
|
})
|
||||||
|
|
||||||
return repository.Backup{
|
return repository.Backup{
|
||||||
CreatedAt: finfo.ModTime(),
|
CreatedAt: finfo.ModTime(),
|
||||||
UUID: uuid,
|
UUID: uuid,
|
||||||
@@ -118,6 +170,62 @@ func ArchiveInfo(gameID, documentRoot, uuid string) (repository.Backup, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Hash(gameID, documentRoot string) (string, error) {
|
||||||
|
path := filepath.Clean(filepath.Join(documentRoot, "data", gameID))
|
||||||
|
|
||||||
|
sdir, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !sdir.IsDir() {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
v, err := getVersion(gameID, documentRoot)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read game metadata: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m, ok := hashCache.Get(gameID); ok {
|
||||||
|
if v == m.Version {
|
||||||
|
return m.MD5, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path = filepath.Join(path, "data.tar.gz")
|
||||||
|
|
||||||
|
h, err := hash.FileMD5(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
hashCache.Register(gameID, cachedInfo{
|
||||||
|
Version: v,
|
||||||
|
MD5: h,
|
||||||
|
})
|
||||||
|
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVersion(gameID, documentRoot string) (int, error) {
|
||||||
|
path := filepath.Join(documentRoot, "data", gameID, "metadata.json")
|
||||||
|
|
||||||
|
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
d := json.NewDecoder(f)
|
||||||
|
var m repository.Metadata
|
||||||
|
if err := d.Decode(&m); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Version, nil
|
||||||
|
}
|
||||||
|
|
||||||
func makeDataFolder(gameID, documentRoot string) error {
|
func makeDataFolder(gameID, documentRoot string) error {
|
||||||
if err := os.MkdirAll(filepath.Join(documentRoot, "data", gameID), 0740); err != nil {
|
if err := os.MkdirAll(filepath.Join(documentRoot, "data", gameID), 0740); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
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
|
package constants
|
||||||
|
|
||||||
const Version = "0.0.2"
|
const Version = "0.0.3"
|
||||||
|
|
||||||
const ApiVersion = 1
|
const ApiVersion = 1
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ type (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotFound error = errors.New("not found")
|
ErrNotFound error = errors.New("not found")
|
||||||
|
ErrUnauthorized error = errors.New("unauthorized (HTTP Error 401)")
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(baseURL, username, password string) *Client {
|
func New(baseURL, username, password string) *Client {
|
||||||
@@ -382,6 +383,10 @@ func (c *Client) get(url string) (obj.HTTPObject, error) {
|
|||||||
return obj.HTTPObject{}, ErrNotFound
|
return obj.HTTPObject{}, ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if res.StatusCode == 401 {
|
||||||
|
return obj.HTTPObject{}, ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
if res.StatusCode != 200 {
|
||||||
return obj.HTTPObject{}, fmt.Errorf("server returns an unexpected status code: %d %s (expected 200)", res.StatusCode, res.Status)
|
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