2020-06-23 04:11:56 +03:00
package utils
import (
2020-10-14 02:45:52 +03:00
"bytes"
2022-12-12 19:40:43 +03:00
"encoding/base64"
2021-02-19 10:05:52 +03:00
"errors"
"fmt"
2022-03-08 03:30:40 +03:00
"io"
2021-07-20 05:22:29 +03:00
"math/rand"
2021-02-19 10:05:52 +03:00
"net/url"
2020-06-23 04:11:56 +03:00
"os"
2021-02-19 10:05:52 +03:00
"os/exec"
2020-10-15 00:07:38 +03:00
"path"
2020-06-23 04:11:56 +03:00
"path/filepath"
2022-01-21 07:01:05 +03:00
"regexp"
2020-06-23 04:11:56 +03:00
"strings"
2023-05-30 20:31:43 +03:00
"time"
2020-07-14 05:07:30 +03:00
2023-08-17 04:19:09 +03:00
"github.com/mssola/user_agent"
2021-02-19 10:05:52 +03:00
log "github.com/sirupsen/logrus"
2020-10-14 02:45:52 +03:00
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/renderer/html"
2024-07-08 21:24:48 +03:00
"mvdan.cc/xurls/v2"
2020-06-23 04:11:56 +03:00
)
2020-11-13 02:14:59 +03:00
// DoesFileExists checks if the file exists.
2020-06-23 04:11:56 +03:00
func DoesFileExists ( name string ) bool {
2021-07-10 04:31:43 +03:00
if _ , err := os . Stat ( name ) ; err == nil {
return true
} else if os . IsNotExist ( err ) {
return false
} else {
log . Errorln ( err )
return false
2020-06-23 04:11:56 +03:00
}
}
2020-11-13 02:14:59 +03:00
// GetRelativePathFromAbsolutePath gets the relative path from the provided absolute path.
2020-06-23 04:11:56 +03:00
func GetRelativePathFromAbsolutePath ( path string ) string {
pathComponents := strings . Split ( path , "/" )
variant := pathComponents [ len ( pathComponents ) - 2 ]
file := pathComponents [ len ( pathComponents ) - 1 ]
return filepath . Join ( variant , file )
}
2021-09-12 10:18:15 +03:00
// GetIndexFromFilePath is a utility that will return the index/key/variant name in a full path.
2020-10-15 00:07:38 +03:00
func GetIndexFromFilePath ( path string ) string {
pathComponents := strings . Split ( path , "/" )
variant := pathComponents [ len ( pathComponents ) - 2 ]
return variant
}
2020-11-13 02:14:59 +03:00
// Copy copies the file to destination.
2020-06-23 04:11:56 +03:00
func Copy ( source , destination string ) error {
2021-11-20 09:42:50 +03:00
input , err := os . ReadFile ( source ) // nolint
2020-06-23 04:11:56 +03:00
if err != nil {
return err
}
2022-03-08 03:37:04 +03:00
return os . WriteFile ( destination , input , 0 o600 )
2020-06-23 04:11:56 +03:00
}
2020-07-14 05:07:30 +03:00
2022-03-08 03:30:40 +03:00
// Move moves the file at source to destination.
2020-10-15 00:07:38 +03:00
func Move ( source , destination string ) error {
2022-03-08 03:30:40 +03:00
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 {
2022-03-08 03:37:04 +03:00
inputFile , err := os . Open ( source ) // nolint: gosec
2022-03-08 03:30:40 +03:00
if err != nil {
return fmt . Errorf ( "Couldn't open source file: %s" , err )
}
2022-03-08 03:37:04 +03:00
outputFile , err := os . Create ( destination ) // nolint: gosec
2022-03-08 03:30:40 +03:00
if err != nil {
2022-03-08 03:37:04 +03:00
_ = inputFile . Close ( )
2022-03-08 03:30:40 +03:00
return fmt . Errorf ( "Couldn't open dest file: %s" , err )
}
defer outputFile . Close ( )
_ , err = io . Copy ( outputFile , inputFile )
2022-03-08 03:37:04 +03:00
_ = inputFile . Close ( )
2022-03-08 03:30:40 +03:00
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
2020-10-15 00:07:38 +03:00
}
2021-09-01 00:49:04 +03:00
// 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" ,
2021-09-01 01:04:29 +03:00
"applecoremedia" ,
2021-09-01 00:49:04 +03:00
}
for _ , playerString := range playerStrings {
if strings . Contains ( strings . ToLower ( userAgent ) , playerString ) {
return true
}
}
return false
}
2023-08-17 04:19:09 +03:00
// 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" ,
"matrix" ,
"synapse" ,
"element" ,
"rocket.chat" ,
"duckduckbot" ,
}
for _ , botString := range botStrings {
if strings . Contains ( strings . ToLower ( userAgent ) , botString ) {
return true
}
}
ua := user_agent . New ( userAgent )
return ua . Bot ( )
}
2021-09-12 10:18:15 +03:00
// RenderSimpleMarkdown will return HTML without sanitization or specific formatting rules.
2020-10-14 02:45:52 +03:00
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 (
2024-07-08 21:24:48 +03:00
xurls . Strict ( ) ,
2020-10-14 02:45:52 +03:00
) ,
) ,
) ,
)
trimmed := strings . TrimSpace ( raw )
var buf bytes . Buffer
if err := markdown . Convert ( [ ] byte ( trimmed ) , & buf ) ; err != nil {
2021-10-25 10:14:42 +03:00
log . Fatalln ( err )
2020-10-14 02:45:52 +03:00
}
2023-05-02 05:45:27 +03:00
return strings . TrimSpace ( buf . String ( ) )
2020-10-14 02:45:52 +03:00
}
2020-10-15 00:07:38 +03:00
2021-09-12 10:18:15 +03:00
// RenderPageContentMarkdown will return HTML specifically handled for the user-specified page content.
2021-02-19 10:05:52 +03:00
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 (
2024-07-08 21:24:48 +03:00
xurls . Strict ( ) ,
2021-02-19 10:05:52 +03:00
) ,
) ,
) ,
)
trimmed := strings . TrimSpace ( raw )
var buf bytes . Buffer
if err := markdown . Convert ( [ ] byte ( trimmed ) , & buf ) ; err != nil {
2021-10-25 10:14:42 +03:00
log . Fatalln ( err )
2021-02-19 10:05:52 +03:00
}
2021-05-23 03:27:39 +03:00
return strings . TrimSpace ( buf . String ( ) )
2021-02-19 10:05:52 +03:00
}
2020-11-13 02:14:59 +03:00
// GetCacheDurationSecondsForPath will return the number of seconds to cache an item.
2020-10-15 00:07:38 +03:00
func GetCacheDurationSecondsForPath ( filePath string ) int {
2021-09-04 07:05:22 +03:00
filename := path . Base ( filePath )
fileExtension := path . Ext ( filePath )
2022-12-27 08:50:54 +03:00
defaultDaysCached := 30
2021-09-04 07:37:29 +03:00
if filename == "thumbnail.jpg" || filename == "preview.gif" {
// Thumbnails & preview gif re-generate during live
2020-10-15 00:07:38 +03:00
return 20
2021-09-04 07:05:22 +03:00
} else if fileExtension == ".js" || fileExtension == ".css" {
2020-10-15 00:07:38 +03:00
// Cache javascript & CSS
2022-12-27 08:50:54 +03:00
return 60 * 60 * 24 * defaultDaysCached
2022-10-03 03:22:13 +03:00
} else if fileExtension == ".ts" || fileExtension == ".woff2" {
2020-10-15 00:07:38 +03:00
// 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
2021-09-04 07:05:22 +03:00
} else if fileExtension == ".m3u8" {
2020-10-15 00:07:38 +03:00
return 0
2021-09-04 07:38:25 +03:00
} else if fileExtension == ".jpg" || fileExtension == ".png" || fileExtension == ".gif" || fileExtension == ".svg" {
2022-12-27 08:50:54 +03:00
return 60 * 60 * 24 * defaultDaysCached
2023-01-23 09:27:27 +03:00
} else if fileExtension == ".html" || filename == "/" || fileExtension == "" {
2023-01-11 11:51:10 +03:00
return 0
2020-10-15 00:07:38 +03:00
}
// Default cache length in seconds
2022-12-27 08:50:54 +03:00
return 60 * 60 * 24 * 1 // For unknown types, cache for 1 day
2020-10-15 00:07:38 +03:00
}
2021-02-19 10:05:52 +03:00
2021-09-12 10:18:15 +03:00
// IsValidURL will return if a URL string is a valid URL or not.
func IsValidURL ( urlToTest string ) bool {
2021-07-09 21:16:44 +03:00
if _ , err := url . ParseRequestURI ( urlToTest ) ; err != nil {
2021-02-19 10:05:52 +03:00
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
}
2021-09-12 10:18:15 +03:00
log . Warnln ( ffmpegPath , "is an invalid path to ffmpeg will try to use a copy in your path, if possible" )
2021-02-19 10:05:52 +03:00
}
// 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 {
2023-03-16 22:36:28 +03:00
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." )
2021-02-19 10:05:52 +03:00
}
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 ( )
2021-11-03 05:27:41 +03:00
// source: https://stackoverflow.com/a/60128480
2022-03-08 03:37:04 +03:00
if mode & 0 o111 == 0 {
2021-02-19 10:05:52 +03:00
return errors . New ( "ffmpeg path is not executable" )
}
return nil
}
2021-07-08 22:35:53 +03:00
2024-07-19 23:07:45 +03:00
// CleanupDirectory removes all contents within the directory, or creates it if it does not exist. Throws fatal error on failure.
2021-07-08 22:35:53 +03:00
func CleanupDirectory ( path string ) {
log . Traceln ( "Cleaning" , path )
2022-03-08 03:37:04 +03:00
if err := os . MkdirAll ( path , 0 o750 ) ; err != nil {
2024-07-19 23:07:45 +03:00
log . Fatalf ( "Unable to create '%s'. Please check the ownership and permissions: %s\n" , path , err )
}
entries , err := os . ReadDir ( path )
if err != nil {
log . Fatalf ( "Unable to read contents of '%s'. Please check the ownership and permissions: %s\n" , path , err )
}
for _ , entry := range entries {
entryPath := filepath . Join ( path , entry . Name ( ) )
if err := os . RemoveAll ( entryPath ) ; err != nil {
log . Fatalf ( "Unable to remove file or directory contained in '%s'. Please check the ownership and permissions: %s\n" , path , err )
}
2021-07-08 22:35:53 +03:00
}
2021-07-09 21:16:44 +03:00
}
2021-07-20 05:22:29 +03:00
2021-11-03 05:27:41 +03:00
// FindInSlice will return if a string is in a slice, and the index of that string.
2021-07-20 05:22:29 +03:00
func FindInSlice ( slice [ ] string , val string ) ( int , bool ) {
for i , item := range slice {
if item == val {
return i , true
}
}
return - 1 , false
}
2022-03-25 09:06:47 +03:00
// StringSliceToMap is a convenience function to convert a slice of strings into
2021-11-03 05:27:41 +03:00
// 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
}
2022-03-25 09:06:47 +03:00
// 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
}
2021-11-03 05:27:41 +03:00
// 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
}
2022-06-29 04:55:21 +03:00
// 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.
2022-08-10 05:56:45 +03:00
func GenerateRandomDisplayColor ( maxColor int ) int {
rangeLower := 0
rangeUpper := maxColor
return rangeLower + rand . Intn ( rangeUpper - rangeLower + 1 ) //nolint:gosec
2021-07-20 05:22:29 +03:00
}
2022-01-13 00:53:10 +03:00
// 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
}
2022-01-21 07:01:05 +03:00
2023-09-07 06:28:15 +03:00
// GetHostnameWithoutPortFromURLString will return the hostname component without the port from a URL object.
func GetHostnameWithoutPortFromURLString ( s string ) string {
u , err := url . Parse ( s )
if err != nil {
return ""
}
return u . Hostname ( )
}
2022-01-21 07:01:05 +03:00
// 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 )
}
2022-03-18 23:33:23 +03:00
// ShuffleStringSlice will shuffle a slice of strings.
func ShuffleStringSlice ( s [ ] string ) [ ] string {
2023-05-30 20:31:43 +03:00
// nolint:gosec
r := rand . New ( rand . NewSource ( time . Now ( ) . Unix ( ) ) )
r . Shuffle ( len ( s ) , func ( i , j int ) {
2022-03-18 23:33:23 +03:00
s [ i ] , s [ j ] = s [ j ] , s [ i ]
} )
return s
}
2022-03-28 02:27:38 +03:00
// IntPercentage returns an int percentage of a number.
func IntPercentage ( x , total int ) int {
return int ( float64 ( x ) / float64 ( total ) * 100 )
}
2022-12-12 19:40:43 +03:00
// 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" )
2023-06-02 22:22:00 +03:00
return nil , "" , err
2022-12-12 19:40:43 +03:00
}
bytes , err = base64 . StdEncoding . DecodeString ( s [ 1 ] )
if err != nil {
2023-06-02 22:22:00 +03:00
return nil , "" , err
2022-12-12 19:40:43 +03:00
}
splitHeader := strings . Split ( s [ 0 ] , ":" )
if len ( splitHeader ) < 2 {
err = errors . New ( "error splitting base64 image header" )
2023-06-02 22:22:00 +03:00
return nil , "" , err
2022-12-12 19:40:43 +03:00
}
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" )
2023-06-02 22:22:00 +03:00
return nil , "" , err
2022-12-12 19:40:43 +03:00
}
return bytes , extension , nil
}