386 lines
8.7 KiB
Go
386 lines
8.7 KiB
Go
package utils
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"os/user"
|
||
"path/filepath"
|
||
"regexp"
|
||
"runtime"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// GetHostsPath 获取系统hosts文件路径
|
||
func GetHostsPath() string {
|
||
switch runtime.GOOS {
|
||
case "windows":
|
||
return "C:\\Windows\\System32\\drivers\\etc\\hosts"
|
||
case "linux", "darwin":
|
||
return "/etc/hosts"
|
||
default:
|
||
return "/etc/hosts"
|
||
}
|
||
}
|
||
|
||
// CheckAdmin 检查是否以管理员权限运行
|
||
func CheckAdmin() {
|
||
if !isRunningAsAdmin() {
|
||
if runtime.GOOS == "windows" {
|
||
LogError("需要管理员权限运行")
|
||
LogInfo("请右键点击命令提示符或PowerShell,选择'以管理员身份运行'")
|
||
} else {
|
||
LogError("需要root权限,请使用sudo运行")
|
||
}
|
||
os.Exit(1)
|
||
}
|
||
|
||
if runtime.GOOS == "windows" {
|
||
LogSuccess("已以管理员权限运行")
|
||
}
|
||
}
|
||
|
||
// ValidateBlockName 验证块名称是否有效
|
||
func ValidateBlockName(name string) bool {
|
||
// 块名称只能包含字母、数字、下划线和连字符
|
||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, name)
|
||
return matched && len(name) > 0 && len(name) <= 50
|
||
}
|
||
|
||
// ValidateDomain 验证域名格式
|
||
func ValidateDomain(domain string) bool {
|
||
// 简单的域名格式验证
|
||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](\.[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9])*$`, domain)
|
||
return matched
|
||
}
|
||
|
||
// ValidateIP 验证IP地址格式
|
||
func ValidateIP(ip string) bool {
|
||
// 简单的IP地址格式验证
|
||
parts := strings.Split(ip, ".")
|
||
if len(parts) != 4 {
|
||
return false
|
||
}
|
||
|
||
for _, part := range parts {
|
||
if len(part) == 0 || len(part) > 3 {
|
||
return false
|
||
}
|
||
|
||
// 检查是否为数字且在0-255范围内
|
||
num := 0
|
||
for _, c := range part {
|
||
if c < '0' || c > '9' {
|
||
return false
|
||
}
|
||
num = num*10 + int(c-'0')
|
||
}
|
||
|
||
if num > 255 {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// TrimString 去除字符串前后空白字符
|
||
func TrimString(s string) string {
|
||
return strings.TrimSpace(s)
|
||
}
|
||
|
||
// IsComment 检查行是否为注释
|
||
func IsComment(line string) bool {
|
||
trimmed := TrimString(line)
|
||
return strings.HasPrefix(trimmed, "#")
|
||
}
|
||
|
||
// FileExists 检查文件是否存在
|
||
func FileExists(path string) bool {
|
||
_, err := os.Stat(path)
|
||
return !os.IsNotExist(err)
|
||
}
|
||
|
||
// WriteFile 写入文件
|
||
func WriteFile(path string, data []byte) error {
|
||
// 创建目录
|
||
dir := strings.ReplaceAll(path, "\\", "/")
|
||
if idx := strings.LastIndex(dir, "/"); idx != -1 {
|
||
if err := os.MkdirAll(dir[:idx], 0755); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return os.WriteFile(path, data, 0644)
|
||
}
|
||
|
||
// ReadFile 读取文件
|
||
func ReadFile(path string) ([]byte, error) {
|
||
return os.ReadFile(path)
|
||
}
|
||
|
||
// BackupFile 备份文件到用户备份目录
|
||
func BackupFile(path string) error {
|
||
data, err := ReadFile(path)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 导入config包获取备份目录
|
||
// 注意:这里需要确保config已经初始化
|
||
backupDir := getBackupDirectory()
|
||
|
||
// 确保备份目录存在
|
||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||
return fmt.Errorf("创建备份目录失败: %v", err)
|
||
}
|
||
|
||
// 生成备份文件名(包含时间戳)
|
||
filename := filepath.Base(path)
|
||
timestamp := time.Now().Format("20060102_150405")
|
||
backupFileName := fmt.Sprintf("%s.%s.backup", filename, timestamp)
|
||
backupPath := filepath.Join(backupDir, backupFileName)
|
||
|
||
// 写入备份文件
|
||
if err := WriteFile(backupPath, data); err != nil {
|
||
return fmt.Errorf("写入备份文件失败: %v", err)
|
||
}
|
||
|
||
// 清理旧备份文件,只保留最新的几个
|
||
if err := cleanOldBackups(backupDir, filename, 5); err != nil {
|
||
// 清理失败不影响备份操作,只记录日志
|
||
LogWarning("清理旧备份文件失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// IsEmptyLine 检查是否为空行
|
||
func IsEmptyLine(line string) bool {
|
||
return len(TrimString(line)) == 0
|
||
}
|
||
|
||
// ParseHostsLine 解析hosts文件行
|
||
func ParseHostsLine(line string) (ip, domain, comment string) {
|
||
line = TrimString(line)
|
||
|
||
// 检查是否为注释行
|
||
if IsComment(line) {
|
||
return "", "", line
|
||
}
|
||
|
||
// 分离注释部分
|
||
commentIndex := strings.Index(line, "#")
|
||
if commentIndex != -1 {
|
||
comment = TrimString(line[commentIndex:])
|
||
line = TrimString(line[:commentIndex])
|
||
}
|
||
|
||
// 分离IP和域名
|
||
parts := strings.Fields(line)
|
||
if len(parts) >= 2 {
|
||
ip = parts[0]
|
||
domain = parts[1]
|
||
}
|
||
|
||
return ip, domain, comment
|
||
}
|
||
|
||
// FormatHostsLine 格式化hosts文件行
|
||
func FormatHostsLine(ip, domain, comment string) string {
|
||
if ip == "" || domain == "" {
|
||
if comment != "" {
|
||
return comment
|
||
}
|
||
return ""
|
||
}
|
||
|
||
line := fmt.Sprintf("%-15s %s", ip, domain)
|
||
if comment != "" && !strings.HasPrefix(comment, "#") {
|
||
line += " # " + comment
|
||
} else if comment != "" {
|
||
line += " " + comment
|
||
}
|
||
|
||
return line
|
||
}
|
||
|
||
// getBackupDirectory 获取备份目录
|
||
func getBackupDirectory() string {
|
||
// 获取用户配置目录
|
||
currentUser, err := user.Current()
|
||
if err != nil {
|
||
return "backup" // 回退到相对路径
|
||
}
|
||
return filepath.Join(currentUser.HomeDir, ".hostsync", "backup")
|
||
}
|
||
|
||
// cleanOldBackups 清理旧备份文件,只保留最新的count个
|
||
func cleanOldBackups(backupDir, originalFileName string, keepCount int) error {
|
||
// 读取备份目录中的文件
|
||
files, err := os.ReadDir(backupDir)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 筛选出匹配原文件名的备份文件
|
||
var backupFiles []os.DirEntry
|
||
prefix := originalFileName + "."
|
||
suffix := ".backup"
|
||
|
||
for _, file := range files {
|
||
if !file.IsDir() && strings.HasPrefix(file.Name(), prefix) && strings.HasSuffix(file.Name(), suffix) {
|
||
backupFiles = append(backupFiles, file)
|
||
}
|
||
}
|
||
|
||
// 如果备份文件数量不超过保留数量,直接返回
|
||
if len(backupFiles) <= keepCount {
|
||
return nil
|
||
}
|
||
|
||
// 按修改时间排序(最新的在前)
|
||
sort.Slice(backupFiles, func(i, j int) bool {
|
||
infoI, _ := backupFiles[i].Info()
|
||
infoJ, _ := backupFiles[j].Info()
|
||
return infoI.ModTime().After(infoJ.ModTime())
|
||
})
|
||
|
||
// 删除多余的旧备份文件
|
||
for i := keepCount; i < len(backupFiles); i++ {
|
||
oldBackupPath := filepath.Join(backupDir, backupFiles[i].Name())
|
||
if err := os.Remove(oldBackupPath); err != nil {
|
||
LogWarning("删除旧备份文件失败: %s, %v", oldBackupPath, err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// DisplayWidth 计算字符串的显示宽度(中文字符占2个宽度)
|
||
func DisplayWidth(s string) int {
|
||
width := 0
|
||
for _, r := range s {
|
||
if r > 127 { // 非ASCII字符
|
||
width += 2
|
||
} else {
|
||
width += 1
|
||
}
|
||
}
|
||
return width
|
||
}
|
||
|
||
// PadString 根据显示宽度填充字符串
|
||
func PadString(s string, width int) string {
|
||
currentWidth := DisplayWidth(s)
|
||
if currentWidth >= width {
|
||
return s
|
||
}
|
||
padding := strings.Repeat(" ", width-currentWidth)
|
||
return s + padding
|
||
}
|
||
|
||
// TruncateWithWidth 根据显示宽度截断字符串
|
||
func TruncateWithWidth(s string, maxWidth int) string {
|
||
if DisplayWidth(s) <= maxWidth {
|
||
return s
|
||
}
|
||
|
||
width := 0
|
||
var result []rune
|
||
for _, r := range s {
|
||
charWidth := 1
|
||
if r > 127 {
|
||
charWidth = 2
|
||
}
|
||
|
||
if width+charWidth > maxWidth-3 { // 为"..."预留3个字符
|
||
break
|
||
}
|
||
|
||
result = append(result, r)
|
||
width += charWidth
|
||
}
|
||
|
||
return string(result) + "..."
|
||
}
|
||
|
||
// FormatTable 格式化表格输出
|
||
func FormatTable(headers []string, rows [][]string, columnWidths []int) {
|
||
// 如果没有指定列宽,自动计算
|
||
if len(columnWidths) == 0 {
|
||
columnWidths = make([]int, len(headers))
|
||
|
||
// 计算标题宽度
|
||
for i, header := range headers {
|
||
columnWidths[i] = DisplayWidth(header)
|
||
}
|
||
|
||
// 计算数据行宽度
|
||
for _, row := range rows {
|
||
for i, cell := range row {
|
||
if i < len(columnWidths) {
|
||
width := DisplayWidth(cell)
|
||
if width > columnWidths[i] {
|
||
columnWidths[i] = width
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 设置最小和最大宽度
|
||
for i := range columnWidths {
|
||
if columnWidths[i] < 8 {
|
||
columnWidths[i] = 8
|
||
}
|
||
if columnWidths[i] > 40 {
|
||
columnWidths[i] = 40
|
||
}
|
||
}
|
||
}
|
||
|
||
// 打印顶部边框
|
||
printTableBorder(columnWidths, "┌", "┬", "┐")
|
||
|
||
// 打印标题行
|
||
fmt.Print("│")
|
||
for i, header := range headers {
|
||
paddedHeader := PadString(" "+header+" ", columnWidths[i]+2)
|
||
fmt.Print(paddedHeader + "│")
|
||
}
|
||
fmt.Println()
|
||
|
||
// 打印标题分隔线
|
||
printTableBorder(columnWidths, "├", "┼", "┤")
|
||
|
||
// 打印数据行
|
||
for _, row := range rows {
|
||
fmt.Print("│")
|
||
for i, cell := range row {
|
||
if i < len(columnWidths) {
|
||
// 截断过长的内容
|
||
truncatedCell := TruncateWithWidth(cell, columnWidths[i])
|
||
paddedCell := PadString(" "+truncatedCell+" ", columnWidths[i]+2)
|
||
fmt.Print(paddedCell + "│")
|
||
}
|
||
}
|
||
fmt.Println()
|
||
}
|
||
|
||
// 打印底部边框
|
||
printTableBorder(columnWidths, "└", "┴", "┘")
|
||
}
|
||
|
||
// printTableBorder 打印表格边框
|
||
func printTableBorder(columnWidths []int, left, middle, right string) {
|
||
fmt.Print(left)
|
||
for i, width := range columnWidths {
|
||
fmt.Print(strings.Repeat("─", width+2))
|
||
if i < len(columnWidths)-1 {
|
||
fmt.Print(middle)
|
||
}
|
||
}
|
||
fmt.Println(right)
|
||
}
|