feat(cli): add settings menu and VLESS log viewer with core selection

- 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
This commit is contained in:
2026-04-06 20:06:35 +06:00
parent d88139af1b
commit 20d24a3639
19 changed files with 45913 additions and 45 deletions

View File

@@ -0,0 +1,273 @@
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
}