diff --git a/.gitignore b/.gitignore index 02a7083..0defaff 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ .idea /config/config.yml +app.env diff --git a/cmd/infrastructure/cloudinary.go b/cmd/infrastructure/cloudinary.go new file mode 100644 index 0000000..8c9adf1 --- /dev/null +++ b/cmd/infrastructure/cloudinary.go @@ -0,0 +1,32 @@ +package infrastructure + +import ( + "context" + "github.com/cloudinary/cloudinary-go/v2" +) + +type Cloudinary struct { + *cloudinary.Cloudinary +} + +func NewCloudinary() *Cloudinary { + cld, _ := credentials() + return &Cloudinary{ + cld, + } +} + +func credentials() (*cloudinary.Cloudinary, context.Context) { + // Add your Cloudinary credentials, set configuration parameter + // Secure=true to return "https" URLs, and create a context + //=================== + cld, err := cloudinary.New() + if err != nil { + panic(err) + } + // CLOUDINARY_URL=cloudinary://API-Key:API-Secret@Cloud-name + // CLOUDINARY_URL=cloudinary://oZ47iHrgrFQq4fe7ksKKlo7tg4A:991793784142871@dsr2xnaj7 + cld.Config.URL.Secure = true + ctx := context.Background() + return cld, ctx +} diff --git a/cmd/infrastructure/module.go b/cmd/infrastructure/module.go index 5abb8ff..8741203 100644 --- a/cmd/infrastructure/module.go +++ b/cmd/infrastructure/module.go @@ -2,4 +2,4 @@ package infrastructure import "go.uber.org/fx" -var Module = fx.Options(fx.Provide(NewDatabase)) +var Module = fx.Options(fx.Provide(NewDatabase, NewCloudinary)) diff --git a/cmd/lib/cloudinary.go b/cmd/lib/cloudinary.go new file mode 100644 index 0000000..610d67e --- /dev/null +++ b/cmd/lib/cloudinary.go @@ -0,0 +1,101 @@ +package lib + +import ( + "context" + "erp/cmd/infrastructure" + "erp/config" + "fmt" + "github.com/cloudinary/cloudinary-go/v2/api" + "github.com/cloudinary/cloudinary-go/v2/api/admin" + "github.com/cloudinary/cloudinary-go/v2/api/uploader" + "go.uber.org/zap" + "mime/multipart" +) + +type CloudinaryRepository interface { + UploadFileCloud(ctx context.Context, file *multipart.FileHeader) (resp *uploader.UploadResult, err error) + GetAssetInfo(ctx context.Context) + TransformImage(ctx context.Context) +} + +type cloudinaryRepository struct { + cld *infrastructure.Cloudinary + config *config.Config + logger *zap.Logger +} + +func NewCloudinaryRepository(cld *infrastructure.Cloudinary, logger *zap.Logger, config *config.Config) CloudinaryRepository { + return &cloudinaryRepository{ + cld: cld, + config: config, + logger: logger, + } +} + +func (r *cloudinaryRepository) UploadFileCloud(ctx context.Context, file *multipart.FileHeader) (resp *uploader.UploadResult, err error) { + + // SaveFile the cloudinary. + // Set the asset's public ID and allow overwriting the asset with new versions + resp, err = r.cld.Upload.Upload(ctx, file, uploader.UploadParams{ + PublicID: r.config.Cloudinary.PublicId, + UniqueFilename: api.Bool(false), + Overwrite: api.Bool(true)}) + if err != nil { + fmt.Println("error") + } + + return resp, err +} + +func (r *cloudinaryRepository) GetAssetInfo(ctx context.Context) { + // Get and use details of the cloudinary + // ============================== + resp, err := r.cld.Admin.Asset(ctx, admin.AssetParams{PublicID: r.config.Cloudinary.PublicId}) + if err != nil { + fmt.Println("error") + } + fmt.Println("****3. Get and use details of the cloudinary****\nDetailed response:\n", resp, "\n") + + // Assign tags to the uploaded cloudinary based on its width. Save the response to the update in the variable 'update_resp'. + if resp.Width > 900 { + updateResp, err := r.cld.Admin.UpdateAsset(ctx, admin.UpdateAssetParams{ + PublicID: r.config.Cloudinary.PublicId, + Tags: []string{"large"}}) + if err != nil { + fmt.Println("error") + } else { + // Log the new tag to the console. + fmt.Println("New tag: ", updateResp.Tags, "\n") + } + } else { + updateResp, err := r.cld.Admin.UpdateAsset(ctx, admin.UpdateAssetParams{ + PublicID: r.config.Cloudinary.PublicId, + Tags: []string{"small"}}) + if err != nil { + fmt.Println("error") + } else { + // Log the new tag to the console. + fmt.Println("New tag: ", updateResp.Tags, "\n") + } + } + +} + +func (r *cloudinaryRepository) TransformImage(ctx context.Context) { + // Instantiate an object for the asset with public ID "my_image" + qs_img, err := r.cld.Image("quickstart_butterfly") + if err != nil { + fmt.Println("error") + } + + // Add the transformation + qs_img.Transformation = "r_max/e_sepia" + + // Generate and log the delivery URL + new_url, err := qs_img.String() + if err != nil { + fmt.Println("error") + } else { + print("****4. Transform the image****\nTransfrmation URL: ", new_url, "\n") + } +} diff --git a/cmd/lib/module.go b/cmd/lib/module.go index 164fd0f..633ba3a 100644 --- a/cmd/lib/module.go +++ b/cmd/lib/module.go @@ -2,4 +2,4 @@ package lib import "go.uber.org/fx" -var Module = fx.Options(fx.Provide(NewZapLogger, NewServer, NewServerGroup)) +var Module = fx.Options(fx.Provide(NewZapLogger, NewServer, NewServerGroup, NewCloudinaryRepository)) diff --git a/cmd/lib/server.go b/cmd/lib/server.go index b30e13c..0d0bd8f 100644 --- a/cmd/lib/server.go +++ b/cmd/lib/server.go @@ -19,7 +19,7 @@ type Handler struct { func NewServerGroup(instance *gin.Engine) *Handler { return &Handler{ - instance.Group("/handler/"), + instance.Group("/api/"), } } diff --git a/config/app.env b/config/app.env new file mode 100644 index 0000000..baeeee6 --- /dev/null +++ b/config/app.env @@ -0,0 +1,2 @@ +ENV=local +CLOUDINARY_URL=cloudinary://991793784142871:oZ47iHrgrFQq4fe7ksKKlo7tg4A@dsr2xnaj7 \ No newline at end of file diff --git a/config/app.env.example b/config/app.env.example new file mode 100644 index 0000000..93b8afd --- /dev/null +++ b/config/app.env.example @@ -0,0 +1,2 @@ +ENV=*** +CLOUDINARY_URL=cloudinary://*** \ No newline at end of file diff --git a/config/config.go b/config/config.go index 0908182..1de853e 100644 --- a/config/config.go +++ b/config/config.go @@ -4,6 +4,7 @@ import ( "erp/utils/constants" "fmt" "os" + "reflect" "github.com/gin-gonic/gin" "github.com/spf13/viper" @@ -16,22 +17,31 @@ var ( configType = "yml" ) +var ( + configEnv = "./config/app.env" + configTypeEnv = "env" + configEnvName = "app" +) + type ( Config struct { - Debug bool `mapstructure:"debug"` - ContextTimeout int `mapstructure:"contextTimeout"` - Server Server `mapstructure:"server"` - Services Services `mapstructure:"services"` - Database Database `mapstructure:"database"` - Logger Logger `mapstructure:"logger"` - Jwt Jwt `mapstructure:"jwt"` + Env Env `mapstructure:"env"` + Debug bool `mapstructure:"debug"` + ContextTimeout int `mapstructure:"contextTimeout"` + Server Server `mapstructure:"server"` + Services Services `mapstructure:"services"` + Database Database `mapstructure:"database"` + Logger Logger `mapstructure:"logger"` + Jwt Jwt `mapstructure:"jwt"` + Cloudinary Cloudinary `mapstructure:"cloudinary"` } Server struct { - Host string `mapstructure:"host"` - Env string `mapstructure:"env"` - UseRedis bool `mapstructure:"useRedis"` - Port int `mapstructure:"port"` + Host string `mapstructure:"host"` + Env string `mapstructure:"env"` + UseRedis bool `mapstructure:"useRedis"` + Port int `mapstructure:"port"` + UploadPath string `mapstructure:"uploadPath"` } Database struct { @@ -45,6 +55,11 @@ type ( TimeZone string `mapstructure:"timeZone"` } + Env struct { + Env string `mapstructure:"ENV"` + CloudinaryURL string `mapstructure:"CLOUDINARY_URL"` + } + Jwt struct { Secret string `mapstructure:"secret"` AccessTokenExpiresIn int64 `mapstructure:"accessTokenExpiresIn"` @@ -59,18 +74,60 @@ type ( Services struct { } + + Cloudinary struct { + CloudName string `mapstructure:"cloudName"` + ApiKey string `mapstructure:"apiKey"` + ApiSecret string `mapstructure:"apiSecret"` + PublicId string `mapstructure:"publicId"` + URL string `mapstructure:"url"` + } ) +func initEnv(conf *Config) { + if err := LoadConfigEnv(configEnv, configTypeEnv); err != nil { + fmt.Printf("unable decode into config struct, %v", err) + } + if err := UnmarsharConfig(&conf.Env); err != nil { + fmt.Printf("unable decode into config struct, %v", err) + } + SetEnv(conf) +} + func NewConfig() *Config { - initConfig() conf := &Config{} - err := viper.Unmarshal(conf) - if err != nil { + initEnv(conf) + initConfig() + if err := UnmarsharConfig(conf); err != nil { fmt.Printf("unable decode into config struct, %v", err) } return conf } +func LoadConfigEnv(configFile, configType string) (err error) { + viper.SetConfigType(configType) + viper.SetConfigFile(configFile) + + if err = viper.ReadInConfig(); err != nil { + fmt.Println(err.Error()) + } + return +} + +func UnmarsharConfig[E any](config *E) error { + return viper.Unmarshal(config) + +} + +func SetEnv(config *Config) { + v := reflect.ValueOf(config.Env) + for i := 0; i < v.NumField(); i++ { + if v.Field(i).Interface() != "" { + os.Setenv(v.Type().Field(i).Tag.Get("mapstructure"), v.Field(i).Interface().(string)) + } + } +} + func initConfig() { var configFile string switch os.Getenv("ENV") { diff --git a/domain/upload.go b/domain/upload.go new file mode 100644 index 0000000..9de6bea --- /dev/null +++ b/domain/upload.go @@ -0,0 +1,21 @@ +package domain + +import ( + "encoding/json" + uuid "github.com/satori/go.uuid" +) + +type File struct { + BaseModel + FileName string `json:"file_name" gorm:"column:file_name;type:varchar(50);not null"` + Path string `json:"path" gorm:"column:path;type:varchar(255);not null"` + Size int64 `json:"size" gorm:"column:size;type:bigint;not null"` + ExtensionName string `json:"type" gorm:"column:extension_name;type:varchar(10);not null"` + Data json.RawMessage `json:"domain" gorm:"column:data;type:jsonb;" swaggertype:"string"` // save domain flexibly + UserId uuid.UUID `json:"user_id" gorm:"column:user_id;type:uuid"` + User User `json:"user" gorm:"foreignKey:UserId; constraint:OnUpdate:CASCADE,OnDelete:CASCADE"` +} + +func (File) TableName() string { + return "files" +} diff --git a/go.mod b/go.mod index 2e62d20..6a25666 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,8 @@ require ( require ( github.com/bytedance/sonic v1.9.1 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudinary/cloudinary-go/v2 v2.7.0 // indirect + github.com/creasty/defaults v1.5.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -30,6 +32,8 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.2.0 // indirect + github.com/gorilla/schema v1.2.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect diff --git a/go.sum b/go.sum index 33acf29..fc9e11b 100644 --- a/go.sum +++ b/go.sum @@ -50,10 +50,14 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudinary/cloudinary-go/v2 v2.7.0 h1:8Fuh/SOen6IQgqH8CLso2E+kuKi2xjbdiyXOspwXFTM= +github.com/cloudinary/cloudinary-go/v2 v2.7.0/go.mod h1:jtSxa6xbzvu4IwChRJVDcXwVXrTRczhbvq3Z1VSoFdk= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creasty/defaults v1.5.1 h1:j8WexcS3d/t4ZmllX4GEkl4wIB/trOr035ajcLHCISM= +github.com/creasty/defaults v1.5.1/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -91,6 +95,7 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-test/deep v1.0.7/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= @@ -151,13 +156,18 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/heimdalr/dag v1.0.1/go.mod h1:t+ZkR+sjKL4xhlE1B9rwpvwfo+x+2R0363efS+Oghns= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= diff --git a/handler/controllers/file.go b/handler/controllers/file.go new file mode 100644 index 0000000..e0b0e58 --- /dev/null +++ b/handler/controllers/file.go @@ -0,0 +1,35 @@ +package controller + +import ( + "erp/handler/dto" + erpservice "erp/service" + "github.com/gin-gonic/gin" + "net/http" +) + +type FileController struct { + dto.BaseController + uploadService erpservice.FileService +} + +func NewFileController(uploadService erpservice.FileService) *FileController { + return &FileController{ + uploadService: uploadService, + } +} + +func (p *FileController) UploadFile(c *gin.Context) { + file, err := c.FormFile("file_request") + if err != nil { + p.ResponseError(c, err) + return + } + + output, err := p.uploadService.UploadImage(c.Request.Context(), file) + if err != nil { + p.ResponseError(c, err) + return + } + + p.Response(c, http.StatusCreated, "Success", output) +} diff --git a/handler/controllers/module.go b/handler/controllers/module.go index 70d5e4d..1164b1d 100644 --- a/handler/controllers/module.go +++ b/handler/controllers/module.go @@ -10,5 +10,5 @@ var Module = fx.Options( NewERPCategoryController, NewERPCustomerController, NewERPEmployeeManagementController, NewERPProductController, NewERPStoreController, NewOrderController, NewPromoteController, NewCashbookController, NewTransactionCategoryController, NewWalletController, - NewBudgetController, NewCategoryProductController, + NewBudgetController, NewCategoryProductController, NewFileController, )) diff --git a/handler/dto/request/upload.go b/handler/dto/request/upload.go new file mode 100644 index 0000000..c87664c --- /dev/null +++ b/handler/dto/request/upload.go @@ -0,0 +1,52 @@ +package request + +import ( + "encoding/json" + uuid "github.com/satori/go.uuid" + "mime/multipart" +) + +type UploadFileRequest struct { + File *multipart.FileHeader `json:"file" swaggerignore:"true" validation:"required"` + UserId uuid.UUID `json:"user_id" swaggerignore:"true"` +} + +type UploadFileResponse struct { + URL string `json:"url"` +} + +type UpdateFileRequest struct { + ID string `json:"id" form:"id" validate:"required"` + File *multipart.FileHeader `json:"file" swaggerignore:"true"` + FileName string `json:"file_name" form:"file_name"` + Data json.RawMessage `json:"data,omitempty" swaggertype:"array,string"` + UserId uuid.UUID `json:"user_id" swaggerignore:"true"` +} + +type DeleteFileRequest struct { + ID string `json:"id" form:"id" validate:"required"` + UserId uuid.UUID `json:"user_id" swaggerignore:"true"` +} + +type FileResponse struct { + ID uuid.UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Image string `json:"image"` + Price float64 `json:"price"` + Status bool `json:"status"` + NumberFile int `json:"number_product"` +} + +type CreateFileRequest struct { + FileName string `json:"file_name" binding:"required"` + Path string `json:"path" binding:"required"` + Size int64 `json:"size" binding:"required"` + ExtensionName string `json:"type" binding:"required"` + Data string `json:"domain" binding:"required"` + UserId string `json:"user_id" binding:"required"` +} + +type UploadImageRequest struct { + File *multipart.FileHeader +} diff --git a/repository/module.go b/repository/module.go index ded7b36..fcc6a43 100644 --- a/repository/module.go +++ b/repository/module.go @@ -10,5 +10,5 @@ var Module = fx.Options(fx.Provide( NewERPProductRepository, NewERPCustomerRepository, NewOrderRepository, NewTransactionRepository, NewOrderItemRepo, NewPromoteRepo, NewTransactionCategoryRepository, NewWalletRepository, - NewBudgetRepository, + NewBudgetRepository, NewFileRepository, )) diff --git a/repository/upload.go b/repository/upload.go new file mode 100644 index 0000000..1d2c379 --- /dev/null +++ b/repository/upload.go @@ -0,0 +1,54 @@ +package repository + +import ( + "context" + "erp/cmd/infrastructure" + "erp/domain" + "erp/utils/api_errors" + "github.com/pkg/errors" + "go.uber.org/zap" +) + +type FileRepository interface { + Create(ctx context.Context, file *domain.File) (err error) + Update(ctx context.Context, file *domain.File) (err error) + GetOneById(ctx context.Context, id string) (res *domain.File, err error) + DeleteById(ctx context.Context, id string) (err error) +} + +type fileRepository struct { + db *infrastructure.Database + logger *zap.Logger +} + +func NewFileRepository(db *infrastructure.Database, logger *zap.Logger) FileRepository { + return &fileRepository{ + db: db, + logger: logger, + } +} + +func (r *fileRepository) Create(ctx context.Context, file *domain.File) (err error) { + err = r.db.Create(&file).Error + return errors.Wrap(err, "create file failed") +} + +func (r *fileRepository) Update(ctx context.Context, file *domain.File) (err error) { + err = r.db.Updates(&file).Error + return errors.Wrap(err, "update file failed") +} + +func (r *fileRepository) GetOneById(ctx context.Context, id string) (res *domain.File, err error) { + var file domain.File + if err := r.db.WithContext(ctx).Where("id = ?", id).First(&file).Error; err != nil { + return nil, errors.New(api_errors.ErrFileNotFound) + } + return &file, nil +} + +func (r *fileRepository) DeleteById(ctx context.Context, id string) (err error) { + if err := r.db.WithContext(ctx).Where("id = ?", id).Delete(&domain.File{}).Error; err != nil { + return errors.Wrap(err, "Delete file failed") + } + return nil +} diff --git a/route/route.go b/route/route.go index 9d12a57..493074b 100644 --- a/route/route.go +++ b/route/route.go @@ -25,6 +25,7 @@ type Route struct { middleware *middlewares.GinMiddleware transactionController *controller.CashbookController categoryProductController *controller.CategoryProductController + fileController *controller.FileController } func NewRoute( @@ -44,6 +45,7 @@ func NewRoute( walletController *controller.WalletController, budgetController *controller.BudgetController, transactionCategoryController *controller.TransactionCategoryController, + fileController *controller.FileController, ) *Route { v1 := handler.Group("/v1") @@ -119,6 +121,8 @@ func NewRoute( v1.DELETE("/cashbook_category/:id", middleware.Auth(true), transactionCategoryController.Delete) v1.GET("/cashbook_category/:id", middleware.Auth(true), transactionCategoryController.GetOne) + v1.POST("/file/upload/", middleware.Auth(false), fileController.UploadFile) + v1.GET("/health/", healthController.Health) return &Route{ diff --git a/service/module.go b/service/module.go index 313e7dd..592c149 100644 --- a/service/module.go +++ b/service/module.go @@ -10,5 +10,5 @@ var Module = fx.Options(fx.Provide( NewCustomerService, NewOrderService, NewOrderItemService, NewPromoteService, NewStoreService, NewERPEmployeeManagementService, NewCashbookService, NewTransactionCategoryService, - NewWalletService, NewBudgetService, + NewWalletService, NewBudgetService, NewFileService, )) diff --git a/service/upload.go b/service/upload.go new file mode 100644 index 0000000..b023f31 --- /dev/null +++ b/service/upload.go @@ -0,0 +1,201 @@ +package service + +import ( + "context" + "erp/cmd/lib" + "erp/config" + "erp/domain" + "erp/handler/dto/request" + "erp/repository" + "erp/utils" + "erp/utils/api_errors" + "erp/utils/constants" + "github.com/cloudinary/cloudinary-go/v2/api/uploader" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" + "io" + "mime/multipart" + "os" + "strings" +) + +type ( + FileService interface { + SaveFile(ctx context.Context, req request.UploadFileRequest) (*domain.File, error) + Update(ctx context.Context, req request.UpdateFileRequest) (*domain.File, error) + Delete(ctx context.Context, req request.DeleteFileRequest) error + GetOne(ctx context.Context, id string) (*domain.File, error) + Download(ctx context.Context, id string) (*domain.File, error) + UploadImage(ctx context.Context, file *multipart.FileHeader) (*uploader.UploadResult, error) + } + fileService struct { + fileRepository repository.FileRepository + config *config.Config + imageRepository lib.CloudinaryRepository + } +) + +func NewFileService(itemRepo repository.FileRepository, config *config.Config, imageRepository lib.CloudinaryRepository) FileService { + return &fileService{ + fileRepository: itemRepo, + config: config, + imageRepository: imageRepository, + } +} + +func createFolder(fileId string, config *config.Config) string { + firstChar := fileId[0:1] + secondChar := fileId[1:2] + uploadPath := config.Server.UploadPath + "/" + firstChar + "/" + secondChar + "/" + + // create folder if not exists + if _, err := os.Stat(uploadPath); os.IsNotExist(err) { + if err := os.MkdirAll(uploadPath, 0755); err != nil { + panic(err) + } + } + return uploadPath +} + +func saveToFolder(file *multipart.FileHeader, uploadPath, id, extensionName string) error { + // Source + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(uploadPath + id + "." + extensionName) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + return nil +} + +func getExtensionNameFromFilename(fileName string) string { + fileNameArr := strings.Split(fileName, ".") + extensionName := "" + if len(fileNameArr) > constants.NumberFileNameSplit { + extensionName = fileNameArr[1] + } + return extensionName +} +func getFilename(fileName string) string { + return strings.Split(fileName, ".")[0] +} + +func (s *fileService) SaveFile(ctx context.Context, req request.UploadFileRequest) (*domain.File, error) { + var err error + fileId := uuid.NewV4().String() + extensionName := getExtensionNameFromFilename(req.File.Filename) + + uploadPath := createFolder(fileId, s.config) + if err = saveToFolder(req.File, uploadPath, fileId, extensionName); err != nil { + return nil, errors.WithStack(err) + } + + file := &domain.File{ + BaseModel: domain.BaseModel{ + ID: uuid.FromStringOrNil(fileId), + }, + Path: uploadPath, + Size: req.File.Size, + ExtensionName: extensionName, + FileName: req.File.Filename, + UserId: req.UserId, + } + if err = s.fileRepository.Create(ctx, file); err != nil { + return nil, err + } + + return file, err +} + +func (s *fileService) Update(ctx context.Context, req request.UpdateFileRequest) (*domain.File, error) { + // get one file + file, err := s.fileRepository.GetOneById(ctx, req.ID) + if err != nil { + return nil, err + } + + // check file is belong to user + if file.UserId != req.UserId { + return nil, errors.New(api_errors.ErrUnauthorizedAccess) + } + + if req.File != nil { + // check filePath is not in ./domain/assets + if !strings.Contains(file.Path, s.config.Server.UploadPath) { + file.Path = createFolder(file.ID.String(), s.config) + } else { + // delete old file + _ = os.Remove(file.Path + file.ID.String() + "." + file.ExtensionName) + } + + file.ExtensionName = getExtensionNameFromFilename(req.File.Filename) + file.Size = req.File.Size + file.UpdaterID = req.UserId + + // create folder if not exists + if _, err := os.Stat(file.Path); os.IsNotExist(err) { + if err := os.MkdirAll(file.Path, 0755); err != nil { + panic(err) + } + } + if err := saveToFolder(req.File, file.Path, file.ID.String(), file.ExtensionName); err != nil { + return nil, errors.WithStack(err) + } + } + + if err = utils.Copy(file, req); err != nil { + return nil, err + } + if req.FileName != "" { + fileName := getFilename(req.FileName) + file.FileName = fileName + "." + file.ExtensionName + } + + if err = s.fileRepository.Update(ctx, file); err != nil { + return nil, err + } + return file, err +} + +func (s *fileService) Delete(ctx context.Context, req request.DeleteFileRequest) error { + // get one file + file, err := s.fileRepository.GetOneById(ctx, req.ID) + if err != nil { + return err + } + + // check file is belong to user + if file.UserId != req.UserId { + return errors.New(api_errors.ErrUnauthorizedAccess) + } + + return s.fileRepository.DeleteById(ctx, req.ID) +} + +func (s *fileService) GetOne(ctx context.Context, id string) (*domain.File, error) { + return s.fileRepository.GetOneById(ctx, id) +} + +func (s *fileService) Download(ctx context.Context, id string) (*domain.File, error) { + return s.fileRepository.GetOneById(ctx, id) +} + +func (s *fileService) UploadImage(ctx context.Context, file *multipart.FileHeader) (*uploader.UploadResult, error) { + res, err := s.imageRepository.UploadFileCloud(ctx, file) + if err != nil { + return nil, err + } + + return res, err +} diff --git a/utils/api_errors/errors.go b/utils/api_errors/errors.go index 0ec8271..bcd30bc 100644 --- a/utils/api_errors/errors.go +++ b/utils/api_errors/errors.go @@ -33,6 +33,7 @@ var ( ErrPromoteCodeRequiredCustomer = "10032" ErrOrderStatus = "10033" ErrWalletNameAlreadyExist = "10034" + ErrFileNotFound = "10035" ) type MessageAndStatus struct { diff --git a/utils/constants/contants.go b/utils/constants/contants.go index cc17e07..8e0c831 100644 --- a/utils/constants/contants.go +++ b/utils/constants/contants.go @@ -4,3 +4,8 @@ const ( StatusIn = "in" StatusOut = "out" ) + +const ( + NumberFileNameSplit = 2 + FolderTemp = "tmp/" +)