github.com/webonyx/up@v0.7.4-0.20180808230834-91b94e551323/internal/util/util.go (about)

     1  // Package util haters gonna hate.
     2  package util
     3  
     4  import (
     5  	"bufio"
     6  	"crypto/md5"
     7  	"encoding/hex"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"math"
    13  	"net"
    14  	"net/http"
    15  	"net/url"
    16  	"os"
    17  	"os/exec"
    18  	"regexp"
    19  	"sort"
    20  	"strings"
    21  	"syscall"
    22  	"time"
    23  
    24  	"github.com/apex/up/internal/colors"
    25  	humanize "github.com/dustin/go-humanize"
    26  	"github.com/pascaldekloe/name"
    27  	"github.com/pkg/errors"
    28  	"github.com/tj/backoff"
    29  	"github.com/tj/go-progress"
    30  	"github.com/tj/go/term"
    31  	"golang.org/x/net/publicsuffix"
    32  )
    33  
    34  // ClearHeader removes all content header fields.
    35  func ClearHeader(h http.Header) {
    36  	h.Del("Content-Type")
    37  	h.Del("Content-Length")
    38  	h.Del("Content-Encoding")
    39  	h.Del("Content-Range")
    40  	h.Del("Content-MD5")
    41  	h.Del("Cache-Control")
    42  	h.Del("ETag")
    43  	h.Del("Last-Modified")
    44  }
    45  
    46  // ManagedByUp appends "Managed by Up".
    47  func ManagedByUp(s string) string {
    48  	if s == "" {
    49  		return "Managed by Up."
    50  	}
    51  
    52  	return s + " (Managed by Up)."
    53  }
    54  
    55  // Exists returns true if the file exists.
    56  func Exists(path string) bool {
    57  	_, err := os.Stat(path)
    58  	return err == nil
    59  }
    60  
    61  // ReadFileJSON reads json from the given path.
    62  func ReadFileJSON(path string, v interface{}) error {
    63  	b, err := ioutil.ReadFile(path)
    64  	if err != nil {
    65  		return errors.Wrap(err, "reading")
    66  	}
    67  
    68  	if err := json.Unmarshal(b, &v); err != nil {
    69  		return errors.Wrap(err, "unmarshaling")
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  // Camelcase string with optional args.
    76  func Camelcase(s string, v ...interface{}) string {
    77  	return name.CamelCase(fmt.Sprintf(s, v...), true)
    78  }
    79  
    80  // NewProgressInt with the given total.
    81  func NewProgressInt(total int) *progress.Bar {
    82  	b := progress.NewInt(total)
    83  	b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
    84  	b.Width = 35
    85  	b.StartDelimiter = colors.Gray("|")
    86  	b.EndDelimiter = colors.Gray("|")
    87  	b.Filled = colors.Purple("█")
    88  	b.Empty = colors.Gray("░")
    89  	return b
    90  }
    91  
    92  // NewInlineProgressInt with the given total.
    93  func NewInlineProgressInt(total int) *progress.Bar {
    94  	b := progress.NewInt(total)
    95  	b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
    96  	b.Width = 20
    97  	b.StartDelimiter = colors.Gray("|")
    98  	b.EndDelimiter = colors.Gray("|")
    99  	b.Filled = colors.Purple("█")
   100  	b.Empty = colors.Gray(" ")
   101  	return b
   102  }
   103  
   104  // Pad helper.
   105  func Pad() func() {
   106  	println()
   107  	return func() {
   108  		println()
   109  	}
   110  }
   111  
   112  // Fatal error.
   113  func Fatal(err error) {
   114  	fmt.Fprintf(os.Stderr, "\n     %s %s\n\n", colors.Red("Error:"), err)
   115  	os.Exit(1)
   116  }
   117  
   118  // IsJSON returns true if the string looks like json.
   119  func IsJSON(s string) bool {
   120  	return len(s) > 1 && s[0] == '{' && s[len(s)-1] == '}'
   121  }
   122  
   123  // IsJSONLog returns true if the string looks likes a json log.
   124  func IsJSONLog(s string) bool {
   125  	return IsJSON(s) && strings.Contains(s, `"level"`)
   126  }
   127  
   128  // IsNotFound returns true if err is not nil and represents a missing resource.
   129  func IsNotFound(err error) bool {
   130  	switch {
   131  	case err == nil:
   132  		return false
   133  	case strings.Contains(err.Error(), "ResourceNotFoundException"):
   134  		return true
   135  	case strings.Contains(err.Error(), "NoSuchEntity"):
   136  		return true
   137  	case strings.Contains(err.Error(), "does not exist"):
   138  		return true
   139  	case strings.Contains(err.Error(), "not found"):
   140  		return true
   141  	default:
   142  		return false
   143  	}
   144  }
   145  
   146  // IsBucketExists returns true if err is not nil and represents an existing bucket.
   147  func IsBucketExists(err error) bool {
   148  	switch {
   149  	case err == nil:
   150  		return false
   151  	case strings.Contains(err.Error(), "BucketAlreadyOwnedByYou"):
   152  		return true
   153  	default:
   154  		return false
   155  	}
   156  }
   157  
   158  // IsThrottled returns true if err is not nil and represents a throttled request.
   159  func IsThrottled(err error) bool {
   160  	switch {
   161  	case err == nil:
   162  		return false
   163  	case strings.Contains(err.Error(), "Throttling: Rate exceeded"):
   164  		return true
   165  	default:
   166  		return false
   167  	}
   168  }
   169  
   170  // IsNoCredentials returns true if err is not nil and represents missing credentials.
   171  func IsNoCredentials(err error) bool {
   172  	switch {
   173  	case err == nil:
   174  		return false
   175  	case strings.Contains(err.Error(), "NoCredentialProviders"):
   176  		return true
   177  	default:
   178  		return false
   179  	}
   180  }
   181  
   182  // Env returns a slice from environment variable map.
   183  func Env(m map[string]string) (env []string) {
   184  	for k, v := range m {
   185  		env = append(env, fmt.Sprintf("%s=%s", k, v))
   186  	}
   187  	return
   188  }
   189  
   190  // PrefixLines prefixes the lines in s with prefix.
   191  func PrefixLines(s string, prefix string) string {
   192  	lines := strings.Split(s, "\n")
   193  	for i, l := range lines {
   194  		lines[i] = prefix + l
   195  	}
   196  	return strings.Join(lines, "\n")
   197  }
   198  
   199  // Indent the given string.
   200  func Indent(s string) string {
   201  	return PrefixLines(s, "  ")
   202  }
   203  
   204  // DefaultString returns d unless s is present.
   205  func DefaultString(s *string, d string) string {
   206  	if s == nil || *s == "" {
   207  		return d
   208  	}
   209  
   210  	return *s
   211  }
   212  
   213  // WaitForListen blocks until `u` is listening with timeout.
   214  func WaitForListen(u *url.URL, timeout time.Duration) error {
   215  	timedout := time.After(timeout)
   216  
   217  	b := backoff.Backoff{
   218  		Min:    100 * time.Millisecond,
   219  		Max:    time.Second,
   220  		Factor: 1.5,
   221  	}
   222  
   223  	for {
   224  		select {
   225  		case <-timedout:
   226  			return errors.Errorf("timed out after %s", timeout)
   227  		case <-time.After(b.Duration()):
   228  			if IsListening(u) {
   229  				return nil
   230  			}
   231  		}
   232  	}
   233  }
   234  
   235  // IsListening returns true if there's a server listening on `u`.
   236  func IsListening(u *url.URL) bool {
   237  	conn, err := net.Dial("tcp", u.Host)
   238  	if err != nil {
   239  		return false
   240  	}
   241  
   242  	conn.Close()
   243  	return true
   244  }
   245  
   246  // ExitStatus returns the exit status of cmd.
   247  func ExitStatus(cmd *exec.Cmd, err error) string {
   248  	ps := cmd.ProcessState
   249  
   250  	if e, ok := err.(*exec.ExitError); ok {
   251  		ps = e.ProcessState
   252  	}
   253  
   254  	if ps != nil {
   255  		s, ok := ps.Sys().(syscall.WaitStatus)
   256  		if ok {
   257  			return fmt.Sprintf("%d", s.ExitStatus())
   258  		}
   259  	}
   260  
   261  	return "?"
   262  }
   263  
   264  // StringsContains returns true if list contains s.
   265  func StringsContains(list []string, s string) bool {
   266  	for _, v := range list {
   267  		if v == s {
   268  			return true
   269  		}
   270  	}
   271  	return false
   272  }
   273  
   274  // BasePath returns a normalized base path,
   275  // stripping the leading '/' if present.
   276  func BasePath(s string) string {
   277  	return strings.TrimLeft(s, "/")
   278  }
   279  
   280  // LogPad outputs a log message with padding.
   281  func LogPad(msg string, v ...interface{}) {
   282  	defer Pad()()
   283  	Log(msg, v...)
   284  }
   285  
   286  // Log outputs a log message.
   287  func Log(msg string, v ...interface{}) {
   288  	fmt.Printf("     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
   289  }
   290  
   291  // LogClear clears the line and outputs a log message.
   292  func LogClear(msg string, v ...interface{}) {
   293  	term.MoveUp(1)
   294  	term.ClearLine()
   295  	fmt.Printf("\r     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
   296  }
   297  
   298  // LogTitle outputs a log title.
   299  func LogTitle(msg string, v ...interface{}) {
   300  	fmt.Printf("\n     \x1b[1m%s\x1b[m\n\n", fmt.Sprintf(msg, v...))
   301  }
   302  
   303  // LogName outputs a log message with name.
   304  func LogName(name, msg string, v ...interface{}) {
   305  	fmt.Printf("     %s %s\n", colors.Purple(name+":"), fmt.Sprintf(msg, v...))
   306  }
   307  
   308  // LogListItem outputs a list item.
   309  func LogListItem(msg string, v ...interface{}) {
   310  	fmt.Printf("      • %s\n", fmt.Sprintf(msg, v...))
   311  }
   312  
   313  // ToFloat returns a float or NaN.
   314  func ToFloat(v interface{}) float64 {
   315  	switch n := v.(type) {
   316  	case int:
   317  		return float64(n)
   318  	case int8:
   319  		return float64(n)
   320  	case int16:
   321  		return float64(n)
   322  	case int32:
   323  		return float64(n)
   324  	case int64:
   325  		return float64(n)
   326  	case uint:
   327  		return float64(n)
   328  	case uint8:
   329  		return float64(n)
   330  	case uint16:
   331  		return float64(n)
   332  	case uint32:
   333  		return float64(n)
   334  	case uint64:
   335  		return float64(n)
   336  	case float32:
   337  		return float64(n)
   338  	case float64:
   339  		return n
   340  	default:
   341  		return math.NaN()
   342  	}
   343  }
   344  
   345  // Milliseconds returns the duration as milliseconds.
   346  func Milliseconds(d time.Duration) int {
   347  	return int(d / time.Millisecond)
   348  }
   349  
   350  // MillisecondsSince returns the duration as milliseconds relative to time t.
   351  func MillisecondsSince(t time.Time) int {
   352  	return int(time.Since(t) / time.Millisecond)
   353  }
   354  
   355  // ParseDuration string with day and month approximation support.
   356  func ParseDuration(s string) (d time.Duration, err error) {
   357  	r := strings.NewReader(s)
   358  
   359  	switch {
   360  	case strings.HasSuffix(s, "d"):
   361  		var v float64
   362  		_, err = fmt.Fscanf(r, "%fd", &v)
   363  		d = time.Duration(v * float64(24*time.Hour))
   364  	case strings.HasSuffix(s, "w"):
   365  		var v float64
   366  		_, err = fmt.Fscanf(r, "%fw", &v)
   367  		d = time.Duration(v * float64(24*time.Hour*7))
   368  	case strings.HasSuffix(s, "mo"):
   369  		var v float64
   370  		_, err = fmt.Fscanf(r, "%fmo", &v)
   371  		d = time.Duration(v * float64(30*24*time.Hour))
   372  	case strings.HasSuffix(s, "M"):
   373  		var v float64
   374  		_, err = fmt.Fscanf(r, "%fM", &v)
   375  		d = time.Duration(v * float64(30*24*time.Hour))
   376  	default:
   377  		d, err = time.ParseDuration(s)
   378  	}
   379  
   380  	return
   381  }
   382  
   383  // Md5 returns an md5 hash for s.
   384  func Md5(s string) string {
   385  	h := md5.New()
   386  	h.Write([]byte(s))
   387  	return hex.EncodeToString(h.Sum(nil))
   388  }
   389  
   390  // Domain returns the effective domain (TLD plus one).
   391  func Domain(s string) string {
   392  	d, err := publicsuffix.EffectiveTLDPlusOne(s)
   393  	if err != nil {
   394  		panic(errors.Wrap(err, "effective domain"))
   395  	}
   396  
   397  	return d
   398  }
   399  
   400  // CertDomainNames returns the certificate domain name
   401  // and alternative names for a requested domain.
   402  func CertDomainNames(s string) []string {
   403  	// effective domain
   404  	if Domain(s) == s {
   405  		return []string{s, "*." + s}
   406  	}
   407  
   408  	// subdomain
   409  	return []string{RemoveSubdomains(s, 1), "*." + RemoveSubdomains(s, 1)}
   410  }
   411  
   412  // IsWildcardDomain returns true if the domain is a wildcard.
   413  func IsWildcardDomain(s string) bool {
   414  	return strings.HasPrefix(s, "*.")
   415  }
   416  
   417  // WildcardMatches returns true if wildcard is a wildcard domain
   418  // and it satisfies the given domain.
   419  func WildcardMatches(wildcard, domain string) bool {
   420  	if !IsWildcardDomain(wildcard) {
   421  		return false
   422  	}
   423  
   424  	w := RemoveSubdomains(wildcard, 1)
   425  	d := RemoveSubdomains(domain, 1)
   426  	return w == d
   427  }
   428  
   429  // RemoveSubdomains returns the domain without the n left-most subdomain(s).
   430  func RemoveSubdomains(s string, n int) string {
   431  	domains := strings.Split(s, ".")
   432  	return strings.Join(domains[n:], ".")
   433  }
   434  
   435  // ParseSections returns INI style sections from r.
   436  func ParseSections(r io.Reader) (sections []string, err error) {
   437  	s := bufio.NewScanner(r)
   438  
   439  	for s.Scan() {
   440  		t := s.Text()
   441  		if strings.HasPrefix(t, "[") {
   442  			sections = append(sections, strings.Trim(t, "[]"))
   443  		}
   444  	}
   445  
   446  	err = s.Err()
   447  	return
   448  }
   449  
   450  // StringMapKeys returns keys for m.
   451  func StringMapKeys(m map[string]string) (keys []string) {
   452  	for k := range m {
   453  		keys = append(keys, k)
   454  	}
   455  	sort.Strings(keys)
   456  	return
   457  }
   458  
   459  // UniqueStrings returns a string slice of unique values.
   460  func UniqueStrings(s []string) (v []string) {
   461  	m := make(map[string]struct{})
   462  	for _, val := range s {
   463  		_, ok := m[val]
   464  		if !ok {
   465  			v = append(v, val)
   466  			m[val] = struct{}{}
   467  		}
   468  	}
   469  	return
   470  }
   471  
   472  // IsCI returns true if the env looks like it's a CI platform.
   473  func IsCI() bool {
   474  	return os.Getenv("CI") == "true"
   475  }
   476  
   477  // EnvironMap returns environment as a map.
   478  func EnvironMap() map[string]string {
   479  	m, _ := ParseEnviron(os.Environ())
   480  	return m
   481  }
   482  
   483  // ParseEnviron returns environment as a map from the given env slice.
   484  func ParseEnviron(env []string) (map[string]string, error) {
   485  	m := make(map[string]string)
   486  
   487  	for i := 0; i < len(env); i++ {
   488  		s := env[i]
   489  
   490  		if strings.ContainsRune(s, '=') {
   491  			p := strings.SplitN(s, "=", 2)
   492  			m[p[0]] = p[1]
   493  			continue
   494  		}
   495  
   496  		if i == len(env)-1 {
   497  			return nil, errors.Errorf("%q is missing a value", s)
   498  		}
   499  
   500  		m[s] = env[i+1]
   501  		i++
   502  	}
   503  
   504  	return m, nil
   505  }
   506  
   507  // EncodeAlias encodes an alias string so that it conforms to the
   508  // requirement of matching (?!^[0-9]+$)([a-zA-Z0-9-_]+).
   509  func EncodeAlias(s string) string {
   510  	return "commit-" + strings.Replace(s, ".", "_", -1)
   511  }
   512  
   513  // DecodeAlias decodes an alias string which was encoded by
   514  // the EncodeAlias function.
   515  func DecodeAlias(s string) string {
   516  	s = strings.Replace(s, "_", ".", -1)
   517  	s = strings.Replace(s, "commit-", "", 1)
   518  	return s
   519  }
   520  
   521  // DateSuffix returns the date suffix for t.
   522  func DateSuffix(t time.Time) string {
   523  	switch t.Day() {
   524  	case 1, 21, 31:
   525  		return "st"
   526  	case 2, 22:
   527  		return "nd"
   528  	case 3, 23:
   529  		return "rd"
   530  	default:
   531  		return "th"
   532  	}
   533  }
   534  
   535  // StripLerna strips the owner portion of a Lerna-based tag. See #670 for
   536  // details. They are in the form of "@owner/repo@0.5.0".
   537  func StripLerna(s string) string {
   538  	if strings.HasPrefix(s, "@") {
   539  		p := strings.Split(s, "@")
   540  		return p[len(p)-1]
   541  	}
   542  
   543  	return s
   544  }
   545  
   546  // FixMultipleSetCookie staggers the casing of each set-cookie
   547  // value to trick API Gateway into setting multiple in the response.
   548  func FixMultipleSetCookie(h http.Header) {
   549  	cookies := h["Set-Cookie"]
   550  
   551  	if len(cookies) == 0 {
   552  		return
   553  	}
   554  
   555  	h.Del("Set-Cookie")
   556  
   557  	for i, v := range cookies {
   558  		h[BinaryCase("set-cookie", i)] = []string{v}
   559  	}
   560  }
   561  
   562  // BinaryCase ported from https://github.com/Gi60s/binary-case/blob/master/index.js#L86.
   563  func BinaryCase(s string, n int) string {
   564  	var res []rune
   565  
   566  	for _, c := range s {
   567  		if c >= 65 && c <= 90 {
   568  			if n&1 > 0 {
   569  				c += 32
   570  			}
   571  			res = append(res, c)
   572  			n >>= 1
   573  		} else if c >= 97 && c <= 122 {
   574  			if n&1 > 0 {
   575  				c -= 32
   576  			}
   577  			res = append(res, c)
   578  			n >>= 1
   579  		} else {
   580  			res = append(res, c)
   581  		}
   582  	}
   583  
   584  	return string(res)
   585  }
   586  
   587  // RelativeDate returns a date formatted relative to now.
   588  func RelativeDate(t time.Time) string {
   589  	switch d := time.Since(t); {
   590  	case d <= time.Hour:
   591  		return humanize.RelTime(time.Now(), t, "from now", "ago")
   592  	default:
   593  		return t.Format(`Jan 2` + DateSuffix(t) + ` 03:04:05pm`)
   594  	}
   595  }
   596  
   597  var numericRe = regexp.MustCompile(`^[0-9]+$`)
   598  
   599  // IsNumeric returns true if s is numeric.
   600  func IsNumeric(s string) bool {
   601  	return numericRe.MatchString(s)
   602  }