UPDATE: Media module
All checks were successful
Build and Release / release (push) Successful in 1m7s
All checks were successful
Build and Release / release (push) Successful in 1m7s
This commit is contained in:
@@ -2,19 +2,35 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
"history-api/pkg/storage"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type MediaService interface {
|
||||
GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, error)
|
||||
GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, error)
|
||||
SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error)
|
||||
DeleteMedia(ctx context.Context, mediaId string) error
|
||||
GetMediaByTarget(ctx context.Context, targetType string, targetId string) ([]*response.MediaResponse, error)
|
||||
DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) error
|
||||
UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error)
|
||||
GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error)
|
||||
PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error)
|
||||
@@ -22,56 +38,292 @@ type MediaService interface {
|
||||
|
||||
type mediaService struct {
|
||||
mediaRepo repositories.MediaRepository
|
||||
tokenRepo repositories.TokenRepository
|
||||
s storage.Storage
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewMediaService(
|
||||
mediaRepo repositories.MediaRepository,
|
||||
tokenRepo repositories.TokenRepository,
|
||||
s storage.Storage,
|
||||
c cache.Cache,
|
||||
) MediaService {
|
||||
return &mediaService{
|
||||
mediaRepo: mediaRepo,
|
||||
tokenRepo: tokenRepo,
|
||||
s: s,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteMedia implements [MediaService].
|
||||
func (m *mediaService) DeleteMedia(ctx context.Context, mediaId string) error {
|
||||
panic("unimplemented")
|
||||
func (m *mediaService) DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) error {
|
||||
mediaIdUUID, err := convert.StringToUUID(mediaId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
media, err := m.mediaRepo.GetByID(ctx, mediaIdUUID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
shoudDelete := false
|
||||
if slices.Contains(claims.Roles, constants.ADMIN) || slices.Contains(claims.Roles, constants.MOD) || media.UserID == claims.UId {
|
||||
shoudDelete = true
|
||||
}
|
||||
|
||||
if !shoudDelete {
|
||||
return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this media")
|
||||
}
|
||||
|
||||
err = m.mediaRepo.Delete(ctx, mediaIdUUID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
m.c.PublishTask(ctx, constants.StreamStorageName, constants.TaskTypeDeleteMedia, media.ToStorageEntity())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GeneratePresignedURL implements [MediaService].
|
||||
func (m *mediaService) GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (m *mediaService) GetMediaByID(ctx context.Context, id string) (*response.MediaResponse, error) {
|
||||
mediaId, err := convert.StringToUUID(id)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
media, err := m.mediaRepo.GetByID(ctx, mediaId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return media.ToResponse(), nil
|
||||
}
|
||||
|
||||
// GetMediaByID implements [MediaService].
|
||||
func (m *mediaService) GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (m *mediaService) GetMediaByUserID(ctx context.Context, id string) ([]*response.MediaResponse, error) {
|
||||
userId, err := convert.StringToUUID(id)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
medias, err := m.mediaRepo.GetByUserID(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return models.MediaEntitiesToResponse(medias), nil
|
||||
}
|
||||
|
||||
// GetMediaByTarget implements [MediaService].
|
||||
func (m *mediaService) GetMediaByTarget(ctx context.Context, targetType string, targetId string) ([]*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetMediaByUserID implements [MediaService].
|
||||
func (m *mediaService) GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// PreSignedCompleted implements [MediaService].
|
||||
func (m *mediaService) PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SearchMedia implements [MediaService].
|
||||
func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error) {
|
||||
panic("unimplemented")
|
||||
arg := sqlc.SearchMediasParams{
|
||||
Limit: int32(dto.Limit + 1),
|
||||
}
|
||||
|
||||
if dto.Cursor != "" {
|
||||
pgID, err := convert.StringToUUID(dto.Cursor)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
|
||||
}
|
||||
arg.Cursor = pgID
|
||||
}
|
||||
|
||||
if dto.Search != "" {
|
||||
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
|
||||
}
|
||||
|
||||
rows, err := m.mediaRepo.Search(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasMore := false
|
||||
var nextCursor string
|
||||
|
||||
if len(rows) > dto.Limit {
|
||||
hasMore = true
|
||||
nextCursor = rows[dto.Limit-1].ID
|
||||
rows = rows[:dto.Limit]
|
||||
}
|
||||
|
||||
res := &response.PaginatedResponse{
|
||||
Data: rows,
|
||||
Status: true,
|
||||
Message: "",
|
||||
}
|
||||
res.Pagination.HasMore = hasMore
|
||||
res.Pagination.NextCursor = nextCursor
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UploadServerSide implements [MediaService].
|
||||
func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
userIdUUID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Cannot open file")
|
||||
}
|
||||
defer file.Close()
|
||||
var reader io.Reader = file
|
||||
fileExt := filepath.Ext(fileHeader.Filename)
|
||||
contentType := fileHeader.Header.Get("Content-Type")
|
||||
mid, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
|
||||
}
|
||||
newFileName := mid.String() + fileExt
|
||||
originalName := fileHeader.Filename
|
||||
encodedName := url.QueryEscape(originalName)
|
||||
|
||||
dispositionType := "attachment"
|
||||
if strings.HasPrefix(contentType, "image/") || contentType == "application/pdf" {
|
||||
dispositionType = "inline"
|
||||
}
|
||||
|
||||
contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
|
||||
dispositionType,
|
||||
"file"+fileExt,
|
||||
encodedName,
|
||||
)
|
||||
|
||||
metadata := map[string]string{
|
||||
"original-name": encodedName,
|
||||
}
|
||||
|
||||
mdByte, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
|
||||
}
|
||||
|
||||
err = m.s.Upload(ctx, newFileName, reader, fileHeader.Size, storage.UploadOptions{
|
||||
ContentType: contentType,
|
||||
ContentDisposition: contentDisposition,
|
||||
Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to upload file to storage")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload file")
|
||||
}
|
||||
|
||||
media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
|
||||
UserID: userIdUUID,
|
||||
StorageKey: newFileName,
|
||||
OriginalName: originalName,
|
||||
MimeType: contentType,
|
||||
Size: fileHeader.Size,
|
||||
FileMetadata: mdByte,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return media.ToResponse(), nil
|
||||
}
|
||||
|
||||
func (m *mediaService) GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error) {
|
||||
fileExt := filepath.Ext(dto.FileName)
|
||||
mid, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
|
||||
}
|
||||
newFileName := mid.String() + fileExt
|
||||
encodedName := url.QueryEscape(dto.FileName)
|
||||
|
||||
dispositionType := "attachment"
|
||||
if dto.ContentType == "application/pdf" || (len(dto.ContentType) > 6 && dto.ContentType[:6] == "image/") {
|
||||
dispositionType = "inline"
|
||||
}
|
||||
|
||||
contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
|
||||
dispositionType, "file"+fileExt, encodedName)
|
||||
|
||||
metadata := map[string]string{
|
||||
"original-name": encodedName,
|
||||
}
|
||||
mdByte, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
|
||||
}
|
||||
|
||||
presignedURL, err := m.s.PresignUpload(ctx, newFileName, constants.PreSignedURLDuration, storage.UploadOptions{
|
||||
ContentType: dto.ContentType,
|
||||
ContentDisposition: contentDisposition,
|
||||
Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate presigned URL")
|
||||
}
|
||||
|
||||
tokenId := uuid.New().String()
|
||||
err = m.tokenRepo.CreateUploadToken(
|
||||
ctx,
|
||||
userId,
|
||||
&models.TokenUploadEntity{
|
||||
ID: tokenId,
|
||||
UserID: userId,
|
||||
StorageKey: newFileName,
|
||||
OriginalName: dto.FileName,
|
||||
MimeType: dto.ContentType,
|
||||
Size: dto.Size,
|
||||
FileMetadata: mdByte,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
|
||||
}
|
||||
|
||||
return &response.PreSignedResponse{
|
||||
TokenID: tokenId,
|
||||
UploadUrl: presignedURL,
|
||||
StorageKey: newFileName,
|
||||
SignedHeaders: map[string]string{
|
||||
"x-amz-meta-original-name": encodedName,
|
||||
"Content-Disposition": contentDisposition,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mediaService) PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error) {
|
||||
token, err := m.tokenRepo.GetUploadToken(ctx, userId, dto.TokenID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get upload token")
|
||||
}
|
||||
if token == nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
|
||||
}
|
||||
userIdUUID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = m.s.Move(
|
||||
ctx,
|
||||
&storage.MoveOptions{
|
||||
Bucket: m.s.GetTempBucket(),
|
||||
Key: token.StorageKey,
|
||||
},
|
||||
&storage.MoveOptions{
|
||||
Bucket: m.s.GetMainBucket(),
|
||||
Key: token.StorageKey,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to move file to final destination")
|
||||
}
|
||||
|
||||
media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
|
||||
UserID: userIdUUID,
|
||||
StorageKey: token.StorageKey,
|
||||
OriginalName: token.OriginalName,
|
||||
MimeType: token.MimeType,
|
||||
Size: token.Size,
|
||||
FileMetadata: token.FileMetadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create media record")
|
||||
}
|
||||
|
||||
_ = m.tokenRepo.DeleteUploadToken(ctx, userId, dto.TokenID)
|
||||
|
||||
return media.ToResponse(), nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type UserService interface {
|
||||
ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error)
|
||||
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||
SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
@@ -208,7 +208,7 @@ func (u *userService) RestoreUser(ctx context.Context, userId string) (*response
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
func (u *userService) Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||
func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||
arg := sqlc.SearchUsersParams{
|
||||
Limit: int32(dto.Limit + 1),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user