Merge branch perfect-panel/master/server into develop
This commit is contained in:
@@ -2,6 +2,8 @@ package constant
|
||||
|
||||
// Version PPanel version
|
||||
var (
|
||||
Version = "unknown version"
|
||||
BuildTime = "unknown time"
|
||||
Version = "unknown version"
|
||||
BuildTime = "unknown time"
|
||||
Repository = "https://github.com/perfect-panel/server"
|
||||
ServiceName = "ApiService"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
func FixedUniqueString(s string, length int, alphabet string) (string, error) {
|
||||
if alphabet == "" {
|
||||
alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
}
|
||||
if length <= 0 {
|
||||
return "", errors.New("length must be > 0")
|
||||
}
|
||||
if length > len(alphabet) {
|
||||
return "", errors.New("length greater than available unique characters")
|
||||
}
|
||||
|
||||
// Generate deterministic seed from SHA256
|
||||
hash := sha256.Sum256([]byte(s))
|
||||
seed := int64(binary.LittleEndian.Uint64(hash[:8])) // 前 8 字节
|
||||
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
|
||||
// Copy alphabet to mutable array
|
||||
data := []rune(alphabet)
|
||||
|
||||
// Deterministic shuffle (Fisher–Yates)
|
||||
for i := len(data) - 1; i > 0; i-- {
|
||||
j := r.Intn(i + 1)
|
||||
data[i], data[j] = data[j], data[i]
|
||||
}
|
||||
|
||||
// Take first N characters
|
||||
return string(data[:length]), nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFixedUniqueString(t *testing.T) {
|
||||
a := "example"
|
||||
b := "example1"
|
||||
c := "example"
|
||||
|
||||
strA1, err := FixedUniqueString(a, 8, "")
|
||||
strB1, err := FixedUniqueString(b, 8, "")
|
||||
strC1, err := FixedUniqueString(c, 8, "")
|
||||
if err != nil {
|
||||
t.Logf("Error: %v", err.Error())
|
||||
return
|
||||
}
|
||||
if strA1 != strC1 {
|
||||
t.Errorf("Expected strA1 and strC1 to be equal, got %s and %s", strA1, strC1)
|
||||
}
|
||||
if strA1 == strB1 {
|
||||
t.Errorf("Expected strA1 and strB1 to be different, got %s and %s", strA1, strB1)
|
||||
}
|
||||
t.Logf("strA1 and strB1 are not equal, strA1: %s, strB1: %s", strA1, strB1)
|
||||
t.Logf("strA1 and strC1 are equal,strA1: %s, strC1: %s", strA1, strC1)
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
githubAPIURL = "https://api.github.com/repos/OmnTeam/server/releases/latest"
|
||||
githubRelURL = "https://github.com/OmnTeam/server/releases"
|
||||
)
|
||||
|
||||
// Release represents a GitHub release
|
||||
type Release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
Assets []Asset `json:"assets"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
|
||||
// Asset represents a release asset
|
||||
type Asset struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
// Updater handles auto-update functionality
|
||||
type Updater struct {
|
||||
CurrentVersion string
|
||||
Owner string
|
||||
Repo string
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// NewUpdater creates a new updater instance
|
||||
func NewUpdater() *Updater {
|
||||
return &Updater{
|
||||
CurrentVersion: constant.Version,
|
||||
Owner: "OmnTeam",
|
||||
Repo: "server",
|
||||
HTTPClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CheckForUpdates checks if a new version is available
|
||||
func (u *Updater) CheckForUpdates() (*Release, bool, error) {
|
||||
req, err := http.NewRequest("GET", githubAPIURL, nil)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
resp, err := u.HTTPClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to fetch release info: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var release Release
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
// Skip draft and prerelease versions
|
||||
if release.Draft || release.Prerelease {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Compare versions
|
||||
hasUpdate := u.compareVersions(release.TagName, u.CurrentVersion)
|
||||
return &release, hasUpdate, nil
|
||||
}
|
||||
|
||||
// compareVersions compares two version strings
|
||||
// Returns true if newVersion is newer than currentVersion
|
||||
func (u *Updater) compareVersions(newVersion, currentVersion string) bool {
|
||||
// Remove 'v' prefix if present
|
||||
newVersion = strings.TrimPrefix(newVersion, "v")
|
||||
currentVersion = strings.TrimPrefix(currentVersion, "v")
|
||||
|
||||
// Handle "unknown version" case
|
||||
if currentVersion == "unknown version" || currentVersion == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return newVersion != currentVersion
|
||||
}
|
||||
|
||||
// Download downloads the appropriate binary for the current platform
|
||||
func (u *Updater) Download(release *Release) (string, error) {
|
||||
assetName := u.getAssetName()
|
||||
|
||||
var targetAsset *Asset
|
||||
for _, asset := range release.Assets {
|
||||
if asset.Name == assetName {
|
||||
targetAsset = &asset
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetAsset == nil {
|
||||
return "", fmt.Errorf("no suitable asset found for %s", assetName)
|
||||
}
|
||||
|
||||
// Create temp directory
|
||||
tempDir, err := os.MkdirTemp("", "ppanel-update-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp directory: %w", err)
|
||||
}
|
||||
|
||||
// Download the file
|
||||
resp, err := u.HTTPClient.Get(targetAsset.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to download asset: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to download: status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Read the entire file into memory
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to read download: %w", err)
|
||||
}
|
||||
|
||||
// Extract the binary
|
||||
binaryPath, err := u.extractBinary(data, tempDir, assetName)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return "", fmt.Errorf("failed to extract binary: %w", err)
|
||||
}
|
||||
|
||||
return binaryPath, nil
|
||||
}
|
||||
|
||||
// getAssetName returns the expected asset name for the current platform
|
||||
func (u *Updater) getAssetName() string {
|
||||
goos := runtime.GOOS
|
||||
goarch := runtime.GOARCH
|
||||
|
||||
// Capitalize first letter of OS
|
||||
osName := strings.Title(goos)
|
||||
|
||||
// Map architecture names to match goreleaser output
|
||||
archName := goarch
|
||||
switch goarch {
|
||||
case "amd64":
|
||||
archName = "x86_64"
|
||||
case "386":
|
||||
archName = "i386"
|
||||
}
|
||||
|
||||
// Format: ppanel-server-{Version}-{Os}-{Arch}.{ext}
|
||||
ext := "tar.gz"
|
||||
if goos == "windows" {
|
||||
ext = "zip"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("ppanel-server-%s-%s-%s.%s", u.CurrentVersion, osName, archName, ext)
|
||||
}
|
||||
|
||||
// extractBinary extracts the binary from the downloaded archive
|
||||
func (u *Updater) extractBinary(data []byte, destDir, assetName string) (string, error) {
|
||||
if strings.HasSuffix(assetName, ".zip") {
|
||||
return u.extractZip(data, destDir)
|
||||
}
|
||||
return u.extractTarGz(data, destDir)
|
||||
}
|
||||
|
||||
// extractZip extracts a zip archive
|
||||
func (u *Updater) extractZip(data []byte, destDir string) (string, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
zipReader, err := zip.NewReader(reader, int64(len(data)))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create zip reader: %w", err)
|
||||
}
|
||||
|
||||
var binaryPath string
|
||||
for _, file := range zipReader.File {
|
||||
// Look for the binary file
|
||||
if strings.Contains(file.Name, "ppanel-server") && !strings.Contains(file.Name, "/") {
|
||||
binaryPath = filepath.Join(destDir, filepath.Base(file.Name))
|
||||
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to open file in zip: %w", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
outFile, err := os.OpenFile(binaryPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if _, err := io.Copy(outFile, rc); err != nil {
|
||||
return "", fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return binaryPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("binary not found in archive")
|
||||
}
|
||||
|
||||
// extractTarGz extracts a tar.gz archive
|
||||
func (u *Updater) extractTarGz(data []byte, destDir string) (string, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
gzReader, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create gzip reader: %w", err)
|
||||
}
|
||||
defer gzReader.Close()
|
||||
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
|
||||
var binaryPath string
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read tar: %w", err)
|
||||
}
|
||||
|
||||
// Look for the binary file
|
||||
if strings.Contains(header.Name, "ppanel-server") && !strings.Contains(header.Name, "/") {
|
||||
binaryPath = filepath.Join(destDir, filepath.Base(header.Name))
|
||||
|
||||
outFile, err := os.OpenFile(binaryPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||
return "", fmt.Errorf("failed to write file: %w", err)
|
||||
}
|
||||
|
||||
return binaryPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("binary not found in archive")
|
||||
}
|
||||
|
||||
// Apply applies the update by replacing the current binary
|
||||
func (u *Updater) Apply(newBinaryPath string) error {
|
||||
// Get current executable path
|
||||
currentPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get current executable path: %w", err)
|
||||
}
|
||||
|
||||
// Resolve symlinks
|
||||
currentPath, err = filepath.EvalSymlinks(currentPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve symlinks: %w", err)
|
||||
}
|
||||
|
||||
// Create backup
|
||||
backupPath := currentPath + ".backup"
|
||||
if err := u.copyFile(currentPath, backupPath); err != nil {
|
||||
return fmt.Errorf("failed to create backup: %w", err)
|
||||
}
|
||||
|
||||
// Replace the binary
|
||||
if err := u.replaceFile(newBinaryPath, currentPath); err != nil {
|
||||
// Restore backup on failure
|
||||
u.copyFile(backupPath, currentPath)
|
||||
return fmt.Errorf("failed to replace binary: %w", err)
|
||||
}
|
||||
|
||||
// Remove backup on success
|
||||
os.Remove(backupPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func (u *Updater) copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dstFile.Sync()
|
||||
}
|
||||
|
||||
// replaceFile replaces dst with src
|
||||
func (u *Updater) replaceFile(src, dst string) error {
|
||||
// On Windows, we need to rename the old file first
|
||||
if runtime.GOOS == "windows" {
|
||||
oldPath := dst + ".old"
|
||||
if err := os.Rename(dst, oldPath); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(oldPath)
|
||||
}
|
||||
|
||||
// Copy the new file
|
||||
if err := u.copyFile(src, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update performs the complete update process
|
||||
func (u *Updater) Update() error {
|
||||
// Check for updates
|
||||
release, hasUpdate, err := u.CheckForUpdates()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check for updates: %w", err)
|
||||
}
|
||||
|
||||
if !hasUpdate {
|
||||
return fmt.Errorf("already running the latest version")
|
||||
}
|
||||
|
||||
fmt.Printf("New version available: %s\n", release.TagName)
|
||||
fmt.Printf("Downloading update...\n")
|
||||
|
||||
// Download the update
|
||||
binaryPath, err := u.Download(release)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download update: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(filepath.Dir(binaryPath))
|
||||
|
||||
fmt.Printf("Applying update...\n")
|
||||
|
||||
// Apply the update
|
||||
if err := u.Apply(binaryPath); err != nil {
|
||||
return fmt.Errorf("failed to apply update: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Update completed successfully! Please restart the application.\n")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewUpdater(t *testing.T) {
|
||||
u := NewUpdater()
|
||||
assert.NotNil(t, u)
|
||||
assert.Equal(t, "OmnTeam", u.Owner)
|
||||
assert.Equal(t, "server", u.Repo)
|
||||
assert.NotNil(t, u.HTTPClient)
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
u := NewUpdater()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
newVersion string
|
||||
currentVersion string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "same version",
|
||||
newVersion: "v1.0.0",
|
||||
currentVersion: "v1.0.0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "different version",
|
||||
newVersion: "v1.1.0",
|
||||
currentVersion: "v1.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "unknown current version",
|
||||
newVersion: "v1.0.0",
|
||||
currentVersion: "unknown version",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "version without v prefix",
|
||||
newVersion: "1.1.0",
|
||||
currentVersion: "1.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "empty current version",
|
||||
newVersion: "v1.0.0",
|
||||
currentVersion: "",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := u.compareVersions(tt.newVersion, tt.currentVersion)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAssetName(t *testing.T) {
|
||||
u := NewUpdater()
|
||||
u.CurrentVersion = "v1.0.0"
|
||||
|
||||
assetName := u.getAssetName()
|
||||
assert.NotEmpty(t, assetName)
|
||||
assert.Contains(t, assetName, "ppanel-server")
|
||||
assert.Contains(t, assetName, "v1.0.0")
|
||||
}
|
||||
+11
-10
@@ -18,16 +18,17 @@ const (
|
||||
|
||||
// User error
|
||||
const (
|
||||
UserExist uint32 = 20001
|
||||
UserNotExist uint32 = 20002
|
||||
UserPasswordError uint32 = 20003
|
||||
UserDisabled uint32 = 20004
|
||||
InsufficientBalance uint32 = 20005
|
||||
StopRegister uint32 = 20006
|
||||
TelegramNotBound uint32 = 20007
|
||||
UserNotBindOauth uint32 = 20008
|
||||
InviteCodeError uint32 = 20009
|
||||
RegisterIPLimit uint32 = 20010
|
||||
UserExist uint32 = 20001
|
||||
UserNotExist uint32 = 20002
|
||||
UserPasswordError uint32 = 20003
|
||||
UserDisabled uint32 = 20004
|
||||
InsufficientBalance uint32 = 20005
|
||||
StopRegister uint32 = 20006
|
||||
TelegramNotBound uint32 = 20007
|
||||
UserNotBindOauth uint32 = 20008
|
||||
InviteCodeError uint32 = 20009
|
||||
UserCommissionNotEnough uint32 = 20010
|
||||
RegisterIPLimit uint32 = 20011
|
||||
)
|
||||
|
||||
// Node error
|
||||
|
||||
Reference in New Issue
Block a user