internal/util/util.go

Summary

Maintainability
A
2 hrs
Test Coverage
// Package util haters gonna hate.
package util

import (
    "bufio"
    "crypto/md5"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "math"
    "net"
    "net/http"
    "net/url"
    "os"
    "os/exec"
    "strings"
    "syscall"
    "time"

    "github.com/apex/up/internal/colors"
    "github.com/pascaldekloe/name"
    "github.com/pkg/errors"
    "github.com/tj/backoff"
    "github.com/tj/go-progress"
    "github.com/tj/go/term"
    "golang.org/x/net/publicsuffix"
)

// ClearHeader removes all content header fields.
func ClearHeader(h http.Header) {
    h.Del("Content-Type")
    h.Del("Content-Length")
    h.Del("Content-Encoding")
    h.Del("Content-Range")
    h.Del("Content-MD5")
    h.Del("Cache-Control")
    h.Del("ETag")
    h.Del("Last-Modified")
}

// ManagedByUp appends "Managed by Up".
func ManagedByUp(s string) string {
    if s == "" {
        return "Managed by Up."
    }

    return s + " (Managed by Up)."
}

// Exists returns true if the file exists.
func Exists(path string) bool {
    _, err := os.Stat(path)
    return err == nil
}

// ReadFileJSON reads json from the given path.
func ReadFileJSON(path string, v interface{}) error {
    b, err := ioutil.ReadFile(path)
    if err != nil {
        return errors.Wrap(err, "reading")
    }

    if err := json.Unmarshal(b, &v); err != nil {
        return errors.Wrap(err, "unmarshaling")
    }

    return nil
}

// Camelcase string with optional args.
func Camelcase(s string, v ...interface{}) string {
    return name.CamelCase(fmt.Sprintf(s, v...), true)
}

// NewProgressInt with the given total.
func NewProgressInt(total int) *progress.Bar {
    b := progress.NewInt(total)
    b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
    b.Width = 35
    b.StartDelimiter = colors.Gray("|")
    b.EndDelimiter = colors.Gray("|")
    b.Filled = colors.Purple("█")
    b.Empty = colors.Gray("░")
    return b
}

// NewInlineProgressInt with the given total.
func NewInlineProgressInt(total int) *progress.Bar {
    b := progress.NewInt(total)
    b.Template(`{{.Bar}} {{.Percent | printf "%0.0f"}}% {{.Text}}`)
    b.Width = 20
    b.StartDelimiter = colors.Gray("|")
    b.EndDelimiter = colors.Gray("|")
    b.Filled = colors.Purple("█")
    b.Empty = colors.Gray(" ")
    return b
}

// Pad helper.
func Pad() func() {
    println()
    return func() {
        println()
    }
}

// Fatal error.
func Fatal(err error) {
    fmt.Fprintf(os.Stderr, "\n     %s %s\n\n", colors.Red("Error:"), err)
    os.Exit(1)
}

// IsJSON returns true if the string looks like json.
func IsJSON(s string) bool {
    return len(s) > 1 && s[0] == '{' && s[len(s)-1] == '}'
}

// IsJSONLog returns true if the string looks likes a json log.
func IsJSONLog(s string) bool {
    return IsJSON(s) && strings.Contains(s, `"level"`)
}

// IsNotFound returns true if err is not nil and represents a missing resource.
func IsNotFound(err error) bool {
    switch {
    case err == nil:
        return false
    case strings.Contains(err.Error(), "ResourceNotFoundException"):
        return true
    case strings.Contains(err.Error(), "NoSuchEntity"):
        return true
    case strings.Contains(err.Error(), "does not exist"):
        return true
    case strings.Contains(err.Error(), "not found"):
        return true
    default:
        return false
    }
}

// IsBucketExists returns true if err is not nil and represents an existing bucket.
func IsBucketExists(err error) bool {
    switch {
    case err == nil:
        return false
    case strings.Contains(err.Error(), "BucketAlreadyOwnedByYou"):
        return true
    default:
        return false
    }
}

// IsThrottled returns true if err is not nil and represents a throttled request.
func IsThrottled(err error) bool {
    switch {
    case err == nil:
        return false
    case strings.Contains(err.Error(), "Throttling: Rate exceeded"):
        return true
    default:
        return false
    }
}

// IsNoCredentials returns true if err is not nil and represents missing credentials.
func IsNoCredentials(err error) bool {
    switch {
    case err == nil:
        return false
    case strings.Contains(err.Error(), "NoCredentialProviders"):
        return true
    default:
        return false
    }
}

// Env returns a slice from environment variable map.
func Env(m map[string]string) (env []string) {
    for k, v := range m {
        env = append(env, fmt.Sprintf("%s=%s", k, v))
    }
    return
}

