Skip to content

Commit

Permalink
avatars with base64
Browse files Browse the repository at this point in the history
  • Loading branch information
AnnHarvard committed Nov 9, 2024
1 parent c0b1e34 commit 5d79ee8
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 24 deletions.
Binary file added assets/avatars/user_77_avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func main() {
jwtSecret := os.Getenv("JWT_SECRET")
storagePath := os.Getenv("AVATAR_STORAGE_PATH")

logger.Debug("avatar_storage_path", "path", storagePath)

userRepo := userRepo.NewAuthRepository(db)
jwtHandler := jwt.NewJWT(string(jwtSecret), logger)
userUseCase := userUsecase.NewUserUsecase(userRepo, storagePath)
Expand Down
63 changes: 53 additions & 10 deletions internal/pkg/user/delivery/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ import (
"2024_2_ThereWillBeName/internal/pkg/middleware"
"2024_2_ThereWillBeName/internal/pkg/user"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log/slog"
"mime"
"net/http"
"strconv"
"strings"

"github.com/gorilla/mux"
)
Expand Down Expand Up @@ -322,38 +326,77 @@ func (h *Handler) UploadAvatar(w http.ResponseWriter, r *http.Request) {
return
}

if err := r.ParseMultipartForm(10 << 20); err != nil {
h.logger.Error("File size exceeds limit", "error", err)
var requestData struct {
Avatar string `json:"avatar"`
}

if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
h.logger.Error("Invalid JSON format", "error", err)
response := httpresponse.ErrorResponse{
Message: "File is too large",
Message: "Invalid request format",
}
httpresponse.SendJSONResponse(w, response, http.StatusBadRequest, h.logger)
return
}

file, header, err := r.FormFile("avatar")
if strings.HasPrefix(requestData.Avatar, "data:image/") {
index := strings.Index(requestData.Avatar, ",")
if index != -1 {
requestData.Avatar = requestData.Avatar[index+1:]
} else {
h.logger.Error("Invalid base64 image format", "error", "missing ',' separator")
response := httpresponse.ErrorResponse{
Message: "Invalid base64 image format",
}
httpresponse.SendJSONResponse(w, response, http.StatusBadRequest, h.logger)
return
}
}

avatarData, err := base64.StdEncoding.DecodeString(requestData.Avatar)
if err != nil {
h.logger.Error("Error reading avatar file", "error", err)
h.logger.Error("Failed to decode base64 image", "error", err)
response := httpresponse.ErrorResponse{
Message: "Invalid base64 image data",
}
httpresponse.SendJSONResponse(w, response, http.StatusBadRequest, h.logger)
return
}

fileType := http.DetectContentType(avatarData)
h.logger.Debug("Detected file type", "fileType", fileType)

if !strings.HasPrefix(fileType, "image/") {
h.logger.Error("Invalid file type", "fileType", fileType)
response := httpresponse.ErrorResponse{
Message: "Invalid file upload",
Message: "Only image files are allowed",
}
httpresponse.SendJSONResponse(w, response, http.StatusBadRequest, h.logger)
return
}
defer file.Close()

h.logger.Debug("Uploading avatar", "userID", userID, "fileName", header.Filename)
ext, err := mime.ExtensionsByType(fileType)
if err != nil || len(ext) == 0 {
h.logger.Error("Unable to determine file extension", "mimeType", fileType)
response := httpresponse.ErrorResponse{
Message: "Unable to determine file extension",
}
httpresponse.SendJSONResponse(w, response, http.StatusBadRequest, h.logger)
return
}

avatarFileName := fmt.Sprintf("user_%d_avatar%s", userID, ext[0])

h.logger.Debug("Uploading avatar", "userID", userID, "avatarFileName", avatarFileName)

var avatarPath string
if avatarPath, err = h.usecase.UploadAvatar(context.Background(), uint(userID), file, header); err != nil {
if avatarPath, err = h.usecase.UploadAvatar(context.Background(), uint(userID), avatarData, avatarFileName); err != nil {
h.logger.Error("Failed to upload avatar", "userID", userID, "error", err)

response := httpresponse.ErrorResponse{
Message: "Failed to upload avatar",
}
httpresponse.SendJSONResponse(w, response, http.StatusInternalServerError, h.logger)
return
}

response := map[string]string{
Expand Down
3 changes: 1 addition & 2 deletions internal/pkg/user/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ package user
import (
"2024_2_ThereWillBeName/internal/models"
"context"
"mime/multipart"
)

//go:generate mockgen -source=interfaces.go -destination=mocks/mock.go
type UserUsecase interface {
SignUp(ctx context.Context, user models.User) (uint, error)
Login(ctx context.Context, login, password string) (models.User, error)
UploadAvatar(ctx context.Context, userID uint, avatarFile multipart.File, header *multipart.FileHeader) (string, error)
UploadAvatar(ctx context.Context, userID uint, avatarData []byte, avatarFileName string) (string, error)
GetProfile(ctx context.Context, userID, requesterID uint) (models.UserProfile, error)
UpdatePassword(ctx context.Context, userData models.User, newPassword string) error
UpdateProfile(ctx context.Context, userID uint, login, email string) error
Expand Down
27 changes: 15 additions & 12 deletions internal/pkg/user/usecase/user_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import (
"context"
"errors"
"fmt"
"io"
"log"
"mime/multipart"
"os"
"path/filepath"

Expand All @@ -22,18 +20,19 @@ type UserUsecaseImpl struct {

func NewUserUsecase(repo user.UserRepo, storagePath string) *UserUsecaseImpl {
return &UserUsecaseImpl{
repo: repo,
repo: repo,
storagePath: storagePath,
}
}

func saveAvatarFile(avatarFile multipart.File, path string) error {
func saveAvatarData(avatarData []byte, path string) error {
outFile, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create avatar file: %w", models.ErrInternal)
}
defer outFile.Close()

if _, err := io.Copy(outFile, avatarFile); err != nil {
if _, err := outFile.Write(avatarData); err != nil {
return fmt.Errorf("failed to write avatar content: %w", models.ErrInternal)
}

Expand Down Expand Up @@ -64,7 +63,7 @@ func (a *UserUsecaseImpl) Login(ctx context.Context, email, password string) (mo
return user, nil
}

func (a *UserUsecaseImpl) UploadAvatar(ctx context.Context, userID uint, avatarFile multipart.File, header *multipart.FileHeader) (string, error) {
func (a *UserUsecaseImpl) UploadAvatar(ctx context.Context, userID uint, avatarData []byte, avatarFileName string) (string, error) {
avatarPath, err := a.repo.GetAvatarPathByUserId(ctx, userID)
if err != nil {
if errors.Is(err, models.ErrNotFound) {
Expand All @@ -73,16 +72,20 @@ func (a *UserUsecaseImpl) UploadAvatar(ctx context.Context, userID uint, avatarF
return "", fmt.Errorf("internal error: %w", models.ErrInternal)
}

fileExt := filepath.Ext(header.Filename)

avatarFileName := fmt.Sprintf("user_%d_avatar%s", userID, fileExt)
realAvatarPath := filepath.Join(a.storagePath, avatarFileName)

if err := saveAvatarFile(avatarFile, realAvatarPath); err != nil {
return err.Error(), err
if avatarPath != "" && avatarPath != avatarFileName {
oldAvatarPath := filepath.Join(a.storagePath, avatarPath)
if err := os.Remove(oldAvatarPath); err != nil {
return "", fmt.Errorf("failed to delete old avatar file: %w", err)
}
}

if err := saveAvatarData(avatarData, realAvatarPath); err != nil {
return "", fmt.Errorf("failed to save avatar file: %w", err)
}

if avatarPath == "" {
if avatarPath == "" || avatarPath != avatarFileName {
if err := a.repo.UpdateAvatarPathByUserId(ctx, userID, avatarFileName); err != nil {
return "", fmt.Errorf("failed to update avatar path in database: %w", models.ErrInternal)
}
Expand Down

0 comments on commit 5d79ee8

Please sign in to comment.