github.com/kaydxh/golang@v0.0.131/pkg/file-cleanup/disk/disk_cleaner.go (about) 1 /* 2 *Copyright (c) 2022, kaydxh 3 * 4 *Permission is hereby granted, free of charge, to any person obtaining a copy 5 *of this software and associated documentation files (the "Software"), to deal 6 *in the Software without restriction, including without limitation the rights 7 *to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 *copies of the Software, and to permit persons to whom the Software is 9 *furnished to do so, subject to the following conditions: 10 * 11 *The above copyright notice and this permission notice shall be included in all 12 *copies or substantial portions of the Software. 13 * 14 *THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 *IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 *FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 *AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 *LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 *OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 *SOFTWARE. 21 */ 22 package disk 23 24 import ( 25 "context" 26 "fmt" 27 "os" 28 "path/filepath" 29 "sync" 30 "time" 31 32 "github.com/kaydxh/golang/go/errors" 33 errors_ "github.com/kaydxh/golang/go/errors" 34 filepath_ "github.com/kaydxh/golang/go/path/filepath" 35 strings_ "github.com/kaydxh/golang/go/strings" 36 syscall_ "github.com/kaydxh/golang/go/syscall" 37 time_ "github.com/kaydxh/golang/go/time" 38 "github.com/sirupsen/logrus" 39 "go.uber.org/atomic" 40 ) 41 42 var ( 43 workDir string 44 ) 45 46 func init() { 47 workDir, _ = os.Getwd() 48 } 49 50 const ( 51 DefaultCheckInterval time.Duration = time.Minute 52 DefaultbaseExpired time.Duration = 72 * time.Hour 53 DefalutRandomizationFactor = 0.1 54 DefalutMultiplier = 0.8 55 DefalutMinInterval = time.Minute 56 ) 57 58 type DiskCleanerConfig struct { 59 checkInterval time.Duration 60 baseExpired time.Duration 61 minExpired time.Duration 62 diskUsageCallBack func(diskPath string, diskUsage float32) 63 cleanPostCallBack func(file string, err error) 64 } 65 66 // DiskCleanerSerivce ... 67 type DiskCleanerSerivce struct { 68 // path:ExponentialBackOffMap 69 epoByPath time_.ExponentialBackOffMap 70 exts []string 71 //0-100 72 diskUsage float32 73 inShutdown atomic.Bool // true when when server is in shutdown 74 75 opts DiskCleanerConfig 76 77 mu sync.Mutex 78 cancel func() 79 } 80 81 func checkAndCanoicalzePaths(paths ...string) ([]string, bool) { 82 var canPaths []string 83 for _, path := range paths { 84 absPath, err := filepath_.CanonicalizePath(path) 85 if err != nil { 86 87 fmt.Printf("err: %v\n", err) 88 return nil, false 89 } 90 91 if absPath == "" || absPath == "/" || absPath == workDir { 92 return nil, false 93 } 94 canPaths = append(canPaths, absPath) 95 } 96 97 return canPaths, true 98 99 } 100 101 // NewDiskCleanerSerivce ... 102 func NewDiskCleanerSerivce( 103 diskUsage float32, 104 paths []string, 105 exts []string, 106 opts ...DiskCleanerConfigOption, 107 ) (*DiskCleanerSerivce, error) { 108 canPaths, ok := checkAndCanoicalzePaths(paths...) 109 if !ok { 110 return nil, fmt.Errorf("invalid paths for disk cheaner") 111 } 112 113 if diskUsage < 0 { 114 diskUsage = 0 115 } 116 if diskUsage > 100 { 117 diskUsage = 100 118 } 119 120 if len(exts) == 0 { 121 return nil, fmt.Errorf("invalid exts for disk cleaner") 122 } 123 124 s := &DiskCleanerSerivce{ 125 diskUsage: diskUsage, 126 exts: exts, 127 } 128 s.opts.ApplyOptions(opts...) 129 130 if s.opts.checkInterval == 0 { 131 s.opts.checkInterval = DefaultCheckInterval 132 } 133 if s.opts.minExpired == 0 { 134 s.opts.minExpired = DefalutMinInterval 135 } 136 if s.opts.baseExpired == 0 { 137 s.opts.baseExpired = DefaultbaseExpired 138 } 139 140 for _, path := range canPaths { 141 exp := time_.NewExponentialBackOff( 142 time_.WithExponentialBackOffOptionInitialInterval(s.opts.baseExpired), 143 time_.WithExponentialBackOffOptionRandomizationFactor(DefalutRandomizationFactor), 144 time_.WithExponentialBackOffOptionMultiplier(DefalutMultiplier), 145 time_.WithExponentialBackOffOptionMaxInterval(s.opts.baseExpired), 146 time_.WithExponentialBackOffOptionMinInterval(s.opts.minExpired), 147 time_.WithExponentialBackOffOptionMaxElapsedTime(0), 148 ) 149 150 s.epoByPath.Store(path, *exp) 151 } 152 fmt.Printf("s: %+v\n", s) 153 return s, nil 154 } 155 156 func (s *DiskCleanerSerivce) getLogger() *logrus.Entry { 157 return logrus.WithField("module", "DiskCleaner") 158 } 159 160 // Run will initialize the backend. It must not block, but may run go routines in the background. 161 func (s *DiskCleanerSerivce) Run(ctx context.Context) error { 162 logger := s.getLogger() 163 logger.Infoln("DiskCleaner Run") 164 if s.inShutdown.Load() { 165 logger.Infoln("DiskCleaner Shutdown") 166 return fmt.Errorf("server closed") 167 } 168 go func() { 169 errors.HandleError(s.Serve(ctx)) 170 }() 171 return nil 172 } 173 174 // Serve ... 175 func (s *DiskCleanerSerivce) Serve(ctx context.Context) error { 176 logger := s.getLogger() 177 logger.Infoln("DiskCleaner Serve") 178 179 if s.inShutdown.Load() { 180 logger.Infoln("DiskCleaner Shutdown") 181 return fmt.Errorf("server closed") 182 } 183 184 defer s.inShutdown.Store(true) 185 ctx, cancel := context.WithCancel(ctx) 186 defer cancel() 187 s.mu.Lock() 188 s.cancel = cancel 189 s.mu.Unlock() 190 191 time_.UntilWithContxt(ctx, func(ctx context.Context) error { 192 err := s.clean(ctx) 193 if err != nil { 194 logger.WithError(err).Errorf("failed to clean") 195 return err 196 } 197 return nil 198 }, s.opts.checkInterval) 199 if err := ctx.Err(); err != nil { 200 logger.WithError(err).Errorf("stopped checking") 201 return err 202 } 203 logger.Info("stopped checking") 204 return nil 205 } 206 207 func (s *DiskCleanerSerivce) clean(ctx context.Context) error { 208 209 var ( 210 wg sync.WaitGroup 211 errs []error 212 ) 213 214 logger := s.getLogger() 215 s.epoByPath.Range(func(path string, ebo time_.ExponentialBackOff) bool { 216 wg.Add(1) 217 go func(diskPath string, ebo time_.ExponentialBackOff) { 218 defer wg.Done() 219 du, err := syscall_.NewDiskUsage(diskPath) 220 if err != nil { 221 s.mu.Lock() 222 errs = append(errs, err) 223 s.mu.Unlock() 224 return 225 } 226 227 if s.opts.diskUsageCallBack != nil { 228 s.opts.diskUsageCallBack(diskPath, du.Usage()) 229 } 230 231 if du.Usage() >= s.diskUsage { 232 //clean 233 logger.Infof( 234 "disk[%v] usage over %v, file expired: %v, start to clean", 235 diskPath, 236 s.diskUsage, 237 ebo.GetCurrentInterval(), 238 ) 239 actualExpired, _ := ebo.NextBackOff() 240 filepath.Walk(diskPath, func(filePath string, info os.FileInfo, err error) error { 241 242 if !info.IsDir() { 243 if strings_.SliceContainsCaseInSensitive(s.exts, filepath.Ext(filePath)) { 244 now := time.Now() 245 if now.Sub(info.ModTime()) > actualExpired { 246 logger.Infof( 247 "delete file %v expired[%v], modify time: %v, now: %v", 248 filePath, 249 actualExpired, 250 info.ModTime(), 251 now, 252 ) 253 err = os.Remove(filePath) 254 if s.opts.cleanPostCallBack != nil { 255 s.opts.cleanPostCallBack(filePath, err) 256 } 257 } 258 } 259 } 260 261 return nil 262 }) 263 264 } else { 265 // reset expired Time 266 ebo.Reset() 267 logger.Debugf("reset disk path: %v expired time: %v", diskPath, ebo.GetCurrentInterval()) 268 } 269 s.epoByPath.Store(diskPath, ebo) 270 271 }(path, ebo) 272 273 return true 274 }) 275 wg.Wait() 276 return errors_.NewAggregate(errs) 277 } 278 279 // Shutdown ... 280 func (s *DiskCleanerSerivce) Shutdown() { 281 s.inShutdown.Store(true) 282 s.mu.Lock() 283 defer s.mu.Unlock() 284 if s.cancel != nil { 285 s.cancel() 286 } 287 }