mirror of
https://github.com/casdoor/casdoor.git
synced 2025-05-23 02:35:49 +08:00

* feat: add CLI downloader feature * feat: add CLI refresh endpoint and scheduler * feat: improve binary names mapping for different platforms and architectures * fix: format binary names in getBinaryNames function * fix: change file permission notation to octal in cli_downloader.go * feat: add isDemoMode check for CLI downloader features
503 lines
12 KiB
Go
503 lines
12 KiB
Go
package controllers
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/beego/beego"
|
|
)
|
|
|
|
const (
|
|
javaCliRepo = "https://api.github.com/repos/jcasbin/casbin-java-cli/releases/latest"
|
|
goCliRepo = "https://api.github.com/repos/casbin/casbin-go-cli/releases/latest"
|
|
rustCliRepo = "https://api.github.com/repos/casbin-rs/casbin-rust-cli/releases/latest"
|
|
downloadFolder = "bin"
|
|
)
|
|
|
|
type ReleaseInfo struct {
|
|
TagName string `json:"tag_name"`
|
|
Assets []struct {
|
|
Name string `json:"name"`
|
|
URL string `json:"browser_download_url"`
|
|
} `json:"assets"`
|
|
}
|
|
|
|
// @Title getBinaryNames
|
|
// @Description Get binary names for different platforms and architectures
|
|
// @Success 200 {map[string]string} map[string]string "Binary names map"
|
|
func getBinaryNames() map[string]string {
|
|
const (
|
|
golang = "go"
|
|
java = "java"
|
|
rust = "rust"
|
|
)
|
|
|
|
arch := runtime.GOARCH
|
|
archMap := map[string]struct{ goArch, rustArch string }{
|
|
"amd64": {"x86_64", "x86_64"},
|
|
"arm64": {"arm64", "aarch64"},
|
|
}
|
|
|
|
archNames, ok := archMap[arch]
|
|
if !ok {
|
|
archNames = struct{ goArch, rustArch string }{arch, arch}
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
return map[string]string{
|
|
golang: fmt.Sprintf("casbin-go-cli_Windows_%s.zip", archNames.goArch),
|
|
java: "casbin-java-cli.jar",
|
|
rust: fmt.Sprintf("casbin-rust-cli-%s-pc-windows-gnu", archNames.rustArch),
|
|
}
|
|
case "darwin":
|
|
return map[string]string{
|
|
golang: fmt.Sprintf("casbin-go-cli_Darwin_%s.tar.gz", archNames.goArch),
|
|
java: "casbin-java-cli.jar",
|
|
rust: fmt.Sprintf("casbin-rust-cli-%s-apple-darwin", archNames.rustArch),
|
|
}
|
|
case "linux":
|
|
return map[string]string{
|
|
golang: fmt.Sprintf("casbin-go-cli_Linux_%s.tar.gz", archNames.goArch),
|
|
java: "casbin-java-cli.jar",
|
|
rust: fmt.Sprintf("casbin-rust-cli-%s-unknown-linux-gnu", archNames.rustArch),
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// @Title getFinalBinaryName
|
|
// @Description Get final binary name for specific language
|
|
// @Param lang string true "Language type (go/java/rust)"
|
|
// @Success 200 {string} string "Final binary name"
|
|
func getFinalBinaryName(lang string) string {
|
|
switch lang {
|
|
case "go":
|
|
if runtime.GOOS == "windows" {
|
|
return "casbin-go-cli.exe"
|
|
}
|
|
return "casbin-go-cli"
|
|
case "java":
|
|
return "casbin-java-cli.jar"
|
|
case "rust":
|
|
if runtime.GOOS == "windows" {
|
|
return "casbin-rust-cli.exe"
|
|
}
|
|
return "casbin-rust-cli"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// @Title getLatestCLIURL
|
|
// @Description Get latest CLI download URL from GitHub
|
|
// @Param repoURL string true "GitHub repository URL"
|
|
// @Param language string true "Language type"
|
|
// @Success 200 {string} string "Download URL and version"
|
|
func getLatestCLIURL(repoURL string, language string) (string, string, error) {
|
|
resp, err := http.Get(repoURL)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var release ReleaseInfo
|
|
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
binaryNames := getBinaryNames()
|
|
if binaryNames == nil {
|
|
return "", "", fmt.Errorf("unsupported OS: %s", runtime.GOOS)
|
|
}
|
|
|
|
binaryName := binaryNames[language]
|
|
for _, asset := range release.Assets {
|
|
if asset.Name == binaryName {
|
|
return asset.URL, release.TagName, nil
|
|
}
|
|
}
|
|
|
|
return "", "", fmt.Errorf("no suitable binary found for OS: %s, language: %s", runtime.GOOS, language)
|
|
}
|
|
|
|
// @Title extractGoCliFile
|
|
// @Description Extract the Go CLI file
|
|
// @Param filePath string true "The file path"
|
|
// @Success 200 {string} string "The extracted file path"
|
|
// @router /extractGoCliFile [post]
|
|
func extractGoCliFile(filePath string) error {
|
|
tempDir := filepath.Join(downloadFolder, "temp")
|
|
if err := os.MkdirAll(tempDir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
if runtime.GOOS == "windows" {
|
|
if err := unzipFile(filePath, tempDir); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := untarFile(filePath, tempDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
execName := "casbin-go-cli"
|
|
if runtime.GOOS == "windows" {
|
|
execName += ".exe"
|
|
}
|
|
|
|
var execPath string
|
|
err := filepath.Walk(tempDir, func(path string, info os.FileInfo, err error) error {
|
|
if info.Name() == execName {
|
|
execPath = path
|
|
return nil
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
finalPath := filepath.Join(downloadFolder, execName)
|
|
if err := os.Rename(execPath, finalPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.Remove(filePath)
|
|
}
|
|
|
|
// @Title unzipFile
|
|
// @Description Unzip the file
|
|
// @Param zipPath string true "The zip file path"
|
|
// @Param destDir string true "The destination directory"
|
|
// @Success 200 {string} string "The extracted file path"
|
|
// @router /unzipFile [post]
|
|
func unzipFile(zipPath, destDir string) error {
|
|
r, err := zip.OpenReader(zipPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
|
|
for _, f := range r.File {
|
|
fpath := filepath.Join(destDir, f.Name)
|
|
|
|
if f.FileInfo().IsDir() {
|
|
os.MkdirAll(fpath, os.ModePerm)
|
|
continue
|
|
}
|
|
|
|
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
outFile.Close()
|
|
return err
|
|
}
|
|
|
|
_, err = io.Copy(outFile, rc)
|
|
outFile.Close()
|
|
rc.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// @Title untarFile
|
|
// @Description Untar the file
|
|
// @Param tarPath string true "The tar file path"
|
|
// @Param destDir string true "The destination directory"
|
|
// @Success 200 {string} string "The extracted file path"
|
|
// @router /untarFile [post]
|
|
func untarFile(tarPath, destDir string) error {
|
|
file, err := os.Open(tarPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
gzr, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer gzr.Close()
|
|
|
|
tr := tar.NewReader(gzr)
|
|
|
|
for {
|
|
header, err := tr.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
path := filepath.Join(destDir, header.Name)
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
|
return err
|
|
}
|
|
case tar.TypeReg:
|
|
outFile, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(outFile, tr); err != nil {
|
|
outFile.Close()
|
|
return err
|
|
}
|
|
outFile.Close()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// @Title createJavaCliWrapper
|
|
// @Description Create the Java CLI wrapper
|
|
// @Param binPath string true "The binary path"
|
|
// @Success 200 {string} string "The created file path"
|
|
// @router /createJavaCliWrapper [post]
|
|
func createJavaCliWrapper(binPath string) error {
|
|
if runtime.GOOS == "windows" {
|
|
// Create a Windows CMD file
|
|
cmdPath := filepath.Join(binPath, "casbin-java-cli.cmd")
|
|
cmdContent := fmt.Sprintf(`@echo off
|
|
java -jar "%s\casbin-java-cli.jar" %%*`, binPath)
|
|
|
|
err := os.WriteFile(cmdPath, []byte(cmdContent), 0o755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
|
}
|
|
} else {
|
|
// Create Unix shell script
|
|
shPath := filepath.Join(binPath, "casbin-java-cli")
|
|
shContent := fmt.Sprintf(`#!/bin/sh
|
|
java -jar "%s/casbin-java-cli.jar" "$@"`, binPath)
|
|
|
|
err := os.WriteFile(shPath, []byte(shContent), 0o755)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create Java CLI wrapper: %v", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// @Title downloadCLI
|
|
// @Description Download and setup CLI tools
|
|
// @Success 200 {error} error "Error if any"
|
|
func downloadCLI() error {
|
|
pathEnv := os.Getenv("PATH")
|
|
binPath, err := filepath.Abs(downloadFolder)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get absolute path to download directory: %v", err)
|
|
}
|
|
|
|
if !strings.Contains(pathEnv, binPath) {
|
|
newPath := fmt.Sprintf("%s%s%s", binPath, string(os.PathListSeparator), pathEnv)
|
|
if err := os.Setenv("PATH", newPath); err != nil {
|
|
return fmt.Errorf("failed to update PATH environment variable: %v", err)
|
|
}
|
|
}
|
|
|
|
if err := os.MkdirAll(downloadFolder, 0o755); err != nil {
|
|
return fmt.Errorf("failed to create download directory: %v", err)
|
|
}
|
|
|
|
repos := map[string]string{
|
|
"java": javaCliRepo,
|
|
"go": goCliRepo,
|
|
"rust": rustCliRepo,
|
|
}
|
|
|
|
for lang, repo := range repos {
|
|
cliURL, version, err := getLatestCLIURL(repo, lang)
|
|
if err != nil {
|
|
fmt.Printf("failed to get %s CLI URL: %v\n", lang, err)
|
|
continue
|
|
}
|
|
|
|
originalPath := filepath.Join(downloadFolder, getBinaryNames()[lang])
|
|
fmt.Printf("downloading %s CLI: %s\n", lang, cliURL)
|
|
|
|
resp, err := http.Get(cliURL)
|
|
if err != nil {
|
|
fmt.Printf("failed to download %s CLI: %v\n", lang, err)
|
|
continue
|
|
}
|
|
|
|
func() {
|
|
defer resp.Body.Close()
|
|
out, err := os.Create(originalPath)
|
|
if err != nil {
|
|
fmt.Printf("failed to create %s CLI file: %v\n", lang, err)
|
|
return
|
|
}
|
|
defer out.Close()
|
|
|
|
if _, err = io.Copy(out, resp.Body); err != nil {
|
|
fmt.Printf("failed to save %s CLI: %v\n", lang, err)
|
|
return
|
|
}
|
|
}()
|
|
|
|
if lang == "go" {
|
|
if err := extractGoCliFile(originalPath); err != nil {
|
|
fmt.Printf("failed to extract Go CLI: %v\n", err)
|
|
continue
|
|
}
|
|
} else {
|
|
finalPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
|
if err := os.Rename(originalPath, finalPath); err != nil {
|
|
fmt.Printf("failed to rename %s CLI: %v\n", lang, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
if runtime.GOOS != "windows" {
|
|
execPath := filepath.Join(downloadFolder, getFinalBinaryName(lang))
|
|
if err := os.Chmod(execPath, 0o755); err != nil {
|
|
fmt.Printf("failed to set %s CLI execution permission: %v\n", lang, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
fmt.Printf("downloaded %s CLI version: %s\n", lang, version)
|
|
|
|
if lang == "java" {
|
|
if err := createJavaCliWrapper(binPath); err != nil {
|
|
fmt.Printf("failed to create Java CLI wrapper: %v\n", err)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// @Title RefreshEngines
|
|
// @Tag CLI API
|
|
// @Description Refresh all CLI engines
|
|
// @Param m query string true "Hash for request validation"
|
|
// @Param t query string true "Timestamp for request validation"
|
|
// @Success 200 {object} controllers.Response The Response object
|
|
// @router /refresh-engines [post]
|
|
func (c *ApiController) RefreshEngines() {
|
|
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
|
c.ResponseError("refresh engines is only available in demo mode")
|
|
return
|
|
}
|
|
|
|
hash := c.Input().Get("m")
|
|
timestamp := c.Input().Get("t")
|
|
|
|
if hash == "" || timestamp == "" {
|
|
c.ResponseError("invalid identifier")
|
|
return
|
|
}
|
|
|
|
requestTime, err := time.Parse(time.RFC3339, timestamp)
|
|
if err != nil {
|
|
c.ResponseError("invalid identifier")
|
|
return
|
|
}
|
|
|
|
timeDiff := time.Since(requestTime)
|
|
if timeDiff > 5*time.Minute || timeDiff < -5*time.Minute {
|
|
c.ResponseError("invalid identifier")
|
|
return
|
|
}
|
|
|
|
version := "casbin-editor-v1"
|
|
rawString := fmt.Sprintf("%s|%s", version, timestamp)
|
|
|
|
hasher := sha256.New()
|
|
hasher.Write([]byte(rawString))
|
|
calculatedHash := strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))
|
|
|
|
if calculatedHash != strings.ToLower(hash) {
|
|
c.ResponseError("invalid identifier")
|
|
return
|
|
}
|
|
|
|
err = downloadCLI()
|
|
if err != nil {
|
|
c.ResponseError(fmt.Sprintf("failed to refresh engines: %v", err))
|
|
return
|
|
}
|
|
|
|
c.ResponseOk(map[string]string{
|
|
"status": "success",
|
|
"message": "CLI engines updated successfully",
|
|
})
|
|
}
|
|
|
|
// @Title ScheduleCLIUpdater
|
|
// @Description Start periodic CLI update scheduler
|
|
func ScheduleCLIUpdater() {
|
|
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
|
return
|
|
}
|
|
|
|
ticker := time.NewTicker(1 * time.Hour)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
err := downloadCLI()
|
|
if err != nil {
|
|
fmt.Printf("failed to update CLI: %v\n", err)
|
|
} else {
|
|
fmt.Println("CLI updated successfully")
|
|
}
|
|
}
|
|
}
|
|
|
|
// @Title DownloadCLI
|
|
// @Description Download the CLI
|
|
// @Success 200 {string} string "The downloaded file path"
|
|
// @router /downloadCLI [post]
|
|
func DownloadCLI() error {
|
|
return downloadCLI()
|
|
}
|
|
|
|
// @Title InitCLIDownloader
|
|
// @Description Initialize CLI downloader and start update scheduler
|
|
func InitCLIDownloader() {
|
|
if !beego.AppConfig.DefaultBool("isDemoMode", false) {
|
|
return
|
|
}
|
|
|
|
err := DownloadCLI()
|
|
if err != nil {
|
|
fmt.Printf("failed to initialize CLI downloader: %v\n", err)
|
|
}
|
|
|
|
go ScheduleCLIUpdater()
|
|
}
|