feat: add CLI version cache and proxy support (#3565)

* feat: add CLI version cache mechanism

* feat: add /api/refresh-engines to allowed endpoints in demo mode

* feat: add proxy support for cli downloader

* feat: add SafeGoroutine for CLIDownloader initialization

* refactor: optimize code structure
This commit is contained in:
Coki 2025-02-08 19:34:19 +08:00 committed by GitHub
parent 7f9f7c6468
commit 5661942175
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 17 deletions

View File

@ -157,7 +157,7 @@ func IsAllowed(subOwner string, subName string, method string, urlPath string, o
func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool { func isAllowedInDemoMode(subOwner string, subName string, method string, urlPath string, objOwner string, objName string) bool {
if method == "POST" { if method == "POST" {
if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" { if strings.HasPrefix(urlPath, "/api/login") || urlPath == "/api/logout" || urlPath == "/api/signup" || urlPath == "/api/callback" || urlPath == "/api/send-verification-code" || urlPath == "/api/send-email" || urlPath == "/api/verify-captcha" || urlPath == "/api/verify-code" || urlPath == "/api/check-user-password" || strings.HasPrefix(urlPath, "/api/mfa/") || urlPath == "/api/webhook" || urlPath == "/api/get-qrcode" || urlPath == "/api/refresh-engines" {
return true return true
} else if urlPath == "/api/update-user" { } else if urlPath == "/api/update-user" {
// Allow ordinary users to update their own information // Allow ordinary users to update their own information

View File

@ -23,9 +23,68 @@ import (
"os/exec" "os/exec"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
) )
type CLIVersionInfo struct {
Version string
BinaryPath string
BinaryTime time.Time
}
var (
cliVersionCache = make(map[string]*CLIVersionInfo)
cliVersionMutex sync.RWMutex
)
// getCLIVersion
// @Title getCLIVersion
// @Description Get CLI version with cache mechanism
// @Param language string The language of CLI (go/java/rust etc.)
// @Return string The version string of CLI
// @Return error Error if CLI execution fails
func getCLIVersion(language string) (string, error) {
binaryName := fmt.Sprintf("casbin-%s-cli", language)
binaryPath, err := exec.LookPath(binaryName)
if err != nil {
return "", fmt.Errorf("executable file not found: %v", err)
}
fileInfo, err := os.Stat(binaryPath)
if err != nil {
return "", fmt.Errorf("failed to get binary info: %v", err)
}
cliVersionMutex.RLock()
if info, exists := cliVersionCache[language]; exists {
if info.BinaryPath == binaryPath && info.BinaryTime == fileInfo.ModTime() {
cliVersionMutex.RUnlock()
return info.Version, nil
}
}
cliVersionMutex.RUnlock()
cmd := exec.Command(binaryName, "--version")
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to get CLI version: %v", err)
}
version := strings.TrimSpace(string(output))
cliVersionMutex.Lock()
cliVersionCache[language] = &CLIVersionInfo{
Version: version,
BinaryPath: binaryPath,
BinaryTime: fileInfo.ModTime(),
}
cliVersionMutex.Unlock()
return version, nil
}
func processArgsToTempFiles(args []string) ([]string, []string, error) { func processArgsToTempFiles(args []string) ([]string, []string, error) {
tempFiles := []string{} tempFiles := []string{}
newArgs := []string{} newArgs := []string{}
@ -93,6 +152,16 @@ func (c *ApiController) RunCasbinCommand() {
return return
} }
if len(args) > 0 && args[0] == "--version" {
version, err := getCLIVersion(language)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(version)
return
}
tempFiles, processedArgs, err := processArgsToTempFiles(args) tempFiles, processedArgs, err := processArgsToTempFiles(args)
defer func() { defer func() {
for _, file := range tempFiles { for _, file := range tempFiles {

View File

@ -9,7 +9,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -17,6 +16,8 @@ import (
"time" "time"
"github.com/beego/beego" "github.com/beego/beego"
"github.com/casdoor/casdoor/proxy"
"github.com/casdoor/casdoor/util"
) )
const ( const (
@ -108,9 +109,10 @@ func getFinalBinaryName(lang string) string {
// @Param language string true "Language type" // @Param language string true "Language type"
// @Success 200 {string} string "Download URL and version" // @Success 200 {string} string "Download URL and version"
func getLatestCLIURL(repoURL string, language string) (string, string, error) { func getLatestCLIURL(repoURL string, language string) (string, string, error) {
resp, err := http.Get(repoURL) client := proxy.GetHttpClient(repoURL)
resp, err := client.Get(repoURL)
if err != nil { if err != nil {
return "", "", err return "", "", fmt.Errorf("failed to fetch release info: %v", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
@ -346,7 +348,8 @@ func downloadCLI() error {
originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang]) originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang])
fmt.Printf("downloading %s CLI: %s\n", lang, cliURL) fmt.Printf("downloading %s CLI: %s\n", lang, cliURL)
resp, err := http.Get(cliURL) client := proxy.GetHttpClient(cliURL)
resp, err := client.Get(cliURL)
if err != nil { if err != nil {
fmt.Printf("failed to download %s CLI: %v\n", lang, err) fmt.Printf("failed to download %s CLI: %v\n", lang, err)
continue continue
@ -354,15 +357,27 @@ func downloadCLI() error {
func() { func() {
defer resp.Body.Close() defer resp.Body.Close()
out, err := os.Create(originalPath)
if err != nil { if err := os.MkdirAll(filepath.Dir(originalPath), 0o755); err != nil {
fmt.Printf("failed to create %s CLI file: %v\n", lang, err) fmt.Printf("failed to create directory for %s CLI: %v\n", lang, err)
return return
} }
defer out.Close()
if _, err = io.Copy(out, resp.Body); err != nil { tmpFile := originalPath + ".tmp"
fmt.Printf("failed to save %s CLI: %v\n", lang, err) out, err := os.Create(tmpFile)
if err != nil {
fmt.Printf("failed to create or write %s CLI: %v\n", lang, err)
return
}
defer func() {
out.Close()
os.Remove(tmpFile)
}()
if _, err = io.Copy(out, resp.Body); err != nil ||
out.Close() != nil ||
os.Rename(tmpFile, originalPath) != nil {
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
return return
} }
}() }()
@ -493,10 +508,12 @@ func InitCLIDownloader() {
return return
} }
err := DownloadCLI() util.SafeGoroutine(func() {
if err != nil { err := DownloadCLI()
fmt.Printf("failed to initialize CLI downloader: %v\n", err) if err != nil {
} fmt.Printf("failed to initialize CLI downloader: %v\n", err)
}
go ScheduleCLIUpdater() ScheduleCLIUpdater()
})
} }

View File

@ -46,7 +46,7 @@ func main() {
object.InitCasvisorConfig() object.InitCasvisorConfig()
util.SafeGoroutine(func() { object.RunSyncUsersJob() }) util.SafeGoroutine(func() { object.RunSyncUsersJob() })
controllers.InitCLIDownloader() util.SafeGoroutine(func() { controllers.InitCLIDownloader() })
// beego.DelStaticPath("/static") // beego.DelStaticPath("/static")
// beego.SetStaticPath("/static", "web/build/static") // beego.SetStaticPath("/static", "web/build/static")