package main import ( "bytes" "clear/utils" "clear/utils/color" "flag" "fmt" "github.com/shirou/gopsutil/v3/disk" "gopkg.in/yaml.v2" "io/ioutil" "log" "os" "path" "path/filepath" "regexp" "sort" "strings" "syscall" "time" ) type Conf struct { Dir string Prefix string Suffix string BeforeTime int64 `yaml:"beforeTime"` Mode string Retain int MinSize int `yaml:"minSize"` Debug bool } type FileType struct { Path string Name string Size int64 ModTime time.Time } // EmailContent 邮件内容 var EmailContent bytes.Buffer // 清理大小总计 var ClearSize int64 // 预估清理大小 var EstimateSize int64 const TimeFormat = "2006-01-02 15:04:05" // 重新计算文件大小的最小值 var MinRecountFileSize int64 // 安全的扩展名 var safeExt = []string{".log", ".json", ".gz", ".tgz", ".tar", ".zip", ".rar", ".mp4", ".out", ".bz2", ".txt"} func init() { MinRecountFileSize = 10 * 1024 * 1024 } func main() { var minSize float64 var configDir string var configFile string var email string var help bool flag.BoolVar(&help, "help", false, "显示帮助") flag.Float64Var(&minSize, "min-size", 10, "阈值比例,例如:磁盘使用率大于90%后执行") flag.StringVar(&configDir, "config-dir", "./rules/", "配置目录") flag.StringVar(&configFile, "config-file", "", "指定配置文件") flag.StringVar(&email, "email", "", "指定邮箱收件人") flag.Parse() if help { flag.Usage() os.Exit(0) } parts, _ := disk.Partitions(true) var minDiskSize uint64 //小于1Gb的磁盘,不做处理 minDiskSize = 1024 * 1024 * 1024 isCheck := false diskData := "" for _, diskinfo := range parts { data, _ := disk.Usage(diskinfo.Mountpoint) if data.Total < minDiskSize { continue } if data.UsedPercent > minSize-1 { isCheck = true UsedPercent := fmt.Sprintf("%.1f", data.UsedPercent) DiskTotalSize := int64(data.Total) LeftSize := int64(data.Free) diskData = "挂载点: " + data.Path + ", 使用比例: " + UsedPercent + "%, 磁盘容量:" + utils.FormatFileSize(DiskTotalSize) + ", 剩余容量: " + utils.FormatFileSize(LeftSize) + ", 磁盘类型:" + data.Fstype fmt.Println(data) break } } if isCheck == false { fmt.Println("磁盘容量没有达到设定的阈值:", minSize, "% , 正常退出") os.Exit(0) } nowTime := time.Now().Format(TimeFormat) fmt.Println("[开始时间]", nowTime) fmt.Println("+-----------------------------------------------------------------------+") fmt.Println("[磁盘信息]", diskData) fmt.Println("+-----------------------------------------------------------------------+") EmailContent.WriteString("[开始时间] " + nowTime + "\n") EmailContent.WriteString("+-----------------------------------------------------------------------+\n") hostname, _ := os.Hostname() ip := utils.GetIp() EmailContent.WriteString("[主机名] " + hostname + "\n") EmailContent.WriteString("[IP] " + ip + "\n") EmailContent.WriteString("[磁盘信息] " + diskData + "\n") EmailContent.WriteString("+-----------------------------------------------------------------------+\n") files, err := ioutil.ReadDir(configDir) if err != nil { fmt.Println("配置目录不存在", err) os.Exit(101) } if configFile == "" { for _, cFile := range files { ext := path.Ext(cFile.Name()) if ext != ".yml" { continue } err = findConf(configDir + cFile.Name()) if err != nil { continue } } } else { _ = findConf(configDir + configFile) } endTime := time.Now().Format(TimeFormat) fmt.Println("+-----------------------------------------------------------------------+") successClearSize := utils.FormatFileSize(ClearSize) estimateSize := utils.FormatFileSize(EstimateSize) fmt.Println("[成功清理]", successClearSize) fmt.Println("[预估清理]", estimateSize) EmailContent.WriteString("+-----------------------------------------------------------------------+\n") EmailContent.WriteString("[成功清理] " + successClearSize + "\n") EmailContent.WriteString("[预估清理] " + estimateSize + "\n") EmailContent.WriteString("[完成时间]" + endTime + "\n") if email != "" { isCheck = false for _, diskinfo := range parts { data, _ := disk.Usage(diskinfo.Mountpoint) if data.Total < minDiskSize { continue } if data.UsedPercent > minSize { isCheck = true break } } if isCheck == true { EmailContent.WriteString("\n 清理任务已执行,但清理规则似乎未完全覆盖,需要人工处理处理 \n") } else { EmailContent.WriteString("\n 磁盘容量已下降,可忽略本邮件 \n") } content := "
" + EmailContent.String() + "" if isCheck == true { utils.SendEmailApi(email, "【磁盘容量不足警告】🔥", content, "日志清理") } else { utils.SendEmailApi(email, "磁盘清理完成通知", content, "日志清理") } } fmt.Println("[完成时间]", endTime) } func findConf(filename string) error { var confList []Conf yamlFile, err := ioutil.ReadFile(filename) //log.Println("yamlFile:", string(yamlFile)) if err != nil { log.Printf("yamlFile.Get err #%v ", err) return err } err = yaml.Unmarshal(yamlFile, &confList) if err != nil { log.Fatalf("配置文件格式错误:%s , %v", filename, err) return err } //打印配置 //fmt.Println(confList) if len(confList) > 0 { fmt.Println("[配置文件]", filename) fmt.Println("+-----------------------------------------------------------------------+") EmailContent.WriteString("[配置文件] ➜ " + filename + "\n") EmailContent.WriteString("+-----------------------------------------------------------------------+\n") for _, conf := range confList { tmpdir := strings.TrimRight(conf.Dir, "/") stringCount := strings.Count(tmpdir, "/") //获取前缀是否配置并满足规则,必须包含 . stringCountPrefix := 1 if conf.Prefix != "" { stringCountPrefix = strings.Count(conf.Prefix, ".") } if stringCount > 1 && stringCountPrefix > 0 { ListDir(conf, conf.Dir) } else if stringCountPrefix == 0 { fmt.Println(color.Magenta("[异常]"), "文件前缀不符合规范,必须包含 \".\" ➜ 当前配置为:", conf.Prefix) EmailContent.WriteString("[异常] 文件前缀不符合规范,必须包含 \".\" ➜ 当前配置为:" + conf.Prefix + "\n") } else { fmt.Println(color.Magenta("[异常]"), "目录规范不符合规范 ➜ 禁止根目录和一级目录 ➜", conf.Dir) EmailContent.WriteString("[异常] 目录规范不符合规范 ➜ 禁止根目录和一级目录 ➜ " + conf.Dir + "\n") } } } else { fmt.Println("+-----------------------------------------------------------------------+") fmt.Println("[配置为空]", filename) } return nil } func ListDir(config Conf, folder string) error { files, errDir := ioutil.ReadDir(folder) if errDir != nil { fmt.Println("[提示]", errDir) return nil } files = sortByTime(files) var filelist []FileType for _, file := range files { if file.IsDir() { ListDir(config, folder+"/"+file.Name()) } else { // 输出绝对路径 strAbsPath, errPath := filepath.Abs(folder + "/" + file.Name()) if errPath != nil { fmt.Println(errPath) } //error.log.20200705 //%2Fworkspace%2Flogs%2Fservice%2Ftest%2Faccesslog.log.20200721 tmpfile := strings.ReplaceAll(file.Name(), "%2F", "") match, _ := regexp.MatchString("^[A-Za-z0-9-_]+\\.log[.-][0-9-]+$", tmpfile) if match == false { //86_70_20200715160311.cache.172.16.49.19 match, _ = regexp.MatchString("^[A-Za-z0-9_-]+\\.cache\\.[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$", file.Name()) } //后缀优先 if config.Suffix != "" { //获取后缀 ext := path.Ext(strAbsPath) if ext == ".properties" { continue } if ext == "" { continue } if match == false { //验证后缀有没有在安全范围 if utils.InArray(ext, safeExt) == false { continue } if ext != config.Suffix { continue } } } else if config.Prefix != "" { ext := path.Ext(strAbsPath) //后缀长度必须大于8 if len(ext) < 8 { continue } //判断前缀是否匹配,0表示,出现在最前面 if strings.Index(file.Name(), config.Prefix) != 0 { continue } } else if match == false { continue } //大于10M的文件,重新计算真实的大小 //fmt.Println(file.Size(),file.Name()) if file.Size() > MinRecountFileSize { nf := file.Sys().(*syscall.Stat_t) nfsize := nf.Blocks * 512 //如果大小 4096 * 2 小于 2 个扇区大小,忽略文件 if nfsize < 8192 { continue } } var fileInfo FileType fileInfo.Path = strAbsPath fileInfo.Size = file.Size() fileInfo.Name = file.Name() fileInfo.ModTime = file.ModTime() //fmt.Println(strAbsPath,file.Size()) filelist = append(filelist, fileInfo) } } //fmt.Println(filelist) //如果配置了保留数量 > 0 minFileSize := config.MinSize if minFileSize > 0 { minFileSize = minFileSize * 1024 * 1024 } beforeTime := config.BeforeTime if beforeTime > 0 { beforeTime = time.Now().Unix() - beforeTime*60 } //fmt.Println(config.MinSize,config) if config.Retain > 0 { if len(filelist) > config.Retain { for i, file := range filelist { //fmt.Println(file.ModTime.Format(TimeFormat),file.Path) if i >= config.Retain { RunRule(config, file, int64(minFileSize), beforeTime) } } } else { return nil } } else { for _, file := range filelist { RunRule(config, file, int64(minFileSize), beforeTime) } } return nil } func RunRule(config Conf, file FileType, minFileSize int64, beforeTime int64) error { filesize := utils.FormatFileSize(file.Size) colorSize := color.Cyan(filesize) if minFileSize > 0 && file.Size < minFileSize { fmt.Println(color.Green("[忽略]"), file.ModTime.Format(TimeFormat), "➜", file.Path, colorSize) return nil } if beforeTime > 0 && file.ModTime.Unix() > beforeTime { fmt.Println(color.Green("[忽略]"), file.ModTime.Format(TimeFormat), "➜", file.Path, colorSize) return nil } var result string if config.Mode == "rm" { if config.Debug == false { //执行删除 err := os.Remove(file.Path) if err != nil { result = "[✖]" } else { result = "[✔]" ClearSize += file.Size } fmt.Println("[删除]", file.ModTime.Format(TimeFormat), "➜", file.Path, colorSize, result) EmailContent.WriteString("[删除]" + file.ModTime.Format(TimeFormat) + " ➜ " + file.Path + " " + filesize + " " + result + "\n") } else { fmt.Println("[删除]", file.ModTime.Format(TimeFormat), "➜", file.Path, colorSize, "[Debug]") EmailContent.WriteString("[删除]" + file.ModTime.Format(TimeFormat) + " ➜ " + file.Path + " " + filesize + " [Debug]\n") } EstimateSize += file.Size } else if config.Mode == "waring" { fmt.Println(color.Red("[告警]"), color.Red(file.ModTime.Format(TimeFormat)), "➜", color.Red(file.Path), colorSize) EmailContent.WriteString("[告警]" + file.ModTime.Format(TimeFormat) + " ➜ " + file.Path + " " + filesize + "\n") return nil } else { //文件大小为0的,不做清空处理 if file.Size == 0 { return nil } if config.Debug == false { err := os.Truncate(file.Path, 0) //f, err := os.OpenFile(file.Path, os.O_WRONLY|os.O_TRUNC, 0600) if err != nil { result = "[✖]" } else { result = "[✔]" ClearSize += file.Size } //f.WriteString("") //f.Close() fmt.Println("[清空]", file.ModTime.Format(TimeFormat), "➜", file.Path, colorSize, result) EmailContent.WriteString("[清空]" + file.ModTime.Format(TimeFormat) + " ➜ " + file.Path + " " + filesize + " " + result + "\n") } else { fmt.Println("[清空]", file.ModTime.Format(TimeFormat), "➜", file.Path, colorSize, "[Debug]") EmailContent.WriteString("[清空]" + file.ModTime.Format(TimeFormat) + " ➜ " + file.Path + " " + filesize + " [Debug]\n") } EstimateSize += file.Size } return nil } func sortByTime(pl []os.FileInfo) []os.FileInfo { sort.Slice(pl, func(i, j int) bool { flag := false if pl[i].ModTime().After(pl[j].ModTime()) { flag = true } else if pl[i].ModTime().Equal(pl[j].ModTime()) { if pl[i].Name() < pl[j].Name() { flag = true } } return flag }) return pl }