tlog.app/go/tlog@v0.23.1/rotating/file.go (about) 1 package rotating 2 3 import ( 4 "fmt" 5 "io" 6 "io/fs" 7 "os" 8 "path/filepath" 9 "strings" 10 "sync" 11 "sync/atomic" 12 "time" 13 14 "tlog.app/go/errors" 15 ) 16 17 type ( 18 File struct { 19 mu sync.Mutex 20 w io.Writer 21 22 name string 23 current string 24 dir, pref, suff string 25 format string 26 num int 27 28 size int64 29 start time.Time 30 31 MaxFileSize int64 32 MaxFileAge time.Duration 33 34 MaxTotalSize int64 35 MaxTotalAge time.Duration 36 MaxTotalFiles int // including current 37 38 // SubstPattern string 39 40 Flags int 41 Mode os.FileMode 42 43 OpenFile FileOpener `deep:"compare=nil"` 44 readdir func(name string) ([]fs.DirEntry, error) `deep:"-"` 45 fstat func(name string) (fs.FileInfo, error) `deep:"-"` 46 remove func(name string) error `deep:"-"` 47 symlink func(target, name string) error `deep:"-"` 48 49 removeSingleflight atomic.Bool 50 } 51 52 FileOpener = func(name string, flags int, mode os.FileMode) (io.Writer, error) 53 ) 54 55 const ( 56 B = 1 << (iota * 10) 57 KiB 58 MiB 59 GiB 60 TiB 61 62 KB = 1e3 63 MB = 1e6 64 GB = 1e9 65 TB = 1e12 66 ) 67 68 var ( 69 // SubstPattern = "XXXX" 70 // TimeFormat = "2006-01-02_15-04" 71 72 patterns = []string{"xxx", "XXX", "ddd"} 73 ) 74 75 func Create(name string) (f *File) { 76 f = &File{ 77 name: name, 78 79 MaxFileSize: 128 * MiB, 80 MaxTotalAge: 28 * 24 * time.Hour, 81 MaxTotalFiles: 10, 82 83 // SubstPattern: SubstPattern, 84 // TimeFormat: TimeFormat, 85 86 OpenFile: openFile, // OpenFileTimeSubstWithSymlink, 87 Flags: os.O_CREATE | os.O_APPEND | os.O_WRONLY, 88 Mode: 0o644, 89 90 readdir: os.ReadDir, 91 fstat: os.Stat, 92 remove: os.Remove, 93 symlink: os.Symlink, 94 } 95 96 return f 97 } 98 99 func (f *File) Write(p []byte) (n int, err error) { 100 defer f.mu.Unlock() 101 f.mu.Lock() 102 103 if f.w == nil || f.size != 0 && 104 (f.MaxFileSize != 0 && f.size+int64(len(p)) > f.MaxFileSize || 105 f.MaxFileAge != 0 && time.Since(f.start) > f.MaxFileAge) { 106 107 err = f.rotate() 108 if err != nil { 109 return 0, errors.Wrap(err, "rotate") 110 } 111 } 112 113 n, err = f.w.Write(p) 114 f.size += int64(n) 115 if err != nil { 116 return 117 } 118 119 return 120 } 121 122 func (f *File) Rotate() error { 123 defer f.mu.Unlock() 124 f.mu.Lock() 125 126 return f.rotate() 127 } 128 129 func (f *File) rotate() (err error) { 130 if f.format == "" { 131 f.dir, f.pref, f.suff, f.format = splitPattern(f.name) 132 133 f.num, err = f.findMaxNum(f.dir, f.pref, f.suff, f.format) 134 if err != nil { 135 return errors.Wrap(err, "find max num") 136 } 137 } 138 139 f.num++ 140 // base := fmt.Sprintf("%s%s%s", f.pref, fmt.Sprintf(f.format, f.num), f.suff) 141 base := f.pref + fmt.Sprintf(f.format, f.num) + f.suff 142 fname := filepath.Join(f.dir, base) 143 144 if c, ok := f.w.(io.Closer); ok { 145 _ = c.Close() 146 } 147 148 f.w, err = f.OpenFile(fname, f.Flags, f.Mode) 149 if err != nil { 150 return errors.Wrap(err, "") 151 } 152 153 now := time.Now() 154 155 f.current = fname 156 f.size = 0 157 f.start = now 158 // f.start = fileCtime(f.fstat, fname, now) 159 160 if f.symlink != nil { 161 link := filepath.Join(f.dir, f.pref+"LATEST"+f.suff) 162 163 _ = f.remove(link) 164 _ = f.symlink(base, link) 165 } 166 167 if f.MaxTotalSize != 0 || f.MaxTotalAge != 0 || f.MaxTotalFiles != 0 { 168 go f.removeOld(f.dir, base, f.pref, f.suff, f.format, f.start) 169 } 170 171 return 172 } 173 174 func (f *File) removeOld(dir, base, pref, suff, format string, now time.Time) error { 175 if f.removeSingleflight.Swap(true) { 176 return errors.New("already running") 177 } 178 179 defer f.removeSingleflight.Store(false) 180 181 files, err := f.matchingFiles(dir, pref, suff, format) 182 if err != nil { 183 return err 184 } 185 186 files, err = f.filesToRemove(dir, base, now, files) 187 if err != nil { 188 return err 189 } 190 191 for _, name := range files { 192 n := filepath.Join(dir, name) 193 194 e := f.remove(n) 195 if err == nil { 196 err = errors.Wrap(e, "remove %v", name) 197 } 198 } 199 200 return nil 201 } 202 203 func (f *File) findMaxNum(dir, pref, suff, format string) (num int, err error) { 204 entries, err := f.readdir(dir) 205 if err != nil { 206 return 0, errors.Wrap(err, "read dir") 207 } 208 209 for _, e := range entries { 210 n := e.Name() 211 212 m, ok := f.parseName(n, pref, suff, format) 213 if !ok { 214 continue 215 } 216 217 if m > num { 218 num = m 219 } 220 } 221 222 return num, nil 223 } 224 225 func (f *File) matchingFiles(dir, pref, suff, format string) ([]string, error) { 226 entries, err := f.readdir(dir) 227 if err != nil { 228 return nil, errors.Wrap(err, "read dir") 229 } 230 231 var files []string 232 233 for _, e := range entries { 234 n := e.Name() 235 236 _, ok := f.parseName(n, pref, suff, format) 237 if !ok { 238 continue 239 } 240 241 files = append(files, n) 242 } 243 244 return files, nil 245 } 246 247 func (f *File) parseName(n, pref, suff, format string) (num int, ok bool) { 248 // defer func() { println("parse name", n, pref, suff, num, ok, loc.Caller(1).String()) }() 249 if !strings.HasPrefix(n, pref) || !strings.HasSuffix(n, suff) || n == pref+"LATEST"+suff { 250 return 0, false 251 } 252 253 uniq := n[len(pref) : len(n)-len(suff)] 254 255 if _, err := fmt.Sscanf(uniq, format, &num); err != nil { 256 return 0, false 257 } 258 259 return num, true 260 } 261 262 func (f *File) filesToRemove(dir, base string, now time.Time, files []string) ([]string, error) { 263 p := len(files) 264 265 for p > 0 && files[p-1] >= base { 266 p-- 267 } 268 269 // tlog.Printw("files to remove", "past", files[:p], "future", files[p:]) 270 271 files = files[:p] 272 size := int64(0) 273 274 for p > 0 { 275 prev := p - 1 276 277 if f.MaxTotalFiles != 0 && len(files)-prev+1 > f.MaxTotalFiles { 278 // tlog.Printw("remove files", "reason", "max_total_files", "x", len(files)-prev, "of", f.MaxTotalFiles, "files", files[:p]) 279 break 280 } 281 282 n := filepath.Join(dir, files[prev]) 283 284 inf, err := f.fstat(n) 285 if err != nil { 286 return nil, errors.Wrap(err, "stat %v", files[prev]) 287 } 288 289 size += inf.Size() 290 291 if f.MaxTotalSize != 0 && size > f.MaxTotalSize { 292 // tlog.Printw("remove files", "reason", "max_total_size", "total_size", size, "of", f.MaxTotalSize, "files", files[:p]) 293 break 294 } 295 296 if f.MaxTotalAge != 0 && now.Sub(ctime(inf, now)) > f.MaxTotalAge { 297 // tlog.Printw("remove files", "reason", "max_total_age", "total_age", now.Sub(ctime(inf, now)), "of", f.MaxTotalAge, "files", files[:p]) 298 break 299 } 300 301 p-- 302 } 303 304 return files[:p], nil 305 } 306 307 func (f *File) Close() (err error) { 308 defer f.mu.Unlock() 309 f.mu.Lock() 310 311 c, ok := f.w.(io.Closer) 312 if ok { 313 err = c.Close() 314 } 315 316 f.w = nil 317 318 return err 319 } 320 321 func IsPattern(name string) bool { 322 _, pos := findPattern(name) 323 324 return pos != -1 325 } 326 327 func splitPattern(name string) (dir, pref, suff, format string) { 328 dir = filepath.Dir(name) 329 base := filepath.Base(name) 330 331 pattern, pos := findPattern(base) 332 333 if pos == -1 { 334 suff = filepath.Ext(base) 335 pref = strings.TrimSuffix(base, suff) 336 format = "%04X" 337 338 return 339 } 340 341 l := len(pattern) 342 343 for pos > 0 && base[pos-1] == pattern[0] { 344 pos-- 345 l++ 346 } 347 348 pref, suff = base[:pos], base[pos+l:] 349 format = fmt.Sprintf("%%0%d%c", l, pattern[0]) 350 351 return 352 } 353 354 func findPattern(name string) (string, int) { 355 var pattern string 356 var pos int = -1 357 358 for _, pat := range patterns { 359 p := strings.LastIndex(name, pat) 360 if p > pos { 361 pos = p 362 pattern = pat 363 } 364 } 365 366 return pattern, pos 367 } 368 369 func openFile(name string, flags int, mode os.FileMode) (io.Writer, error) { 370 return os.OpenFile(name, flags, mode) 371 }