github.com/kelleygo/clashcore@v1.0.2/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/kelleygo/clashcore/constant/provider" 11 "github.com/kelleygo/clashcore/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 }