From b2425d310b232ba2e2e2149f75b987937f16dcad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lie=20DELHAIE?= Date: Mon, 18 Aug 2025 19:38:42 +0200 Subject: [PATCH] fix things --- cmd/server/api/api.go | 112 ++++++----------------------------- cmd/server/api/responses.go | 75 ++++++++--------------- cmd/server/runner.go | 18 +++++- pkg/constants/constants.go | 2 +- pkg/data/data.go | 6 +- pkg/repository/repository.go | 23 ++++++- 6 files changed, 82 insertions(+), 154 deletions(-) diff --git a/cmd/server/api/api.go b/cmd/server/api/api.go index 816a199..4f80bf2 100644 --- a/cmd/server/api/api.go +++ b/cmd/server/api/api.go @@ -3,7 +3,6 @@ package api import ( "cloudsave/pkg/data" "cloudsave/pkg/repository" - "encoding/json" "errors" "fmt" "log/slog" @@ -36,7 +35,7 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string, } router := chi.NewRouter() router.NotFound(func(writer http.ResponseWriter, request *http.Request) { - notFound("This route does not exist", writer, request) + notFound("id not found", writer, request) }) router.MethodNotAllowed(func(writer http.ResponseWriter, request *http.Request) { methodNotAllowed(writer, request) @@ -81,43 +80,13 @@ func NewServer(documentRoot string, srv *data.Service, creds map[string]string, } func (s HTTPServer) all(w http.ResponseWriter, r *http.Request) { - path := filepath.Join(s.documentRoot, "data") - datastore := make([]repository.Metadata, 0) - - if _, err := os.Stat(path); err != nil { - if errors.Is(err, os.ErrNotExist) { - ok(datastore, w, r) - return - } - fmt.Fprintln(os.Stderr, "failed to open datastore (", s.documentRoot, "):", err) - internalServerError(w, r) - return - } - - ds, err := os.ReadDir(path) + datastore, err := s.Service.AllGames() if err != nil { - fmt.Fprintln(os.Stderr, "failed to open datastore (", s.documentRoot, "):", err) + slog.Error(err.Error()) internalServerError(w, r) return } - for _, d := range ds { - content, err := os.ReadFile(filepath.Join(path, d.Name(), "metadata.json")) - if err != nil { - slog.Error("error: failed to load metadata.json", "err", err) - continue - } - - var m repository.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) } @@ -125,32 +94,19 @@ 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) + fi, err := os.Stat(filepath.Join(path, "data.tar.gz")) 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) + f, err := s.Service.Repository().ReadBlob(repository.NewGameIdentifier(id)) 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() { + slog.Error(err.Error()) internalServerError(w, r) return } + defer f.Close() // Set headers w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"") @@ -274,32 +230,19 @@ func (s HTTPServer) histDownload(w http.ResponseWriter, r *http.Request) { uuid := chi.URLParam(r, "uuid") path := filepath.Clean(filepath.Join(s.documentRoot, "data", id, "hist", uuid)) - sdir, err := os.Stat(path) + fi, err := os.Stat(filepath.Join(path, "data.tar.gz")) 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) + f, err := s.Service.Repository().ReadBlob(repository.NewBackupIdentifier(id, uuid)) 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() { + slog.Error(err.Error()) internalServerError(w, r) return } + defer f.Close() // Set headers w.Header().Set("Content-Disposition", "attachment; filename=\"data.tar.gz\"") @@ -348,37 +291,16 @@ func (s HTTPServer) hash(w http.ResponseWriter, r *http.Request) { func (s HTTPServer) metadata(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) + metadata, err := s.Service.One(id) if err != nil { - notFound("id not found", w, r) - return - } - - if !sdir.IsDir() { - notFound("id not found", w, r) - return - } - - path = filepath.Join(path, "metadata.json") - - f, err := os.OpenFile(path, os.O_RDONLY, 0) - if err != nil { - notFound("id not found", w, r) - return - } - defer f.Close() - - var metadata repository.Metadata - d := json.NewDecoder(f) - err = d.Decode(&metadata) - if err != nil { - fmt.Fprintln(os.Stderr, "error: an error occured while reading data:", err) + if errors.Is(err, repository.ErrNotFound) { + notFound("id not found", w, r) + return + } + slog.Error(err.Error()) internalServerError(w, r) return } - ok(metadata, w, r) } diff --git a/cmd/server/api/responses.go b/cmd/server/api/responses.go index 474fc40..43bb4ee 100644 --- a/cmd/server/api/responses.go +++ b/cmd/server/api/responses.go @@ -3,13 +3,13 @@ package api import ( "cloudsave/pkg/remote/obj" "encoding/json" - "log" + "log/slog" "net/http" "time" ) func internalServerError(w http.ResponseWriter, r *http.Request) { - e := obj.HTTPError{ + payload := obj.HTTPError{ HTTPCore: obj.HTTPCore{ Status: http.StatusInternalServerError, Path: r.RequestURI, @@ -19,20 +19,16 @@ func internalServerError(w http.ResponseWriter, r *http.Request) { 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) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) } } func notFound(message string, w http.ResponseWriter, r *http.Request) { - e := obj.HTTPError{ + payload := obj.HTTPError{ HTTPCore: obj.HTTPCore{ Status: http.StatusNotFound, Path: r.RequestURI, @@ -42,20 +38,16 @@ func notFound(message string, w http.ResponseWriter, r *http.Request) { 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) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) } } func methodNotAllowed(w http.ResponseWriter, r *http.Request) { - e := obj.HTTPError{ + payload := obj.HTTPError{ HTTPCore: obj.HTTPCore{ Status: http.StatusMethodNotAllowed, Path: r.RequestURI, @@ -65,20 +57,16 @@ func methodNotAllowed(w http.ResponseWriter, r *http.Request) { 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) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) } } func unauthorized(w http.ResponseWriter, r *http.Request) { - e := obj.HTTPError{ + payload := obj.HTTPError{ HTTPCore: obj.HTTPCore{ Status: http.StatusUnauthorized, Path: r.RequestURI, @@ -88,21 +76,17 @@ func unauthorized(w http.ResponseWriter, r *http.Request) { 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) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) } } func ok(o interface{}, w http.ResponseWriter, r *http.Request) { - e := obj.HTTPObject{ + payload := obj.HTTPObject{ HTTPCore: obj.HTTPCore{ Status: http.StatusOK, Path: r.RequestURI, @@ -110,20 +94,15 @@ func ok(o interface{}, w http.ResponseWriter, r *http.Request) { }, Data: o, } - - 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) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) } } func badRequest(message string, w http.ResponseWriter, r *http.Request) { - e := obj.HTTPError{ + payload := obj.HTTPError{ HTTPCore: obj.HTTPCore{ Status: http.StatusBadRequest, Path: r.RequestURI, @@ -133,14 +112,10 @@ func badRequest(message string, w http.ResponseWriter, r *http.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) + e := json.NewEncoder(w) + if err := e.Encode(payload); err != nil { + slog.Error(err.Error()) } } diff --git a/cmd/server/runner.go b/cmd/server/runner.go index 3e532f2..18deffd 100644 --- a/cmd/server/runner.go +++ b/cmd/server/runner.go @@ -8,6 +8,7 @@ import ( "cloudsave/pkg/repository" "flag" "fmt" + "log/slog" "path/filepath" "runtime" "strconv" @@ -18,18 +19,27 @@ func run() { var documentRoot string var port int - var noCache bool + var noCache, verbose bool 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.BoolVar(&noCache, "no-cache", false, "Disable the cache") + flag.BoolVar(&verbose, "verbose", false, "Show more logs") flag.Parse() + if verbose { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + + slog.Info("loading .htpasswd") h, err := htpasswd.Open(filepath.Join(documentRoot, ".htpasswd")) if err != nil { fatal("failed to load .htpasswd: "+err.Error(), 1) } + slog.Info("users loaded: " + strconv.Itoa(len(h.Content())) + " user(s) loaded") + var repo repository.Repository - if noCache { + if !noCache { + slog.Info("loading eager repository...") r, err := repository.NewEagerRepository(filepath.Join(documentRoot, "data")) if err != nil { fatal("failed to load datastore: "+err.Error(), 1) @@ -39,17 +49,19 @@ func run() { } repo = r } else { + slog.Info("loading lazy repository...") repo, err = repository.NewLazyRepository(filepath.Join(documentRoot, "data")) if err != nil { fatal("failed to load datastore: "+err.Error(), 1) } } + slog.Info("repository loaded") s := data.NewService(repo) server := api.NewServer(documentRoot, s, h.Content(), port) - fmt.Println("starting server at :" + strconv.Itoa(port)) + fmt.Println("server started at :" + strconv.Itoa(port)) if err := server.Server.ListenAndServe(); err != nil { fatal("failed to start server: "+err.Error(), 1) } diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index c2577b7..a4ddb48 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -1,5 +1,5 @@ package constants -const Version = "0.0.4a" +const Version = "0.0.4b" const ApiVersion = 1 diff --git a/pkg/data/data.go b/pkg/data/data.go index 417fad3..93c4304 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -268,8 +268,6 @@ func (l Service) PullBackup(gameID, backupID string, cli *client.Client) error { return fmt.Errorf("failed to pull backup: %w", err) } - - return nil } @@ -372,6 +370,10 @@ func (l Service) ApplyBackup(gameID, backupID string) error { return l.apply(filepath.Join(path, "data.tar.gz"), g.Path) } +func (l Service) Repository() repository.Repository { + return l.repo +} + func (l Service) apply(src, dst string) error { if err := os.RemoveAll(dst); err != nil { return fmt.Errorf("failed to remove old save: %w", err) diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go index 36d1e0a..563a7d6 100644 --- a/pkg/repository/repository.go +++ b/pkg/repository/repository.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "log/slog" "os" "path/filepath" "time" @@ -74,7 +75,7 @@ type ( Metadata(gameID GameIdentifier) (Metadata, error) LastScan(gameID GameIdentifier) (time.Time, error) - ReadBlob(gameID Identifier) (io.Reader, error) + ReadBlob(gameID Identifier) (io.ReadSeekCloser, error) Backup(id BackupIdentifier) (Backup, error) Remote(id GameIdentifier) (*Remote, error) @@ -132,10 +133,16 @@ func NewLazyRepository(dataRootPath string) (*LazyRepository, error) { } func (l *LazyRepository) Mkdir(id Identifier) error { - return os.MkdirAll(l.DataPath(id), 0740) + path := l.DataPath(id) + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + slog.Debug("making directory", "path", path, "id", id, "perm", "0740") + return os.MkdirAll(path, 0740) + } + return nil } func (l *LazyRepository) All() ([]string, error) { + slog.Debug("loading all current data...") dir, err := os.ReadDir(l.dataRoot) if err != nil { return nil, fmt.Errorf("failed to open directory: %w", err) @@ -152,6 +159,7 @@ func (l *LazyRepository) All() ([]string, error) { func (l *LazyRepository) AllHist(id GameIdentifier) ([]string, error) { path := l.DataPath(id) + slog.Debug("loading hist data...", "id", id) dir, err := os.ReadDir(filepath.Join(path, "hist")) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -171,6 +179,7 @@ func (l *LazyRepository) AllHist(id GameIdentifier) ([]string, error) { func (l *LazyRepository) WriteBlob(ID Identifier) (io.Writer, error) { path := l.DataPath(ID) + slog.Debug("loading write buffer...", "id", ID) dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) if err != nil { return nil, fmt.Errorf("failed to open destination file: %w", err) @@ -182,6 +191,7 @@ func (l *LazyRepository) WriteBlob(ID Identifier) (io.Writer, error) { func (l *LazyRepository) WriteMetadata(id GameIdentifier, m Metadata) error { path := l.DataPath(id) + slog.Debug("writing metadata", "id", id, "metadata", m) dst, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) if err != nil { return fmt.Errorf("failed to open destination file: %w", err) @@ -199,6 +209,7 @@ func (l *LazyRepository) WriteMetadata(id GameIdentifier, m Metadata) error { func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) { path := l.DataPath(id) + slog.Debug("loading metadata", "id", id) src, err := os.OpenFile(filepath.Join(path, "metadata.json"), os.O_RDONLY, 0) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -220,6 +231,7 @@ func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) { return Metadata{}, fmt.Errorf("failed to open archive: %w", err) } + slog.Debug("loading md5 hash", "id", id) m.MD5, err = hash.FileMD5(filepath.Join(path, "data.tar.gz")) if err != nil { return Metadata{}, fmt.Errorf("failed to calculate md5: %w", err) @@ -231,6 +243,7 @@ func (l *LazyRepository) Metadata(id GameIdentifier) (Metadata, error) { func (l *LazyRepository) Backup(id BackupIdentifier) (Backup, error) { path := l.DataPath(id) + slog.Debug("loading hist metadata", "id", id) fs, err := os.Stat(filepath.Join(path, "data.tar.gz")) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -239,6 +252,7 @@ func (l *LazyRepository) Backup(id BackupIdentifier) (Backup, error) { return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err) } + slog.Debug("loading md5 hash", "id", id) h, err := hash.FileMD5(filepath.Join(path, "data.tar.gz")) if err != nil { return Backup{}, fmt.Errorf("corrupted datastore: failed to open metadata: %w", err) @@ -274,6 +288,7 @@ func (l *LazyRepository) LastScan(id GameIdentifier) (time.Time, error) { func (l *LazyRepository) ResetLastScan(id GameIdentifier) error { path := l.DataPath(id) + slog.Debug("resetting last scan datetime for", "id", id) f, err := os.OpenFile(filepath.Join(path, ".last_run"), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0740) if err != nil { return fmt.Errorf("failed to open file: %w", err) @@ -289,9 +304,10 @@ func (l *LazyRepository) ResetLastScan(id GameIdentifier) error { return nil } -func (l *LazyRepository) ReadBlob(id Identifier) (io.Reader, error) { +func (l *LazyRepository) ReadBlob(id Identifier) (io.ReadSeekCloser, error) { path := l.DataPath(id) + slog.Debug("loading read buffer...", "id", id) dst, err := os.OpenFile(filepath.Join(path, "data.tar.gz"), os.O_RDONLY, 0) if err != nil { return nil, fmt.Errorf("failed to open blob: %w", err) @@ -344,6 +360,7 @@ func (l *LazyRepository) Remote(id GameIdentifier) (*Remote, error) { func (l *LazyRepository) Remove(id GameIdentifier) error { path := l.DataPath(id) + slog.Debug("removing data", "id", id) if err := os.RemoveAll(path); err != nil { return fmt.Errorf("failed to remove game folder from the datastore: %w", err) }