github.com/orangenpresse/up@v0.6.0/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  	"strings"
    19  	"syscall"
    20  	"time"
    21  
    22  	"github.com/apex/up/internal/colors"
    23  	"github.com/pascaldekloe/name"
    24  	"github.com/pkg/errors"
    25  	"github.com/tj/backoff"
    26  	"github.com/tj/go-progress"
    27  	"github.com/tj/go/term"
    28  	"golang.org/x/net/publicsuffix"
    29  )
    30  
    31  // Fields retained when clearing.
    32  var keepFields = map[string]bool{
    33  	"X-Powered-By": true,
    34  }
    35  
    36  // ClearHeader removes all header fields.
    37  func ClearHeader(h http.Header) {
    38  	for k := range h {
    39  		if keepFields[k] {
    40  			continue
    41  		}
    42  
    43  		h.Del(k)
    44  	}
    45  }
    46  
    47  // ManagedByUp appends "Managed by Up".
    48  func ManagedByUp(s string) string {
    49  	if s == "" {
    50  		return "Managed by Up."
    51  	}
    52  
    53  	return s + " (Managed by Up)."
    54  }
    55  
    56  // Exists returns true if the file exists.
    57  func Exists(path string) bool {
    58  	_, err := os.Stat(path)
    59  	return err == nil
    60  }
    61  
    62  // ReadFileJSON reads json from the given path.
    63  func ReadFileJSON(path string, v interface{}) error {
    64  	b, err := ioutil.ReadFile(path)
    65  	if err != nil {
    66  		return errors.Wrap(err, "reading")
    67  	}
    68  
    69  	if err := json.Unmarshal(b, &v); err != nil {
    70  		return errors.Wrap(err, "unmarshaling")
    71  	}
    72  
    73  	return nil
    74  }
    75  
    76  // Camelcase string with optional args.
    77  func Camelcase(s string, v ...interface{}) string {
    78  	return name.CamelCase(fmt.Sprintf(s, v...), true)
    79  }
    80  
    81  // NewProgressInt with the given total.
    82  func NewProgressInt(total int) *progress.Bar {
    83  	b := progress.NewInt(total)
    84  	b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
    85  	b.Width = 35
    86  	b.StartDelimiter = colors.Gray("|")
    87  	b.EndDelimiter = colors.Gray("|")
    88  	b.Filled = colors.Purple("█")
    89  	b.Empty = colors.Gray("░")
    90  	return b
    91  }
    92  
    93  // NewInlineProgressInt with the given total.
    94  func NewInlineProgressInt(total int) *progress.Bar {
    95  	b := progress.NewInt(total)
    96  	b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
    97  	b.Width = 20
    98  	b.StartDelimiter = colors.Gray("|")
    99  	b.EndDelimiter = colors.Gray("|")
   100  	b.Filled = colors.Purple("█")
   101  	b.Empty = colors.Gray(" ")
   102  	return b
   103  }
   104  
   105  // Pad helper.
   106  func Pad() func() {
   107  	println()
   108  	return func() {
   109  		println()
   110  	}
   111  }
   112  
   113  // Fatal error.
   114  func Fatal(err error) {
   115  	fmt.Fprintf(os.Stderr, "\n     %s %s\n\n", colors.Red("Error:"), err)
   116  	os.Exit(1)
   117  }
   118  
   119  // IsJSON returns true if the string looks like json.
   120  func IsJSON(s string) bool {
   121  	return len(s) > 1 && s[0] == '{' && s[len(s)-1] == '}'
   122  }
   123  
   124  // IsJSONLog returns true if the string looks likes a json log.
   125  func IsJSONLog(s string) bool {
   126  	return IsJSON(s) && strings.Contains(s, `"level"`)
   127  }
   128  
   129  // IsNotFound returns true if err is not nil and represents a missing resource.
   130  func IsNotFound(err error) bool {
   131  	switch {
   132  	case err == nil:
   133  		return false
   134  	case strings.Contains(err.Error(), "ResourceNotFoundException"):
   135  		return true
   136  	case strings.Contains(err.Error(), "does not exist"):
   137  		return true
   138  	case strings.Contains(err.Error(), "not found"):
   139  		return true
   140  	default:
   141  		return false
   142  	}
   143  }
   144  
   145  // IsBucketExists returns true if err is not nil and represents an existing bucket.
   146  func IsBucketExists(err error) bool {
   147  	switch {
   148  	case err == nil:
   149  		return false
   150  	case strings.Contains(err.Error(), "BucketAlreadyOwnedByYou"):
   151  		return true
   152  	default:
   153  		return false
   154  	}
   155  }
   156  
   157  // IsThrottled returns true if err is not nil and represents a throttled request.
   158  func IsThrottled(err error) bool {
   159  	switch {
   160  	case err == nil:
   161  		return false
   162  	case strings.Contains(err.Error(), "Throttling: Rate exceeded"):
   163  		return true
   164  	default:
   165  		return false
   166  	}
   167  }
   168  
   169  // IsNoCredentials returns true if err is not nil and represents missing credentials.
   170  func IsNoCredentials(err error) bool {
   171  	switch {
   172  	case err == nil:
   173  		return false
   174  	case strings.Contains(err.Error(), "NoCredentialProviders"):
   175  		return true
   176  	default:
   177  		return false
   178  	}
   179  }
   180  
   181  // Env returns a slice from environment variable map.
   182  func Env(m map[string]string) (env []string) {
   183  	for k, v := range m {
   184  		env = append(env, fmt.Sprintf("%s=%s", k, v))
   185  	}
   186  	return
   187  }
   188  
   189  // PrefixLines prefixes the lines in s with prefix.
   190  func PrefixLines(s string, prefix string) string {
   191  	lines := strings.Split(s, "\n")
   192  	for i, l := range lines {
   193  		lines[i] = prefix + l
   194  	}
   195  	return strings.Join(lines, "\n")
   196  }
   197  
   198  // Indent the given string.
   199  func Indent(s string) string {
   200  	return PrefixLines(s, "  ")
   201  }
   202  
   203  // WaitForListen blocks until `u` is listening with timeout.
   204  func WaitForListen(u *url.URL, timeout time.Duration) error {
   205  	timedout := time.After(timeout)
   206  
   207  	b := backoff.Backoff{
   208  		Min:    100 * time.Millisecond,
   209  		Max:    time.Second,
   210  		Factor: 1.5,
   211  	}
   212  
   213  	for {
   214  		select {
   215  		case <-timedout:
   216  			return errors.Errorf("timed out after %s", timeout)
   217  		case <-time.After(b.Duration()):
   218  			if IsListening(u) {
   219  				return nil
   220  			}
   221  		}
   222  	}
   223  }
   224  
   225  // IsListening returns true if there's a server listening on `u`.
   226  func IsListening(u *url.URL) bool {
   227  	conn, err := net.Dial("tcp", u.Host)
   228  	if err != nil {
   229  		return false
   230  	}
   231  
   232  	conn.Close()
   233  	return true
   234  }
   235  
   236  // ExitStatus returns the exit status of cmd.
   237  func ExitStatus(cmd *exec.Cmd, err error) string {
   238  	ps := cmd.ProcessState
   239  
   240  	if e, ok := err.(*exec.ExitError); ok {
   241  		ps = e.ProcessState
   242  	}
   243  
   244  	if ps != nil {
   245  		s, ok := ps.Sys().(syscall.WaitStatus)
   246  		if ok {
   247  			return fmt.Sprintf("%d", s.ExitStatus())
   248  		}
   249  	}
   250  
   251  	return "?"
   252  }
   253  
   254  // StringsContains returns true if list contains s.
   255  func StringsContains(list []string, s string) bool {
   256  	for _, v := range list {
   257  		if v == s {
   258  			return true
   259  		}
   260  	}
   261  	return false
   262  }
   263  
   264  // BasePath returns a normalized base path,
   265  // stripping the leading '/' if present.
   266  func BasePath(s string) string {
   267  	return strings.TrimLeft(s, "/")
   268  }
   269  
   270  // LogPad outputs a log message with padding.
   271  func LogPad(msg string, v ...interface{}) {
   272  	defer Pad()()
   273  	Log(msg, v...)
   274  }
   275  
   276  // Log outputs a log message.
   277  func Log(msg string, v ...interface{}) {
   278  	fmt.Printf("     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
   279  }
   280  
   281  // LogClear clears the line and outputs a log message.
   282  func LogClear(msg string, v ...interface{}) {
   283  	term.MoveUp(1)
   284  	term.ClearLine()
   285  	fmt.Printf("\r     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
   286  }
   287  
   288  // LogTitle outputs a log title.
   289  func LogTitle(msg string, v ...interface{}) {
   290  	fmt.Printf("\n     \x1b[1m%s\x1b[m\n\n", fmt.Sprintf(msg, v...))
   291  }
   292  
   293  // LogName outputs a log message with name.
   294  func LogName(name, msg string, v ...interface{}) {
   295  	fmt.Printf("     %s %s\n", colors.Purple(name+":"), fmt.Sprintf(msg, v...))
   296  }
   297  
   298  // LogListItem outputs a list item.
   299  func LogListItem(msg string, v ...interface{}) {
   300  	fmt.Printf("      • %s\n", fmt.Sprintf(msg, v...))
   301  }
   302  
   303  // ToFloat returns a float or NaN.
   304  func ToFloat(v interface{}) float64 {
   305  	switch n := v.(type) {
   306  	case int:
   307  		return float64(n)
   308  	case int8:
   309  		return float64(n)
   310  	case int16:
   311  		return float64(n)
   312  	case int32:
   313  		return float64(n)
   314  	case int64:
   315  		return float64(n)
   316  	case uint:
   317  		return float64(n)
   318  	case uint8:
   319  		return float64(n)
   320  	case uint16:
   321  		return float64(n)
   322  	case uint32:
   323  		return float64(n)
   324  	case uint64:
   325  		return float64(n)
   326  	case float32:
   327  		return float64(n)
   328  	case float64:
   329  		return n
   330  	default:
   331  		return math.NaN()
   332  	}
   333  }
   334  
   335  // Milliseconds returns the duration as milliseconds.
   336  func Milliseconds(d time.Duration) int {
   337  	return int(d / time.Millisecond)
   338  }
   339  
   340  // MillisecondsSince returns the duration as milliseconds relative to time t.
   341  func MillisecondsSince(t time.Time) int {
   342  	return int(time.Since(t) / time.Millisecond)
   343  }
   344  
   345  // ParseDuration string with day and month approximation support.
   346  func ParseDuration(s string) (d time.Duration, err error) {
   347  	r := strings.NewReader(s)
   348  
   349  	switch {
   350  	case strings.HasSuffix(s, "d"):
   351  		var v float64
   352  		_, err = fmt.Fscanf(r, "%fd", &v)
   353  		d = time.Duration(v * float64(24*time.Hour))
   354  	case strings.HasSuffix(s, "w"):
   355  		var v float64
   356  		_, err = fmt.Fscanf(r, "%fw", &v)
   357  		d = time.Duration(v * float64(24*time.Hour*7))
   358  	case strings.HasSuffix(s, "mo"):
   359  		var v float64
   360  		_, err = fmt.Fscanf(r, "%fmo", &v)
   361  		d = time.Duration(v * float64(30*24*time.Hour))
   362  	case strings.HasSuffix(s, "M"):
   363  		var v float64
   364  		_, err = fmt.Fscanf(r, "%fM", &v)
   365  		d = time.Duration(v * float64(30*24*time.Hour))
   366  	default:
   367  		d, err = time.ParseDuration(s)
   368  	}
   369  
   370  	return
   371  }
   372  
   373  // Md5 returns an md5 hash for s.
   374  func Md5(s string) string {
   375  	h := md5.New()
   376  	h.Write([]byte(s))
   377  	return hex.EncodeToString(h.Sum(nil))
   378  }
   379  
   380  // Domain returns the effective domain (TLD plus one).
   381  func Domain(s string) string {
   382  	d, err := publicsuffix.EffectiveTLDPlusOne(s)
   383  	if err != nil {
   384  		panic(errors.Wrap(err, "effective domain"))
   385  	}
   386  
   387  	return d
   388  }
   389  
   390  // CertDomainNames returns the certificate domain name
   391  // and alternative names for a requested domain.
   392  func CertDomainNames(s string) []string {
   393  	// effective domain
   394  	if Domain(s) == s {
   395  		return []string{s, "*." + s}
   396  	}
   397  
   398  	// subdomain
   399  	return []string{RemoveSubdomains(s, 1), "*." + RemoveSubdomains(s, 1)}
   400  }
   401  
   402  // IsWildcardDomain returns true if the domain is a wildcard.
   403  func IsWildcardDomain(s string) bool {
   404  	return strings.HasPrefix(s, "*.")
   405  }
   406  
   407  // WildcardMatches returns true if wildcard is a wildcard domain
   408  // and it satisfies the given domain.
   409  func WildcardMatches(wildcard, domain string) bool {
   410  	if !IsWildcardDomain(wildcard) {
   411  		return false
   412  	}
   413  
   414  	w := RemoveSubdomains(wildcard, 1)
   415  	d := RemoveSubdomains(domain, 1)
   416  	return w == d
   417  }
   418  
   419  // RemoveSubdomains returns the domain without the n left-most subdomain(s).
   420  func RemoveSubdomains(s string, n int) string {
   421  	domains := strings.Split(s, ".")
   422  	return strings.Join(domains[n:], ".")
   423  }
   424  
   425  // ParseSections returns INI style sections from r.
   426  func ParseSections(r io.Reader) (sections []string, err error) {
   427  	s := bufio.NewScanner(r)
   428  
   429  	for s.Scan() {
   430  		t := s.Text()
   431  		if strings.HasPrefix(t, "[") {
   432  			sections = append(sections, strings.Trim(t, "[]"))
   433  		}
   434  	}
   435  
   436  	err = s.Err()
   437  
   438  	return
   439  }
   440  
   441  // UniqueStrings returns a string slice of unique values.
   442  func UniqueStrings(s []string) (v []string) {
   443  	m := make(map[string]struct{})
   444  	for _, val := range s {
   445  		_, ok := m[val]
   446  		if !ok {
   447  			v = append(v, val)
   448  			m[val] = struct{}{}
   449  		}
   450  	}
   451  	return
   452  }
   453  
   454  // IsCI returns true if the env looks like it's a CI platform.
   455  func IsCI() bool {
   456  	return os.Getenv("CI") == "true"
   457  }
   458  
   459  // EncodeAlias encodes an alias string so that it conforms to the
   460  // requirement of matching (?!^[0-9]+$)([a-zA-Z0-9-_]+).
   461  func EncodeAlias(s string) string {
   462  	return "commit-" + strings.Replace(s, ".", "_", -1)
   463  }
   464  
   465  // DecodeAlias decodes an alias string which was encoded by
   466  // the EncodeAlias function.
   467  func DecodeAlias(s string) string {
   468  	s = strings.Replace(s, "_", ".", -1)
   469  	s = strings.Replace(s, "commit-", "", 1)
   470  	return s
   471  }
   472  
   473  // DateSuffix returns the date suffix for t.
   474  func DateSuffix(t time.Time) string {
   475  	switch t.Day() {
   476  	case 1, 21, 31:
   477  		return "st"
   478  	case 2, 22:
   479  		return "nd"
   480  	case 3, 23:
   481  		return "rd"
   482  	default:
   483  		return "th"
   484  	}
   485  }