hostSync/utils/utils.go

386 lines
8.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}