diff --git a/cmd/database.go b/cmd/database.go index fd7a4cde..a7805c06 100644 --- a/cmd/database.go +++ b/cmd/database.go @@ -125,5 +125,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error { log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully") + err = datastore.Container.UserDataStore.SyncStructs(new(models.TransactionPictureInfo)) + + if err != nil { + return err + } + + log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction picture table maintained successfully") + return nil } diff --git a/cmd/webserver.go b/cmd/webserver.go index 2d2acd9f..3c420a81 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -161,6 +161,14 @@ func startWebServer(c *core.CliContext) error { } } + if config.EnableTransactionPictures { + pictureRoute := router.Group("/pictures") + pictureRoute.Use(bindMiddleware(middlewares.JWTAuthorizationByQueryString)) + { + pictureRoute.GET("/:fileName", bindImage(api.TransactionPictures.TransactionPictureGetHandler)) + } + } + router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler)) if config.Mode == settings.MODE_DEVELOPMENT { @@ -309,6 +317,11 @@ func startWebServer(c *core.CliContext) error { apiV1Route.POST("/transactions/modify.json", bindApi(api.Transactions.TransactionModifyHandler)) apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler)) + // Transaction Pictures + if config.EnableTransactionPictures { + apiV1Route.POST("/transaction/pictures/upload.json", bindApi(api.TransactionPictures.TransactionPictureUploadHandler)) + } + // Transaction Categories apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler)) apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler)) diff --git a/conf/ezbookkeeping.ini b/conf/ezbookkeeping.ini index f72af87d..607b49d8 100644 --- a/conf/ezbookkeeping.ini +++ b/conf/ezbookkeeping.ini @@ -199,6 +199,9 @@ enable_forget_password = true # Set to true to require email must be verified when use forget password forget_password_require_email_verify = false +# Set to true to allow users to upload transaction pictures +enable_transaction_picture = true + # Set to true to allow users to create scheduled transaction enable_scheduled_transaction = true diff --git a/pkg/api/base.go b/pkg/api/base.go index 9c0c822f..481f3251 100644 --- a/pkg/api/base.go +++ b/pkg/api/base.go @@ -1,12 +1,16 @@ package api import ( + "fmt" + "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" "github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/settings" ) +const internalTransactionPictureUrlFormat = "%spictures/%d.%s" + // ApiUsingConfig represents an api that need to use config type ApiUsingConfig struct { container *settings.ConfigContainer @@ -17,6 +21,12 @@ func (a *ApiUsingConfig) CurrentConfig() *settings.Config { return a.container.Current } +// GetTransactionPictureInfoResponse returns the view-object of transaction picture basic info according to the transaction picture model +func (a *ApiUsingConfig) GetTransactionPictureInfoResponse(picture *models.TransactionPictureInfo) *models.TransactionPictureInfoBasicResponse { + originalUrl := fmt.Sprintf(internalTransactionPictureUrlFormat, a.CurrentConfig().RootUrl, picture.PictureId, picture.PictureExtension) + return picture.ToTransactionPictureInfoBasicResponse(originalUrl) +} + // GetAfterRegisterNotificationContent returns the notification content displayed each time users register func (a *ApiUsingConfig) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string { language := userLanguage diff --git a/pkg/api/transaction_pictures.go b/pkg/api/transaction_pictures.go new file mode 100644 index 00000000..0a81988a --- /dev/null +++ b/pkg/api/transaction_pictures.go @@ -0,0 +1,153 @@ +package api + +import ( + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/log" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/services" + "github.com/mayswind/ezbookkeeping/pkg/settings" + "github.com/mayswind/ezbookkeeping/pkg/utils" +) + +// TransactionPicturesApi represents transaction pictures api +type TransactionPicturesApi struct { + ApiUsingConfig + ApiUsingDuplicateChecker + users *services.UserService + pictures *services.TransactionPictureService +} + +// Initialize a transaction api singleton instance +var ( + TransactionPictures = &TransactionPicturesApi{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + container: duplicatechecker.Container, + }, + users: services.Users, + pictures: services.TransactionPictures, + } +) + +// TransactionPictureUploadHandler saves transaction picture by request parameters for current user +func (a *TransactionPicturesApi) TransactionPictureUploadHandler(c *core.WebContext) (any, *errs.Error) { + uid := c.GetCurrentUid() + form, err := c.MultipartForm() + + if err != nil { + log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get multi-part form data for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrParameterInvalid + } + + pictureFiles := form.File["picture"] + + if len(pictureFiles) < 1 { + log.Warnf(c, "[transaction_pictures.TransactionPictureUploadHandler] there is no transaction picture in request for user \"uid:%d\"", uid) + return nil, errs.ErrNoTransactionPicture + } + + if pictureFiles[0].Size < 1 { + log.Warnf(c, "[transaction_pictures.TransactionPictureUploadHandler] the size of transaction picture in request is zero for user \"uid:%d\"", uid) + return nil, errs.ErrTransactionPictureIsEmpty + } + + fileExtension := utils.GetFileNameExtension(pictureFiles[0].Filename) + + if utils.GetImageContentType(fileExtension) == "" { + log.Warnf(c, "[transaction_pictures.TransactionPictureUploadHandler] the file extension \"%s\" of transaction picture in request is not supported for user \"uid:%d\"", fileExtension, uid) + return nil, errs.ErrImageTypeNotSupported + } + + pictureFile, err := pictureFiles[0].Open() + + if err != nil { + log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get transaction picture file from request for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.ErrOperationFailed + } + + pictureInfo := a.createNewPictureInfoModel(uid, fileExtension, c.ClientIP()) + + clientSessionIds := form.Value["clientSessionId"] + clientSessionId := "" + + if len(clientSessionIds) > 0 { + clientSessionId = clientSessionIds[0] + } + + if a.CurrentConfig().EnableDuplicateSubmissionsCheck && clientSessionId != "" { + found, remark := a.GetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_PICTURE, uid, clientSessionId) + + if found { + log.Infof(c, "[transaction_pictures.TransactionPictureUploadHandler] another transaction picture \"id:%s\" has been uploaded for user \"uid:%d\"", remark, uid) + pictureId, err := utils.StringToInt64(remark) + + if err == nil { + pictureInfo, err = a.pictures.GetPictureInfoByPictureId(c, uid, pictureId) + + if err != nil { + log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get existed transaction picture \"id:%d\" for user \"uid:%d\", because %s", pictureId, uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + pictureInfoResp := a.GetTransactionPictureInfoResponse(pictureInfo) + + return pictureInfoResp, nil + } + } + } + + err = a.pictures.UploadPicture(c, pictureInfo, pictureFile) + + if err != nil { + log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to update transaction picture for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrOperationFailed) + } + + a.SetSubmissionRemark(duplicatechecker.DUPLICATE_CHECKER_TYPE_NEW_PICTURE, uid, clientSessionId, utils.Int64ToString(pictureInfo.PictureId)) + pictureInfoResp := a.GetTransactionPictureInfoResponse(pictureInfo) + + return pictureInfoResp, nil +} + +// TransactionPictureGetHandler returns transaction picture data for current user +func (a *TransactionPicturesApi) TransactionPictureGetHandler(c *core.WebContext) ([]byte, string, *errs.Error) { + fileName := c.Param("fileName") + fileExtension := utils.GetFileNameExtension(fileName) + contentType := utils.GetImageContentType(fileExtension) + + if contentType == "" { + return nil, "", errs.ErrImageTypeNotSupported + } + + fileBaseName := utils.GetFileNameWithoutExtension(fileName) + pictureId, err := utils.StringToInt64(fileBaseName) + + if err != nil { + return nil, "", errs.ErrTransactionPictureIdInvalid + } + + uid := c.GetCurrentUid() + pictureData, err := a.pictures.GetPictureByPictureId(c, uid, pictureId, fileExtension) + + if err != nil { + if !errs.IsCustomError(err) { + log.Errorf(c, "[transaction_pictures.TransactionPictureUploadHandler] failed to get transaction picture, because %s", err.Error()) + } + + return nil, "", errs.Or(err, errs.ErrOperationFailed) + } + + return pictureData, contentType, nil +} + +func (a *TransactionPicturesApi) createNewPictureInfoModel(uid int64, fileExtension string, clientIp string) *models.TransactionPictureInfo { + return &models.TransactionPictureInfo{ + Uid: uid, + PictureExtension: fileExtension, + CreatedIp: clientIp, + } +} diff --git a/pkg/duplicatechecker/duplicate_checker_type.go b/pkg/duplicatechecker/duplicate_checker_type.go index 6ce03f15..06b916bc 100644 --- a/pkg/duplicatechecker/duplicate_checker_type.go +++ b/pkg/duplicatechecker/duplicate_checker_type.go @@ -10,4 +10,5 @@ const ( DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2 DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3 DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4 + DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 5 ) diff --git a/pkg/errs/error.go b/pkg/errs/error.go index 9dbab269..5a02ad56 100644 --- a/pkg/errs/error.go +++ b/pkg/errs/error.go @@ -36,6 +36,7 @@ const ( NormalSubcategoryDataManagement = 8 NormalSubcategoryMapProxy = 9 NormalSubcategoryTemplate = 10 + NormalSubcategoryPicture = 11 ) // Error represents the specific error returned to user diff --git a/pkg/errs/transaction_picture.go b/pkg/errs/transaction_picture.go new file mode 100644 index 00000000..9a13002d --- /dev/null +++ b/pkg/errs/transaction_picture.go @@ -0,0 +1,13 @@ +package errs + +import "net/http" + +// Error codes related to transaction pictures +var ( + ErrTransactionPictureIdInvalid = NewNormalError(NormalSubcategoryPicture, 0, http.StatusBadRequest, "transaction picture id is invalid") + ErrTransactionPictureNotFound = NewNormalError(NormalSubcategoryPicture, 1, http.StatusBadRequest, "transaction picture not found") + ErrNoTransactionPicture = NewNormalError(NormalSubcategoryPicture, 2, http.StatusBadRequest, "no transaction picture") + ErrTransactionPictureIsEmpty = NewNormalError(NormalSubcategoryPicture, 3, http.StatusBadRequest, "transaction picture is empty") + ErrTransactionPictureNoExists = NewNormalError(NormalSubcategoryPicture, 4, http.StatusNotFound, "transaction picture not exists") + ErrTransactionPictureExtensionInvalid = NewNormalError(NormalSubcategoryPicture, 5, http.StatusNotFound, "transaction picture file extension invalid") +) diff --git a/pkg/middlewares/server_settings_cookie.go b/pkg/middlewares/server_settings_cookie.go index 65845793..64793f64 100644 --- a/pkg/middlewares/server_settings_cookie.go +++ b/pkg/middlewares/server_settings_cookie.go @@ -19,6 +19,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc { buildBooleanSetting("r", config.EnableUserRegister), buildBooleanSetting("f", config.EnableUserForgetPassword), buildBooleanSetting("v", config.EnableUserVerifyEmail), + buildBooleanSetting("p", config.EnableTransactionPictures), buildBooleanSetting("s", config.EnableScheduledTransaction), buildBooleanSetting("e", config.EnableDataExport), buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)), diff --git a/pkg/models/transaction_picture_info.go b/pkg/models/transaction_picture_info.go new file mode 100644 index 00000000..b2ec6acd --- /dev/null +++ b/pkg/models/transaction_picture_info.go @@ -0,0 +1,28 @@ +package models + +// TransactionPictureInfo represents transaction picture file info stored in database +type TransactionPictureInfo struct { + Uid int64 `xorm:"INDEX(IDX_transaction_picture_uid_deleted_transaction_id_picture_id) INDEX(IDX_transaction_picture_uid_deleted_picture_id) NOT NULL"` + Deleted bool `xorm:"INDEX(IDX_transaction_picture_uid_deleted_transaction_id_picture_id) INDEX(IDX_transaction_picture_uid_deleted_picture_id) NOT NULL"` + TransactionId int64 `xorm:"INDEX(IDX_transaction_picture_uid_deleted_transaction_id_picture_id) NOT NULL"` + PictureId int64 `xorm:"PK INDEX(IDX_transaction_picture_uid_deleted_transaction_id_picture_id) INDEX(IDX_transaction_picture_uid_deleted_picture_id)"` + PictureExtension string `xorm:"VARCHAR(10) NOT NULL"` + CreatedIp string `xorm:"VARCHAR(39)"` + CreatedUnixTime int64 + UpdatedUnixTime int64 + DeletedUnixTime int64 +} + +// TransactionPictureInfoBasicResponse represents a view-object of transaction picture basic info +type TransactionPictureInfoBasicResponse struct { + PictureId int64 `json:"pictureId,string"` + OriginalUrl string `json:"originalUrl"` +} + +// ToTransactionPictureInfoBasicResponse returns a view-object according to database model +func (p *TransactionPictureInfo) ToTransactionPictureInfoBasicResponse(originalUrl string) *TransactionPictureInfoBasicResponse { + return &TransactionPictureInfoBasicResponse{ + PictureId: p.PictureId, + OriginalUrl: originalUrl, + } +} diff --git a/pkg/services/base.go b/pkg/services/base.go index 35144270..ef6b7449 100644 --- a/pkg/services/base.go +++ b/pkg/services/base.go @@ -2,12 +2,14 @@ package services import ( "fmt" + "path/filepath" "github.com/mayswind/ezbookkeeping/pkg/datastore" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/mail" "github.com/mayswind/ezbookkeeping/pkg/settings" "github.com/mayswind/ezbookkeeping/pkg/storage" + "github.com/mayswind/ezbookkeeping/pkg/utils" "github.com/mayswind/ezbookkeeping/pkg/uuid" ) @@ -115,6 +117,30 @@ func (s *ServiceUsingStorage) DeleteAvatar(uid int64, fileExtension string) erro return s.container.DeleteAvatar(s.getUserAvatarPath(uid, fileExtension)) } +// ExistsTransactionPicture returns whether the transaction picture exists from the current transaction picture object storage +func (s *ServiceUsingStorage) ExistsTransactionPicture(uid int64, pictureId int64, fileExtension string) (bool, error) { + return s.container.ExistsTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension)) +} + +// ReadTransactionPicture returns the transaction picture from the current transaction picture object storage +func (s *ServiceUsingStorage) ReadTransactionPicture(uid int64, pictureId int64, fileExtension string) (storage.ObjectInStorage, error) { + return s.container.ReadTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension)) +} + +// SaveTransactionPicture returns whether save the transaction picture into the current transaction picture object storage successfully +func (s *ServiceUsingStorage) SaveTransactionPicture(uid int64, pictureId int64, object storage.ObjectInStorage, fileExtension string) error { + return s.container.SaveTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension), object) +} + +// DeleteTransactionPicture returns whether delete the transaction picture from the current transaction picture object storage successfully +func (s *ServiceUsingStorage) DeleteTransactionPicture(uid int64, pictureId int64, fileExtension string) error { + return s.container.DeleteTransactionPicture(s.getTransactionPicturePath(uid, pictureId, fileExtension)) +} + func (s *ServiceUsingStorage) getUserAvatarPath(uid int64, fileExtension string) string { return fmt.Sprintf("%d.%s", uid, fileExtension) } + +func (s *ServiceUsingStorage) getTransactionPicturePath(uid int64, pictureId int64, fileExtension string) string { + return filepath.Join(utils.Int64ToString(uid), fmt.Sprintf("%d.%s", pictureId, fileExtension)) +} diff --git a/pkg/services/transaction_pictures.go b/pkg/services/transaction_pictures.go new file mode 100644 index 00000000..9b0732fb --- /dev/null +++ b/pkg/services/transaction_pictures.go @@ -0,0 +1,140 @@ +package services + +import ( + "io" + "mime/multipart" + "os" + "time" + + "xorm.io/xorm" + + "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/datastore" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/models" + "github.com/mayswind/ezbookkeeping/pkg/storage" + "github.com/mayswind/ezbookkeeping/pkg/uuid" +) + +// TransactionPictureService represents transaction picture service +type TransactionPictureService struct { + ServiceUsingDB + ServiceUsingUuid + ServiceUsingStorage +} + +// Initialize a transaction picture service singleton instance +var ( + TransactionPictures = &TransactionPictureService{ + ServiceUsingDB: ServiceUsingDB{ + container: datastore.Container, + }, + ServiceUsingUuid: ServiceUsingUuid{ + container: uuid.Container, + }, + ServiceUsingStorage: ServiceUsingStorage{ + container: storage.Container, + }, + } +) + +// GetPictureInfoByPictureId returns a transaction picture info model according to transaction picture id +func (s *TransactionPictureService) GetPictureInfoByPictureId(c core.Context, uid int64, pictureId int64) (*models.TransactionPictureInfo, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if pictureId <= 0 { + return nil, errs.ErrTransactionPictureIdInvalid + } + + pictureInfo := &models.TransactionPictureInfo{} + has, err := s.UserDataDB(uid).NewSession(c).ID(pictureId).Where("uid=? AND deleted=?", uid, false).Get(pictureInfo) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrTransactionPictureNotFound + } + + return pictureInfo, nil +} + +// GetPictureByPictureId returns the transaction picture data according to transaction picture id +func (s *TransactionPictureService) GetPictureByPictureId(c core.Context, uid int64, pictureId int64, fileExtension string) ([]byte, error) { + if uid <= 0 { + return nil, errs.ErrUserIdInvalid + } + + if pictureId <= 0 { + return nil, errs.ErrTransactionPictureIdInvalid + } + + pictureInfo := &models.TransactionPictureInfo{} + has, err := s.UserDataDB(uid).NewSession(c).ID(pictureId).Where("uid=? AND deleted=?", uid, false).Get(pictureInfo) + + if err != nil { + return nil, err + } else if !has { + return nil, errs.ErrTransactionPictureNotFound + } + + if pictureInfo.PictureExtension == "" { + return nil, errs.ErrTransactionPictureNotFound + } + + if pictureInfo.PictureExtension != fileExtension { + return nil, errs.ErrTransactionPictureExtensionInvalid + } + + pictureFile, err := s.ReadTransactionPicture(pictureInfo.Uid, pictureInfo.PictureId, pictureInfo.PictureExtension) + + if os.IsNotExist(err) { + return nil, errs.ErrTransactionPictureNoExists + } + + if err != nil { + return nil, err + } + + defer pictureFile.Close() + + pictureData, err := io.ReadAll(pictureFile) + + if err != nil { + return nil, err + } + + return pictureData, nil +} + +// UploadPicture uploads the transaction picture for specified user +func (s *TransactionPictureService) UploadPicture(c core.Context, pictureInfo *models.TransactionPictureInfo, pictureFile multipart.File) error { + if pictureInfo.Uid <= 0 { + return errs.ErrUserIdInvalid + } + + defer pictureFile.Close() + + pictureInfo.PictureId = s.GenerateUuid(uuid.UUID_TYPE_USER) + + if pictureInfo.PictureId < 1 { + return errs.ErrSystemIsBusy + } + + pictureInfo.TransactionId = 0 + pictureInfo.Deleted = false + pictureInfo.CreatedUnixTime = time.Now().Unix() + pictureInfo.UpdatedUnixTime = time.Now().Unix() + + err := s.SaveTransactionPicture(pictureInfo.Uid, pictureInfo.PictureId, pictureFile, pictureInfo.PictureExtension) + + if err != nil { + return err + } + + return s.UserDataDB(pictureInfo.Uid).DoTransaction(c, func(sess *xorm.Session) error { + _, err := sess.Insert(pictureInfo) + return err + }) +} diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index a0f8acf4..50c61b3f 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -272,6 +272,7 @@ type Config struct { EnableUserForceVerifyEmail bool EnableUserForgetPassword bool ForgetPasswordRequireVerifyEmail bool + EnableTransactionPictures bool EnableScheduledTransaction bool AvatarProvider core.UserAvatarProviderType @@ -741,6 +742,7 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str config.EnableUserForceVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_force_email_verify", false) config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false) config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false) + config.EnableTransactionPictures = getConfigItemBoolValue(configFile, sectionName, "enable_transaction_picture", false) config.EnableScheduledTransaction = getConfigItemBoolValue(configFile, sectionName, "enable_scheduled_transaction", false) if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_INTERNAL) { diff --git a/pkg/storage/minio_storage.go b/pkg/storage/minio_storage.go index c99277f1..d0388aa6 100644 --- a/pkg/storage/minio_storage.go +++ b/pkg/storage/minio_storage.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "net/http" + "strings" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" @@ -40,6 +41,7 @@ func NewMinIOObjectStorage(config *settings.Config, pathPrefix string) (*MinIOOb } storage.rootPath = storage.getFinalPath(pathPrefix) + storage.rootPath = strings.ReplaceAll(storage.rootPath, "\\", "/") ctx := context.Background() exists, err := minIOClient.BucketExists(ctx, minIOConfig.Bucket) @@ -104,5 +106,7 @@ func (s *MinIOObjectStorage) getFinalPath(path string) string { path = path[1:] } + path = strings.ReplaceAll(path, "\\", "/") + return rootPath + path } diff --git a/pkg/storage/storage_container.go b/pkg/storage/storage_container.go index 4396e415..e4ba7301 100644 --- a/pkg/storage/storage_container.go +++ b/pkg/storage/storage_container.go @@ -6,10 +6,12 @@ import ( ) const avatarPathPrefix = "avatar" +const transactionPicturePathPrefix = "transaction" // StorageContainer contains the current object storage type StorageContainer struct { - AvatarCurrentStorage ObjectStorage + AvatarCurrentStorage ObjectStorage + TransactionPictureCurrentStorage ObjectStorage } // Initialize a object storage container singleton instance @@ -19,19 +21,23 @@ var ( // InitializeStorageContainer initializes the current object storage according to the config func InitializeStorageContainer(config *settings.Config) error { - if config.StorageType == settings.LocalFileSystemObjectStorageType { - avatarStorage, err := NewLocalFileSystemObjectStorage(config, avatarPathPrefix) - Container.AvatarCurrentStorage = avatarStorage - - return err - } else if config.StorageType == settings.MinIOStorageType { - avatarStorage, err := NewMinIOObjectStorage(config, avatarPathPrefix) - Container.AvatarCurrentStorage = avatarStorage + avatarStorage, err := newObjectStorage(config, avatarPathPrefix) + if err != nil { return err } - return errs.ErrInvalidStorageType + Container.AvatarCurrentStorage = avatarStorage + + transactionPictureStorage, err := newObjectStorage(config, transactionPicturePathPrefix) + + if err != nil { + return err + } + + Container.TransactionPictureCurrentStorage = transactionPictureStorage + + return nil } // ExistsAvatar returns whether the avatar file exists from the current avatar object storage @@ -53,3 +59,33 @@ func (s *StorageContainer) SaveAvatar(path string, object ObjectInStorage) error func (s *StorageContainer) DeleteAvatar(path string) error { return s.AvatarCurrentStorage.Delete(path) } + +// ExistsTransactionPicture returns whether the transaction picture file exists from the current transaction picture object storage +func (s *StorageContainer) ExistsTransactionPicture(path string) (bool, error) { + return s.TransactionPictureCurrentStorage.Exists(path) +} + +// ReadTransactionPicture returns the transaction picture file from the current transaction picture object storage +func (s *StorageContainer) ReadTransactionPicture(path string) (ObjectInStorage, error) { + return s.TransactionPictureCurrentStorage.Read(path) +} + +// SaveTransactionPicture returns whether save the transaction picture file into the current transaction picture object storage successfully +func (s *StorageContainer) SaveTransactionPicture(path string, object ObjectInStorage) error { + return s.TransactionPictureCurrentStorage.Save(path, object) +} + +// DeleteTransactionPicture returns whether delete the transaction picture file from the current transaction picture object storage successfully +func (s *StorageContainer) DeleteTransactionPicture(path string) error { + return s.TransactionPictureCurrentStorage.Delete(path) +} + +func newObjectStorage(config *settings.Config, pathPrefix string) (ObjectStorage, error) { + if config.StorageType == settings.LocalFileSystemObjectStorageType { + return NewLocalFileSystemObjectStorage(config, pathPrefix) + } else if config.StorageType == settings.MinIOStorageType { + return NewMinIOObjectStorage(config, pathPrefix) + } + + return nil, errs.ErrInvalidStorageType +} diff --git a/pkg/uuid/uuid_type.go b/pkg/uuid/uuid_type.go index 80416998..6731db1a 100644 --- a/pkg/uuid/uuid_type.go +++ b/pkg/uuid/uuid_type.go @@ -13,4 +13,5 @@ const ( UUID_TYPE_TAG UuidType = 5 UUID_TYPE_TAG_INDEX UuidType = 6 UUID_TYPE_TEMPLATE UuidType = 7 + UUID_TYPE_PICTURE UuidType = 8 ) diff --git a/src/locales/en.json b/src/locales/en.json index 2b71ac14..babbb3cd 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1088,6 +1088,12 @@ "scheduled transaction is not enabled": "Scheduled transaction is not enabled", "scheduled transaction frequency is invalid": "Scheduled transaction frequency is invalid", "transaction template has too many tags": "There are too many tags in this transaction template", + "transaction picture id is invalid": "Transaction picture ID is invalid", + "transaction picture not found": "Transaction picture is not found", + "no transaction picture": "There is no transaction picture file", + "transaction picture is empty": "Transaction picture file is empty", + "transaction picture not exists": "Transaction picture does not exist", + "transaction picture file extension invalid": "Transaction picture file extension is invalid", "query items cannot be blank": "There are no query items", "query items too much": "There are too many query items", "query items have invalid item": "There is invalid item in query items", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index 51f0e8b0..9ab0882d 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1088,6 +1088,12 @@ "scheduled transaction is not enabled": "定时交易没有启用", "scheduled transaction frequency is invalid": "定时交易周期无效", "transaction template has too many tags": "交易模板中的标签过多", + "transaction picture id is invalid": "交易图片ID无效", + "transaction picture not found": "交易图片不存在", + "no transaction picture": "没有交易图片文件", + "transaction picture is empty": "交易图片文件为空", + "transaction picture not exists": "交易图片不存在", + "transaction picture file extension invalid": "交易图片文件扩展名无效", "query items cannot be blank": "请求项目不能为空", "query items too much": "请求项目过多", "query items have invalid item": "请求项目中有非法项目",