├── .env ├── global ├── db.go ├── tracer.go ├── version.go ├── log.go ├── validator.go ├── env.go └── config.go ├── stop.sh ├── cmd ├── old_gen │ ├── db_gen │ │ └── db_driver │ │ │ ├── db.go │ │ │ ├── mysql.go │ │ │ └── sqlite.go │ └── gorm_gen │ │ ├── README.MD │ │ ├── pkg │ │ ├── utils.go │ │ ├── parser.go │ │ └── generator.go │ │ └── main.go ├── version.go ├── root.go ├── model_gen │ └── gen.go ├── run.go ├── gorm_gen │ └── gen.go └── mfmt │ └── main.go ├── pkg ├── util │ ├── md5.go │ ├── base64.go │ ├── password.go │ ├── xor.go │ └── authcode_encrypt.go ├── convert │ ├── map.go │ ├── convert.go │ ├── json.go │ └── copy_struct.go ├── fileurl │ ├── url.go │ └── file.go ├── limiter │ ├── limiter.go │ └── method_limiter.go ├── order │ └── order_sn.go ├── tracer │ └── tracer.go ├── rand │ └── slice.go ├── email │ └── email.go ├── app │ ├── pagination.go │ ├── dateime.go │ ├── form.go │ ├── token.go │ └── app.go ├── httpclient │ └── client.go ├── errors │ ├── err_test.go │ └── err.go ├── storage │ ├── aliyun_oss │ │ ├── operation.go │ │ └── oss.go │ ├── local_fs │ │ ├── local.go │ │ └── operation.go │ ├── webdav │ │ ├── webdav.go │ │ └── operation.go │ ├── aws_s3 │ │ ├── operation.go │ │ └── s3.go │ ├── cloudflare_r2 │ │ ├── operation.go │ │ └── r2.go │ ├── minio │ │ ├── operation.go │ │ └── minio.go │ └── storage.go ├── gin_tools │ ├── json.go │ ├── form.go │ └── param.go ├── validator │ └── custom_validator.go ├── code │ ├── lang.go │ ├── code.go │ └── common.go ├── timex │ └── time.go ├── safe_close │ └── safe_close.go └── logger │ └── logger.go ├── start_console.sh ├── main.go ├── start.sh ├── internal ├── model │ ├── model.go │ ├── user.gen.go │ └── cloud_config.gen.go ├── middleware │ ├── context_timeout.go │ ├── 404nofound.go │ ├── app_info.go │ ├── limiter.go │ ├── lang.go │ ├── auth_token.go │ ├── cors.go │ ├── user_auth_token.go │ ├── tracer.go │ ├── access_log.go │ └── recovery.go ├── service │ └── service.go ├── routers │ ├── api_router │ │ ├── metrics.go │ │ ├── upload.go │ │ ├── user.go │ │ └── cloud_config.go │ ├── pprof.go │ └── router.go ├── query │ └── gen.go └── dao │ ├── dao_user.go │ └── dao.go ├── docker_image_clean.sh ├── frontend ├── index.html └── site.svg ├── entrypoint.sh ├── docker-compose.yaml ├── docker_redeploy.sh ├── .github └── workflows │ ├── test.yml │ └── go-release-docker.yml ├── scripts └── gormgen.sh ├── .gitignore ├── .air.toml ├── Dockerfile ├── db.sql ├── readme-zh.md ├── Makefile └── docs └── docs.go /.env: -------------------------------------------------------------------------------- 1 | RUNTIME_ENVIROMENT=DEVELOPMENT -------------------------------------------------------------------------------- /global/db.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import "gorm.io/gorm" 4 | 5 | var ( 6 | DBEngine *gorm.DB 7 | ) 8 | -------------------------------------------------------------------------------- /global/tracer.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import "github.com/opentracing/opentracing-go" 4 | 5 | var ( 6 | Tracer opentracing.Tracer 7 | ) 8 | -------------------------------------------------------------------------------- /global/version.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | var Version string 4 | var GitTag = "2000.01.01.release" 5 | var BuildTime = "2000-01-01T00:00:00+0800" 6 | -------------------------------------------------------------------------------- /stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rootdir="$( cd "$( dirname "$0" )" && pwd )" 3 | webroot=${rootdir} 4 | 5 | ps aux|grep goapi_starfission|awk '{print $2}'|xargs kill -9 6 | -------------------------------------------------------------------------------- /global/log.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | var Logger *zap.Logger 8 | 9 | func Log() *zap.Logger { 10 | return Logger 11 | } 12 | -------------------------------------------------------------------------------- /cmd/old_gen/db_gen/db_driver/db.go: -------------------------------------------------------------------------------- 1 | package db_driver 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Repo interface { 8 | i() 9 | GetDb() *gorm.DB 10 | DbClose() error 11 | DbType() string 12 | } 13 | -------------------------------------------------------------------------------- /pkg/util/md5.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | // 生成md5 9 | func EncodeMD5(str string) string { 10 | h := md5.New() 11 | h.Write([]byte(str)) 12 | return hex.EncodeToString(h.Sum(nil)) 13 | } 14 | -------------------------------------------------------------------------------- /start_console.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rootdir="$( cd "$( dirname "$0" )" && pwd )" 3 | webroot=${rootdir} 4 | 5 | cd ${webroot} 6 | cp -rf ./new/apiRun ./apiRun 7 | chmod +x ./apiRun 8 | ps aux|grep goapi_starfission|awk '{print $2}'|xargs kill -9 9 | ${webroot}/apiRun 10 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | 6 | "github.com/haierkeys/custom-image-gateway/cmd" 7 | ) 8 | 9 | //go:embed frontend 10 | var efs embed.FS 11 | 12 | //go:embed config/config.yaml 13 | var c string 14 | 15 | func main() { 16 | cmd.Execute(efs, c) 17 | } 18 | -------------------------------------------------------------------------------- /global/validator.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/pkg/validator" 5 | 6 | ut "github.com/go-playground/universal-translator" 7 | ) 8 | 9 | var ( 10 | Validator *validator.CustomValidator 11 | Ut *ut.UniversalTranslator 12 | ) 13 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | rootdir="$( cd "$( dirname "$0" )" && pwd )" 3 | webroot=${rootdir} 4 | 5 | cd ${webroot} 6 | cp -rf ./new/apiRun ./apiRun 7 | chmod +x ./apiRun 8 | ps aux|grep goapi_starfission|awk '{print $2}'|xargs kill -9 9 | exec nohup ${webroot}/apiRun > ./storage/logs/c.log 2>&1 & 10 | -------------------------------------------------------------------------------- /global/env.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 5 | ) 6 | 7 | var ( 8 | // 程序执行目录 9 | ROOT string 10 | Name string = "Obsidian Image API Gateway" 11 | ) 12 | 13 | func init() { 14 | 15 | filename := fileurl.GetExePath() 16 | ROOT = filename + "/" 17 | 18 | } 19 | -------------------------------------------------------------------------------- /pkg/convert/map.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func MapAnyToMapStr(p map[string]interface{}) map[string]string { 8 | mapString := make(map[string]string) 9 | 10 | for key, value := range p { 11 | strKey := fmt.Sprintf("%v", key) 12 | strValue := fmt.Sprintf("%v", value) 13 | 14 | mapString[strKey] = strValue 15 | } 16 | return mapString 17 | } 18 | -------------------------------------------------------------------------------- /pkg/fileurl/url.go: -------------------------------------------------------------------------------- 1 | package fileurl 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | ) 7 | 8 | // UrlEscape 转义文件路径 9 | func UrlEscape(fileKey string) string { 10 | if strings.Contains(fileKey, "/") { 11 | i := strings.LastIndex(fileKey, "/") 12 | fileKey = fileKey[:i+1] + url.PathEscape(fileKey[i+1:]) 13 | } else { 14 | fileKey = url.PathEscape(fileKey) 15 | } 16 | return fileKey 17 | } 18 | -------------------------------------------------------------------------------- /internal/model/model.go: -------------------------------------------------------------------------------- 1 | 2 | package model 3 | 4 | import ( 5 | "sync" 6 | 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var once sync.Once 11 | 12 | func AutoMigrate(db *gorm.DB, key string) { 13 | switch key { 14 | 15 | case "CloudConfig": 16 | once.Do(func() { 17 | db.AutoMigrate(CloudConfig{}) 18 | }) 19 | 20 | case "User": 21 | once.Do(func() { 22 | db.AutoMigrate(User{}) 23 | }) 24 | } 25 | } -------------------------------------------------------------------------------- /cmd/old_gen/gorm_gen/README.MD: -------------------------------------------------------------------------------- 1 | ## 执行命令 2 | 在根目录下执行脚本:`./scripts/gormgen.sh addr user pass name tables`; 3 | - addr:数据库地址,例如:127.0.0.1:3306 4 | - user:账号,例如:root 5 | - pass:密码,例如:root 6 | - name:数据库名称,例如:go_gin_api 7 | - tables:表名,默认为 *,多个表名可用“,”分割,例如:user_demo 8 | 9 | 例如: 10 | ``` 11 | ./scripts/gormgen.sh 127.0.0.1:3306 root root go_gin_api user_demo 12 | ``` 13 | 14 | ## 参考 15 | - https://github.com/MohamedBassem/gormgen -------------------------------------------------------------------------------- /internal/middleware/context_timeout.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func ContextTimeout(t time.Duration) func(c *gin.Context) { 11 | return func(c *gin.Context) { 12 | ctx, cancel := context.WithTimeout(c.Request.Context(), t) 13 | defer cancel() 14 | 15 | c.Request = c.Request.WithContext(ctx) 16 | c.Next() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pkg/util/base64.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/base64" 5 | ) 6 | 7 | // base64 加密 8 | func base64Encode(s string) string { 9 | return base64.StdEncoding.EncodeToString([]byte(s)) 10 | } 11 | 12 | // base64 解密 13 | func base64Decode(s string) string { 14 | sByte, err := base64.StdEncoding.DecodeString(s) 15 | if err == nil { 16 | return string(sByte) 17 | } else { 18 | return "" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/middleware/404nofound.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/pkg/app" 5 | "github.com/haierkeys/custom-image-gateway/pkg/code" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func NoFound() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | response := app.NewResponse(c) 13 | response.ToResponse(code.ErrorNotFoundAPI) 14 | c.Abort() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/util/password.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "golang.org/x/crypto/bcrypt" 5 | ) 6 | 7 | func GeneratePasswordHash(password string) (string, error) { 8 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10) 9 | return string(bytes), err 10 | } 11 | func CheckPasswordHash(hash, password string) bool { 12 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 13 | return err == nil 14 | } 15 | -------------------------------------------------------------------------------- /cmd/old_gen/gorm_gen/pkg/utils.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "strings" 4 | 5 | // SQLColumnToHumpStyle sql转换成驼峰模式 6 | func SQLColumnToHumpStyle(in string) (ret string) { 7 | for i := 0; i < len(in); i++ { 8 | if i > 0 && in[i-1] == '_' && in[i] != '_' { 9 | s := strings.ToUpper(string(in[i])) 10 | ret += s 11 | } else if in[i] == '_' { 12 | continue 13 | } else { 14 | ret += string(in[i]) 15 | } 16 | } 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /pkg/util/xor.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | func XorEncodeStr(msg []byte, key []byte) (out []byte) { 4 | ml := len(msg) 5 | kl := len(key) 6 | for i := 0; i < ml; i++ { 7 | out = append(out, (msg[i])^(key[i%kl])) 8 | } 9 | return out 10 | } 11 | 12 | func XorEncodeStrRune(msg []rune, key []rune) (out []rune) { 13 | ml := len(msg) 14 | kl := len(key) 15 | for i := 0; i < ml; i++ { 16 | out = append(out, (msg[i])^(key[i%kl])) 17 | } 18 | return out 19 | } 20 | -------------------------------------------------------------------------------- /internal/middleware/app_info.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/global" 5 | "github.com/haierkeys/custom-image-gateway/pkg/app" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func AppInfo() gin.HandlerFunc { 11 | 12 | return func(c *gin.Context) { 13 | c.Set("app_name", global.Name) 14 | c.Set("app_version", global.Version) 15 | c.Set("access_host", app.GetAccessHost(c)) 16 | 17 | c.Next() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/haierkeys/custom-image-gateway/global" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var versionCmd = &cobra.Command{ 12 | Use: "version", 13 | Short: "Print out version info and exit.", 14 | Run: func(cmd *cobra.Command, args []string) { 15 | fmt.Printf("v%s ( Git:%s ) BuidTime:%s\n", global.Version, global.GitTag, global.BuildTime) 16 | 17 | }, 18 | } 19 | 20 | func init() { 21 | rootCmd.AddCommand(versionCmd) 22 | } 23 | -------------------------------------------------------------------------------- /docker_image_clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "docker images clean shell" 3 | projectName=$(pwd | awk -F "/" '{print $NF}') 4 | dockerrm=$(docker images | grep "${projectName}" | awk '{print $3}' | awk '!a[$0]++') 5 | 6 | if [ -n "$dockerrm" ]; then 7 | docker rmi -f ${dockerrm} 8 | echo "docker images ${projectName} clean OK" 9 | fi 10 | 11 | dockerrm=$(docker images | grep "none" | awk '{print $3}' | awk '!a[$0]++') 12 | if [ -n "$dockerrm" ]; then 13 | docker rmi -f ${dockerrm} 14 | echo "docker images none clean OK" 15 | fi 16 | -------------------------------------------------------------------------------- /pkg/limiter/limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/juju/ratelimit" 8 | ) 9 | 10 | type Face interface { 11 | Key(c *gin.Context) string 12 | GetBucket(key string) (*ratelimit.Bucket, bool) 13 | AddBuckets(rules ...BucketRule) Face 14 | } 15 | 16 | type Limiter struct { 17 | limiterBuckets map[string]*ratelimit.Bucket 18 | } 19 | 20 | type BucketRule struct { 21 | Key string 22 | FillInterval time.Duration 23 | Capacity int64 24 | Quantum int64 25 | } 26 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Custom Image Auto Uploader Public Cloud Storage Interface Management 8 | 9 | 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var frontendFiles embed.FS 12 | var configDefault string 13 | var rootCmd = &cobra.Command{ 14 | Use: "image-api", 15 | Short: "obsidian image-api gateway", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | cmd.HelpTemplate() 18 | cmd.Help() 19 | }, 20 | } 21 | 22 | func Execute(efs embed.FS, c string) { 23 | frontendFiles = efs 24 | configDefault = c 25 | if err := rootCmd.Execute(); err != nil { 26 | fmt.Println(err) 27 | os.Exit(1) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/global" 5 | "github.com/haierkeys/custom-image-gateway/internal/dao" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type Service struct { 11 | ctx *gin.Context 12 | dao *dao.Dao 13 | } 14 | 15 | func New(ctx *gin.Context) Service { 16 | 17 | svc := Service{ctx: ctx} 18 | svc.dao = dao.New(global.DBEngine, ctx) 19 | 20 | // svc.dao = dao.New(otgorm.WithContext(svc.ctx, global.DBEngine)) 21 | 22 | return svc 23 | } 24 | 25 | func (svc *Service) Ctx() *gin.Context { 26 | return svc.ctx 27 | } 28 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 检查环境变量 4 | if [ -z "$P_NAME" ] || [ -z "$P_BIN" ]; then 5 | echo "Error: P_NAME or P_BIN not set" 6 | exit 1 7 | fi 8 | 9 | # 切换目录 10 | cd "/${P_NAME}/" || { echo "Failed to cd to /${P_NAME}/"; exit 1; } 11 | 12 | # 创建日志目录和文件 13 | mkdir -p storage/logs || { echo "Failed to create logs dir"; exit 1; } 14 | touch storage/logs/c.log || { echo "Failed to create c.log"; exit 1; } 15 | 16 | # 备份旧日志,统一后缀为 .log 17 | mv storage/logs/c.log "storage/logs/c_$(date '+%Y%m%d%H%M%S').log" || { echo "Failed to rename log"; exit 1; } 18 | 19 | # 运行程序并记录日志到 c.log 20 | "/${P_NAME}/${P_BIN}" run 2>&1 | tee storage/logs/c.log -------------------------------------------------------------------------------- /internal/middleware/limiter.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/pkg/app" 5 | "github.com/haierkeys/custom-image-gateway/pkg/code" 6 | "github.com/haierkeys/custom-image-gateway/pkg/limiter" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func RateLimiter(l limiter.Face) gin.HandlerFunc { 12 | return func(c *gin.Context) { 13 | key := l.Key(c) 14 | if bucket, ok := l.GetBucket(key); ok { 15 | count := bucket.TakeAvailable(1) 16 | if count == 0 { 17 | response := app.NewResponse(c) 18 | response.ToResponse(code.ErrorTooManyRequests) 19 | c.Abort() 20 | return 21 | } 22 | } 23 | 24 | c.Next() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/routers/api_router/metrics.go: -------------------------------------------------------------------------------- 1 | package apiRouter 2 | 3 | import ( 4 | "expvar" 5 | "fmt" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func Expvar(c *gin.Context) { 11 | c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") 12 | first := true 13 | report := func(key string, value interface{}) { 14 | if !first { 15 | fmt.Fprintf(c.Writer, ",\n") 16 | } 17 | first = false 18 | if str, ok := value.(string); ok { 19 | fmt.Fprintf(c.Writer, "%q: %q", key, str) 20 | } else { 21 | fmt.Fprintf(c.Writer, "%q: %v", key, value) 22 | } 23 | } 24 | 25 | fmt.Fprintf(c.Writer, "{\n") 26 | expvar.Do(func(kv expvar.KeyValue) { 27 | report(kv.Key, kv.Value) 28 | }) 29 | fmt.Fprintf(c.Writer, "\n}\n") 30 | } 31 | -------------------------------------------------------------------------------- /internal/middleware/lang.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/global" 5 | "github.com/haierkeys/custom-image-gateway/pkg/code" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func Lang() gin.HandlerFunc { 11 | 12 | return func(c *gin.Context) { 13 | 14 | var lang string 15 | 16 | if s, exist := c.GetQuery("lang"); exist { 17 | lang = s 18 | } else if s = c.GetHeader("lang"); len(s) != 0 { 19 | lang = s 20 | } 21 | 22 | trans, found := global.Ut.GetTranslator(lang) 23 | 24 | if found { 25 | c.Set("trans", trans) 26 | } else { 27 | trans, _ := global.Ut.GetTranslator("zh") 28 | c.Set("trans", trans) 29 | } 30 | 31 | code.SetGlobalDefaultLang(lang) 32 | 33 | c.Next() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /pkg/convert/convert.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import "strconv" 4 | 5 | type StrTo string 6 | 7 | func (s StrTo) String() string { 8 | return string(s) 9 | } 10 | 11 | func (s StrTo) Int() (int, error) { 12 | v, err := strconv.Atoi(s.String()) 13 | return v, err 14 | } 15 | 16 | func (s StrTo) MustInt() int { 17 | v, _ := s.Int() 18 | return v 19 | } 20 | 21 | func (s StrTo) UInt32() (uint32, error) { 22 | v, err := strconv.Atoi(s.String()) 23 | return uint32(v), err 24 | } 25 | func (s StrTo) Int64() (int64, error) { 26 | v, err := strconv.Atoi(s.String()) 27 | return int64(v), err 28 | } 29 | 30 | func (s StrTo) MustInt64() int64 { 31 | v, _ := s.Int64() 32 | return v 33 | } 34 | 35 | func (s StrTo) MustUInt32() uint32 { 36 | v, _ := s.UInt32() 37 | return v 38 | } 39 | -------------------------------------------------------------------------------- /pkg/order/order_sn.go: -------------------------------------------------------------------------------- 1 | package order 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync/atomic" 7 | "time" 8 | 9 | "github.com/w3liu/go-common/constant/timeformat" 10 | ) 11 | 12 | var num int64 13 | 14 | // 生成24位订单号 15 | // 前面17位代表时间精确到毫秒,中间3位代表进程id,最后4位代表序号 16 | func Generate(t time.Time) string { 17 | s := t.Format(timeformat.Continuity) 18 | m := t.UnixNano()/1e6 - t.UnixNano()/1e9*1e3 19 | ms := sup(m, 3) 20 | p := os.Getpid() % 1000 21 | ps := sup(int64(p), 3) 22 | i := atomic.AddInt64(&num, 1) 23 | r := i % 10000 24 | rs := sup(r, 4) 25 | n := fmt.Sprintf("%s%s%s%s", s, ms, ps, rs) 26 | return n 27 | } 28 | 29 | // 对长度不足n的数字前面补0 30 | func sup(i int64, n int) string { 31 | m := fmt.Sprintf("%d", i) 32 | for len(m) < n { 33 | m = fmt.Sprintf("0%s", m) 34 | } 35 | return m 36 | } 37 | -------------------------------------------------------------------------------- /pkg/tracer/tracer.go: -------------------------------------------------------------------------------- 1 | package tracer 2 | 3 | import ( 4 | "io" 5 | "time" 6 | 7 | "github.com/opentracing/opentracing-go" 8 | "github.com/uber/jaeger-client-go/config" 9 | ) 10 | 11 | func NewJaegerTracer(serviceName, agentHostPort string) (opentracing.Tracer, io.Closer, error) { 12 | cfg := &config.Configuration{ 13 | ServiceName: serviceName, 14 | Sampler: &config.SamplerConfig{ 15 | Type: "const", 16 | Param: 1, 17 | }, 18 | Reporter: &config.ReporterConfig{ 19 | LogSpans: true, 20 | BufferFlushInterval: 1 * time.Second, 21 | LocalAgentHostPort: agentHostPort, 22 | }, 23 | } 24 | tracer, closer, err := cfg.NewTracer() 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | opentracing.SetGlobalTracer(tracer) 29 | return tracer, closer, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/rand/slice.go: -------------------------------------------------------------------------------- 1 | package rand 2 | 3 | import ( 4 | "math/rand" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | var r *rand.Rand 10 | 11 | func init() { 12 | r = rand.New(rand.NewSource(time.Now().UnixNano())) 13 | } 14 | 15 | // 随机从 字符串slice 里抽取一个返回 16 | func RandomStrSliceOne(s []string) string { 17 | return s[r.Intn(len(s))] 18 | } 19 | 20 | func GetRandString(length int) string { 21 | if length < 1 { 22 | return "" 23 | } 24 | char := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 25 | charArr := strings.Split(char, "") 26 | charlen := len(charArr) 27 | ran := rand.New(rand.NewSource(time.Now().Unix())) 28 | 29 | rchar := make([]string, 0, length) 30 | for i := 1; i <= length; i++ { 31 | rchar = append(rchar, charArr[ran.Intn(charlen)]) 32 | } 33 | return strings.Join(rchar, "") 34 | } 35 | -------------------------------------------------------------------------------- /pkg/email/email.go: -------------------------------------------------------------------------------- 1 | package email 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | "gopkg.in/gomail.v2" 7 | ) 8 | 9 | type Email struct { 10 | *SMTPInfo 11 | } 12 | 13 | type SMTPInfo struct { 14 | Host string 15 | Port int 16 | IsSSL bool 17 | UserName string 18 | Password string 19 | From string 20 | } 21 | 22 | func NewEmail(info *SMTPInfo) *Email { 23 | return &Email{SMTPInfo: info} 24 | } 25 | 26 | func (e *Email) SendMail(to []string, subject, body string) error { 27 | m := gomail.NewMessage() 28 | m.SetHeader("From", e.From) 29 | m.SetHeader("To", to...) 30 | m.SetHeader("Subject", subject) 31 | m.SetBody("text/html", body) 32 | 33 | dialer := gomail.NewDialer(e.Host, e.Port, e.UserName, e.Password) 34 | dialer.TLSConfig = &tls.Config{InsecureSkipVerify: e.IsSSL} 35 | return dialer.DialAndSend(m) 36 | } 37 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | image-api: 3 | image: haierkeys/custom-image-gateway:latest 4 | container_name: image-api 5 | ports: 6 | - "9000:9000" 7 | - "9001:9001" 8 | volumes: 9 | - /data/image-api/storage/:/api/storage/ 10 | - /data/image-api/config/:/api/config/ 11 | labels: 12 | - "com.centurylinklabs.watchtower.enable=true" # 添加标签,标记为需要更新 13 | 14 | watchtower: 15 | image: containrrr/watchtower 16 | container_name: watchtower 17 | volumes: 18 | - /var/run/docker.sock:/var/run/docker.sock 19 | environment: 20 | - WATCHTOWER_SCHEDULE=0 0,30 * * * * # 每半小时检查一次 21 | - WATCHTOWER_CLEANUP=true # 删除旧镜像 22 | - WATCHTOWER_MONITOR_ONLY=true # 只监控带有特定标签的容器 23 | - WATCHTOWER_INCLUDE_STOPPED=false # 不包括已停止的容器(可选) 24 | restart: unless-stopped -------------------------------------------------------------------------------- /pkg/app/pagination.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/global" 5 | "github.com/haierkeys/custom-image-gateway/pkg/convert" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func GetPage(c *gin.Context) int { 11 | page := convert.StrTo(c.Query("page")).MustInt() 12 | if page <= 0 { 13 | return 1 14 | } 15 | 16 | return page 17 | } 18 | 19 | func GetPageSize(c *gin.Context) int { 20 | pageSize := convert.StrTo(c.Query("pageSize")).MustInt() 21 | if pageSize <= 0 { 22 | return global.Config.App.DefaultPageSize 23 | } 24 | if pageSize > global.Config.App.MaxPageSize { 25 | return global.Config.App.MaxPageSize 26 | } 27 | 28 | return pageSize 29 | } 30 | 31 | func GetPageOffset(page, pageSize int) int { 32 | result := 0 33 | if page > 0 { 34 | result = (page - 1) * pageSize 35 | } 36 | 37 | return result 38 | } 39 | -------------------------------------------------------------------------------- /pkg/httpclient/client.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | func Get(url string) { 12 | 13 | httpReq, _ := http.NewRequest("GET", url, nil) 14 | httpReq.Header.Add("Content-type", "application/json") 15 | httpReq.Host = "www.example.com" 16 | } 17 | 18 | func Post(postURL string, postData map[string][]string) (string, error) { 19 | 20 | data := url.Values(postData) 21 | 22 | client := http.Client{ 23 | Timeout: 10 * time.Second, 24 | } 25 | 26 | resp, err := client.Post( 27 | postURL, 28 | "application/x-www-form-urlencoded", 29 | strings.NewReader(data.Encode()), 30 | ) 31 | if err != nil { 32 | return "", err 33 | } 34 | 35 | defer resp.Body.Close() 36 | body, err := io.ReadAll(resp.Body) 37 | if err != nil { 38 | return "", err 39 | } 40 | return string(body), err 41 | } 42 | -------------------------------------------------------------------------------- /docker_redeploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ProjectRegistry="registry.cn-shanghai.aliyuncs.com/xxx/xxxxx" 4 | ProjectPath=`pwd` 5 | ProjectName="xxxxx" 6 | 7 | Usage() { 8 | echo "Usage:" 9 | echo "test.sh [-t Git tag]" 10 | echo "Description:" 11 | exit 12 | } 13 | 14 | while getopts ':t:h:' OPT; do 15 | case $OPT in 16 | t) TAG="$OPTARG";; 17 | h) Usage;; 18 | ?) Usage;; 19 | esac 20 | done 21 | 22 | if [ ${TAG} ];then 23 | docker pull $ProjectRegistry:$TAG 24 | 25 | echo "Stop "$ProjectName 26 | 27 | docker stop $ProjectName 28 | #docker rm -v 29 | docker rm -f $ProjectName 30 | echo "Start new xxxxx" 31 | docker run -tid --name $ProjectName \ 32 | -p 8000:8000 -p 8001:8001 -p 8002:8002 \ 33 | -v $ProjectPath/storage/:/api/storage/ \ 34 | -v $ProjectPath/configs/:/api/configs/ \ 35 | $ProjectRegistry:$TAG 36 | fi 37 | 38 | 39 | -------------------------------------------------------------------------------- /pkg/errors/err_test.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "go.uber.org/zap" 8 | ) 9 | 10 | func TestErr(t *testing.T) { 11 | logger, _ := zap.NewProduction() 12 | 13 | logger.Info("errorf", zap.Error(Errorf("%s %d", "127.0.0.1", 80))) 14 | 15 | err := New("a dummy err") 16 | logger.Info("new", zap.Error(err)) 17 | 18 | err = Wrap(err, "ping timeout err") 19 | logger.Info("wrap", zap.Error(err)) 20 | 21 | err = Wrapf(err, "ip: %s port: %d", "localhost", 80) 22 | logger.Info("wrapf", zap.Error(err)) 23 | 24 | err = WithStack(err) 25 | logger.Info("withstack", zap.Error(err)) 26 | 27 | logger.Info("wrap std", zap.Error(Wrap(errors.New("std err"), "some err occurs"))) 28 | 29 | logger.Info("wrapf std", zap.Error(Wrapf(errors.New("std err"), "ip: %s port: %d", "localhost", 80))) 30 | 31 | logger.Info("withstack std", zap.Error(WithStack(errors.New("std err")))) 32 | 33 | t.Logf("%+v", New("a dummy error")) 34 | } 35 | -------------------------------------------------------------------------------- /cmd/old_gen/gorm_gen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "os" 7 | "strings" 8 | 9 | "github.com/haierkeys/custom-image-gateway/cmd/old_gen/gorm_gen/pkg" 10 | ) 11 | 12 | var ( 13 | input string 14 | structs []string 15 | prefix string 16 | ) 17 | 18 | func init() { 19 | flagStructs := flag.String("structs", "", "[Required] The name of schema structs to generate structs for, comma seperated\n") 20 | flagInput := flag.String("input", "", "[Required] The name of the input file dir\n") 21 | flagpre := flag.String("pre", "", "[Required] db_driver.TablePrefix\n") 22 | flag.Parse() 23 | 24 | if *flagStructs == "" || *flagInput == "" { 25 | flag.Usage() 26 | os.Exit(1) 27 | } 28 | 29 | structs = strings.Split(*flagStructs, ",") 30 | input = *flagInput 31 | prefix = *flagpre 32 | } 33 | 34 | func main() { 35 | gen := pkg.NewGenerator(input) 36 | p := pkg.NewParser(input) 37 | if err := gen.ParserAST(p, structs, prefix).Generate().Format().Flush(); err != nil { 38 | log.Fatalln(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /internal/middleware/auth_token.go: -------------------------------------------------------------------------------- 1 | /** 2 | @author: haierkeys 3 | @since: 2022/9/14 4 | @desc: 5 | **/ 6 | 7 | package middleware 8 | 9 | import ( 10 | "github.com/haierkeys/custom-image-gateway/global" 11 | "github.com/haierkeys/custom-image-gateway/pkg/app" 12 | "github.com/haierkeys/custom-image-gateway/pkg/code" 13 | 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | func AuthToken() gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | 20 | if global.Config.Security.AuthToken == "" { 21 | c.Next() 22 | } 23 | 24 | response := app.NewResponse(c) 25 | 26 | var token string 27 | 28 | if s, exist := c.GetQuery("authorization"); exist { 29 | token = s 30 | } else if s, exist = c.GetQuery("Authorization"); exist { 31 | token = s 32 | } else if s = c.GetHeader("authorization"); len(s) != 0 { 33 | token = s 34 | } else if s = c.GetHeader("Authorization"); len(s) != 0 { 35 | token = s 36 | } 37 | 38 | if token != global.Config.Security.AuthToken { 39 | response.ToResponse(code.ErrorInvalidAuthToken) 40 | c.Abort() 41 | } 42 | c.Next() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/limiter/method_limiter.go: -------------------------------------------------------------------------------- 1 | package limiter 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/juju/ratelimit" 8 | ) 9 | 10 | type MethodLimiter struct { 11 | *Limiter 12 | } 13 | 14 | func NewMethodLimiter() Face { 15 | l := &Limiter{limiterBuckets: make(map[string]*ratelimit.Bucket)} 16 | return MethodLimiter{ 17 | Limiter: l, 18 | } 19 | } 20 | 21 | func (l MethodLimiter) Key(c *gin.Context) string { 22 | uri := c.Request.RequestURI 23 | index := strings.Index(uri, "?") 24 | if index == -1 { 25 | return uri 26 | } 27 | 28 | return uri[:index] 29 | } 30 | 31 | func (l MethodLimiter) GetBucket(key string) (*ratelimit.Bucket, bool) { 32 | bucket, ok := l.limiterBuckets[key] 33 | return bucket, ok 34 | } 35 | 36 | func (l MethodLimiter) AddBuckets(rules ...BucketRule) Face { 37 | for _, rule := range rules { 38 | if _, ok := l.limiterBuckets[rule.Key]; !ok { 39 | bucket := ratelimit.NewBucketWithQuantum( 40 | rule.FillInterval, 41 | rule.Capacity, 42 | rule.Quantum, 43 | ) 44 | l.limiterBuckets[rule.Key] = bucket 45 | } 46 | } 47 | 48 | return l 49 | } 50 | -------------------------------------------------------------------------------- /cmd/old_gen/db_gen/db_driver/mysql.go: -------------------------------------------------------------------------------- 1 | package db_driver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | "gorm.io/driver/mysql" 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/schema" 10 | ) 11 | 12 | var _ Repo = (*mysqlRepo)(nil) 13 | 14 | type mysqlRepo struct { 15 | DbConn *gorm.DB 16 | } 17 | 18 | func (d *mysqlRepo) i() {} 19 | 20 | func (d *mysqlRepo) GetDb() *gorm.DB { 21 | return d.DbConn 22 | } 23 | 24 | func (d *mysqlRepo) DbClose() error { 25 | sqlDB, err := d.DbConn.DB() 26 | if err != nil { 27 | return err 28 | } 29 | return sqlDB.Close() 30 | } 31 | 32 | func (d *mysqlRepo) DbType() string { 33 | return "mysql" 34 | } 35 | 36 | func NewMysql(Dsn string) (Repo, error) { 37 | 38 | db, err := gorm.Open(mysql.Open(Dsn), &gorm.Config{ 39 | NamingStrategy: schema.NamingStrategy{ 40 | SingularTable: true, 41 | }, 42 | // Logger: logger.Default.LogMode(logger.Info), // 日志配置 43 | }) 44 | 45 | if err != nil { 46 | return nil, errors.Wrap(err, fmt.Sprintf("[db_driver connection failed] %s", Dsn)) 47 | } 48 | 49 | db.Set("gorm:table_options", "CHARSET=utf8mb4") 50 | 51 | return &mysqlRepo{ 52 | DbConn: db, 53 | }, nil 54 | } 55 | -------------------------------------------------------------------------------- /pkg/app/dateime.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type Datetime time.Time 12 | 13 | func (t *Datetime) UnmarshalJSON(data []byte) error { 14 | if string(data) == "null" { 15 | return nil 16 | } 17 | var err error 18 | //前端接收的时间字符串 19 | str := string(data) 20 | //去除接收的str收尾多余的" 21 | timeStr := strings.Trim(str, "\"") 22 | t1, err := time.Parse("2006-01-02 15:04:05", timeStr) 23 | *t = Datetime(t1) 24 | return err 25 | } 26 | 27 | func (t Datetime) MarshalJSON() ([]byte, error) { 28 | formatted := fmt.Sprintf("\"%v\"", time.Time(t).Format("2006-01-02 15:04:05")) 29 | return []byte(formatted), nil 30 | } 31 | 32 | func (t Datetime) Value() (driver.Value, error) { 33 | // MyTime 转换成 time.Time 类型 34 | tTime := time.Time(t) 35 | return tTime.Format("2006-01-02 15:04:05"), nil 36 | } 37 | 38 | func (t *Datetime) Scan(v interface{}) error { 39 | switch vt := v.(type) { 40 | case time.Time: 41 | // 字符串转成 time.Time 类型 42 | *t = Datetime(vt) 43 | default: 44 | return errors.New("类型处理错误") 45 | } 46 | return nil 47 | } 48 | 49 | func (t *Datetime) String() string { 50 | return fmt.Sprintf("hhh:%s", time.Time(*t).String()) 51 | } 52 | -------------------------------------------------------------------------------- /pkg/storage/aliyun_oss/operation.go: -------------------------------------------------------------------------------- 1 | package aliyun_oss 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 8 | ) 9 | 10 | func (p *OSS) GetBucket(bucketName string) error { 11 | // Get bucket 12 | if len(bucketName) <= 0 { 13 | bucketName = p.Config.BucketName 14 | } 15 | var err error 16 | p.Bucket, err = p.Client.Bucket(bucketName) 17 | return err 18 | } 19 | 20 | func (p *OSS) SendFile(fileKey string, file io.Reader, itype string) (string, error) { 21 | if p.Bucket == nil { 22 | err := p.GetBucket("") 23 | if err != nil { 24 | return "", err 25 | } 26 | } 27 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 28 | err := p.Bucket.PutObject(fileKey, file) 29 | if err != nil { 30 | return "", err 31 | } 32 | return fileKey, nil 33 | } 34 | 35 | func (p *OSS) SendContent(fileKey string, content []byte) (string, error) { 36 | 37 | if p.Bucket == nil { 38 | err := p.GetBucket("") 39 | if err != nil { 40 | return "", err 41 | } 42 | } 43 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 44 | err := p.Bucket.PutObject(fileKey, bytes.NewReader(content)) 45 | if err != nil { 46 | return "", err 47 | } 48 | return fileKey, nil 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | workflow_dispatch: 8 | 9 | env: 10 | PLUGIN_NAME: custom-image-gateway 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Create Release 20 | id: create_release 21 | uses: actions/create-release@v1 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | with: 25 | tag_name: ${{ github.ref }} 26 | release_name: ${{ github.ref }} 27 | draft: false 28 | prerelease: false 29 | - name: echo create_release 30 | run: echo ${{ steps.create_release.outputs.upload_url }} 31 | - name: Set Time 32 | run: echo "TIME=`echo $(TZ='Asia/Shanghai' date +'%FT%T%z')`" >> $GITHUB_ENV 33 | - name: Get Tag Version 34 | run: echo "TAG_VERSION=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV 35 | - name: Get Tag Version 36 | run: echo "LDFLAGS=-ldflags \"-X global.GitTag=${{ env.TAG_VERSION }} -X global.BuildTime=${{ env.TIME }}\"" >> $GITHUB_ENV 37 | 38 | -------------------------------------------------------------------------------- /frontend/site.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /scripts/gormgen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | shellExit() 3 | { 4 | if [ $1 -eq 1 ]; then 5 | printf "\nfailed!!!\n\n" 6 | exit 1 7 | fi 8 | } 9 | 10 | 11 | printf "\nRegenerating file\n\nstart build markdown and model file" 12 | 13 | 14 | # docker run --name mariadb -p 3306:3306 -e MYSQL_ROOT_PASSWORD=xxxx -v /data/mariadb/data:/var/lib/mysql -d mariadb:10.1.21 15 | #go run -v ./cmd/db_gen/main.go -type sqlite -dsn storage/database/db.db -name main -table pre -prefix pre_ -savedir test 16 | #go run -v ./cmd/db_gen/main.go -type mysql -dsn "root:root@tcp(192.168.138.190:3306)/main?charset=utf8mb4&parseTime=true&loc=Local" -name main -table pre -prefix pre_ -savedir test 17 | 18 | 19 | # scripts/gormgen.sh sqlite storage/database/db.db main pre_ pre_ test 20 | 21 | time go run -v ./cmd/db_gen/main.go -type "$1" -dsn "$2" -name $3 -table "$4" -prefix "$5" -savedir "$6" 22 | 23 | printf "\nBuild markdown and model file succeed!\n" 24 | 25 | shellExit $? 26 | 27 | 28 | 29 | printf "\ncreate curd code : \n" 30 | time go build -o gormgen ./cmd/gorm_gen/main.go 31 | shellExit $? 32 | 33 | mv gormgen $GOPATH/bin/ 34 | shellExit $? 35 | 36 | go generate ./... 37 | shellExit $? 38 | 39 | printf "\nFormatting code\n\n" 40 | time go run -v ./cmd/mfmt/main.go 41 | shellExit $? 42 | 43 | printf "\nDone.\n\n" 44 | -------------------------------------------------------------------------------- /internal/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func Cors() gin.HandlerFunc { 11 | 12 | return func(c *gin.Context) { 13 | 14 | var domain string 15 | if s, exist := c.GetQuery("domain"); exist { 16 | domain = s 17 | } else { 18 | domain = c.GetHeader("domain") 19 | } 20 | 21 | if domain != "" && !strings.HasPrefix(domain, "http"+"://") { 22 | xForwardedProto := c.GetHeader("X-Forwarded-Proto") 23 | if xForwardedProto == "https" { 24 | domain = "https" + "://" + domain 25 | } else { 26 | domain = "http" + "://" + domain 27 | } 28 | } 29 | 30 | c.Header("Access-Control-Allow-Credentials", "true") 31 | c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS") 32 | c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, AccessToken, X-CSRF-Token, Authorization, Debug, Domain, Token, Lang, Content-Type, Content-Length, Accept") 33 | 34 | if domain != "" { 35 | c.Header("Access-Control-Allow-Origin", domain) 36 | } else { 37 | c.Header("Access-Control-Allow-Origin", "*") 38 | } 39 | 40 | // 允许放行OPTIONS请求 41 | if c.Request.Method == "OPTIONS" { 42 | c.AbortWithStatus(http.StatusNoContent) 43 | } 44 | c.Next() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cmd/old_gen/db_gen/db_driver/sqlite.go: -------------------------------------------------------------------------------- 1 | package db_driver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | "gorm.io/driver/sqlite" 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/logger" 10 | "gorm.io/gorm/schema" 11 | ) 12 | 13 | var _ Repo = (*sqliteRepo)(nil) 14 | 15 | type sqliteRepo struct { 16 | DbConn *gorm.DB 17 | } 18 | 19 | func (d *sqliteRepo) i() {} 20 | 21 | func (d *sqliteRepo) GetDb() *gorm.DB { 22 | return d.DbConn 23 | } 24 | 25 | func (d *sqliteRepo) DbClose() error { 26 | sqlDB, err := d.DbConn.DB() 27 | if err != nil { 28 | return err 29 | } 30 | return sqlDB.Close() 31 | } 32 | 33 | func (d *sqliteRepo) DbNames() string { 34 | return "PRAGMA database_list" 35 | } 36 | 37 | func (d *sqliteRepo) DbType() string { 38 | return "sqlite" 39 | } 40 | 41 | func NewSqlite(Dsn string) (Repo, error) { 42 | 43 | db, err := gorm.Open(sqlite.Open(Dsn), &gorm.Config{ 44 | Logger: logger.Default.LogMode(logger.Warn), 45 | NamingStrategy: schema.NamingStrategy{ 46 | SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` 47 | }, 48 | }) 49 | 50 | if err != nil { 51 | return nil, errors.Wrap(err, fmt.Sprintf("[db_driver connection failed] %s", Dsn)) 52 | } 53 | 54 | db.Set("gorm:table_options", "CHARSET=utf8mb4") 55 | 56 | return &sqliteRepo{ 57 | DbConn: db, 58 | }, nil 59 | } 60 | -------------------------------------------------------------------------------- /internal/middleware/user_auth_token.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/pkg/app" 5 | "github.com/haierkeys/custom-image-gateway/pkg/code" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func UserAuthToken() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | var token string 13 | response := app.NewResponse(c) 14 | 15 | if s, exist := c.GetQuery("authorization"); exist { 16 | token = s 17 | } else if s, exist = c.GetQuery("Authorization"); exist { 18 | token = s 19 | } else if s = c.GetHeader("authorization"); len(s) != 0 { 20 | token = s 21 | } else if s = c.GetHeader("Authorization"); len(s) != 0 { 22 | token = s 23 | } else if s, exist := c.GetQuery("token"); exist { 24 | token = s 25 | } else if s, exist = c.GetQuery("Token"); exist { 26 | token = s 27 | } else if s = c.GetHeader("token"); len(s) != 0 { 28 | token = s 29 | } else if s = c.GetHeader("Token"); len(s) != 0 { 30 | token = s 31 | } 32 | 33 | if token == "" { 34 | response.ToResponse(code.ErrorNotUserAuthToken) 35 | c.Abort() 36 | } else { 37 | if user, err := app.ParseToken(token); err != nil { 38 | response.ToResponse(code.ErrorInvalidUserAuthToken) 39 | c.Abort() 40 | } else { 41 | c.Set("user_token", user) 42 | } 43 | } 44 | 45 | c.Next() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /pkg/storage/local_fs/local.go: -------------------------------------------------------------------------------- 1 | package local_fs 2 | 3 | type Config struct { 4 | IsEnabled bool `yaml:"is-enable"` 5 | HttpfsIsEnable bool `yaml:"httpfs-is-enable"` 6 | IsUserEnabled bool `yaml:"is-user-enable"` 7 | SavePath string `yaml:"save-path"` 8 | } 9 | 10 | type LocalFS struct { 11 | IsCheckSave bool 12 | Config *Config 13 | } 14 | 15 | func NewClient(cf map[string]any) (*LocalFS, error) { 16 | 17 | var IsEnabled bool 18 | switch t := cf["IsEnabled"].(type) { 19 | case int64: 20 | if t == 0 { 21 | IsEnabled = false 22 | } else { 23 | IsEnabled = true 24 | } 25 | case bool: 26 | IsEnabled = t 27 | } 28 | 29 | var IsUserEnabled bool 30 | switch t := cf["IsUserEnabled"].(type) { 31 | case int64: 32 | if t == 0 { 33 | IsUserEnabled = false 34 | } else { 35 | IsUserEnabled = true 36 | } 37 | case bool: 38 | IsUserEnabled = t 39 | } 40 | 41 | var HttpfsIsEnable bool 42 | switch t := cf["HttpfsIsEnable"].(type) { 43 | case int64: 44 | if t == 0 { 45 | HttpfsIsEnable = false 46 | } else { 47 | HttpfsIsEnable = true 48 | } 49 | case bool: 50 | HttpfsIsEnable = t 51 | } 52 | 53 | conf := &Config{ 54 | IsEnabled: IsEnabled, 55 | IsUserEnabled: IsUserEnabled, 56 | HttpfsIsEnable: HttpfsIsEnable, 57 | SavePath: cf["SavePath"].(string), 58 | } 59 | return &LocalFS{ 60 | Config: conf, 61 | }, nil 62 | } 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- Composer ----------------------------------------- 2 | /composer.phar 3 | /composer.lock 4 | 5 | # -- Editores ----------------------------------------- 6 | # vim 7 | .*.sw[a-z] 8 | *.un~ 9 | Session.vim 10 | .netrwhist 11 | 12 | # vscode 13 | /.vscode 14 | .vscode 15 | 16 | # eclipse 17 | *.pydevproject 18 | .project 19 | .metadata 20 | tmp/** 21 | tmp/**/* 22 | *.tmp 23 | *.bak 24 | *.swp 25 | *~.nib 26 | local.properties 27 | .classpath 28 | .settings/ 29 | .loadpath 30 | .externalToolBuilders/ 31 | *.launch 32 | .buildpath 33 | 34 | # phpstorm 35 | .idea 36 | 37 | # textmate 38 | *.tmproj 39 | *.tmproject 40 | tmtags 41 | 42 | # sublimetext 43 | /*.sublime-project 44 | *.sublime-workspace 45 | 46 | # netbeans 47 | nbproject/private/ 48 | build/ 49 | nbbuild/ 50 | dist/ 51 | nbdist/ 52 | nbactions.xml 53 | nb-configuration.xml 54 | 55 | # -- Sistemas Operativos ------------------------------ 56 | # Windows 57 | Thumbs.db 58 | ehthumbs.db 59 | Desktop.ini 60 | $RECYCLE.BIN/ 61 | 62 | # Linux 63 | !.gitignore 64 | !.htaccess 65 | !.env 66 | *~ 67 | 68 | # Mac OS X 69 | .DS_Store 70 | .AppleDouble 71 | .LSOverride 72 | Icon 73 | ._* 74 | .Spotlight-V100 75 | .Trashes 76 | 77 | # -- Project ----------------------------------------- 78 | /build 79 | /storage/logs 80 | /storage/uploads 81 | /storage/ 82 | /config/config-dev.yaml -------------------------------------------------------------------------------- /internal/middleware/tracer.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | // 4 | //import "C" 5 | //import ( 6 | // "github.com/gin-gonic/gin" 7 | //) 8 | // 9 | //func Tracing() func(c *gin.Context) { 10 | // return func(c *gin.Context) { 11 | // //var newCtx context.Context 12 | // //var span opentracing.Span 13 | // //spanCtx, err := opentracing.GlobalTracer().Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(c.Request.Header)) 14 | // //if err != nil { 15 | // // span, newCtx = opentracing.StartSpanFromContextWithTracer(c.Request.Context(), global.Tracer, c.Request.URL.Path) 16 | // //} else { 17 | // // span, newCtx = opentracing.StartSpanFromContextWithTracer( 18 | // // c.Request.Context(), 19 | // // global.Tracer, 20 | // // c.Request.URL.Path, 21 | // // opentracing.ChildOf(spanCtx), 22 | // // opentracing.Tag{Key: string(ext.Component), Value: "HTTP"}, 23 | // // ) 24 | // //} 25 | // //defer span.Finish() 26 | // // 27 | // //var traceID string 28 | // //var spanID string 29 | // //var spanContext = span.Context() 30 | // //switch spanContext.(type) { 31 | // //case jaeger.SpanContext: 32 | // // jaegerContext := spanContext.(jaeger.SpanContext) 33 | // // traceID = jaegerContext.TraceID().String() 34 | // // spanID = jaegerContext.SpanID().String() 35 | // //} 36 | // //c.Set("X-Trace-ID", traceID) 37 | // //c.Set("X-Span-ID", spanID) 38 | // //c.Request = c.Request.WithContext(newCtx) 39 | // //c.Next() 40 | // } 41 | //} 42 | -------------------------------------------------------------------------------- /pkg/gin_tools/json.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Manu Martinez-Almeida. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package gin_tools 6 | 7 | import ( 8 | "encoding/json" 9 | "errors" 10 | "io" 11 | "net/http" 12 | ) 13 | 14 | // EnableDecoderUseNumber is used to call the UseNumber method on the JSON 15 | // Decoder instance. UseNumber causes the Decoder to unmarshal a number into an 16 | // interface{} as a Number instead of as a float64. 17 | var EnableDecoderUseNumber = false 18 | 19 | // EnableDecoderDisallowUnknownFields is used to call the DisallowUnknownFields method 20 | // on the JSON Decoder instance. DisallowUnknownFields causes the Decoder to 21 | // return an error when the destination is a struct and the input contains object 22 | // keys which do not match any non-ignored, exported fields in the destination. 23 | var EnableDecoderDisallowUnknownFields = false 24 | 25 | type jsonBinding struct{} 26 | 27 | func (jsonBinding) Name() string { 28 | return "json" 29 | } 30 | 31 | func (jsonBinding) Parse(req *http.Request, params *map[string]string) error { 32 | if req == nil || req.Body == nil { 33 | return errors.New("invalid request") 34 | } 35 | 36 | err := decodeJSON(req.Body, params) 37 | return err 38 | } 39 | 40 | func decodeJSON(r io.Reader, obj any) error { 41 | decoder := json.NewDecoder(r) 42 | if err := decoder.Decode(obj); err != nil { 43 | //return err 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /internal/model/user.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import "github.com/haierkeys/custom-image-gateway/pkg/timex" 8 | 9 | const TableNameUser = "user" 10 | 11 | // User mapped from table 12 | type User struct { 13 | UID int64 `gorm:"column:uid;primaryKey" json:"uid" form:"uid"` 14 | Email string `gorm:"column:email;uniqueIndex:idx_user_email,priority:1" json:"email" form:"email"` 15 | Username string `gorm:"column:username" json:"username" form:"username"` 16 | Password string `gorm:"column:password" json:"password" form:"password"` 17 | Salt string `gorm:"column:salt" json:"salt" form:"salt"` 18 | Token string `gorm:"column:token;not null" json:"token" form:"token"` 19 | Avatar string `gorm:"column:avatar;not null" json:"avatar" form:"avatar"` 20 | IsDeleted int64 `gorm:"column:is_deleted;not null" json:"isDeleted" form:"isDeleted"` 21 | UpdatedAt timex.Time `gorm:"column:updated_at;type:datetime;autoUpdateTime" json:"updatedAt" form:"updatedAt"` 22 | CreatedAt timex.Time `gorm:"column:created_at;type:datetime;autoCreateTime" json:"createdAt" form:"createdAt"` 23 | DeletedAt timex.Time `gorm:"column:deleted_at;type:datetime;default:NULL" json:"deletedAt" form:"deletedAt"` 24 | } 25 | 26 | // TableName User's table name 27 | func (*User) TableName() string { 28 | return TableNameUser 29 | } 30 | -------------------------------------------------------------------------------- /cmd/model_gen/gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // gorm gen configure 4 | import ( 5 | "os" 6 | "reflect" 7 | "strings" 8 | 9 | "github.com/haierkeys/custom-image-gateway/internal/query" 10 | "gorm.io/gen" 11 | ) 12 | 13 | func main() { 14 | g := gen.NewGenerator(gen.Config{ 15 | // 默认会在 OutPath 目录生成CRUD代码,并且同目录下生成 model 包 16 | // 所以OutPath最终package不能设置为model,在有数据库表同步的情况下会产生冲突 17 | // 若一定要使用可以通过ModelPkgPath单独指定model package的名称 18 | OutPath: "./internal/query", 19 | /* ModelPkgPath: "dal/model"*/ 20 | // gen.WithoutContext:禁用WithContext模式 21 | // gen.WithDefaultQuery:生成一个全局Query对象Q 22 | // gen.WithQueryInterface:生成Query接口 23 | Mode: gen.WithQueryInterface, 24 | WithUnitTest: false, 25 | FieldWithTypeTag: false, 26 | }) 27 | v := reflect.ValueOf(query.Query{}) 28 | goContent := ` 29 | package model 30 | 31 | import ( 32 | "sync" 33 | 34 | "gorm.io/gorm" 35 | ) 36 | 37 | var once sync.Once 38 | 39 | func AutoMigrate(db *gorm.DB, key string) { 40 | switch key { 41 | ` 42 | goContentFunc := ` 43 | case "{NAME}": 44 | once.Do(func() { 45 | db.AutoMigrate({NAME}{}) 46 | }) 47 | ` 48 | 49 | if v.Kind() == reflect.Struct { 50 | t := v.Type() 51 | for i := 0; i < v.NumField(); i++ { 52 | field := t.Field(i) 53 | if field.Name == "db" { 54 | continue 55 | } 56 | goContent += strings.ReplaceAll(goContentFunc, "{NAME}", field.Name) 57 | //goContentHeader += fmt.Sprintf("type %s = %s\n", field.Name, field.Type.Name()) 58 | } 59 | goContent += "\t}\n}" 60 | 61 | _ = os.WriteFile(g.OutPath[0:len(g.OutPath)-6]+"/model/model.go", []byte(goContent), os.ModePerm) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/gin_tools/form.go: -------------------------------------------------------------------------------- 1 | // // Copyright 2014 Manu Martinez-Almeida. All rights reserved. 2 | // // Use of this source code is governed by a MIT style 3 | // // license that can be found in the LICENSE file. 4 | package gin_tools 5 | 6 | // 7 | //import ( 8 | // "errors" 9 | // "net/httpclient" 10 | //) 11 | // 12 | //const defaultMemory = 32 << 20 13 | // 14 | //type formBinding struct{} 15 | //type formMultipartBinding struct{} 16 | // 17 | //func (formBinding) Name() string { 18 | // return "form" 19 | //} 20 | // 21 | //func (formBinding) Parse(req *httpclient.Request, params *map[string]string) error { 22 | // if err := req.ParseForm(); err != nil { 23 | // return err 24 | // } 25 | // if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, httpclient.ErrNotMultipart) { 26 | // return err 27 | // } 28 | // 29 | // if err := req.ParseForm(); err != nil { 30 | // return err 31 | // } 32 | // if err := req.ParseMultipartForm(defaultMemory); err != nil { 33 | // if err != httpclient.ErrNotMultipart { 34 | // return err 35 | // } 36 | // } 37 | // postMap := *params 38 | // for k, v := range req.PostForm { 39 | // if len(v) > 1 { 40 | // postMap[k] = v 41 | // } else if len(v) == 1 { 42 | // postMap[k] = v[0] 43 | // } 44 | // } 45 | // //if err := mapForm(obj, req.Form); err != nil { 46 | // // return nil, err 47 | // //} 48 | // return nil 49 | //} 50 | // 51 | //func (formMultipartBinding) Name() string { 52 | // return "multipart/form-data" 53 | //} 54 | // 55 | //func (formMultipartBinding) Parse(req *httpclient.Request, params *map[string]string) error { 56 | // if err := req.ParseMultipartForm(defaultMemory); err != nil { 57 | // return err 58 | // } 59 | // //if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil { 60 | // // return nil, err 61 | // //} 62 | // 63 | // return nil 64 | // 65 | //} 66 | -------------------------------------------------------------------------------- /pkg/validator/custom_validator.go: -------------------------------------------------------------------------------- 1 | package validator 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | 7 | "github.com/haierkeys/custom-image-gateway/pkg/timex" 8 | 9 | "github.com/gin-gonic/gin/binding" 10 | "github.com/go-playground/validator/v10" 11 | ) 12 | 13 | type CustomValidator struct { 14 | Once sync.Once 15 | Validate *validator.Validate 16 | } 17 | 18 | func NewCustomValidator() *CustomValidator { 19 | return &CustomValidator{} 20 | } 21 | 22 | func (v *CustomValidator) ValidateStruct(obj interface{}) error { 23 | if kindOfData(obj) == reflect.Struct { 24 | v.lazyinit() 25 | if err := v.Validate.Struct(obj); err != nil { 26 | return err 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | func (v *CustomValidator) Engine() interface{} { 34 | v.lazyinit() 35 | return v.Validate 36 | } 37 | 38 | func (v *CustomValidator) lazyinit() { 39 | v.Once.Do(func() { 40 | v.Validate = validator.New() 41 | v.Validate.SetTagName("binding") 42 | }) 43 | } 44 | 45 | func kindOfData(data interface{}) reflect.Kind { 46 | value := reflect.ValueOf(data) 47 | valueType := value.Kind() 48 | 49 | if valueType == reflect.Ptr { 50 | valueType = value.Elem().Kind() 51 | } 52 | return valueType 53 | } 54 | 55 | func RegisterCustom() { 56 | if v, ok := binding.Validator.Engine().(*validator.Validate); ok { 57 | // 注册 model.LocalTime 类型的自定义校验规则 58 | v.RegisterCustomTypeFunc(ValidateJSONDateType, timex.Time{}) 59 | } 60 | } 61 | 62 | func ValidateJSONDateType(field reflect.Value) interface{} { 63 | if field.Type() == reflect.TypeOf(timex.Time{}) { 64 | timeStr := field.Interface().(timex.Time).String() 65 | // 0001-01-01 00:00:00 是 go 中 time.Time 类型的空值 66 | // 这里返回 Nil 则会被 validator 判定为空值,而无法通过 `binding:"required"` 规则 67 | if timeStr == "0001-01-01 00:00:00" { 68 | return nil 69 | } 70 | return timeStr 71 | } 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/routers/pprof.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "net/http" 5 | "net/http/pprof" 6 | 7 | "github.com/haierkeys/custom-image-gateway/global" 8 | "github.com/haierkeys/custom-image-gateway/internal/middleware" 9 | apiRouter "github.com/haierkeys/custom-image-gateway/internal/routers/api_router" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/prometheus/client_golang/prometheus/promhttp" 13 | ) 14 | 15 | const ( 16 | // DefaultPrefix url prefix of pprof 17 | DefaultPrefix = "/debug/pprof" 18 | ) 19 | 20 | func NewPrivateRouter() *gin.Engine { 21 | 22 | r := gin.New() 23 | 24 | if global.Config.Server.RunMode == "debug" { 25 | r.Use(gin.Recovery()) 26 | } else { 27 | r.Use(middleware.Recovery()) 28 | } 29 | 30 | // prom监控 31 | r.GET("/debug/vars", apiRouter.Expvar) 32 | r.GET("metrics", gin.WrapH(promhttp.Handler())) 33 | 34 | if global.Config.Server.RunMode == "debug" { 35 | p := r.Group("pprof") 36 | { 37 | p.GET("/", pprofHandler(pprof.Index)) 38 | p.GET("/cmdline", pprofHandler(pprof.Cmdline)) 39 | p.GET("/profile", pprofHandler(pprof.Profile)) 40 | p.POST("/symbol", pprofHandler(pprof.Symbol)) 41 | p.GET("/symbol", pprofHandler(pprof.Symbol)) 42 | p.GET("/trace", pprofHandler(pprof.Trace)) 43 | p.GET("/allocs", pprofHandler(pprof.Handler("allocs").ServeHTTP)) 44 | p.GET("/block", pprofHandler(pprof.Handler("block").ServeHTTP)) 45 | p.GET("/goroutine", pprofHandler(pprof.Handler("goroutine").ServeHTTP)) 46 | p.GET("/heap", pprofHandler(pprof.Handler("heap").ServeHTTP)) 47 | p.GET("/mutex", pprofHandler(pprof.Handler("mutex").ServeHTTP)) 48 | p.GET("/threadcreate", pprofHandler(pprof.Handler("threadcreate").ServeHTTP)) 49 | } 50 | } 51 | 52 | return r 53 | } 54 | 55 | func pprofHandler(h http.HandlerFunc) gin.HandlerFunc { 56 | handler := h 57 | return func(c *gin.Context) { 58 | handler.ServeHTTP(c.Writer, c.Request) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/code/lang.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // lang 类型,用来存储英文和中文文本 10 | type lang struct { 11 | en string // 英文 12 | zh string // 中文 13 | } 14 | 15 | // 默认语言为英文 16 | var lng = "zh" 17 | 18 | const FALLBACK_LNG = "zh" 19 | 20 | // getMessage 方法根据传入的语言返回相应的消息 21 | func (l lang) GetMessage() string { 22 | if lng == "" { 23 | lng = FALLBACK_LNG 24 | } 25 | // 获取语言字段 26 | val := reflect.ValueOf(l) 27 | field := val.FieldByName(lng) 28 | // 如果语言字段有效且非空,返回该语言的消息 29 | if field.IsValid() && field.String() != "" { 30 | return field.String() 31 | } 32 | // 如果指定语言无效,返回回退语言的消息 33 | fallbackField := val.FieldByName(FALLBACK_LNG) 34 | if fallbackField.IsValid() && fallbackField.String() != "" { 35 | return fallbackField.String() 36 | } 37 | // 如果回退语言也没有消息,返回默认的错误信息 38 | return fmt.Sprintf("No message available for language: %s", lng) 39 | } 40 | 41 | // getSupportedLanguages 函数返回 lang 类型支持的所有语言 42 | func GetSupportedLanguages() []string { 43 | var languages []string 44 | // 通过反射获取 lang 类型的字段 45 | typ := reflect.TypeOf(lang{}) 46 | // 遍历结构体的字段,获取字段名 47 | for i := 0; i < typ.NumField(); i++ { 48 | field := typ.Field(i) 49 | languages = append(languages, field.Name) 50 | } 51 | return languages 52 | } 53 | 54 | // 设置全局默认语言 55 | func SetGlobalDefaultLang(language string) error { 56 | // 支持的语言列表 57 | supportedLanguages := GetSupportedLanguages() 58 | 59 | // 检查语言是否在支持的语言列表中 60 | isValidLang := false 61 | for _, lang := range supportedLanguages { 62 | if language == lang { 63 | isValidLang = true 64 | break 65 | } 66 | } 67 | // 如果语言有效,设置全局语言 68 | if isValidLang { 69 | lng = language 70 | return nil 71 | } 72 | // 如果语言无效,返回错误并设置为默认语言 73 | lng = FALLBACK_LNG 74 | return errors.New("unsupported language type, set defaulting to " + FALLBACK_LNG) 75 | } 76 | 77 | // 设置全局默认语言 78 | func GetGlobalDefaultLang() string { 79 | return lng 80 | } 81 | -------------------------------------------------------------------------------- /pkg/storage/local_fs/operation.go: -------------------------------------------------------------------------------- 1 | package local_fs 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "os" 8 | "path" 9 | 10 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 11 | ) 12 | 13 | func (p *LocalFS) CheckSave() error { 14 | 15 | savePath := p.getSavePath() 16 | 17 | if fileurl.IsExist(savePath) { 18 | if err := fileurl.CreatePath(savePath, os.ModePerm); err != nil { 19 | return errors.New("failed to create the save-fileurl directory") 20 | } 21 | } 22 | if fileurl.IsPermission(savePath) { 23 | return errors.New("no permission to upload the save fileurl directory") 24 | } 25 | p.IsCheckSave = true 26 | return nil 27 | } 28 | 29 | func (p *LocalFS) getSavePath() string { 30 | return fileurl.PathSuffixCheckAdd(p.Config.SavePath, "/") 31 | } 32 | 33 | // SendFile 上传文件 34 | func (p *LocalFS) SendFile(fileKey string, file io.Reader, itype string) (string, error) { 35 | if !p.IsCheckSave { 36 | if err := p.CheckSave(); err != nil { 37 | return "", err 38 | } 39 | } 40 | 41 | dstFileKey := p.getSavePath() + fileKey 42 | 43 | err := os.MkdirAll(path.Dir(dstFileKey), os.ModePerm) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | out, err := os.Create(dstFileKey) 49 | if err != nil { 50 | return "", err 51 | } 52 | defer out.Close() 53 | 54 | // file.Seek(0, 0) 55 | _, err = io.Copy(out, file) 56 | if err != nil { 57 | return "", err 58 | } else { 59 | return dstFileKey, nil 60 | } 61 | } 62 | 63 | func (p *LocalFS) SendContent(fileKey string, content []byte) (string, error) { 64 | 65 | if !p.IsCheckSave { 66 | if err := p.CheckSave(); err != nil { 67 | return "", err 68 | } 69 | } 70 | 71 | dstFileKey := p.getSavePath() + fileKey 72 | 73 | out, err := os.Create(dstFileKey) 74 | if err != nil { 75 | return "", err 76 | } 77 | defer out.Close() 78 | 79 | _, err = io.Copy(out, bytes.NewReader(content)) 80 | if err != nil { 81 | return "", err 82 | } else { 83 | return dstFileKey, nil 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | # [Air](https://github.com/air-verse/air) 的 TOML 格式配置文件 2 | 3 | # 工作目录 4 | # 可以是 . 或绝对路径,请注意后续的目录必须位于根目录下。 5 | 6 | root = "." 7 | testdata_dir = "testdata" 8 | tmp_dir = "build" 9 | 10 | [build] 11 | # 每次构建前要运行的命令数组 12 | pre_cmd = ["go mod tidy"] 13 | # 纯 shell 命令,你也可以使用 `make`。 14 | cmd = "go build -o ./build/api ./main.go" 15 | # go build -o ./build/api ./main.go 16 | # 每次按下 ^C 后要运行的命令数组 17 | post_cmd = [] 18 | 19 | # 由 `cmd` 生成的可执行文件。 20 | bin = "./build/api" 21 | # 自定义可执行文件,可在运行应用程序时设置环境变量。 22 | full_bin = "" 23 | # 运行二进制文件(bin/full_bin)时添加其他参数。会运行 './tmp/main hello world'。 24 | args_bin = ["run"] 25 | # 监控这些文件扩展名。 26 | include_ext = ["go", "tpl", "tmpl", "html"] 27 | # 忽略这些文件扩展名或目录。 28 | exclude_dir = ["assets", "tmp","storage", "vendor"] 29 | # 如果指定了,则监控这些目录。 30 | include_dir = [] 31 | # 监控这些文件。 32 | include_file = [] 33 | # 忽略这些文件。 34 | exclude_file = [] 35 | # 忽略特定的正则表达式匹配的文件。 36 | exclude_regex = ["_test\\.go"] 37 | # 忽略未更改的文件。 38 | exclude_unchanged = true 39 | # 跟随符号链接的目录 40 | follow_symlink = true 41 | # 此日志文件位于你的 tmp_dir 中。 42 | log = "air.log" 43 | # 使用轮询而不是 fsnotify 来检测文件更改。 44 | poll = true 45 | # 轮询间隔(默认最低间隔为 500ms)。 46 | poll_interval = 500 # 毫秒 47 | # 如果更改过于频繁,不一定需要每次都触发构建。 48 | delay = 1000 # 毫秒 49 | # 当构建出错时停止运行旧的二进制文件。 50 | stop_on_error = true 51 | # 发送终止信号前发送中断信号(Windows 不支持此功能) 52 | send_interrupt = false 53 | # 发送中断信号后的延迟 54 | kill_delay = 500 # 纳秒 55 | # 重新运行二进制文件与否 56 | rerun = false 57 | # 每次执行后的延迟 58 | rerun_delay = 500 59 | 60 | [log] 61 | # 显示日志时间 62 | time = false 63 | # 仅显示主日志(静音监视器、构建器、运行程序日志) 64 | main_only = false 65 | # 使所有由 air 生成的日志静音 66 | silent = false 67 | 68 | [color] 69 | # 自定义每个部分的颜色。如果找不到对应颜色,则使用原始应用日志。 70 | main = "magenta" 71 | watcher = "cyan" 72 | build = "yellow" 73 | runner = "green" 74 | 75 | [misc] 76 | # 退出时删除 tmp 目录 77 | clean_on_exit = false 78 | 79 | [screen] 80 | # 重建时清除屏幕 81 | clear_on_rebuild = true 82 | # 保持滚动 83 | keep_scroll = true 84 | 85 | [proxy] 86 | # 启用浏览器上的实时重载。 87 | enabled = false 88 | proxy_port = 8090 89 | app_port = 8080 90 | -------------------------------------------------------------------------------- /pkg/storage/aliyun_oss/oss.go: -------------------------------------------------------------------------------- 1 | package aliyun_oss 2 | 3 | import ( 4 | oss_sdk "github.com/aliyun/aliyun-oss-go-sdk/oss" 5 | ) 6 | 7 | type Config struct { 8 | IsEnabled bool `yaml:"is-enable"` 9 | IsUserEnabled bool `yaml:"is-user-enable"` 10 | Endpoint string `yaml:"endpoint"` 11 | BucketName string `yaml:"bucket-name"` 12 | AccessKeyID string `yaml:"access-key-id"` 13 | AccessKeySecret string `yaml:"access-key-secret"` 14 | CustomPath string `yaml:"custom-path"` 15 | } 16 | 17 | type OSS struct { 18 | Client *oss_sdk.Client 19 | Bucket *oss_sdk.Bucket 20 | Config *Config 21 | } 22 | 23 | var clients = make(map[string]*OSS) 24 | 25 | func NewClient(cf map[string]any) (*OSS, error) { 26 | 27 | var IsEnabled bool 28 | switch t := cf["IsEnabled"].(type) { 29 | case int64: 30 | if t == 0 { 31 | IsEnabled = false 32 | } else { 33 | IsEnabled = true 34 | } 35 | case bool: 36 | IsEnabled = t 37 | } 38 | 39 | var IsUserEnabled bool 40 | switch t := cf["IsUserEnabled"].(type) { 41 | case int64: 42 | if t == 0 { 43 | IsUserEnabled = false 44 | } else { 45 | IsUserEnabled = true 46 | } 47 | case bool: 48 | IsUserEnabled = t 49 | } 50 | 51 | conf := &Config{ 52 | IsEnabled: IsEnabled, 53 | IsUserEnabled: IsUserEnabled, 54 | Endpoint: cf["Endpoint"].(string), 55 | BucketName: cf["BucketName"].(string), 56 | AccessKeyID: cf["AccessKeyID"].(string), 57 | AccessKeySecret: cf["AccessKeySecret"].(string), 58 | CustomPath: cf["CustomPath"].(string), 59 | } 60 | 61 | var id = conf.AccessKeyID 62 | var endpoint = conf.Endpoint 63 | var accessKeyId = conf.AccessKeyID 64 | var accessKeySecret = conf.AccessKeySecret 65 | 66 | var err error 67 | if clients[id] != nil { 68 | return clients[id], nil 69 | } 70 | // New client 71 | ossClient, err := oss_sdk.New(endpoint, accessKeyId, accessKeySecret) 72 | if err != nil { 73 | return nil, err 74 | } 75 | clients[id] = &OSS{ 76 | Client: ossClient, 77 | Config: conf, 78 | } 79 | return clients[id], nil 80 | } 81 | -------------------------------------------------------------------------------- /pkg/storage/webdav/webdav.go: -------------------------------------------------------------------------------- 1 | // webdav.go 2 | 3 | package webdav 4 | 5 | import ( 6 | "github.com/studio-b12/gowebdav" 7 | ) 8 | 9 | // Config 结构体用于存储 WebDAV 连接信息。 10 | type Config struct { 11 | IsEnabled bool `yaml:"is-enable"` 12 | IsUserEnabled bool `yaml:"is-user-enable"` 13 | Endpoint string `yaml:"endpoint"` 14 | Path string `yaml:"path"` 15 | User string `yaml:"user"` 16 | Password string `yaml:"password"` 17 | CustomPath string `yaml:"custom-path"` 18 | } 19 | 20 | // WebDAV 结构体表示 WebDAV 客户端。 21 | type WebDAV struct { 22 | Client *gowebdav.Client 23 | Config *Config 24 | } 25 | 26 | var clients = make(map[string]*WebDAV) 27 | 28 | // NewClient 创建一个新的 WebDAV 客户端实例。 29 | func NewClient(cf map[string]any) (*WebDAV, error) { 30 | // New client 31 | 32 | var IsEnabled bool 33 | switch t := cf["IsEnabled"].(type) { 34 | case int64: 35 | if t == 0 { 36 | IsEnabled = false 37 | } else { 38 | IsEnabled = true 39 | } 40 | case bool: 41 | IsEnabled = t 42 | } 43 | 44 | var IsUserEnabled bool 45 | switch t := cf["IsUserEnabled"].(type) { 46 | case int64: 47 | if t == 0 { 48 | IsUserEnabled = false 49 | } else { 50 | IsUserEnabled = true 51 | } 52 | case bool: 53 | IsUserEnabled = t 54 | } 55 | 56 | conf := &Config{ 57 | IsEnabled: IsEnabled, 58 | IsUserEnabled: IsUserEnabled, 59 | Endpoint: cf["Endpoint"].(string), 60 | User: cf["User"].(string), 61 | Password: cf["Password"].(string), 62 | CustomPath: cf["CustomPath"].(string), 63 | } 64 | 65 | var endpoint = conf.Endpoint 66 | var path = conf.Path 67 | var user = conf.User 68 | var password = conf.Password 69 | var customPath = conf.CustomPath 70 | 71 | if clients[endpoint+path+user+customPath] != nil { 72 | return clients[endpoint+path+user+customPath], nil 73 | } 74 | 75 | c := gowebdav.NewClient(endpoint, user, password) 76 | c.Connect() 77 | 78 | clients[endpoint+path+user+customPath] = &WebDAV{ 79 | Client: c, 80 | Config: conf, 81 | } 82 | return clients[endpoint+path+user+customPath], nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/convert/json.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "strconv" 7 | "strings" 8 | "unicode" 9 | 10 | "golang.org/x/text/cases" 11 | "golang.org/x/text/language" 12 | ) 13 | 14 | // 驼峰式写法转为下划线写法 15 | func Camel2Case(name string) string { 16 | buffer := NewBuffer() 17 | for i, r := range name { 18 | if unicode.IsUpper(r) { 19 | if i != 0 { 20 | buffer.Append('_') 21 | } 22 | buffer.Append(unicode.ToLower(r)) 23 | } else { 24 | buffer.Append(r) 25 | } 26 | } 27 | return buffer.String() 28 | } 29 | 30 | // 下划线写法转为驼峰写法 31 | func Case2Camel(name string) string { 32 | name = strings.Replace(name, "_", " ", -1) 33 | name = cases.Title(language.Und, cases.NoLower).String(name) 34 | return strings.Replace(name, " ", "", -1) 35 | } 36 | 37 | // 下划线写法转为驼峰写法 38 | func Case2LowerCamel(name string) string { 39 | return Lcfirst(Case2Camel(name)) 40 | } 41 | 42 | // 首字母大写 43 | func Ucfirst(str string) string { 44 | for i, v := range str { 45 | return string(unicode.ToUpper(v)) + str[i+1:] 46 | } 47 | return "" 48 | } 49 | 50 | // 首字母小写 51 | func Lcfirst(str string) string { 52 | for i, v := range str { 53 | return string(unicode.ToLower(v)) + str[i+1:] 54 | } 55 | return "" 56 | } 57 | 58 | // 内嵌bytes.Buffer,支持连写 59 | type Buffer struct { 60 | *bytes.Buffer 61 | } 62 | 63 | func NewBuffer() *Buffer { 64 | return &Buffer{Buffer: new(bytes.Buffer)} 65 | } 66 | 67 | func (b *Buffer) Append(i interface{}) *Buffer { 68 | switch val := i.(type) { 69 | case int: 70 | b.append(strconv.Itoa(val)) 71 | case int64: 72 | b.append(strconv.FormatInt(val, 10)) 73 | case uint: 74 | b.append(strconv.FormatUint(uint64(val), 10)) 75 | case uint64: 76 | b.append(strconv.FormatUint(val, 10)) 77 | case string: 78 | b.append(val) 79 | case []byte: 80 | b.Write(val) 81 | case rune: 82 | b.WriteRune(val) 83 | } 84 | return b 85 | } 86 | 87 | func (b *Buffer) append(s string) *Buffer { 88 | defer func() { 89 | if err := recover(); err != nil { 90 | log.Println("*****内存不够了!******") 91 | } 92 | }() 93 | b.WriteString(s) 94 | return b 95 | } 96 | -------------------------------------------------------------------------------- /pkg/app/form.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/gin-gonic/gin" 8 | ut "github.com/go-playground/universal-translator" 9 | "github.com/go-playground/validator/v10" 10 | ) 11 | 12 | type ValidError struct { 13 | Key string 14 | Message string 15 | } 16 | 17 | type ValidErrors []*ValidError 18 | 19 | func (v *ValidError) Error() string { 20 | return v.Message 21 | } 22 | 23 | func (v *ValidError) Field() string { 24 | return v.Key 25 | } 26 | 27 | func (v *ValidError) Map() map[string]string { 28 | return map[string]string{v.Key: v.Message} 29 | } 30 | 31 | func (v ValidErrors) Error() string { 32 | return strings.Join(v.Errors(), ",") 33 | } 34 | 35 | func (v ValidErrors) Errors() []string { 36 | var errs []string 37 | for _, err := range v { 38 | errs = append(errs, err.Error()) 39 | } 40 | 41 | return errs 42 | } 43 | 44 | func (v ValidErrors) ErrorsToString() string { 45 | var errs []string 46 | for _, err := range v { 47 | errs = append(errs, err.Error()) 48 | } 49 | 50 | return strings.Join(errs, ",") 51 | } 52 | 53 | func (v ValidErrors) Maps() []map[string]string { 54 | var maps []map[string]string 55 | for _, err := range v { 56 | maps = append(maps, err.Map()) 57 | } 58 | 59 | return maps 60 | } 61 | 62 | func (v ValidErrors) MapsToString() string { 63 | maps := v.Maps() 64 | re, _ := json.Marshal(maps) 65 | return string(re) 66 | } 67 | 68 | // BindAndValid 绑定请求参数并进行验证,支持多语言 69 | func BindAndValid(c *gin.Context, obj interface{}) (bool, ValidErrors) { 70 | var errs ValidErrors 71 | 72 | // 使用全局验证器进行验证 73 | if err := c.ShouldBind(obj); err != nil { 74 | // 如果验证失败,检查错误类型 75 | if validationErrors, ok := err.(validator.ValidationErrors); ok { 76 | // 获取翻译器 77 | v := c.Value("trans") 78 | trans := v.(ut.Translator) 79 | 80 | // 遍历验证错误并进行翻译 81 | for _, validationErr := range validationErrors { 82 | translatedMsg := validationErr.Translate(trans) // 翻译错误消息 83 | errs = append(errs, &ValidError{ 84 | Key: validationErr.Field(), 85 | Message: translatedMsg, 86 | }) 87 | } 88 | } 89 | 90 | return false, errs // 返回验证错误 91 | } 92 | 93 | return true, nil // 绑定和验证都成功,返回 true 94 | } 95 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM woahbase/alpine-glibc:latest 2 | MAINTAINER HaierKeys 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | ARG VERSION 6 | ARG BUILD_DATE 7 | ARG GIT_COMMIT 8 | 9 | ARG VERSION=${VERSION} 10 | ARG BUILD_DATE=${BUILD_DATE} 11 | ARG GIT_COMMIT=${GIT_COMMIT} 12 | 13 | 14 | 15 | LABEL name="custom-image-gateway" 16 | LABEL version=${VERSION} 17 | LABEL description="Provide image resizing, cropping, upload/download, and cloud storage features for Obsidian CIAU." 18 | LABEL maintainer="HaierKeys " 19 | 20 | 21 | LABEL org.opencontainers.image.title="Obsidian Image API Gateway" 22 | LABEL org.opencontainers.image.created=${BUILD_DATE} 23 | LABEL org.opencontainers.image.authors="HaierKeys " 24 | LABEL org.opencontainers.image.version=${VERSION} 25 | LABEL org.opencontainers.image.description="Provide image resizing, cropping, upload/download, and cloud storage features for Obsidian CIAU." 26 | LABEL org.opencontainers.image.url="https://github.com/haierkeys/custom-image-gateway" 27 | LABEL org.opencontainers.image.source="https://github.com/haierkeys/custom-image-gateway" 28 | LABEL org.opencontainers.image.documentation="https://raw.githubusercontent.com/haierkeys/custom-image-gateway/refs/heads/main/README.md" 29 | LABEL org.opencontainers.image.revision=${GIT_COMMIT} 30 | LABEL org.opencontainers.image.licenses="Apache-2.0" 31 | LABEL org.opencontainers.image.vendor="HaierKeys" 32 | 33 | 34 | 35 | ENV TZ=Asia/Shanghai 36 | ENV P_NAME=api 37 | ENV P_BIN=image-api 38 | RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories 39 | RUN apk --update add libstdc++ curl ca-certificates bash curl gcompat tzdata && \ 40 | cp /usr/share/zoneinfo/${TZ} /etc/localtime && \ 41 | echo ${TZ} > /etc/timezone && \ 42 | rm -rf /tmp/* /var/cache/apk/* 43 | 44 | EXPOSE 9000 9001 45 | RUN mkdir -p /${P_NAME}/ 46 | VOLUME /${P_NAME}/config 47 | VOLUME /${P_NAME}/storage 48 | COPY ./build/${TARGETOS}_${TARGETARCH}/${P_BIN} /${P_NAME}/ 49 | 50 | # 将脚本复制到容器中 51 | COPY entrypoint.sh /entrypoint.sh 52 | 53 | # 给脚本执行权限 54 | RUN chmod +x /entrypoint.sh 55 | 56 | # 使用 ENTRYPOINT 执行脚本 57 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /internal/middleware/access_log.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "bytes" 5 | "time" 6 | 7 | "github.com/haierkeys/custom-image-gateway/global" 8 | 9 | "github.com/gin-gonic/gin" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | type AccessLogWriter struct { 14 | gin.ResponseWriter 15 | body *bytes.Buffer 16 | } 17 | 18 | func (w AccessLogWriter) Write(p []byte) (int, error) { 19 | if n, err := w.body.Write(p); err != nil { 20 | return n, err 21 | } 22 | return w.ResponseWriter.Write(p) 23 | } 24 | 25 | func AccessLog() gin.HandlerFunc { 26 | return func(c *gin.Context) { 27 | 28 | bodyWriter := &AccessLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} 29 | c.Writer = bodyWriter 30 | 31 | path := c.Request.URL.Path 32 | query := c.Request.URL.RawQuery 33 | 34 | startTime := time.Now() 35 | c.Next() 36 | 37 | timeCost := time.Since(startTime) 38 | statusCode, isExists := c.Get("status_code") 39 | 40 | if isExists { 41 | global.Log().Info(path, 42 | zap.Int("status", c.Writer.Status()), 43 | zap.String("method", c.Request.Method), 44 | zap.String("fileurl", path), 45 | zap.String("query", query), 46 | zap.String("ip", c.ClientIP()), 47 | zap.String("start-time", startTime.Format("2006-01-02 15:04:05")), 48 | zap.String("user-agent", c.Request.UserAgent()), 49 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), 50 | zap.Duration("time-cost", timeCost), 51 | zap.Int("status_code", statusCode.(int)), 52 | zap.String("request", c.Request.PostForm.Encode()), 53 | zap.String("response", bodyWriter.body.String()), 54 | ) 55 | } else { 56 | global.Log().Info(path, 57 | zap.Int("status", c.Writer.Status()), 58 | zap.String("method", c.Request.Method), 59 | zap.String("fileurl", path), 60 | zap.String("query", query), 61 | zap.String("ip", c.ClientIP()), 62 | zap.String("start-time", startTime.Format("2006-01-02 15:04:05")), 63 | zap.String("user-agent", c.Request.UserAgent()), 64 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), 65 | zap.Duration("time-cost", timeCost), 66 | zap.String("request", c.Request.PostForm.Encode()), 67 | zap.String("response", bodyWriter.body.String()), 68 | ) 69 | } 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /pkg/code/code.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type Code struct { 9 | // 状态码 10 | code int 11 | // 状态 12 | status bool 13 | // 错误消息 14 | Lang lang 15 | // 错误消息 16 | msg string 17 | // 数据 18 | data interface{} 19 | // 错误详细信息 20 | details []string 21 | // 是否含有详情 22 | haveDetails bool 23 | } 24 | 25 | var codes = map[int]string{} 26 | var maxcode = 0 27 | 28 | func NewError(code int, l lang) *Code { 29 | if _, ok := codes[code]; ok { 30 | panic(fmt.Sprintf("错误码 %d 已经存在,请更换一个", code)) 31 | } 32 | 33 | codes[code] = l.GetMessage() 34 | 35 | if code > maxcode { 36 | maxcode = code 37 | } 38 | 39 | return &Code{code: code, status: false, Lang: l} 40 | } 41 | 42 | func incr(code int) int { 43 | if code > maxcode { 44 | return code 45 | } else { 46 | return maxcode + 1 47 | } 48 | } 49 | 50 | var sussCodes = map[int]string{} 51 | 52 | func NewSuss(code int, l lang) *Code { 53 | if _, ok := sussCodes[code]; ok { 54 | panic(fmt.Sprintf("成功码 %d 已经存在,请更换一个", code)) 55 | } 56 | sussCodes[code] = l.GetMessage() 57 | if code > maxcode { 58 | maxcode = code 59 | } 60 | 61 | return &Code{code: code, status: true, Lang: l} 62 | } 63 | 64 | func (e *Code) Error() string { 65 | return e.Msg() 66 | } 67 | 68 | func (e *Code) Code() int { 69 | return e.code 70 | } 71 | 72 | func (e *Code) Status() bool { 73 | return e.status 74 | } 75 | 76 | func (e *Code) Msg() string { 77 | return e.Lang.GetMessage() 78 | } 79 | 80 | func (e *Code) Msgf(args []interface{}) string { 81 | return fmt.Sprintf(e.msg, args...) 82 | } 83 | 84 | func (e *Code) Details() []string { 85 | return e.details 86 | } 87 | 88 | func (e *Code) Data() interface{} { 89 | return e.data 90 | } 91 | 92 | func (e *Code) HaveDetails() bool { 93 | return e.haveDetails 94 | } 95 | 96 | func (e *Code) WithData(data interface{}) *Code { 97 | e.data = data 98 | return e 99 | } 100 | 101 | func (e *Code) WithDetails(details ...string) *Code { 102 | e.haveDetails = true 103 | e.details = []string{} 104 | for _, d := range details { 105 | e.details = append(e.details, d) 106 | } 107 | return e 108 | } 109 | 110 | func (e *Code) StatusCode() int { 111 | return http.StatusOK 112 | } 113 | -------------------------------------------------------------------------------- /pkg/errors/err.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "runtime" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func callers() []uintptr { 12 | var pcs [32]uintptr 13 | l := runtime.Callers(3, pcs[:]) 14 | return pcs[:l] 15 | } 16 | 17 | // Error with caller stack information 18 | type Error interface { 19 | error 20 | t() 21 | } 22 | 23 | var _ Error = (*item)(nil) 24 | var _ fmt.Formatter = (*item)(nil) 25 | 26 | type item struct { 27 | msg string 28 | stack []uintptr 29 | } 30 | 31 | func (i *item) Error() string { 32 | return i.msg 33 | } 34 | 35 | func (i *item) t() {} 36 | 37 | // Format used by go.uber.org/zap in Verbose 38 | func (i *item) Format(s fmt.State, verb rune) { 39 | io.WriteString(s, i.msg) 40 | io.WriteString(s, "\n") 41 | 42 | for _, pc := range i.stack { 43 | fmt.Fprintf(s, "%+v\n", errors.Frame(pc)) 44 | } 45 | } 46 | 47 | // New create a new error 48 | func New(msg string) Error { 49 | return &item{msg: msg, stack: callers()} 50 | } 51 | 52 | // Errorf create a new error 53 | func Errorf(format string, args ...interface{}) Error { 54 | return &item{msg: fmt.Sprintf(format, args...), stack: callers()} 55 | } 56 | 57 | // Wrap with some extra message into err 58 | func Wrap(err error, msg string) Error { 59 | if err == nil { 60 | return nil 61 | } 62 | 63 | e, ok := err.(*item) 64 | if !ok { 65 | return &item{msg: fmt.Sprintf("%s; %s", msg, err.Error()), stack: callers()} 66 | } 67 | 68 | e.msg = fmt.Sprintf("%s; %s", msg, e.msg) 69 | return e 70 | } 71 | 72 | // Wrapf with some extra message into err 73 | func Wrapf(err error, format string, args ...interface{}) Error { 74 | if err == nil { 75 | return nil 76 | } 77 | 78 | msg := fmt.Sprintf(format, args...) 79 | 80 | e, ok := err.(*item) 81 | if !ok { 82 | return &item{msg: fmt.Sprintf("%s; %s", msg, err.Error()), stack: callers()} 83 | } 84 | 85 | e.msg = fmt.Sprintf("%s; %s", msg, e.msg) 86 | return e 87 | } 88 | 89 | // WithStack add caller stack information 90 | func WithStack(err error) Error { 91 | if err == nil { 92 | return nil 93 | } 94 | 95 | if e, ok := err.(*item); ok { 96 | return e 97 | } 98 | 99 | return &item{msg: err.Error(), stack: callers()} 100 | } 101 | -------------------------------------------------------------------------------- /internal/model/cloud_config.gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package model 6 | 7 | import "github.com/haierkeys/custom-image-gateway/pkg/timex" 8 | 9 | const TableNameCloudConfig = "cloud_config" 10 | 11 | // CloudConfig mapped from table 12 | type CloudConfig struct { 13 | ID int64 `gorm:"column:id;primaryKey" json:"id" form:"id"` 14 | UID int64 `gorm:"column:uid;not null;index:idx_cloud_config_uid,priority:1" json:"uid" form:"uid"` 15 | Type string `gorm:"column:type" json:"type" form:"type"` 16 | Endpoint string `gorm:"column:endpoint" json:"endpoint" form:"endpoint"` 17 | Region string `gorm:"column:region" json:"region" form:"region"` 18 | AccountID string `gorm:"column:account_id" json:"accountId" form:"accountId"` 19 | BucketName string `gorm:"column:bucket_name" json:"bucketName" form:"bucketName"` 20 | AccessKeyID string `gorm:"column:access_key_id" json:"accessKeyId" form:"accessKeyId"` 21 | AccessKeySecret string `gorm:"column:access_key_secret" json:"accessKeySecret" form:"accessKeySecret"` 22 | CustomPath string `gorm:"column:custom_path" json:"customPath" form:"customPath"` 23 | AccessURLPrefix string `gorm:"column:access_url_prefix" json:"accessUrlPrefix" form:"accessUrlPrefix"` 24 | User string `gorm:"column:user" json:"user" form:"user"` 25 | Password string `gorm:"column:password" json:"password" form:"password"` 26 | IsEnabled int64 `gorm:"column:is_enabled;not null;default:1" json:"isEnabled" form:"isEnabled"` 27 | IsDeleted int64 `gorm:"column:is_deleted;not null" json:"isDeleted" form:"isDeleted"` 28 | CreatedAt timex.Time `gorm:"column:created_at;type:datetime;autoCreateTime" json:"createdAt" form:"createdAt"` 29 | UpdatedAt timex.Time `gorm:"column:updated_at;type:datetime;autoUpdateTime" json:"updatedAt" form:"updatedAt"` 30 | DeletedAt timex.Time `gorm:"column:deleted_at;type:datetime;default:NULL" json:"deletedAt" form:"deletedAt"` 31 | } 32 | 33 | // TableName CloudConfig's table name 34 | func (*CloudConfig) TableName() string { 35 | return TableNameCloudConfig 36 | } 37 | -------------------------------------------------------------------------------- /pkg/storage/aws_s3/operation.go: -------------------------------------------------------------------------------- 1 | package aws_s3 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/service/s3" 14 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | func (p *S3) GetBucket(bucketName string) string { 19 | 20 | // Get bucket 21 | if len(bucketName) <= 0 { 22 | bucketName = p.Config.BucketName 23 | } 24 | 25 | return bucketName 26 | } 27 | 28 | // UploadByFile 上传文件 29 | func (p *S3) SendFile(fileKey string, file io.Reader, itype string) (string, error) { 30 | 31 | ctx := context.Background() 32 | bucket := p.GetBucket("") 33 | 34 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 35 | 36 | // k, _ := h.Open() 37 | 38 | _, err := p.S3Client.PutObject(ctx, &s3.PutObjectInput{ 39 | Bucket: aws.String(bucket), 40 | Key: aws.String(fileKey), 41 | Body: file, 42 | ContentType: aws.String(itype), 43 | }) 44 | 45 | if err != nil { 46 | return "", errors.Wrap(err, "aws_s3") 47 | } 48 | 49 | return fileKey, nil 50 | } 51 | 52 | func (p *S3) SendContent(fileKey string, content []byte) (string, error) { 53 | 54 | ctx := context.Background() 55 | bucket := p.GetBucket("") 56 | 57 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 58 | 59 | input := &s3.PutObjectInput{ 60 | Bucket: aws.String(bucket), 61 | Key: aws.String(fileKey), 62 | Body: bytes.NewReader(content), 63 | ChecksumAlgorithm: types.ChecksumAlgorithmSha256, 64 | } 65 | output, err := p.S3Manager.Upload(ctx, input) 66 | if err != nil { 67 | var noBucket *types.NoSuchBucket 68 | if errors.As(err, &noBucket) { 69 | fmt.Printf("Bucket %s does not exist.\n", bucket) 70 | err = noBucket 71 | } 72 | } else { 73 | err := s3.NewObjectExistsWaiter(p.S3Client).Wait(ctx, &s3.HeadObjectInput{ 74 | Bucket: aws.String(bucket), 75 | Key: aws.String(fileKey), 76 | }, time.Minute) 77 | if err != nil { 78 | fmt.Printf("Failed attempt to wait for object %s to exist in %s.\n", fileKey, bucket) 79 | } else { 80 | _ = *output.Key 81 | } 82 | } 83 | 84 | return fileKey, errors.Wrap(err, "aws_s3") 85 | } 86 | -------------------------------------------------------------------------------- /internal/middleware/recovery.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "runtime/debug" 7 | 8 | "github.com/haierkeys/custom-image-gateway/global" 9 | "github.com/haierkeys/custom-image-gateway/pkg/app" 10 | "github.com/haierkeys/custom-image-gateway/pkg/code" 11 | 12 | "github.com/gin-gonic/gin" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | func Recovery() gin.HandlerFunc { 17 | return func(c *gin.Context) { 18 | path := c.Request.URL.Path 19 | query := c.Request.URL.RawQuery 20 | defer func() { 21 | if err := recover(); err != nil { 22 | var errorMsg string 23 | switch err.(type) { 24 | case string: 25 | errorMsg = err.(string) 26 | case error: 27 | // 记录 error 类型的错误 28 | global.Log().Error("Recovered from panic", 29 | zap.Int("status", c.Writer.Status()), 30 | zap.String("router", path), 31 | zap.String("method", c.Request.Method), 32 | zap.String("query", query), 33 | zap.String("ip", c.ClientIP()), 34 | zap.String("user-agent", c.Request.UserAgent()), 35 | zap.String("request", c.Request.PostForm.Encode()), 36 | zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()), // 记录错误的上下文 37 | zap.Error(err.(error)), // 错误信息 38 | zap.String("stack", string(debug.Stack())), // 错误堆栈 39 | ) 40 | errorMsg = err.(error).Error() 41 | default: 42 | // 如果是其它类型的 panic(如非错误类型的 panic) 43 | global.Log().Error("Recovered from unknown panic", 44 | zap.Int("status", c.Writer.Status()), 45 | zap.String("router", path), 46 | zap.String("method", c.Request.Method), 47 | zap.String("query", query), 48 | zap.String("ip", c.ClientIP()), 49 | zap.String("user-agent", c.Request.UserAgent()), 50 | zap.String("request", c.Request.PostForm.Encode()), 51 | zap.String("panic_value", fmt.Sprintf("%v", err)), // 记录 panic 的值 52 | zap.String("stack", string(debug.Stack())), // 错误堆栈 53 | ) 54 | } 55 | 56 | // 打印错误堆栈到控制台 57 | log.Printf("Recovered from panic: %v", errorMsg) 58 | log.Printf("Stack trace:\n%s", string(debug.Stack())) 59 | 60 | // 返回统一的错误响应 61 | app.NewResponse(c).ToResponse(code.ErrorServerInternal.WithDetails(errorMsg)) 62 | } 63 | }() 64 | 65 | c.Next() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/storage/cloudflare_r2/operation.go: -------------------------------------------------------------------------------- 1 | package cloudflare_r2 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/service/s3" 14 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | func (p *R2) GetBucket(bucketName string) string { 19 | 20 | // Get bucket 21 | if len(bucketName) <= 0 { 22 | bucketName = p.Config.BucketName 23 | } 24 | 25 | return bucketName 26 | } 27 | 28 | // UploadByFile 上传文件 29 | func (p *R2) SendFile(fileKey string, file io.Reader, itype string) (string, error) { 30 | 31 | ctx := context.Background() 32 | bucket := p.GetBucket("") 33 | 34 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 35 | // k, _ := h.Open() 36 | _, err := p.S3Client.PutObject(ctx, &s3.PutObjectInput{ 37 | Bucket: aws.String(bucket), 38 | Key: aws.String(fileKey), 39 | Body: file, 40 | ContentType: aws.String(itype), 41 | }) 42 | 43 | if err != nil { 44 | return "", errors.Wrap(err, "cloudflare_r2") 45 | } 46 | 47 | return fileKey, nil 48 | } 49 | 50 | func (p *R2) SendContent(fileKey string, content []byte) (string, error) { 51 | 52 | ctx := context.Background() 53 | bucket := p.GetBucket("") 54 | 55 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 56 | 57 | input := &s3.PutObjectInput{ 58 | Bucket: aws.String(bucket), 59 | Key: aws.String(fileKey), 60 | Body: bytes.NewReader(content), 61 | ChecksumAlgorithm: types.ChecksumAlgorithmSha256, 62 | } 63 | output, err := p.S3Manager.Upload(ctx, input) 64 | if err != nil { 65 | var noBucket *types.NoSuchBucket 66 | if errors.As(err, &noBucket) { 67 | fmt.Printf("Bucket %s does not exist.\n", bucket) 68 | err = noBucket 69 | } 70 | } else { 71 | err := s3.NewObjectExistsWaiter(p.S3Client).Wait(ctx, &s3.HeadObjectInput{ 72 | Bucket: aws.String(bucket), 73 | Key: aws.String(fileKey), 74 | }, time.Minute) 75 | if err != nil { 76 | fmt.Printf("Failed attempt to wait for object %s to exist in %s.\n", fileKey, bucket) 77 | } else { 78 | _ = *output.Key 79 | } 80 | } 81 | 82 | return fileKey, errors.Wrap(err, "cloudflare_r2") 83 | } 84 | -------------------------------------------------------------------------------- /pkg/storage/minio/operation.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "time" 9 | 10 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/service/s3" 14 | "github.com/aws/aws-sdk-go-v2/service/s3/types" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | func (p *MinIO) GetBucket(bucketName string) string { 19 | 20 | // Get bucket 21 | if len(bucketName) <= 0 { 22 | bucketName = p.Config.BucketName 23 | } 24 | 25 | return bucketName 26 | } 27 | 28 | // UploadByFile 上传文件 29 | func (p *MinIO) SendFile(fileKey string, file io.Reader, itype string) (string, error) { 30 | 31 | ctx := context.Background() 32 | bucket := p.GetBucket("") 33 | 34 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 35 | 36 | // k, _ := h.Open() 37 | 38 | _, err := p.S3Client.PutObject(ctx, &s3.PutObjectInput{ 39 | Bucket: aws.String(bucket), 40 | Key: aws.String(fileKey), 41 | Body: file, 42 | ContentType: aws.String(itype), 43 | }) 44 | 45 | if err != nil { 46 | return "", errors.Wrap(err, "minio") 47 | } 48 | 49 | return fileurl.PathSuffixCheckAdd(p.Config.BucketName, "/") + fileKey, nil 50 | } 51 | 52 | func (p *MinIO) SendContent(fileKey string, content []byte) (string, error) { 53 | 54 | ctx := context.Background() 55 | bucket := p.GetBucket("") 56 | 57 | fileKey = fileurl.PathSuffixCheckAdd(p.Config.CustomPath, "/") + fileKey 58 | 59 | input := &s3.PutObjectInput{ 60 | Bucket: aws.String(bucket), 61 | Key: aws.String(fileKey), 62 | Body: bytes.NewReader(content), 63 | ChecksumAlgorithm: types.ChecksumAlgorithmSha256, 64 | } 65 | output, err := p.S3Manager.Upload(ctx, input) 66 | if err != nil { 67 | var noBucket *types.NoSuchBucket 68 | if errors.As(err, &noBucket) { 69 | fmt.Printf("Bucket %s does not exist.\n", bucket) 70 | err = noBucket 71 | } 72 | } else { 73 | err := s3.NewObjectExistsWaiter(p.S3Client).Wait(ctx, &s3.HeadObjectInput{ 74 | Bucket: aws.String(bucket), 75 | Key: aws.String(fileKey), 76 | }, time.Minute) 77 | if err != nil { 78 | fmt.Printf("Failed attempt to wait for object %s to exist in %s.\n", fileKey, bucket) 79 | } else { 80 | _ = *output.Key 81 | } 82 | } 83 | 84 | return fileurl.PathSuffixCheckAdd(p.Config.BucketName, "/") + fileKey, errors.Wrap(err, "minio") 85 | } 86 | -------------------------------------------------------------------------------- /internal/query/gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by gorm.io/gen. DO NOT EDIT. 2 | // Code generated by gorm.io/gen. DO NOT EDIT. 3 | // Code generated by gorm.io/gen. DO NOT EDIT. 4 | 5 | package query 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | 11 | "gorm.io/gorm" 12 | 13 | "gorm.io/gen" 14 | 15 | "gorm.io/plugin/dbresolver" 16 | ) 17 | 18 | func Use(db *gorm.DB, opts ...gen.DOOption) *Query { 19 | return &Query{ 20 | db: db, 21 | CloudConfig: newCloudConfig(db, opts...), 22 | User: newUser(db, opts...), 23 | } 24 | } 25 | 26 | type Query struct { 27 | db *gorm.DB 28 | 29 | CloudConfig cloudConfig 30 | User user 31 | } 32 | 33 | func (q *Query) Available() bool { return q.db != nil } 34 | 35 | func (q *Query) clone(db *gorm.DB) *Query { 36 | return &Query{ 37 | db: db, 38 | CloudConfig: q.CloudConfig.clone(db), 39 | User: q.User.clone(db), 40 | } 41 | } 42 | 43 | func (q *Query) ReadDB() *Query { 44 | return q.ReplaceDB(q.db.Clauses(dbresolver.Read)) 45 | } 46 | 47 | func (q *Query) WriteDB() *Query { 48 | return q.ReplaceDB(q.db.Clauses(dbresolver.Write)) 49 | } 50 | 51 | func (q *Query) ReplaceDB(db *gorm.DB) *Query { 52 | return &Query{ 53 | db: db, 54 | CloudConfig: q.CloudConfig.replaceDB(db), 55 | User: q.User.replaceDB(db), 56 | } 57 | } 58 | 59 | type queryCtx struct { 60 | CloudConfig ICloudConfigDo 61 | User IUserDo 62 | } 63 | 64 | func (q *Query) WithContext(ctx context.Context) *queryCtx { 65 | return &queryCtx{ 66 | CloudConfig: q.CloudConfig.WithContext(ctx), 67 | User: q.User.WithContext(ctx), 68 | } 69 | } 70 | 71 | func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error { 72 | return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...) 73 | } 74 | 75 | func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx { 76 | tx := q.db.Begin(opts...) 77 | return &QueryTx{Query: q.clone(tx), Error: tx.Error} 78 | } 79 | 80 | type QueryTx struct { 81 | *Query 82 | Error error 83 | } 84 | 85 | func (q *QueryTx) Commit() error { 86 | return q.db.Commit().Error 87 | } 88 | 89 | func (q *QueryTx) Rollback() error { 90 | return q.db.Rollback().Error 91 | } 92 | 93 | func (q *QueryTx) SavePoint(name string) error { 94 | return q.db.SavePoint(name).Error 95 | } 96 | 97 | func (q *QueryTx) RollbackTo(name string) error { 98 | return q.db.RollbackTo(name).Error 99 | } 100 | -------------------------------------------------------------------------------- /pkg/storage/aws_s3/s3.go: -------------------------------------------------------------------------------- 1 | package aws_s3 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/config" 7 | "github.com/aws/aws-sdk-go-v2/credentials" 8 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 9 | "github.com/aws/aws-sdk-go-v2/service/s3" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type Config struct { 14 | IsEnabled bool `yaml:"is-enable"` 15 | IsUserEnabled bool `yaml:"is-user-enable"` 16 | Region string `yaml:"region"` 17 | BucketName string `yaml:"bucket-name"` 18 | AccessKeyID string `yaml:"access-key-id"` 19 | AccessKeySecret string `yaml:"access-key-secret"` 20 | CustomPath string `yaml:"custom-path"` 21 | } 22 | 23 | type S3 struct { 24 | S3Client *s3.Client 25 | S3Manager *manager.Uploader 26 | Config *Config 27 | } 28 | 29 | var clients = make(map[string]*S3) 30 | 31 | func NewClient(cf map[string]any) (*S3, error) { 32 | // New client 33 | 34 | var IsEnabled bool 35 | switch t := cf["IsEnabled"].(type) { 36 | case int64: 37 | if t == 0 { 38 | IsEnabled = false 39 | } else { 40 | IsEnabled = true 41 | } 42 | case bool: 43 | IsEnabled = t 44 | } 45 | 46 | var IsUserEnabled bool 47 | switch t := cf["IsUserEnabled"].(type) { 48 | case int64: 49 | if t == 0 { 50 | IsUserEnabled = false 51 | } else { 52 | IsUserEnabled = true 53 | } 54 | case bool: 55 | IsUserEnabled = t 56 | } 57 | 58 | conf := &Config{ 59 | IsEnabled: IsEnabled, 60 | IsUserEnabled: IsUserEnabled, 61 | Region: cf["Region"].(string), 62 | BucketName: cf["BucketName"].(string), 63 | AccessKeyID: cf["AccessKeyID"].(string), 64 | AccessKeySecret: cf["AccessKeySecret"].(string), 65 | CustomPath: cf["CustomPath"].(string), 66 | } 67 | 68 | var region = conf.Region 69 | var accessKeyId = conf.AccessKeyID 70 | var accessKeySecret = conf.AccessKeySecret 71 | 72 | if clients[accessKeyId] != nil { 73 | return clients[accessKeyId], nil 74 | } 75 | 76 | cfg, err := config.LoadDefaultConfig(context.TODO(), 77 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret, "")), 78 | config.WithRegion(region), 79 | ) 80 | if err != nil { 81 | return nil, errors.Wrap(err, "aws_s3") 82 | } 83 | 84 | client := s3.NewFromConfig(cfg, func(o *s3.Options) {}) 85 | 86 | if err != nil { 87 | return nil, errors.Wrap(err, "aws_s3") 88 | } 89 | 90 | clients[accessKeyId] = &S3{ 91 | S3Client: client, 92 | Config: conf, 93 | } 94 | return clients[accessKeyId], nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/storage/webdav/operation.go: -------------------------------------------------------------------------------- 1 | // operation.go 2 | 3 | package webdav 4 | 5 | import ( 6 | "io" 7 | "os" 8 | 9 | "github.com/haierkeys/custom-image-gateway/pkg/errors" 10 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 11 | ) 12 | 13 | // SendFile 将本地文件上传到 WebDAV 服务器。 14 | func (w *WebDAV) SendFile(fileKey string, file io.Reader, itype string) (string, error) { 15 | 16 | fileKey = fileurl.PathSuffixCheckAdd(w.Config.CustomPath, "/") + fileKey 17 | 18 | err := w.Client.MkdirAll(w.Config.CustomPath, 0644) 19 | if err != nil { 20 | return "", errors.Wrap(err, "webdav") 21 | } 22 | 23 | content, err := io.ReadAll(file) 24 | if err != nil { 25 | return "", errors.Wrap(err, "webdav") 26 | } 27 | 28 | err = w.Client.Write(fileKey, content, os.ModePerm) 29 | 30 | if err != nil { 31 | return "", errors.Wrap(err, "webdav") 32 | } 33 | 34 | return fileKey, nil 35 | } 36 | 37 | // SendContent 将二进制内容上传到 WebDAV 服务器。 38 | func (w *WebDAV) SendContent(fileKey string, content []byte) (string, error) { 39 | 40 | fileKey = fileurl.PathSuffixCheckAdd(w.Config.CustomPath, "/") + fileKey 41 | 42 | err := w.Client.Write(fileKey, content, os.ModePerm) 43 | 44 | if err != nil { 45 | return "", errors.Wrap(err, "webdav") 46 | } 47 | 48 | return fileKey, nil 49 | } 50 | 51 | // // DownloadFile 从 WebDAV 服务器下载文件到本地。 52 | // func (w *WebDAV) DownloadFile(remotePath, localPath string) error { 53 | // err := w.Client.DownloadFile(remotePath, localPath) 54 | // if err != nil { 55 | // return fmt.Errorf("下载文件失败: %v", err) 56 | // } 57 | 58 | // return nil 59 | // } 60 | 61 | // // DeleteFile 从 WebDAV 服务器删除文件。 62 | // func (w *WebDAV) DeleteFile(remotePath string) error { 63 | // err := w.Client.Remove(remotePath) 64 | // if err != nil { 65 | // return fmt.Errorf("删除文件失败: %v", err) 66 | // } 67 | 68 | // return nil 69 | // } 70 | 71 | // // MkDir 在 WebDAV 服务器上创建目录。 72 | // func (w *WebDAV) MkDir(remotePath string) error { 73 | // err := w.Client.Mkdir(remotePath) 74 | // if err != nil { 75 | // if !gowebdav.IsErrExist(err) { 76 | // return fmt.Errorf("创建目录失败: %v", err) 77 | // } 78 | // // 如果目录已存在,忽略错误 79 | // log.Printf("目录 %s 已存在,忽略错误", remotePath) 80 | // } 81 | 82 | // return nil 83 | // } 84 | 85 | // // ListFiles 列出 WebDAV 服务器上的文件和目录。 86 | // func (w *WebDAV) ListFiles(remotePath string) ([]string, error) { 87 | // files, err := w.Client.ReadDir(remotePath) 88 | // if err != nil { 89 | // return nil, fmt.Errorf("列出文件失败: %v", err) 90 | // } 91 | 92 | // var fileNames []string 93 | // for _, file := range files { 94 | // fileNames = append(fileNames, file.Name()) 95 | // } 96 | 97 | // return fileNames, nil 98 | // } 99 | -------------------------------------------------------------------------------- /pkg/storage/cloudflare_r2/r2.go: -------------------------------------------------------------------------------- 1 | package cloudflare_r2 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/config" 9 | "github.com/aws/aws-sdk-go-v2/credentials" 10 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 11 | "github.com/aws/aws-sdk-go-v2/service/s3" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type Config struct { 16 | IsEnabled bool `yaml:"is-enable"` 17 | IsUserEnabled bool `yaml:"is-user-enable"` 18 | AccountID string `yaml:"account-id"` 19 | BucketName string `yaml:"bucket-name"` 20 | AccessKeyID string `yaml:"access-key-id"` 21 | AccessKeySecret string `yaml:"access-key-secret"` 22 | CustomPath string `yaml:"custom-path"` 23 | } 24 | 25 | type R2 struct { 26 | S3Client *s3.Client 27 | S3Manager *manager.Uploader 28 | Config *Config 29 | } 30 | 31 | var clients = make(map[string]*R2) 32 | 33 | func NewClient(cf map[string]any) (*R2, error) { 34 | 35 | var IsEnabled bool 36 | switch t := cf["IsEnabled"].(type) { 37 | case int64: 38 | if t == 0 { 39 | IsEnabled = false 40 | } else { 41 | IsEnabled = true 42 | } 43 | case bool: 44 | IsEnabled = t 45 | } 46 | 47 | var IsUserEnabled bool 48 | switch t := cf["IsUserEnabled"].(type) { 49 | case int64: 50 | if t == 0 { 51 | IsUserEnabled = false 52 | } else { 53 | IsUserEnabled = true 54 | } 55 | case bool: 56 | IsUserEnabled = t 57 | } 58 | 59 | conf := &Config{ 60 | IsEnabled: IsEnabled, 61 | IsUserEnabled: IsUserEnabled, 62 | AccountID: cf["AccountID"].(string), 63 | BucketName: cf["BucketName"].(string), 64 | AccessKeyID: cf["AccessKeyID"].(string), 65 | AccessKeySecret: cf["AccessKeySecret"].(string), 66 | CustomPath: cf["CustomPath"].(string), 67 | } 68 | 69 | var accountId = conf.AccountID 70 | var accessKeyId = conf.AccessKeyID 71 | var accessKeySecret = conf.AccessKeySecret 72 | 73 | if clients[accessKeyId] != nil { 74 | return clients[accessKeyId], nil 75 | } 76 | 77 | cfg, err := config.LoadDefaultConfig(context.TODO(), 78 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret, "")), 79 | config.WithRegion("auto"), 80 | ) 81 | if err != nil { 82 | 83 | return nil, errors.Wrap(err, "cloudflare_r2") 84 | } 85 | 86 | client := s3.NewFromConfig(cfg, func(o *s3.Options) { 87 | o.BaseEndpoint = aws.String(fmt.Sprintf("https://%s.r2.cloudflarestorage.com", accountId)) 88 | }) 89 | 90 | if err != nil { 91 | return nil, errors.Wrap(err, "cloudflare_r2") 92 | } 93 | clients[accessKeyId] = &R2{ 94 | S3Client: client, 95 | Config: conf, 96 | } 97 | return clients[accessKeyId], nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/timex/time.go: -------------------------------------------------------------------------------- 1 | package timex 2 | 3 | import ( 4 | "database/sql/driver" 5 | "errors" 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | const TimeFormat = "2006-01-02 15:04:05" 11 | 12 | type Time time.Time 13 | 14 | func (t *Time) UnmarshalJSON(data []byte) (err error) { 15 | if len(data) == 2 { 16 | *t = Time(time.Time{}) 17 | return 18 | } 19 | 20 | now, err := time.Parse(`"`+TimeFormat+`"`, string(data)) 21 | *t = Time(now) 22 | return 23 | } 24 | 25 | func (t Time) MarshalJSON() ([]byte, error) { 26 | tTime := time.Time(t) 27 | // 如果时间值是空或者0值 返回为null 如果写空字符串会报出异常时间 28 | // 下面是修复0001-01-01问题的 29 | if &t == nil || t.IsZero() { 30 | return []byte("null"), nil 31 | } 32 | return []byte(fmt.Sprintf("\"%s\"", tTime.Format(TimeFormat))), nil 33 | 34 | } 35 | 36 | func (t *Time) IsZero() bool { 37 | return time.Time(*t).IsZero() 38 | } 39 | 40 | func (t Time) Value() (driver.Value, error) { 41 | if t.String() == "0000-00-00 00:00:00" { 42 | return nil, nil 43 | } 44 | if t.String() == "0001-01-01 00:00:00" { 45 | return nil, nil 46 | } 47 | return time.Time(t).Format(TimeFormat), nil 48 | } 49 | 50 | func (t *Time) Scan(v any) error { 51 | timeValue, ok := v.(time.Time) 52 | if !ok { 53 | return errors.New(fmt.Sprint("Failed to unmarshal time value:", v)) 54 | } 55 | *t = Time(timeValue) 56 | return nil 57 | 58 | } 59 | 60 | func (t Time) String() string { 61 | return time.Time(t).Format(TimeFormat) 62 | } 63 | 64 | func (t Time) StringSource() string { 65 | return time.Time(t).String() 66 | } 67 | 68 | func Now() Time { 69 | return Time(time.Now()) 70 | } 71 | 72 | // After reports whether the time instant t is after u. 73 | func (t Time) After(u Time) bool { 74 | ts := time.Time(t) 75 | return ts.After(time.Time(u)) 76 | } 77 | 78 | // Before reports whether the time instant t is before u. 79 | func (t Time) Before(u Time) bool { 80 | ts := time.Time(t) 81 | return ts.Before(time.Time(u)) 82 | } 83 | 84 | // Equal reports whether t and u represent the same time instant. 85 | // Two times can be equal even if they are in different locations. 86 | // For example, 6:00 +0200 and 4:00 UTC are Equal. 87 | // See the documentation on the Time type for the pitfalls of using == with 88 | // Time values; most code should use Equal instead. 89 | func (t Time) Equal(u Time) bool { 90 | ts := time.Time(t) 91 | return ts.Equal(time.Time(u)) 92 | } 93 | 94 | // Add returns the time t+d. 95 | func (t Time) Add(d time.Duration) Time { 96 | ts := time.Time(t) 97 | return Time(ts.Add(d)) 98 | } 99 | 100 | func Since(t Time) time.Duration { 101 | ts := time.Time(t) 102 | return time.Since(ts) 103 | } 104 | -------------------------------------------------------------------------------- /pkg/safe_close/safe_close.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022, IrineSistiana 3 | * 4 | * This file is part of mosdns. 5 | * 6 | * mosdns is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * mosdns is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package safe_close 21 | 22 | import "sync" 23 | 24 | // SafeClose can achieve safe close where WaitClosed returns only after 25 | // all sub goroutines exited. 26 | // 27 | // 1. Main service goroutine starts and wait on ReceiveCloseSignal. 28 | // 2. Any service's sub goroutine should be started by Attach and wait on ReceiveCloseSignal. 29 | // 3. If any fatal err occurs, any service goroutine can call SendCloseSignal to close the service. 30 | // 4. Any third party caller can call SendCloseSignal to close the service. 31 | type SafeClose struct { 32 | m sync.Mutex 33 | wg sync.WaitGroup 34 | closeSignal chan struct{} 35 | closeErr error 36 | } 37 | 38 | func NewSafeClose() *SafeClose { 39 | return &SafeClose{ 40 | closeSignal: make(chan struct{}), 41 | } 42 | } 43 | 44 | // WaitClosed waits until all SendCloseSignal is called and all 45 | // attached funcs in SafeClose are done. 46 | func (s *SafeClose) WaitClosed() error { 47 | <-s.closeSignal 48 | s.wg.Wait() 49 | return s.closeErr 50 | } 51 | 52 | // SendCloseSignal sends a close signal. Unblock WaitClosed. 53 | // The given error will be read by WaitClosed. 54 | // Once SendCloseSignal is called, following calls are noop. 55 | func (s *SafeClose) SendCloseSignal(err error) { 56 | s.m.Lock() 57 | select { 58 | case <-s.closeSignal: 59 | default: 60 | s.closeErr = err 61 | close(s.closeSignal) 62 | } 63 | s.m.Unlock() 64 | } 65 | 66 | func (s *SafeClose) ReceiveCloseSignal() <-chan struct{} { 67 | return s.closeSignal 68 | } 69 | 70 | // Attach add this goroutine to s.wg WaitClosed. 71 | // f must receive closeSignal and call done when it is done. 72 | // If s was closed, f will not run. 73 | func (s *SafeClose) Attach(f func(done func(), closeSignal <-chan struct{})) { 74 | s.m.Lock() 75 | select { 76 | case <-s.closeSignal: 77 | default: 78 | s.wg.Add(1) 79 | go func() { 80 | f(s.wg.Done, s.closeSignal) 81 | }() 82 | } 83 | s.m.Unlock() 84 | } 85 | -------------------------------------------------------------------------------- /pkg/gin_tools/param.go: -------------------------------------------------------------------------------- 1 | /** 2 | @author: haierkeys 3 | @since: 2022/9/14 4 | @desc: //TODO 5 | **/ 6 | 7 | package gin_tools 8 | 9 | import ( 10 | "bytes" 11 | "encoding/json" 12 | "io" 13 | "net/http" 14 | "sync" 15 | 16 | "github.com/gin-gonic/gin" 17 | ) 18 | 19 | func RequestParams(c *gin.Context) (map[string]interface{}, error) { 20 | 21 | const defaultMemory = 32 << 20 22 | contentType := c.ContentType() 23 | 24 | var ( 25 | dataMap = make(map[string]interface{}) 26 | queryMap = make(map[string]interface{}) 27 | postMap = make(map[string]interface{}) 28 | ) 29 | 30 | // @see gin@v1.7.7/binding/query.go ==> func (queryBinding) Bind(req *httpclient.Request, obj interface{}) 31 | for k := range c.Request.URL.Query() { 32 | queryMap[k] = c.Query(k) 33 | } 34 | 35 | if "application/json" == contentType { 36 | var bodyBytes []byte 37 | if c.Request.Body != nil { 38 | bodyBytes, _ = io.ReadAll(c.Request.Body) 39 | } 40 | c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) 41 | // @see gin@v1.7.7/binding/json.go ==> func (jsonBinding) Bind(req *httpclient.Request, obj interface{}) 42 | if c.Request != nil && c.Request.Body != nil { 43 | if err := json.NewDecoder(c.Request.Body).Decode(&postMap); err != nil { 44 | return nil, err 45 | } 46 | } 47 | c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) 48 | } else if "multipart/form-data" == contentType { 49 | // @see gin@v1.7.7/binding/form.go ==> func (formMultipartBinding) Bind(req *httpclient.Request, obj interface{}) 50 | if err := c.Request.ParseMultipartForm(defaultMemory); err != nil { 51 | return nil, err 52 | } 53 | for k, v := range c.Request.PostForm { 54 | if len(v) > 1 { 55 | postMap[k] = v 56 | } else if len(v) == 1 { 57 | postMap[k] = v[0] 58 | } 59 | } 60 | } else { 61 | // ParseForm 解析 URL 中的查询字符串,并将解析结果更新到 r.Form 字段 62 | // 对于 POST 或 PUT 请求,ParseForm 还会将 body 当作表单解析, 63 | // 并将结果既更新到 r.PostForm 也更新到 r.Form。解析结果中, 64 | // POST 或 PUT 请求主体要优先于 URL 查询字符串(同名变量,主体的值在查询字符串的值前面) 65 | // @see gin@v1.7.7/binding/form.go ==> func (formBinding) Bind(req *httpclient.Request, obj interface{}) 66 | if err := c.Request.ParseForm(); err != nil { 67 | return nil, err 68 | } 69 | if err := c.Request.ParseMultipartForm(defaultMemory); err != nil { 70 | if err != http.ErrNotMultipart { 71 | return nil, err 72 | } 73 | } 74 | for k, v := range c.Request.PostForm { 75 | if len(v) > 1 { 76 | postMap[k] = v 77 | } else if len(v) == 1 { 78 | postMap[k] = v[0] 79 | } 80 | } 81 | } 82 | 83 | var mu sync.RWMutex 84 | for k, v := range queryMap { 85 | mu.Lock() 86 | dataMap[k] = v 87 | mu.Unlock() 88 | } 89 | for k, v := range postMap { 90 | mu.Lock() 91 | dataMap[k] = v 92 | mu.Unlock() 93 | } 94 | 95 | return dataMap, nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/storage/minio/minio.go: -------------------------------------------------------------------------------- 1 | package minio 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | "github.com/aws/aws-sdk-go-v2/credentials" 9 | "github.com/aws/aws-sdk-go-v2/feature/s3/manager" 10 | "github.com/aws/aws-sdk-go-v2/service/s3" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type Config struct { 15 | IsEnabled bool `yaml:"is-enable"` 16 | IsUserEnabled bool `yaml:"is-user-enable"` 17 | BucketName string `yaml:"bucket-name"` 18 | Endpoint string `yaml:"endpoint"` 19 | Region string `yaml:"region"` 20 | AccessKeyID string `yaml:"access-key-id"` 21 | AccessKeySecret string `yaml:"access-key-secret"` 22 | CustomPath string `yaml:"custom-path"` 23 | } 24 | 25 | type MinIO struct { 26 | S3Client *s3.Client 27 | S3Manager *manager.Uploader 28 | Config *Config 29 | } 30 | 31 | var clients = make(map[string]*MinIO) 32 | 33 | func NewClient(cf map[string]any) (*MinIO, error) { 34 | // New client 35 | 36 | var IsEnabled bool 37 | switch t := cf["IsEnabled"].(type) { 38 | case int64: 39 | if t == 0 { 40 | IsEnabled = false 41 | } else { 42 | IsEnabled = true 43 | } 44 | case bool: 45 | IsEnabled = t 46 | } 47 | 48 | var IsUserEnabled bool 49 | switch t := cf["IsUserEnabled"].(type) { 50 | case int64: 51 | if t == 0 { 52 | IsUserEnabled = false 53 | } else { 54 | IsUserEnabled = true 55 | } 56 | case bool: 57 | IsUserEnabled = t 58 | } 59 | 60 | conf := &Config{ 61 | IsEnabled: IsEnabled, 62 | IsUserEnabled: IsUserEnabled, 63 | Endpoint: cf["Endpoint"].(string), 64 | Region: cf["Region"].(string), 65 | BucketName: cf["BucketName"].(string), 66 | AccessKeyID: cf["AccessKeyID"].(string), 67 | AccessKeySecret: cf["AccessKeySecret"].(string), 68 | CustomPath: cf["CustomPath"].(string), 69 | } 70 | 71 | var endpoint = conf.Endpoint 72 | var region = conf.Region 73 | var accessKeyId = conf.AccessKeyID 74 | var accessKeySecret = conf.AccessKeySecret 75 | 76 | if clients[accessKeyId] != nil { 77 | return clients[accessKeyId], nil 78 | } 79 | 80 | cfg, err := config.LoadDefaultConfig(context.TODO(), 81 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(accessKeyId, accessKeySecret, "")), 82 | config.WithRegion(region), 83 | ) 84 | 85 | if err != nil { 86 | return nil, errors.Wrap(err, "minio") 87 | } 88 | 89 | client := s3.NewFromConfig(cfg, func(o *s3.Options) { 90 | o.UsePathStyle = true 91 | o.BaseEndpoint = aws.String(endpoint) 92 | }) 93 | 94 | if err != nil { 95 | return nil, errors.Wrap(err, "minio") 96 | } 97 | 98 | clients[accessKeyId] = &MinIO{ 99 | S3Client: client, 100 | Config: conf, 101 | } 102 | return clients[accessKeyId], nil 103 | } 104 | -------------------------------------------------------------------------------- /internal/routers/api_router/upload.go: -------------------------------------------------------------------------------- 1 | package apiRouter 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/global" 5 | "github.com/haierkeys/custom-image-gateway/internal/service" 6 | "github.com/haierkeys/custom-image-gateway/pkg/app" 7 | "github.com/haierkeys/custom-image-gateway/pkg/code" 8 | 9 | "github.com/gin-gonic/gin" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | type Upload struct{} 14 | 15 | func NewUpload() Upload { 16 | return Upload{} 17 | } 18 | 19 | // Upload 上传文件 20 | func (u Upload) Upload(c *gin.Context) { 21 | 22 | params := &service.ClientUploadParams{} 23 | response := app.NewResponse(c) 24 | valid, errs := app.BindAndValid(c, params) 25 | 26 | if !valid { 27 | global.Logger.Error("apiRouter.UserUpload.BindAndValid errs: %v", zap.Error(errs)) 28 | response.ToResponse(code.ErrorInvalidParams.WithDetails(errs.ErrorsToString()).WithData(errs.MapsToString())) 29 | return 30 | } 31 | 32 | var svcUploadFileData *service.FileInfo 33 | var svc = service.New(c) 34 | var err error 35 | 36 | file, fileHeader, errf := c.Request.FormFile("imagefile") 37 | if errf != nil { 38 | global.Logger.Error("apiRouter.UserUpload.ErrorInvalidParams len 0", zap.Error(errf)) 39 | response.ToResponse(code.ErrorInvalidParams) 40 | } 41 | defer file.Close() 42 | 43 | svcUploadFileData, err = svc.UploadFile(file, fileHeader, params) 44 | if err != nil { 45 | global.Logger.Error("svc.UploadFile err: %v", zap.Error(err)) 46 | response.ToResponse(code.ErrorUploadFileFailed.WithDetails(err.Error())) 47 | return 48 | } 49 | 50 | response.ToResponse(code.Success.WithData(svcUploadFileData)) 51 | 52 | } 53 | 54 | // Upload 上传文件 55 | func (u Upload) UserUpload(c *gin.Context) { 56 | 57 | params := &service.ClientUploadParams{} 58 | response := app.NewResponse(c) 59 | valid, errs := app.BindAndValid(c, params) 60 | 61 | if !valid { 62 | global.Logger.Error("apiRouter.UserUpload.BindAndValid errs: %v", zap.Error(errs)) 63 | response.ToResponse(code.ErrorInvalidParams.WithDetails(errs.ErrorsToString()).WithData(errs.MapsToString())) 64 | return 65 | } 66 | 67 | var svcUploadFileData *service.FileInfo 68 | var svc = service.New(c) 69 | var err error 70 | 71 | file, fileHeader, errf := c.Request.FormFile("imagefile") 72 | if errf != nil { 73 | global.Logger.Error("apiRouter.UserUpload.ErrorInvalidParams len 0", zap.Error(errf)) 74 | response.ToResponse(code.ErrorInvalidParams) 75 | } 76 | defer file.Close() 77 | 78 | uid := app.GetUID(c) 79 | if uid == 0 { 80 | global.Logger.Error("apiRouter.UserUpload svc UserLogin err uid=0") 81 | response.ToResponse(code.ErrorNotUserAuthToken) 82 | return 83 | } 84 | svcUploadFileData, err = svc.UserUploadFile(uid, file, fileHeader, params) 85 | if err != nil { 86 | global.Logger.Error("svc.UplUserUploadFileoadFile err: %v", zap.Error(err)) 87 | response.ToResponse(code.ErrorUploadFileFailed.WithDetails(err.Error())) 88 | return 89 | } 90 | 91 | response.ToResponse(code.Success.WithData(svcUploadFileData)) 92 | } 93 | -------------------------------------------------------------------------------- /internal/routers/router.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | "time" 8 | 9 | _ "github.com/haierkeys/custom-image-gateway/docs" 10 | "github.com/haierkeys/custom-image-gateway/global" 11 | "github.com/haierkeys/custom-image-gateway/internal/middleware" 12 | apiRouter "github.com/haierkeys/custom-image-gateway/internal/routers/api_router" 13 | "github.com/haierkeys/custom-image-gateway/pkg/limiter" 14 | 15 | "github.com/gin-gonic/gin" 16 | swaggerFiles "github.com/swaggo/files" 17 | ginSwagger "github.com/swaggo/gin-swagger" 18 | ) 19 | 20 | var methodLimiters = limiter.NewMethodLimiter().AddBuckets( 21 | limiter.BucketRule{ 22 | Key: "/auth", 23 | FillInterval: time.Second, 24 | Capacity: 10, 25 | Quantum: 10, 26 | }, 27 | ) 28 | 29 | func NewRouter(frontendFiles embed.FS) *gin.Engine { 30 | 31 | frontendAssets, _ := fs.Sub(frontendFiles, "frontend/assets") 32 | frontendIndexContent, _ := frontendFiles.ReadFile("frontend/index.html") 33 | 34 | r := gin.New() 35 | r.GET("/", func(c *gin.Context) { 36 | c.Data(http.StatusOK, "text/html; charset=utf-8", frontendIndexContent) 37 | }) 38 | r.StaticFS("/assets", http.FS(frontendAssets)) 39 | api := r.Group("/api") 40 | { 41 | api.Use(middleware.AppInfo()) 42 | api.Use(gin.Logger()) 43 | api.Use(middleware.RateLimiter(methodLimiters)) 44 | api.Use(middleware.ContextTimeout(time.Duration(global.Config.App.DefaultContextTimeout) * time.Second)) 45 | api.Use(middleware.Cors()) 46 | api.Use(middleware.Lang()) 47 | api.Use(middleware.AccessLog()) 48 | api.Use(middleware.Recovery()) 49 | // 对404 的处理 50 | // r.NoRoute(middleware.NoFound()) 51 | // r.Use(middleware.Tracing()) 52 | api.GET("/debug/vars", apiRouter.Expvar) 53 | api.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) 54 | 55 | userApiR := api.Group("/user") 56 | { 57 | userApiR.POST("/register", apiRouter.NewUser().Register) 58 | userApiR.POST("/login", apiRouter.NewUser().Login) 59 | 60 | userApiR.Use(middleware.UserAuthToken()).POST("/change_password", apiRouter.NewUser().UserChangePassword) 61 | userApiR.Use(middleware.UserAuthToken()).GET("/cloud_config_enabled_types", apiRouter.NewCloudConfig().EnabledTypes) 62 | userApiR.Use(middleware.UserAuthToken()).POST("/cloud_config", apiRouter.NewCloudConfig().UpdateAndCreate) 63 | userApiR.Use(middleware.UserAuthToken()).DELETE("/cloud_config", apiRouter.NewCloudConfig().Delete) 64 | userApiR.Use(middleware.UserAuthToken()).GET("/cloud_config", apiRouter.NewCloudConfig().List) 65 | userApiR.Use(middleware.UserAuthToken()).POST("/upload", apiRouter.NewUpload().UserUpload) 66 | } 67 | 68 | api.Use(middleware.AuthToken()).POST("/upload", apiRouter.NewUpload().Upload) 69 | 70 | } 71 | if global.Config.LocalFS.HttpfsIsEnable { 72 | r.StaticFS(global.Config.LocalFS.SavePath, http.Dir(global.Config.LocalFS.SavePath)) 73 | } 74 | r.Use(middleware.Cors()) 75 | r.NoRoute(middleware.NoFound()) 76 | 77 | return r 78 | } 79 | -------------------------------------------------------------------------------- /pkg/convert/copy_struct.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // CopyStruct 12 | // dst 目标结构体,src 源结构体 13 | // 它会把src与dst的相同字段名的值,复制到dst中 14 | func StructAssign(src any, dst any) any { 15 | bVal := reflect.ValueOf(dst).Elem() // 获取reflect.Type类型 16 | vVal := reflect.ValueOf(src).Elem() // 获取reflect.Type类型 17 | vTypeOfT := vVal.Type() 18 | for i := 0; i < vVal.NumField(); i++ { 19 | // 在要修改的结构体中查询有数据结构体中相同属性的字段,有则修改其值 20 | name := vTypeOfT.Field(i).Name 21 | if ok := bVal.FieldByName(name).IsValid(); ok { 22 | bVal.FieldByName(name).Set(reflect.ValueOf(vVal.Field(i).Interface())) 23 | } 24 | } 25 | return dst 26 | } 27 | 28 | /** 29 | * @Description: 结构体map互转 30 | * @param param interface{} 需要被转的数据 31 | * @param data interface{} 转换完成后的数据 需要用引用传进来 32 | * @return []string{} 33 | */ 34 | func StructToMap(param any, data map[string]interface{}) error { 35 | str, _ := json.Marshal(param) 36 | error := json.Unmarshal(str, &data) 37 | if error != nil { 38 | return error 39 | } else { 40 | return nil 41 | } 42 | 43 | } 44 | 45 | func StructToMapByReflect(obj any) map[string]any { 46 | val := reflect.ValueOf(obj) 47 | if val.Kind() == reflect.Ptr { 48 | val = val.Elem() 49 | } 50 | if val.Kind() != reflect.Struct { 51 | return nil 52 | } 53 | 54 | result := make(map[string]any) 55 | typ := val.Type() 56 | 57 | for i := 0; i < val.NumField(); i++ { 58 | field := val.Field(i) 59 | 60 | fieldName := typ.Field(i).Name 61 | if field.CanInterface() { 62 | // 如果字段是 Struct,递归处理 63 | if field.Kind() == reflect.Struct { 64 | result[fieldName] = StructToMapByReflect(field.Interface()) 65 | } else { 66 | result[fieldName] = field.Interface() 67 | } 68 | } 69 | } 70 | 71 | return result 72 | } 73 | 74 | func StructToModelMap(param any, data map[string]any, key string) error { 75 | 76 | // 获取反射值 77 | val := reflect.ValueOf(param) 78 | 79 | if val.Kind() == reflect.Ptr { 80 | val = val.Elem() 81 | } 82 | 83 | // 确保传入的是结构体 84 | if val.Kind() != reflect.Struct { 85 | return errors.New("not struct") 86 | } 87 | 88 | // 获取结构体类型 89 | typ := val.Type() 90 | 91 | // 遍历结构体字段 92 | for i := 0; i < val.NumField(); i++ { 93 | 94 | if key == "" || typ.Field(i).Name == key { 95 | continue 96 | } 97 | 98 | // 获取 GORM 的 column 标签 99 | tags := splitGormTag(typ.Field(i).Tag.Get("gorm")) 100 | 101 | if tags["column"] != "" { 102 | data[tags["column"]] = val.Field(i).Interface() 103 | } 104 | } 105 | 106 | return nil 107 | 108 | } 109 | 110 | // 分割 GORM 标签 111 | func splitGormTag(tag string) map[string]string { 112 | tags := strings.Split(tag, ";") 113 | 114 | parts := make(map[string]string, 0) 115 | 116 | for _, part := range tags { 117 | kv := strings.SplitN(part, ":", 2) 118 | if len(kv) == 2 { 119 | parts[kv[0]] = kv[1] 120 | } 121 | } 122 | 123 | return parts 124 | } 125 | -------------------------------------------------------------------------------- /internal/routers/api_router/user.go: -------------------------------------------------------------------------------- 1 | package apiRouter 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/global" 5 | "github.com/haierkeys/custom-image-gateway/internal/service" 6 | "github.com/haierkeys/custom-image-gateway/pkg/app" 7 | "github.com/haierkeys/custom-image-gateway/pkg/code" 8 | 9 | "github.com/gin-gonic/gin" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | type User struct { 14 | } 15 | 16 | func NewUser() *User { 17 | return &User{} 18 | } 19 | 20 | // Register 用户注册 21 | func (h *User) Register(c *gin.Context) { 22 | response := app.NewResponse(c) 23 | params := &service.UserCreateRequest{} 24 | 25 | valid, errs := app.BindAndValid(c, params) 26 | 27 | if !valid { 28 | global.Logger.Error("apiRouter.user.Register.BindAndValid errs: %v", zap.Error(errs)) 29 | response.ToResponse(code.ErrorInvalidParams.WithDetails(errs.ErrorsToString()).WithData(errs.MapsToString())) 30 | return 31 | } 32 | 33 | svc := service.New(c) 34 | svcData, err := svc.UserRegister(params) 35 | 36 | if err != nil { 37 | global.Logger.Error("apiRouter.user.Register svc UserRegister err: %v", zap.Error(err)) 38 | response.ToResponse(code.ErrorUserRegister.WithDetails(err.Error())) 39 | return 40 | } 41 | 42 | response.ToResponse(code.Success.WithData(svcData)) 43 | } 44 | 45 | // Login 用户登录 46 | func (h *User) Login(c *gin.Context) { 47 | 48 | params := &service.UserLoginRequest{} 49 | response := app.NewResponse(c) 50 | 51 | valid, errs := app.BindAndValid(c, params) 52 | 53 | if !valid { 54 | global.Logger.Error("apiRouter.user.Login.BindAndValid errs: %v", zap.Error(errs)) 55 | response.ToResponse(code.ErrorInvalidParams.WithDetails(errs.ErrorsToString()).WithData(errs.MapsToString())) 56 | return 57 | } 58 | 59 | svc := service.New(c) 60 | svcData, err := svc.UserLogin(params) 61 | 62 | if err != nil { 63 | 64 | global.Logger.Error("apiRouter.user.Login svc UserLogin err: %v", zap.Error(err)) 65 | response.ToResponse(code.ErrorUserLoginFailed.WithDetails(err.Error())) 66 | return 67 | } 68 | 69 | response.ToResponse(code.Success.WithData(svcData)) 70 | } 71 | 72 | func (h *User) UserChangePassword(c *gin.Context) { 73 | params := &service.UserChangePasswordRequest{} 74 | response := app.NewResponse(c) 75 | valid, errs := app.BindAndValid(c, params) 76 | if !valid { 77 | global.Logger.Error("apiRouter.UserChangePassword.UserChangePassword.BindAndValid errs: %v", zap.Error(errs)) 78 | response.ToResponse(code.ErrorInvalidParams.WithDetails(errs.ErrorsToString()).WithData(errs.MapsToString())) 79 | return 80 | } 81 | uid := app.GetUID(c) 82 | if uid == 0 { 83 | global.Logger.Error("apiRouter.UserChangePassword.UserChangePassword err uid=0") 84 | response.ToResponse(code.ErrorNotUserAuthToken) 85 | return 86 | } 87 | svc := service.New(c) 88 | err := svc.UserChangePassword(uid, params) 89 | if err != nil { 90 | global.Logger.Error("apiRouter.UserChangePassword.UserChangePassword svc UserChangePassword err: %v", zap.Error(err)) 91 | response.ToResponse(code.Failed.WithDetails(err.Error())) 92 | return 93 | } 94 | response.ToResponse(code.SuccessPasswordUpdate) 95 | } 96 | -------------------------------------------------------------------------------- /internal/dao/dao_user.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/internal/model" 5 | "github.com/haierkeys/custom-image-gateway/internal/query" 6 | "github.com/haierkeys/custom-image-gateway/pkg/convert" 7 | "github.com/haierkeys/custom-image-gateway/pkg/timex" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type User struct { 12 | UID int64 `gorm:"column:uid;primaryKey" json:"uid" type:"uid" form:"uid"` 13 | Email string `gorm:"column:email" json:"email" type:"email" form:"email"` 14 | Username string `gorm:"column:username" json:"username" type:"username" form:"username"` 15 | Password string `gorm:"column:password" json:"password" type:"password" form:"password"` 16 | Salt string `gorm:"column:salt" json:"salt" type:"salt" form:"salt"` 17 | Token string `gorm:"column:token" json:"token" type:"token" form:"token"` 18 | Avatar string `gorm:"column:avatar" json:"avatar" type:"avatar" form:"avatar"` 19 | IsDeleted int64 `gorm:"column:is_deleted" json:"isDeleted" type:"isDeleted" form:"isDeleted"` 20 | UpdatedAt timex.Time `gorm:"column:updated_at;type:datetime;autoUpdateTime:false" json:"updatedAt" type:"updatedAt" form:"updatedAt"` 21 | CreatedAt timex.Time `gorm:"column:created_at;type:datetime;autoUpdateTime:false" json:"createdAt" type:"createdAt" form:"createdAt"` 22 | DeletedAt timex.Time `gorm:"column:deleted_at;type:datetime;autoUpdateTime:false" json:"deletedAt" type:"deletedAt" form:"deletedAt"` 23 | } 24 | 25 | func (d *Dao) user() *query.Query { 26 | return d.Use( 27 | func(g *gorm.DB) { 28 | model.AutoMigrate(g, "User") 29 | }, 30 | ) 31 | } 32 | 33 | // GetUserByUID 根据用户ID获取用户信息 34 | func (d *Dao) GetUserByUID(uid int64) (*User, error) { 35 | u := d.user().User 36 | m, err := u.WithContext(d.ctx).Where(u.UID.Eq(uid), u.IsDeleted.Eq(0)).First() 37 | // 如果发生错误,返回 nil 和错误 38 | if err != nil { 39 | return nil, err 40 | } 41 | // 将查询结果转换为 User 结构体,并返回 42 | return convert.StructAssign(m, &User{}).(*User), nil 43 | } 44 | 45 | // GetUserByEmail 根据电子邮件获取用户信息 46 | func (d *Dao) GetUserByEmail(email string) (*User, error) { 47 | u := d.user().User 48 | m, err := u.WithContext(d.ctx).Where(u.Email.Eq(email), u.IsDeleted.Eq(0)).First() 49 | if err != nil { 50 | return nil, err 51 | } 52 | return convert.StructAssign(m, &User{}).(*User), nil 53 | } 54 | 55 | // GetUserByUsername 根据用户名获取用户信息 56 | 57 | func (d *Dao) GetUserByUsername(username string) (*User, error) { 58 | u := d.user().User 59 | m, err := u.WithContext(d.ctx).Where(u.Username.Eq(username), u.IsDeleted.Eq(0)).First() 60 | if err != nil { 61 | return nil, err 62 | } 63 | return convert.StructAssign(m, &User{}).(*User), nil 64 | } 65 | 66 | // CreateUser 创建用户 67 | func (d *Dao) CreateUser(dao *User) (*User, error) { // 修改函数名为 CreateUser 68 | m := convert.StructAssign(dao, &model.User{}).(*model.User) 69 | u := d.user().User 70 | err := u.WithContext(d.ctx).Create(m) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return convert.StructAssign(m, &User{}).(*User), nil 75 | } 76 | 77 | // UpdateUser 更新用户密码 78 | func (d *Dao) UserUpdatePassword(password string, uid int64) error { 79 | u := d.user().User 80 | 81 | _, err := u.WithContext(d.ctx).Where( 82 | u.UID.Eq(uid), 83 | ).UpdateSimple( 84 | u.Password.Value(password), 85 | u.UpdatedAt.Value(timex.Now()), 86 | ) 87 | return err 88 | } 89 | -------------------------------------------------------------------------------- /cmd/old_gen/gorm_gen/pkg/parser.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "go/ast" 5 | "go/build" 6 | "go/parser" 7 | "go/token" 8 | "log" 9 | "strings" 10 | 11 | "gorm.io/gorm" 12 | ) 13 | 14 | // The Parser is used to parse a directory and expose information about the structs defined in the files of this directory. 15 | type Parser struct { 16 | dir string 17 | pkg *build.Package 18 | parsedFiles []*ast.File 19 | } 20 | 21 | // NewParser create a new parser instance. 22 | func NewParser(dir string) *Parser { 23 | return &Parser{ 24 | dir: dir, 25 | } 26 | } 27 | 28 | // getPackage parse dir get go file and package 29 | func (p *Parser) getPackage() { 30 | pkg, err := build.Default.ImportDir(p.dir, build.ImportComment) 31 | if err != nil { 32 | log.Fatalf("cannot process directory %s: %s", p.dir, err) 33 | } 34 | p.pkg = pkg 35 | 36 | } 37 | 38 | // parseGoFiles parse go file 39 | func (p *Parser) parseGoFiles() { 40 | var parsedFiles []*ast.File 41 | fs := token.NewFileSet() 42 | for _, file := range p.pkg.GoFiles { 43 | file = p.dir + "/" + file 44 | parsedFile, err := parser.ParseFile(fs, file, nil, 0) 45 | if err != nil { 46 | log.Fatalf("parsing package: %s: %s\n", file, err) 47 | } 48 | parsedFiles = append(parsedFiles, parsedFile) 49 | } 50 | p.parsedFiles = parsedFiles 51 | } 52 | 53 | // parseTypes parse type of struct 54 | func (p *Parser) parseTypes(file *ast.File) (ret []structConfig) { 55 | ast.Inspect(file, func(n ast.Node) bool { 56 | decl, ok := n.(*ast.GenDecl) 57 | if !ok || decl.Tok != token.TYPE { 58 | return true 59 | } 60 | 61 | for _, spec := range decl.Specs { 62 | var ( 63 | data structConfig 64 | ) 65 | typeSpec, _ok := spec.(*ast.TypeSpec) 66 | if !_ok { 67 | continue 68 | } 69 | // We only care about struct declaration (for now) 70 | var structType *ast.StructType 71 | if structType, ok = typeSpec.Type.(*ast.StructType); !ok { 72 | continue 73 | } 74 | 75 | data.StructName = typeSpec.Name.Name 76 | for _, v := range structType.Fields.List { 77 | var ( 78 | optionField fieldConfig 79 | ) 80 | 81 | if t, _ok := v.Type.(*ast.Ident); _ok { 82 | optionField.FieldType = t.String() 83 | } else { 84 | if v.Tag != nil { 85 | if strings.Contains(v.Tag.Value, "gorm") && strings.Contains(v.Tag.Value, "time") { 86 | optionField.FieldType = "time.Time" 87 | } 88 | } 89 | } 90 | 91 | if len(v.Names) > 0 { 92 | optionField.FieldName = v.Names[0].String() 93 | optionField.ColumnName = gorm.ToDBName(optionField.FieldName) 94 | optionField.HumpName = SQLColumnToHumpStyle(optionField.ColumnName) 95 | } 96 | 97 | data.OptionFields = append(data.OptionFields, optionField) 98 | } 99 | 100 | ret = append(ret, data) 101 | } 102 | return true 103 | }) 104 | return 105 | } 106 | 107 | // Parse should be called before any type querying for the parser. It takes the directory to be parsed and extracts all the structs defined in this directory. 108 | func (p *Parser) Parse() (ret []structConfig) { 109 | var ( 110 | data []structConfig 111 | ) 112 | p.getPackage() 113 | p.parseGoFiles() 114 | for _, f := range p.parsedFiles { 115 | data = append(data, p.parseTypes(f)...) 116 | } 117 | return data 118 | } 119 | -------------------------------------------------------------------------------- /pkg/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/haierkeys/custom-image-gateway/global" 7 | "github.com/haierkeys/custom-image-gateway/pkg/code" 8 | "github.com/haierkeys/custom-image-gateway/pkg/storage/aliyun_oss" 9 | "github.com/haierkeys/custom-image-gateway/pkg/storage/aws_s3" 10 | "github.com/haierkeys/custom-image-gateway/pkg/storage/cloudflare_r2" 11 | "github.com/haierkeys/custom-image-gateway/pkg/storage/local_fs" 12 | "github.com/haierkeys/custom-image-gateway/pkg/storage/minio" 13 | "github.com/haierkeys/custom-image-gateway/pkg/storage/webdav" 14 | ) 15 | 16 | type Type = string 17 | type CloudType = Type 18 | 19 | const OSS CloudType = "oss" 20 | const R2 CloudType = "r2" 21 | const S3 CloudType = "s3" 22 | const LOCAL Type = "localfs" 23 | const MinIO CloudType = "minio" 24 | const WebDAV CloudType = "webdav" 25 | 26 | var StorageTypeMap = map[Type]bool{ 27 | OSS: true, 28 | R2: true, 29 | S3: true, 30 | LOCAL: true, 31 | MinIO: true, 32 | WebDAV: true, 33 | } 34 | 35 | var CloudStorageTypeMap = map[Type]bool{ 36 | OSS: true, 37 | R2: true, 38 | S3: true, 39 | MinIO: true, 40 | } 41 | 42 | type Storager interface { 43 | SendFile(pathKey string, file io.Reader, cType string) (string, error) 44 | SendContent(pathKey string, content []byte) (string, error) 45 | } 46 | 47 | var Instance map[Type]Storager 48 | 49 | func NewClient(cType Type, config map[string]any) (Storager, error) { 50 | 51 | if cType == LOCAL { 52 | return local_fs.NewClient(config) 53 | } else if cType == OSS { 54 | return aliyun_oss.NewClient(config) 55 | } else if cType == R2 { 56 | return cloudflare_r2.NewClient(config) 57 | } else if cType == S3 { 58 | return aws_s3.NewClient(config) 59 | } else if cType == MinIO { 60 | return minio.NewClient(config) 61 | } else if cType == WebDAV { 62 | return webdav.NewClient(config) 63 | } 64 | return nil, code.ErrorInvalidStorageType 65 | } 66 | 67 | func IsUserEnabled(cType Type) error { 68 | 69 | // 检查云存储类型是否有效 70 | if !StorageTypeMap[cType] { 71 | return code.ErrorInvalidCloudStorageType 72 | } 73 | 74 | if cType == LOCAL && !global.Config.LocalFS.IsUserEnabled { 75 | return code.ErrorUserLocalFSDisabled 76 | } else if cType == OSS && !global.Config.OSS.IsUserEnabled { 77 | return code.ErrorUserALIOSSDisabled 78 | } else if cType == R2 && !global.Config.CloudflueR2.IsUserEnabled { 79 | return code.ErrorUserCloudflueR2Disabled 80 | } else if cType == S3 && !global.Config.AWSS3.IsUserEnabled { 81 | return code.ErrorUserAWSS3Disabled 82 | } else if cType == MinIO && !global.Config.MinIO.IsUserEnabled { 83 | return code.ErrorUserMinIODisabled 84 | } 85 | return nil 86 | } 87 | 88 | func GetIsUserEnabledStorageTypes() []CloudType { 89 | 90 | var list []CloudType 91 | if global.Config.CloudflueR2.IsUserEnabled { 92 | list = append(list, R2) 93 | } 94 | if global.Config.OSS.IsUserEnabled { 95 | list = append(list, OSS) 96 | } 97 | if global.Config.AWSS3.IsUserEnabled { 98 | list = append(list, S3) 99 | } 100 | if global.Config.MinIO.IsUserEnabled { 101 | list = append(list, MinIO) 102 | } 103 | if global.Config.LocalFS.IsUserEnabled { 104 | list = append(list, LOCAL) 105 | } 106 | if global.Config.WebDAV.IsUserEnabled { 107 | list = append(list, WebDAV) 108 | } 109 | return list 110 | } 111 | -------------------------------------------------------------------------------- /pkg/logger/logger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2020-2022, IrineSistiana 3 | * 4 | * This file is part of mosdns. 5 | * 6 | * mosdns is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * mosdns is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with this program. If not, see . 18 | */ 19 | 20 | package logger 21 | 22 | import ( 23 | "fmt" 24 | "os" 25 | 26 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 27 | 28 | "go.uber.org/zap" 29 | "go.uber.org/zap/zapcore" 30 | ) 31 | 32 | type Config struct { 33 | // Level, See also zapcore.ParseLevel. 34 | Level string `yaml:"level"` 35 | 36 | // File that logger will be writen into. 37 | // Default is stderr. 38 | File string `yaml:"file"` 39 | 40 | // Production enables json output. 41 | Production bool `yaml:"production"` 42 | } 43 | 44 | var ( 45 | stderr = zapcore.Lock(os.Stderr) 46 | lvl = zap.NewAtomicLevelAt(zap.InfoLevel) 47 | l = zap.New(zapcore.NewCore(zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), stderr, lvl)) 48 | s = l.Sugar() 49 | 50 | nop = zap.NewNop() 51 | ) 52 | 53 | func NewLogger(lc Config) (*zap.Logger, error) { 54 | 55 | if !fileurl.IsExist(lc.File) { 56 | fileurl.CreatePath(lc.File, os.ModePerm) 57 | } 58 | 59 | lvl, err := zapcore.ParseLevel(lc.Level) 60 | if err != nil { 61 | return nil, fmt.Errorf("invalid log level: %w", err) 62 | } 63 | 64 | var fileOut zapcore.WriteSyncer 65 | if lf := lc.File; len(lf) > 0 { 66 | f, _, err := zap.Open(lf) 67 | if err != nil { 68 | return nil, fmt.Errorf("open log file: %w", err) 69 | } 70 | fileOut = zapcore.Lock(f) 71 | 72 | var fileEncoder zapcore.Encoder 73 | if lc.Production { 74 | fileEncoder = zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) 75 | } else { 76 | fileEncoder = zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) 77 | } 78 | 79 | consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) 80 | 81 | consoleCore := zapcore.NewCore(consoleEncoder, zapcore.NewMultiWriteSyncer(zapcore.AddSync(stderr)), lvl) 82 | fileCore := zapcore.NewCore(fileEncoder, zapcore.NewMultiWriteSyncer(zapcore.AddSync(fileOut)), lvl) 83 | 84 | // 使用 zapcore.NewTee 合并两个 Core 85 | return zap.New(zapcore.NewTee(consoleCore, fileCore)), nil 86 | 87 | } else { 88 | return zap.New(zapcore.NewCore(zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), stderr, lvl)), nil 89 | } 90 | } 91 | 92 | // L is a global logger. 93 | func L() *zap.Logger { 94 | return l 95 | } 96 | 97 | // SetLevel sets the log level for the global logger. 98 | func SetLevel(l zapcore.Level) { 99 | lvl.SetLevel(l) 100 | } 101 | 102 | // S is a global logger. 103 | func S() *zap.SugaredLogger { 104 | return s 105 | } 106 | 107 | // Nop is a logger that never writes out logs. 108 | func Nop() *zap.Logger { 109 | return nop 110 | } 111 | -------------------------------------------------------------------------------- /pkg/app/token.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/haierkeys/custom-image-gateway/global" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/golang-jwt/jwt/v5" 11 | ) 12 | 13 | // UserEntity represents the user data stored in the JWT. 14 | type UserEntity struct { 15 | UID int64 `json:"uid"` 16 | Nickname string `json:"nickname"` 17 | IP string `json:"ip"` 18 | jwt.RegisteredClaims 19 | } 20 | 21 | // ParseToken parses a JWT token and returns the user data. 22 | func ParseToken(tokenString string) (*UserEntity, error) { 23 | // Initialize a new instance of `Claims` 24 | claims := &UserEntity{} 25 | 26 | // Parse the JWT string and store the result in `claims`. 27 | // Note that we are passing the key in this method as well. This method will return an error 28 | // if the token is invalid (if it has expired according to the expiry time we set, or if the signature does not match). 29 | token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 30 | // Don't forget to validate the alg is what you expect: 31 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 32 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 33 | } 34 | return []byte(global.Config.Security.AuthTokenKey), nil 35 | }) 36 | 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if !token.Valid { 42 | return nil, fmt.Errorf("invalid token") 43 | } 44 | 45 | return claims, nil 46 | } 47 | 48 | // GenerateToken generates a new JWT token for a user. 49 | func GenerateToken(uid int64, nickname string, ip string, expiry int64) (string, error) { 50 | // Create the Claims 51 | expirationTime := time.Now().Add(time.Duration(expiry) * time.Second).Unix() 52 | claims := &UserEntity{ 53 | UID: uid, 54 | Nickname: nickname, 55 | IP: ip, 56 | RegisteredClaims: jwt.RegisteredClaims{ 57 | // In JWT, the expiry time is expressed as unix milliseconds 58 | ExpiresAt: jwt.NewNumericDate(time.Unix(expirationTime, 0)), 59 | IssuedAt: jwt.NewNumericDate(time.Now()), 60 | NotBefore: jwt.NewNumericDate(time.Now()), 61 | Issuer: global.Name, 62 | Subject: "user-token", 63 | ID: fmt.Sprintf("%d", uid), // Use UID as unique token ID 64 | }, 65 | } 66 | // Declare the token with the algorithm used for signing, and the claims 67 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 68 | // Create the JWT string 69 | tokenString, err := token.SignedString([]byte(global.Config.Security.AuthTokenKey)) 70 | if err != nil { 71 | return "", err 72 | } 73 | 74 | return tokenString, nil 75 | } 76 | 77 | // GetUID extracts the user ID from the request context. 78 | func GetUID(ctx *gin.Context) (out int64) { 79 | user, exist := ctx.Get("user_token") 80 | if exist { 81 | if userEntity, ok := user.(*UserEntity); ok { 82 | out = userEntity.UID 83 | } 84 | } 85 | return 86 | } 87 | 88 | // GetIP extracts the user IP from the request context. 89 | func GetIP(ctx *gin.Context) (out string) { 90 | user, exist := ctx.Get("user_token") 91 | if exist { 92 | if userEntity, ok := user.(*UserEntity); ok { 93 | out = userEntity.IP 94 | } 95 | } 96 | return 97 | } 98 | 99 | // SetTokenToContext set token to gin.Context 100 | func SetTokenToContext(ctx *gin.Context, tokenString string) error { 101 | user, err := ParseToken(tokenString) 102 | if err != nil { 103 | return err 104 | } 105 | ctx.Set("user_token", user) 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /internal/routers/api_router/cloud_config.go: -------------------------------------------------------------------------------- 1 | package apiRouter 2 | 3 | import ( 4 | "github.com/haierkeys/custom-image-gateway/global" 5 | "github.com/haierkeys/custom-image-gateway/internal/service" 6 | "github.com/haierkeys/custom-image-gateway/pkg/app" 7 | "github.com/haierkeys/custom-image-gateway/pkg/code" 8 | 9 | "github.com/gin-gonic/gin" 10 | "go.uber.org/zap" 11 | ) 12 | 13 | type CloudConfig struct { 14 | } 15 | 16 | func NewCloudConfig() *CloudConfig { 17 | return &CloudConfig{} 18 | } 19 | 20 | func (t *CloudConfig) EnabledTypes(c *gin.Context) { 21 | response := app.NewResponse(c) 22 | uid := app.GetUID(c) 23 | if uid == 0 { 24 | global.Logger.Error("apiRouter.CloudConfig.Types err uid=0") 25 | response.ToResponse(code.ErrorNotUserAuthToken) 26 | return 27 | } 28 | svc := service.New(c) 29 | list, err := svc.CloudTypeEnabledList() 30 | if err != nil { 31 | global.Logger.Error("apiRouter.CloudConfig.Types svc CloudTypeList err: %v", zap.Error(err)) 32 | response.ToResponse(code.Failed.WithDetails(err.Error())) 33 | return 34 | } 35 | response.ToResponse(code.Success.WithData(list)) 36 | } 37 | 38 | func (t *CloudConfig) UpdateAndCreate(c *gin.Context) { 39 | params := &service.CloudConfigRequest{} 40 | response := app.NewResponse(c) 41 | valid, errs := app.BindAndValid(c, params) 42 | if !valid { 43 | global.Logger.Error("apiRouter.CloudConfig.UpdateAndCreate.BindAndValid errs: %v", zap.Error(errs)) 44 | response.ToResponse(code.ErrorInvalidParams.WithDetails(errs.ErrorsToString()).WithData(errs.MapsToString())) 45 | return 46 | } 47 | uid := app.GetUID(c) 48 | if uid == 0 { 49 | global.Logger.Error("apiRouter.CloudConfig.UpdateAndCreate err uid=0") 50 | response.ToResponse(code.ErrorNotUserAuthToken) 51 | return 52 | } 53 | svc := service.New(c) 54 | id, err := svc.CloudConfigUpdateAndCreate(uid, params) 55 | if err != nil { 56 | global.Logger.Error("apiRouter.CloudConfig.UpdateAndCreate svc UpdateAndCreate err: %v", zap.Error(err)) 57 | response.ToResponse(code.Failed.WithDetails(err.Error())) 58 | return 59 | } 60 | if params.ID == 0 { 61 | response.ToResponse(code.SuccessCreate.WithData(id)) 62 | } else { 63 | response.ToResponse(code.SuccessUpdate.WithData(id)) 64 | } 65 | } 66 | 67 | func (t *CloudConfig) Delete(c *gin.Context) { 68 | param := service.DeleteCloudConfigRequest{} 69 | response := app.NewResponse(c) 70 | valid, errs := app.BindAndValid(c, ¶m) 71 | if !valid { 72 | global.Logger.Error("apiRouter.CloudConfig.Delete.BindAndValid svc Delete err: %v", zap.Error(errs)) 73 | response.ToResponse(code.ErrorInvalidParams.WithDetails(errs.ErrorsToString()).WithData(errs.MapsToString())) 74 | return 75 | } 76 | uid := app.GetUID(c) 77 | if uid == 0 { 78 | global.Logger.Error("apiRouter.CloudConfig.Delete err uid=0") 79 | response.ToResponse(code.ErrorNotUserAuthToken) 80 | return 81 | } 82 | svc := service.New(c) 83 | err := svc.CloudConfigDelete(uid, ¶m) 84 | if err != nil { 85 | global.Logger.Error("apiRouter.CloudConfig.Delete svc Delete err: %v", zap.Error(err)) 86 | response.ToResponse(code.Failed.WithDetails(err.Error())) 87 | return 88 | } 89 | response.ToResponse(code.SuccessDelete) 90 | } 91 | 92 | func (t *CloudConfig) List(c *gin.Context) { 93 | response := app.NewResponse(c) 94 | uid := app.GetUID(c) 95 | if uid == 0 { 96 | global.Logger.Error("apiRouter.CloudConfig.List err uid=0") 97 | response.ToResponse(code.ErrorNotUserAuthToken) 98 | return 99 | } 100 | svc := service.New(c) 101 | list, total, err := svc.CloudConfigList(uid, &app.Pager{Page: 1, PageSize: 10}) 102 | if err != nil { 103 | response.ToResponse(code.Failed.WithDetails(err.Error())) 104 | return 105 | } 106 | response.ToResponseList(code.Success, list, total) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | "github.com/haierkeys/custom-image-gateway/pkg/code" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | type Response struct { 13 | Ctx *gin.Context 14 | } 15 | 16 | type Pager struct { 17 | // 页码 18 | Page int `json:"page"` 19 | // 每页数量 20 | PageSize int `json:"pageSize"` 21 | // 总行数 22 | TotalRows int `json:"totalRows"` 23 | } 24 | type ListRes struct { 25 | // 数据清单 26 | List interface{} `json:"list"` 27 | // 翻页信息 28 | Pager Pager `json:"pager"` 29 | } 30 | 31 | type ResResult struct { 32 | // 业务状态码 33 | Code int `json:"code"` 34 | // 状态 35 | Status bool `json:"status"` 36 | // 失败&&成功消息 37 | Msg interface{} `json:"message"` 38 | // 数据集合 39 | Data interface{} `json:"data"` 40 | } 41 | 42 | type ResDetailsResult struct { 43 | // 业务状态码 44 | Code int `json:"code"` 45 | // 状态 46 | Status bool `json:"status"` 47 | // 失败&&成功消息 48 | Msg interface{} `json:"message"` 49 | // 错误格式数据 50 | Data interface{} `json:"data"` 51 | // 错误支付 52 | Details interface{} `json:"details"` 53 | } 54 | 55 | type ResListResult struct { 56 | // 业务状态码 57 | Code int `json:"code"` 58 | // 状态 59 | Status bool `json:"status"` 60 | // 失败&&成功消息 61 | Msg interface{} `json:"message"` 62 | // 数据集合 63 | Data ListRes `json:"data"` 64 | } 65 | 66 | func NewResponse(ctx *gin.Context) *Response { 67 | return &Response{ 68 | Ctx: ctx, 69 | } 70 | } 71 | 72 | // RequestParamStrParse 解析 73 | func RequestParamStrParse(c *gin.Context, param any) { 74 | tParam := reflect.TypeOf(param).Elem() 75 | vParam := reflect.ValueOf(param).Elem() 76 | for i := 0; i < tParam.NumField(); i++ { 77 | name := tParam.Field(i).Name 78 | if nameType, ok := tParam.FieldByName(name); ok { 79 | dstName := nameType.Tag.Get("request") 80 | if dstName != "" { 81 | paramName := nameType.Tag.Get("form") 82 | if value, ok := c.GetQuery(paramName); ok { 83 | vParam.FieldByName(dstName).SetString(value) 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | // GetRequestIP 获取ip 91 | func GetRequestIP(c *gin.Context) string { 92 | reqIP := c.ClientIP() 93 | if reqIP == "::1" { 94 | reqIP = "127.0.0.1" 95 | } 96 | return reqIP 97 | } 98 | 99 | func GetAccessHost(c *gin.Context) string { 100 | AccessProto := "" 101 | if proto := c.Request.Header.Get("X-Forwarded-Proto"); proto == "" { 102 | AccessProto = "http" + "://" 103 | } else { 104 | AccessProto = proto + "://" 105 | } 106 | return AccessProto + c.Request.Host 107 | } 108 | 109 | // ToResponse 输出到浏览器 110 | func (r *Response) ToResponse(code *code.Code) { 111 | 112 | r.Ctx.Set("status_code", code.StatusCode()) 113 | if code.HaveDetails() { 114 | details := strings.Join(code.Details(), ",") 115 | r.SendResultResponse(code.StatusCode(), ResDetailsResult{ 116 | Code: code.Code(), 117 | Status: code.Status(), 118 | Msg: code.Lang.GetMessage(), 119 | Data: code.Data(), 120 | Details: details, 121 | }) 122 | } else { 123 | r.SendResultResponse(code.StatusCode(), ResResult{ 124 | Code: code.Code(), 125 | Status: code.Status(), 126 | Msg: code.Lang.GetMessage(), 127 | Data: code.Data(), 128 | }) 129 | } 130 | } 131 | 132 | func (r *Response) ToResponseList(code *code.Code, list interface{}, totalRows int) { 133 | 134 | r.Ctx.Set("status_code", code.StatusCode()) 135 | 136 | r.SendResultResponse(code.StatusCode(), ResListResult{ 137 | Code: code.Code(), 138 | Status: code.Status(), 139 | Msg: code.Lang.GetMessage(), 140 | Data: ListRes{ 141 | List: list, 142 | Pager: Pager{ 143 | Page: GetPage(r.Ctx), 144 | PageSize: GetPageSize(r.Ctx), 145 | TotalRows: totalRows, 146 | }, 147 | }, 148 | }) 149 | } 150 | 151 | func (r *Response) SendResultResponse(statusCode int, content interface{}) { 152 | r.Ctx.JSON(statusCode, content) 153 | } 154 | -------------------------------------------------------------------------------- /cmd/old_gen/gorm_gen/pkg/generator.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "go/format" 8 | "io/ioutil" 9 | "log" 10 | "strings" 11 | 12 | "gorm.io/gorm" 13 | ) 14 | 15 | // fieldConfig 16 | type fieldConfig struct { 17 | FieldName string 18 | ColumnName string 19 | FieldType string 20 | HumpName string 21 | } 22 | 23 | // structConfig 24 | type structConfig struct { 25 | config 26 | StructName string 27 | OnlyFields []fieldConfig 28 | OptionFields []fieldConfig 29 | } 30 | 31 | type ImportPkg struct { 32 | Pkg string 33 | } 34 | 35 | type structHelpers struct { 36 | Titelize func(string) string 37 | } 38 | 39 | type config struct { 40 | PkgName string 41 | Helpers structHelpers 42 | QueryBuilderName string 43 | PrimaryIdName string 44 | Prefix string 45 | } 46 | 47 | // The Generator is the one responsible for generating the code, adding the imports, formating, and writing it to the file. 48 | type Generator struct { 49 | buf map[string]*bytes.Buffer 50 | inputFile string 51 | config config 52 | structConfigs []structConfig 53 | } 54 | 55 | // NewGenerator function creates an instance of the generator given the name of the output file as an argument. 56 | func NewGenerator(outputFile string) *Generator { 57 | return &Generator{ 58 | buf: map[string]*bytes.Buffer{}, 59 | inputFile: outputFile, 60 | } 61 | } 62 | 63 | // ParserAST parse by go file 64 | func (g *Generator) ParserAST(p *Parser, structs []string, prefix string) (ret *Generator) { 65 | for _, v := range structs { 66 | g.buf[gorm.ToDBName(v)] = new(bytes.Buffer) 67 | } 68 | g.structConfigs = p.Parse() 69 | g.config.PkgName = p.pkg.Name 70 | g.config.Helpers = structHelpers{ 71 | Titelize: strings.Title, 72 | } 73 | g.config.QueryBuilderName = SQLColumnToHumpStyle(p.pkg.Name) + "QueryBuilder" 74 | 75 | //g.config.PrimaryIdName = g.structConfigs[0].OptionFields[0].FieldName 76 | 77 | for _, one := range g.structConfigs { 78 | if !strings.HasSuffix(one.StructName, "RepoQueryBuilder") { 79 | g.config.PrimaryIdName = one.OptionFields[0].FieldName 80 | break 81 | } 82 | } 83 | 84 | g.config.Prefix = prefix 85 | 86 | return g 87 | } 88 | 89 | func (g *Generator) checkConfig() (err error) { 90 | if len(g.config.PkgName) == 0 { 91 | err = errors.New("package name dose'n set") 92 | return 93 | } 94 | for i := 0; i < len(g.structConfigs); i++ { 95 | g.structConfigs[i].config = g.config 96 | } 97 | return 98 | } 99 | 100 | // Generate executes the template and store it in an internal buffer. 101 | func (g *Generator) Generate() *Generator { 102 | if err := g.checkConfig(); err != nil { 103 | panic(err) 104 | } 105 | 106 | for _, v := range g.structConfigs { 107 | if _, ok := g.buf[gorm.ToDBName(v.StructName)]; !ok { 108 | continue 109 | } 110 | if err := outputTemplate.Execute(g.buf[gorm.ToDBName(v.StructName)], v); err != nil { 111 | panic(err) 112 | } 113 | } 114 | 115 | return g 116 | } 117 | 118 | // Format function formats the output of the generation. 119 | func (g *Generator) Format() *Generator { 120 | for k := range g.buf { 121 | formattedOutput, err := format.Source(g.buf[k].Bytes()) 122 | if err != nil { 123 | panic(err) 124 | } 125 | g.buf[k] = bytes.NewBuffer(formattedOutput) 126 | } 127 | return g 128 | } 129 | 130 | // Flush function writes the output to the output file. 131 | func (g *Generator) Flush() error { 132 | 133 | for k := range g.buf { 134 | //filename := g.inputFile + "/query_builder_" + strings.ToLower(k) + "_query_builder.go" 135 | filename := g.inputFile + "/query.go" 136 | if err := ioutil.WriteFile(filename, g.buf[k].Bytes(), 0777); err != nil { 137 | log.Fatalln(err) 138 | } 139 | fmt.Println(" └── file : ", fmt.Sprintf("%s_repo/gen_%s.go", strings.ToLower(k), strings.ToLower(k))) 140 | } 141 | return nil 142 | } 143 | -------------------------------------------------------------------------------- /db.sql: -------------------------------------------------------------------------------- 1 | 2 | # sqlite3 3 | 4 | PRAGMA foreign_keys = false; 5 | 6 | -- ---------------------------- 7 | -- Table structure for pre_user 8 | -- ---------------------------- 9 | DROP TABLE IF EXISTS "user"; 10 | CREATE TABLE "user" ( 11 | "uid" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 12 | "email" text DEFAULT '', 13 | "username" text DEFAULT '', 14 | "password" text DEFAULT '', 15 | "salt" text DEFAULT '', 16 | "token" text NOT NULL DEFAULT '', 17 | "avatar" text NOT NULL DEFAULT '', 18 | "is_deleted" integer NOT NULL DEFAULT 0, 19 | "updated_at" datetime DEFAULT NULL, 20 | "created_at" datetime DEFAULT NULL, 21 | "deleted_at" datetime DEFAULT NULL, 22 | UNIQUE ("email" ASC) 23 | ); 24 | CREATE UNIQUE INDEX "idx_user_email" ON "user" ("email" ASC); 25 | 26 | DROP TABLE IF EXISTS "cloud_config"; 27 | CREATE TABLE "cloud_config" ( 28 | "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, 29 | "uid" integer NOT NULL DEFAULT 0, 30 | "type" text DEFAULT '', 31 | "endpoint" text DEFAULT '', 32 | "region" text DEFAULT '', 33 | "account_id" text DEFAULT '', 34 | "bucket_name" text DEFAULT '', 35 | "access_key_id" text DEFAULT '', 36 | "access_key_secret" text DEFAULT '', 37 | "custom_path" text DEFAULT '', 38 | "access_url_prefix" text DEFAULT '', 39 | "user" text DEFAULT '', 40 | "password" text DEFAULT '', 41 | "is_enabled" integer NOT NULL DEFAULT 1, 42 | "is_deleted" integer NOT NULL DEFAULT 0, 43 | "created_at" datetime DEFAULT NULL, 44 | "updated_at" datetime DEFAULT NULL, 45 | "deleted_at" datetime DEFAULT NULL 46 | ); 47 | CREATE INDEX "idx_cloud_config_uid" ON "cloud_config" ("uid" ASC); 48 | 49 | 50 | PRAGMA foreign_keys = true; 51 | 52 | 53 | ## mysql 54 | DROP TABLE IF EXISTS `pre_user`; 55 | CREATE TABLE `pre_user` ( 56 | `uid` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户id', 57 | `email` char(255) NOT NULL DEFAULT '' COMMENT '邮箱地址', 58 | `username` char(255) NOT NULL DEFAULT '' COMMENT '用户名', 59 | `password` char(32) NOT NULL DEFAULT '' COMMENT '密码', 60 | `salt` char(24) NOT NULL DEFAULT '' COMMENT '密码混淆码', 61 | `token` char(255) NOT NULL DEFAULT '' COMMENT '用户授权令牌', 62 | `avatar` char(255) NOT NULL DEFAULT '' COMMENT '用户头像路径', 63 | `is_deleted` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否删除', 64 | `updated_at` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间', 65 | `created_at` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间', 66 | `deleted_at` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '标记删除时间', 67 | PRIMARY KEY (`uid`) , 68 | UNIQUE INDEX `email`(`email`) 69 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '用户表'; 70 | 71 | DROP TABLE IF EXISTS "pre_cloud_config"; 72 | 73 | CREATE TABLE `pre_cloud_config` ( 74 | `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '配置id', 75 | `uid` bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户id', 76 | `type` char(255) NOT NULL DEFAULT '' COMMENT '类型', 77 | `bucket_name` char(255) NOT NULL DEFAULT '' COMMENT '存储桶名称', 78 | `account_id` char(255) NOT NULL DEFAULT '' COMMENT '账户id', 79 | `access_key_id` char(255) NOT NULL DEFAULT '' COMMENT '访问密钥id', 80 | `access_key_secret` char(255) NOT NULL DEFAULT '' COMMENT '访问密钥', 81 | `custom_path` char(255) NOT NULL DEFAULT '' COMMENT '自定义路径', 82 | `access_url_prefix` char(255) NOT NULL DEFAULT '' COMMENT '访问地址前缀', 83 | `is_enabled` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否启用', 84 | `is_deleted` tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否删除', 85 | `updated_at` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '更新时间', 86 | `created_at` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间', 87 | `deleted_at` datetime(0) NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '标记删除时间', 88 | PRIMARY KEY (`id`), 89 | INDEX `uid`(`uid`) 90 | ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COMMENT = '云配置表'; 91 | -------------------------------------------------------------------------------- /cmd/run.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "time" 9 | 10 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 11 | 12 | "github.com/radovskyb/watcher" 13 | "github.com/spf13/cobra" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | type runFlags struct { 18 | dir string // 项目根目录 19 | port string // 启动端口 20 | runMode string // 启动模式 21 | config string // 指定要使用的配置文件路径 22 | } 23 | 24 | func init() { 25 | runEnv := new(runFlags) 26 | 27 | var runCommand = &cobra.Command{ 28 | Use: "run [-c config_file] [-d working_dir] [-p port]", 29 | Short: "Run service", 30 | Run: func(cmd *cobra.Command, args []string) { 31 | if len(runEnv.dir) > 0 { 32 | err := os.Chdir(runEnv.dir) 33 | if err != nil { 34 | log.Println("failed to change the current working directory, ", err) 35 | } 36 | log.Println("working directory changed", zap.String("fileurl", runEnv.dir).String) 37 | } 38 | 39 | if len(runEnv.config) <= 0 { 40 | if fileurl.IsExist("config/config-dev.yaml") { 41 | runEnv.config = "config/config-dev.yaml" 42 | } else if fileurl.IsExist("config.yaml") { 43 | runEnv.config = "config.yaml" 44 | } else if fileurl.IsExist("config/config.yaml") { 45 | runEnv.config = "config/config.yaml" 46 | } else { 47 | 48 | log.Println("config file not found") 49 | runEnv.config = "config/config.yaml" 50 | 51 | if err := fileurl.CreatePath(runEnv.config, os.ModePerm); err != nil { 52 | log.Println("config file auto create error:", err) 53 | return 54 | } 55 | 56 | file, err := os.OpenFile(runEnv.config, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) 57 | if err != nil { 58 | log.Println("config file auto create error:", err) 59 | return 60 | } 61 | defer file.Close() 62 | _, err = file.WriteString(configDefault) 63 | if err != nil { 64 | log.Println("config file auto create writing error:", err) 65 | return 66 | } 67 | log.Println("config file auto create successfully") 68 | 69 | } 70 | } 71 | 72 | s, err := NewServer(runEnv) 73 | if err != nil { 74 | s.logger.Error("api service start err ", zap.Error(err)) 75 | } 76 | 77 | go func() { 78 | 79 | w := watcher.New() 80 | 81 | // 将 SetMaxEvents 设置为 1,以便在每个监听周期中至多接收 1 个事件 82 | // 如果没有设置 SetMaxEvents,默认情况下会发送所有事件。 83 | w.SetMaxEvents(1) 84 | 85 | // 只通知重命名和移动事件。 86 | w.FilterOps(watcher.Write) 87 | 88 | go func() { 89 | for { 90 | select { 91 | case event := <-w.Event: 92 | 93 | s.logger.Info("config watcher change", zap.String("event", event.Op.String()), zap.String("file", event.Path)) 94 | s.sc.SendCloseSignal(nil) 95 | 96 | // 重新初始化 server 97 | s, err = NewServer(runEnv) 98 | if err != nil { 99 | s.logger.Error("service start err", zap.Error(err)) 100 | } 101 | 102 | case err := <-w.Error: 103 | s.logger.Error("config watcher error", zap.Error(err)) 104 | case <-w.Closed: 105 | log.Println("config watcher closed") 106 | } 107 | } 108 | }() 109 | 110 | // 监听 config.yaml 文件 111 | if err := w.Add(runEnv.config); err != nil { 112 | s.logger.Error("config watcher file error", zap.Error(err)) 113 | } 114 | 115 | // 启动监听 116 | if err := w.Start(time.Second * 5); err != nil { 117 | s.logger.Error("config watcher start error", zap.Error(err)) 118 | } 119 | }() 120 | 121 | quit1 := make(chan os.Signal) 122 | signal.Notify(quit1, syscall.SIGINT, syscall.SIGTERM) 123 | <-quit1 124 | s.sc.SendCloseSignal(nil) 125 | s.logger.Info("api service has been shut down.") 126 | 127 | }, 128 | } 129 | 130 | rootCmd.AddCommand(runCommand) 131 | fs := runCommand.Flags() 132 | fs.StringVarP(&runEnv.dir, "dir", "d", "", "run dir") 133 | fs.StringVarP(&runEnv.port, "port", "p", "", "run port") 134 | fs.StringVarP(&runEnv.runMode, "mode", "m", "", "run mode") 135 | fs.StringVarP(&runEnv.config, "config", "c", "", "config file") 136 | 137 | } 138 | -------------------------------------------------------------------------------- /pkg/fileurl/file.go: -------------------------------------------------------------------------------- 1 | package fileurl 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "mime/multipart" 7 | "os" 8 | "os/exec" 9 | "path" 10 | "path/filepath" 11 | "runtime" 12 | "strings" 13 | "time" 14 | 15 | "github.com/google/uuid" 16 | ) 17 | 18 | type FileType int 19 | 20 | const ImageType FileType = iota + 1 21 | 22 | // IsFile 判断所给路径是否为文件 23 | func IsFile(path string) bool { 24 | return !IsDir(path) 25 | } 26 | 27 | // IsDir 判断所给路径是否为文件夹 28 | func IsDir(path string) bool { 29 | s, err := os.Stat(path) 30 | if err != nil { 31 | return false 32 | } 33 | return s.IsDir() 34 | 35 | } 36 | 37 | // GetFileName 获取文件路径 38 | func GetFileName(name string) string { 39 | ext := GetFileExt(name) 40 | fileName := strings.TrimSuffix(name, ext) 41 | // fileName = util.EncodeMD5(fileName) 42 | return fileName + ext 43 | } 44 | 45 | func GetFileNameOrRandom(fileName string) string { 46 | // 通过剪切板上传的附件 都是一个默认名字 47 | if fileName == "image.png" { 48 | fileName = GetFileName(uuid.New().String() + fileName) 49 | } else { 50 | fileName = GetFileName(fileName) 51 | } 52 | return fileName 53 | } 54 | 55 | // GetFileExt 获取文件后缀 56 | func GetFileExt(name string) string { 57 | return path.Ext(name) 58 | } 59 | 60 | // GetDatePath 获取日期保存路径 61 | func GetDatePath(timeFormat string) string { 62 | now := time.Now() 63 | if timeFormat == "" { 64 | timeFormat = "200601/02" 65 | } 66 | datePath := PathSuffixCheckAdd(now.Format(timeFormat), "/") 67 | return datePath 68 | } 69 | 70 | // IsContainExt 判断文件后缀是否在允许范围内 71 | func IsContainExt(t FileType, name string, uploadAllowExts []string) bool { 72 | ext := GetFileExt(name) 73 | ext = strings.ToUpper(ext) 74 | switch t { 75 | case ImageType: 76 | for _, allowExt := range uploadAllowExts { 77 | if strings.ToUpper(allowExt) == ext { 78 | return true 79 | } 80 | } 81 | } 82 | return false 83 | } 84 | 85 | // IsFileSizeAllowed 文件大小是否被允许 86 | func IsFileSizeAllowed(t FileType, f multipart.File, uploadMaxSize int) bool { 87 | content, _ := io.ReadAll(f) 88 | size := len(content) 89 | switch t { 90 | case ImageType: 91 | if size >= uploadMaxSize*1024*1024 { 92 | return true 93 | } 94 | } 95 | return false 96 | } 97 | 98 | // IsPermission 检查文件权限 99 | func IsPermission(dst string) bool { 100 | _, err := os.Stat(dst) 101 | return os.IsPermission(err) 102 | } 103 | 104 | // IsExist 判断所给路径是否不存在 105 | func IsExist(dst string) bool { 106 | _, err := os.Stat(dst) // os.Stat获取文件信息 107 | if err != nil { 108 | return os.IsExist(err) 109 | } 110 | return true 111 | } 112 | 113 | // CreatePath 创建路径 114 | func CreatePath(dst string, perm os.FileMode) error { 115 | dir := filepath.Dir(dst) 116 | err := os.MkdirAll(dir, perm) 117 | if err != nil { 118 | return err 119 | } 120 | return nil 121 | } 122 | 123 | // GetExePath 获取当前执行文件的路径 124 | func GetExePath() string { 125 | file, _ := exec.LookPath(os.Args[0]) 126 | path, _ := filepath.Abs(file) 127 | index := strings.LastIndex(path, string(os.PathSeparator)) 128 | return path[:index] 129 | } 130 | 131 | // PathSuffixCheckAdd 检查路径后缀,如果没有则添加 132 | func PathSuffixCheckAdd(path string, suffix string) string { 133 | if !strings.HasSuffix(path, suffix) { 134 | path = path + suffix 135 | } 136 | return path 137 | } 138 | 139 | // IsAbsPath 判断是否为绝对路径 140 | func IsAbsPath(path string) bool { 141 | if runtime.GOOS == "windows" { 142 | // Windows系统 143 | if filepath.VolumeName(path) != "" { 144 | return true // 如果有盘符,则为绝对路径 145 | } 146 | return filepath.IsAbs(path) // 检查是否是绝对路径 147 | } 148 | // UNIX/Linux系统 149 | return filepath.IsAbs(path) 150 | } 151 | 152 | // GetAbsPath 获取绝对路径 153 | func GetAbsPath(path string, root string) (string, error) { 154 | if root != "" { 155 | root = PathSuffixCheckAdd(root, "/") 156 | } 157 | realPath := root + path 158 | // 如果本身就是绝对路径 就直接返回 159 | if !IsAbsPath(realPath) { 160 | pwdDir, _ := os.Getwd() 161 | realPath = PathSuffixCheckAdd(pwdDir, "/") + path 162 | } 163 | if IsExist(realPath) { 164 | return realPath, nil 165 | } else { 166 | return "", errors.New("file not exists") 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /cmd/gorm_gen/gen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // gorm gen configure 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "os" 9 | "strings" 10 | 11 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 12 | "gorm.io/driver/mysql" 13 | "gorm.io/driver/sqlite" 14 | "gorm.io/gen" 15 | "gorm.io/gen/field" 16 | "gorm.io/gorm" 17 | "gorm.io/gorm/logger" 18 | "gorm.io/gorm/schema" 19 | ) 20 | 21 | var ( 22 | dbType string 23 | dbDsn string 24 | step int 25 | ) 26 | 27 | func init() { 28 | 29 | dType := flag.String("type", "", "输入类型") 30 | dsn := flag.String("dsn", "", "输入DB dsn地址") 31 | dStep := flag.Int("step", 0, "输入执行步骤") 32 | 33 | flag.Parse() 34 | dbType = *dType 35 | dbDsn = *dsn 36 | step = *dStep 37 | } 38 | 39 | // SQLColumnToHumpStyle sql转换成驼峰模式 40 | func SQLColumnToHumpStyle(in string) (ret string) { 41 | for i := 0; i < len(in); i++ { 42 | if i > 0 && in[i-1] == '_' && in[i] != '_' { 43 | s := strings.ToUpper(string(in[i])) 44 | ret += s 45 | } else if in[i] == '_' { 46 | continue 47 | } else { 48 | ret += string(in[i]) 49 | } 50 | } 51 | return 52 | } 53 | 54 | func Db(dsn string, dbType string) *gorm.DB { 55 | 56 | db, err := gorm.Open(useDia(dsn, dbType), &gorm.Config{ 57 | Logger: logger.Default.LogMode(logger.Silent), 58 | NamingStrategy: schema.NamingStrategy{ 59 | SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` 60 | }, 61 | }) 62 | if err != nil { 63 | panic(fmt.Errorf("connect db fail: %w", err)) 64 | } 65 | return db 66 | } 67 | 68 | func useDia(dsn string, dbType string) gorm.Dialector { 69 | if dbType == "mysql" { 70 | return mysql.Open(dsn) 71 | } else if dbType == "sqlite" { 72 | 73 | if !fileurl.IsExist(dsn) { 74 | fileurl.CreatePath(dsn, os.ModePerm) 75 | } 76 | return sqlite.Open(dsn) 77 | } 78 | return nil 79 | } 80 | 81 | func main() { 82 | 83 | g := gen.NewGenerator(gen.Config{ 84 | // 默认会在 OutPath 目录生成CRUD代码,并且同目录下生成 model 包 85 | // 所以OutPath最终package不能设置为model,在有数据库表同步的情况下会产生冲突 86 | // 若一定要使用可以通过ModelPkgPath单独指定model package的名称 87 | OutPath: "./internal/query", 88 | /* ModelPkgPath: "dal/model"*/ 89 | 90 | // gen.WithoutContext:禁用WithContext模式 91 | // gen.WithDefaultQuery:生成一个全局Query对象Q 92 | // gen.WithQueryInterface:生成Query接口 93 | Mode: gen.WithQueryInterface, 94 | WithUnitTest: false, 95 | FieldWithTypeTag: false, 96 | FieldWithIndexTag: true, 97 | }) 98 | 99 | db := Db(dbDsn, dbType) 100 | g.UseDB(db) 101 | 102 | var dataMap = map[string]func(gorm.ColumnType) (dataType string){ 103 | // int mapping 104 | "integer": func(columnType gorm.ColumnType) (dataType string) { 105 | if n, ok := columnType.Nullable(); ok && n { 106 | return "int64" 107 | } 108 | return "int64" 109 | }, 110 | "int": func(columnType gorm.ColumnType) (dataType string) { 111 | if n, ok := columnType.Nullable(); ok && n { 112 | return "int64" 113 | } 114 | return "int64" 115 | }, 116 | } 117 | g.WithDataTypeMap(dataMap) 118 | 119 | opts := []gen.ModelOpt{ 120 | //gen.FieldType("uid", "int64"), 121 | gen.FieldType("created_at", "timex.Time"), 122 | gen.FieldType("updated_at", "timex.Time"), 123 | gen.FieldType("deleted_at", "timex.Time"), 124 | gen.FieldGORMTag("created_at", func(tag field.GormTag) field.GormTag { 125 | tag.Set("autoCreateTime", "") 126 | tag.Set("type", "datetime") 127 | 128 | return tag 129 | }), 130 | gen.FieldGORMTag("updated_at", func(tag field.GormTag) field.GormTag { 131 | tag.Set("autoUpdateTime", "") 132 | tag.Set("type", "datetime") 133 | 134 | return tag 135 | }), 136 | gen.FieldGORMTag("deleted_at", func(tag field.GormTag) field.GormTag { 137 | tag.Set("type", "datetime") 138 | tag.Set("default", "NULL") 139 | return tag 140 | }), 141 | gen.FieldJSONTagWithNS(func(columnName string) string { 142 | return SQLColumnToHumpStyle(columnName) 143 | }), 144 | 145 | gen.FieldNewTagWithNS("form", func(columnName string) string { 146 | return SQLColumnToHumpStyle(columnName) 147 | }), 148 | } 149 | 150 | tableList, _ := db.Migrator().GetTables() 151 | 152 | for _, table := range tableList { 153 | if table == "sqlite_sequence" { 154 | continue 155 | } 156 | if strings.HasPrefix(table, "sqlite_") { 157 | continue 158 | } 159 | 160 | g.ApplyBasic(g.GenerateModel(table, opts...)) 161 | } 162 | g.Execute() 163 | 164 | } 165 | -------------------------------------------------------------------------------- /internal/dao/dao.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/haierkeys/custom-image-gateway/global" 11 | "github.com/haierkeys/custom-image-gateway/internal/query" 12 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 13 | 14 | "github.com/glebarez/sqlite" 15 | "github.com/haierkeys/gormTracing" 16 | "gorm.io/driver/mysql" 17 | "gorm.io/gorm" 18 | "gorm.io/gorm/logger" 19 | "gorm.io/gorm/schema" 20 | ) 21 | 22 | type Dao struct { 23 | Db *gorm.DB 24 | KeyDb map[string]*gorm.DB 25 | ctx context.Context 26 | } 27 | 28 | func New(db *gorm.DB, ctx context.Context) *Dao { 29 | return &Dao{Db: db, ctx: ctx, KeyDb: make(map[string]*gorm.DB)} 30 | } 31 | 32 | func (d *Dao) Use(f func(*gorm.DB), key ...string) *query.Query { 33 | 34 | var db *gorm.DB 35 | if len(key) > 0 { 36 | db = d.UseDb(key...) 37 | } else { 38 | db = d.Db 39 | } 40 | f(db) 41 | return query.Use(db) 42 | } 43 | 44 | func (d *Dao) UseDb(k ...string) *gorm.DB { 45 | 46 | key := k[0] 47 | if db, ok := d.KeyDb[key]; ok { 48 | return db 49 | } 50 | 51 | c := global.Config.Database 52 | 53 | if c.Type == "mysql" { 54 | c.Name = c.Name + "_" + key 55 | } else if c.Type == "sqlite" { 56 | c.Path = c.Path + "_" + key 57 | } 58 | 59 | db := d.Db.Session(&gorm.Session{}) 60 | 61 | db.Dialector = d.switchDB(global.Config.Database, key) 62 | d.KeyDb[key] = db 63 | return db 64 | 65 | } 66 | 67 | // switchDB 重新初始化 GORM 连接以切换数据库 68 | func (d *Dao) switchDB(c global.Database, key string) gorm.Dialector { 69 | 70 | if c.Type == "mysql" { 71 | c.Name = c.Name + "_" + key 72 | } else if c.Type == "sqlite" { 73 | c.Path = c.Path + "_" + key 74 | } else { 75 | return nil 76 | } 77 | return useDia(c) 78 | } 79 | 80 | func NewDBEngine(c global.Database) (*gorm.DB, error) { 81 | 82 | var db *gorm.DB 83 | var err error 84 | 85 | db, err = gorm.Open(useDia(c), &gorm.Config{ 86 | Logger: logger.Default.LogMode(logger.Info), 87 | NamingStrategy: schema.NamingStrategy{ 88 | TablePrefix: c.TablePrefix, // 表名前缀,`User` 的表名应该是 `t_users` 89 | SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` 90 | }, 91 | }) 92 | if err != nil { 93 | return nil, err 94 | } 95 | if global.Config.Server.RunMode == "debug" { 96 | db.Config.Logger = logger.Default.LogMode(logger.Info) 97 | } else { 98 | db.Config.Logger = logger.Default.LogMode(logger.Silent) 99 | } 100 | 101 | // 获取通用数据库对象 sql.DB ,然后使用其提供的功能 102 | sqlDB, err := db.DB() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | // SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。 108 | sqlDB.SetMaxIdleConns(c.MaxIdleConns) 109 | 110 | // SetMaxOpenConns 设置打开数据库连接的最大数量。 111 | sqlDB.SetMaxOpenConns(c.MaxOpenConns) 112 | 113 | // SetConnMaxLifetime 设置了连接可复用的最大时间。 114 | sqlDB.SetConnMaxLifetime(time.Minute * 10) 115 | 116 | _ = db.Use(&gormTracing.OpentracingPlugin{}) 117 | 118 | return db, nil 119 | 120 | } 121 | 122 | func NewDBEngineTest(c global.Database) (*gorm.DB, error) { 123 | 124 | var db *gorm.DB 125 | var err error 126 | 127 | db, err = gorm.Open(useDia(c), &gorm.Config{ 128 | Logger: logger.Default.LogMode(logger.Info), 129 | NamingStrategy: schema.NamingStrategy{ 130 | TablePrefix: c.TablePrefix, // 表名前缀,`User` 的表名应该是 `t_users` 131 | SingularTable: true, // 使用单数表名,启用该选项,此时,`User` 的表名应该是 `t_user` 132 | }, 133 | }) 134 | if err != nil { 135 | return nil, err 136 | } 137 | 138 | // 获取通用数据库对象 sql.DB ,然后使用其提供的功能 139 | sqlDB, err := db.DB() 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | // SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。 145 | sqlDB.SetMaxIdleConns(c.MaxIdleConns) 146 | 147 | // SetMaxOpenConns 设置打开数据库连接的最大数量。 148 | sqlDB.SetMaxOpenConns(c.MaxOpenConns) 149 | 150 | // SetConnMaxLifetime 设置了连接可复用的最大时间。 151 | sqlDB.SetConnMaxLifetime(time.Minute * 10) 152 | 153 | _ = db.Use(&gormTracing.OpentracingPlugin{}) 154 | 155 | return db, nil 156 | 157 | } 158 | 159 | func useDia(c global.Database) gorm.Dialector { 160 | if c.Type == "mysql" { 161 | return mysql.Open(fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local", 162 | c.UserName, 163 | c.Password, 164 | c.Host, 165 | c.Name, 166 | c.Charset, 167 | c.ParseTime, 168 | )) 169 | } else if c.Type == "sqlite" { 170 | 171 | filepath.Dir(c.Path) 172 | 173 | if !fileurl.IsExist(c.Path) { 174 | fileurl.CreatePath(c.Path, os.ModePerm) 175 | } 176 | return sqlite.Open(c.Path) 177 | } 178 | return nil 179 | 180 | } 181 | -------------------------------------------------------------------------------- /cmd/mfmt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/sha256" 7 | "fmt" 8 | "go/ast" 9 | "go/format" 10 | "go/parser" 11 | "go/token" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | 18 | "github.com/pkg/errors" 19 | "go.uber.org/zap" 20 | "golang.org/x/tools/go/packages" 21 | ) 22 | 23 | var stdlib = make(map[string]bool) 24 | 25 | func init() { 26 | pkgs, err := packages.Load(nil, "std") 27 | if err != nil { 28 | log.Fatal("get go stdlib err", zap.Error(err)) 29 | } 30 | 31 | for _, pkg := range pkgs { 32 | if !strings.HasPrefix(pkg.ID, "vendor") { 33 | stdlib[pkg.ID] = true 34 | } 35 | } 36 | } 37 | 38 | var module string 39 | 40 | func init() { 41 | file, err := os.Open("./go.mod") 42 | if err != nil { 43 | log.Fatal("no go.mod file found", zap.Error(err)) 44 | } 45 | defer file.Close() 46 | 47 | scanner := bufio.NewScanner(file) 48 | for scanner.Scan() { 49 | line := strings.TrimSpace(scanner.Text()) 50 | if strings.HasPrefix(line, "module ") { 51 | module = strings.TrimSpace(line[7:]) 52 | break 53 | } 54 | } 55 | 56 | if module == "" { 57 | log.Fatal("go.mod illegal") 58 | } 59 | } 60 | 61 | func main() { 62 | 63 | err := filepath.Walk("./", func(path string, info os.FileInfo, err error) error { 64 | if err != nil { 65 | return err 66 | } 67 | 68 | if info.IsDir() || strings.HasPrefix(path, ".") || strings.HasPrefix(path, "vendor") || strings.HasSuffix(path, ".pb.go") || filepath.Ext(path) != ".go" { 69 | return nil 70 | } 71 | 72 | raw, err := ioutil.ReadFile(path) 73 | if err != nil { 74 | return errors.Wrapf(err, "read file %s err", path) 75 | } 76 | 77 | digest0 := sha256.Sum256(raw) 78 | if raw, err = format.Source(raw); err != nil { 79 | return errors.Wrapf(err, "format file %s err", path) 80 | } 81 | 82 | file, err := parser.ParseFile(token.NewFileSet(), "", raw, 0) 83 | if err != nil { 84 | return errors.Wrapf(err, "parse file %s err", path) 85 | } 86 | 87 | var first, last int 88 | var imports []*ast.ImportSpec 89 | comments := make(map[string]string) 90 | 91 | ast.Inspect(file, func(n ast.Node) bool { 92 | switch spec := n.(type) { 93 | case *ast.ImportSpec: 94 | if first == 0 { 95 | first = int(spec.Pos()) 96 | } 97 | last = int(spec.End()) 98 | 99 | imports = append(imports, spec) 100 | 101 | k := last - 1 102 | for ; k < len(raw); k++ { 103 | if raw[k] == '\r' || raw[k] == '\n' { 104 | break 105 | } 106 | } 107 | 108 | comment := string(raw[last-1 : k]) 109 | if index := strings.Index(comment, "//"); index != -1 { 110 | comments[spec.Path.Value] = strings.TrimSpace(comment[index+2:]) 111 | } 112 | } 113 | return true 114 | }) 115 | 116 | if imports != nil { 117 | buf := bytes.NewBuffer(nil) 118 | buf.Write(raw[:first-2]) 119 | buf.WriteString(sort(imports, comments)) 120 | buf.Write(raw[last-1:]) 121 | 122 | if raw, err = format.Source(buf.Bytes()); err != nil { 123 | return errors.Wrapf(err, "double format file %s err", path) 124 | } 125 | } 126 | 127 | digest1 := sha256.Sum256(raw) 128 | if !bytes.Equal(digest0[:], digest1[:]) { 129 | fmt.Println(path) 130 | } 131 | 132 | if err = ioutil.WriteFile(path, raw, info.Mode()); err != nil { 133 | return errors.Wrapf(err, "write file %s err", path) 134 | } 135 | 136 | return nil 137 | }) 138 | if err != nil { 139 | log.Fatal("scan project err", zap.Error(err)) 140 | } 141 | } 142 | 143 | func sort(imports []*ast.ImportSpec, comments map[string]string) string { 144 | system := bytes.NewBuffer(nil) 145 | group := bytes.NewBuffer(nil) 146 | others := bytes.NewBuffer(nil) 147 | 148 | for _, pkg := range imports { 149 | value := strings.Trim(pkg.Path.Value, `"`) 150 | switch { 151 | case stdlib[value]: 152 | if pkg.Name != nil { 153 | system.WriteString(pkg.Name.String()) 154 | system.WriteString(" ") 155 | } 156 | 157 | system.WriteString(pkg.Path.Value) 158 | if comment, ok := comments[pkg.Path.Value]; ok { 159 | system.WriteString(" ") 160 | system.WriteString("// ") 161 | system.WriteString(comment) 162 | } 163 | system.WriteString("\n") 164 | 165 | case strings.HasPrefix(value, module): 166 | if pkg.Name != nil { 167 | group.WriteString(pkg.Name.String()) 168 | group.WriteString(" ") 169 | } 170 | 171 | group.WriteString(pkg.Path.Value) 172 | if comment, ok := comments[pkg.Path.Value]; ok { 173 | group.WriteString(" ") 174 | group.WriteString("// ") 175 | group.WriteString(comment) 176 | } 177 | group.WriteString("\n") 178 | 179 | default: 180 | if pkg.Name != nil { 181 | others.WriteString(pkg.Name.String()) 182 | others.WriteString(" ") 183 | } 184 | 185 | others.WriteString(pkg.Path.Value) 186 | if comment, ok := comments[pkg.Path.Value]; ok { 187 | others.WriteString(" ") 188 | others.WriteString("// ") 189 | others.WriteString(comment) 190 | } 191 | others.WriteString("\n") 192 | } 193 | } 194 | 195 | return fmt.Sprintf("%s\n%s\n%s", system.String(), group.String(), others.String()) 196 | } 197 | -------------------------------------------------------------------------------- /global/config.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/haierkeys/custom-image-gateway/pkg/fileurl" 7 | "github.com/haierkeys/custom-image-gateway/pkg/storage/aliyun_oss" 8 | "github.com/haierkeys/custom-image-gateway/pkg/storage/aws_s3" 9 | "github.com/haierkeys/custom-image-gateway/pkg/storage/cloudflare_r2" 10 | "github.com/haierkeys/custom-image-gateway/pkg/storage/local_fs" 11 | "github.com/haierkeys/custom-image-gateway/pkg/storage/minio" 12 | "github.com/haierkeys/custom-image-gateway/pkg/storage/webdav" 13 | 14 | "github.com/pkg/errors" 15 | "gopkg.in/yaml.v3" 16 | ) 17 | 18 | var ( 19 | Config *config 20 | ) 21 | 22 | type config struct { 23 | File string 24 | Server server `yaml:"server"` 25 | Log LogConfig `yaml:"log"` 26 | Database Database `yaml:"database"` 27 | User user `yaml:"user"` 28 | App app `yaml:"app"` 29 | Email email `yaml:"email"` 30 | Security security `yaml:"security"` 31 | LocalFS local_fs.Config `yaml:"local-fs"` 32 | OSS aliyun_oss.Config `yaml:"oss"` 33 | CloudflueR2 cloudflare_r2.Config `yaml:"cloudflue-r2"` 34 | MinIO minio.Config `yaml:"minio"` 35 | AWSS3 aws_s3.Config `yaml:"aws-s3"` 36 | WebDAV webdav.Config `yaml:"webdav"` 37 | } 38 | 39 | type LogConfig struct { 40 | // Level, See also zapcore.ParseLevel. 41 | Level string `yaml:"level"` 42 | 43 | // File that logger will be writen into. 44 | // Default is stderr. 45 | File string `yaml:"file"` 46 | 47 | // Production enables json output. 48 | Production bool `yaml:"production"` 49 | } 50 | 51 | // Server is a struct that holds the server settings 52 | type server struct { 53 | // RunMode is a string that holds the run mode of the server 54 | RunMode string `yaml:"run-mode"` 55 | // HttpPort is a string that holds the http port of the server 56 | HttpPort string `yaml:"http-port"` 57 | // ReadTimeout is a duration that holds the read timeout of the server 58 | ReadTimeout int `yaml:"read-timeout"` 59 | // WriteTimeout is a duration that holds the write timeout of the server 60 | WriteTimeout int `yaml:"write-timeout"` 61 | // PrivateHttpListen is a string that holds the private http listen address of the server 62 | PrivateHttpListen string `yaml:"private-http-listen"` 63 | } 64 | 65 | type security struct { 66 | AuthToken string `yaml:"auth-token"` 67 | AuthTokenKey string `yaml:"auth-token-key"` 68 | } 69 | 70 | type Database struct { 71 | // 数据库类型 72 | Type string `yaml:"type"` 73 | // sqlite数据库文件 74 | Path string `yaml:"path"` 75 | // 用户名 76 | UserName string `yaml:"username"` 77 | // 密码 78 | Password string `yaml:"password"` 79 | // 主机 80 | Host string `yaml:"host"` 81 | // 数据库名 82 | Name string `yaml:"name"` 83 | // 表前缀 84 | TablePrefix string `yaml:"table-prefix"` 85 | 86 | // 是否启用自动迁移 87 | AutoMigrate bool `yaml:"auto-migrate"` // 新增字段 88 | 89 | // 字符集 90 | Charset string `yaml:"charset"` 91 | // 是否解析时间 92 | ParseTime bool `yaml:"parse-time"` 93 | // 最大闲置连接数 94 | MaxIdleConns int `yaml:"max-idle-conns"` 95 | // 最大打开连接数 96 | MaxOpenConns int `yaml:"max-open-conns"` 97 | } 98 | 99 | type user struct { 100 | // 是否启用 101 | IsEnabled bool `yaml:"is-enable"` 102 | // 注册是否启用 103 | RegisterIsEnable bool `yaml:"register-is-enable"` 104 | } 105 | 106 | type app struct { 107 | // 默认页面大小 108 | DefaultPageSize int `yaml:"default-page-size"` 109 | // 最大页面大小 110 | MaxPageSize int `yaml:"max-page-size"` 111 | // 默认上下文超时时间 112 | DefaultContextTimeout int `yaml:"default-context-timeout"` 113 | // 日志保存路径 114 | LogSavePath string `yaml:"log-save-fileurl"` 115 | // 日志文件名 116 | LogFile string `yaml:"log-file"` 117 | 118 | // 上传临时路径 119 | TempPath string `yaml:"temp-fileurl"` 120 | // 上传服务器URL 121 | UploadUrlPre string `yaml:"upload-url-pre"` 122 | //上传日期路径设置 123 | UploadDatePath string `yaml:"upload-date-path"` 124 | // 上传图片最大尺寸 125 | UploadMaxSize int `yaml:"upload-max-size"` 126 | // 上传图片允许的扩展名 127 | UploadAllowExts []string `yaml:"upload-allow-exts"` 128 | 129 | ImageMaxSizeWidth int `yaml:"image-max-size-width"` 130 | ImageMaxSizeHeight int `yaml:"image-max-size-height"` 131 | ImageQuality int `yaml:"image-quality"` 132 | } 133 | 134 | type email struct { 135 | ErrorReportEnable bool `yaml:"error-report-enable"` 136 | Host string `yaml:"host"` 137 | Port int `yaml:"port"` 138 | UserName string `yaml:"username"` 139 | Password string `yaml:"password"` 140 | IsSSL bool `yaml:"is-ssl"` 141 | From string `yaml:"from"` 142 | To []string `yaml:"to"` 143 | } 144 | 145 | // ConfigLoad 初始化 146 | func ConfigLoad(f string) (string, error) { 147 | 148 | realpath, err := fileurl.GetAbsPath(f, "") 149 | if err != nil { 150 | return realpath, err 151 | } 152 | c := new(config) 153 | 154 | c.File = f 155 | file, err := os.ReadFile(f) 156 | if err != nil { 157 | return realpath, errors.Wrap(err, "read config file failed") 158 | } 159 | 160 | err = yaml.Unmarshal(file, c) 161 | if err != nil { 162 | return realpath, errors.Wrap(err, "parse config file failed") 163 | } 164 | Config = c 165 | return realpath, nil 166 | 167 | } 168 | -------------------------------------------------------------------------------- /pkg/util/authcode_encrypt.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | const ( 14 | tokenLimit = 10 15 | tokenStart = 8 16 | tokenEnd = 18 17 | ) 18 | 19 | func AuthCodeEncrypt(token string, action string, key string) (out string, err error) { 20 | var ( 21 | strauth string 22 | tokenByte []byte 23 | keyByte []byte 24 | ) 25 | if len(token) == 0 { 26 | return out, errors.New("token is not allowed to be empty") 27 | } 28 | if len(action) == 0 { 29 | action = "EN" 30 | } 31 | if action == "DE" { 32 | token = strings.Replace(token, "[a]", "+", -1) 33 | token = strings.Replace(token, "[b]", "&", -1) 34 | token = strings.Replace(token, "[c]", "/", -1) 35 | } 36 | 37 | tokenLen := len(token) 38 | if tokenLen <= tokenLimit { 39 | return out, errors.New("The token length does not meet the requirements") 40 | } 41 | if action == "EN" { 42 | strauth = EncodeMD5(token)[tokenStart:tokenEnd] 43 | } else { 44 | strauth = token[tokenLen-tokenLimit : tokenLen] 45 | tokenByte, _ = base64.StdEncoding.DecodeString(token[0 : tokenLen-tokenLimit]) 46 | token = string(tokenByte) 47 | } 48 | 49 | key = EncodeMD5(strauth + key) 50 | 51 | tokenByte = []byte(token) 52 | keyByte = []byte(key) 53 | tmpCode := XorEncodeStr(tokenByte, keyByte) 54 | code := string(tmpCode) 55 | if action == "DE" { 56 | if EncodeMD5(code)[tokenStart:tokenEnd] == strauth { 57 | out = code 58 | } 59 | } else { 60 | out = base64.StdEncoding.EncodeToString([]byte(code + strauth)) 61 | out = strings.Replace(out, "[a]", "+", -1) 62 | out = strings.Replace(out, "[b]", "&", -1) 63 | out = strings.Replace(out, "[c]", "/", -1) 64 | } 65 | return out, nil 66 | } 67 | 68 | /** 69 | * $string 明文或密文 70 | * $operation 加密ENCODE或解密DECODE 71 | * $key 密钥 72 | * $expiry 密钥有效期 73 | */ 74 | func AuthDzCodeEncrypt(str, operation, key string, expiry int64) (string, error) { 75 | // 动态密匙长度,相同的明文会生成不同密文就是依靠动态密匙 76 | // 加入随机密钥,可以令密文无任何规律,即便是原文和密钥完全相同,加密结果也会每次不同,增大破解难度。 77 | // 取值越大,密文变动规律越大,密文变化 = 16 的 ckeyLength 次方 78 | // 当此值为 0 时,则不产生随机密钥 79 | ckeyLength := 4 80 | 81 | // 密匙 82 | if key == "" { 83 | key = "STARFISSION_AUTH_KEY" 84 | } 85 | 86 | key = EncodeMD5(key) 87 | 88 | // 密匙a会参与加解密 89 | keya := EncodeMD5(key[:16]) 90 | // 密匙b会用来做数据完整性验证 91 | keyb := EncodeMD5(key[16:]) 92 | // 密匙c用于变化生成的密文 93 | keyc := "" 94 | if ckeyLength != 0 { 95 | if operation == "DECODE" { 96 | keyc = str[:ckeyLength] 97 | } else { 98 | sTime := EncodeMD5(time.Now().String()) 99 | sLen := 32 - ckeyLength 100 | keyc = sTime[sLen:] 101 | } 102 | } 103 | 104 | // 参与运算的密匙 105 | cryptKey := fmt.Sprintf("%s%s", keya, EncodeMD5(keya+keyc)) 106 | keyLength := len(cryptKey) 107 | 108 | // 明文,前10位用来保存时间戳,解密时验证数据有效性,10到26位用来保存$keyb(密匙b),解密时会通过这个密匙验证数据完整性 109 | // 如果是解码的话,会从第$ckeyLength位开始,因为密文前$ckeyLength位保存 动态密匙,以保证解密正确 110 | if operation == "DECODE" { 111 | str = strings.Replace(str, "[a]", "+", -1) 112 | str = strings.Replace(str, "[b]", "&", -1) 113 | str = strings.Replace(str, "[c]", "/", -1) 114 | 115 | strByte, err := base64.StdEncoding.DecodeString(str[ckeyLength:]) 116 | if err != nil { 117 | fmt.Println(err) 118 | return "", err 119 | } 120 | str = string(strByte) 121 | } else { 122 | 123 | if expiry != 0 { 124 | expiry = expiry + time.Now().Unix() 125 | } 126 | tmpMd5 := EncodeMD5(str + keyb) 127 | str = fmt.Sprintf("%010d%s%s", expiry, tmpMd5[:16], str) 128 | } 129 | 130 | stringLength := len(str) 131 | resdata := make([]byte, 0, stringLength) 132 | var rndkey, box [256]int 133 | // 产生密匙簿 134 | j := 0 135 | a := 0 136 | i := 0 137 | tmp := 0 138 | for i = 0; i < 256; i++ { 139 | rndkey[i] = int(cryptKey[i%keyLength]) 140 | box[i] = i 141 | } 142 | // 用固定的算法,打乱密匙簿,增加随机性,好像很复杂,实际上并不会增加密文的强度 143 | for i = 0; i < 256; i++ { 144 | j = (j + box[i] + rndkey[i]) % 256 145 | tmp = box[i] 146 | box[i] = box[j] 147 | box[j] = tmp 148 | } 149 | // 核心加解密部分 150 | a = 0 151 | j = 0 152 | tmp = 0 153 | for i = 0; i < stringLength; i++ { 154 | a = (a + 1) % 256 155 | j = (j + box[a]) % 256 156 | tmp = box[a] 157 | box[a] = box[j] 158 | box[j] = tmp 159 | // 从密匙簿得出密匙进行异或,再转成字符 160 | resdata = append(resdata, byte(int(str[i])^box[(box[a]+box[j])%256])) 161 | } 162 | result := string(resdata) 163 | 164 | if operation == "DECODE" { 165 | // substr($result, 0, 10) == 0 验证数据有效性 166 | // substr($result, 0, 10) - time() > 0 验证数据有效性 167 | // substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16) 验证数据完整性 168 | // 验证数据有效性,请看未加密明文的格式 169 | frontTen, _ := strconv.ParseInt(result[:10], 10, 0) 170 | if (frontTen == 0 || frontTen-time.Now().Unix() > 0) && result[10:26] == EncodeMD5(result[26:] + keyb)[:16] { 171 | return result[26:], nil 172 | } else { 173 | return "", errors.New("AuthCode Encrypt error") 174 | } 175 | } else { 176 | // 把动态密匙保存在密文里,这也是为什么同样的明文,生产不同密文后能解密的原因 177 | // 因为加密后的密文可能是一些特殊字符,复制过程可能会丢失,所以用base64编码 178 | result = keyc + base64.StdEncoding.EncodeToString([]byte(result)) 179 | 180 | result = strings.Replace(result, "+", "[a]", -1) 181 | result = strings.Replace(result, "&", "[b]", -1) 182 | result = strings.Replace(result, "/", "[c]", -1) 183 | 184 | return result, nil 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /.github/workflows/go-release-docker.yml: -------------------------------------------------------------------------------- 1 | name: go-release-docker 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "*" 8 | 9 | jobs: 10 | create-release: 11 | name: Create Release 12 | runs-on: ubuntu-latest 13 | outputs: 14 | upload_url: ${{ steps.create_release.outputs.upload_url }} 15 | steps: 16 | - name: Create Release 17 | id: create_release 18 | uses: softprops/action-gh-release@v2 19 | env: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | draft: false 23 | prerelease: false 24 | 25 | build-push-docker: 26 | runs-on: ubuntu-latest 27 | needs: create-release 28 | outputs: 29 | NAME: ${{ env.NAME }} 30 | IMAGE_TAG: ${{ env.IMAGE_TAG }} 31 | TAG_VERSION: ${{ env.TAG_VERSION }} 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v4 35 | 36 | - name: Set Environment Variables 37 | run: | 38 | echo "NAME=$(basename ${GITHUB_REPOSITORY})" >> ${GITHUB_ENV} 39 | echo "IMAGE_TAG=$(basename ${GITHUB_REF})" >> ${GITHUB_ENV} 40 | echo "TAG_VERSION=$(git describe --tags --abbrev=0)" >> ${GITHUB_ENV} 41 | echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> ${GITHUB_ENV} 42 | echo "GIT_COMMIT=$(git rev-parse --short HEAD)" >> ${GITHUB_ENV} 43 | 44 | - name: Append 'latest' tag if on main branch 45 | if: github.ref == 'refs/heads/main' || env.IMAGE_TAG == env.TAG_VERSION 46 | run: echo "IMAGE_TAG=${{ env.IMAGE_TAG }},latest" >> ${GITHUB_ENV} 47 | 48 | - uses: actions/setup-go@v5 49 | with: 50 | go-version: 'stable' 51 | 52 | - name: Check Go Version 53 | run: go version # 验证版本 54 | 55 | - name: Go Build Prepare 56 | run: go install github.com/mitchellh/gox@latest 57 | 58 | - name: Go Build Multi-platform 59 | run: make gox-all 60 | 61 | - name: Upload Build Artifacts 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: build_file 65 | path: ./build/ 66 | 67 | - name: Upload Config Artifacts 68 | uses: actions/upload-artifact@v4 69 | with: 70 | name: config 71 | path: ./config 72 | 73 | - uses: docker/setup-qemu-action@v2 74 | - uses: docker/setup-buildx-action@v2 75 | 76 | - name: Docker Build & Publish to GitHub Container Registry 77 | uses: elgohr/Publish-Docker-Github-Action@v5 78 | with: 79 | dockerfile: Dockerfile 80 | name: ${{ github.actor }}/${{ env.NAME }} 81 | username: ${{ github.actor }} 82 | password: ${{ secrets.GITHUB_TOKEN }} 83 | platforms: linux/amd64,linux/arm64 84 | registry: ghcr.io 85 | snapshot: false 86 | tags: "${{ env.IMAGE_TAG }}" 87 | buildargs: | 88 | VERSION=${{ env.IMAGE_TAG }} 89 | BUILD_DATE=${{ env.BUILD_DATE }} 90 | GIT_COMMIT=${{ env.GIT_COMMIT }} 91 | 92 | - name: Docker Build & Publish to DockerHub 93 | uses: elgohr/Publish-Docker-Github-Action@v5 94 | with: 95 | dockerfile: Dockerfile 96 | name: ${{ github.actor }}/${{ env.NAME }} 97 | username: ${{ github.actor }} 98 | password: ${{ secrets.DOCKERHUB_TOKEN }} 99 | platforms: linux/amd64,linux/arm64 100 | snapshot: false 101 | tags: "${{ env.IMAGE_TAG }}" 102 | buildargs: | 103 | VERSION=${{ env.IMAGE_TAG }} 104 | BUILD_DATE=${{ env.BUILD_DATE }} 105 | GIT_COMMIT=${{ env.GIT_COMMIT }} 106 | 107 | push-release: 108 | needs: [create-release, build-push-docker] 109 | runs-on: ubuntu-latest 110 | strategy: 111 | matrix: 112 | jobs: 113 | - { goos: darwin, goarch: amd64, cc: "" } 114 | - { goos: darwin, goarch: arm64, cc: "" } 115 | - { goos: linux, goarch: amd64, cc: "" } 116 | - { goos: linux, goarch: arm64, cc: "" } 117 | - { goos: windows, goarch: amd64, cc: "", ext: ".exe" } 118 | steps: 119 | - name: Set NAME env 120 | run: echo "NAME=${{ needs.build-push-docker.outputs.NAME }}-${{ needs.build-push-docker.outputs.TAG_VERSION }}" >> ${GITHUB_ENV} 121 | 122 | - name: Download Build Artifacts 123 | uses: actions/download-artifact@v4 124 | with: 125 | name: build_file 126 | path: ./build/ 127 | 128 | - name: Download Config Artifacts 129 | uses: actions/download-artifact@v4 130 | with: 131 | name: config 132 | path: ./config 133 | 134 | - name: Create GZip Archive 135 | run: | 136 | tar -czvf ./build/${{ env.NAME }}-${{ matrix.jobs.goos }}-${{ matrix.jobs.goarch }}.tar.gz ./config -C ./build/${{ matrix.jobs.goos }}_${{ matrix.jobs.goarch }}/ . 137 | 138 | - name: Upload GZip to Release 139 | uses: actions/upload-release-asset@v1 140 | env: 141 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 142 | with: 143 | upload_url: ${{ needs.create-release.outputs.upload_url }} 144 | asset_path: ./build/${{ env.NAME }}-${{ matrix.jobs.goos }}-${{ matrix.jobs.goarch }}.tar.gz 145 | asset_name: ${{ env.NAME }}-${{ matrix.jobs.goos }}-${{ matrix.jobs.goarch }}.tar.gz 146 | asset_content_type: application/gzip -------------------------------------------------------------------------------- /readme-zh.md: -------------------------------------------------------------------------------- 1 | [中文文档](readme-zh.md) / [English Document](README.md) 2 | 3 | # Custom Image Gateway 4 | 5 |

