diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5dcd8de..8d8e079 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -47,6 +47,9 @@ jobs: with: context: . file: ./Dockerfile + build-args: | + dev=true + version=${{ github.sha }} push: true tags: ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }} @@ -56,6 +59,9 @@ jobs: with: context: . file: ./Dockerfile + build-args: | + dev=false + version=${{ steps.set_tag.outputs.tag }} push: true tags: | ghcr.io/${{ github.repository }}:${{ steps.set_tag.outputs.tag }} diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9570789..8c99d22 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -23,12 +23,10 @@ jobs: - name: Build run: | - LDFLAGS="-s -w" + LDFLAGS="-s -w -X config.Version=${{ github.ref_name }}" # Linux - CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-386 main.go CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-amd64 main.go - CGO_ENABLED=0 GOOS=linux GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm main.go CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-linux-arm64 main.go # Darwin @@ -36,9 +34,7 @@ jobs: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-darwin-arm64 main.go # Windows - CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-386.exe main.go CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-amd64.exe main.go - CGO_ENABLED=0 GOOS=windows GOARCH=arm go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm.exe main.go CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -ldflags="$LDFLAGS" -o output/sub2clash-windows-arm64.exe main.go - name: Create Release @@ -52,16 +48,6 @@ jobs: draft: false prerelease: false - - name: Upload Release Asset (Linux 386) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./output/sub2clash-linux-386 - asset_name: sub2clash-linux-386 - asset_content_type: application/octet-stream - - name: Upload Release Asset (Linux amd64) uses: actions/upload-release-asset@v1 env: @@ -72,16 +58,6 @@ jobs: asset_name: sub2clash-linux-amd64 asset_content_type: application/octet-stream - - name: Upload Release Asset (Linux arm) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./output/sub2clash-linux-arm - asset_name: sub2clash-linux-arm - asset_content_type: application/octet-stream - - name: Upload Release Asset (Linux arm64) uses: actions/upload-release-asset@v1 env: @@ -112,16 +88,6 @@ jobs: asset_name: sub2clash-darwin-arm64 asset_content_type: application/octet-stream - - name: Upload Release Asset (Windows 386) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./output/sub2clash-windows-386.exe - asset_name: sub2clash-windows-386.exe - asset_content_type: application/octet-stream - - name: Upload Release Asset (Windows amd64) uses: actions/upload-release-asset@v1 env: @@ -132,16 +98,6 @@ jobs: asset_name: sub2clash-windows-amd64.exe asset_content_type: application/octet-stream - - name: Upload Release Asset (Windows arm) - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: ./output/sub2clash-windows-arm.exe - asset_name: sub2clash-windows-arm.exe - asset_content_type: application/octet-stream - - name: Upload Release Asset (Windows arm64) uses: actions/upload-release-asset@v1 env: diff --git a/.gitignore b/.gitignore index 4081dca..7f8d47b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ dist subs test logs -sub2clash.db \ No newline at end of file +sub2clash.db +.env diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 17bb435..16b8440 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -10,10 +10,8 @@ builds: - darwin goarch: - amd64 - - arm - arm64 - - 386 ldflags: - - -s -w + - -s -w -X sub2clash/config.Version={{ .Version }} no_unique_dist_dir: true binary: "{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 9f1a96a..755bc46 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,12 @@ WORKDIR /app COPY . . RUN go mod download +# 获取参数 +ARG version +ARG dev + # 使用 -ldflags 参数进行编译 -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o sub2clash main.go +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X sub2clash/config.Version=${version} -X sub2clash/config.Dev=${dev}" -o sub2clash main.go FROM alpine:latest diff --git a/README.md b/README.md index 0867c09..ba8b5d5 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ | REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` | | CACHE_EXPIRE | 订阅缓存时间(秒) | `300` | | LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` | +| SHORT_LINK_LENGTH | 短链长度 | `6` | ### API diff --git a/api/controller/default.go b/api/controller/default.go index c45f72d..0b58d97 100644 --- a/api/controller/default.go +++ b/api/controller/default.go @@ -7,6 +7,7 @@ import ( "gopkg.in/yaml.v3" "net/url" "regexp" + "sort" "strings" "sub2clash/model" "sub2clash/parser" @@ -43,6 +44,7 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template if err != nil { return nil, errors.New("解析模板失败: " + err.Error()) } + var proxyList []model.Proxy // 加载订阅 for i := range query.Subs { data, err := utils.LoadSubscription(query.Subs[i], query.Refresh) @@ -50,31 +52,49 @@ func BuildSub(clashType model.ClashType, query validator.SubValidator, template return nil, errors.New("加载订阅失败: " + err.Error()) } // 解析订阅 - var proxyList []model.Proxy + err = yaml.Unmarshal(data, &sub) if err != nil { reg, _ := regexp.Compile("(ssr|ss|vmess|trojan|http|https)://") if reg.Match(data) { - proxyList = utils.ParseProxy(strings.Split(string(data), "\n")...) + p := utils.ParseProxy(strings.Split(string(data), "\n")...) + proxyList = append(proxyList, p...) } else { // 如果无法直接解析,尝试Base64解码 base64, err := parser.DecodeBase64(string(data)) if err != nil { return nil, errors.New("加载订阅失败: " + err.Error()) } - proxyList = utils.ParseProxy(strings.Split(base64, "\n")...) + p := utils.ParseProxy(strings.Split(base64, "\n")...) + proxyList = append(proxyList, p...) } } else { - proxyList = sub.Proxies + proxyList = append(proxyList, sub.Proxies...) } - utils.AddProxy(sub, query.AutoTest, query.Lazy, query.Sort, clashType, proxyList...) } + // 将新增节点都添加到临时变量 t 中,防止策略组排序错乱 + var t = &model.Subscription{} + utils.AddProxy(t, query.AutoTest, query.Lazy, clashType, proxyList...) // 处理自定义代理 utils.AddProxy( - sub, query.AutoTest, query.Lazy, query.Sort, clashType, + t, query.AutoTest, query.Lazy, clashType, utils.ParseProxy(query.Proxies...)..., ) - MergeSubAndTemplate(temp, sub) + // 排序策略组 + switch query.Sort { + case "sizeasc": + sort.Sort(model.ProxyGroupsSortBySize(t.ProxyGroups)) + case "sizedesc": + sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(t.ProxyGroups))) + case "nameasc": + sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups)) + case "namedesc": + sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(t.ProxyGroups))) + default: + sort.Sort(model.ProxyGroupsSortByName(t.ProxyGroups)) + } + // 合并新节点和模板 + MergeSubAndTemplate(temp, t) // 处理自定义规则 for _, v := range query.Rules { if v.Prepend { diff --git a/api/controller/short_link.go b/api/controller/short_link.go index 4f5872d..1eea508 100644 --- a/api/controller/short_link.go +++ b/api/controller/short_link.go @@ -1,12 +1,14 @@ package controller import ( - "crypto/sha256" - "encoding/hex" + "errors" "github.com/gin-gonic/gin" + "gorm.io/gorm" "net/http" + "strings" "sub2clash/config" "sub2clash/model" + "sub2clash/utils" "sub2clash/utils/database" "sub2clash/validator" "time" @@ -18,12 +20,32 @@ func ShortLinkGenHandler(c *gin.Context) { if err := c.ShouldBind(¶ms); err != nil { c.String(400, "参数错误: "+err.Error()) } - // 生成短链接 - //hash := utils.RandomString(6) - shortLink := sha256.Sum224([]byte(params.Url)) - hash := hex.EncodeToString(shortLink[:]) + if strings.TrimSpace(params.Url) == "" { + c.String(400, "参数错误") + return + } + // 生成hash + hash := utils.RandomString(config.Default.ShortLinkLength) // 存入数据库 - database.DB.FirstOrCreate( + var item model.ShortLink + result := database.FindShortLinkByUrl(params.Url, &item) + if result.Error == nil { + c.String(200, item.Hash) + return + } else { + if !errors.Is(result.Error, gorm.ErrRecordNotFound) { + c.String(500, "数据库错误: "+result.Error.Error()) + return + } + } + // 如果记录存在则重新生成hash,直到记录不存在 + result = database.FindShortLinkByHash(hash, &item) + for result.Error == nil { + hash = utils.RandomString(config.Default.ShortLinkLength) + result = database.FindShortLinkByHash(hash, &item) + } + // 创建记录 + database.FirstOrCreateShortLink( &model.ShortLink{ Hash: hash, Url: params.Url, @@ -37,17 +59,21 @@ func ShortLinkGenHandler(c *gin.Context) { func ShortLinkGetHandler(c *gin.Context) { // 获取动态路由 hash := c.Param("hash") + if strings.TrimSpace(hash) == "" { + c.String(400, "参数错误") + return + } // 查询数据库 var shortLink model.ShortLink - result := database.DB.Where("hash = ?", hash).First(&shortLink) - // 更新最后访问时间 - shortLink.LastRequestTime = time.Now().Unix() - database.DB.Save(&shortLink) + result := database.FindShortLinkByHash(hash, &shortLink) // 重定向 if result.Error != nil { c.String(404, "未找到短链接") return } + // 更新最后访问时间 + shortLink.LastRequestTime = time.Now().Unix() + database.SaveShortLink(&shortLink) uri := config.Default.BasePath + shortLink.Url c.Redirect(http.StatusTemporaryRedirect, uri) } diff --git a/api/route.go b/api/route.go index 51dc3aa..17d54b4 100644 --- a/api/route.go +++ b/api/route.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" "html/template" "sub2clash/api/controller" + "sub2clash/config" "sub2clash/middleware" ) @@ -17,7 +18,11 @@ func SetRoute(r *gin.Engine) { r.SetHTMLTemplate(template.Must(template.New("").ParseFS(templates, "templates/*"))) r.GET( "/", func(c *gin.Context) { - c.HTML(200, "index.html", nil) + c.HTML( + 200, "index.html", gin.H{ + "Version": config.Version, + }, + ) }, ) r.GET( diff --git a/api/templates/index.html b/api/templates/index.html index 8c63925..4b3bf5e 100644 --- a/api/templates/index.html +++ b/api/templates/index.html @@ -1,163 +1,240 @@ - + - - - - + + + sub2clash - + - + - + - - -
-
+ +
+