// PrefixLines prefixes the lines in s with prefix.
func PrefixLines(s string, prefix string) string {
    lines := strings.Split(s, "\n")
    for i, l := range lines {
        lines[i] = prefix + l
    }
    return strings.Join(lines, "\n")
}

// Indent the given string.
func Indent(s string) string {
    return PrefixLines(s, "  ")
}

// WaitForListen blocks until `u` is listening with timeout.
func WaitForListen(u *url.URL, timeout time.Duration) error {
    timedout := time.After(timeout)

    b := backoff.Backoff{
        Min:    100 * time.Millisecond,
        Max:    time.Second,
        Factor: 1.5,
    }

    for {
        select {
        case <-timedout:
            return errors.Errorf("timed out after %s", timeout)
        case <-time.After(b.Duration()):
            if IsListening(u) {
                return nil
            }
        }
    }
}

// IsListening returns true if there's a server listening on `u`.
func IsListening(u *url.URL) bool {
    conn, err := net.Dial("tcp", u.Host)
    if err != nil {
        return false
    }

    conn.Close()
    return true
}

// ExitStatus returns the exit status of cmd.
func ExitStatus(cmd *exec.Cmd, err error) string {
    ps := cmd.ProcessState

    if e, ok := err.(*exec.ExitError); ok {
        ps = e.ProcessState
    }

    if ps != nil {
        s, ok := ps.Sys().(syscall.WaitStatus)
        if ok {
            return fmt.Sprintf("%d", s.ExitStatus())
        }
    }

    return "?"
}

// StringsContains returns true if list contains s.
func StringsContains(list []string, s string) bool {
    for _, v := range list {
        if v == s {
            return true
        }
    }
    return false
}

// BasePath returns a normalized base path,
// stripping the leading '/' if present.
func BasePath(s string) string {
    return strings.TrimLeft(s, "/")
}

// LogPad outputs a log message with padding.
func LogPad(msg string, v ...interface{}) {
    defer Pad()()
    Log(msg, v...)
}