6 | version 7 | license 8 |

9 | 10 | 该项目为 [Obsidian Custom Image Auto Uploader](https://github.com/haierkeys/obsidian-custom-image-auto-uploader) 插件提供图片上传、存储与云同步服务。 11 | 12 | ## 功能清单 13 | 14 | - [x] 支持图片上传 15 | - [x] 支持令牌授权,提升 API 安全性 16 | - [x] 支持图片 HTTP 访问(基础功能,建议使用 Nginx 替代) 17 | - [x] 存储支持: 18 | - [x] 同时保存至本地或云存储,方便后续迁移 19 | - [x] 本地存储支持(为 NAS 准备,功能已测试) 20 | - [x] 支持阿里云 OSS 云存储(功能已实现,尚未测试) 21 | - [x] 支持 Cloudflare R2 云存储(功能已实现,已测试) 22 | - [x] 支持 Amazon S3(功能已实现,已测试) 23 | - [x] 增加 MinIO 存储支持(v1.5+) 24 | - [x] 支持 WebDAV 存储(v2.5+) 25 | - [x] 提供 Docker 安装支持,便于在家庭 NAS 或远程服务器上使用 26 | - [x] 提供公共服务 API && Web界面,方便提供公共服务 用户公共接口 & Web 界面 27 | 28 | ## 更新日志 29 | 30 | 查看完整的更新内容,请访问 [Changelog](https://github.com/haierkeys/custom-image-gateway/releases)。 31 | 32 | ## 价格 33 | 34 | 本软件是开源且免费的。如果您想表示感谢或帮助支持继续开发,可以通过以下方式为我提供支持: 35 | 36 | [BuyMeACoffee](https://ko-fi.com/haierkeys) 37 | 38 | ## 快速开始 39 | ### 安装 40 | 41 | - 目录设置 42 | 43 | ```bash 44 | # 创建项目所需的目录 45 | mkdir -p /data/image-api 46 | cd /data/image-api 47 | 48 | mkdir -p ./config && mkdir -p ./storage/logs && mkdir -p ./storage/uploads 49 | ``` 50 | 51 | 首次启动如果不下载配置文件,程序会自动生成一个默认配置到 **config/config.yaml** 52 | 53 | 如果你想从网络下载一个默认配置 使用以下命令来下载 54 | 55 | ```bash 56 | # 从开源库下载默认配置文件到配置目录 57 | wget -P ./config/ https://raw.githubusercontent.com/haierkeys/custom-image-gateway/main/config/config.yaml 58 | ``` 59 | 60 | - 二进制安装 61 | 62 | 从 [Releases](https://github.com/haierkeys/custom-image-gateway/releases) 下载最新版本,解压后执行: 63 | 64 | ```bash 65 | ./image-api run -c config/config.yaml 66 | ``` 67 | 68 | 69 | - 容器化安装(Docker 方式) 70 | 71 | Docker 命令: 72 | 73 | ```bash 74 | # 拉取最新的容器镜像 75 | docker pull haierkeys/custom-image-gateway:latest 76 | 77 | # 创建并启动容器 78 | docker run -tid --name image-api \ 79 | -p 9000:9000 -p 9001:9001 \ 80 | -v /data/image-api/storage/:/api/storage/ \ 81 | -v /data/image-api/config/:/api/config/ \ 82 | haierkeys/custom-image-gateway:latest 83 | ``` 84 | 85 | Docker Compose 86 | 使用 *containrrr/watchtower* 来监听镜像实现自动更新项目 87 | **docker-compose.yaml** 内容如下 88 | 89 | ```yaml 90 | # docker-compose.yaml 91 | services: 92 | image-api: 93 | image: haierkeys/custom-image-gateway:latest # 你的应用镜像 94 | container_name: image-api 95 | ports: 96 | - "9000:9000" # 映射端口 9000 97 | - "9001:9001" # 映射端口 9001 98 | volumes: 99 | - /data/image-api/storage/:/api/storage/ # 映射存储目录 100 | - /data/image-api/config/:/api/config/ # 映射配置目录 101 | restart: always 102 | 103 | ``` 104 | 105 | 执行 **docker compose** 106 | 107 | 以服务方式注册 docker 容器 108 | 109 | ```bash 110 | docker compose up -d 111 | ``` 112 | 113 | 注销并销毁 docker 容器 114 | 115 | ```bash 116 | docker compose down 117 | ``` 118 | 119 | 120 | 121 | ### 使用 122 | 123 | - **使用单服务网关** 124 | 125 | 支持 `本地存储`, `OSS` , `Cloudflare R2` , `Amazon S3` , `MinIO`, `WebDAV` 126 | 127 | 需要修改 [config.yaml](config/config.yaml#http-port) 128 | 129 | 修改`http-port` 和 `auth-token` 两个选项 130 | 131 | 启动网关程序 132 | 133 | API 网关地址为 `http://{IP:PORT}/api/upload` 134 | 135 | API 访问令牌为 `auth-token` 内容 136 | 137 | 138 | - **使用 多用户 开放网关** 139 | 140 | 支持 `本地存储`, `OSS` , `Cloudflare R2` , `Amazon S3` , `MinIO` ( v2.3+ ), `WebDAV` ( v2.5+ ) 141 | 142 | 需要在 [config.yaml](config/config.yaml#user) 中修改 143 | 144 | `http-port` 和 `database` 145 | 146 | 同时修改 `user.is-enable` 和 `user.register-is-enable` 为 `true` 147 | 148 | 启动网关程序 149 | 150 | 访问 `WebGUI` 地址 `http://{IP:PORT}` 进行用户注册配置 151 | 152 | ![Image](https://github.com/user-attachments/assets/39c798de-b243-42c1-a75a-cd179913fc49) 153 | 154 | API 网关地址为 `http://{IP:PORT}/api/user/upload` 155 | 156 | 点击在 `WebGUI` 复制 API 配置 获取配置信息 157 | 158 | 159 | 160 | - **存储类型说明** 161 | 162 | 163 | | 存储类型 | 说明 | 164 | |----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 165 | | 服务器本地存储 | 默认的保存路径为: `/data/storage/uploads` 关联配置项`config.local-fs.save-path` 为 `storage/uploads`,
如果使用网关图片资源访问服务, 需要 `config.local-fs.httpfs-is-enable` 设置为 `true`
对应的 `访问地址前缀` 为 `http://{IP:PORT}`, 使用单服务网关设置 `config.app.upload-url-pre`
推荐使用 Nginx 来实现资源访问 | 166 | 167 | 168 | 169 | ### 配置说明 170 | 171 | 默认的配置文件名为 **config.yaml**,请将其放置在 **根目录** 或 **config** 目录下。 172 | 173 | 更多配置详情请参考: 174 | 175 | - [config/config.yaml](config/config.yaml) 176 | 177 | 178 | ## 其他资源 179 | 180 | - [Obsidian Custom Image Auto Uploader](https://github.com/haierkeys/https://github.com/haierkeys/obsidian-custom-image-auto-uploader) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # docker login --username=xxxxxx registry.cn-shanghai.aliyuncs.com 3 | include .env 4 | #export $(shell sed 's/=.*//' .env) 5 | REPO = $(eval REPO := $$(shell go list -f '{{.ImportPath}}' .))$(value REPO) 6 | 7 | DockerHubUser = haierkeys 8 | DockerHubName = custom-image-gateway 9 | 10 | 11 | # DockerHubName = $(shell basename "$(PWD)") 12 | projectRootDir = $(shell pwd) 13 | 14 | 15 | ReleaseTagPre = release-v 16 | DevelopTagPre = develop-v 17 | 18 | P_NAME = api 19 | P_BIN = image-api 20 | 21 | 22 | platform = $(shell uname -m) 23 | 24 | 25 | 26 | # These are the values we want to pass for Version and BuildTime 27 | # GitTag = $(shell git describe --tags) 28 | GitTag = $(shell git describe --tags --abbrev=0) 29 | GitVersion = $(shell git log -1 --format=%h) 30 | GitVersionDesc = $(shell git log -1 --format=%s) 31 | BuildTime=$(shell date +%FT%T%z) 32 | 33 | 34 | # Go parameters 35 | goCmd = go 36 | 37 | ifeq ($(platform),arm64) 38 | buildCmd = build 39 | else 40 | buildCmd = build 41 | endif 42 | 43 | # CGO=CGO_ENABLED=0 CC=musl-gcc 44 | CGO=CGO_ENABLED=0 45 | 46 | # Setup the -ldflags option for go build here, interpolate the variable values 47 | # -linkmode "external" -extldflags "-static" 48 | LDFLAGS=-ldflags '-X ${REPO}/global.Version=$(GitTag) -X "${REPO}/global.GitTag=$(GitVersion) / $(GitVersionDesc)" -X ${REPO}/global.BuildTime=$(BuildTime)' 49 | #LDFLAGS=-tags "sqlite_omit_load_extension" -ldflags '-extldflags "-static -fpic" -X ${REPO}/global.Version=$(GitTag) -X "${REPO}/global.GitTag=$(GitVersion) / $(GitVersionDesc)" -X ${REPO}/global.BuildTime=$(BuildTime)' 50 | #LDFLAGS =-tags musl -ldflags '-linkmode "external" -extldflags "-static"' 51 | 52 | 53 | 54 | goBuild = $(goCmd) $(buildCmd) ${LDFLAGS} 55 | goRun = $(goCmd) run ${LDFLAGS} 56 | 57 | goClean = $(goCmd) clean 58 | goTest = $(goCmd) test 59 | goGet = $(goCmd) get -u 60 | 61 | 62 | 63 | sourceDir = $(projectRootDir) 64 | cfgDir = $(projectRootDir)/config 65 | cfgFile = $(cfgDir)/config.yaml 66 | buildDir = $(projectRootDir)/build 67 | 68 | 69 | .PHONY: all build-all run test clean push-online push-dev build-macos-amd64 build-macos-arm64 build-linux-amd64 build-linux-arm64 build-winmdows-amd64 70 | all: test build-all 71 | 72 | 73 | build-all: 74 | # $(call checkStatic) 75 | $(MAKE) build-macos-amd64 76 | $(MAKE) build-macos-arm64 77 | $(MAKE) build-linux-amd64 78 | $(MAKE) build-linux-arm64 79 | $(MAKE) build-winmdows-amd64 80 | 81 | 82 | run: 83 | # $(call checkStatic) 84 | $(call init) 85 | $(goRun)-v $(sourceDir) 86 | 87 | # build2: 88 | # $(call init) 89 | # $(goBuild) -o $(binAdm) -v $(sourceAdmDir) 90 | # $(goBuild) -o $(binNode) -v $(sourceNodeDir) 91 | # mv $(binAdm) $(buildAdmDir) 92 | # mv $(binNode) $(buildNodeDir) 93 | 94 | test: 95 | @echo $(DockerHubName) 96 | @echo "Test Completed" 97 | 98 | # $(goTest) -v -race -coverprofile=coverage.txt -covermode=atomic $(sourceAdmDir) 99 | # $(goTest) -v -race -coverprofile=coverage.txt -covermode=atomic $(sourceNodeDir) 100 | clean: 101 | rm -rf $(buildDir) 102 | 103 | push-online: build-linux 104 | $(call dockerImageClean) 105 | docker build --platform linux/amd64 -t $(DockerHubUser)/$(DockerHubName):latest -f Dockerfile . 106 | docker tag $(DockerHubUser)/$(DockerHubName):latest $(DockerHubUser)/$(DockerHubName):$(ReleaseTagPre)$(GitTag) 107 | 108 | docker push $(DockerHubUser)/$(DockerHubName):$(ReleaseTagPre)$(GitTag) 109 | docker push $(DockerHubUser)/$(DockerHubName):latest 110 | 111 | 112 | push-dev: build-linux 113 | $(call dockerImageClean) 114 | docker build --platform linux/amd64 -t $(DockerHubUser)/$(DockerHubName):dev-latest -f Dockerfile . 115 | docker tag $(DockerHubUser)/$(DockerHubName):dev-latest $(DockerHubUser)/$(DockerHubName):$(DevelopTagPre)$(GitTag) 116 | 117 | docker push $(DockerHubUser)/$(DockerHubName):$(DevelopTagPre)$(GitTag) 118 | docker push $(DockerHubUser)/$(DockerHubName):dev-latest 119 | 120 | 121 | 122 | build-macos-amd64: 123 | $(CGO) GOOS=darwin GOARCH=amd64 $(goBuild) -o $(buildDir)/darwin_amd64/${P_BIN} $(bin) -v $(sourceDir) 124 | build-macos-arm64: 125 | $(CGO) GOOS=darwin GOARCH=arm64 $(goBuild) -o $(buildDir)/darwin_arm64/${P_BIN} -v $(sourceDir) 126 | build-linux-amd64: 127 | # CGO_ENABLED=1 CC=musl-gcc GOOS=linux GOARCH=amd64 $(goBuild) -o $(buildDir)/linux_amd64/${P_BIN} -v $(sourceDir) 128 | $(CGO) GOOS=linux GOARCH=amd64 $(goBuild) -o $(buildDir)/linux_amd64/${P_BIN} -v $(sourceDir) 129 | build-linux-arm64: 130 | $(CGO) GOOS=linux GOARCH=arm64 $(goBuild) -o $(buildDir)/linux_arm64/${P_BIN} -v $(sourceDir) 131 | build-windows-amd64: 132 | # CGO_ENABLED=0 CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC="x86_64-w64-mingw32-gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -lssp" $(goBuild) -o $(bin).exe -v $(sourceDir) 133 | $(CGO) GOOS=windows GOARCH=amd64 $(goBuild) -o $(buildDir)/windows_amd64/${P_BIN}.exe -v $(sourceDir) 134 | gox-linux: 135 | $(CGO) gox ${LDFLAGS} -osarch="linux/amd64 linux/arm64" -output="$(buildDir)/{{.OS}}_{{.Arch}}/${P_BIN}" 136 | gox-all: 137 | $(CGO) gox ${LDFLAGS} -osarch="darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64" -output="$(buildDir)/{{.OS}}_{{.Arch}}/${P_BIN}" 138 | old-gen: 139 | scripts/gormgen.sh sqlite storage/database/db.db main pre_ pre_ main_gen 140 | gen: 141 | go run -v ./cmd/gorm_gen/gen.go -type sqlite -dsn storage/database/db.db 142 | go run -v ./cmd/model_gen/gen.go 143 | 144 | define dockerImageClean 145 | @echo "docker Image Clean" 146 | bash docker_image_clean.sh 147 | endef 148 | 149 | define init 150 | @echo "Build Init" 151 | endef 152 | 153 | 154 | -------------------------------------------------------------------------------- /docs/docs.go: -------------------------------------------------------------------------------- 1 | // Package docs GENERATED BY SWAG; DO NOT EDIT 2 | // This file was generated by swaggo/swag 3 | package docs 4 | 5 | import "github.com/swaggo/swag" 6 | 7 | const docTemplate = `{ 8 | "schemes": {{ marshal .Schemes }}, 9 | "swagger": "2.0", 10 | "info": { 11 | "description": "{{escape .Description}}", 12 | "title": "{{.Title}}", 13 | "contact": {}, 14 | "version": "{{.Version}}" 15 | }, 16 | "host": "{{.Host}}", 17 | "basePath": "{{.BasePath}}", 18 | "paths": { 19 | "/api/v1/client/version": { 20 | "get": { 21 | "produces": [ 22 | "application/json" 23 | ], 24 | "summary": "获取客户端版本信息", 25 | "parameters": [ 26 | { 27 | "maxLength": 100, 28 | "type": "string", 29 | "description": "标签名称", 30 | "name": "platform", 31 | "in": "query" 32 | }, 33 | { 34 | "enum": [ 35 | 0, 36 | 1 37 | ], 38 | "type": "integer", 39 | "default": 1, 40 | "description": "状态", 41 | "name": "versionCode", 42 | "in": "query" 43 | } 44 | ], 45 | "responses": { 46 | "200": { 47 | "description": "请求错误", 48 | "schema": { 49 | "$ref": "#/definitions/service.ClientVersion" 50 | } 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | "definitions": { 57 | "app.ErrResult": { 58 | "type": "object", 59 | "properties": { 60 | "code": { 61 | "description": "HTTP状态码" 62 | }, 63 | "data": { 64 | "description": "错误格式数据" 65 | }, 66 | "details": { 67 | "description": "错误支付" 68 | }, 69 | "msg": { 70 | "description": "失败\u0026\u0026成功消息" 71 | }, 72 | "status": { 73 | "description": "业务状态码" 74 | } 75 | } 76 | }, 77 | "app.ListRes": { 78 | "type": "object", 79 | "properties": { 80 | "list": { 81 | "description": "数据清单" 82 | }, 83 | "pager": { 84 | "description": "翻页信息", 85 | "$ref": "#/definitions/app.Pager" 86 | } 87 | } 88 | }, 89 | "app.Pager": { 90 | "type": "object", 91 | "properties": { 92 | "page": { 93 | "description": "页码", 94 | "type": "integer" 95 | }, 96 | "pageSize": { 97 | "description": "每页数量", 98 | "type": "integer" 99 | }, 100 | "totalRows": { 101 | "description": "总行数", 102 | "type": "integer" 103 | } 104 | } 105 | }, 106 | "app.ResListResult": { 107 | "type": "object", 108 | "properties": { 109 | "code": { 110 | "description": "HTTP状态码" 111 | }, 112 | "data": { 113 | "description": "数据集合", 114 | "$ref": "#/definitions/app.ListRes" 115 | }, 116 | "msg": { 117 | "description": "失败\u0026\u0026成功消息" 118 | }, 119 | "status": { 120 | "description": "业务状态码" 121 | } 122 | } 123 | }, 124 | "service.ClientVersion": { 125 | "type": "object", 126 | "properties": { 127 | "ID": { 128 | "description": "用户id", 129 | "type": "integer" 130 | }, 131 | "createdAt": { 132 | "description": "创建时间", 133 | "type": "string" 134 | }, 135 | "details": { 136 | "description": "版本更新详情", 137 | "type": "string" 138 | }, 139 | "platform": { 140 | "description": "平台 1:安卓 2:ios", 141 | "type": "integer" 142 | }, 143 | "resourceURL": { 144 | "description": "版本更新资源地址", 145 | "type": "string" 146 | }, 147 | "updatedAt": { 148 | "description": "更新时间", 149 | "type": "string" 150 | }, 151 | "versionCode": { 152 | "description": "版本号", 153 | "type": "integer" 154 | }, 155 | "versionName": { 156 | "description": "版本名", 157 | "type": "string" 158 | } 159 | } 160 | } 161 | } 162 | }` 163 | 164 | // SwaggerInfo holds exported Swagger Info so clients can modify it 165 | var SwaggerInfo = &swag.Spec{ 166 | Version: "", 167 | Host: "", 168 | BasePath: "", 169 | Schemes: []string{}, 170 | Title: "", 171 | Description: "", 172 | InfoInstanceName: "swagger", 173 | SwaggerTemplate: docTemplate, 174 | } 175 | 176 | func init() { 177 | swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) 178 | } 179 | -------------------------------------------------------------------------------- /pkg/code/common.go: -------------------------------------------------------------------------------- 1 | package code 2 | 3 | var ( 4 | Failed = NewError(0, lang{zh: "失败", en: "Failed"}) 5 | Success = NewSuss(1, lang{zh: "成功", en: "Success"}) 6 | SuccessCreate = NewSuss(2, lang{zh: "创建成功", en: "Create Success"}) 7 | SuccessUpdate = NewSuss(3, lang{zh: "更新成功", en: "Update Success"}) 8 | SuccessDelete = NewSuss(4, lang{zh: "删除成功", en: "Delete Success"}) 9 | SuccessPasswordUpdate = NewSuss(5, lang{zh: "密码修改成功", en: "Password Update Success"}) 10 | 11 | ErrorServerInternal = NewError(incr(500), lang{zh: "服务器内部错误", en: "Server Internal Error"}) 12 | ErrorNotFoundAPI = NewError(incr(400), lang{zh: "找不到API", en: "Not Found API"}) 13 | ErrorInvalidParams = NewError(incr(400), lang{zh: "参数验证失败", en: "Invalid Params"}) 14 | ErrorTooManyRequests = NewError(incr(400), lang{zh: "请求过多", en: "Too Many Requests"}) 15 | ErrorInvalidAuthToken = NewError(incr(400), lang{zh: "访问令牌效验失败", en: "Invalid Auth Token"}) 16 | ErrorNotUserAuthToken = NewError(incr(400), lang{zh: "尚未登录,请先登录", en: "Not logged in. Please log in first."}) 17 | ErrorInvalidUserAuthToken = NewError(incr(400), lang{zh: "登录状态失效,请重新登录", en: "Session expired, please log in again."}) 18 | ErrorInvalidToken = NewError(incr(400), lang{zh: "您的访问缺少用户令牌", en: "Your access is missing a user token"}) 19 | ErrorTokenExpired = NewError(incr(400), lang{zh: "用户令牌已过期", en: "User token has expired"}) 20 | ErrorUserRegister = NewError(incr(400), lang{zh: "用户注册失败", en: "User registration failed"}) 21 | ErrorPasswordNotValid = NewError(incr(400), lang{zh: "密码不符合规则", en: "Password does not meet the rules"}) 22 | ErrorUserLoginPasswordFailed = NewError(incr(400), lang{zh: "密码错误", en: "Password error"}) 23 | ErrorUserOldPasswordFailed = NewError(incr(400), lang{zh: "当前密码验证错误", en: "Current password verification error"}) 24 | ErrorMultiUserPublicAPIClosed = NewError(incr(400), lang{zh: "多用户开放接口已经关闭,请联系管理员配置 config.user.is-user-enable 选项", en: "Multi-user open interface has been closed, please contact the administrator to configure the config.user.is-user-enable option"}) 25 | ErrorUserRegisterIsDisable = NewError(incr(400), lang{zh: "用户注册已关闭,请联系管理员配置 config.user.register-is-enable 选项", en: "User registration is closed, please contact the administrator to configure the config.user.register-is-enable option"}) 26 | ErrorUserLoginFailed = NewError(incr(400), lang{zh: "用户登录失败", en: "User login failed"}) 27 | ErrorUserNotFound = NewError(incr(400), lang{zh: "用户不存在", en: "Username does not exist"}) 28 | ErrorUserAlreadyExists = NewError(incr(400), lang{zh: "用户已经存在", en: "Username already exists"}) 29 | ErrorUserEmailAlreadyExists = NewError(incr(400), lang{zh: "用户邮箱已存在", en: "User email already exists"}) 30 | ErrorUserUsernameNotValid = NewError(incr(400), lang{zh: "用户名不符合规则,用户名长度为3-15位,只能包含字母、数字或下划线", en: "The username does not meet the rules, the username length is 3-15 digits, and can only contain letters, numbers or underscores"}) 31 | ErrorUserPasswordNotMatch = NewError(incr(400), lang{zh: "密码与密码确认不一致", en: "Password and password confirmation do not match"}) 32 | ErrorUserPasswordNotValid = NewError(incr(400), lang{zh: "密码不符合规则,密码长度为6-20位,只能包含字母、数字或下划线", en: "Password does not meet the rules, password length is 6-20 digits, and can only contain letters, numbers or underscores"}) 33 | ErrorDBQuery = NewError(incr(400), lang{zh: "数据库查询失败", en: "Database query failed"}) 34 | ErrorUploadFileFailed = NewError(incr(400), lang{zh: "上传文件失败", en: "Upload file failed"}) 35 | ErrorInvalidCloudStorageType = NewError(incr(400), lang{zh: "云存储类型无效", en: "Invalid cloud storage type"}) 36 | ErrorInvalidStorageType = NewError(incr(400), lang{zh: "存储类型无效", en: "Invalid storage type"}) 37 | ErrorInvalidCloudStorageBucketName = NewError(incr(400), lang{zh: "云存储桶名无效", en: "Invalid cloud storage bucket name"}) 38 | ErrorInvalidCloudStorageAccessKeyID = NewError(incr(400), lang{zh: "云存储访问密钥ID无效", en: "Invalid cloud storage access key ID"}) 39 | ErrorInvalidCloudStorageAccessKeySecret = NewError(incr(400), lang{zh: "云存储访问密钥无效", en: "Invalid cloud storage access key"}) 40 | ErrorInvalidCloudStorageAccountID = NewError(incr(400), lang{zh: "云存储账户ID无效", en: "Invalid cloud storage account ID"}) 41 | ErrorInvalidCloudStorageRegion = NewError(incr(400), lang{zh: "云存储区域无效", en: "Invalid cloud storage region"}) 42 | ErrorInvalidCloudStorageEndpoint = NewError(incr(400), lang{zh: "云存储端点无效", en: "Invalid cloud storage endpoint"}) 43 | ErrorUserCloudflueR2Disabled = NewError(incr(400), lang{zh: "多用户开放网关存储类型 Cloudflue R2 未开启", en: "Multi-user open gateway storage type Cloudflue R2 is not enabled"}) 44 | ErrorUserALIOSSDisabled = NewError(incr(400), lang{zh: "多用户开放网关存储类型 Aliyun OSS 未开启", en: "Multi-user open gateway storage type Aliyun OSS is not enabled"}) 45 | ErrorUserAWSS3Disabled = NewError(incr(400), lang{zh: "多用户开放网关存储类型 AWS S3 未开启", en: "Multi-user open gateway storage type AWS S3 is not enabled"}) 46 | ErrorUserMinIODisabled = NewError(incr(400), lang{zh: "多用户开放网关存储类型 MinIO 未开启", en: "Multi-user open gateway storage type MinIO is not enabled"}) 47 | ErrorUserLocalFSDisabled = NewError(incr(400), lang{zh: "多用户开放网关存储类型 服务器本地存储 未开启", en: "Multi-user open gateway storage type server local storage is not enabled"}) 48 | ErrorWebDAVInvalidEndpoint = NewError(incr(400), lang{zh: "WebDAV服务器URL无效", en: "WebDAV server URL is invalid"}) 49 | ErrorWebDAVInvalidUser = NewError(incr(400), lang{zh: "WebDAV服务器用户名不能为空", en: "WebDAV server username is invalid"}) 50 | ErrorWebDAVInvalidPassword = NewError(incr(400), lang{zh: "WebDAV服务器密码不能为空", en: "WebDAV server URL is invalid"}) 51 | ErrorInvalidAccessURLPrefix = NewError(incr(400), lang{zh: "访问地址前缀不能为空", en: "Access URL prefix cannot be empty"}) 52 | ) 53 | --------------------------------------------------------------------------------