util.go 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. package util
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "github.com/gabriel-vasile/mimetype"
  8. "golang.org/x/term"
  9. "io"
  10. "math/rand"
  11. "os"
  12. "regexp"
  13. "strconv"
  14. "strings"
  15. "sync"
  16. "time"
  17. )
  18. const (
  19. randomStringCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  20. )
  21. var (
  22. random = rand.New(rand.NewSource(time.Now().UnixNano()))
  23. randomMutex = sync.Mutex{}
  24. sizeStrRegex = regexp.MustCompile(`(?i)^(\d+)([gmkb])?$`)
  25. errInvalidPriority = errors.New("invalid priority")
  26. )
  27. // FileExists checks if a file exists, and returns true if it does
  28. func FileExists(filename string) bool {
  29. stat, _ := os.Stat(filename)
  30. return stat != nil
  31. }
  32. // InStringList returns true if needle is contained in haystack
  33. func InStringList(haystack []string, needle string) bool {
  34. for _, s := range haystack {
  35. if s == needle {
  36. return true
  37. }
  38. }
  39. return false
  40. }
  41. // InStringListAll returns true if all needles are contained in haystack
  42. func InStringListAll(haystack []string, needles []string) bool {
  43. matches := 0
  44. for _, s := range haystack {
  45. for _, needle := range needles {
  46. if s == needle {
  47. matches++
  48. }
  49. }
  50. }
  51. return matches == len(needles)
  52. }
  53. // InIntList returns true if needle is contained in haystack
  54. func InIntList(haystack []int, needle int) bool {
  55. for _, s := range haystack {
  56. if s == needle {
  57. return true
  58. }
  59. }
  60. return false
  61. }
  62. // SplitNoEmpty splits a string using strings.Split, but filters out empty strings
  63. func SplitNoEmpty(s string, sep string) []string {
  64. res := make([]string, 0)
  65. for _, r := range strings.Split(s, sep) {
  66. if r != "" {
  67. res = append(res, r)
  68. }
  69. }
  70. return res
  71. }
  72. // SplitKV splits a string into a key/value pair using a separator, and trimming space. If the separator
  73. // is not found, key is empty.
  74. func SplitKV(s string, sep string) (key string, value string) {
  75. kv := strings.SplitN(strings.TrimSpace(s), sep, 2)
  76. if len(kv) == 2 {
  77. return strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])
  78. }
  79. return "", strings.TrimSpace(kv[0])
  80. }
  81. // LastString returns the last string in a slice, or def if s is empty
  82. func LastString(s []string, def string) string {
  83. if len(s) == 0 {
  84. return def
  85. }
  86. return s[len(s)-1]
  87. }
  88. // RandomString returns a random string with a given length
  89. func RandomString(length int) string {
  90. randomMutex.Lock() // Who would have thought that random.Intn() is not thread-safe?!
  91. defer randomMutex.Unlock()
  92. b := make([]byte, length)
  93. for i := range b {
  94. b[i] = randomStringCharset[random.Intn(len(randomStringCharset))]
  95. }
  96. return string(b)
  97. }
  98. // ValidRandomString returns true if the given string matches the format created by RandomString
  99. func ValidRandomString(s string, length int) bool {
  100. if len(s) != length {
  101. return false
  102. }
  103. for _, c := range strings.Split(s, "") {
  104. if !strings.Contains(randomStringCharset, c) {
  105. return false
  106. }
  107. }
  108. return true
  109. }
  110. // DurationToHuman converts a duration to a human-readable format
  111. func DurationToHuman(d time.Duration) (str string) {
  112. if d == 0 {
  113. return "0"
  114. }
  115. d = d.Round(time.Second)
  116. days := d / time.Hour / 24
  117. if days > 0 {
  118. str += fmt.Sprintf("%dd", days)
  119. }
  120. d -= days * time.Hour * 24
  121. hours := d / time.Hour
  122. if hours > 0 {
  123. str += fmt.Sprintf("%dh", hours)
  124. }
  125. d -= hours * time.Hour
  126. minutes := d / time.Minute
  127. if minutes > 0 {
  128. str += fmt.Sprintf("%dm", minutes)
  129. }
  130. d -= minutes * time.Minute
  131. seconds := d / time.Second
  132. if seconds > 0 {
  133. str += fmt.Sprintf("%ds", seconds)
  134. }
  135. return
  136. }
  137. // ParsePriority parses a priority string into its equivalent integer value
  138. func ParsePriority(priority string) (int, error) {
  139. switch strings.TrimSpace(strings.ToLower(priority)) {
  140. case "":
  141. return 0, nil
  142. case "1", "min":
  143. return 1, nil
  144. case "2", "low":
  145. return 2, nil
  146. case "3", "default":
  147. return 3, nil
  148. case "4", "high":
  149. return 4, nil
  150. case "5", "max", "urgent":
  151. return 5, nil
  152. default:
  153. return 0, errInvalidPriority
  154. }
  155. }
  156. // PriorityString converts a priority number to a string
  157. func PriorityString(priority int) (string, error) {
  158. switch priority {
  159. case 0:
  160. return "default", nil
  161. case 1:
  162. return "min", nil
  163. case 2:
  164. return "low", nil
  165. case 3:
  166. return "default", nil
  167. case 4:
  168. return "high", nil
  169. case 5:
  170. return "max", nil
  171. default:
  172. return "", errInvalidPriority
  173. }
  174. }
  175. // ShortTopicURL shortens the topic URL to be human-friendly, removing the http:// or https://
  176. func ShortTopicURL(s string) string {
  177. return strings.TrimPrefix(strings.TrimPrefix(s, "https://"), "http://")
  178. }
  179. // DetectContentType probes the byte array b and returns mime type and file extension.
  180. // The filename is only used to override certain special cases.
  181. func DetectContentType(b []byte, filename string) (mimeType string, ext string) {
  182. if strings.HasSuffix(strings.ToLower(filename), ".apk") {
  183. return "application/vnd.android.package-archive", ".apk"
  184. }
  185. m := mimetype.Detect(b)
  186. mimeType, ext = m.String(), m.Extension()
  187. if ext == "" {
  188. ext = ".bin"
  189. }
  190. return
  191. }
  192. // ParseSize parses a size string like 2K or 2M into bytes. If no unit is found, e.g. 123, bytes is assumed.
  193. func ParseSize(s string) (int64, error) {
  194. matches := sizeStrRegex.FindStringSubmatch(s)
  195. if matches == nil {
  196. return -1, fmt.Errorf("invalid size %s", s)
  197. }
  198. value, err := strconv.Atoi(matches[1])
  199. if err != nil {
  200. return -1, fmt.Errorf("cannot convert number %s", matches[1])
  201. }
  202. switch strings.ToUpper(matches[2]) {
  203. case "G":
  204. return int64(value) * 1024 * 1024 * 1024, nil
  205. case "M":
  206. return int64(value) * 1024 * 1024, nil
  207. case "K":
  208. return int64(value) * 1024, nil
  209. default:
  210. return int64(value), nil
  211. }
  212. }
  213. // ReadPassword will read a password from STDIN. If the terminal supports it, it will not print the
  214. // input characters to the screen. If not, it'll just read using normal readline semantics (useful for testing).
  215. func ReadPassword(in io.Reader) ([]byte, error) {
  216. // If in is a file and a character device (a TTY), use term.ReadPassword
  217. if f, ok := in.(*os.File); ok {
  218. stat, err := f.Stat()
  219. if err != nil {
  220. return nil, err
  221. }
  222. if (stat.Mode() & os.ModeCharDevice) == os.ModeCharDevice {
  223. password, err := term.ReadPassword(int(f.Fd())) // This is always going to be 0
  224. if err != nil {
  225. return nil, err
  226. }
  227. return password, nil
  228. }
  229. }
  230. // Fallback: Manually read util \n if found, see #69 for details why this is so manual
  231. password := make([]byte, 0)
  232. buf := make([]byte, 1)
  233. for {
  234. _, err := in.Read(buf)
  235. if err == io.EOF || buf[0] == '\n' {
  236. break
  237. } else if err != nil {
  238. return nil, err
  239. } else if len(password) > 10240 {
  240. return nil, errors.New("passwords this long are not supported")
  241. }
  242. password = append(password, buf[0])
  243. }
  244. return password, nil
  245. }
  246. // BasicAuth encodes the Authorization header value for basic auth
  247. func BasicAuth(user, pass string) string {
  248. return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", user, pass))))
  249. }
  250. // MaybeMarshalJSON returns a JSON string of the given object, or "<cannot serialize>" if serialization failed.
  251. // This is useful for logging purposes where a failure doesn't matter that much.
  252. func MaybeMarshalJSON(v interface{}) string {
  253. jsonBytes, err := json.MarshalIndent(v, "", " ")
  254. if err != nil {
  255. return "<cannot serialize>"
  256. }
  257. if len(jsonBytes) > 5000 {
  258. return string(jsonBytes)[:5000]
  259. }
  260. return string(jsonBytes)
  261. }