owncast/utils/utils.go
2021-09-21 11:40:28 -07:00

289 lines
7.2 KiB
Go

package utils
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"math/rand"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"github.com/mssola/user_agent"
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 := ioutil.ReadFile(source) // nolint
if err != nil {
return err
}
return ioutil.WriteFile(destination, input, 0600)
}
// Move moves the file to destination.
func Move(source, destination string) error {
return os.Rename(source, destination)
}
// IsUserAgentABot returns if a web client user-agent is seen as a bot.
func IsUserAgentABot(userAgent string) bool {
if userAgent == "" {
return false
}
botStrings := []string{
"mastodon",
"pleroma",
"applebot",
"whatsapp",
}
for _, botString := range botStrings {
if strings.Contains(strings.ToLower(userAgent), botString) {
return true
}
}
ua := user_agent.New(userAgent)
return ua.Bot()
}
// 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 {
panic(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 {
panic(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)
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 * 10
} else if fileExtension == ".ts" {
// 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
}
// Default cache length in seconds
return 60 * 10
}
// 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 determine path to ffmpeg. Please make sure it is installed either globally or a copy exists in the owncast directory.")
}
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&0111 == 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, 0750); err != nil {
log.Fatalln("Unable to create directory. Please check the ownership and permissions", err)
}
}
// FindInSlice will return the index if a string is located in a slice of strings.
func FindInSlice(slice []string, val string) (int, bool) {
for i, item := range slice {
if item == val {
return i, true
}
}
return -1, false
}
// GenerateRandomDisplayColor will return a random _hue_ to be used when displaying a user.
// The UI should determine the right saturation and lightness in order to make it look right.
func GenerateRandomDisplayColor() int {
rangeLower := 0
rangeUpper := 360
return rangeLower + rand.Intn(rangeUpper-rangeLower+1) //nolint
}