code.gitea.io/gitea@v1.22.3/modules/util/rotatingfilewriter/writer.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package rotatingfilewriter 5 6 import ( 7 "bufio" 8 "compress/gzip" 9 "errors" 10 "fmt" 11 "os" 12 "path/filepath" 13 "strings" 14 "sync" 15 "time" 16 17 "code.gitea.io/gitea/modules/graceful/releasereopen" 18 "code.gitea.io/gitea/modules/util" 19 ) 20 21 type Options struct { 22 Rotate bool 23 MaximumSize int64 24 RotateDaily bool 25 KeepDays int 26 Compress bool 27 CompressionLevel int 28 } 29 30 type RotatingFileWriter struct { 31 mu sync.Mutex 32 fd *os.File 33 34 currentSize int64 35 openDate int 36 37 options Options 38 39 cancelReleaseReopen func() 40 } 41 42 var ErrorPrintf func(format string, args ...any) 43 44 // errorf tries to print error messages. Since this writer could be used by a logger system, this is the last chance to show the error in some cases 45 func errorf(format string, args ...any) { 46 if ErrorPrintf != nil { 47 ErrorPrintf("rotatingfilewriter: "+format+"\n", args...) 48 } 49 } 50 51 // Open creates a new rotating file writer. 52 // Notice: if a file is opened by two rotators, there will be conflicts when rotating. 53 // In the future, there should be "rotating file manager" 54 func Open(filename string, options *Options) (*RotatingFileWriter, error) { 55 if options == nil { 56 options = &Options{} 57 } 58 59 rfw := &RotatingFileWriter{ 60 options: *options, 61 } 62 63 if err := rfw.open(filename); err != nil { 64 return nil, err 65 } 66 67 rfw.cancelReleaseReopen = releasereopen.GetManager().Register(rfw) 68 return rfw, nil 69 } 70 71 func (rfw *RotatingFileWriter) Write(b []byte) (int, error) { 72 if rfw.options.Rotate && ((rfw.options.MaximumSize > 0 && rfw.currentSize >= rfw.options.MaximumSize) || (rfw.options.RotateDaily && time.Now().Day() != rfw.openDate)) { 73 if err := rfw.DoRotate(); err != nil { 74 // if this writer is used by a logger system, it's the logger system's responsibility to handle/show the error 75 return 0, err 76 } 77 } 78 79 n, err := rfw.fd.Write(b) 80 if err == nil { 81 rfw.currentSize += int64(n) 82 } 83 return n, err 84 } 85 86 func (rfw *RotatingFileWriter) Flush() error { 87 return rfw.fd.Sync() 88 } 89 90 func (rfw *RotatingFileWriter) Close() error { 91 rfw.mu.Lock() 92 if rfw.cancelReleaseReopen != nil { 93 rfw.cancelReleaseReopen() 94 rfw.cancelReleaseReopen = nil 95 } 96 rfw.mu.Unlock() 97 return rfw.fd.Close() 98 } 99 100 func (rfw *RotatingFileWriter) open(filename string) error { 101 fd, err := os.OpenFile(filename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0o660) 102 if err != nil { 103 return err 104 } 105 106 rfw.fd = fd 107 108 finfo, err := fd.Stat() 109 if err != nil { 110 return err 111 } 112 rfw.currentSize = finfo.Size() 113 rfw.openDate = finfo.ModTime().Day() 114 115 return nil 116 } 117 118 func (rfw *RotatingFileWriter) ReleaseReopen() error { 119 return errors.Join( 120 rfw.fd.Close(), 121 rfw.open(rfw.fd.Name()), 122 ) 123 } 124 125 // DoRotate the log file creating a backup like xx.2013-01-01.2 126 func (rfw *RotatingFileWriter) DoRotate() error { 127 if !rfw.options.Rotate { 128 return nil 129 } 130 131 rfw.mu.Lock() 132 defer rfw.mu.Unlock() 133 134 prefix := fmt.Sprintf("%s.%s.", rfw.fd.Name(), time.Now().Format("2006-01-02")) 135 136 var err error 137 fname := "" 138 for i := 1; err == nil && i <= 999; i++ { 139 fname = prefix + fmt.Sprintf("%03d", i) 140 _, err = os.Lstat(fname) 141 if rfw.options.Compress && err != nil { 142 _, err = os.Lstat(fname + ".gz") 143 } 144 } 145 // return error if the last file checked still existed 146 if err == nil { 147 return fmt.Errorf("cannot find free file to rename %s", rfw.fd.Name()) 148 } 149 150 fd := rfw.fd 151 if err := fd.Close(); err != nil { // close file before rename 152 return err 153 } 154 155 if err := util.Rename(fd.Name(), fname); err != nil { 156 return err 157 } 158 159 if rfw.options.Compress { 160 go func() { 161 err := compressOldFile(fname, rfw.options.CompressionLevel) 162 if err != nil { 163 errorf("DoRotate: %v", err) 164 } 165 }() 166 } 167 168 if err := rfw.open(fd.Name()); err != nil { 169 return err 170 } 171 172 go deleteOldFiles( 173 filepath.Dir(fd.Name()), 174 filepath.Base(fd.Name()), 175 time.Now().AddDate(0, 0, -rfw.options.KeepDays), 176 ) 177 178 return nil 179 } 180 181 func compressOldFile(fname string, compressionLevel int) error { 182 reader, err := os.Open(fname) 183 if err != nil { 184 return fmt.Errorf("compressOldFile: failed to open existing file %s: %w", fname, err) 185 } 186 defer reader.Close() 187 188 buffer := bufio.NewReader(reader) 189 fnameGz := fname + ".gz" 190 fw, err := os.OpenFile(fnameGz, os.O_WRONLY|os.O_CREATE, 0o660) 191 if err != nil { 192 return fmt.Errorf("compressOldFile: failed to open new file %s: %w", fnameGz, err) 193 } 194 defer fw.Close() 195 196 zw, err := gzip.NewWriterLevel(fw, compressionLevel) 197 if err != nil { 198 return fmt.Errorf("compressOldFile: failed to create gzip writer: %w", err) 199 } 200 defer zw.Close() 201 202 _, err = buffer.WriteTo(zw) 203 if err != nil { 204 _ = zw.Close() 205 _ = fw.Close() 206 _ = util.Remove(fname + ".gz") 207 return fmt.Errorf("compressOldFile: failed to write to gz file: %w", err) 208 } 209 _ = reader.Close() 210 211 err = util.Remove(fname) 212 if err != nil { 213 return fmt.Errorf("compressOldFile: failed to delete old file: %w", err) 214 } 215 return nil 216 } 217 218 func deleteOldFiles(dir, prefix string, removeBefore time.Time) { 219 err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) (returnErr error) { 220 defer func() { 221 if r := recover(); r != nil { 222 returnErr = fmt.Errorf("unable to delete old file '%s', error: %+v", path, r) 223 } 224 }() 225 226 if err != nil { 227 return err 228 } 229 if d.IsDir() { 230 return nil 231 } 232 info, err := d.Info() 233 if err != nil { 234 return err 235 } 236 if info.ModTime().Before(removeBefore) { 237 if strings.HasPrefix(filepath.Base(path), prefix) { 238 return util.Remove(path) 239 } 240 } 241 return nil 242 }) 243 if err != nil { 244 errorf("deleteOldFiles: failed to delete old file: %v", err) 245 } 246 }