diff --git a/core/storageproviders/rewriteLocalPlaylist.go b/core/storageproviders/rewriteLocalPlaylist.go index db346d21c..5ba433ee2 100644 --- a/core/storageproviders/rewriteLocalPlaylist.go +++ b/core/storageproviders/rewriteLocalPlaylist.go @@ -13,7 +13,7 @@ import ( ) // rewriteRemotePlaylist will take a local playlist and rewrite it to have absolute URLs to remote locations. -func rewriteRemotePlaylist(localFilePath, remoteServingEndpoint string) error { +func rewriteRemotePlaylist(localFilePath, remoteServingEndpoint, pathPrefix string) error { f, err := os.Open(localFilePath) // nolint if err != nil { log.Fatalln(err) @@ -25,7 +25,14 @@ func rewriteRemotePlaylist(localFilePath, remoteServingEndpoint string) error { } for _, item := range p.Variants { - item.URI = remoteServingEndpoint + filepath.Join("/hls", item.URI) + // Determine the final path to this playlist. + var finalPath string + if pathPrefix != "" { + finalPath = filepath.Join(pathPrefix, "/hls") + } else { + finalPath = "/hls" + } + item.URI = remoteServingEndpoint + filepath.Join(finalPath, item.URI) } publicPath := filepath.Join(config.HLSStoragePath, filepath.Base(localFilePath)) diff --git a/core/storageproviders/s3Storage.go b/core/storageproviders/s3Storage.go index 72c6aa332..e73233305 100644 --- a/core/storageproviders/s3Storage.go +++ b/core/storageproviders/s3Storage.go @@ -37,6 +37,7 @@ type S3Storage struct { s3AccessKey string s3Secret string s3ACL string + s3PathPrefix string s3ForcePathStyle bool // If we try to upload a playlist but it is not yet on disk @@ -73,6 +74,7 @@ func (s *S3Storage) Setup() error { s.s3AccessKey = s3Config.AccessKey s.s3Secret = s3Config.Secret s.s3ACL = s3Config.ACL + s.s3PathPrefix = s3Config.PathPrefix s.s3ForcePathStyle = s3Config.ForcePathStyle s.sess = s.connectAWS() @@ -107,6 +109,7 @@ func (s *S3Storage) SegmentWritten(localFilePath string) { // so the segments and the HLS playlist referencing // them are in sync. playlistPath := filepath.Join(filepath.Dir(localFilePath), "stream.m3u8") + if _, err := s.Save(playlistPath, 0); err != nil { s.queuedPlaylistUpdates[playlistPath] = playlistPath if pErr, ok := err.(*os.PathError); ok { @@ -133,7 +136,7 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) { // 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 - if err := rewriteRemotePlaylist(localFilePath, s.host); err != nil { + if err := rewriteRemotePlaylist(localFilePath, s.host, s.s3PathPrefix); err != nil { log.Warnln(err) } } @@ -151,6 +154,12 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) { // Build the remote path by adding the "hls" path prefix. remotePath := strings.Join([]string{"hls", normalizedPath}, "") + // If a custom path prefix is set prepend it. + if s.s3PathPrefix != "" { + prefix := strings.TrimPrefix(s.s3PathPrefix, "/") + remotePath = strings.Join([]string{prefix, remotePath}, "/") + } + maxAgeSeconds := utils.GetCacheDurationSecondsForPath(filePath) cacheControlHeader := fmt.Sprintf("max-age=%d", maxAgeSeconds) diff --git a/models/s3Storage.go b/models/s3Storage.go index 5eca8c063..522d1b8e5 100644 --- a/models/s3Storage.go +++ b/models/s3Storage.go @@ -11,6 +11,9 @@ type S3 struct { ACL string `json:"acl,omitempty"` ForcePathStyle bool `json:"forcePathStyle"` + // PathPrefix is an optional prefix for object storage. + PathPrefix string `json:"pathPrefix,omitempty"` + // This property is no longer used as of v0.1.1. See the standalone // property that was pulled out of here instead. It's only left here // to allow the migration to take place without data loss. diff --git a/web/components/admin/config/server/EditStorage.tsx b/web/components/admin/config/server/EditStorage.tsx index fc5767728..0206441c0 100644 --- a/web/components/admin/config/server/EditStorage.tsx +++ b/web/components/admin/config/server/EditStorage.tsx @@ -28,7 +28,8 @@ const { Panel } = Collapse; // we could probably add more detailed checks here // `currentValues` is what's currently in the global store and in the db function checkSaveable(formValues: any, currentValues: any) { - const { endpoint, accessKey, secret, bucket, region, enabled, acl, forcePathStyle } = formValues; + const { endpoint, accessKey, secret, bucket, region, enabled, acl, forcePathStyle, pathPrefix } = + formValues; // if fields are filled out and different from what's in store, then return true if (enabled) { if (!!endpoint && isValidUrl(endpoint) && !!accessKey && !!secret && !!bucket && !!region) { @@ -39,6 +40,7 @@ function checkSaveable(formValues: any, currentValues: any) { secret !== currentValues.secret || bucket !== currentValues.bucket || region !== currentValues.region || + pathPrefix !== currentValues.pathPrefix || (!currentValues.acl && acl !== '') || (!!currentValues.acl && acl !== currentValues.acl) || forcePathStyle !== currentValues.forcePathStyle @@ -72,6 +74,7 @@ export default function EditStorage() { endpoint = '', region = '', secret = '', + pathPrefix = '', forcePathStyle = false, } = s3; @@ -84,6 +87,7 @@ export default function EditStorage() { endpoint, region, secret, + pathPrefix, forcePathStyle, }); setShouldDisplayForm(enabled); @@ -219,6 +223,14 @@ export default function EditStorage() { /> +
+ +
+