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 }