From 6b414284d4380e2a80b42ce27f79415e42644d75 Mon Sep 17 00:00:00 2001 From: kene Date: Thu, 5 Dec 2024 15:49:45 +0100 Subject: [PATCH] updated file uploader --- .env.example | 5 ++- config/conf.go | 17 ++++------ go.mod | 3 ++ go.sum | 6 ++++ managers/books.go | 19 +++++++---- models/accounts.go | 10 ------ models/book.go | 24 -------------- routes/books.go | 37 ++++++++++++---------- routes/file_uploader.go | 70 ++++++++++++++++++++++------------------- schemas/base.go | 4 +-- schemas/book.go | 6 ++-- schemas/profiles.go | 6 ++-- 12 files changed, 98 insertions(+), 109 deletions(-) diff --git a/.env.example b/.env.example index cb44a04..7091b7f 100644 --- a/.env.example +++ b/.env.example @@ -45,4 +45,7 @@ BOOK_COVER_IMAGES_BUCKET= USER_IMAGES_BUCKET= ID_FRONT_IMAGES_BUCKET= ID_BACK_IMAGES_BUCKET= -PGADMIN_PASSWORD= \ No newline at end of file +PGADMIN_PASSWORD= +CLOUDINARY_CLOUD_NAME= +CLOUDINARY_API_KEY= +CLOUDINARY_API_SECRET= \ No newline at end of file diff --git a/config/conf.go b/config/conf.go index 5c33f23..bc70f2d 100644 --- a/config/conf.go +++ b/config/conf.go @@ -45,17 +45,12 @@ type Config struct { StripeCheckoutSuccessUrlPath string `mapstructure:"STRIPE_CHECKOUT_SUCCESS_URL_PATH"` StripeWebhookSecret string `mapstructure:"STRIPE_WEBHOOK_SECRET"` SocketSecret string `mapstructure:"SOCKET_SECRET"` - S3AccessKey string `mapstructure:"S3_ACCESS_KEY"` - S3SecretKey string `mapstructure:"S3_SECRET_KEY"` - S3EndpointUrl string `mapstructure:"S3_ENDPOINT_URL"` - BookCoverImagesBucket string `mapstructure:"BOOK_COVER_IMAGES_BUCKET"` - UserImagesBucket string `mapstructure:"USER_IMAGES_BUCKET"` - IDFrontImagesBucket string `mapstructure:"ID_FRONT_IMAGES_BUCKET"` - IDBackImagesBucket string `mapstructure:"ID_BACK_IMAGES_BUCKET"` - ICPPrivateKey string `mapstructure:"ICP_PRIVATE_KEY"` - ICPPublicKey string `mapstructure:"ICP_PUBLIC_KEY"` - - PGAdminPassword string `mapstructure:"PGADMIN_PASSWORD"` + ICPPrivateKey string `mapstructure:"ICP_PRIVATE_KEY"` + ICPPublicKey string `mapstructure:"ICP_PUBLIC_KEY"` + PGAdminPassword string `mapstructure:"PGADMIN_PASSWORD"` + CloudinaryCloudName string `mapstructure:"CLOUDINARY_CLOUD_NAME"` + CloudinaryApiKey string `mapstructure:"CLOUDINARY_API_KEY"` + CloudinaryApiSecret string `mapstructure:"CLOUDINARY_API_SECRET"` } func GetConfig() (config Config) { diff --git a/go.mod b/go.mod index 90af3d6..9153e02 100644 --- a/go.mod +++ b/go.mod @@ -38,8 +38,10 @@ require ( github.com/aviate-labs/leb128 v0.3.0 // indirect github.com/aviate-labs/secp256k1 v0.0.0-5e6736a // indirect github.com/bits-and-blooms/bitset v1.7.0 // indirect + github.com/cloudinary/cloudinary-go/v2 v2.9.0 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.2-0.20240215234832-d72fcb379d3e // indirect + github.com/creasty/defaults v1.7.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.6.0 // indirect @@ -60,6 +62,7 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/s2a-go v0.1.7 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/gorilla/schema v1.4.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect diff --git a/go.sum b/go.sum index a60b7dc..a4faf5b 100644 --- a/go.sum +++ b/go.sum @@ -25,12 +25,16 @@ github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHl github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudinary/cloudinary-go/v2 v2.9.0 h1:8C76QklmuV4qmKAC7cUnu9D68X9kCkFMuLspPikECCo= +github.com/cloudinary/cloudinary-go/v2 v2.9.0/go.mod h1:ireC4gqVetsjVhYlwjUJwKTbZuWjEIynbR9zQTlqsvo= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.12.2-0.20240215234832-d72fcb379d3e h1:MKdOuCiy2DAX1tMp2YsmtNDaqdigpY6B5cZQDJ9BvEo= github.com/consensys/gnark-crypto v0.12.2-0.20240215234832-d72fcb379d3e/go.mod h1:wKqwsieaKPThcFkHe0d0zMsbHEUWFmZcG7KBCse210o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creasty/defaults v1.7.0 h1:eNdqZvc5B509z18lD8yc212CAqJNvfT1Jq6L8WowdBA= +github.com/creasty/defaults v1.7.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM= 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= @@ -174,6 +178,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= diff --git a/managers/books.go b/managers/books.go index 54b9779..def708b 100644 --- a/managers/books.go +++ b/managers/books.go @@ -122,18 +122,21 @@ func (b BookManager) Create(db *gorm.DB, author models.User, data schemas.BookCr return book } -func (b BookManager) Update(db *gorm.DB, book models.Book, data schemas.BookCreateSchema, genre models.Genre, Tags []models.Tag) models.Book { +func (b BookManager) Update(db *gorm.DB, book models.Book, data schemas.BookCreateSchema, genre models.Genre, coverImage string, Tags []models.Tag) models.Book { book.Title = data.Title book.Blurb = data.Blurb book.AgeDiscretion = data.AgeDiscretion book.GenreID = genre.ID book.Genre = genre book.Tags = Tags + if coverImage != "" { + book.CoverImage = coverImage + } db.Omit("Tags.*").Save(&book) return book } -func (b BookManager) SetContract(db *gorm.DB, book models.Book, data schemas.ContractCreateSchema) models.Book { +func (b BookManager) SetContract(db *gorm.DB, book models.Book, idFrontImage string, idBackImage string, data schemas.ContractCreateSchema) models.Book { book.FullName = data.FullName book.Email = data.Email book.PenName = data.PenName @@ -152,10 +155,14 @@ func (b BookManager) SetContract(db *gorm.DB, book models.Book, data schemas.Con book.Synopsis = data.Synopsis book.Outline = data.Outline book.IntendedContract = data.IntendedContract - book.FullPurchaseMode = data.FullPurchaseMode - username := book.Author.Username - book.IDFrontImage = username - book.IDBackImage = username + book.FullPurchaseMode = data.FullPurchaseMode + if idFrontImage != "" { + book.IDFrontImage = idFrontImage + } + if idBackImage != "" { + book.IDBackImage = idBackImage + } + if book.ContractStatus == choices.CTS_DECLINED { book.ContractStatus = choices.CTS_UPDATED } diff --git a/models/accounts.go b/models/accounts.go index a36ba86..e9dd55d 100644 --- a/models/accounts.go +++ b/models/accounts.go @@ -48,15 +48,6 @@ func (user User) SubscriptionExpired() bool { return time.Now().After(*user.SubscriptionExpiry) } -func (u User) AvatarUrl() *string { - avatar := u.Avatar - if avatar != "" { - avatarUrl := fmt.Sprintf("%s/%s/%s", cfg.S3EndpointUrl, cfg.UserImagesBucket, u.Avatar) - return &avatarUrl - } - return &avatar -} - func (user User) BooksCount() int { return len(user.Books) } @@ -75,7 +66,6 @@ func (user User) FullName() string { func (user *User) BeforeCreate(tx *gorm.DB) (err error) { user.Password = utils.HashPassword(user.Password) - user.Avatar = user.Username return } diff --git a/models/book.go b/models/book.go index 125e071..954b722 100644 --- a/models/book.go +++ b/models/book.go @@ -1,7 +1,6 @@ package models import ( - "fmt" "strings" "github.com/LitPad/backend/models/choices" @@ -81,28 +80,6 @@ type Book struct { ContractStatus choices.ContractStatusChoice `gorm:"default:PENDING"` } -func (b Book) CoverImageUrl() string { - return fmt.Sprintf("%s/%s/%s", cfg.S3EndpointUrl, cfg.BookCoverImagesBucket, b.CoverImage) -} - -func (b Book) IDFrontImageUrl() *string { - imageText := b.IDFrontImage - if imageText != "" { - image := fmt.Sprintf("%s/%s/%s", cfg.S3EndpointUrl, cfg.IDFrontImagesBucket, imageText) - return &image - } - return nil -} - -func (b Book) IDBackImageUrl() *string { - imageText := b.IDBackImage - if imageText != "" { - image := fmt.Sprintf("%s/%s/%s", cfg.S3EndpointUrl, cfg.IDBackImagesBucket, imageText) - return &image - } - return nil -} - func (b Book) ViewsCount() int { views := b.Views if len(views) > 0 { @@ -150,7 +127,6 @@ func (b *Book) GenerateUniqueSlug(tx *gorm.DB) string { func (b *Book) BeforeCreate(tx *gorm.DB) (err error) { slug := b.GenerateUniqueSlug(tx) b.Slug = slug - b.CoverImage = slug return } diff --git a/routes/books.go b/routes/books.go index c7689f8..dc97283 100644 --- a/routes/books.go +++ b/routes/books.go @@ -247,9 +247,9 @@ func (ep Endpoint) CreateBook(c *fiber.Ctx) error { return c.Status(422).JSON(err) } - book := bookManager.Create(db, *author, data, genre, "", tags) // Upload File - UploadFile(file, book.CoverImage, cfg.BookCoverImagesBucket) + coverImage := UploadFile(file, "BOOK_COVER_IMAGES") + book := bookManager.Create(db, *author, data, genre, coverImage, tags) response := schemas.BookResponseSchema{ ResponseSchema: ResponseMessage("Book created successfully"), Data: schemas.BookSchema{}.Init(book), @@ -307,11 +307,14 @@ func (ep Endpoint) UpdateBook(c *fiber.Ctx) error { return c.Status(422).JSON(err) } - updatedBook := bookManager.Update(db, *book, data, genre, tags) // Upload File + coverImage := "" if file != nil { - UploadFile(file, updatedBook.CoverImage, cfg.BookCoverImagesBucket) + coverImage = UploadFile(file, "BOOK_COVER_IMAGES") } + + updatedBook := bookManager.Update(db, *book, data, genre, coverImage, tags) + response := schemas.BookResponseSchema{ ResponseSchema: ResponseMessage("Book updated successfully"), Data: schemas.BookSchema{}.Init(updatedBook), @@ -902,25 +905,27 @@ func (ep Endpoint) SetContract(c *fiber.Ctx) error { if book.FullName == "" { imageRequired = true } - id_front_image_file, id_front_image_file_err := ValidateImage(c, "id_front_image", imageRequired) - if id_front_image_file_err != nil { - return c.Status(422).JSON(id_front_image_file_err) + idFrontImageFile, idFrontImageFileErr := ValidateImage(c, "id_front_image", imageRequired) + if idFrontImageFileErr != nil { + return c.Status(422).JSON(idFrontImageFileErr) } - id_back_image_file, id_back_image_file_err := ValidateImage(c, "id_back_image", imageRequired) - if id_back_image_file_err != nil { - return c.Status(422).JSON(id_back_image_file_err) + idBackImageFile, idBackImageFileErr := ValidateImage(c, "id_back_image", imageRequired) + if idBackImageFileErr != nil { + return c.Status(422).JSON(idBackImageFileErr) } - updatedBook := bookManager.SetContract(db, *book, data) - // Upload File - if id_front_image_file != nil { - UploadFile(id_front_image_file, updatedBook.IDFrontImage, cfg.IDFrontImagesBucket) + var idFrontImage string + var idBackImage string + if idFrontImageFile != nil { + idFrontImage = UploadFile(idFrontImageFile, "ID_FRONT_IMAGES") } - if id_back_image_file != nil { - UploadFile(id_back_image_file, updatedBook.IDBackImage, cfg.IDBackImagesBucket) + if idBackImageFile != nil { + idBackImage = UploadFile(idBackImageFile, "ID_BACK_IMAGES") } + + updatedBook := bookManager.SetContract(db, *book, idFrontImage, idBackImage, data) response := schemas.ContractResponseSchema{ ResponseSchema: ResponseMessage("Contract set successfully"), Data: schemas.ContractSchema{}.Init(updatedBook), diff --git a/routes/file_uploader.go b/routes/file_uploader.go index 13a115f..8a2f739 100644 --- a/routes/file_uploader.go +++ b/routes/file_uploader.go @@ -1,51 +1,55 @@ package routes import ( + "context" + "fmt" "log" "mime/multipart" "net/http" "github.com/LitPad/backend/utils" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/gofiber/fiber/v2" + + "github.com/cloudinary/cloudinary-go/v2" + "github.com/cloudinary/cloudinary-go/v2/api/uploader" ) -var sess *session.Session - -func init() { - var err error - // Configure the S3 client - sess, err = session.NewSession(&aws.Config{ - Region: aws.String("us-east-1"), // Dummy region, replace if necessary - Credentials: credentials.NewStaticCredentials(cfg.S3AccessKey, cfg.S3SecretKey, ""), - Endpoint: aws.String(cfg.S3EndpointUrl), - S3ForcePathStyle: aws.Bool(true), - }) +var cld *cloudinary.Cloudinary +var err error + +func init () { + // Initialize Cloudinary client + cld, err = cloudinary.NewFromParams(cfg.CloudinaryCloudName, cfg.CloudinaryApiKey, cfg.CloudinaryApiSecret) if err != nil { - log.Fatalf("Failed to create session: %v", err) + fmt.Println("failed to initialize Cloudinary client: %w", err) } } -// uploadToCloudinary uploads the file to Cloudinary and returns the URL of the uploaded file -func UploadFile(fileHeader *multipart.FileHeader, key string, folder string) { - file, err := fileHeader.Open() +func UploadFile(file *multipart.FileHeader, folder string) string { + if cfg.Debug { + folder = fmt.Sprintf("test/%s", folder) + } else { + folder = fmt.Sprintf("live/%s", folder) + } + + // Open the file + src, err := file.Open() if err != nil { - log.Println("failed to open file") + fmt.Println("failed to open file: %w", err) + return "" } - defer file.Close() - uploader := s3manager.NewUploader(sess) - - _, err = uploader.Upload(&s3manager.UploadInput{ - Bucket: aws.String(folder), - Key: aws.String(key), - Body: file, - }) + defer src.Close() + + // Upload the file to Cloudinary + uploadResult, err := cld.Upload.Upload(context.Background(), src, uploader.UploadParams{Folder: folder}) if err != nil { - log.Println("failed to upload file") - } + fmt.Println("failed to upload to Cloudinary: %w", err) + return "" + } + log.Println("Hahalalalla") + + // Return the secure URL of the uploaded file + return uploadResult.SecureURL } func ValidateImage(c *fiber.Ctx, name string, required bool) (*multipart.FileHeader, *utils.ErrorResponse) { @@ -68,7 +72,7 @@ func ValidateImage(c *fiber.Ctx, name string, required bool) (*multipart.FileHea if err != nil { return nil, &errData } - + defer fileHandle.Close() // Read the first 512 bytes for content type detection @@ -81,8 +85,8 @@ func ValidateImage(c *fiber.Ctx, name string, required bool) (*multipart.FileHea // Detect the content type contentType := http.DetectContentType(buffer) switch contentType { - case "image/jpeg", "image/png", "image/gif": - return file, nil + case "image/jpeg", "image/png", "image/gif": + return file, nil } return nil, &errData } diff --git a/schemas/base.go b/schemas/base.go index 99993f6..42fd109 100644 --- a/schemas/base.go +++ b/schemas/base.go @@ -24,12 +24,12 @@ type UserDataSchema struct { // For short user data FullName string `json:"full_name"` Username string `json:"username"` - Avatar *string `json:"avatar"` + Avatar string `json:"avatar"` } func (u UserDataSchema) Init(user models.User) UserDataSchema { u.FullName = user.FullName() u.Username = user.Username - u.Avatar = user.AvatarUrl() + u.Avatar = user.Avatar return u } \ No newline at end of file diff --git a/schemas/book.go b/schemas/book.go index f6cc379..c3e95a9 100644 --- a/schemas/book.go +++ b/schemas/book.go @@ -110,7 +110,7 @@ func (b BookSchema) Init(book models.Book) BookSchema { b.PartialViewChapter = &chapter } - b.CoverImage = book.CoverImageUrl() + b.CoverImage = book.CoverImage b.Views = book.ViewsCount() b.CreatedAt = book.CreatedAt b.UpdatedAt = book.UpdatedAt @@ -301,8 +301,8 @@ func (c ContractSchema) Init(book models.Book) ContractSchema { c.ContractStatus = book.ContractStatus c.FullPrice = book.FullPrice c.ChapterPrice = book.ChapterPrice - c.IDFrontImage = *book.IDFrontImageUrl() - c.IDBackImage = *book.IDBackImageUrl() + c.IDFrontImage = book.IDFrontImage + c.IDBackImage = book.IDBackImage return c } diff --git a/schemas/profiles.go b/schemas/profiles.go index 69a79e4..5be71fe 100644 --- a/schemas/profiles.go +++ b/schemas/profiles.go @@ -20,7 +20,7 @@ type FollowerData struct { func (dto FollowerData) FromModel(user models.User) FollowerData { dto.Name = user.FullName() dto.Username = user.Username - dto.Avatar = user.AvatarUrl() + dto.Avatar = &user.Avatar dto.AccountType = user.AccountType dto.FollowersCount = user.FollowersCount() dto.StoriesCount = user.BooksCount() @@ -59,7 +59,7 @@ func (u UserProfile) Init(user models.User) UserProfile { LastName: user.LastName, Username: user.Username, Email: user.Email, - Avatar: user.AvatarUrl(), + Avatar: &user.Avatar, Bio: user.Bio, AccountType: user.AccountType, Followers: followers, @@ -119,7 +119,7 @@ func (n NotificationSchema) Init(notification models.Notification, showReceiver n.Book = &NotificationBookSchema{ Title: notification.Book.Title, Slug: notification.Book.Slug, - CoverImage: notification.Book.CoverImageUrl(), + CoverImage: notification.Book.CoverImage, } } n.ReviewID = notification.ReviewID