From fce2d2eb79366ba92777de1a73286b02ef9e2b71 Mon Sep 17 00:00:00 2001 From: nitezs Date: Sun, 17 Sep 2023 16:20:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=BD=93=E8=AE=A2?= =?UTF-8?q?=E9=98=85=E9=93=BE=E6=8E=A5=E6=9C=89=E5=A4=9A=E4=B8=AA=20clash?= =?UTF-8?q?=20=E9=85=8D=E7=BD=AE=E6=97=B6=E4=B8=A2=E5=A4=B1=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit modify: 修改数据库路径 modify: 修改短链生成逻辑 update: 增加检测更新 modify: 统一输出信息为英文 update: 修改 Dockerfile fix: 修改 docker.yml modify: 调整代码结构 --- .env.example | 3 +- .github/workflows/docker.yml | 6 +++ .github/workflows/go.yml | 46 +--------------------- .goreleaser.yaml | 4 +- Dockerfile | 6 ++- README.md | 22 +++++------ api/controller/default.go | 34 +++++++++++++---- api/controller/short_link.go | 40 +++++++++++++++----- api/route.go | 7 +++- api/templates/index.html | 7 ++-- config/config.go | 35 ++++++++++++----- docker-compose.yml | 3 +- go.mod | 8 ++-- go.sum | 18 ++++----- logger/logger.go | 19 ++++------ main.go | 67 ++++++++++++++++----------------- model/github.go | 12 ++++++ model/proxy_group.go | 1 - parser/ss.go | 8 ++-- parser/ssr.go | 2 +- parser/trojan.go | 6 +-- parser/vless.go | 6 +-- parser/vmess.go | 2 +- utils/check_update.go | 33 ++++++++++++++++ utils/database/database.go | 10 ++++- utils/mkdir.go | 30 +++++++++++++++ utils/os.go | 16 -------- utils/proxy.go | 25 +----------- utils/write_default_template.go | 37 ++++++++++++++++++ 29 files changed, 307 insertions(+), 206 deletions(-) create mode 100644 model/github.go create mode 100644 utils/check_update.go create mode 100644 utils/mkdir.go delete mode 100644 utils/os.go create mode 100644 utils/write_default_template.go diff --git a/.env.example b/.env.example index ac2538b..59b631e 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,5 @@ CLASH_TEMPLATE=clash_template.json REQUEST_RETRY_TIMES=3 REQUEST_MAX_FILE_SIZE=1048576 CACHE_EXPIRE=300 -LOG_LEVEL=info \ No newline at end of file +LOG_LEVEL=info +BASE_PATH=/ \ No newline at end of file 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/.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 fbc9882..ba8b5d5 100644 --- a/README.md +++ b/README.md @@ -25,15 +25,17 @@ 可以通过编辑 .env 文件来修改默认配置,docker 直接添加环境变量 -| 变量名 | 说明 | 默认值 | -|-----------------------|----------------------------------------|-----------------------| -| PORT | 端口 | `8011` | -| META_TEMPLATE | meta 模板文件名 | `template_meta.yaml` | -| CLASH_TEMPLATE | clash 模板文件名 | `template_clash.yaml` | -| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` | -| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` | -| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` | -| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` | +| 变量名 | 说明 | 默认值 | +|-----------------------|-----------------------------------------------------------|-----------------------| +| BASE_PATH | 程序运行子路径,例如将服务反代在 `https://example.com/sub` 则此变量值应为 `/sub` | `/` | +| PORT | 端口 | `8011` | +| META_TEMPLATE | meta 模板文件名 | `template_meta.yaml` | +| CLASH_TEMPLATE | clash 模板文件名 | `template_clash.yaml` | +| REQUEST_RETRY_TIMES | Get 请求重试次数 | `3` | +| REQUEST_MAX_FILE_SIZE | Get 请求订阅文件最大大小(byte) | `1048576` | +| CACHE_EXPIRE | 订阅缓存时间(秒) | `300` | +| LOG_LEVEL | 日志等级,可选值 `debug`,`info`,`warn`,`error` | `info` | +| SHORT_LINK_LENGTH | 短链长度 | `6` | ### API @@ -63,5 +65,3 @@ [代理链接解析](./parser)还没有经过严格测试,可能会出现解析错误的情况,如果出现问题请提交 issue ## TODO - -- [x] 可视化面板 \ No newline at end of file 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 ae5035d..648fcf8 100644 --- a/api/controller/short_link.go +++ b/api/controller/short_link.go @@ -1,10 +1,13 @@ package controller import ( - "crypto/sha256" - "encoding/hex" + "errors" "github.com/gin-gonic/gin" + "gorm.io/gorm" + "net/http" + "sub2clash/config" "sub2clash/model" + "sub2clash/utils" "sub2clash/utils/database" "sub2clash/validator" "time" @@ -16,11 +19,27 @@ 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[:]) + // 生成hash + hash := utils.RandomString(config.Default.ShortLinkLength) // 存入数据库 + var item model.ShortLink + result := database.DB.Model(&model.ShortLink{}).Where("url = ?", params.Url).First(&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.DB.Model(&model.ShortLink{}).Where("hash = ?", hash).First(&item) + for result.Error == nil { + hash = utils.RandomString(config.Default.ShortLinkLength) + result = database.DB.Model(&model.ShortLink{}).Where("hash = ?", hash).First(&item) + } + // 创建记录 database.DB.FirstOrCreate( &model.ShortLink{ Hash: hash, @@ -38,13 +57,14 @@ func ShortLinkGetHandler(c *gin.Context) { // 查询数据库 var shortLink model.ShortLink result := database.DB.Where("hash = ?", hash).First(&shortLink) - // 更新最后访问时间 - shortLink.LastRequestTime = time.Now().Unix() - database.DB.Save(&shortLink) // 重定向 if result.Error != nil { c.String(404, "未找到短链接") return } - c.Redirect(302, "/"+shortLink.Url) + // 更新最后访问时间 + shortLink.LastRequestTime = time.Now().Unix() + database.DB.Save(&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 9cb3a4b..a21652a 100644 --- a/api/templates/index.html +++ b/api/templates/index.html @@ -95,13 +95,13 @@

