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  }