diff --git a/internal/app/mapper/media.go b/internal/app/mapper/media.go index 834d74c..953dccc 100644 --- a/internal/app/mapper/media.go +++ b/internal/app/mapper/media.go @@ -1,9 +1,55 @@ package mapper -import "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/domain/responses" +import ( + "fmt" + + "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/domain" + "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/domain/responses" +) func MakeResponsePostMedia(id int) *responses.PostMedia { return &responses.PostMedia{ ID: id, } } + +func MakeResponseGetDefaultMedia(defaultMedia *domain.DefaultMedia, media *domain.MediaFile) (*responses.GetDefaultMedia, error) { + if defaultMedia == nil || media == nil { + return nil, fmt.Errorf("got nil defaultMedia or media") + } + if defaultMedia.MediaID != media.ID { + return nil, fmt.Errorf("got not matching media_id in defaultMedia and media id: %v != %v", defaultMedia.MediaID, media.ID) + } + + resp := responses.GetDefaultMedia{} + resp.DefaultID = defaultMedia.ID + resp.ID = media.ID + resp.Key = media.Key + resp.Name = media.Name + + return &resp, nil +} + +func MakeResponseAllDefaultMedia(defaultMedias []domain.DefaultMedia, mediaFiles map[int]domain.MediaFile) (*responses.GetAllDefaultMedia, error) { + resp := responses.GetAllDefaultMedia{} + for _, defaultMedia := range defaultMedias { + media, ok := mediaFiles[defaultMedia.MediaID] + if !ok { + return nil, fmt.Errorf("can't find media for default media id %v", defaultMedia.MediaID) + } + dMedia := responses.GetDefaultMedia{} + dMedia.DefaultID = defaultMedia.ID + dMedia.ID = media.ID + dMedia.Key = media.Key + dMedia.Name = media.Name + resp.Media = append(resp.Media, dMedia) + } + return &resp, nil +} + +func MakeResponsePostDefaultMedia(id, mediaID int) *responses.PostDefaultMedia { + return &responses.PostDefaultMedia{ + ID: id, + MediaId: mediaID, + } +} diff --git a/internal/app/media.go b/internal/app/media.go index 82c52a1..ad84c5b 100644 --- a/internal/app/media.go +++ b/internal/app/media.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/app/mapper" + "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/domain" "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/domain/requests" "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/domain/responses" "github.com/sirupsen/logrus" @@ -13,7 +14,12 @@ import ( type mediaStorage interface { PutMediaFile(ctx context.Context, name string, key string, data []byte) (int, error) + GetMediaFile(_ context.Context, id int) (*domain.MediaFile, error) + GetMediaFiles(_ context.Context, ids []int) (map[int]domain.MediaFile, error) ClearUpMedia(ctx context.Context, logger *logrus.Logger) error + GetDefaultMedia(ctx context.Context, id int) (*domain.DefaultMedia, error) + GetAllDefaultMedia(ctx context.Context) ([]domain.DefaultMedia, error) + PutDefaultMedia(ctx context.Context, name string, key string, data []byte) (id int, mediaId int, err error) } type MediaService struct { @@ -54,3 +60,47 @@ func (s *MediaService) PostMediaBcrypt(ctx context.Context, req *requests.PostMe func (s *MediaService) ClearUpMedia(ctx context.Context, logger *logrus.Logger) error { return s.storage.ClearUpMedia(ctx, logger) } + +func (s *MediaService) GetMediaDefault(ctx context.Context, ID int) (*responses.GetDefaultMedia, error) { + defaultMedia, err := s.storage.GetDefaultMedia(ctx, ID) + if err != nil { + return nil, fmt.Errorf("can't storage.GetDefaultMedia: %w", err) + } + + media, err := s.storage.GetMediaFile(ctx, defaultMedia.MediaID) + if err != nil { + return nil, fmt.Errorf("can't storage.GetMediaFile: %w", err) + } + + return mapper.MakeResponseGetDefaultMedia(defaultMedia, media) +} + +func (s *MediaService) GetAllMediaDefault(ctx context.Context) (*responses.GetAllDefaultMedia, error) { + defaultMedias, err := s.storage.GetAllDefaultMedia(ctx) + if err != nil { + return nil, fmt.Errorf("can't storage.GetAllDefaultMedia: %w", err) + } + + if len(defaultMedias) == 0 { + return nil, fmt.Errorf("no default media") + } + ids := make([]int, 0, len(defaultMedias)) + for _, defaultMedia := range defaultMedias { + ids = append(ids, defaultMedia.MediaID) + } + + mediaFiles, err := s.storage.GetMediaFiles(ctx, ids) + if err != nil { + return nil, fmt.Errorf("can't storage.GetMediaFiles: %w", err) + } + + return mapper.MakeResponseAllDefaultMedia(defaultMedias, mediaFiles) +} + +func (s *MediaService) PutMediaDefault(ctx context.Context, name string, data []byte) (*responses.PostDefaultMedia, error) { + id, mediaID, err := s.storage.PutDefaultMedia(ctx, name, name, data) + if err != nil { + return nil, fmt.Errorf("can't storage.PutDefaultMedia: %v", err) + } + return mapper.MakeResponsePostDefaultMedia(id, mediaID), nil +} diff --git a/internal/domain/mediafile.go b/internal/domain/mediafile.go index f55b99e..de92373 100644 --- a/internal/domain/mediafile.go +++ b/internal/domain/mediafile.go @@ -12,3 +12,8 @@ type ClubPhoto struct { ClubID int `json:"club_id"` RefNumber int `json:"ref_number"` } + +type DefaultMedia struct { + ID int `json:"id"` + MediaID int `json:"media_id"` +} diff --git a/internal/domain/requests/get-default-media.go b/internal/domain/requests/get-default-media.go new file mode 100644 index 0000000..0354e7c --- /dev/null +++ b/internal/domain/requests/get-default-media.go @@ -0,0 +1,30 @@ +package requests + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/go-chi/chi" +) + +type GetDefaultMedia struct { + ID int `json:"id"` +} + +func (c *GetDefaultMedia) Bind(req *http.Request) error { + id, err := strconv.Atoi(chi.URLParam(req, "id")) + if err != nil { + return fmt.Errorf("can't Atoi on id in request: %w", err) + } + + c.ID = id + return c.validate() +} + +func (c *GetDefaultMedia) validate() error { + if c.ID == 0 { + return fmt.Errorf("require: id") + } + return nil +} diff --git a/internal/domain/requests/post-default-media.go b/internal/domain/requests/post-default-media.go new file mode 100644 index 0000000..806347a --- /dev/null +++ b/internal/domain/requests/post-default-media.go @@ -0,0 +1,46 @@ +package requests + +import ( + "encoding/json" + "net/http" +) + +type PostDefaultMedia struct { + PostMedia +} + +type PostMediaDefaultPointer struct { + PostMediaPointer +} + +func (m *PostDefaultMedia) Bind(req *http.Request) error { + decoder := json.NewDecoder(req.Body) + decoder.DisallowUnknownFields() + pm := PostMediaDefaultPointer{} + err := decoder.Decode(&pm) + if err != nil { + return err + } + if decoder.More() { + return err + } + err = pm.validate() + if err != nil { + return err + } + *m = PostDefaultMedia{ + PostMedia: PostMedia{ + Name: *pm.Name, + Data: pm.Data, + }, + } + return m.validate() +} + +func (pf *PostMediaDefaultPointer) validate() error { + return pf.PostMediaPointer.validate() +} + +func (m *PostDefaultMedia) validate() error { + return m.PostMedia.validate() +} diff --git a/internal/domain/responses/get-all-default-media.go b/internal/domain/responses/get-all-default-media.go new file mode 100644 index 0000000..f8f7b45 --- /dev/null +++ b/internal/domain/responses/get-all-default-media.go @@ -0,0 +1,5 @@ +package responses + +type GetAllDefaultMedia struct { + Media []GetDefaultMedia `json:"media"` +} diff --git a/internal/domain/responses/get-default-media.go b/internal/domain/responses/get-default-media.go new file mode 100644 index 0000000..522ca79 --- /dev/null +++ b/internal/domain/responses/get-default-media.go @@ -0,0 +1,8 @@ +package responses + +import "github.com/STUD-IT-team/bmstu-stud-web-backend/internal/domain" + +type GetDefaultMedia struct { + domain.MediaFile + DefaultID int `json:"default_id"` +} diff --git a/internal/domain/responses/post-default-media.go b/internal/domain/responses/post-default-media.go new file mode 100644 index 0000000..3932487 --- /dev/null +++ b/internal/domain/responses/post-default-media.go @@ -0,0 +1,6 @@ +package responses + +type PostDefaultMedia struct { + ID int `json:"id"` + MediaId int `json:"media_id"` +} diff --git a/internal/infrastructure/postgres/mediafile.go b/internal/infrastructure/postgres/mediafile.go index 6326c50..438d1ea 100644 --- a/internal/infrastructure/postgres/mediafile.go +++ b/internal/infrastructure/postgres/mediafile.go @@ -12,11 +12,11 @@ const getMediaFile = "SELECT name, key FROM mediafile WHERE id = $1 AND id > 0" func (p *Postgres) GetMediaFile(id int) (*domain.MediaFile, error) { f := domain.MediaFile{} err := p.db.QueryRow(getMediaFile, id).Scan(&f.Name, &f.Key) - if err == nil { - return &f, nil + if err != nil { + return nil, err } f.ID = id - return nil, err + return &f, nil } const getMediaFiles = "SELECT id, name, key FROM mediafile WHERE id = ANY($1) AND id > 0" @@ -120,3 +120,45 @@ func (p *Postgres) GetAllMediaKeys(ctx context.Context) ([]string, error) { } return keys, nil } + +const getDefautlMedia = "SELECT id, media_id FROM default_media WHERE id = $1" + +func (p *Postgres) GetDefautlMedia(ctx context.Context, id int) (*domain.DefaultMedia, error) { + var d domain.DefaultMedia + err := p.db.QueryRow(getDefautlMedia, id).Scan(&d.ID, &d.MediaID) + if err == nil { + return &d, nil + } + return nil, err +} + +const getAllDefaultMedia = "SELECT id, media_id FROM default_media" + +func (p *Postgres) GetAllDefaultMedia(ctx context.Context) ([]domain.DefaultMedia, error) { + defaultMedia := make([]domain.DefaultMedia, 0) + rows, err := p.db.Query(getAllDefaultMedia) + if err != nil { + return nil, err + } + + for rows.Next() { + d := domain.DefaultMedia{} + err := rows.Scan(&d.ID, &d.MediaID) + if err != nil { + return nil, err + } + defaultMedia = append(defaultMedia, d) + } + + return defaultMedia, nil +} + +const addDefaultMedia = "INSERT INTO default_media (media_id) VALUES ($1) RETURNING id" + +func (p *Postgres) AddDefaultMedia(ctx context.Context, mediaID int) (int, error) { + err := p.db.QueryRow(addDefaultMedia, mediaID).Scan(&mediaID) + if err != nil { + return 0, wrapPostgresError(err.(pgx.PgError).Code, err) + } + return mediaID, nil +} diff --git a/internal/ports/media.go b/internal/ports/media.go index 1889d3e..d683b1b 100644 --- a/internal/ports/media.go +++ b/internal/ports/media.go @@ -44,7 +44,7 @@ func (h *MediaHandler) Routes() chi.Router { r.Get("/default/{id}", h.r.Wrap(h.GetMediaDefaultByID)) r.Post("/default", h.r.Wrap(h.PostMediaDefault)) r.Delete("/default", h.r.Wrap(h.DeleteMediaDefault)) - r.Put("/default/{id}", h.r.Wrap(h.PutMediaDefault)) + r.Put("/default/{id}", h.r.Wrap(h.UpdateMediaDefault)) return r } @@ -63,7 +63,7 @@ func (h *MediaHandler) Routes() chi.Router { // @Router /media/public [post] // @Security Authorized func (h *MediaHandler) PostMediaPublic(w http.ResponseWriter, req *http.Request) handler.Response { - h.logger.Info("PostHandler: got PostMediaPublic request") + h.logger.Info("MediaHandler: got PostMediaPublic request") accessToken, err := getAccessToken(req) if err != nil { @@ -77,6 +77,8 @@ func (h *MediaHandler) PostMediaPublic(w http.ResponseWriter, req *http.Request) return handler.UnauthorizedResponse() } + h.logger.Infof("MediaHandler: authenticated: %v", resp.MemberID) + media := &requests.PostMedia{} err = media.Bind(req) @@ -85,7 +87,7 @@ func (h *MediaHandler) PostMediaPublic(w http.ResponseWriter, req *http.Request) return handler.BadRequestResponse() } - h.logger.Infof("PostHandler: parsed PostMediaPublic request") + h.logger.Infof("MediaHandler: parsed PostMediaPublic request") response, err := h.media.PostMedia(context.Background(), media) if err != nil { @@ -96,7 +98,7 @@ func (h *MediaHandler) PostMediaPublic(w http.ResponseWriter, req *http.Request) return handler.InternalServerErrorResponse() } - h.logger.Info("PostHandler: done PostMediaPublic request") + h.logger.Info("MediaHandler: done PostMediaPublic request") return handler.OkResponse(response) } @@ -115,7 +117,7 @@ func (h *MediaHandler) PostMediaPublic(w http.ResponseWriter, req *http.Request) // @Router /media/private [post] // @Security Authorized func (h *MediaHandler) PostMediaPrivate(w http.ResponseWriter, req *http.Request) handler.Response { - h.logger.Info("PostHandler: got PostMediaPrivate request") + h.logger.Info("MediaHandler: got PostMediaPrivate request") accessToken, err := getAccessToken(req) if err != nil { @@ -129,6 +131,8 @@ func (h *MediaHandler) PostMediaPrivate(w http.ResponseWriter, req *http.Request return handler.UnauthorizedResponse() } + h.logger.Infof("MediaHandler: authenticated: %v", resp.MemberID) + media := &requests.PostMedia{} err = media.Bind(req) @@ -137,7 +141,7 @@ func (h *MediaHandler) PostMediaPrivate(w http.ResponseWriter, req *http.Request return handler.BadRequestResponse() } - h.logger.Infof("PostHandler: parsed PostMediaPrivate request") + h.logger.Infof("MediaHandler: parsed PostMediaPrivate request") response, err := h.media.PostMediaBcrypt(context.Background(), media) if err != nil { @@ -148,27 +152,88 @@ func (h *MediaHandler) PostMediaPrivate(w http.ResponseWriter, req *http.Request return handler.InternalServerErrorResponse() } - h.logger.Info("PostHandler: done PostMediaPrivate request") + h.logger.Info("MediaHandler: done PostMediaPrivate request") return handler.OkResponse(response) } func (h *MediaHandler) GetMediaDefault(w http.ResponseWriter, req *http.Request) handler.Response { - return nil + h.logger.Infof("Mediahandler: got GetMediaDefault request") + + resp, err := h.media.GetAllMediaDefault(context.Background()) + if err != nil { + h.logger.Warnf("can't service.GetAllMediaDefault GetMediaDefault: %v", err) + return handler.NotFoundResponse() + } + h.logger.Infof("MediaHandler: done GetMediaDefault request") + return handler.OkResponse(resp) } func (h *MediaHandler) GetMediaDefaultByID(w http.ResponseWriter, req *http.Request) handler.Response { - return nil + h.logger.Infof("Mediahandler: got GetMediaDefaultById request") + defaultMedia := requests.GetDefaultMedia{} + + err := defaultMedia.Bind(req) + if err != nil { + h.logger.Warnf("can't parse request GetMediaDefault: %v", err) + return handler.BadRequestResponse() + } + + h.logger.Infof("MediaHandler: parsed GetMediaDefault request") + + response, err := h.media.GetMediaDefault(context.Background(), defaultMedia.ID) + + if err != nil { + h.logger.Warnf("can't service.GetMediaDefault GetMediaDefault: %v", err) + return handler.NotFoundResponse() + } + h.logger.Infof("MediaHandler: done GetMediaDefault request") + return handler.OkResponse(response) } func (h *MediaHandler) PostMediaDefault(w http.ResponseWriter, req *http.Request) handler.Response { - return nil + h.logger.Infof("Mediahandler: got PostMediaDefault request") + + accessToken, err := getAccessToken(req) + if err != nil { + h.logger.Warnf("can't get access token PostMediaPublic: %v", err) + return handler.UnauthorizedResponse() + } + + resp, err := h.guard.Check(context.Background(), &requests.CheckRequest{AccessToken: accessToken}) + if err != nil || !resp.Valid { + h.logger.Warnf("Unauthorized request: %v", err) + return handler.UnauthorizedResponse() + } + + h.logger.Infof("MediaHandler: authenticated: %v", resp.MemberID) + + defaultMedia := requests.PostDefaultMedia{} + + err = defaultMedia.Bind(req) + if err != nil { + h.logger.Warnf("can't parse request PostMediaDefault: %v", err) + return handler.BadRequestResponse() + } + + h.logger.Infof("MediaHandler: parsed PostMediaDefault request") + + response, err := h.media.PutMediaDefault(context.Background(), defaultMedia.Name, defaultMedia.Data) + if err != nil { + h.logger.Warnf("can't service.PutMediaDefault PostMediaDefault: %v", err) + if errors.Is(err, postgres.ErrPostgresUniqueConstraintViolation) { + return handler.ConflictResponse() + } + return handler.InternalServerErrorResponse() + } + h.logger.Infof("MediaHandler: done PostMediaDefault request") + return handler.OkResponse(response) } func (h *MediaHandler) DeleteMediaDefault(w http.ResponseWriter, req *http.Request) handler.Response { return nil } -func (h *MediaHandler) PutMediaDefault(w http.ResponseWriter, req *http.Request) handler.Response { +func (h *MediaHandler) UpdateMediaDefault(w http.ResponseWriter, req *http.Request) handler.Response { return nil } diff --git a/internal/storage/mediafile.go b/internal/storage/mediafile.go index 322b38f..2a0ce35 100644 --- a/internal/storage/mediafile.go +++ b/internal/storage/mediafile.go @@ -19,6 +19,9 @@ type mediaFileStorage interface { DeleteUnusedMedia(ctx context.Context, logger *logrus.Logger) error DeleteUnknownMedia(ctx context.Context, logger *logrus.Logger) error ClearUpMedia(ctx context.Context, logger *logrus.Logger) error + GetDefaultMedia(ctx context.Context, id int) (*domain.DefaultMedia, error) + GetAllDefaultMedia(ctx context.Context) ([]domain.DefaultMedia, error) + PutDefaultMedia(ctx context.Context, name string, key string, data []byte) (id int, mediaId int, err error) } func (s *storage) GetMediaFile(_ context.Context, id int) (*domain.MediaFile, error) { @@ -195,3 +198,23 @@ func (s *storage) ClearUpMedia(ctx context.Context, logger *logrus.Logger) error } return s.DeleteUnknownMedia(ctx, logger) } + +func (s *storage) GetDefaultMedia(ctx context.Context, id int) (*domain.DefaultMedia, error) { + return s.postgres.GetDefautlMedia(ctx, id) +} + +func (s *storage) GetAllDefaultMedia(ctx context.Context) ([]domain.DefaultMedia, error) { + return s.postgres.GetAllDefaultMedia(ctx) +} + +func (s *storage) PutDefaultMedia(ctx context.Context, name string, key string, data []byte) (id int, mediaId int, err error) { + id, err = s.PutMediaFile(ctx, name, key, data) + if err != nil { + return 0, 0, fmt.Errorf("can't put media file: %v", err) + } + mediaId, err = s.postgres.AddDefaultMedia(ctx, id) + if err != nil { + return 0, 0, fmt.Errorf("can't add default media: %v", err) + } + return id, mediaId, nil +} diff --git a/migrations/015_insert_default_media.sql b/migrations/015_insert_default_media.sql new file mode 100644 index 0000000..a83c4e3 --- /dev/null +++ b/migrations/015_insert_default_media.sql @@ -0,0 +1,19 @@ +-- +goose Up +-- +goose StatementBegin +INSERT INTO default_media (media_id) +VALUES +(1), +(1), +(1), +(1), +(1), +(1), +(1), +(1); + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DELETE FROM default_media +-- +goose StatementEnd