4 次代碼提交 ff128a7275 ... f87c68ea68

作者 SHA1 備註 提交日期
  MHSanaei f87c68ea68 Add workflow to clean old GitHub Actions caches 1 周之前
  Ebrahim Tahernejad 687e8cf1ba [Windows] Use MSYS2 to fix the runtime CGO problem (#3689) 1 周之前
  Nebulosa 03f04194f2 Update geofiles according 304 http respond (#3690) 1 周之前
  Alimpo 248700a8a3 fix: trim whitespace from comma-separated list values in routing rules (#3734) 1 周之前

+ 31 - 0
.github/workflows/cleanup_caches.yml

@@ -0,0 +1,31 @@
+name: Cleanup Caches
+on:
+  schedule:
+    - cron: '0 3 * * 0'   # every Sunday
+  workflow_dispatch:
+
+jobs:
+  cleanup:
+    runs-on: ubuntu-latest
+    permissions:
+      actions: write
+    steps:
+      - name: Delete caches older than 3 days
+        env:
+          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          CUTOFF_DATE=$(date -d "3 days ago" -Ins --utc | sed 's/+0000/Z/')
+          echo "Deleting caches older than: $CUTOFF_DATE"
+          
+          CACHE_IDS=$(gh api --paginate repos/${{ github.repository }}/actions/caches \
+            --jq ".actions_caches[] | select(.last_accessed_at < \"$CUTOFF_DATE\") | .id" 2>/dev/null)
+          
+          if [ -z "$CACHE_IDS" ]; then
+            echo "No old caches found to delete."
+          else
+            echo "$CACHE_IDS" | while read CACHE_ID; do
+              echo "Deleting cache: $CACHE_ID"
+              gh api -X DELETE repos/${{ github.repository }}/actions/caches/$CACHE_ID
+            done
+            echo "Old caches deleted successfully."
+          fi

+ 28 - 7
.github/workflows/release.yml

@@ -173,16 +173,37 @@ jobs:
           go-version-file: go.mod
           check-latest: true
 
-      - name: Build 3X-UI for Windows
-        shell: pwsh
+      - name: Install MSYS2
+        uses: msys2/setup-msys2@v2
+        with:
+          msystem: MINGW64
+          update: true
+          install: >-
+            mingw-w64-x86_64-gcc
+            mingw-w64-x86_64-sqlite3
+            mingw-w64-x86_64-pkg-config
+
+      - name: Build 3X-UI for Windows (CGO)
+        shell: msys2 {0}
         run: |
-          $env:CGO_ENABLED="1"
-          $env:GOOS="windows"
-          $env:GOARCH="amd64"
+          export PATH="/c/hostedtoolcache/windows/go/$(ls /c/hostedtoolcache/windows/go | sort -V | tail -n1)/x64/bin:$PATH"
+
+          export CGO_ENABLED=1
+          export GOOS=windows
+          export GOARCH=amd64
+          export CC=x86_64-w64-mingw32-gcc
+
+          which go
+          go version
+          gcc --version
+
           go build -ldflags "-w -s" -o xui-release.exe -v main.go
-          
+
+      - name: Copy and download resources
+        shell: pwsh
+        run: |
           mkdir x-ui
-          Copy-Item xui-release.exe x-ui\
+          Copy-Item xui-release.exe x-ui\x-ui.exe
           mkdir x-ui\bin
           cd x-ui\bin
           

+ 9 - 9
sub/subController.go

@@ -3,8 +3,8 @@ package sub
 import (
 	"encoding/base64"
 	"fmt"
-	"strings"
 	"strconv"
+	"strings"
 
 	"github.com/mhsanaei/3x-ui/v2/config"
 
@@ -64,8 +64,8 @@ func NewSUBController(
 		subEncrypt:       encrypt,
 		updateInterval:   update,
 
-		subService:       sub,
-		subJsonService:   NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
+		subService:     sub,
+		subJsonService: NewSubJsonService(jsonFragment, jsonNoise, jsonMux, jsonRules, sub),
 	}
 	a.initRouter(g)
 	return a
@@ -170,13 +170,13 @@ func (a *SUBController) subJsons(c *gin.Context) {
 
 // ApplyCommonHeaders sets common HTTP headers for subscription responses including user info, update interval, and profile title.
 func (a *SUBController) ApplyCommonHeaders(
-	c *gin.Context, 
-	header, 
-	updateInterval, 
-	profileTitle string, 
+	c *gin.Context,
+	header,
+	updateInterval,
+	profileTitle string,
 	profileSupportUrl string,
-	profileUrl string, 
-	profileAnnounce string, 
+	profileUrl string,
+	profileAnnounce string,
 	profileEnableRouting bool,
 	profileRoutingRules string,
 ) {

+ 4 - 4
web/html/modals/xray_rule_modal.html

@@ -219,14 +219,14 @@
       rule = {};
       newRule = {};
       rule.type = "field";
-      rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
-      rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
+      rule.domain = value.domain.length > 0 ? value.domain.split(',').map(s => s.trim()) : [];
+      rule.ip = value.ip.length > 0 ? value.ip.split(',').map(s => s.trim()) : [];
       rule.port = value.port;
       rule.sourcePort = value.sourcePort;
       rule.vlessRoute = value.vlessRoute;
       rule.network = value.network;
-      rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',') : [];
-      rule.user = value.user.length > 0 ? value.user.split(',') : [];
+      rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',').map(s => s.trim()) : [];
+      rule.user = value.user.length > 0 ? value.user.split(',').map(s => s.trim()) : [];
       rule.inboundTag = value.inboundTag;
       rule.protocol = value.protocol;
       rule.attrs = Object.fromEntries(value.attrs);

+ 49 - 2
web/service/server.go

@@ -1087,13 +1087,60 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
 			return common.NewErrorf("Invalid geofile name: %s not in allowlist", fileName)
 		}
 	}
+
 	downloadFile := func(url, destPath string) error {
-		resp, err := http.Get(url)
+		var req *http.Request
+		req, err := http.NewRequest("GET", url, nil)
+		if err != nil {
+			return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
+		}
+
+		var localFileModTime time.Time
+		if fileInfo, err := os.Stat(destPath); err == nil {
+			localFileModTime = fileInfo.ModTime()
+			if !localFileModTime.IsZero() {
+				req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat))
+			}
+		}
+
+		client := &http.Client{}
+		resp, err := client.Do(req)
 		if err != nil {
 			return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
 		}
 		defer resp.Body.Close()
 
+		// Parse Last-Modified header from server
+		var serverModTime time.Time
+		serverModTimeStr := resp.Header.Get("Last-Modified")
+		if serverModTimeStr != "" {
+			parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr)
+			if err != nil {
+				logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err)
+			} else {
+				serverModTime = parsedTime
+			}
+		}
+
+		// Function to update local file's modification time
+		updateFileModTime := func() {
+			if !serverModTime.IsZero() {
+				if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil {
+					logger.Warningf("Failed to update modification time for %s: %v", destPath, err)
+				}
+			}
+		}
+
+		// Handle 304 Not Modified
+		if resp.StatusCode == http.StatusNotModified {
+			updateFileModTime()
+			return nil
+		}
+
+		if resp.StatusCode != http.StatusOK {
+			return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
+		}
+
 		file, err := os.Create(destPath)
 		if err != nil {
 			return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
@@ -1105,6 +1152,7 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
 			return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
 		}
 
+		updateFileModTime()
 		return nil
 	}
 
@@ -1114,7 +1162,6 @@ func (s *ServerService) UpdateGeofile(fileName string) error {
 		for _, file := range files {
 			// Sanitize the filename from our allowlist as an extra precaution
 			destPath := filepath.Join(config.GetBinFolderPath(), filepath.Base(file.FileName))
-
 			if err := downloadFile(file.URL, destPath); err != nil {
 				errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", file.FileName, err))
 			}

+ 3 - 3
web/service/tgbot.go

@@ -2322,9 +2322,9 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
 	// If pre-configured URIs are available, use them directly
 	if subURI != "" {
 		if !strings.HasSuffix(subURI, "/") {
-			subURI = subURI + "/" 
+			subURI = subURI + "/"
 		}
-		subURL = fmt.Sprintf("%s%s", subURI, client.SubID) 
+		subURL = fmt.Sprintf("%s%s", subURI, client.SubID)
 	} else {
 		subURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
 	}
@@ -2333,7 +2333,7 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
 		if !strings.HasSuffix(subJsonURI, "/") {
 			subJsonURI = subJsonURI + "/"
 		}
-		subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID) 
+		subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID)
 	} else {
 
 		subJsonURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)