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