2020-06-23 03:11:56 +02:00
package storageproviders
2020-06-03 10:34:05 +02:00
import (
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"
2023-03-16 19:07:42 +01:00
"path"
2020-10-14 23:07:38 +02:00
"path/filepath"
2023-05-31 20:10:04 +02:00
"sort"
2021-10-06 01:45:39 +02:00
"strings"
2023-12-19 06:12:50 +01:00
"sync"
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/utils"
2023-05-31 20:10:04 +02:00
"github.com/pkg/errors"
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"
2023-05-31 20:10:04 +02:00
"github.com/aws/aws-sdk-go/service/s3"
2020-06-03 10:34:05 +02:00
"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-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 {
2023-12-19 06:12:50 +01: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
2023-05-31 20:10:04 +02:00
s3Client * s3 . S3
2020-06-03 10:34:05 +02:00
2023-10-08 23:22:28 +02:00
uploader * s3manager . Uploader
2023-12-19 06:12:50 +01:00
sess * session . Session
s3Secret string
2023-10-08 23:22:28 +02:00
2020-07-28 22:17:39 +02:00
s3Bucket string
2023-10-08 23:22:28 +02:00
s3Region string
s3ServingEndpoint string
2020-07-28 22:17:39 +02:00
s3AccessKey string
2020-10-03 23:35:03 +02:00
s3ACL string
2023-08-02 22:35:47 +02:00
s3PathPrefix string
2023-05-31 20:10:04 +02:00
2023-10-08 23:22:28 +02:00
s3Endpoint string
host string
2023-05-31 20:10:04 +02:00
2023-12-19 06:12:50 +01:00
lock sync . Mutex
2023-10-08 23:22:28 +02:00
s3ForcePathStyle bool
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 ) ,
2023-12-19 06:12:50 +01:00
lock : sync . Mutex { } ,
2021-10-12 01:29:36 +02:00
}
}
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 ( )
2023-05-30 23:05:24 +02:00
customVideoServingEndpoint := data . GetVideoServingEndpoint ( )
2023-05-31 20:10:04 +02:00
2023-05-30 23:05:24 +02:00
if customVideoServingEndpoint != "" {
s . host = customVideoServingEndpoint
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
2023-08-02 22:35:47 +02:00
s . s3PathPrefix = s3Config . PathPrefix
2021-10-29 02:27:44 +02:00
s . s3ForcePathStyle = s3Config . ForcePathStyle
2020-06-03 10:34:05 +02:00
s . sess = s . connectAWS ( )
2023-05-31 20:10:04 +02:00
s . s3Client = s3 . New ( s . sess )
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" )
2023-08-02 22:35:47 +02:00
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.
2023-12-19 06:12:50 +01:00
s . lock . Lock ( )
defer s . lock . Unlock ( )
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
2023-09-22 02:58:02 +02:00
if err := rewritePlaylistLocations ( localFilePath , s . host , s . s3PathPrefix ) ; 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 } , "" )
2023-08-02 22:35:47 +02:00
// If a custom path prefix is set prepend it.
if s . s3PathPrefix != "" {
prefix := strings . TrimPrefix ( s . s3PathPrefix , "/" )
remotePath = strings . Join ( [ ] string { prefix , remotePath } , "/" )
}
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 )
2023-03-16 19:07:42 +01:00
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
2023-03-16 19:07:42 +01:00
if path . Ext ( filePath ) == ".m3u8" {
noCacheHeader := "no-cache, no-store, must-revalidate"
contentType := "application/x-mpegURL"
uploadInput . CacheControl = & noCacheHeader
uploadInput . ContentType = & contentType
}
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
2023-08-04 05:33:44 +02:00
// Upload failure. Remove the local file.
s . removeLocalFile ( filePath )
2023-04-01 22:15:35 +02:00
return "" , fmt . Errorf ( "Giving up uploading %s to object storage %s" , filePath , s . s3Endpoint )
2020-06-03 10:34:05 +02:00
}
2023-08-04 05:33:44 +02:00
// Upload success. Remove the local file.
s . removeLocalFile ( filePath )
2020-06-23 03:11:56 +02:00
return response . Location , nil
2020-06-03 10:34:05 +02:00
}
2023-05-31 20:10:04 +02:00
func ( s * S3Storage ) Cleanup ( ) error {
// Determine how many files we should keep on S3 storage
maxNumber := data . GetStreamLatencyLevel ( ) . SegmentCount
buffer := 20
keys , err := s . getDeletableVideoSegmentsWithOffset ( maxNumber + buffer )
if err != nil {
return err
}
2023-07-25 00:12:04 +02:00
if len ( keys ) > 0 {
s . deleteObjects ( keys )
}
2023-05-31 20:10:04 +02:00
return nil
}
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
}
2023-05-31 20:10:04 +02:00
func ( s * S3Storage ) getDeletableVideoSegmentsWithOffset ( offset int ) ( [ ] s3object , error ) {
objectsToDelete , err := s . retrieveAllVideoSegments ( )
if err != nil {
return nil , err
}
2023-07-25 00:12:04 +02:00
if offset > len ( objectsToDelete ) - 1 {
offset = len ( objectsToDelete ) - 1
}
2023-05-31 20:10:04 +02:00
objectsToDelete = objectsToDelete [ offset : len ( objectsToDelete ) - 1 ]
return objectsToDelete , nil
}
2023-08-04 05:33:44 +02:00
func ( s * S3Storage ) removeLocalFile ( filePath string ) {
cleanFilepath := filepath . Clean ( filePath )
if err := os . Remove ( cleanFilepath ) ; err != nil {
log . Errorln ( err )
}
}
2023-05-31 20:10:04 +02:00
func ( s * S3Storage ) deleteObjects ( objects [ ] s3object ) {
keys := make ( [ ] * s3 . ObjectIdentifier , len ( objects ) )
for i , object := range objects {
keys [ i ] = & s3 . ObjectIdentifier { Key : aws . String ( object . key ) }
}
log . Debugln ( "Deleting" , len ( keys ) , "objects from S3 bucket:" , s . s3Bucket )
deleteObjectsRequest := & s3 . DeleteObjectsInput {
Bucket : aws . String ( s . s3Bucket ) ,
Delete : & s3 . Delete {
Objects : keys ,
Quiet : aws . Bool ( true ) ,
} ,
}
_ , err := s . s3Client . DeleteObjects ( deleteObjectsRequest )
if err != nil {
log . Errorf ( "Unable to delete objects from bucket %q, %v\n" , s . s3Bucket , err )
}
}
func ( s * S3Storage ) retrieveAllVideoSegments ( ) ( [ ] s3object , error ) {
allObjectsListRequest := & s3 . ListObjectsInput {
Bucket : aws . String ( s . s3Bucket ) ,
}
// Fetch all objects in the bucket
allObjectsListResponse , err := s . s3Client . ListObjects ( allObjectsListRequest )
if err != nil {
return nil , errors . Wrap ( err , "Unable to fetch list of items in bucket for cleanup" )
}
// Filter out non-video segments
allObjects := [ ] s3object { }
for _ , item := range allObjectsListResponse . Contents {
if ! strings . HasSuffix ( * item . Key , ".ts" ) {
continue
}
allObjects = append ( allObjects , s3object {
key : * item . Key ,
lastModified : * item . LastModified ,
} )
}
// Sort the results by timestamp
sort . Slice ( allObjects , func ( i , j int ) bool {
return allObjects [ i ] . lastModified . After ( allObjects [ j ] . lastModified )
} )
return allObjects , nil
}
type s3object struct {
lastModified time . Time
2023-10-08 23:22:28 +02:00
key string
2023-05-31 20:10:04 +02:00
}