397 lines
9.1 KiB
Go
397 lines
9.1 KiB
Go
package core
|
||
|
||
import (
|
||
"bufio"
|
||
"fmt"
|
||
"os"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/evil7/hostsync/config"
|
||
"github.com/evil7/hostsync/utils"
|
||
)
|
||
|
||
// HostEntry hosts条目
|
||
type HostEntry struct {
|
||
IP string
|
||
Domain string
|
||
Comment string
|
||
Enabled bool
|
||
}
|
||
|
||
// HostBlock hosts块
|
||
type HostBlock struct {
|
||
Name string
|
||
Entries []HostEntry
|
||
DNS string
|
||
DoH string
|
||
Server string
|
||
CronJob string
|
||
UpdateAt time.Time
|
||
Enabled bool
|
||
Comments []string
|
||
}
|
||
|
||
// HostsManager hosts文件管理器
|
||
type HostsManager struct {
|
||
FilePath string
|
||
Blocks map[string]*HostBlock
|
||
Others []string // 非块内容
|
||
}
|
||
|
||
var (
|
||
blockStartPattern = regexp.MustCompile(`^#\s*([a-zA-Z0-9_-]+):\s*$`)
|
||
blockEndPattern = regexp.MustCompile(`^#\s*([a-zA-Z0-9_-]+);\s*$`)
|
||
commentPattern = regexp.MustCompile(`^#\s*(\w+):\s*(.*)$`)
|
||
hostPattern = regexp.MustCompile(`^#?\s*([^\s#]+)\s+([^\s#]+)(?:\s*#(.*))?$`)
|
||
)
|
||
|
||
// NewHostsManager 创建hosts管理器
|
||
func NewHostsManager() *HostsManager {
|
||
return &HostsManager{
|
||
FilePath: config.AppConfig.HostsPath,
|
||
Blocks: make(map[string]*HostBlock),
|
||
Others: make([]string, 0),
|
||
}
|
||
}
|
||
|
||
// Load 加载hosts文件
|
||
func (hm *HostsManager) Load() error {
|
||
if !utils.FileExists(hm.FilePath) {
|
||
return fmt.Errorf("hosts文件不存在: %s", hm.FilePath)
|
||
}
|
||
|
||
file, err := os.Open(hm.FilePath)
|
||
if err != nil {
|
||
return fmt.Errorf("打开hosts文件失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
scanner := bufio.NewScanner(file)
|
||
var currentBlock *HostBlock
|
||
var inBlock bool
|
||
for scanner.Scan() {
|
||
line := strings.TrimSpace(scanner.Text())
|
||
|
||
// 先检查是否是配置注释(在块内)
|
||
if inBlock && currentBlock != nil {
|
||
if matches := commentPattern.FindStringSubmatch(line); matches != nil {
|
||
// 这是配置注释,处理块内容
|
||
hm.parseBlockLine(line, currentBlock)
|
||
continue
|
||
}
|
||
}
|
||
|
||
// 检查块开始
|
||
if matches := blockStartPattern.FindStringSubmatch(line); matches != nil {
|
||
blockName := matches[1]
|
||
currentBlock = &HostBlock{
|
||
Name: blockName,
|
||
Entries: make([]HostEntry, 0),
|
||
Comments: make([]string, 0),
|
||
Enabled: true,
|
||
}
|
||
hm.Blocks[blockName] = currentBlock
|
||
inBlock = true
|
||
continue
|
||
}
|
||
|
||
// 检查块结束
|
||
if matches := blockEndPattern.FindStringSubmatch(line); matches != nil {
|
||
inBlock = false
|
||
currentBlock = nil
|
||
continue
|
||
}
|
||
if inBlock && currentBlock != nil {
|
||
// 处理块内容
|
||
hm.parseBlockLine(line, currentBlock)
|
||
} else {
|
||
// 非块内容 - 只保存非空行
|
||
originalLine := scanner.Text()
|
||
if strings.TrimSpace(originalLine) != "" {
|
||
hm.Others = append(hm.Others, originalLine)
|
||
}
|
||
}
|
||
}
|
||
|
||
return scanner.Err()
|
||
}
|
||
|
||
// parseBlockLine 解析块内的行
|
||
func (hm *HostsManager) parseBlockLine(line string, block *HostBlock) {
|
||
originalLine := line
|
||
trimmedLine := strings.TrimSpace(line)
|
||
|
||
// 检查是否是配置注释
|
||
if matches := commentPattern.FindStringSubmatch(trimmedLine); matches != nil {
|
||
key, value := matches[1], matches[2]
|
||
switch key {
|
||
case "useDns":
|
||
block.DNS = value
|
||
case "useDoh":
|
||
block.DoH = value
|
||
case "useSrv":
|
||
block.Server = value
|
||
case "cronJob":
|
||
block.CronJob = value
|
||
case "updateAt":
|
||
if t, err := time.Parse("2006-01-02 15:04:05", value); err == nil {
|
||
block.UpdateAt = t
|
||
}
|
||
}
|
||
return
|
||
}
|
||
|
||
// 检查是否是host条目(包括被注释的)
|
||
if matches := hostPattern.FindStringSubmatch(trimmedLine); matches != nil {
|
||
ip, domain := matches[1], matches[2]
|
||
comment := ""
|
||
enabled := true
|
||
|
||
if len(matches) > 3 && matches[3] != "" {
|
||
comment = strings.TrimSpace(matches[3])
|
||
}
|
||
|
||
// 检查是否被注释掉(以 # 开头)
|
||
if strings.HasPrefix(trimmedLine, "#") {
|
||
enabled = false
|
||
// 需要从注释中提取实际的 IP 和域名
|
||
// 重新解析去掉开头 # 后的内容
|
||
lineWithoutComment := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "#"))
|
||
if newMatches := regexp.MustCompile(`^([^\s#]+)\s+([^\s#]+)(?:\s*#(.*))?$`).FindStringSubmatch(lineWithoutComment); newMatches != nil {
|
||
ip, domain = newMatches[1], newMatches[2]
|
||
if len(newMatches) > 3 && newMatches[3] != "" {
|
||
comment = strings.TrimSpace(newMatches[3])
|
||
}
|
||
}
|
||
}
|
||
|
||
entry := HostEntry{
|
||
IP: ip,
|
||
Domain: domain,
|
||
Comment: comment,
|
||
Enabled: enabled,
|
||
}
|
||
block.Entries = append(block.Entries, entry)
|
||
return
|
||
} // 其他注释
|
||
if strings.HasPrefix(trimmedLine, "#") {
|
||
block.Comments = append(block.Comments, originalLine)
|
||
}
|
||
}
|
||
|
||
// Save 保存hosts文件
|
||
func (hm *HostsManager) Save() error {
|
||
// 备份原文件
|
||
if err := utils.BackupFile(hm.FilePath); err != nil {
|
||
return fmt.Errorf("备份文件失败: %v", err)
|
||
}
|
||
|
||
file, err := os.Create(hm.FilePath)
|
||
if err != nil {
|
||
return fmt.Errorf("创建hosts文件失败: %v", err)
|
||
}
|
||
defer file.Close()
|
||
|
||
// 写入非块内容 (过滤空行并确保末尾只有一个空行)
|
||
hasNonBlockContent := false
|
||
for _, line := range hm.Others {
|
||
line = strings.TrimSpace(line)
|
||
if line != "" {
|
||
if hasNonBlockContent {
|
||
fmt.Fprintln(file) // 只在有内容时添加空行分隔
|
||
}
|
||
fmt.Fprintln(file, line)
|
||
hasNonBlockContent = true
|
||
}
|
||
}
|
||
|
||
// 写入块内容
|
||
blockNames := make([]string, 0, len(hm.Blocks))
|
||
for name := range hm.Blocks {
|
||
blockNames = append(blockNames, name)
|
||
}
|
||
sort.Strings(blockNames)
|
||
|
||
for i, name := range blockNames {
|
||
block := hm.Blocks[name]
|
||
// 在块之间添加空行分隔(除了第一个块且前面有非块内容)
|
||
if i > 0 || hasNonBlockContent {
|
||
fmt.Fprintln(file)
|
||
}
|
||
hm.writeBlock(file, block)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// writeBlock 写入块内容
|
||
func (hm *HostsManager) writeBlock(file *os.File, block *HostBlock) {
|
||
fmt.Fprintf(file, "# %s:\n", block.Name)
|
||
|
||
// 写入hosts条目
|
||
for _, entry := range block.Entries {
|
||
prefix := ""
|
||
if !entry.Enabled {
|
||
prefix = "# "
|
||
}
|
||
|
||
if entry.Comment != "" {
|
||
fmt.Fprintf(file, "%s%-16s %s # %s\n", prefix, entry.IP, entry.Domain, entry.Comment)
|
||
} else {
|
||
fmt.Fprintf(file, "%s%-16s %s\n", prefix, entry.IP, entry.Domain)
|
||
}
|
||
}
|
||
|
||
// 写入配置注释
|
||
if block.DNS != "" {
|
||
fmt.Fprintf(file, "# useDns: %s\n", block.DNS)
|
||
}
|
||
if block.DoH != "" {
|
||
fmt.Fprintf(file, "# useDoh: %s\n", block.DoH)
|
||
}
|
||
if block.Server != "" {
|
||
fmt.Fprintf(file, "# useSrv: %s\n", block.Server)
|
||
}
|
||
if block.CronJob != "" {
|
||
fmt.Fprintf(file, "# cronJob: %s\n", block.CronJob)
|
||
}
|
||
if !block.UpdateAt.IsZero() {
|
||
fmt.Fprintf(file, "# updateAt: %s\n", block.UpdateAt.Format("2006-01-02 15:04:05"))
|
||
}
|
||
|
||
// 写入其他注释
|
||
for _, comment := range block.Comments {
|
||
fmt.Fprintln(file, comment)
|
||
}
|
||
|
||
fmt.Fprintf(file, "# %s;\n", block.Name)
|
||
}
|
||
|
||
// GetBlock 获取指定块
|
||
func (hm *HostsManager) GetBlock(name string) *HostBlock {
|
||
return hm.Blocks[name]
|
||
}
|
||
|
||
// CreateBlock 创建新块
|
||
func (hm *HostsManager) CreateBlock(name string) *HostBlock {
|
||
if !utils.ValidateBlockName(name) {
|
||
return nil
|
||
}
|
||
|
||
if _, exists := hm.Blocks[name]; exists {
|
||
return hm.Blocks[name]
|
||
}
|
||
|
||
block := &HostBlock{
|
||
Name: name,
|
||
Entries: make([]HostEntry, 0),
|
||
Comments: make([]string, 0),
|
||
Enabled: true,
|
||
}
|
||
hm.Blocks[name] = block
|
||
return block
|
||
}
|
||
|
||
// DeleteBlock 删除块
|
||
func (hm *HostsManager) DeleteBlock(name string) error {
|
||
if _, exists := hm.Blocks[name]; !exists {
|
||
return fmt.Errorf("块不存在: %s", name)
|
||
}
|
||
|
||
delete(hm.Blocks, name)
|
||
return nil
|
||
}
|
||
|
||
// EnableBlock 启用块
|
||
func (hm *HostsManager) EnableBlock(name string) error {
|
||
block := hm.GetBlock(name)
|
||
if block == nil {
|
||
return fmt.Errorf("块不存在: %s", name)
|
||
}
|
||
|
||
for i := range block.Entries {
|
||
block.Entries[i].Enabled = true
|
||
}
|
||
block.Enabled = true
|
||
|
||
return hm.Save()
|
||
}
|
||
|
||
// DisableBlock 禁用块
|
||
func (hm *HostsManager) DisableBlock(name string) error {
|
||
block := hm.GetBlock(name)
|
||
if block == nil {
|
||
return fmt.Errorf("块不存在: %s", name)
|
||
}
|
||
|
||
for i := range block.Entries {
|
||
block.Entries[i].Enabled = false
|
||
}
|
||
block.Enabled = false
|
||
|
||
return hm.Save()
|
||
}
|
||
|
||
// AddEntry 添加条目
|
||
func (hm *HostsManager) AddEntry(blockName, domain, ip string) error {
|
||
block := hm.GetBlock(blockName)
|
||
if block == nil {
|
||
block = hm.CreateBlock(blockName)
|
||
if block == nil {
|
||
return fmt.Errorf("无法创建块: %s", blockName)
|
||
}
|
||
}
|
||
|
||
// 检查是否已存在
|
||
for i, entry := range block.Entries {
|
||
if entry.Domain == domain {
|
||
// 更新IP
|
||
block.Entries[i].IP = ip
|
||
block.Entries[i].Enabled = true
|
||
return hm.Save()
|
||
}
|
||
}
|
||
|
||
// 添加新条目
|
||
entry := HostEntry{
|
||
IP: ip,
|
||
Domain: domain,
|
||
Enabled: true,
|
||
}
|
||
block.Entries = append(block.Entries, entry)
|
||
|
||
return hm.Save()
|
||
}
|
||
|
||
// RemoveEntry 删除条目
|
||
func (hm *HostsManager) RemoveEntry(blockName, domain string) error {
|
||
block := hm.GetBlock(blockName)
|
||
if block == nil {
|
||
return fmt.Errorf("块不存在: %s", blockName)
|
||
}
|
||
|
||
for i, entry := range block.Entries {
|
||
if entry.Domain == domain {
|
||
// 删除条目
|
||
block.Entries = append(block.Entries[:i], block.Entries[i+1:]...)
|
||
return hm.Save()
|
||
}
|
||
}
|
||
|
||
return fmt.Errorf("域名不存在: %s", domain)
|
||
}
|
||
|
||
// UpdateBlockTime 更新块的更新时间
|
||
func (hm *HostsManager) UpdateBlockTime(blockName string) error {
|
||
block := hm.GetBlock(blockName)
|
||
if block == nil {
|
||
return fmt.Errorf("块不存在: %s", blockName)
|
||
}
|
||
|
||
block.UpdateAt = time.Now()
|
||
return hm.Save()
|
||
}
|