github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/fs/bwtimetable.go (about) 1 package fs 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "strconv" 8 "strings" 9 "time" 10 ) 11 12 // BwPair represents an upload and a download bandwidth 13 type BwPair struct { 14 Tx SizeSuffix // upload bandwidth 15 Rx SizeSuffix // download bandwidth 16 } 17 18 // String returns a printable representation of a BwPair 19 func (bp *BwPair) String() string { 20 var out strings.Builder 21 out.WriteString(bp.Tx.String()) 22 if bp.Rx != bp.Tx { 23 out.WriteRune(':') 24 out.WriteString(bp.Rx.String()) 25 } 26 return out.String() 27 } 28 29 // Set the bandwidth from a string which is either 30 // SizeSuffix or SizeSuffix:SizeSuffix (for tx:rx bandwidth) 31 func (bp *BwPair) Set(s string) (err error) { 32 colon := strings.Index(s, ":") 33 stx, srx := s, "" 34 if colon >= 0 { 35 stx, srx = s[:colon], s[colon+1:] 36 } 37 err = bp.Tx.Set(stx) 38 if err != nil { 39 return err 40 } 41 if colon < 0 { 42 bp.Rx = bp.Tx 43 } else { 44 err = bp.Rx.Set(srx) 45 if err != nil { 46 return err 47 } 48 } 49 return nil 50 } 51 52 // IsSet returns true if either of the bandwidth limits are set 53 func (bp *BwPair) IsSet() bool { 54 return bp.Tx > 0 || bp.Rx > 0 55 } 56 57 // BwTimeSlot represents a bandwidth configuration at a point in time. 58 type BwTimeSlot struct { 59 DayOfTheWeek int 60 HHMM int 61 Bandwidth BwPair 62 } 63 64 // BwTimetable contains all configured time slots. 65 type BwTimetable []BwTimeSlot 66 67 // String returns a printable representation of BwTimetable. 68 func (x BwTimetable) String() string { 69 var out strings.Builder 70 bwOnly := len(x) == 1 && x[0].DayOfTheWeek == 0 && x[0].HHMM == 0 71 for _, ts := range x { 72 if out.Len() != 0 { 73 out.WriteRune(' ') 74 } 75 if !bwOnly { 76 _, _ = fmt.Fprintf(&out, "%s-%02d:%02d,", time.Weekday(ts.DayOfTheWeek).String()[:3], ts.HHMM/100, ts.HHMM%100) 77 } 78 out.WriteString(ts.Bandwidth.String()) 79 } 80 return out.String() 81 } 82 83 // Basic hour format checking 84 func validateHour(HHMM string) error { 85 if len(HHMM) != 5 { 86 return fmt.Errorf("invalid time specification (hh:mm): %q", HHMM) 87 } 88 hh, err := strconv.Atoi(HHMM[0:2]) 89 if err != nil { 90 return fmt.Errorf("invalid hour in time specification %q: %v", HHMM, err) 91 } 92 if hh < 0 || hh > 23 { 93 return fmt.Errorf("invalid hour (must be between 00 and 23): %q", hh) 94 } 95 mm, err := strconv.Atoi(HHMM[3:]) 96 if err != nil { 97 return fmt.Errorf("invalid minute in time specification: %q: %v", HHMM, err) 98 } 99 if mm < 0 || mm > 59 { 100 return fmt.Errorf("invalid minute (must be between 00 and 59): %q", hh) 101 } 102 return nil 103 } 104 105 // Basic weekday format checking 106 func parseWeekday(dayOfWeek string) (int, error) { 107 dayOfWeek = strings.ToLower(dayOfWeek) 108 if dayOfWeek == "sun" || dayOfWeek == "sunday" { 109 return 0, nil 110 } 111 if dayOfWeek == "mon" || dayOfWeek == "monday" { 112 return 1, nil 113 } 114 if dayOfWeek == "tue" || dayOfWeek == "tuesday" { 115 return 2, nil 116 } 117 if dayOfWeek == "wed" || dayOfWeek == "wednesday" { 118 return 3, nil 119 } 120 if dayOfWeek == "thu" || dayOfWeek == "thursday" { 121 return 4, nil 122 } 123 if dayOfWeek == "fri" || dayOfWeek == "friday" { 124 return 5, nil 125 } 126 if dayOfWeek == "sat" || dayOfWeek == "saturday" { 127 return 6, nil 128 } 129 return 0, fmt.Errorf("invalid weekday: %q", dayOfWeek) 130 } 131 132 // Set the bandwidth timetable. 133 func (x *BwTimetable) Set(s string) error { 134 // The timetable is formatted as: 135 // "dayOfWeek-hh:mm,bandwidth dayOfWeek-hh:mm,bandwidth..." ex: "Mon-10:00,10G Mon-11:30,1G Tue-18:00,off" 136 // If only a single bandwidth identifier is provided, we assume constant bandwidth. 137 138 if len(s) == 0 { 139 return errors.New("empty string") 140 } 141 // Single value without time specification. 142 if !strings.Contains(s, " ") && !strings.Contains(s, ",") { 143 ts := BwTimeSlot{} 144 if err := ts.Bandwidth.Set(s); err != nil { 145 return err 146 } 147 ts.DayOfTheWeek = 0 148 ts.HHMM = 0 149 *x = BwTimetable{ts} 150 return nil 151 } 152 153 for _, tok := range strings.Split(s, " ") { 154 tv := strings.Split(tok, ",") 155 156 // Format must be dayOfWeek-HH:MM,BW 157 if len(tv) != 2 { 158 return fmt.Errorf("invalid time/bandwidth specification: %q", tok) 159 } 160 161 weekday := 0 162 HHMM := "" 163 if !strings.Contains(tv[0], "-") { 164 HHMM = tv[0] 165 if err := validateHour(HHMM); err != nil { 166 return err 167 } 168 for i := 0; i < 7; i++ { 169 hh, _ := strconv.Atoi(HHMM[0:2]) 170 mm, _ := strconv.Atoi(HHMM[3:]) 171 ts := BwTimeSlot{ 172 DayOfTheWeek: i, 173 HHMM: (hh * 100) + mm, 174 } 175 if err := ts.Bandwidth.Set(tv[1]); err != nil { 176 return err 177 } 178 *x = append(*x, ts) 179 } 180 } else { 181 timespec := strings.Split(tv[0], "-") 182 if len(timespec) != 2 { 183 return fmt.Errorf("invalid time specification: %q", tv[0]) 184 } 185 var err error 186 weekday, err = parseWeekday(timespec[0]) 187 if err != nil { 188 return err 189 } 190 HHMM = timespec[1] 191 if err := validateHour(HHMM); err != nil { 192 return err 193 } 194 195 hh, _ := strconv.Atoi(HHMM[0:2]) 196 mm, _ := strconv.Atoi(HHMM[3:]) 197 ts := BwTimeSlot{ 198 DayOfTheWeek: weekday, 199 HHMM: (hh * 100) + mm, 200 } 201 // Bandwidth limit for this time slot. 202 if err := ts.Bandwidth.Set(tv[1]); err != nil { 203 return err 204 } 205 *x = append(*x, ts) 206 } 207 } 208 return nil 209 } 210 211 // Difference in minutes between lateDayOfWeekHHMM and earlyDayOfWeekHHMM 212 func timeDiff(lateDayOfWeekHHMM int, earlyDayOfWeekHHMM int) int { 213 214 lateTimeMinutes := (lateDayOfWeekHHMM / 10000) * 24 * 60 215 lateTimeMinutes += ((lateDayOfWeekHHMM / 100) % 100) * 60 216 lateTimeMinutes += lateDayOfWeekHHMM % 100 217 218 earlyTimeMinutes := (earlyDayOfWeekHHMM / 10000) * 24 * 60 219 earlyTimeMinutes += ((earlyDayOfWeekHHMM / 100) % 100) * 60 220 earlyTimeMinutes += earlyDayOfWeekHHMM % 100 221 222 return lateTimeMinutes - earlyTimeMinutes 223 } 224 225 // LimitAt returns a BwTimeSlot for the time requested. 226 func (x BwTimetable) LimitAt(tt time.Time) BwTimeSlot { 227 // If the timetable is empty, we return an unlimited BwTimeSlot starting at Sunday midnight. 228 if len(x) == 0 { 229 return BwTimeSlot{Bandwidth: BwPair{-1, -1}} 230 } 231 232 dayOfWeekHHMM := int(tt.Weekday())*10000 + tt.Hour()*100 + tt.Minute() 233 234 // By default, we return the last element in the timetable. This 235 // satisfies two conditions: 1) If there's only one element it 236 // will always be selected, and 2) The last element of the table 237 // will "wrap around" until overridden by an earlier time slot. 238 // there's only one time slot in the timetable. 239 ret := x[len(x)-1] 240 mindif := 0 241 first := true 242 243 // Look for most recent time slot. 244 for _, ts := range x { 245 // Ignore the past 246 if dayOfWeekHHMM < (ts.DayOfTheWeek*10000)+ts.HHMM { 247 continue 248 } 249 dif := timeDiff(dayOfWeekHHMM, (ts.DayOfTheWeek*10000)+ts.HHMM) 250 if first { 251 mindif = dif 252 first = false 253 } 254 if dif <= mindif { 255 mindif = dif 256 ret = ts 257 } 258 } 259 260 return ret 261 } 262 263 // Type of the value 264 func (x BwTimetable) Type() string { 265 return "BwTimetable" 266 } 267 268 // UnmarshalJSON unmarshals a string value 269 func (x *BwTimetable) UnmarshalJSON(in []byte) error { 270 var s string 271 err := json.Unmarshal(in, &s) 272 if err != nil { 273 return err 274 } 275 return x.Set(s) 276 } 277 278 // MarshalJSON marshals as a string value 279 func (x BwTimetable) MarshalJSON() ([]byte, error) { 280 s := x.String() 281 return json.Marshal(s) 282 }