sub2clash

- +
- +
@@ -136,6 +136,7 @@

sub2clash

@@ -298,7 +299,7 @@

sub2clash

function generateShortLink() { const apiShortLink = document.getElementById("apiShortLink"); - axios.post("/short", { + axios.post("./short", { "url": generateURI() }, { headers: { diff --git a/config/config.go b/config/config.go index dd9ee6e..9a7e6c4 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "errors" "github.com/joho/godotenv" "os" "strconv" @@ -14,11 +15,15 @@ type Config struct { RequestMaxFileSize int64 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", @@ -27,15 +32,13 @@ func init() { Port: 8011, CacheExpire: 60 * 5, LogLevel: "info", + BasePath: "/", } - err := godotenv.Load() - if err != nil { - return - } + _ = 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 } @@ -48,25 +51,39 @@ 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) } if os.Getenv("LOG_LEVEL") != "" { Default.LogLevel = os.Getenv("LOG_LEVEL") } + if os.Getenv("BASE_PATH") != "" { + Default.BasePath = os.Getenv("BASE_PATH") + if Default.BasePath[len(Default.BasePath)-1] != '/' { + 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/go.mod b/go.mod index 1d817a0..a68fd39 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/glebarez/sqlite v1.9.0 github.com/joho/godotenv v1.5.1 - go.uber.org/zap v1.25.0 + go.uber.org/zap v1.26.0 golang.org/x/text v0.13.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.25.4 @@ -44,8 +44,8 @@ require ( golang.org/x/net v0.15.0 // indirect golang.org/x/sys v0.12.0 // indirect google.golang.org/protobuf v1.31.0 // indirect - modernc.org/libc v1.24.1 // indirect + modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.6.0 // indirect - modernc.org/sqlite v1.25.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect ) diff --git a/go.sum b/go.sum index 171c894..90f2f48 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= -github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= @@ -91,8 +89,8 @@ go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= -go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -117,13 +115,13 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= -modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= -modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA= -modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 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) + // 加载配置 + err := config.LoadConfig() + // 初始化日志 + logger.InitLogger(config.Default.LogLevel) + if err != nil { + logger.Logger.Panic("load config failed", zap.Error(err)) } - if err := utils.MKDir("templates"); err != nil { - os.Exit(1) + // 检查更新 + 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") } - if err := writeTemplate(config.Default.MetaTemplate, templateMeta); err != nil { - os.Exit(1) + // 创建文件夹 + err = utils.MkEssentialDir() + if err != nil { + logger.Logger.Panic("create essential dir failed", zap.Error(err)) } - if err := writeTemplate(config.Default.ClashTemplate, templateClash); err != nil { - os.Exit(1) + // 写入默认模板 + err = utils.WriteDefalutTemplate(templateMeta, templateClash) + if err != nil { + logger.Logger.Panic("write default template failed", zap.Error(err)) } - err := database.ConnectDB() + // 连接数据库 + err = database.ConnectDB() if err != nil { - panic(err) + 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..9d9700b 100644 --- a/utils/database/database.go +++ b/utils/database/database.go @@ -3,16 +3,22 @@ package database import ( "github.com/glebarez/sqlite" "gorm.io/gorm" + "path/filepath" "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{}) + if err != nil { + return err } if err != nil { return err 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 +}