server
This commit is contained in:
@@ -12,20 +12,18 @@ import (
|
||||
|
||||
type (
|
||||
ListCmd struct {
|
||||
name string
|
||||
}
|
||||
)
|
||||
|
||||
func (*ListCmd) Name() string { return "list" }
|
||||
func (*ListCmd) Synopsis() string { return "list all game registered" }
|
||||
func (*ListCmd) Usage() string {
|
||||
return `add:
|
||||
return `list:
|
||||
List all game registered
|
||||
`
|
||||
}
|
||||
|
||||
func (p *ListCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.StringVar(&p.name, "name", "", "Override the name of the game")
|
||||
}
|
||||
|
||||
func (p *ListCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
|
||||
@@ -2,7 +2,6 @@ package sync
|
||||
|
||||
import (
|
||||
"cloudsave/pkg/remote"
|
||||
"cloudsave/pkg/sync/ssh"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -28,15 +27,15 @@ func (p *SyncCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (p *SyncCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
remotes, err := remote.All()
|
||||
_, err := remote.All()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: failed to load datastore:", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
for _, remote := range remotes {
|
||||
ssh.SFTPSyncer{}.Sync(remote)
|
||||
}
|
||||
/*for _, remote := range remotes {
|
||||
|
||||
}*/
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
38
cmd/cli/commands/version/version.go
Normal file
38
cmd/cli/commands/version/version.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"cloudsave/pkg/constants"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/subcommands"
|
||||
)
|
||||
|
||||
type (
|
||||
VersionCmd struct {
|
||||
}
|
||||
)
|
||||
|
||||
func (*VersionCmd) Name() string { return "version" }
|
||||
func (*VersionCmd) Synopsis() string { return "show version and system information" }
|
||||
func (*VersionCmd) Usage() string {
|
||||
return `add:
|
||||
Show version and system information
|
||||
`
|
||||
}
|
||||
|
||||
func (p *VersionCmd) SetFlags(f *flag.FlagSet) {
|
||||
}
|
||||
|
||||
func (p *VersionCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
|
||||
fmt.Println("Client: CloudSave cli")
|
||||
fmt.Println(" Version: " + constants.Version)
|
||||
fmt.Println(" API version: " + strconv.Itoa(constants.ApiVersion))
|
||||
fmt.Println(" Go version: " + runtime.Version())
|
||||
fmt.Println(" OS/Arch: " + runtime.GOOS + "/" + runtime.GOARCH)
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"cloudsave/cmd/cli/commands/remove"
|
||||
"cloudsave/cmd/cli/commands/run"
|
||||
"cloudsave/cmd/cli/commands/sync"
|
||||
"cloudsave/cmd/cli/commands/version"
|
||||
"context"
|
||||
"flag"
|
||||
"os"
|
||||
@@ -18,6 +19,7 @@ func main() {
|
||||
subcommands.Register(subcommands.HelpCommand(), "help")
|
||||
subcommands.Register(subcommands.FlagsCommand(), "help")
|
||||
subcommands.Register(subcommands.CommandsCommand(), "help")
|
||||
subcommands.Register(&version.VersionCmd{}, "help")
|
||||
|
||||
subcommands.Register(&add.AddCmd{}, "management")
|
||||
subcommands.Register(&run.RunCmd{}, "management")
|
||||
|
||||
181
cmd/server/api/api.go
Normal file
181
cmd/server/api/api.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"cloudsave/pkg/game"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type (
|
||||
HTTPServer struct {
|
||||
Server *http.Server
|
||||
documentRoot string
|
||||
}
|
||||
)
|
||||
|
||||
// NewServer start the http server
|
||||
func NewServer(documentRoot string, creds map[string]string, port int) *HTTPServer {
|
||||
if !filepath.IsAbs(documentRoot) {
|
||||
panic("the document root is not an absolute path")
|
||||
}
|
||||
s := &HTTPServer{
|
||||
documentRoot: documentRoot,
|
||||
}
|
||||
router := chi.NewRouter()
|
||||
router.NotFound(func(writer http.ResponseWriter, request *http.Request) {
|
||||
notFound("This route does not exist", writer, request)
|
||||
})
|
||||
router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) {
|
||||
methodNotAllowed(writer, request)
|
||||
})
|
||||
router.Use(middleware.Logger)
|
||||
router.Use(recoverMiddleware)
|
||||
router.Use(middleware.Compress(5, "application/gzip"))
|
||||
router.Use(BasicAuth("cloudsave", creds))
|
||||
router.Use(middleware.Heartbeat("/heartbeat"))
|
||||
router.Route("/api", func(routerAPI chi.Router) {
|
||||
routerAPI.Route("/v1", func(r chi.Router) {
|
||||
// Get information about the server
|
||||
r.Get("/version", s.Information)
|
||||
// Secured routes
|
||||
r.Group(func(secureRouter chi.Router) {
|
||||
// Save files routes
|
||||
secureRouter.Route("/games", func(gameRouter chi.Router) {
|
||||
// List all available saves
|
||||
gameRouter.Get("/", s.all)
|
||||
// Data routes
|
||||
gameRouter.Group(func(uploadRouter chi.Router) {
|
||||
uploadRouter.Post("/{id}/data", s.upload)
|
||||
uploadRouter.Get("/{id}/data", s.download)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
s.Server = &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", port),
|
||||
Handler: router,
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s HTTPServer) all(w http.ResponseWriter, r *http.Request) {
|
||||
ds, err := os.ReadDir(s.documentRoot)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "failed to open datastore (", s.documentRoot, "):", err)
|
||||
internalServerError(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
datastore := make([]game.Metadata, 0)
|
||||
for _, d := range ds {
|
||||
content, err := os.ReadFile(filepath.Join(s.documentRoot, d.Name(), "metadata.json"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var m game.Metadata
|
||||
err = json.Unmarshal(content, &m)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "corrupted datastore: failed to parse %s/metadata.json: %s", d.Name(), err)
|
||||
internalServerError(w, r)
|
||||
}
|
||||
|
||||
datastore = append(datastore, m)
|
||||
}
|
||||
|
||||
ok(datastore, w, r)
|
||||
}
|
||||
|
||||
func (s HTTPServer) download(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
path := filepath.Clean(filepath.Join(s.documentRoot, "data", id))
|
||||
|
||||
sdir, err := os.Stat(path)
|
||||
if err != nil {
|
||||
notFound("id not found", w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !sdir.IsDir() {
|
||||
notFound("id not found", w, r)
|
||||
return
|
||||
}
|
||||
|
||||
path = filepath.Join(path, "data.tar.gz")
|
||||
|
||||
f, err := os.OpenFile(path, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
notFound("id not found", w, r)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Get file info to set headers
|
||||
fi, err := f.Stat()
|
||||
if err != nil || fi.IsDir() {
|
||||
internalServerError(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Set headers
|
||||
w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"")
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(fi.Size(), 10))
|
||||
w.WriteHeader(200)
|
||||
|
||||
// Stream the file content
|
||||
http.ServeContent(w, r, "data.tar.gz", fi.ModTime(), f)
|
||||
}
|
||||
|
||||
func (s HTTPServer) upload(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
path := filepath.Clean(filepath.Join(s.documentRoot, "data", id, "data.tar.gz"))
|
||||
|
||||
// Limit max upload size (e.g., 500 MB)
|
||||
r.Body = http.MaxBytesReader(w, r.Body, 500<<20)
|
||||
|
||||
// Parse multipart form
|
||||
err := r.ParseMultipartForm(500 << 20) // 500 MB
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: failed to load payload:", err)
|
||||
badRequest("bad payload", w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve file
|
||||
file, _, err := r.FormFile("payload")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: cannot find payload in the form:", err)
|
||||
badRequest("payload not found", w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Create destination file
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0640)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: failed to open file:", err)
|
||||
internalServerError(w, r)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Copy the uploaded content to the file
|
||||
if _, err := io.Copy(f, file); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error: an error occured while downloading data:", err)
|
||||
internalServerError(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond success
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
46
cmd/server/api/middlewares.go
Normal file
46
cmd/server/api/middlewares.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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) {
|
||||
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
|
||||
unauthorized(w, r)
|
||||
}
|
||||
187
cmd/server/api/responses.go
Normal file
187
cmd/server/api/responses.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
httpCore struct {
|
||||
Status int `json:"status"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
httpError struct {
|
||||
httpCore
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
httpObject struct {
|
||||
httpCore
|
||||
Data any `json:"data"`
|
||||
}
|
||||
)
|
||||
|
||||
func internalServerError(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
httpCore: httpCore{
|
||||
Status: http.StatusInternalServerError,
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Error: "Internal Server Error",
|
||||
Message: "The server encountered an unexpected condition that prevented it from fulfilling the request.",
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func notFound(message string, w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
httpCore: httpCore{
|
||||
Status: http.StatusNotFound,
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Error: "Not Found",
|
||||
Message: message,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
httpCore: httpCore{
|
||||
Status: http.StatusMethodNotAllowed,
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Error: "Method Not Allowed",
|
||||
Message: "The server knows the request method, but the target resource doesn't support this method",
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func unauthorized(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
httpCore: httpCore{
|
||||
Status: http.StatusUnauthorized,
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Error: "Unauthorized",
|
||||
Message: "The request has not been completed because it lacks valid authentication credentials for the requested resource.",
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Header().Add("WWW-Authenticate", "Custom realm=\"loginUserHandler via /api/login\"")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func forbidden(w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
httpCore: httpCore{
|
||||
Status: http.StatusForbidden,
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Error: "Forbidden",
|
||||
Message: "The access is permanently forbidden and tied to the application logic, such as insufficient rights to a resource.",
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ok(obj interface{}, w http.ResponseWriter, r *http.Request) {
|
||||
e := httpObject{
|
||||
httpCore: httpCore{
|
||||
Status: http.StatusOK,
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Data: obj,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func badRequest(message string, w http.ResponseWriter, r *http.Request) {
|
||||
e := httpError{
|
||||
httpCore: httpCore{
|
||||
Status: http.StatusBadRequest,
|
||||
Path: r.RequestURI,
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Error: "Bad Request",
|
||||
Message: message,
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, err = w.Write(payload)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
26
cmd/server/api/system.go
Normal file
26
cmd/server/api/system.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"cloudsave/pkg/constants"
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type information struct {
|
||||
Version string `json:"version"`
|
||||
APIVersion int `json:"api_version"`
|
||||
GoVersion string `json:"go_version"`
|
||||
OSName string `json:"os_name"`
|
||||
OSArchitecture string `json:"os_architecture"`
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Information(w http.ResponseWriter, r *http.Request) {
|
||||
info := information{
|
||||
Version: constants.Version,
|
||||
APIVersion: constants.ApiVersion,
|
||||
GoVersion: runtime.Version(),
|
||||
OSName: runtime.GOOS,
|
||||
OSArchitecture: runtime.GOARCH,
|
||||
}
|
||||
ok(info, w, r)
|
||||
}
|
||||
19
cmd/server/main.go
Normal file
19
cmd/server/main.go
Normal file
@@ -0,0 +1,19 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const defaultDocumentRoot string = "/var/lib/cloudsave"
|
||||
|
||||
func main() {
|
||||
run()
|
||||
}
|
||||
|
||||
func fatal(message string, exitCode int) {
|
||||
fmt.Fprintln(os.Stderr, message)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
17
cmd/server/main_windows.go
Normal file
17
cmd/server/main_windows.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cloudsave/pkg/tools/windows"
|
||||
"os"
|
||||
)
|
||||
|
||||
const defaultDocumentRoot string = "C:/ProgramData/CloudSave"
|
||||
|
||||
func main() {
|
||||
run()
|
||||
}
|
||||
|
||||
func fatal(message string, exitCode int) {
|
||||
windows.MessageBox(windows.NULL, message, "CloudSave", windows.MB_OK)
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
34
cmd/server/runner.go
Normal file
34
cmd/server/runner.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cloudsave/cmd/server/api"
|
||||
"cloudsave/cmd/server/security/htpasswd"
|
||||
"cloudsave/pkg/constants"
|
||||
"flag"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func run() {
|
||||
fmt.Printf("CloudSave server -- v%s.%s.%s\n\n", constants.Version, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
var documentRoot string
|
||||
var port int
|
||||
flag.StringVar(&documentRoot, "document-root", defaultDocumentRoot, "Define the path to the document root")
|
||||
flag.IntVar(&port, "port", 8080, "Define the port of the server")
|
||||
flag.Parse()
|
||||
|
||||
h, err := htpasswd.Open(filepath.Join(documentRoot, ".htpasswd"))
|
||||
if err != nil {
|
||||
fatal("failed to load .htpasswd: "+err.Error(), 1)
|
||||
}
|
||||
|
||||
server := api.NewServer(documentRoot, h.Content(), port)
|
||||
|
||||
fmt.Println("starting server at :" + strconv.Itoa(port))
|
||||
if err := server.Server.ListenAndServe(); err != nil {
|
||||
fatal("failed to start server: "+err.Error(), 1)
|
||||
}
|
||||
}
|
||||
37
cmd/server/security/htpasswd/htpasswd.go
Normal file
37
cmd/server/security/htpasswd/htpasswd.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package htpasswd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
File struct {
|
||||
data map[string]string
|
||||
}
|
||||
)
|
||||
|
||||
func Open(path string) (File, error) {
|
||||
c, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return File{}, err
|
||||
}
|
||||
|
||||
f := File{
|
||||
data: make(map[string]string),
|
||||
}
|
||||
creds := strings.Split(string(c), "\n")
|
||||
for _, cred := range creds {
|
||||
kv := strings.Split(cred, ":")
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
f.data[kv[0]] = kv[1]
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (f File) Content() map[string]string {
|
||||
return f.data
|
||||
}
|
||||
Reference in New Issue
Block a user