- Add settings menu to switch between Xray and V2Ray cores for VLESS connections - Implement core type persistence in configuration with LoadSettings/SaveSettings - Add VLESS error and access log viewer showing last 30 and 20 lines respectively - Display current core type and system time in main menu - Update VLESS connection to use selected core dynamically - Refactor monitor.go to accept 'q' key input for graceful exit instead of signal handling - Add proxy platform-specific implementations (proxy_unix.go, proxy_windows.go) - Add downloader module for managing binary resources - Include V2Ray and Xray configuration files and geodata (geoip.dat, geosite.dat) - Update CLI imports to include path/filepath and time packages - Improve user experience with core selection visibility and log diagnostics
274 lines
6.4 KiB
Go
274 lines
6.4 KiB
Go
package downloader
|
||
|
||
import (
|
||
"archive/zip"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"runtime"
|
||
"strings"
|
||
|
||
"github.com/fatih/color"
|
||
)
|
||
|
||
const (
|
||
XrayVersion = "24.12.18"
|
||
V2RayVersion = "5.22.0"
|
||
XrayDownloadURL = "https://github.com/XTLS/Xray-core/releases/download/v%s/Xray-windows-64.zip"
|
||
V2RayDownloadURL = "https://github.com/v2fly/v2ray-core/releases/download/v%s/v2ray-windows-64.zip"
|
||
)
|
||
|
||
var (
|
||
green = color.New(color.FgGreen).SprintFunc()
|
||
yellow = color.New(color.FgYellow).SprintFunc()
|
||
cyan = color.New(color.FgCyan).SprintFunc()
|
||
)
|
||
|
||
// DownloadCore загружает Xray или V2Ray
|
||
func DownloadCore(coreDir, coreName string) error {
|
||
var downloadURL, version, exeName string
|
||
|
||
if coreName == "xray" {
|
||
downloadURL = fmt.Sprintf(XrayDownloadURL, XrayVersion)
|
||
version = XrayVersion
|
||
exeName = "xray.exe"
|
||
} else if coreName == "v2ray" {
|
||
downloadURL = fmt.Sprintf(V2RayDownloadURL, V2RayVersion)
|
||
version = V2RayVersion
|
||
exeName = "v2ray.exe"
|
||
} else {
|
||
return fmt.Errorf("неизвестный тип ядра: %s", coreName)
|
||
}
|
||
|
||
// Проверяем, существует ли уже
|
||
coreExe := filepath.Join(coreDir, exeName)
|
||
if _, err := os.Stat(coreExe); err == nil {
|
||
return nil
|
||
}
|
||
|
||
fmt.Println("\n" + strings.Repeat("=", 60))
|
||
fmt.Printf("%s %s не найден. Начинаю загрузку...\n", yellow("⚠"), strings.ToUpper(coreName))
|
||
fmt.Println(strings.Repeat("=", 60))
|
||
|
||
// Создаем директорию
|
||
if err := os.MkdirAll(coreDir, 0755); err != nil {
|
||
return fmt.Errorf("ошибка создания директории: %w", err)
|
||
}
|
||
|
||
fmt.Printf("\nВерсия: %s\n", cyan(version))
|
||
fmt.Printf("URL: %s\n\n", downloadURL)
|
||
|
||
// Загружаем файл
|
||
zipPath := filepath.Join(coreDir, coreName+".zip")
|
||
if err := downloadFile(zipPath, downloadURL); err != nil {
|
||
return fmt.Errorf("ошибка загрузки: %w", err)
|
||
}
|
||
|
||
fmt.Printf("\n%s Загрузка завершена\n", green("✓"))
|
||
fmt.Printf("%s Распаковка архива...\n", yellow("⚙"))
|
||
|
||
// Распаковываем
|
||
if err := unzip(zipPath, coreDir); err != nil {
|
||
os.Remove(zipPath)
|
||
return fmt.Errorf("ошибка распаковки: %w", err)
|
||
}
|
||
|
||
os.Remove(zipPath)
|
||
|
||
fmt.Printf("%s %s успешно установлен в %s\n", green("✓"), strings.ToUpper(coreName), coreDir)
|
||
fmt.Println(strings.Repeat("=", 60))
|
||
|
||
return nil
|
||
}
|
||
|
||
// DownloadXray загружает Xray
|
||
func DownloadXray(xrayDir string) error {
|
||
return DownloadCore(xrayDir, "xray")
|
||
}
|
||
|
||
// DownloadV2Ray загружает V2Ray
|
||
func DownloadV2Ray(v2rayDir string) error {
|
||
return DownloadCore(v2rayDir, "v2ray")
|
||
}
|
||
|
||
// downloadFile загружает файл с прогресс-баром
|
||
func downloadFile(filepath string, url string) error {
|
||
out, err := os.Create(filepath)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer out.Close()
|
||
|
||
resp, err := http.Get(url)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
return fmt.Errorf("ошибка загрузки: HTTP %d", resp.StatusCode)
|
||
}
|
||
|
||
size := resp.ContentLength
|
||
counter := &WriteCounter{Total: size}
|
||
|
||
_, err = io.Copy(out, io.TeeReader(resp.Body, counter))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
fmt.Println()
|
||
return nil
|
||
}
|
||
|
||
// WriteCounter считает загруженные байты
|
||
type WriteCounter struct {
|
||
Total int64
|
||
Downloaded int64
|
||
}
|
||
|
||
func (wc *WriteCounter) Write(p []byte) (int, error) {
|
||
n := len(p)
|
||
wc.Downloaded += int64(n)
|
||
wc.PrintProgress()
|
||
return n, nil
|
||
}
|
||
|
||
func (wc *WriteCounter) PrintProgress() {
|
||
fmt.Printf("\r%s", strings.Repeat(" ", 60))
|
||
percent := float64(wc.Downloaded) / float64(wc.Total) * 100
|
||
downloaded := formatBytes(wc.Downloaded)
|
||
total := formatBytes(wc.Total)
|
||
fmt.Printf("\rЗагрузка: %s / %s (%.1f%%)", downloaded, total, percent)
|
||
}
|
||
|
||
func formatBytes(bytes int64) string {
|
||
const unit = 1024
|
||
if bytes < unit {
|
||
return fmt.Sprintf("%d B", bytes)
|
||
}
|
||
div, exp := int64(unit), 0
|
||
for n := bytes / unit; n >= unit; n /= unit {
|
||
div *= unit
|
||
exp++
|
||
}
|
||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||
}
|
||
|
||
// unzip распаковывает zip архив
|
||
func unzip(src, dest string) error {
|
||
r, err := zip.OpenReader(src)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer r.Close()
|
||
|
||
for _, f := range r.File {
|
||
fpath := filepath.Join(dest, f.Name)
|
||
|
||
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) {
|
||
return fmt.Errorf("недопустимый путь файла: %s", fpath)
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// CheckAndDownloadXray проверяет и загружает Xray
|
||
func CheckAndDownloadXray(xrayDir string) error {
|
||
if runtime.GOOS != "windows" {
|
||
return nil
|
||
}
|
||
|
||
xrayExe := filepath.Join(xrayDir, "xray.exe")
|
||
if _, err := os.Stat(xrayExe); os.IsNotExist(err) {
|
||
return DownloadXray(xrayDir)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CheckAndDownloadV2Ray проверяет и загружает V2Ray
|
||
func CheckAndDownloadV2Ray(v2rayDir string) error {
|
||
if runtime.GOOS != "windows" {
|
||
return nil
|
||
}
|
||
|
||
v2rayExe := filepath.Join(v2rayDir, "v2ray.exe")
|
||
if _, err := os.Stat(v2rayExe); os.IsNotExist(err) {
|
||
return DownloadV2Ray(v2rayDir)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// CheckAndDownloadBoth проверяет и загружает оба ядра
|
||
func CheckAndDownloadBoth(xrayDir, v2rayDir string) error {
|
||
if runtime.GOOS != "windows" {
|
||
return nil
|
||
}
|
||
|
||
// Проверяем Xray
|
||
xrayExe := filepath.Join(xrayDir, "xray.exe")
|
||
xrayExists := true
|
||
if _, err := os.Stat(xrayExe); os.IsNotExist(err) {
|
||
xrayExists = false
|
||
}
|
||
|
||
// Проверяем V2Ray
|
||
v2rayExe := filepath.Join(v2rayDir, "v2ray.exe")
|
||
v2rayExists := true
|
||
if _, err := os.Stat(v2rayExe); os.IsNotExist(err) {
|
||
v2rayExists = false
|
||
}
|
||
|
||
// Если оба есть, ничего не делаем
|
||
if xrayExists && v2rayExists {
|
||
return nil
|
||
}
|
||
|
||
// Загружаем недостающие
|
||
if !xrayExists {
|
||
if err := DownloadXray(xrayDir); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
if !v2rayExists {
|
||
if err := DownloadV2Ray(v2rayDir); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|