github.com/metacubex/mihomo@v1.18.5/component/resource/fetcher.go (about)

     1  package resource
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"os"
     7  	"path/filepath"
     8  	"time"
     9  
    10  	types "github.com/metacubex/mihomo/constant/provider"
    11  	"github.com/metacubex/mihomo/log"
    12  
    13  	"github.com/samber/lo"
    14  )
    15  
    16  var (
    17  	fileMode os.FileMode = 0o666
    18  	dirMode  os.FileMode = 0o755
    19  )
    20  
    21  type Parser[V any] func([]byte) (V, error)
    22  
    23  type Fetcher[V any] struct {
    24  	resourceType string
    25  	name         string
    26  	vehicle      types.Vehicle
    27  	UpdatedAt    time.Time
    28  	done         chan struct{}
    29  	hash         [16]byte
    30  	parser       Parser[V]
    31  	interval     time.Duration
    32  	OnUpdate     func(V)
    33  }
    34  
    35  func (f *Fetcher[V]) Name() string {
    36  	return f.name
    37  }
    38  
    39  func (f *Fetcher[V]) Vehicle() types.Vehicle {
    40  	return f.vehicle
    41  }
    42  
    43  func (f *Fetcher[V]) VehicleType() types.VehicleType {
    44  	return f.vehicle.Type()
    45  }
    46  
    47  func (f *Fetcher[V]) Initial() (V, error) {
    48  	var (
    49  		buf         []byte
    50  		err         error
    51  		isLocal     bool
    52  		forceUpdate bool
    53  	)
    54  
    55  	if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
    56  		buf, err = os.ReadFile(f.vehicle.Path())
    57  		modTime := stat.ModTime()
    58  		f.UpdatedAt = modTime
    59  		isLocal = true
    60  		if f.interval != 0 && modTime.Add(f.interval).Before(time.Now()) {
    61  			log.Warnln("[Provider] %s not updated for a long time, force refresh", f.Name())
    62  			forceUpdate = true
    63  		}
    64  	} else {
    65  		buf, err = f.vehicle.Read()
    66  		f.UpdatedAt = time.Now()
    67  	}
    68  
    69  	if err != nil {
    70  		return lo.Empty[V](), err
    71  	}
    72  
    73  	var contents V
    74  	if forceUpdate {
    75  		var forceBuf []byte
    76  		if forceBuf, err = f.vehicle.Read(); err == nil {
    77  			if contents, err = f.parser(forceBuf); err == nil {
    78  				isLocal = false
    79  				buf = forceBuf
    80  			}
    81  		}
    82  	}
    83  
    84  	if err != nil || !forceUpdate {
    85  		contents, err = f.parser(buf)
    86  	}
    87  
    88  	if err != nil {
    89  		if !isLocal {
    90  			return lo.Empty[V](), err
    91  		}
    92  
    93  		// parse local file error, fallback to remote
    94  		buf, err = f.vehicle.Read()
    95  		if err != nil {
    96  			return lo.Empty[V](), err
    97  		}
    98  
    99  		contents, err = f.parser(buf)
   100  		if err != nil {
   101  			return lo.Empty[V](), err
   102  		}
   103  
   104  		isLocal = false
   105  	}
   106  
   107  	if f.vehicle.Type() != types.File && !isLocal {
   108  		if err := safeWrite(f.vehicle.Path(), buf); err != nil {
   109  			return lo.Empty[V](), err
   110  		}
   111  	}
   112  
   113  	f.hash = md5.Sum(buf)
   114  
   115  	// pull contents automatically
   116  	if f.interval > 0 {
   117  		go f.pullLoop()
   118  	}
   119  
   120  	return contents, nil
   121  }
   122  
   123  func (f *Fetcher[V]) Update() (V, bool, error) {
   124  	buf, err := f.vehicle.Read()
   125  	if err != nil {
   126  		return lo.Empty[V](), false, err
   127  	}
   128  
   129  	now := time.Now()
   130  	hash := md5.Sum(buf)
   131  	if bytes.Equal(f.hash[:], hash[:]) {
   132  		f.UpdatedAt = now
   133  		_ = os.Chtimes(f.vehicle.Path(), now, now)
   134  		return lo.Empty[V](), true, nil
   135  	}
   136  
   137  	contents, err := f.parser(buf)
   138  	if err != nil {
   139  		return lo.Empty[V](), false, err
   140  	}
   141  
   142  	if f.vehicle.Type() != types.File {
   143  		if err := safeWrite(f.vehicle.Path(), buf); err != nil {
   144  			return lo.Empty[V](), false, err
   145  		}
   146  	}
   147  
   148  	f.UpdatedAt = now
   149  	f.hash = hash
   150  
   151  	return contents, false, nil
   152  }
   153  
   154  func (f *Fetcher[V]) Destroy() error {
   155  	if f.interval > 0 {
   156  		f.done <- struct{}{}
   157  	}
   158  	return nil
   159  }
   160  
   161  func (f *Fetcher[V]) pullLoop() {
   162  	initialInterval := f.interval - time.Since(f.UpdatedAt)
   163  	if initialInterval > f.interval {
   164  		initialInterval = f.interval
   165  	}
   166  
   167  	timer := time.NewTimer(initialInterval)
   168  	defer timer.Stop()
   169  	for {
   170  		select {
   171  		case <-timer.C:
   172  			timer.Reset(f.interval)
   173  			elm, same, err := f.Update()
   174  			if err != nil {
   175  				log.Errorln("[Provider] %s pull error: %s", f.Name(), err.Error())
   176  				continue
   177  			}
   178  
   179  			if same {
   180  				log.Debugln("[Provider] %s's content doesn't change", f.Name())
   181  				continue
   182  			}
   183  
   184  			log.Infoln("[Provider] %s's content update", f.Name())
   185  			if f.OnUpdate != nil {
   186  				f.OnUpdate(elm)
   187  			}
   188  		case <-f.done:
   189  			return
   190  		}
   191  	}
   192  }
   193  
   194  func safeWrite(path string, buf []byte) error {
   195  	dir := filepath.Dir(path)
   196  
   197  	if _, err := os.Stat(dir); os.IsNotExist(err) {
   198  		if err := os.MkdirAll(dir, dirMode); err != nil {
   199  			return err
   200  		}
   201  	}
   202  
   203  	return os.WriteFile(path, buf, fileMode)
   204  }
   205  
   206  func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] {
   207  
   208  	return &Fetcher[V]{
   209  		name:     name,
   210  		vehicle:  vehicle,
   211  		parser:   parser,
   212  		done:     make(chan struct{}, 8),
   213  		OnUpdate: onUpdate,
   214  		interval: interval,
   215  	}
   216  }