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"
2020-06-03 11:34:05 +03:00
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
)
2020-10-15 00:07:38 +03:00
// If we try to upload a playlist but it is not yet on disk
// then keep a reference to it here.
var _queuedPlaylistUpdates = make ( map [ string ] string , 0 )
2020-06-23 04:11:56 +03:00
//S3Storage is the s3 implementation of the ChunkStorageProvider
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
2020-06-03 11:34:05 +03:00
}
2020-10-15 00:07:38 +03:00
var _uploader * s3manager . Uploader
2020-06-23 04:11:56 +03:00
//Setup sets up the s3 storage for saving the video to s3
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
2020-10-15 00:07:38 +03:00
if config . Config . S3 . ServingEndpoint != "" {
s . host = config . Config . S3 . ServingEndpoint
} else {
s . host = fmt . Sprintf ( "%s/%s" , config . Config . S3 . Endpoint , config . Config . S3 . Bucket )
}
2020-06-23 04:11:56 +03:00
s . s3Endpoint = config . Config . S3 . Endpoint
2020-07-28 23:17:39 +03:00
s . s3ServingEndpoint = config . Config . S3 . ServingEndpoint
2020-06-23 04:11:56 +03:00
s . s3Region = config . Config . S3 . Region
s . s3Bucket = config . Config . S3 . Bucket
s . s3AccessKey = config . Config . S3 . AccessKey
s . s3Secret = config . Config . S3 . Secret
2020-07-28 07:41:51 +03:00
s . s3ACL = config . Config . S3 . ACL
2020-06-03 11:34:05 +03:00
s . sess = s . connectAWS ( )
2020-06-23 04:11:56 +03:00
2020-10-15 00:07:38 +03:00
_uploader = s3manager . NewUploader ( s . sess )
2020-06-23 04:11:56 +03:00
return nil
2020-06-03 11:34:05 +03:00
}
2020-10-15 00:07:38 +03:00
// SegmentWritten is called when a single segment of video is written
func ( s * S3Storage ) SegmentWritten ( localFilePath string ) {
index := utils . GetIndexFromFilePath ( localFilePath )
performanceMonitorKey := "s3upload-" + index
utils . StartPerformanceMonitor ( performanceMonitorKey )
// Upload the segment
2020-10-17 01:04:31 +03:00
_ , err := s . Save ( localFilePath , 0 )
if err != nil {
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 {
if averagePerformance > float64 ( config . Config . GetVideoSegmentSecondsLength ( ) ) * 0.9 {
log . Warnln ( "Possible slow uploads: average upload S3 save duration" , averagePerformance , "ms. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/" )
}
}
// 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" )
_ , err = s . Save ( playlistPath , 0 )
if err != nil {
_queuedPlaylistUpdates [ playlistPath ] = playlistPath
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
}
}
}
// VariantPlaylistWritten is called when a variant hls playlist is written
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.
if _ , ok := _queuedPlaylistUpdates [ localFilePath ] ; ok {
2020-10-17 01:04:31 +03:00
_ , err := s . Save ( localFilePath , 0 )
if err != nil {
log . Errorln ( err )
2020-10-15 00:07:38 +03:00
_queuedPlaylistUpdates [ localFilePath ] = localFilePath
}
delete ( _queuedPlaylistUpdates , localFilePath )
}
}
// MasterPlaylistWritten is called when the master hls playlist is written
func ( s * S3Storage ) MasterPlaylistWritten ( localFilePath string ) {
// Rewrite the playlist to use absolute remote S3 URLs
s . rewriteRemotePlaylist ( localFilePath )
}
2020-06-03 11:34:05 +03:00
2020-10-15 00:07:38 +03:00
// Save saves the file to the s3 bucket
func ( s * S3Storage ) Save ( filePath string , retryCount int ) ( string , error ) {
2020-06-09 19:31:27 +03:00
file , err := os . Open ( filePath )
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
2020-10-15 00:07:38 +03:00
maxAgeSeconds := utils . GetCacheDurationSecondsForPath ( filePath )
cacheControlHeader := fmt . Sprintf ( "Cache-Control: 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
Key : aws . String ( filePath ) , // Name of the file to be saved
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
response , err := _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-10-15 00:07:38 +03:00
} else {
log . Warnln ( "Giving up on" , filePath , err )
return "" , fmt . Errorf ( "Giving up on %s" , filePath )
2020-06-18 08:01:53 +03:00
}
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-06-10 00:01:42 +03:00
Region : aws . String ( s . s3Region ) ,
Credentials : creds ,
Endpoint : aws . String ( s . s3Endpoint ) ,
S3ForcePathStyle : aws . Bool ( true ) ,
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 {
f , err := os . Open ( filePath )
if err != nil {
panic ( err )
}
p := m3u8 . NewMasterPlaylist ( )
err = p . DecodeFrom ( bufio . NewReader ( f ) , false )
for _ , item := range p . Variants {
item . URI = s . host + filepath . Join ( "/hls" , item . URI )
}
publicPath := filepath . Join ( config . PublicHLSStoragePath , filepath . Base ( filePath ) )
newPlaylist := p . String ( )
return playlist . WritePlaylist ( newPlaylist , publicPath )
}