diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 0000000..324792e --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1,3 @@ +config.yml +README.md +.idea \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 5e57eea..20fc5d7 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -8,11 +8,11 @@ COPY go.mod go.sum ./ RUN go mod download # Add the directories which contain the golang scripts -COPY cmd ./ +COPY . . COPY config.prod.yml ./config.yml # Build -RUN CGO_ENABLED=0 GOOS=linux go build -o /api +RUN CGO_ENABLED=0 GOOS=linux go build -C cmd -o /api # Optional: # To bind to a TCP port, runtime parameters must be supplied to the docker command. diff --git a/api/cmd/main.go b/api/cmd/main.go index 107592b..7118c46 100644 --- a/api/cmd/main.go +++ b/api/cmd/main.go @@ -1,6 +1,9 @@ package main import ( + "api/controllers" + "api/repositories" + "api/services" "fmt" "github.com/gin-gonic/gin" "github.com/spf13/viper" @@ -8,63 +11,28 @@ import ( "net/http" ) -var db = make(map[string]string) - func setupRouter() *gin.Engine { - // Disable Console Color - // gin.DisableConsoleColor() + //Setup Gin r := gin.Default() + //Setup Repositories + gamesRepository := repositories.GameRepository() + gamesService := services.GameService(gamesRepository) + gamesController := controllers.GameController(gamesService) + // Ping test r.GET("/ping", func(c *gin.Context) { c.String(http.StatusOK, "pong") }) - // Get user value - r.GET("/user/:name", func(c *gin.Context) { - user := c.Params.ByName("name") - value, ok := db[user] - if ok { - c.JSON(http.StatusOK, gin.H{"user": user, "value": value}) - } else { - c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"}) - } - }) - - // Authorized group (uses gin.BasicAuth() middleware) - // Same than: - // authorized := r.Group("/") - // authorized.Use(gin.BasicAuth(gin.Credentials{ - // "foo": "bar", - // "manu": "123", - //})) - authorized := r.Group("/", gin.BasicAuth(gin.Accounts{ - "foo": "bar", // user:foo password:bar - "manu": "123", // user:manu password:123 - })) - - /* example curl for /admin with basicauth header - Zm9vOmJhcg== is base64("foo:bar") - - curl -X POST \ - http://localhost:8080/admin \ - -H 'authorization: Basic Zm9vOmJhcg==' \ - -H 'content-type: application/json' \ - -d '{"value":"bar"}' - */ - authorized.POST("admin", func(c *gin.Context) { - user := c.MustGet(gin.AuthUserKey).(string) - - // Parse JSON - var json struct { - Value string `json:"value" binding:"required"` - } - - if c.Bind(&json) == nil { - db[user] = json.Value - c.JSON(http.StatusOK, gin.H{"status": "ok"}) - } - }) + //Upload a game + r.POST("/games/", gamesController.UploadGame) + //Get all uploaded games + r.GET("/games", gamesController.GetAllGames) + //Get a specific game by its id + r.GET("/games/:id", gamesController.GetGameById) + //Delete a specific game, identified by its id + r.DELETE("/games/:id", gamesController.DeleteGameById) return r } diff --git a/api/controllers/gameController.go b/api/controllers/gameController.go new file mode 100644 index 0000000..8bcce33 --- /dev/null +++ b/api/controllers/gameController.go @@ -0,0 +1,114 @@ +package controllers + +import ( + "api/dtos" + "api/services" + "fmt" + "github.com/dranikpg/dto-mapper" + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "net/http" +) + +type IGameController interface { + GetAllGames(c *gin.Context) + GetGameById(c *gin.Context) + UploadGame(c *gin.Context) + DeleteGameById(c *gin.Context) +} + +type gameController struct { + service services.IGameService +} + +func (g gameController) GetAllGames(c *gin.Context) { + //Get Games + games, err := g.service.FindAll() + if err != nil { //TODO handle different errors + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + //Map to dto + resultDto := []dtos.GetGameByIdResponseBody{} + err = dto.Map(&resultDto, games) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, resultDto) +} + +func (g gameController) GetGameById(c *gin.Context) { + _uuid := getUUIDFromRequest(c) + if _uuid != uuid.Nil { + //Get game by uuid + game, err := g.service.FindByID(_uuid) + if err != nil { //TODO handle different errors + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + //Map to dto + resultDto := dtos.GetGameByIdResponseBody{} + err = dto.Map(&resultDto, game) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + return + } + + c.IndentedJSON(http.StatusOK, resultDto) + } +} + +func (g gameController) UploadGame(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()}) + } + + _, err = g.service.Save(file) + + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + } + + c.Header("content-location", fmt.Sprintf("%s/games/%s", c.Request.Host, file.Filename)) + c.String(http.StatusCreated, "") +} + +func (g gameController) DeleteGameById(c *gin.Context) { + _uuid := getUUIDFromRequest(c) + if _uuid != uuid.Nil { + err := g.service.Delete(_uuid) + if err != nil { //TODO handle different errors + c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error()}) + } else { + c.Status(http.StatusNoContent) + } + } + + //TODO Implement delete game + c.String(http.StatusNoContent, "") +} + +func GameController(service services.IGameService) IGameController { + return &gameController{ + service: service, + } +} + +// Parses the UUID from the request param "uuid" and returns it. +// It returns HTTP 400 and uuid.nil if the uuid is invalid or null +func getUUIDFromRequest(c *gin.Context) uuid.UUID { + _uuid, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid game ID"}) + return uuid.Nil + } else if _uuid == uuid.Nil { + c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid game ID"}) + return uuid.Nil + } + return _uuid +} diff --git a/api/dtos/gameDtos.go b/api/dtos/gameDtos.go new file mode 100644 index 0000000..91700b7 --- /dev/null +++ b/api/dtos/gameDtos.go @@ -0,0 +1,20 @@ +package dtos + +import ( + "api/shared" + "github.com/google/uuid" +) + +type GetAllGamesResponseBody struct { + ID uuid.UUID `json:"id"` + Title string `json:"title"` + Status shared.GameStatus `json:"status"` + Url string `json:"url"` +} + +type GetGameByIdResponseBody struct { + ID uuid.UUID `json:"id"` + Title string `json:"title"` + Status shared.GameStatus `json:"status"` + Url string `json:"url"` +} diff --git a/api/go.mod b/api/go.mod index c474e9f..4c78ded 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,7 +3,9 @@ module api go 1.20 require ( + github.com/dranikpg/dto-mapper v0.2.1 github.com/gin-gonic/gin v1.9.1 + github.com/google/uuid v1.4.0 github.com/spf13/viper v1.18.2 ) diff --git a/api/go.sum b/api/go.sum index 56afd6b..d61bd09 100644 --- a/api/go.sum +++ b/api/go.sum @@ -14,6 +14,8 @@ github.com/cloudwego/iasm v0.1.1/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/dranikpg/dto-mapper v0.2.1 h1:1DaphrSfBXZVlVolCP+XspMzBAFYGne91+SK594xyTg= +github.com/dranikpg/dto-mapper v0.2.1/go.mod h1:Hkidt8Lkurm7pLPYOiq3I/LlIBmDdB4J4c/VMqFXHfg= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= @@ -34,6 +36,8 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/api/models/game.go b/api/models/game.go new file mode 100644 index 0000000..1462db1 --- /dev/null +++ b/api/models/game.go @@ -0,0 +1,14 @@ +package models + +import ( + "api/shared" + "github.com/google/uuid" +) + +type Game struct { + ID uuid.UUID `json:"id"` + Title string `json:"title"` + StorageLocation string `json:"storageLocation"` + Status shared.GameStatus `json:"status"` + Url string `json:"url"` +} diff --git a/api/repositories/gameRepository.go b/api/repositories/gameRepository.go new file mode 100644 index 0000000..c0655cd --- /dev/null +++ b/api/repositories/gameRepository.go @@ -0,0 +1,66 @@ +package repositories + +import ( + "api/models" + "api/shared" + "github.com/google/uuid" +) + +type IGameRepository interface { + FindAll() ([]models.Game, error) + FindByID(id uuid.UUID) (models.Game, error) + Save(game models.Game) (models.Game, error) + Delete(id uuid.UUID) error +} + +type gameRepository struct { + //TODO add database +} + +func GameRepository() IGameRepository { + return &gameRepository{ + //TODO add database + } +} + +func (g gameRepository) FindAll() ([]models.Game, error) { + //TODO implement me + return []models.Game{ + { + ID: uuid.New(), + Title: "Mock1", + StorageLocation: "Mock1", + Status: shared.Status_Installed, + Url: "https://localhost:4200", + }, + { + ID: uuid.New(), + Title: "Mock2", + StorageLocation: "Mock2", + Status: shared.Status_Installed, + Url: "https://localhost:4200", + }, + }, nil +} + +func (g gameRepository) FindByID(id uuid.UUID) (models.Game, error) { + //TODO implement me + return models.Game{ + ID: id, + Title: "Mock", + StorageLocation: "Mock", + Status: shared.Status_Installed, + Url: "https://localhost:4200", + }, nil +} + +func (g gameRepository) Save(game models.Game) (models.Game, error) { + //TODO implement me + game.ID = uuid.New() + return game, nil +} + +func (g gameRepository) Delete(id uuid.UUID) error { + //TODO implement me + return nil +} diff --git a/api/services/gameService.go b/api/services/gameService.go new file mode 100644 index 0000000..ffb31af --- /dev/null +++ b/api/services/gameService.go @@ -0,0 +1,52 @@ +package services + +import ( + "api/models" + "api/repositories" + "api/shared" + "github.com/google/uuid" + "mime/multipart" +) + +type IGameService interface { + FindAll() ([]models.Game, error) + FindByID(id uuid.UUID) (models.Game, error) + Save(file *multipart.FileHeader) (models.Game, error) + Delete(id uuid.UUID) error +} + +type gameService struct { + repository repositories.IGameRepository +} + +func (g gameService) FindAll() ([]models.Game, error) { + return g.repository.FindAll() +} + +func (g gameService) FindByID(id uuid.UUID) (models.Game, error) { + return g.repository.FindByID(id) +} + +func (g gameService) Save(file *multipart.FileHeader) (models.Game, error) { + //TODO save file + + game := models.Game{ + ID: uuid.New(), + Title: file.Filename, + StorageLocation: "", + Status: shared.Status_New, + Url: "", + } + + return g.repository.Save(game) +} + +func (g gameService) Delete(id uuid.UUID) error { + return g.repository.Delete(id) +} + +func GameService(repository repositories.IGameRepository) IGameService { + return &gameService{ + repository: repository, + } +} diff --git a/api/shared/types.go b/api/shared/types.go new file mode 100644 index 0000000..0c09770 --- /dev/null +++ b/api/shared/types.go @@ -0,0 +1,10 @@ +package shared + +type GameStatus string + +const ( + Status_New GameStatus = "New" + Status_Installing GameStatus = "installing" + Status_Installed GameStatus = "installed" + Status_Error GameStatus = "error" +)