diff --git a/internal/server/api/api.go b/internal/server/api/api.go index 132bf8e..8228e12 100644 --- a/internal/server/api/api.go +++ b/internal/server/api/api.go @@ -30,7 +30,7 @@ func Kickoff(logger *slog.Logger, env bootstrap.Environment, db *gorm.DB) { } api := r.Group("/api") - routes.RegisterApiRoutes(api, db) + routes.RegisterApiRoutes(api, env) // also register the 2 api subroutes routes.RegisterKeyRoutes(api, db) routes.RegisterCtrlRoutes(api, db) diff --git a/internal/server/api/assets/response.go b/internal/server/api/assets/response.go index a198aaf..fc05849 100644 --- a/internal/server/api/assets/response.go +++ b/internal/server/api/assets/response.go @@ -1,6 +1,7 @@ package assets import ( + "fmt" "net/http" "time" @@ -8,11 +9,16 @@ import ( ) const ( - OkMes string = "OK" - IntErrMes string = "An internal error occured, contact your administrator" + OkMes string = "OK" + CreationMes string = "Object successfully created" + DeletionMes string = "Object successfully deleted" + NotFoundMes string = "Requested object not found" + BadRequestMes string = "Request did not satisfy requirements" + ConflictMes string = "Duplicate object" + IntErrMes string = "An internal error occured, contact your administrator" ) -type BasicResponse struct { +type ResponseObject struct { Msg string `json:"msg"` Data any `json:"data"` } @@ -33,13 +39,73 @@ type FileResponse struct { ID int `json:"id"` MetaName string `json:"metaName"` FileName string `json:"fileName"` + FilePath string `json:"filePath"` + Checksum string `json:"checksum"` MediaType string `json:"mediaType"` CreatedAt time.Time `json:"createdAt"` - ExpiresAt time.Time `json:"expiresAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func FileDownloadResponse(c *gin.Context, fp string) { + c.File(fp) +} + +func BasicResponse(c *gin.Context, data ...any) { + fmt.Println(len(data)) + switch len(data) { + case 0: + // empty slice so just an ok message + c.JSON(http.StatusOK, ResponseObject{ + Msg: OkMes, + }) + // single object in our slice (even if the object itself is a slice) + case 1: + c.JSON(http.StatusOK, ResponseObject{ + Msg: OkMes, + Data: data[0], + }) + // multiple objects inside our slice + default: + c.JSON(http.StatusOK, ResponseObject{ + Msg: OkMes, + Data: data, + }) + } +} + +func CreationResponse(c *gin.Context, data any) { + c.JSON(http.StatusCreated, ResponseObject{ + Msg: CreationMes, + Data: data, + }) +} + +func DeletionResponse(c *gin.Context) { + c.JSON(http.StatusOK, ResponseObject{ + Msg: DeletionMes, + }) +} + +func NotFoundResponse(c *gin.Context) { + c.JSON(http.StatusNotFound, ResponseObject{ + Msg: NotFoundMes, + }) +} + +func BadRequestResponse(c *gin.Context) { + c.JSON(http.StatusBadRequest, ResponseObject{ + Msg: BadRequestMes, + }) +} + +func DuplicateResponse(c *gin.Context) { + c.JSON(http.StatusConflict, ResponseObject{ + Msg: ConflictMes, + }) } func InternalErrorResponse(c *gin.Context) { - c.JSON(http.StatusInternalServerError, BasicResponse{ + c.JSON(http.StatusInternalServerError, ResponseObject{ Msg: IntErrMes, }) } diff --git a/internal/server/api/middleware/middleware.go b/internal/server/api/middleware/middleware.go index 24bc078..f213d9c 100644 --- a/internal/server/api/middleware/middleware.go +++ b/internal/server/api/middleware/middleware.go @@ -42,7 +42,7 @@ func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authorizationHeader := c.GetHeader("Authorization") if len(authorizationHeader) == 0 { - c.AbortWithStatusJSON(http.StatusUnauthorized, assets.BasicResponse{ + c.AbortWithStatusJSON(http.StatusUnauthorized, assets.ResponseObject{ Msg: "Authorization header is required", }) return @@ -52,7 +52,7 @@ func AuthMiddleware() gin.HandlerFunc { // The header must be a specific format, 0 being the bearer text and 1 being the token itself, making it 2 pieces total // In the following if statement we verify both parts if the part after Bearer is empty its only 1 part for example if len(headerParts) != 2 || headerParts[0] != "Bearer" { - c.AbortWithStatusJSON(http.StatusUnauthorized, assets.BasicResponse{ + c.AbortWithStatusJSON(http.StatusUnauthorized, assets.ResponseObject{ Msg: "Authorization header is invalid", }) return diff --git a/internal/server/api/routes/api.go b/internal/server/api/routes/api.go deleted file mode 100644 index 74b6881..0000000 --- a/internal/server/api/routes/api.go +++ /dev/null @@ -1,16 +0,0 @@ -package routes - -import ( - "github.com/gin-gonic/gin" - "gorm.io/gorm" -) - -func RegisterApiRoutes(api *gin.RouterGroup, db *gorm.DB) { - api.GET("/version", func(c *gin.Context) { - - }) - - api.GET("/codename", func(c *gin.Context) { - - }) -} diff --git a/internal/server/api/routes/apiroute.go b/internal/server/api/routes/apiroute.go new file mode 100644 index 0000000..224aecb --- /dev/null +++ b/internal/server/api/routes/apiroute.go @@ -0,0 +1,21 @@ +package routes + +import ( + "log/slog" + "orbits-server/internal/server/api/assets" + "orbits-server/internal/server/bootstrap" + + "github.com/gin-gonic/gin" +) + +func RegisterApiRoutes(api *gin.RouterGroup, env bootstrap.Environment) { + api.GET("/version", func(c *gin.Context) { + slog.Debug("received request for the version endpoint") + assets.BasicResponse(c, env.Version) + }) + + api.GET("/codename", func(c *gin.Context) { + slog.Debug("received request for the codename endpoint") + assets.BasicResponse(c, env.Codename) + }) +} diff --git a/internal/server/api/routes/ctrl.go b/internal/server/api/routes/ctrlroute.go similarity index 87% rename from internal/server/api/routes/ctrl.go rename to internal/server/api/routes/ctrlroute.go index 05b0e9b..888b6b1 100644 --- a/internal/server/api/routes/ctrl.go +++ b/internal/server/api/routes/ctrlroute.go @@ -2,7 +2,6 @@ package routes import ( "log/slog" - "net/http" "orbits-server/internal/server/api/assets" "orbits-server/internal/server/service" @@ -24,10 +23,7 @@ func RegisterCtrlRoutes(api *gin.RouterGroup, db *gorm.DB) { return } - c.JSON(http.StatusOK, assets.BasicResponse{ - Msg: assets.OkMes, - Data: command, - }) + assets.BasicResponse(c, command) }) ctrl.PATCH("/command", func(c *gin.Context) { diff --git a/internal/server/api/routes/file.go b/internal/server/api/routes/fileroute.go similarity index 60% rename from internal/server/api/routes/file.go rename to internal/server/api/routes/fileroute.go index 6bf71d1..b57e777 100644 --- a/internal/server/api/routes/file.go +++ b/internal/server/api/routes/fileroute.go @@ -1,8 +1,8 @@ package routes import ( + "errors" "log/slog" - "net/http" "orbits-server/internal/server/api/assets" "orbits-server/internal/server/bootstrap" "orbits-server/internal/server/service" @@ -18,25 +18,23 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go // for example: /file/ // file download route / display contents - file.GET("/:filename", func(c *gin.Context) { - fileParam := c.Param("filename") - p := filepath.Join(env.ContentDirectory, fileParam) + file.GET("/:fileName", func(c *gin.Context) { + fileParam := c.Param("fileName") + fp := filepath.Join(env.ContentDirectory, fileParam) - c.File(p) + assets.FileDownloadResponse(c, fp) }) // upload route file.POST("/upload", func(c *gin.Context) { - fh, err := c.FormFile("file") + fileHeader, err := c.FormFile("file") if err != nil { slog.Debug("no file or file headers provided on the request", "error", err) - c.JSON(http.StatusBadRequest, assets.BasicResponse{ - Msg: "a file is required", - }) + assets.BadRequestResponse(c) return } - stream, err := fh.Open() + stream, err := fileHeader.Open() if err != nil { slog.Error("failed to a reader stream") assets.InternalErrorResponse(c) @@ -44,19 +42,25 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go } defer stream.Close() - fileRecord, err := fileService.Create(stream, fh.Filename) + fileResponse, err := fileService.Create(stream, fileHeader.Filename) if err != nil { slog.Error("failed to enroll file to the database", "error", err) + + if errors.Is(err, gorm.ErrDuplicatedKey) { + assets.DuplicateResponse(c) + return + } + assets.InternalErrorResponse(c) return } // save to filesystem after everything has given a green light - if err := c.SaveUploadedFile(fh, fileRecord.FilePath); err != nil { + if err := c.SaveUploadedFile(fileHeader, fileResponse.FilePath); err != nil { slog.Error("failed to save to disk, rolling back database", "error", err) // rollback db if the write has failed - fileService.DeleteByID(fileRecord.ID) + fileService.DeleteRecordByID(fileResponse.ID) // give the response to the client assets.InternalErrorResponse(c) @@ -64,32 +68,25 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go } slog.Info("saved file to local filesystem and database") - c.JSON(http.StatusCreated, assets.BasicResponse{ - Msg: "file has succesfully been uploaded", - Data: fileRecord, - }) + assets.CreationResponse(c, fileResponse) }) // delete route file.DELETE("/:filename", func(c *gin.Context) { fileParam := c.Param("filename") - fileRecord, err := fileService.DeleteByName(fileParam) - if err != nil { + + if err := fileService.DeleteByName(fileParam); err != nil { slog.Error("file not found", "error", err) - c.JSON(http.StatusNotFound, assets.BasicResponse{ - Msg: "file was not found", - }) + assets.NotFoundResponse(c) return } - slog.Info("received a delete request for a file", "file", fileRecord, "filename", fileParam) - c.JSON(http.StatusOK, assets.BasicResponse{ - Msg: "file deleted succesfully", - }) + slog.Info("received a delete request for a file", "fileName", fileParam) + assets.DeletionResponse(c) }) // define a route to check what is registered - file.GET("/available", func(c *gin.Context) { + file.GET("/list", func(c *gin.Context) { files, err := fileService.ListFiles() if err != nil { slog.Error("failed to retrieve available files", "error", err) @@ -97,9 +94,6 @@ func RegisterFileRoutes(file *gin.RouterGroup, env bootstrap.Environment, db *go return } - c.JSON(http.StatusOK, assets.BasicResponse{ - Msg: assets.OkMes, - Data: files, - }) + assets.BasicResponse(c, files) }) } diff --git a/internal/server/api/routes/key.go b/internal/server/api/routes/keyroute.go similarity index 69% rename from internal/server/api/routes/key.go rename to internal/server/api/routes/keyroute.go index d455d31..92208ff 100644 --- a/internal/server/api/routes/key.go +++ b/internal/server/api/routes/keyroute.go @@ -2,7 +2,6 @@ package routes import ( "log/slog" - "net/http" "orbits-server/internal/server/api/assets" "orbits-server/internal/server/service" @@ -36,13 +35,18 @@ func RegisterKeyRoutes(api *gin.RouterGroup, db *gorm.DB) { slog.Info("saved key to database") - c.JSON(http.StatusCreated, assets.BasicResponse{ - Msg: "key has succesfully been created and saved", - Data: keyResponse, - }) + assets.CreationResponse(c, keyResponse) }) - key.DELETE("/:key", func(c *gin.Context) { + key.DELETE("/:keyName", func(c *gin.Context) { + keyParam := c.Param("keyName") + if err := keyService.DeleteByName(keyParam); err != nil { + slog.Error("key not found", "error", err) + assets.NotFoundResponse(c) + } + + slog.Info("received a delete request for a key", "keyName", keyParam) + assets.DeletionResponse(c) }) } diff --git a/internal/server/database/functions.go b/internal/server/database/functions.go index a2f279e..fc871e0 100644 --- a/internal/server/database/functions.go +++ b/internal/server/database/functions.go @@ -30,6 +30,12 @@ func ListKeys(db *gorm.DB) ([]AccessKey, error) { return keys, err } +func FindKeyByName(db *gorm.DB, name string) (AccessKey, error) { + var key AccessKey + err := db.Where("key_name = ?", name).First(&key).Error + return key, err +} + func CreateKey(db *gorm.DB, k *AccessKey) error { return db.Create(&k).Error } diff --git a/internal/server/service/fileservice.go b/internal/server/service/fileservice.go index 70d33e6..50b39e1 100644 --- a/internal/server/service/fileservice.go +++ b/internal/server/service/fileservice.go @@ -20,55 +20,68 @@ func NewFileService(db *gorm.DB, env bootstrap.Environment) *FileService { } func (s *FileService) ListFiles() ([]assets.FileResponse, error) { - files, err := database.ListFiles(s.db) + fileRecords, err := database.ListFiles(s.db) if err != nil { return nil, err } - resp := make([]assets.FileResponse, 0, len(files)) + resp := make([]assets.FileResponse, 0, len(fileRecords)) - for _, f := range files { + for _, f := range fileRecords { resp = append(resp, assets.FileResponse{ ID: f.ID, MetaName: f.MetaName, - FileName: f.FileName, MediaType: string(f.MediaType), + FileName: f.FileName, + FilePath: f.FilePath, + Checksum: f.Checksum, CreatedAt: f.CreatedAt, - ExpiresAt: f.ExpiresAt, + UpdatedAt: f.ExpiresAt, }) } return resp, nil } -func (s *FileService) Create(r io.Reader, filename string) (database.File, error) { - fileRecord, err := database.BuildFileRecord(r, filename, s.env.ContentDirectory) +func (s *FileService) Create(r io.Reader, filename string) (assets.FileResponse, error) { + f, err := database.BuildFileRecord(r, filename, s.env.ContentDirectory) if err != nil { - return database.File{}, err + return assets.FileResponse{}, err } - if err := database.CreateFile(s.db, &fileRecord); err != nil { - return database.File{}, err + if err := database.CreateFile(s.db, &f); err != nil { + return assets.FileResponse{}, err } - return fileRecord, nil + resp := assets.FileResponse{ + ID: f.ID, + MetaName: f.MetaName, + MediaType: string(f.MediaType), + FileName: f.FileName, + FilePath: f.FilePath, + Checksum: f.Checksum, + CreatedAt: f.CreatedAt, + UpdatedAt: f.UpdatedAt, + } + + return resp, nil } -func (s *FileService) DeleteByName(filename string) (database.File, error) { +func (s *FileService) DeleteByName(filename string) error { fileRecord, err := database.FindFileByName(s.db, filename) if err != nil { - return database.File{}, err + return err } _ = os.Remove(fileRecord.FilePath) if err := database.DeleteFileByID(s.db, fileRecord.ID); err != nil { - return database.File{}, err + return err } - return fileRecord, nil + return nil } -func (s *FileService) DeleteByID(id int) { - _ = database.DeleteFileByID(s.db, id) +func (s *FileService) DeleteRecordByID(id int) error { + return database.DeleteFileByID(s.db, id) } diff --git a/internal/server/service/keyservice.go b/internal/server/service/keyservice.go index 8f9a10b..f6fb1e5 100644 --- a/internal/server/service/keyservice.go +++ b/internal/server/service/keyservice.go @@ -50,6 +50,15 @@ func (s *KeyService) Create(name string, expiresAt time.Time) (assets.KeyRespons return keyResponse, nil } -func (s *KeyService) DeleteByName(name string) { +func (s *KeyService) DeleteByName(name string) error { + keyRecord, err := database.FindKeyByName(s.db, name) + if err != nil { + return err + } + if err := database.DeleteKeyByID(s.db, keyRecord.ID); err != nil { + return err + } + + return nil }