github.com/igoogolx/clash@v1.19.8/adapter/provider/fetcher.go (about)

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