mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-16 07:57:33 +08:00
supports local file system object storage and use it as the default avatar provider
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,6 +15,8 @@ import (
|
||||
"github.com/mayswind/ezbookkeeping/pkg/models"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/services"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/storage"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/validators"
|
||||
)
|
||||
|
||||
@@ -494,6 +498,128 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.Context) (any, *errs.Error)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UserUpdateAvatarHandler saves user avatar by request parameters for current user
|
||||
func (a *UsersApi) UserUpdateAvatarHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
form, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get multi-part form data for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrParameterInvalid
|
||||
}
|
||||
|
||||
avatars := form.File["avatar"]
|
||||
|
||||
if len(avatars) < 1 {
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] there is no user avatar in request for user \"uid:%d\"", user.Uid)
|
||||
return nil, errs.ErrNoUserAvatar
|
||||
}
|
||||
|
||||
if avatars[0].Size < 1 {
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the size of user avatar in request is zero for user \"uid:%d\"", user.Uid)
|
||||
return nil, errs.ErrUserAvatarIsEmpty
|
||||
}
|
||||
|
||||
fileExtension := utils.GetFileNameExtension(avatars[0].Filename)
|
||||
|
||||
if utils.GetImageContentType(fileExtension) == "" {
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] the file extension \"%s\" of user avatar in request is not supported for user \"uid:%d\"", fileExtension, user.Uid)
|
||||
return nil, errs.ErrImageTypeNotSupported
|
||||
}
|
||||
|
||||
avatarFile, err := avatars[0].Open()
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to get avatar file from request for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
defer avatarFile.Close()
|
||||
|
||||
err = storage.Container.SaveAvatar(user.Uid, avatarFile, fileExtension)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to save avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
err = a.users.UpdateUserAvatar(c, user.Uid, fileExtension)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
if fileExtension != user.CustomAvatarType {
|
||||
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
|
||||
|
||||
if err != nil {
|
||||
log.WarnfWithRequestId(c, "[users.UserUpdateAvatarHandler] failed to delete old avatar with extension \"%s\" for user \"uid:%d\", because %s", user.CustomAvatarType, user.Uid, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
user.CustomAvatarType = fileExtension
|
||||
userResp := user.ToUserProfileResponse()
|
||||
return userResp, nil
|
||||
}
|
||||
|
||||
// UserRemoveAvatarHandler removes user avatar by request parameters for current user
|
||||
func (a *UsersApi) UserRemoveAvatarHandler(c *core.Context) (any, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.CustomAvatarType == "" {
|
||||
return nil, errs.ErrNothingWillBeUpdated
|
||||
}
|
||||
|
||||
err = storage.Container.DeleteAvatar(user.Uid, user.CustomAvatarType)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete avatar file for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
|
||||
exists, err := storage.Container.ExistsAvatar(user.Uid, user.CustomAvatarType)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to check whether avatar file exist for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
if exists {
|
||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to delete whether avatar file exist for user \"uid:%d\", the avatar file still exist", user.Uid)
|
||||
return nil, errs.ErrOperationFailed
|
||||
}
|
||||
}
|
||||
|
||||
err = a.users.UpdateUserAvatar(c, user.Uid, "")
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserRemoveAvatarHandler] failed to update user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, errs.Or(err, errs.ErrOperationFailed)
|
||||
}
|
||||
|
||||
user.CustomAvatarType = ""
|
||||
userResp := user.ToUserProfileResponse()
|
||||
return userResp, nil
|
||||
}
|
||||
|
||||
// UserSendVerifyEmailByUnloginUserHandler sends unlogin user verify email
|
||||
func (a *UsersApi) UserSendVerifyEmailByUnloginUserHandler(c *core.Context) (any, *errs.Error) {
|
||||
if !settings.Container.Current.EnableUserVerifyEmail {
|
||||
@@ -593,3 +719,60 @@ func (a *UsersApi) UserSendVerifyEmailByLoginedUserHandler(c *core.Context) (any
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// UserGetAvatarHandler returns user avatar data for current user
|
||||
func (a *UsersApi) UserGetAvatarHandler(c *core.Context) ([]byte, string, *errs.Error) {
|
||||
uid := c.GetCurrentUid()
|
||||
user, err := a.users.GetUserById(c, uid)
|
||||
|
||||
if err != nil {
|
||||
if !errs.IsCustomError(err) {
|
||||
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user, because %s", err.Error())
|
||||
}
|
||||
|
||||
return nil, "", errs.ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.CustomAvatarType == "" {
|
||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user does not have avatar for user \"uid:%d\"", user.Uid)
|
||||
return nil, "", errs.ErrUserAvatarNoExists
|
||||
}
|
||||
|
||||
fileName := c.Param("fileName")
|
||||
fileBaseName := utils.GetFileNameWithoutExtension(fileName)
|
||||
|
||||
if utils.Int64ToString(user.Uid) != fileBaseName {
|
||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] cannot get other user avatar \"uid:%s\" for user \"uid:%d\"", fileBaseName, user.Uid)
|
||||
return nil, "", errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
fileExtension := utils.GetFileNameExtension(fileName)
|
||||
|
||||
if user.CustomAvatarType != fileExtension {
|
||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar extension is invalid \"%s\" for user \"uid:%d\"", fileExtension, user.Uid)
|
||||
return nil, "", errs.ErrUserAvatarNoExists
|
||||
}
|
||||
|
||||
avatarFile, err := storage.Container.ReadAvatar(user.Uid, fileExtension)
|
||||
|
||||
if os.IsNotExist(err) {
|
||||
log.WarnfWithRequestId(c, "[users.UserGetAvatarHandler] user avatar file not exist for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, "", errs.ErrUserAvatarNoExists
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to get user avatar object for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, "", errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
defer avatarFile.Close()
|
||||
|
||||
avatarData, err := io.ReadAll(avatarFile)
|
||||
|
||||
if err != nil {
|
||||
log.ErrorfWithRequestId(c, "[users.UserGetAvatarHandler] failed to read user avatar object data for user \"uid:%d\", because %s", user.Uid, err.Error())
|
||||
return nil, "", errs.ErrOperationFailed
|
||||
}
|
||||
|
||||
return avatarData, utils.GetImageContentType(fileExtension), nil
|
||||
}
|
||||
|
||||
+4
-1
@@ -12,8 +12,11 @@ type MiddlewareHandlerFunc func(*Context)
|
||||
// ApiHandlerFunc represents the api handler function
|
||||
type ApiHandlerFunc func(*Context) (any, *errs.Error)
|
||||
|
||||
// DataHandlerFunc represents the handler function that returns byte array
|
||||
// DataHandlerFunc represents the handler function that returns file data byte array and file name
|
||||
type DataHandlerFunc func(*Context) ([]byte, string, *errs.Error)
|
||||
|
||||
// ImageHandlerFunc represents the handler function that returns image byte array and content type
|
||||
type ImageHandlerFunc func(*Context) ([]byte, string, *errs.Error)
|
||||
|
||||
// ProxyHandlerFunc represents the reverse proxy handler function
|
||||
type ProxyHandlerFunc func(*Context) (*httputil.ReverseProxy, *errs.Error)
|
||||
|
||||
+14
-12
@@ -9,16 +9,18 @@ var (
|
||||
ErrInvalidLogMode = NewSystemError(SystemSubcategorySetting, 2, http.StatusInternalServerError, "invalid log mode")
|
||||
ErrInvalidLogLevel = NewSystemError(SystemSubcategorySetting, 3, http.StatusInternalServerError, "invalid log level")
|
||||
ErrGettingLocalAddress = NewSystemError(SystemSubcategorySetting, 4, http.StatusInternalServerError, "failed to get local address")
|
||||
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid uuid mode")
|
||||
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid duplicate checker type")
|
||||
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
|
||||
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid token expired time")
|
||||
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid token min refresh interval")
|
||||
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid temporary token expired time")
|
||||
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid email verify token expired time")
|
||||
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid avatar provider")
|
||||
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid map provider")
|
||||
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid amap security verification method")
|
||||
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid password reset token expired time")
|
||||
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid exchange rates data source")
|
||||
ErrInvalidStorageType = NewSystemError(SystemSubcategorySetting, 5, http.StatusInternalServerError, "invalid storage type")
|
||||
ErrInvalidLocalFileSystemStoragePath = NewSystemError(SystemSubcategorySetting, 6, http.StatusInternalServerError, "invalid local file system storage path")
|
||||
ErrInvalidUuidMode = NewSystemError(SystemSubcategorySetting, 7, http.StatusInternalServerError, "invalid uuid mode")
|
||||
ErrInvalidDuplicateCheckerType = NewSystemError(SystemSubcategorySetting, 8, http.StatusInternalServerError, "invalid duplicate checker type")
|
||||
ErrInvalidInMemoryDuplicateCheckerCleanupInterval = NewSystemError(SystemSubcategorySetting, 9, http.StatusInternalServerError, "invalid in-memory duplicate checker cleanup interval")
|
||||
ErrInvalidTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 10, http.StatusInternalServerError, "invalid token expired time")
|
||||
ErrInvalidTokenMinRefreshInterval = NewSystemError(SystemSubcategorySetting, 11, http.StatusInternalServerError, "invalid token min refresh interval")
|
||||
ErrInvalidTemporaryTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 12, http.StatusInternalServerError, "invalid temporary token expired time")
|
||||
ErrInvalidEmailVerifyTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 13, http.StatusInternalServerError, "invalid email verify token expired time")
|
||||
ErrInvalidAvatarProvider = NewSystemError(SystemSubcategorySetting, 14, http.StatusInternalServerError, "invalid avatar provider")
|
||||
ErrInvalidMapProvider = NewSystemError(SystemSubcategorySetting, 15, http.StatusInternalServerError, "invalid map provider")
|
||||
ErrInvalidAmapSecurityVerificationMethod = NewSystemError(SystemSubcategorySetting, 16, http.StatusInternalServerError, "invalid amap security verification method")
|
||||
ErrInvalidPasswordResetTokenExpiredTime = NewSystemError(SystemSubcategorySetting, 17, http.StatusInternalServerError, "invalid password reset token expired time")
|
||||
ErrInvalidExchangeRatesDataSource = NewSystemError(SystemSubcategorySetting, 18, http.StatusInternalServerError, "invalid exchange rates data source")
|
||||
)
|
||||
|
||||
+7
-6
@@ -4,10 +4,11 @@ import "net/http"
|
||||
|
||||
// Error codes related to transaction categories
|
||||
var (
|
||||
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
||||
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
||||
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
||||
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
||||
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
||||
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
||||
ErrSystemError = NewSystemError(SystemSubcategoryDefault, 0, http.StatusInternalServerError, "system error")
|
||||
ErrApiNotFound = NewSystemError(SystemSubcategoryDefault, 1, http.StatusNotFound, "api not found")
|
||||
ErrMethodNotAllowed = NewSystemError(SystemSubcategoryDefault, 2, http.StatusMethodNotAllowed, "method not allowed")
|
||||
ErrNotImplemented = NewSystemError(SystemSubcategoryDefault, 3, http.StatusNotImplemented, "not implemented")
|
||||
ErrSystemIsBusy = NewSystemError(SystemSubcategoryDefault, 4, http.StatusServiceUnavailable, "system is busy")
|
||||
ErrNotSupported = NewSystemError(SystemSubcategoryDefault, 5, http.StatusBadRequest, "not supported")
|
||||
ErrImageTypeNotSupported = NewSystemError(SystemSubcategoryDefault, 6, http.StatusBadRequest, "image type not supported")
|
||||
)
|
||||
|
||||
@@ -31,4 +31,7 @@ var (
|
||||
ErrEmailValidationNotAllowed = NewNormalError(NormalSubcategoryUser, 22, http.StatusBadRequest, "email validation not allowed")
|
||||
ErrDecimalSeparatorAndDigitGroupingSymbolCannotBeEqual = NewNormalError(NormalSubcategoryUser, 23, http.StatusBadRequest, "decimal separator and digit grouping symbol cannot be equal")
|
||||
ErrUserDefaultAccountIsHidden = NewNormalError(NormalSubcategoryUser, 24, http.StatusBadRequest, "user default account is hidden")
|
||||
ErrNoUserAvatar = NewNormalError(NormalSubcategoryUser, 25, http.StatusBadRequest, "no user avatar")
|
||||
ErrUserAvatarIsEmpty = NewNormalError(NormalSubcategoryUser, 26, http.StatusBadRequest, "user avatar is empty")
|
||||
ErrUserAvatarNoExists = NewNormalError(NormalSubcategoryUser, 27, http.StatusNotFound, "user avatar not exists")
|
||||
)
|
||||
|
||||
+4
-1
@@ -88,6 +88,7 @@ type User struct {
|
||||
Nickname string `xorm:"VARCHAR(64) NOT NULL"`
|
||||
Password string `xorm:"VARCHAR(64) NOT NULL"`
|
||||
Salt string `xorm:"VARCHAR(10) NOT NULL"`
|
||||
CustomAvatarType string `xorm:"VARCHAR(10)"`
|
||||
DefaultAccountId int64
|
||||
TransactionEditScope TransactionEditScope `xorm:"TINYINT NOT NULL"`
|
||||
Language string `xorm:"VARCHAR(10)"`
|
||||
@@ -313,7 +314,9 @@ func (u *User) getAvatarProvider() string {
|
||||
func (u *User) getAvatarUrl() string {
|
||||
avatarProvider := settings.Container.Current.AvatarProvider
|
||||
|
||||
if avatarProvider == settings.GravatarProvider {
|
||||
if avatarProvider == settings.InternalAvatarProvider {
|
||||
return utils.GetInternalAvatarUrl(u.Uid, u.CustomAvatarType, settings.Container.Current.RootUrl)
|
||||
} else if avatarProvider == settings.GravatarProvider {
|
||||
return utils.GetGravatarUrl(u.Email)
|
||||
}
|
||||
|
||||
|
||||
@@ -294,6 +294,25 @@ func (s *UserService) UpdateUser(c *core.Context, user *models.User, modifyUserL
|
||||
return keyProfileUpdated, emailSetToUnverified, nil
|
||||
}
|
||||
|
||||
// UpdateUserAvatar updated the custom avatar type of specified user
|
||||
func (s *UserService) UpdateUserAvatar(c *core.Context, uid int64, customAvatarType string) error {
|
||||
if uid <= 0 {
|
||||
return errs.ErrUserIdInvalid
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
updateModel := &models.User{
|
||||
CustomAvatarType: customAvatarType,
|
||||
UpdatedUnixTime: now,
|
||||
}
|
||||
|
||||
return s.UserDB().DoTransaction(c, func(sess *xorm.Session) error {
|
||||
_, err := sess.ID(uid).Cols("custom_avatar_type", "updated_unix_time").Where("deleted=?", false).Update(updateModel)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateUserLastLoginTime updates the last login time field
|
||||
func (s *UserService) UpdateUserLastLoginTime(c *core.Context, uid int64) error {
|
||||
if uid <= 0 {
|
||||
|
||||
+39
-3
@@ -57,6 +57,11 @@ const (
|
||||
Sqlite3DbType string = "sqlite3"
|
||||
)
|
||||
|
||||
// Object Storage types
|
||||
const (
|
||||
LocalFileSystemObjectStorageType string = "local_filesystem"
|
||||
)
|
||||
|
||||
// Uuid generator types
|
||||
const (
|
||||
InternalUuidGeneratorType string = "internal"
|
||||
@@ -69,7 +74,8 @@ const (
|
||||
|
||||
// User avatar provider types
|
||||
const (
|
||||
GravatarProvider string = "gravatar"
|
||||
InternalAvatarProvider string = "internal"
|
||||
GravatarProvider string = "gravatar"
|
||||
)
|
||||
|
||||
// Map provider types
|
||||
@@ -201,6 +207,10 @@ type Config struct {
|
||||
LogLevel Level
|
||||
FileLogPath string
|
||||
|
||||
// Storage
|
||||
StorageType string
|
||||
LocalFileSystemPath string
|
||||
|
||||
// Uuid
|
||||
UuidGeneratorType string
|
||||
UuidServerId uint8
|
||||
@@ -311,6 +321,12 @@ func LoadConfiguration(configFilePath string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = loadStorageConfiguration(config, cfgFile, "storage")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = loadUuidConfiguration(config, cfgFile, "uuid")
|
||||
|
||||
if err != nil {
|
||||
@@ -523,6 +539,24 @@ func loadLogConfiguration(config *Config, configFile *ini.File, sectionName stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadStorageConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||
if getConfigItemStringValue(configFile, sectionName, "storage_type") == LocalFileSystemObjectStorageType {
|
||||
config.StorageType = LocalFileSystemObjectStorageType
|
||||
} else {
|
||||
return errs.ErrInvalidStorageType
|
||||
}
|
||||
|
||||
localFileSystemRootPath := getConfigItemStringValue(configFile, sectionName, "local_filesystem_path")
|
||||
finalLocalFileSystemRootPath, err := getFinalPath(config.WorkingPath, localFileSystemRootPath)
|
||||
config.LocalFileSystemPath = finalLocalFileSystemRootPath
|
||||
|
||||
if config.StorageType == LocalFileSystemObjectStorageType && err != nil {
|
||||
return errs.ErrInvalidLocalFileSystemStoragePath
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadUuidConfiguration(config *Config, configFile *ini.File, sectionName string) error {
|
||||
if getConfigItemStringValue(configFile, sectionName, "generator_type") == InternalUuidGeneratorType {
|
||||
config.UuidGeneratorType = InternalUuidGeneratorType
|
||||
@@ -619,10 +653,12 @@ func loadUserConfiguration(config *Config, configFile *ini.File, sectionName str
|
||||
config.EnableUserForgetPassword = getConfigItemBoolValue(configFile, sectionName, "enable_forget_password", false)
|
||||
config.ForgetPasswordRequireVerifyEmail = getConfigItemBoolValue(configFile, sectionName, "forget_password_require_email_verify", false)
|
||||
|
||||
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
|
||||
config.AvatarProvider = ""
|
||||
if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == InternalAvatarProvider {
|
||||
config.AvatarProvider = InternalAvatarProvider
|
||||
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == GravatarProvider {
|
||||
config.AvatarProvider = GravatarProvider
|
||||
} else if getConfigItemStringValue(configFile, sectionName, "avatar_provider") == "" {
|
||||
config.AvatarProvider = ""
|
||||
} else {
|
||||
return errs.ErrInvalidAvatarProvider
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
||||
)
|
||||
|
||||
// LocalFileSystemObjectStorage represents local file system object storage
|
||||
type LocalFileSystemObjectStorage struct {
|
||||
rootPath string
|
||||
}
|
||||
|
||||
// NewLocalFileSystemObjectStorage returns a local file system object storage
|
||||
func NewLocalFileSystemObjectStorage(config *settings.Config, pathPrefix string) (*LocalFileSystemObjectStorage, error) {
|
||||
storage := &LocalFileSystemObjectStorage{
|
||||
rootPath: filepath.Join(config.LocalFileSystemPath, pathPrefix),
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(storage.rootPath, os.ModePerm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// Exists returns whether the file exists
|
||||
func (s *LocalFileSystemObjectStorage) Exists(path string) (bool, error) {
|
||||
return utils.IsExists(s.getFinalPath(path))
|
||||
}
|
||||
|
||||
// Read returns the object instance according to specified the file path
|
||||
func (s *LocalFileSystemObjectStorage) Read(path string) (ObjectInStorage, error) {
|
||||
return os.Open(s.getFinalPath(path))
|
||||
}
|
||||
|
||||
// Save returns whether save the object instance successfully
|
||||
func (s *LocalFileSystemObjectStorage) Save(path string, object ObjectInStorage) error {
|
||||
finalPath := s.getFinalPath(path)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(finalPath), os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile, err := os.Create(finalPath)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer targetFile.Close()
|
||||
|
||||
_, err = io.Copy(targetFile, object)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete returns whether delete the object according to specified the file path successfully
|
||||
func (s *LocalFileSystemObjectStorage) Delete(path string) error {
|
||||
return os.Remove(s.getFinalPath(path))
|
||||
}
|
||||
|
||||
func (s *LocalFileSystemObjectStorage) getFinalPath(path string) string {
|
||||
return filepath.Join(s.rootPath, path)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// ObjectInStorage represents the object instance in the storage
|
||||
type ObjectInStorage interface {
|
||||
io.ReadCloser
|
||||
io.Seeker
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package storage
|
||||
|
||||
// ObjectStorage represents an object storage to store file object
|
||||
type ObjectStorage interface {
|
||||
Exists(path string) (bool, error)
|
||||
Read(path string) (ObjectInStorage, error)
|
||||
Save(path string, object ObjectInStorage) error
|
||||
Delete(path string) error
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
||||
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
||||
)
|
||||
|
||||
const avatarPathPrefix = "avatar"
|
||||
|
||||
// StorageContainer contains the current object storage
|
||||
type StorageContainer struct {
|
||||
AvatarCurrentStorage ObjectStorage
|
||||
}
|
||||
|
||||
// Initialize a object storage container singleton instance
|
||||
var (
|
||||
Container = &StorageContainer{}
|
||||
)
|
||||
|
||||
// InitializeStorageContainer initializes the current object storage according to the config
|
||||
func InitializeStorageContainer(config *settings.Config) error {
|
||||
if config.StorageType == settings.LocalFileSystemObjectStorageType {
|
||||
storage, err := NewLocalFileSystemObjectStorage(config, avatarPathPrefix)
|
||||
Container.AvatarCurrentStorage = storage
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return errs.ErrInvalidStorageType
|
||||
}
|
||||
|
||||
// ExistsAvatar returns whether the user avatar exists from the current object storage
|
||||
func (s *StorageContainer) ExistsAvatar(uid int64, fileExtension string) (bool, error) {
|
||||
return s.AvatarCurrentStorage.Exists(s.getUserAvatarPath(uid, fileExtension))
|
||||
}
|
||||
|
||||
// ReadAvatar returns the user avatar from the current object storage
|
||||
func (s *StorageContainer) ReadAvatar(uid int64, fileExtension string) (ObjectInStorage, error) {
|
||||
return s.AvatarCurrentStorage.Read(s.getUserAvatarPath(uid, fileExtension))
|
||||
}
|
||||
|
||||
// SaveAvatar returns whether save the user avatar into the current object storage successfully
|
||||
func (s *StorageContainer) SaveAvatar(uid int64, object ObjectInStorage, fileExtension string) error {
|
||||
return s.AvatarCurrentStorage.Save(s.getUserAvatarPath(uid, fileExtension), object)
|
||||
}
|
||||
|
||||
// DeleteAvatar returns whether delete the user avatar from the current object storage successfully
|
||||
func (s *StorageContainer) DeleteAvatar(uid int64, fileExtension string) error {
|
||||
return s.AvatarCurrentStorage.Delete(s.getUserAvatarPath(uid, fileExtension))
|
||||
}
|
||||
|
||||
func (s *StorageContainer) getUserAvatarPath(uid int64, fileExtension string) string {
|
||||
return fmt.Sprintf("%d.%s", uid, fileExtension)
|
||||
}
|
||||
@@ -7,6 +7,15 @@ import (
|
||||
|
||||
const gravatarUrlFormat = "https://www.gravatar.com/avatar/%s"
|
||||
|
||||
// GetInternalAvatarUrl returns the internal avatar url
|
||||
func GetInternalAvatarUrl(uid int64, avatarFileExtesion string, webRootUrl string) string {
|
||||
if avatarFileExtesion == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%savatar/%d.%s", webRootUrl, uid, avatarFileExtesion)
|
||||
}
|
||||
|
||||
// GetGravatarUrl returns the Gravatar url according to the specified user email address
|
||||
func GetGravatarUrl(email string) string {
|
||||
email = strings.TrimSpace(email)
|
||||
|
||||
@@ -6,6 +6,16 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetInternalAvatarUrl(t *testing.T) {
|
||||
expectedValue := "https://demo.ezbookkeeping.mayswind.net/avatar/1234567890.jpg"
|
||||
actualValue := GetInternalAvatarUrl(1234567890, "jpg", "https://demo.ezbookkeeping.mayswind.net/")
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
|
||||
expectedValue = ""
|
||||
actualValue = GetInternalAvatarUrl(1234567890, "", "https://demo.ezbookkeeping.mayswind.net/")
|
||||
assert.Equal(t, expectedValue, actualValue)
|
||||
}
|
||||
|
||||
func TestGetGravatarUrl(t *testing.T) {
|
||||
// Reference: https://en.gravatar.com/site/implement/hash/
|
||||
expectedValue := "https://www.gravatar.com/avatar/0bc83cb571cd1c50ba6f3e8a78ef1346"
|
||||
|
||||
@@ -3,9 +3,29 @@ package utils
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var imageFileExtensionContentTypeMap = map[string]string{
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
"png": "image/png",
|
||||
"gif": "image/gif",
|
||||
"webp": "image/webp",
|
||||
}
|
||||
|
||||
// GetImageContentType returns the content type of specified image file extension or returns empty when the file extension is not image or not supported
|
||||
func GetImageContentType(fileExtension string) string {
|
||||
contentType, exists := imageFileExtensionContentTypeMap[fileExtension]
|
||||
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return contentType
|
||||
}
|
||||
|
||||
// ListFileNamesWithPrefixAndSuffix returns file name list which has specified prefix and suffix
|
||||
func ListFileNamesWithPrefixAndSuffix(path string, prefix string, suffix string) []string {
|
||||
dir, err := os.Open(path)
|
||||
@@ -69,6 +89,29 @@ func WriteFile(path string, data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// GetFileNameWithoutExtension returns the file name without extension
|
||||
func GetFileNameWithoutExtension(path string) string {
|
||||
fileName := filepath.Base(path)
|
||||
extension := filepath.Ext(fileName)
|
||||
|
||||
if len(extension) < 1 {
|
||||
return fileName
|
||||
}
|
||||
|
||||
return fileName[0 : len(fileName)-len(extension)]
|
||||
}
|
||||
|
||||
// GetFileNameExtension returns the file extension without dot
|
||||
func GetFileNameExtension(path string) string {
|
||||
extension := filepath.Ext(path)
|
||||
|
||||
if len(extension) < 1 || extension[0] != '.' {
|
||||
return extension
|
||||
}
|
||||
|
||||
return extension[1:]
|
||||
}
|
||||
|
||||
// IdentReader returns the original io reader
|
||||
func IdentReader(encoding string, input io.Reader) (io.Reader, error) {
|
||||
return input, nil
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetImageContentType(t *testing.T) {
|
||||
fileName := "gif"
|
||||
expectedContentType := "image/gif"
|
||||
actualContentType := GetImageContentType(fileName)
|
||||
assert.Equal(t, expectedContentType, actualContentType)
|
||||
|
||||
fileName = "bmp"
|
||||
expectedContentType = ""
|
||||
actualContentType = GetImageContentType(fileName)
|
||||
assert.Equal(t, expectedContentType, actualContentType)
|
||||
}
|
||||
|
||||
func TestGetFileNameWithoutExtension(t *testing.T) {
|
||||
fileName := "name.ext"
|
||||
expectedName := "name"
|
||||
actualName := GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "C:\\name.ext"
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "/root/name.ext"
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "name"
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "C:\\name"
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "/root/name"
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "name."
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "C:\\name."
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "/root/name."
|
||||
expectedName = "name"
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = ".ext"
|
||||
expectedName = ""
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "C:\\.ext"
|
||||
expectedName = ""
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
|
||||
fileName = "/root/.ext"
|
||||
expectedName = ""
|
||||
actualName = GetFileNameWithoutExtension(fileName)
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
}
|
||||
|
||||
func TestGetFileNameExtension(t *testing.T) {
|
||||
fileName := "name.ext"
|
||||
expectedExt := "ext"
|
||||
actualExt := GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "C:\\name.ext"
|
||||
expectedExt = "ext"
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "/root/name.ext"
|
||||
expectedExt = "ext"
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "name"
|
||||
expectedExt = ""
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "C:\\name"
|
||||
expectedExt = ""
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "/root/name"
|
||||
expectedExt = ""
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "name."
|
||||
expectedExt = ""
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "C:\\name."
|
||||
expectedExt = ""
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "/root/name."
|
||||
expectedExt = ""
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = ".ext"
|
||||
expectedExt = "ext"
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "C:\\.ext"
|
||||
expectedExt = "ext"
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
|
||||
fileName = "/root/.ext"
|
||||
expectedExt = "ext"
|
||||
actualExt = GetFileNameExtension(fileName)
|
||||
assert.Equal(t, expectedExt, actualExt)
|
||||
}
|
||||
Reference in New Issue
Block a user