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