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 }