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