123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- package utils
- import (
- "bytes"
- "encoding/base64"
- "errors"
- "fmt"
- "io"
- "math/rand"
- "net/url"
- "os"
- "os/exec"
- "path"
- "path/filepath"
- "regexp"
- "strings"
- "time"
- log "github.com/sirupsen/logrus"
- "github.com/yuin/goldmark"
- "github.com/yuin/goldmark/extension"
- "github.com/yuin/goldmark/renderer/html"
- "mvdan.cc/xurls"
- )
- // DoesFileExists checks if the file exists.
- func DoesFileExists(name string) bool {
- if _, err := os.Stat(name); err == nil {
- return true
- } else if os.IsNotExist(err) {
- return false
- } else {
- log.Errorln(err)
- return false
- }
- }
- // GetRelativePathFromAbsolutePath gets the relative path from the provided absolute path.
- func GetRelativePathFromAbsolutePath(path string) string {
- pathComponents := strings.Split(path, "/")
- variant := pathComponents[len(pathComponents)-2]
- file := pathComponents[len(pathComponents)-1]
- return filepath.Join(variant, file)
- }
- // GetIndexFromFilePath is a utility that will return the index/key/variant name in a full path.
- func GetIndexFromFilePath(path string) string {
- pathComponents := strings.Split(path, "/")
- variant := pathComponents[len(pathComponents)-2]
- return variant
- }
- // Copy copies the file to destination.
- func Copy(source, destination string) error {
- input, err := os.ReadFile(source) // nolint
- if err != nil {
- return err
- }
- return os.WriteFile(destination, input, 0o600)
- }
- // Move moves the file at source to destination.
- func Move(source, destination string) error {
- err := os.Rename(source, destination)
- if err != nil {
- log.Warnln("Moving with os.Rename failed, falling back to copy and delete!", err)
- return moveFallback(source, destination)
- }
- return nil
- }
- // moveFallback moves a file using a copy followed by a delete, which works across file systems.
- // source: https://gist.github.com/var23rav/23ae5d0d4d830aff886c3c970b8f6c6b
- func moveFallback(source, destination string) error {
- inputFile, err := os.Open(source) // nolint: gosec
- if err != nil {
- return fmt.Errorf("Couldn't open source file: %s", err)
- }
- outputFile, err := os.Create(destination) // nolint: gosec
- if err != nil {
- _ = inputFile.Close()
- return fmt.Errorf("Couldn't open dest file: %s", err)
- }
- defer outputFile.Close()
- _, err = io.Copy(outputFile, inputFile)
- _ = inputFile.Close()
- if err != nil {
- return fmt.Errorf("Writing to output file failed: %s", err)
- }
- // The copy was successful, so now delete the original file
- err = os.Remove(source)
- if err != nil {
- return fmt.Errorf("Failed removing original file: %s", err)
- }
- return nil
- }
- // IsUserAgentAPlayer returns if a web client user-agent is seen as a media player.
- func IsUserAgentAPlayer(userAgent string) bool {
- if userAgent == "" {
- return false
- }
- playerStrings := []string{
- "mpv",
- "player",
- "vlc",
- "applecoremedia",
- }
- for _, playerString := range playerStrings {
- if strings.Contains(strings.ToLower(userAgent), playerString) {
- return true
- }
- }
- return false
- }
- // RenderSimpleMarkdown will return HTML without sanitization or specific formatting rules.
- func RenderSimpleMarkdown(raw string) string {
- markdown := goldmark.New(
- goldmark.WithRendererOptions(
- html.WithUnsafe(),
- ),
- goldmark.WithExtensions(
- extension.NewLinkify(
- extension.WithLinkifyAllowedProtocols([][]byte{
- []byte("http:"),
- []byte("https:"),
- }),
- extension.WithLinkifyURLRegexp(
- xurls.Strict,
- ),
- ),
- ),
- )
- trimmed := strings.TrimSpace(raw)
- var buf bytes.Buffer
- if err := markdown.Convert([]byte(trimmed), &buf); err != nil {
- log.Fatalln(err)
- }
- return buf.String()
- }
- // RenderPageContentMarkdown will return HTML specifically handled for the user-specified page content.
- func RenderPageContentMarkdown(raw string) string {
- markdown := goldmark.New(
- goldmark.WithRendererOptions(
- html.WithUnsafe(),
- ),
- goldmark.WithExtensions(
- extension.GFM,
- extension.NewLinkify(
- extension.WithLinkifyAllowedProtocols([][]byte{
- []byte("http:"),
- []byte("https:"),
- }),
- extension.WithLinkifyURLRegexp(
- xurls.Strict,
- ),
- ),
- ),
- )
- trimmed := strings.TrimSpace(raw)
- var buf bytes.Buffer
- if err := markdown.Convert([]byte(trimmed), &buf); err != nil {
- log.Fatalln(err)
- }
- return strings.TrimSpace(buf.String())
- }
- // GetCacheDurationSecondsForPath will return the number of seconds to cache an item.
- func GetCacheDurationSecondsForPath(filePath string) int {
- filename := path.Base(filePath)
- fileExtension := path.Ext(filePath)
- defaultDaysCached := 30
- if filename == "thumbnail.jpg" || filename == "preview.gif" {
- // Thumbnails & preview gif re-generate during live
- return 20
- } else if fileExtension == ".js" || fileExtension == ".css" {
- // Cache javascript & CSS
- return 60 * 60 * 24 * defaultDaysCached
- } else if fileExtension == ".ts" || fileExtension == ".woff2" {
- // Cache video segments as long as you want. They can't change.
- // This matters most for local hosting of segments for recordings
- // and not for live or 3rd party storage.
- return 31557600
- } else if fileExtension == ".m3u8" {
- return 0
- } else if fileExtension == ".jpg" || fileExtension == ".png" || fileExtension == ".gif" || fileExtension == ".svg" {
- return 60 * 60 * 24 * defaultDaysCached
- } else if fileExtension == ".html" || filename == "/" || fileExtension == "" {
- return 0
- }
- // Default cache length in seconds
- return 60 * 60 * 24 * 1 // For unknown types, cache for 1 day
- }
- // IsValidURL will return if a URL string is a valid URL or not.
- func IsValidURL(urlToTest string) bool {
- if _, err := url.ParseRequestURI(urlToTest); err != nil {
- return false
- }
- u, err := url.Parse(urlToTest)
- if err != nil || u.Scheme == "" || u.Host == "" {
- return false
- }
- return true
- }
- // ValidatedFfmpegPath will take a proposed path to ffmpeg and return a validated path.
- func ValidatedFfmpegPath(ffmpegPath string) string {
- if ffmpegPath != "" {
- if err := VerifyFFMpegPath(ffmpegPath); err == nil {
- return ffmpegPath
- }
- log.Warnln(ffmpegPath, "is an invalid path to ffmpeg will try to use a copy in your path, if possible")
- }
- // First look to see if ffmpeg is in the current working directory
- localCopy := "./ffmpeg"
- hasLocalCopyError := VerifyFFMpegPath(localCopy)
- if hasLocalCopyError == nil {
- // No error, so all is good. Use the local copy.
- return localCopy
- }
- cmd := exec.Command("which", "ffmpeg")
- out, err := cmd.CombinedOutput()
- if err != nil {
- log.Fatalln("Unable to locate ffmpeg. Either install it globally on your system or put the ffmpeg binary in the same directory as Owncast. The binary must be named ffmpeg.")
- }
- path := strings.TrimSpace(string(out))
- return path
- }
- // VerifyFFMpegPath verifies that the path exists, is a file, and is executable.
- func VerifyFFMpegPath(path string) error {
- stat, err := os.Stat(path)
- if os.IsNotExist(err) {
- return errors.New("ffmpeg path does not exist")
- }
- if err != nil {
- return fmt.Errorf("error while verifying the ffmpeg path: %s", err.Error())
- }
- if stat.IsDir() {
- return errors.New("ffmpeg path can not be a folder")
- }
- mode := stat.Mode()
- // source: https://stackoverflow.com/a/60128480
- if mode&0o111 == 0 {
- return errors.New("ffmpeg path is not executable")
- }
- return nil
- }
- // CleanupDirectory removes the directory and makes it fresh again. Throws fatal error on failure.
- func CleanupDirectory(path string) {
- log.Traceln("Cleaning", path)
- if err := os.RemoveAll(path); err != nil {
- log.Fatalln("Unable to remove directory. Please check the ownership and permissions", err)
- }
- if err := os.MkdirAll(path, 0o750); err != nil {
- log.Fatalln("Unable to create directory. Please check the ownership and permissions", err)
- }
- }
- // FindInSlice will return if a string is in a slice, and the index of that string.
- func FindInSlice(slice []string, val string) (int, bool) {
- for i, item := range slice {
- if item == val {
- return i, true
- }
- }
- return -1, false
- }
- // StringSliceToMap is a convenience function to convert a slice of strings into
- // a map using the string as the key.
- func StringSliceToMap(stringSlice []string) map[string]interface{} {
- stringMap := map[string]interface{}{}
- for _, str := range stringSlice {
- stringMap[str] = true
- }
- return stringMap
- }
- // Float64MapToSlice is a convenience function to convert a map of floats into.
- func Float64MapToSlice(float64Map map[string]float64) []float64 {
- float64Slice := []float64{}
- for _, val := range float64Map {
- float64Slice = append(float64Slice, val)
- }
- return float64Slice
- }
- // StringMapKeys returns a slice of string keys from a map.
- func StringMapKeys(stringMap map[string]interface{}) []string {
- stringSlice := []string{}
- for k := range stringMap {
- stringSlice = append(stringSlice, k)
- }
- return stringSlice
- }
- // GenerateRandomDisplayColor will return a random number that is used for
- // referencing a color value client-side. These colors are seen as
- // --theme-user-colors-n.
- func GenerateRandomDisplayColor(maxColor int) int {
- rangeLower := 0
- rangeUpper := maxColor
- return rangeLower + rand.Intn(rangeUpper-rangeLower+1) //nolint:gosec
- }
- // GetHostnameFromURL will return the hostname component from a URL string.
- func GetHostnameFromURL(u url.URL) string {
- return u.Host
- }
- // GetHostnameFromURLString will return the hostname component from a URL object.
- func GetHostnameFromURLString(s string) string {
- u, err := url.Parse(s)
- if err != nil {
- return ""
- }
- return u.Host
- }
- // GetHashtagsFromText returns all the #Hashtags from a string.
- func GetHashtagsFromText(text string) []string {
- re := regexp.MustCompile(`#[a-zA-Z0-9_]+`)
- return re.FindAllString(text, -1)
- }
- // ShuffleStringSlice will shuffle a slice of strings.
- func ShuffleStringSlice(s []string) []string {
- rand.Seed(time.Now().UnixNano())
- rand.Shuffle(len(s), func(i, j int) {
- s[i], s[j] = s[j], s[i]
- })
- return s
- }
- // IntPercentage returns an int percentage of a number.
- func IntPercentage(x, total int) int {
- return int(float64(x) / float64(total) * 100)
- }
- // DecodeBase64Image decodes a base64 image string into a byte array, returning the extension (including dot) for the content type.
- func DecodeBase64Image(url string) (bytes []byte, extension string, err error) {
- s := strings.SplitN(url, ",", 2)
- if len(s) < 2 {
- err = errors.New("error splitting base64 image data")
- return
- }
- bytes, err = base64.StdEncoding.DecodeString(s[1])
- if err != nil {
- return
- }
- splitHeader := strings.Split(s[0], ":")
- if len(splitHeader) < 2 {
- err = errors.New("error splitting base64 image header")
- return
- }
- contentType := strings.Split(splitHeader[1], ";")[0]
- if contentType == "image/svg+xml" {
- extension = ".svg"
- } else if contentType == "image/gif" {
- extension = ".gif"
- } else if contentType == "image/png" {
- extension = ".png"
- } else if contentType == "image/jpeg" {
- extension = ".jpeg"
- }
- if extension == "" {
- err = errors.New("missing or invalid contentType in base64 image")
- return
- }
- return bytes, extension, nil
- }
|