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  }