util.go 6.4 KB

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