sub2clash

- 通用订阅链接转 Clash(Meta) 配置工具 使用文档 -
- -
+ 通用订阅链接转 Clash(Meta) 配置工具 + 使用文档 +
+
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- - + +
- + - -
+ +
- - - + + +
- - - + + +
+
+ + +
+

+ Powered by + sub2clash +

+

Version {{.Version}}

+
- - - -
- - - - + }); + } + + diff --git a/config/config.go b/config/config.go index d7f7978..2371992 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "github.com/joho/godotenv" "os" "strconv" @@ -15,11 +16,14 @@ type Config struct { CacheExpire int64 LogLevel string BasePath string + ShortLinkLength int } var Default *Config +var Version string +var Dev string -func init() { +func LoadConfig() error { Default = &Config{ MetaTemplate: "template_meta.yaml", ClashTemplate: "template_clash.yaml", @@ -29,12 +33,13 @@ func init() { CacheExpire: 60 * 5, LogLevel: "info", BasePath: "/", + ShortLinkLength: 6, } _ = godotenv.Load() if os.Getenv("PORT") != "" { atoi, err := strconv.Atoi(os.Getenv("PORT")) if err != nil { - panic("PORT invalid") + return errors.New("PORT invalid") } Default.Port = atoi } @@ -47,21 +52,21 @@ func init() { if os.Getenv("REQUEST_RETRY_TIMES") != "" { atoi, err := strconv.Atoi(os.Getenv("REQUEST_RETRY_TIMES")) if err != nil { - panic("REQUEST_RETRY_TIMES invalid") + return errors.New("REQUEST_RETRY_TIMES invalid") } Default.RequestRetryTimes = atoi } if os.Getenv("REQUEST_MAX_FILE_SIZE") != "" { atoi, err := strconv.Atoi(os.Getenv("REQUEST_MAX_FILE_SIZE")) if err != nil { - panic("REQUEST_MAX_FILE_SIZE invalid") + return errors.New("REQUEST_MAX_FILE_SIZE invalid") } Default.RequestMaxFileSize = int64(atoi) } if os.Getenv("CACHE_EXPIRE") != "" { atoi, err := strconv.Atoi(os.Getenv("CACHE_EXPIRE")) if err != nil { - panic("CACHE_EXPIRE invalid") + return errors.New("CACHE_EXPIRE invalid") } Default.CacheExpire = int64(atoi) } @@ -74,4 +79,12 @@ func init() { Default.BasePath += "/" } } + if os.Getenv("SHORT_LINK_LENGTH") != "" { + atoi, err := strconv.Atoi(os.Getenv("SHORT_LINK_LENGTH")) + if err != nil { + return errors.New("SHORT_LINK_LENGTH invalid") + } + Default.ShortLinkLength = atoi + } + return nil } diff --git a/docker-compose.yml b/docker-compose.yml index 8a3b07a..9eadcb6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,8 @@ services: - "8011:8011" volumes: - ./logs:/app/logs - # - ./templates:/app/templates + - ./templates:/app/templates + - ./data:/app/data # environment: # - PORT=8011 # - META_TEMPLATE=template_meta.yaml diff --git a/logger/logger.go b/logger/logger.go index e141cf9..55abcf3 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -4,18 +4,18 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "path/filepath" - "sub2clash/config" - "sub2clash/utils" "sync" "time" ) var ( - Logger *zap.Logger - lock sync.Mutex + Logger *zap.Logger + lock sync.Mutex + logLevel string ) -func init() { +func InitLogger(level string) { + logLevel = level buildLogger() go rotateLogs() } @@ -24,7 +24,7 @@ func buildLogger() { lock.Lock() defer lock.Unlock() var level zapcore.Level - switch config.Default.LogLevel { + switch logLevel { case "error": level = zap.ErrorLevel case "debug": @@ -36,10 +36,6 @@ func buildLogger() { default: level = zap.InfoLevel } - err := utils.MKDir("logs") - if err != nil { - panic("创建日志失败" + err.Error()) - } zapConfig := zap.NewProductionConfig() zapConfig.Encoding = "console" zapConfig.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder @@ -47,9 +43,10 @@ func buildLogger() { zapConfig.OutputPaths = []string{"stdout", getLogFileName("info")} zapConfig.ErrorOutputPaths = []string{"stderr", getLogFileName("error")} zapConfig.Level = zap.NewAtomicLevelAt(level) + var err error Logger, err = zapConfig.Build() if err != nil { - panic("创建日志失败" + err.Error()) + panic("log failed" + err.Error()) } } diff --git a/main.go b/main.go index 3bf78a3..72e5f58 100644 --- a/main.go +++ b/main.go @@ -5,8 +5,6 @@ import ( "github.com/gin-gonic/gin" "go.uber.org/zap" "io" - "os" - "path/filepath" "strconv" "sub2clash/api" "sub2clash/config" @@ -21,43 +19,44 @@ var templateMeta string //go:embed templates/template_clash.yaml var templateClash string -func writeTemplate(path string, template string) error { - tPath := filepath.Join( - "templates", path, - ) - if _, err := os.Stat(tPath); os.IsNotExist(err) { - file, err := os.Create(tPath) - if err != nil { - return err - } - defer func(file *os.File) { - _ = file.Close() - }(file) - _, err = file.WriteString(template) - if err != nil { - return err - } - } - return nil -} - func init() { - if err := utils.MKDir("subs"); err != nil { - os.Exit(1) - } - if err := utils.MKDir("templates"); err != nil { - os.Exit(1) - } - if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { - os.Exit(1) - } - if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil { - os.Exit(1) - } - err := database.ConnectDB() + // 加载配置 + err := config.LoadConfig() + // 初始化日志 + logger.InitLogger(config.Default.LogLevel) if err != nil { - panic(err) + logger.Logger.Panic("load config failed", zap.Error(err)) } + // 检查更新 + if config.Dev != "true" { + go func() { + update, newVersion, err := utils.CheckUpdate() + if err != nil { + logger.Logger.Warn("check update failed", zap.Error(err)) + } + if update { + logger.Logger.Info("new version is available", zap.String("version", newVersion)) + } + }() + } else { + logger.Logger.Info("running in dev mode") + } + // 创建文件夹 + err = utils.MkEssentialDir() + if err != nil { + logger.Logger.Panic("create essential dir failed", zap.Error(err)) + } + // 写入默认模板 + err = utils.WriteDefalutTemplate(templateMeta, templateClash) + if err != nil { + logger.Logger.Panic("write default template failed", zap.Error(err)) + } + // 连接数据库 + err = database.ConnectDB() + if err != nil { + logger.Logger.Panic("database connect failed", zap.Error(err)) + } + logger.Logger.Info("database connect success") } func main() { @@ -69,10 +68,10 @@ func main() { r := gin.Default() // 设置路由 api.SetRoute(r) - logger.Logger.Info("Server is running at http://localhost:" + strconv.Itoa(config.Default.Port)) + logger.Logger.Info("server is running at http://localhost:" + strconv.Itoa(config.Default.Port)) err := r.Run(":" + strconv.Itoa(config.Default.Port)) if err != nil { - logger.Logger.Error("Server run error", zap.Error(err)) + logger.Logger.Error("server running failed", zap.Error(err)) return } } diff --git a/model/github.go b/model/github.go new file mode 100644 index 0000000..eef70db --- /dev/null +++ b/model/github.go @@ -0,0 +1,12 @@ +package model + +type Tags []struct { + Name string `json:"name"` + ZipballUrl string `json:"zipball_url"` + TarballUrl string `json:"tarball_url"` + Commit struct { + Sha string `json:"sha"` + Url string `json:"url"` + } + NodeId string `json:"node_id"` +} diff --git a/model/proxy_group.go b/model/proxy_group.go index 26179c6..14ba7cf 100644 --- a/model/proxy_group.go +++ b/model/proxy_group.go @@ -39,7 +39,6 @@ func (p ProxyGroupsSortByName) Less(i, j int) bool { bestMatch, _, _ := matcher.Match(language.Make("zh")) // 使用最佳匹配的语言进行排序 c := collate.New(bestMatch) - return c.CompareString(p[i].Name, p[j].Name) < 0 } diff --git a/parser/ss.go b/parser/ss.go index eebeae5..a654fdf 100644 --- a/parser/ss.go +++ b/parser/ss.go @@ -12,12 +12,12 @@ import ( func ParseSS(proxy string) (model.Proxy, error) { // 判断是否以 ss:// 开头 if !strings.HasPrefix(proxy, "ss://") { - return model.Proxy{}, fmt.Errorf("无效的 ss Url") + return model.Proxy{}, fmt.Errorf("invalid ss Url") } // 分割 parts := strings.SplitN(strings.TrimPrefix(proxy, "ss://"), "@", 2) if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 ss Url") + return model.Proxy{}, fmt.Errorf("invalid ss Url") } if !strings.Contains(parts[0], ":") { // 解码 @@ -29,13 +29,13 @@ func ParseSS(proxy string) (model.Proxy, error) { } credentials := strings.SplitN(parts[0], ":", 2) if len(credentials) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 ss 凭证") + return model.Proxy{}, fmt.Errorf("invalid ss Url") } // 分割 serverInfo := strings.SplitN(parts[1], "#", 2) serverAndPort := strings.SplitN(serverInfo[0], ":", 2) if len(serverAndPort) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 ss 服务器和端口") + return model.Proxy{}, fmt.Errorf("invalid ss Url") } // 转换端口字符串为数字 port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) diff --git a/parser/ssr.go b/parser/ssr.go index 0c6f749..afc9b79 100644 --- a/parser/ssr.go +++ b/parser/ssr.go @@ -11,7 +11,7 @@ import ( func ParseShadowsocksR(proxy string) (model.Proxy, error) { // 判断是否以 ssr:// 开头 if !strings.HasPrefix(proxy, "ssr://") { - return model.Proxy{}, fmt.Errorf("无效的 ssr Url") + return model.Proxy{}, fmt.Errorf("invalid ssr Url") } var err error proxy = strings.TrimPrefix(proxy, "ssr://") diff --git a/parser/trojan.go b/parser/trojan.go index c8c2d2c..d16398d 100644 --- a/parser/trojan.go +++ b/parser/trojan.go @@ -11,12 +11,12 @@ import ( func ParseTrojan(proxy string) (model.Proxy, error) { // 判断是否以 trojan:// 开头 if !strings.HasPrefix(proxy, "trojan://") { - return model.Proxy{}, fmt.Errorf("无效的 trojan Url") + return model.Proxy{}, fmt.Errorf("invalid trojan Url") } // 分割 parts := strings.SplitN(strings.TrimPrefix(proxy, "trojan://"), "@", 2) if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 trojan Url") + return model.Proxy{}, fmt.Errorf("invalid trojan Url") } // 分割 serverInfo := strings.SplitN(parts[1], "#", 2) @@ -27,7 +27,7 @@ func ParseTrojan(proxy string) (model.Proxy, error) { return model.Proxy{}, err } if len(serverAndPort) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 trojan 服务器和端口") + return model.Proxy{}, fmt.Errorf("invalid trojan") } // 处理端口 port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) diff --git a/parser/vless.go b/parser/vless.go index 6740855..5e56467 100644 --- a/parser/vless.go +++ b/parser/vless.go @@ -11,12 +11,12 @@ import ( func ParseVless(proxy string) (model.Proxy, error) { // 判断是否以 vless:// 开头 if !strings.HasPrefix(proxy, "vless://") { - return model.Proxy{}, fmt.Errorf("无效的 vless Url") + return model.Proxy{}, fmt.Errorf("invalid vless Url") } // 分割 parts := strings.SplitN(strings.TrimPrefix(proxy, "vless://"), "@", 2) if len(parts) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 vless Url") + return model.Proxy{}, fmt.Errorf("invalid vless Url") } // 分割 serverInfo := strings.SplitN(parts[1], "#", 2) @@ -27,7 +27,7 @@ func ParseVless(proxy string) (model.Proxy, error) { return model.Proxy{}, err } if len(serverAndPort) != 2 { - return model.Proxy{}, fmt.Errorf("无效的 vless 服务器和端口") + return model.Proxy{}, fmt.Errorf("invalid vless") } // 处理端口 port, err := strconv.Atoi(strings.TrimSpace(serverAndPort[1])) diff --git a/parser/vmess.go b/parser/vmess.go index 0dcd268..60b846c 100644 --- a/parser/vmess.go +++ b/parser/vmess.go @@ -12,7 +12,7 @@ import ( func ParseVmess(proxy string) (model.Proxy, error) { // 判断是否以 vmess:// 开头 if !strings.HasPrefix(proxy, "vmess://") { - return model.Proxy{}, fmt.Errorf("无效的 vmess Url") + return model.Proxy{}, fmt.Errorf("invalid vmess Url") } // 解码 base64, err := DecodeBase64(strings.TrimPrefix(proxy, "vmess://")) diff --git a/utils/check_update.go b/utils/check_update.go new file mode 100644 index 0000000..bd6cf53 --- /dev/null +++ b/utils/check_update.go @@ -0,0 +1,33 @@ +package utils + +import ( + "encoding/json" + "errors" + "io" + "sub2clash/config" + "sub2clash/model" +) + +func CheckUpdate() (bool, string, error) { + get, err := Get("https://api.github.com/repos/nitezs/sub2clash/tags") + if err != nil { + return false, "", errors.New("get version info failed" + err.Error()) + + } + var version model.Tags + all, err := io.ReadAll(get.Body) + if err != nil { + return false, "", errors.New("get version info failed" + err.Error()) + + } + err = json.Unmarshal(all, &version) + if err != nil { + return false, "", errors.New("get version info failed" + err.Error()) + + } + if version[0].Name == config.Version { + return false, "", nil + } else { + return true, version[0].Name, nil + } +} diff --git a/utils/database/database.go b/utils/database/database.go index 3740ca9..2c5c02c 100644 --- a/utils/database/database.go +++ b/utils/database/database.go @@ -2,17 +2,29 @@ package database import ( "github.com/glebarez/sqlite" + "go.uber.org/zap" "gorm.io/gorm" + "path/filepath" + "sub2clash/logger" "sub2clash/model" + "sub2clash/utils" ) var DB *gorm.DB func ConnectDB() error { // 用上面的数据库连接初始化 gorm - db, err := gorm.Open(sqlite.Open("sub2clash.db"), &gorm.Config{}) + err := utils.MKDir("data") if err != nil { - panic(err) + return err + } + db, err := gorm.Open( + sqlite.Open(filepath.Join("data", "sub2clash.db")), &gorm.Config{ + Logger: nil, + }, + ) + if err != nil { + return err } if err != nil { return err @@ -24,3 +36,23 @@ func ConnectDB() error { } return nil } + +func FindShortLinkByUrl(url string, shortLink *model.ShortLink) *gorm.DB { + logger.Logger.Debug("find short link by url", zap.String("url", url)) + return DB.Where("url = ?", url).First(&shortLink) +} + +func FindShortLinkByHash(hash string, shortLink *model.ShortLink) *gorm.DB { + logger.Logger.Debug("find short link by hash", zap.String("hash", hash)) + return DB.Where("hash = ?", hash).First(&shortLink) +} + +func SaveShortLink(shortLink *model.ShortLink) { + logger.Logger.Debug("save short link", zap.String("hash", shortLink.Hash)) + DB.Save(shortLink) +} + +func FirstOrCreateShortLink(shortLink *model.ShortLink) { + logger.Logger.Debug("first or create short link", zap.String("hash", shortLink.Hash)) + DB.FirstOrCreate(shortLink) +} diff --git a/utils/mkdir.go b/utils/mkdir.go new file mode 100644 index 0000000..45d276a --- /dev/null +++ b/utils/mkdir.go @@ -0,0 +1,30 @@ +package utils + +import ( + "errors" + "os" +) + +func MKDir(dir string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, os.ModePerm) + if err != nil { + + return err + } + } + return nil +} + +func MkEssentialDir() error { + if err := MKDir("subs"); err != nil { + return errors.New("create subs dir failed" + err.Error()) + } + if err := MKDir("templates"); err != nil { + return errors.New("create templates dir failed" + err.Error()) + } + if err := MKDir("logs"); err != nil { + return errors.New("create logs dir failed" + err.Error()) + } + return nil +} diff --git a/utils/os.go b/utils/os.go deleted file mode 100644 index e318160..0000000 --- a/utils/os.go +++ /dev/null @@ -1,16 +0,0 @@ -package utils - -import ( - "os" -) - -func MKDir(dir string) error { - if _, err := os.Stat(dir); os.IsNotExist(err) { - err := os.MkdirAll(dir, os.ModePerm) - if err != nil { - - return err - } - } - return nil -} diff --git a/utils/proxy.go b/utils/proxy.go index 4623bde..c51b720 100644 --- a/utils/proxy.go +++ b/utils/proxy.go @@ -1,7 +1,6 @@ package utils import ( - "sort" "strings" "sub2clash/model" "sub2clash/parser" @@ -30,12 +29,10 @@ func GetContryName(proxy model.Proxy) string { func AddProxy( sub *model.Subscription, autotest bool, - lazy bool, sortStrategy string, - clashType model.ClashType, proxies ...model.Proxy, + lazy bool, clashType model.ClashType, proxies ...model.Proxy, ) { newCountryGroupNames := make([]string, 0) proxyTypes := model.GetSupportProxyTypes(clashType) - // 添加节点 for _, proxy := range proxies { if !proxyTypes[proxy.Type] { @@ -85,26 +82,6 @@ func AddProxy( newCountryGroupNames = append(newCountryGroupNames, countryName) } } - // 统计国家策略组数量 - countryGroupCount := 0 - for i := range sub.ProxyGroups { - if sub.ProxyGroups[i].IsCountryGrop { - countryGroupCount++ - } - } - // 对国家策略组进行排序 - switch sortStrategy { - case "sizeasc": - sort.Sort(model.ProxyGroupsSortBySize(sub.ProxyGroups[:countryGroupCount])) - case "sizedesc": - sort.Sort(sort.Reverse(model.ProxyGroupsSortBySize(sub.ProxyGroups[:countryGroupCount]))) - case "nameasc": - sort.Sort(model.ProxyGroupsSortByName(sub.ProxyGroups[:countryGroupCount])) - case "namedesc": - sort.Sort(sort.Reverse(model.ProxyGroupsSortByName(sub.ProxyGroups[:countryGroupCount]))) - default: - sort.Sort(model.ProxyGroupsSortByName(sub.ProxyGroups[:countryGroupCount])) - } } func ParseProxy(proxies ...string) []model.Proxy { diff --git a/utils/write_default_template.go b/utils/write_default_template.go new file mode 100644 index 0000000..07a60ae --- /dev/null +++ b/utils/write_default_template.go @@ -0,0 +1,37 @@ +package utils + +import ( + "os" + "path/filepath" + "sub2clash/config" +) + +func writeTemplate(path string, template string) error { + tPath := filepath.Join( + "templates", path, + ) + if _, err := os.Stat(tPath); os.IsNotExist(err) { + file, err := os.Create(tPath) + if err != nil { + return err + } + defer func(file *os.File) { + _ = file.Close() + }(file) + _, err = file.WriteString(template) + if err != nil { + return err + } + } + return nil +} + +func WriteDefalutTemplate(templateMeta string, templateClash string) error { + if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { + return err + } + if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil { + return err + } + return nil +}