2020-06-23 04:11:56 +03:00
package storageproviders
2020-06-03 11:34:05 +03:00
import (
"bufio"
2020-07-28 23:17:39 +03:00
"fmt"
2020-06-03 11:34:05 +03:00
"os"
2020-10-15 00:07:38 +03:00
"path/filepath"
2021-10-06 02:45:39 +03:00
"strings"
2020-06-03 11:34:05 +03:00
2021-02-19 10:05:52 +03:00
"github.com/owncast/owncast/core/data"
2020-10-15 00:07:38 +03:00
"github.com/owncast/owncast/core/playlist"
"github.com/owncast/owncast/utils"
2020-06-03 11:34:05 +03:00
log "github.com/sirupsen/logrus"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
2020-06-23 04:11:56 +03:00
2020-10-05 20:07:09 +03:00
"github.com/owncast/owncast/config"
2020-10-15 00:07:38 +03:00
"github.com/grafov/m3u8"
2020-06-03 11:34:05 +03:00
)
2021-09-12 10:18:15 +03:00
// S3Storage is the s3 implementation of a storage provider.
2020-06-03 11:34:05 +03:00
type S3Storage struct {
sess * session . Session
host string
2020-07-28 23:17:39 +03:00
s3Endpoint string
s3ServingEndpoint string
s3Region string
s3Bucket string
s3AccessKey string
s3Secret string
2020-10-04 00:35:03 +03:00
s3ACL string
2021-10-12 02:29:36 +03:00
// If we try to upload a playlist but it is not yet on disk
// then keep a reference to it here.
queuedPlaylistUpdates map [ string ] string
uploader * s3manager . Uploader
2020-06-03 11:34:05 +03:00
}
2021-10-12 02:29:36 +03:00
// NewS3Storage returns a new S3Storage instance.
func NewS3Storage ( ) * S3Storage {
return & S3Storage {
queuedPlaylistUpdates : make ( map [ string ] string ) ,
}
}
2020-10-15 00:07:38 +03:00
2020-11-13 02:14:59 +03:00
// Setup sets up the s3 storage for saving the video to s3.
2020-06-23 04:11:56 +03:00
func ( s * S3Storage ) Setup ( ) error {
2020-07-07 07:27:31 +03:00
log . Trace ( "Setting up S3 for external storage of video..." )
2020-06-03 11:34:05 +03:00
2021-02-19 10:05:52 +03:00
s3Config := data . GetS3Config ( )
if s3Config . ServingEndpoint != "" {
s . host = s3Config . ServingEndpoint
2020-10-15 00:07:38 +03:00
} else {
2021-02-19 10:05:52 +03:00
s . host = fmt . Sprintf ( "%s/%s" , s3Config . Endpoint , s3Config . Bucket )
2020-10-15 00:07:38 +03:00
}
2021-02-19 10:05:52 +03:00
s . s3Endpoint = s3Config . Endpoint
s . s3ServingEndpoint = s3Config . ServingEndpoint
s . s3Region = s3Config . Region
s . s3Bucket = s3Config . Bucket
s . s3AccessKey = s3Config . AccessKey
s . s3Secret = s3Config . Secret
s . s3ACL = s3Config . ACL
2020-06-03 11:34:05 +03:00
s . sess = s . connectAWS ( )
2020-06-23 04:11:56 +03:00
2021-10-12 02:29:36 +03:00
s . uploader = s3manager . NewUploader ( s . sess )
2020-10-15 00:07:38 +03:00
2020-06-23 04:11:56 +03:00
return nil
2020-06-03 11:34:05 +03:00
}
2020-11-13 02:14:59 +03:00
// SegmentWritten is called when a single segment of video is written.
2020-10-15 00:07:38 +03:00
func ( s * S3Storage ) SegmentWritten ( localFilePath string ) {
index := utils . GetIndexFromFilePath ( localFilePath )
performanceMonitorKey := "s3upload-" + index
utils . StartPerformanceMonitor ( performanceMonitorKey )
// Upload the segment
2021-07-09 21:16:44 +03:00
if _ , err := s . Save ( localFilePath , 0 ) ; err != nil {
2020-10-17 01:04:31 +03:00
log . Errorln ( err )
2020-10-15 00:07:38 +03:00
return
}
averagePerformance := utils . GetAveragePerformance ( performanceMonitorKey )
// Warn the user about long-running save operations
if averagePerformance != 0 {
2021-02-19 10:05:52 +03:00
if averagePerformance > float64 ( data . GetStreamLatencyLevel ( ) . SecondsPerSegment ) * 0.9 {
2020-11-21 01:11:19 +03:00
log . Warnln ( "Possible slow uploads: average upload S3 save duration" , averagePerformance , "s. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/" )
2020-10-15 00:07:38 +03:00
}
}
// Upload the variant playlist for this segment
// so the segments and the HLS playlist referencing
// them are in sync.
2020-10-17 01:04:31 +03:00
playlistPath := filepath . Join ( filepath . Dir ( localFilePath ) , "stream.m3u8" )
2021-07-09 21:16:44 +03:00
if _ , err := s . Save ( playlistPath , 0 ) ; err != nil {
2021-10-12 02:29:36 +03:00
s . queuedPlaylistUpdates [ playlistPath ] = playlistPath
2020-10-17 01:04:31 +03:00
if pErr , ok := err . ( * os . PathError ) ; ok {
2020-10-15 00:07:38 +03:00
log . Debugln ( pErr . Path , "does not yet exist locally when trying to upload to S3 storage." )
return
}
}
}
2020-11-13 02:14:59 +03:00
// VariantPlaylistWritten is called when a variant hls playlist is written.
2020-10-15 00:07:38 +03:00
func ( s * S3Storage ) VariantPlaylistWritten ( localFilePath string ) {
// We are uploading the variant playlist after uploading the segment
2020-11-13 01:57:24 +03:00
// to make sure we're not referring to files in a playlist that don't
2020-10-15 00:07:38 +03:00
// yet exist. See SegmentWritten.
2021-10-12 02:29:36 +03:00
if _ , ok := s . queuedPlaylistUpdates [ localFilePath ] ; ok {
2021-07-09 21:16:44 +03:00
if _ , err := s . Save ( localFilePath , 0 ) ; err != nil {
2020-10-17 01:04:31 +03:00
log . Errorln ( err )
2021-10-12 02:29:36 +03:00
s . queuedPlaylistUpdates [ localFilePath ] = localFilePath
2020-10-15 00:07:38 +03:00
}
2021-10-12 02:29:36 +03:00
delete ( s . queuedPlaylistUpdates , localFilePath )
2020-10-15 00:07:38 +03:00
}
}
2020-11-13 02:14:59 +03:00
// MasterPlaylistWritten is called when the master hls playlist is written.
2020-10-15 00:07:38 +03:00
func ( s * S3Storage ) MasterPlaylistWritten ( localFilePath string ) {
// Rewrite the playlist to use absolute remote S3 URLs
2021-07-09 21:16:44 +03:00
if err := s . rewriteRemotePlaylist ( localFilePath ) ; err != nil {
2020-11-15 05:39:53 +03:00
log . Warnln ( err )
}
2020-10-15 00:07:38 +03:00
}
2020-06-03 11:34:05 +03:00
2020-11-13 02:14:59 +03:00
// Save saves the file to the s3 bucket.
2020-10-15 00:07:38 +03:00
func ( s * S3Storage ) Save ( filePath string , retryCount int ) ( string , error ) {
2021-09-12 10:18:15 +03:00
file , err := os . Open ( filePath ) // nolint
2020-06-03 11:34:05 +03:00
if err != nil {
2020-06-23 04:11:56 +03:00
return "" , err
2020-06-03 11:34:05 +03:00
}
2020-06-23 04:11:56 +03:00
defer file . Close ( )
2020-06-03 11:34:05 +03:00
2021-10-06 02:45:39 +03:00
// Convert the local path to the variant/file path by stripping the local storage location.
normalizedPath := strings . TrimPrefix ( filePath , config . HLSStoragePath )
// Build the remote path by adding the "hls" path prefix.
remotePath := strings . Join ( [ ] string { "hls" , normalizedPath } , "" )
2020-10-15 00:07:38 +03:00
maxAgeSeconds := utils . GetCacheDurationSecondsForPath ( filePath )
2021-05-23 23:35:05 +03:00
cacheControlHeader := fmt . Sprintf ( "max-age=%d" , maxAgeSeconds )
2020-07-28 07:41:51 +03:00
uploadInput := & s3manager . UploadInput {
2020-10-15 00:07:38 +03:00
Bucket : aws . String ( s . s3Bucket ) , // Bucket to be used
2021-10-06 02:45:39 +03:00
Key : aws . String ( remotePath ) , // Name of the file to be saved
2020-10-15 00:07:38 +03:00
Body : file , // File
CacheControl : & cacheControlHeader ,
2020-07-28 07:41:51 +03:00
}
2020-10-15 00:07:38 +03:00
2020-07-28 07:41:51 +03:00
if s . s3ACL != "" {
uploadInput . ACL = aws . String ( s . s3ACL )
2020-10-15 00:07:38 +03:00
} else {
// Default ACL
uploadInput . ACL = aws . String ( "public-read" )
2020-07-28 07:41:51 +03:00
}
2020-10-15 00:07:38 +03:00
2021-10-12 02:29:36 +03:00
response , err := s . uploader . Upload ( uploadInput )
2020-06-03 11:34:05 +03:00
if err != nil {
2020-10-15 00:07:38 +03:00
log . Traceln ( "error uploading:" , filePath , err . Error ( ) )
2020-06-18 08:01:53 +03:00
if retryCount < 4 {
2020-10-15 00:07:38 +03:00
log . Traceln ( "Retrying..." )
2020-06-23 04:11:56 +03:00
return s . Save ( filePath , retryCount + 1 )
2020-06-18 08:01:53 +03:00
}
2021-09-12 10:18:15 +03:00
log . Warnln ( "Giving up on" , filePath , err )
return "" , fmt . Errorf ( "Giving up on %s" , filePath )
2020-06-03 11:34:05 +03:00
}
2020-06-23 04:11:56 +03:00
return response . Location , nil
2020-06-03 11:34:05 +03:00
}
2020-10-15 00:07:38 +03:00
func ( s * S3Storage ) connectAWS ( ) * session . Session {
2020-06-03 11:34:05 +03:00
creds := credentials . NewStaticCredentials ( s . s3AccessKey , s . s3Secret , "" )
_ , err := creds . Get ( )
if err != nil {
2020-06-18 09:01:49 +03:00
log . Panicln ( err )
2020-06-03 11:34:05 +03:00
}
sess , err := session . NewSession (
& aws . Config {
2020-12-31 22:50:18 +03:00
Region : aws . String ( s . s3Region ) ,
Credentials : creds ,
Endpoint : aws . String ( s . s3Endpoint ) ,
2020-06-03 11:34:05 +03:00
} ,
)
if err != nil {
2020-06-18 09:01:49 +03:00
log . Panicln ( err )
2020-06-03 11:34:05 +03:00
}
return sess
}
2020-10-15 00:07:38 +03:00
// rewriteRemotePlaylist will take a local playlist and rewrite it to have absolute URLs to remote locations.
func ( s * S3Storage ) rewriteRemotePlaylist ( filePath string ) error {
2021-09-12 10:18:15 +03:00
f , err := os . Open ( filePath ) // nolint
2020-10-15 00:07:38 +03:00
if err != nil {
2021-10-25 10:14:42 +03:00
log . Fatalln ( err )
2020-10-15 00:07:38 +03:00
}
p := m3u8 . NewMasterPlaylist ( )
2021-07-09 21:16:44 +03:00
if err := p . DecodeFrom ( bufio . NewReader ( f ) , false ) ; err != nil {
2020-11-15 05:39:53 +03:00
log . Warnln ( err )
}
2020-10-15 00:07:38 +03:00
for _ , item := range p . Variants {
item . URI = s . host + filepath . Join ( "/hls" , item . URI )
}
2021-09-12 21:32:42 +03:00
publicPath := filepath . Join ( config . HLSStoragePath , filepath . Base ( filePath ) )
2020-10-15 00:07:38 +03:00
newPlaylist := p . String ( )
return playlist . WritePlaylist ( newPlaylist , publicPath )
}