├── .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 |
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 |
7 |
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 | [
](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 | 
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 |
--------------------------------------------------------------------------------