// Log outputs a log message.
func Log(msg string, v ...interface{}) {
    fmt.Printf("     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
}

// LogClear clears the line and outputs a log message.
func LogClear(msg string, v ...interface{}) {
    term.MoveUp(1)
    term.ClearLine()
    fmt.Printf("\r     %s\n", colors.Purple(fmt.Sprintf(msg, v...)))
}

// LogTitle outputs a log title.
func LogTitle(msg string, v ...interface{}) {
    fmt.Printf("\n     \x1b[1m%s\x1b[m\n\n", fmt.Sprintf(msg, v...))
}

// LogName outputs a log message with name.
func LogName(name, msg string, v ...interface{}) {
    fmt.Printf("     %s %s\n", colors.Purple(name+":"), fmt.Sprintf(msg, v...))
}

// LogListItem outputs a list item.
func LogListItem(msg string, v ...interface{}) {
    fmt.Printf("      • %s\n", fmt.Sprintf(msg, v...))
}

// ToFloat returns a float or NaN.
func ToFloat(v interface{}) float64 {
    switch n := v.(type) {
    case int:
        return float64(n)
    case int8:
        return float64(n)
    case int16:
        return float64(n)
    case int32:
        return float64(n)
    case int64:
        return float64(n)
    case uint:
        return float64(n)
    case uint8:
        return float64(n)
    case uint16:
        return float64(n)
    case uint32:
        return float64(n)
    case uint64:
        return float64(n)
    case float32:
        return float64(n)
    case float64:
        return n
    default:
        return math.NaN()
    }
}

// Milliseconds returns the duration as milliseconds.
func Milliseconds(d time.Duration) int {
    return int(d / time.Millisecond)
}

// MillisecondsSince returns the duration as milliseconds relative to time t.
func MillisecondsSince(t time.Time) int {
    return int(time.Since(t) / time.Millisecond)
}

// ParseDuration string with day and month approximation support.
func ParseDuration(s string) (d time.Duration, err error) {
    r := strings.NewReader(s)

    switch {
    case strings.HasSuffix(s, "d"):
        var v float64
        _, err = fmt.Fscanf(r, "%fd", &v)
        d = time.Duration(v * float64(24*time.Hour))
    case strings.HasSuffix(s, "w"):
        var v float64
        _, err = fmt.Fscanf(r, "%fw", &v)
        d = time.Duration(v * float64(24*time.Hour*7))
    case strings.HasSuffix(s, "mo"):
        var v float64
        _, err = fmt.Fscanf(r, "%fmo", &v)
        d = time.Duration(v * float64(30*24*time.Hour))
    case strings.HasSuffix(s, "M"):
        var v float64
        _, err = fmt.Fscanf(r, "%fM", &v)
        d = time.Duration(v * float64(30*24*time.Hour))
    default:
        d, err = time.ParseDuration(s)
    }

    return
}

// Md5 returns an md5 hash for s.
func Md5(s string) string {
    h := md5.New()
    h.Write([]byte(s))
    return hex.EncodeToString(h.Sum(nil))
}

// Domain returns the effective domain (TLD plus one).
func Domain(s string) string {
    d, err := publicsuffix.EffectiveTLDPlusOne(s)
    if err != nil {
        panic(errors.Wrap(err, "effective domain"))
    }

    return d
}

// CertDomainNames returns the certificate domain name
// and alternative names for a requested domain.
func CertDomainNames(s string) []string {
    // effective domain
    if Domain(s) == s {
        return []string{s, "*." + s}
    }

    // subdomain
    return []string{RemoveSubdomains(s, 1), "*." + RemoveSubdomains(s, 1)}
}

// IsWildcardDomain returns true if the domain is a wildcard.
func IsWildcardDomain(s string) bool {
    return strings.HasPrefix(s, "*.")
}

// WildcardMatches returns true if wildcard is a wildcard domain
// and it satisfies the given domain.
func WildcardMatches(wildcard, domain string) bool {
    if !IsWildcardDomain(wildcard) {
        return false
    }

    w := RemoveSubdomains(wildcard, 1)
    d := RemoveSubdomains(domain, 1)
    return w == d
}

// RemoveSubdomains returns the domain without the n left-most subdomain(s).
func RemoveSubdomains(s string, n int) string {
    domains := strings.Split(s, ".")
    return strings.Join(domains[n:], ".")
}

// ParseSections returns INI style sections from r.
func ParseSections(r io.Reader) (sections []string, err error) {
    s := bufio.NewScanner(r)

    for s.Scan() {
        t := s.Text()
        if strings.HasPrefix(t, "[") {
            sections = append(sections, strings.Trim(t, "[]"))
        }
    }

    err = s.Err()

    return
}

// UniqueStrings returns a string slice of unique values.
func UniqueStrings(s []string) (v []string) {
    m := make(map[string]struct{})
    for _, val := range s {
        _, ok := m[val]
        if !ok {
            v = append(v, val)
            m[val] = struct{}{}
        }
    }
    return
}

// IsCI returns true if the env looks like it's a CI platform.
func IsCI() bool {
    return os.Getenv("CI") == "true"
}

// EncodeAlias encodes an alias string so that it conforms to the
// requirement of matching (?!^[0-9]+$)([a-zA-Z0-9-_]+).
func EncodeAlias(s string) string {
    return "commit-" + strings.Replace(s, ".", "_", -1)
}

// DecodeAlias decodes an alias string which was encoded by
// the EncodeAlias function.
func DecodeAlias(s string) string {
    s = strings.Replace(s, "_", ".", -1)
    s = strings.Replace(s, "commit-", "", 1)
    return s
}

// DateSuffix returns the date suffix for t.
func DateSuffix(t time.Time) string {
    switch t.Day() {
    case 1, 21, 31:
        return "st"
    case 2, 22:
        return "nd"
    case 3, 23:
        return "rd"
    default:
        return "th"
    }
}

// StripLerna strips the owner portion of a Lerna-based tag. See #670 for
// details. They are in the form of "@owner/repo@0.5.0".
func StripLerna(s string) string {
    if strings.HasPrefix(s, "@") {
        p := strings.Split(s, "@")
        return p[len(p)-1]
    }

    return s
}

// FixMultipleSetCookie staggers the casing of each set-cookie
// value to trick API Gateway into setting multiple in the response.
func FixMultipleSetCookie(h http.Header) {
    cookies := h["Set-Cookie"]

    if len(cookies) == 0 {
        return
    }

    h.Del("Set-Cookie")

    for i, v := range cookies {
        h[BinaryCase("set-cookie", i)] = []string{v}
    }
}

// BinaryCase ported from https://github.com/Gi60s/binary-case/blob/master/index.js#L86.
func BinaryCase(s string, n int) string {
    var res []rune

    for _, c := range s {
        if c >= 65 && c <= 90 {
            if n&1 > 0 {
                c += 32
            }
            res = append(res, c)
            n >>= 1
        } else if c >= 97 && c <= 122 {
            if n&1 > 0 {
                c -= 32
            }
            res = append(res, c)
            n >>= 1
        } else {
            res = append(res, c)
        }
    }

    return string(res)
}