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