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 }