util.go 5.8 KB

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