add transaction picture upload api
This commit is contained in:
@@ -125,5 +125,13 @@ func updateAllDatabaseTablesStructure(c *core.CliContext) error {
|
|||||||
|
|
||||||
log.BootInfof(c, "[database.updateAllDatabaseTablesStructure] transaction template table maintained successfully")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
router.GET("/healthz.json", bindApi(api.Healths.HealthStatusHandler))
|
||||||
|
|
||||||
if config.Mode == settings.MODE_DEVELOPMENT {
|
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/modify.json", bindApi(api.Transactions.TransactionModifyHandler))
|
||||||
apiV1Route.POST("/transactions/delete.json", bindApi(api.Transactions.TransactionDeleteHandler))
|
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
|
// Transaction Categories
|
||||||
apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler))
|
apiV1Route.GET("/transaction/categories/list.json", bindApi(api.TransactionCategories.CategoryListHandler))
|
||||||
apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler))
|
apiV1Route.GET("/transaction/categories/get.json", bindApi(api.TransactionCategories.CategoryGetHandler))
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ enable_forget_password = true
|
|||||||
# Set to true to require email must be verified when use forget password
|
# Set to true to require email must be verified when use forget password
|
||||||
forget_password_require_email_verify = false
|
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
|
# Set to true to allow users to create scheduled transaction
|
||||||
enable_scheduled_transaction = true
|
enable_scheduled_transaction = true
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
"github.com/mayswind/ezbookkeeping/pkg/duplicatechecker"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const internalTransactionPictureUrlFormat = "%spictures/%d.%s"
|
||||||
|
|
||||||
// ApiUsingConfig represents an api that need to use config
|
// ApiUsingConfig represents an api that need to use config
|
||||||
type ApiUsingConfig struct {
|
type ApiUsingConfig struct {
|
||||||
container *settings.ConfigContainer
|
container *settings.ConfigContainer
|
||||||
@@ -17,6 +21,12 @@ func (a *ApiUsingConfig) CurrentConfig() *settings.Config {
|
|||||||
return a.container.Current
|
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
|
// GetAfterRegisterNotificationContent returns the notification content displayed each time users register
|
||||||
func (a *ApiUsingConfig) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string {
|
func (a *ApiUsingConfig) GetAfterRegisterNotificationContent(userLanguage string, clientLanguage string) string {
|
||||||
language := userLanguage
|
language := userLanguage
|
||||||
|
|||||||
153
pkg/api/transaction_pictures.go
Normal file
153
pkg/api/transaction_pictures.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,4 +10,5 @@ const (
|
|||||||
DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2
|
DUPLICATE_CHECKER_TYPE_NEW_CATEGORY DuplicateCheckerType = 2
|
||||||
DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3
|
DUPLICATE_CHECKER_TYPE_NEW_TRANSACTION DuplicateCheckerType = 3
|
||||||
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
|
DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4
|
||||||
|
DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 5
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const (
|
|||||||
NormalSubcategoryDataManagement = 8
|
NormalSubcategoryDataManagement = 8
|
||||||
NormalSubcategoryMapProxy = 9
|
NormalSubcategoryMapProxy = 9
|
||||||
NormalSubcategoryTemplate = 10
|
NormalSubcategoryTemplate = 10
|
||||||
|
NormalSubcategoryPicture = 11
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error represents the specific error returned to user
|
// Error represents the specific error returned to user
|
||||||
|
|||||||
13
pkg/errs/transaction_picture.go
Normal file
13
pkg/errs/transaction_picture.go
Normal file
@@ -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")
|
||||||
|
)
|
||||||
@@ -19,6 +19,7 @@ func ServerSettingsCookie(config *settings.Config) core.MiddlewareHandlerFunc {
|
|||||||
buildBooleanSetting("r", config.EnableUserRegister),
|
buildBooleanSetting("r", config.EnableUserRegister),
|
||||||
buildBooleanSetting("f", config.EnableUserForgetPassword),
|
buildBooleanSetting("f", config.EnableUserForgetPassword),
|
||||||
buildBooleanSetting("v", config.EnableUserVerifyEmail),
|
buildBooleanSetting("v", config.EnableUserVerifyEmail),
|
||||||
|
buildBooleanSetting("p", config.EnableTransactionPictures),
|
||||||
buildBooleanSetting("s", config.EnableScheduledTransaction),
|
buildBooleanSetting("s", config.EnableScheduledTransaction),
|
||||||
buildBooleanSetting("e", config.EnableDataExport),
|
buildBooleanSetting("e", config.EnableDataExport),
|
||||||
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
|
buildStringSetting("m", strings.Replace(config.MapProvider, "_", "-", -1)),
|
||||||
|
|||||||
28
pkg/models/transaction_picture_info.go
Normal file
28
pkg/models/transaction_picture_info.go
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,12 +2,14 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
"github.com/mayswind/ezbookkeeping/pkg/datastore"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
"github.com/mayswind/ezbookkeeping/pkg/mail"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
||||||
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||||
"github.com/mayswind/ezbookkeeping/pkg/uuid"
|
"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))
|
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 {
|
func (s *ServiceUsingStorage) getUserAvatarPath(uid int64, fileExtension string) string {
|
||||||
return fmt.Sprintf("%d.%s", uid, fileExtension)
|
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))
|
||||||
|
}
|
||||||
|
|||||||
140
pkg/services/transaction_pictures.go
Normal file
140
pkg/services/transaction_pictures.go
Normal file
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -272,6 +272,7 @@ type Config struct {
|
|||||||
EnableUserForceVerifyEmail bool
|
EnableUserForceVerifyEmail bool
|
||||||
EnableUserForgetPassword bool
|
EnableUserForgetPassword bool
|
||||||
ForgetPasswordRequireVerifyEmail bool
|
ForgetPasswordRequireVerifyEmail bool
|
||||||
|
EnableTransactionPictures bool
|
||||||
EnableScheduledTransaction bool
|
EnableScheduledTransaction bool
|
||||||
AvatarProvider core.UserAvatarProviderType
|
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.EnableUserForceVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "enable_force_email_verify", false)
|
||||||
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
||||||
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", 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)
|
config.EnableScheduledTransaction = getConfigItemBoolValue(configFile, sectionName, "enable_scheduled_transaction", false)
|
||||||
|
|
||||||
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_INTERNAL) {
|
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == string(core.USER_AVATAR_PROVIDER_INTERNAL) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
"github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"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 = storage.getFinalPath(pathPrefix)
|
||||||
|
storage.rootPath = strings.ReplaceAll(storage.rootPath, "\\", "/")
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
exists, err := minIOClient.BucketExists(ctx, minIOConfig.Bucket)
|
exists, err := minIOClient.BucketExists(ctx, minIOConfig.Bucket)
|
||||||
@@ -104,5 +106,7 @@ func (s *MinIOObjectStorage) getFinalPath(path string) string {
|
|||||||
path = path[1:]
|
path = path[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
path = strings.ReplaceAll(path, "\\", "/")
|
||||||
|
|
||||||
return rootPath + path
|
return rootPath + path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const avatarPathPrefix = "avatar"
|
const avatarPathPrefix = "avatar"
|
||||||
|
const transactionPicturePathPrefix = "transaction"
|
||||||
|
|
||||||
// StorageContainer contains the current object storage
|
// StorageContainer contains the current object storage
|
||||||
type StorageContainer struct {
|
type StorageContainer struct {
|
||||||
AvatarCurrentStorage ObjectStorage
|
AvatarCurrentStorage ObjectStorage
|
||||||
|
TransactionPictureCurrentStorage ObjectStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a object storage container singleton instance
|
// Initialize a object storage container singleton instance
|
||||||
@@ -19,19 +21,23 @@ var (
|
|||||||
|
|
||||||
// InitializeStorageContainer initializes the current object storage according to the config
|
// InitializeStorageContainer initializes the current object storage according to the config
|
||||||
func InitializeStorageContainer(config *settings.Config) error {
|
func InitializeStorageContainer(config *settings.Config) error {
|
||||||
if config.StorageType == settings.LocalFileSystemObjectStorageType {
|
avatarStorage, err := newObjectStorage(config, avatarPathPrefix)
|
||||||
avatarStorage, err := NewLocalFileSystemObjectStorage(config, avatarPathPrefix)
|
|
||||||
Container.AvatarCurrentStorage = avatarStorage
|
|
||||||
|
|
||||||
return err
|
|
||||||
} else if config.StorageType == settings.MinIOStorageType {
|
|
||||||
avatarStorage, err := NewMinIOObjectStorage(config, avatarPathPrefix)
|
|
||||||
Container.AvatarCurrentStorage = avatarStorage
|
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
return err
|
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
|
// 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 {
|
func (s *StorageContainer) DeleteAvatar(path string) error {
|
||||||
return s.AvatarCurrentStorage.Delete(path)
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,4 +13,5 @@ const (
|
|||||||
UUID_TYPE_TAG UuidType = 5
|
UUID_TYPE_TAG UuidType = 5
|
||||||
UUID_TYPE_TAG_INDEX UuidType = 6
|
UUID_TYPE_TAG_INDEX UuidType = 6
|
||||||
UUID_TYPE_TEMPLATE UuidType = 7
|
UUID_TYPE_TEMPLATE UuidType = 7
|
||||||
|
UUID_TYPE_PICTURE UuidType = 8
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1088,6 +1088,12 @@
|
|||||||
"scheduled transaction is not enabled": "Scheduled transaction is not enabled",
|
"scheduled transaction is not enabled": "Scheduled transaction is not enabled",
|
||||||
"scheduled transaction frequency is invalid": "Scheduled transaction frequency is invalid",
|
"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 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 cannot be blank": "There are no query items",
|
||||||
"query items too much": "There are too many query items",
|
"query items too much": "There are too many query items",
|
||||||
"query items have invalid item": "There is invalid item in query items",
|
"query items have invalid item": "There is invalid item in query items",
|
||||||
|
|||||||
@@ -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": "交易模板中的标签过多",
|
"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 cannot be blank": "请求项目不能为空",
|
||||||
"query items too much": "请求项目过多",
|
"query items too much": "请求项目过多",
|
||||||
"query items have invalid item": "请求项目中有非法项目",
|
"query items have invalid item": "请求项目中有非法项目",
|
||||||
|
|||||||
Reference in New Issue
Block a user