dnscontrol/pkg/cloudflare-go/stream.go
Tom Limoncelli 7fd6a74e0c
CLOUDFLAREAPI: CF_REDIRECT/CF_TEMP_REDIRECT should dtrt using Single Redirects (#3002)
Co-authored-by: Josh Zhang <jzhang1@stackoverflow.com>
2024-06-18 17:38:50 -04:00

574 lines
21 KiB
Go

package cloudflare
import (
"bytes"
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/goccy/go-json"
)
var (
// ErrMissingUploadURL is for when a URL is required but missing.
ErrMissingUploadURL = errors.New("required url missing")
// ErrMissingMaxDuration is for when MaxDuration is required but missing.
ErrMissingMaxDuration = errors.New("required max duration missing")
// ErrMissingVideoID is for when VideoID is required but missing.
ErrMissingVideoID = errors.New("required video id missing")
// ErrMissingFilePath is for when FilePath is required but missing.
ErrMissingFilePath = errors.New("required file path missing")
// ErrMissingTusResumable is for when TusResumable is required but missing.
ErrMissingTusResumable = errors.New("required tus resumable missing")
// ErrInvalidTusResumable is for when TusResumable is invalid.
ErrInvalidTusResumable = errors.New("invalid tus resumable")
// ErrMarshallingTUSMetadata is for when TUS metadata cannot be marshalled.
ErrMarshallingTUSMetadata = errors.New("error marshalling TUS metadata")
// ErrMissingUploadLength is for when UploadLength is required but missing.
ErrMissingUploadLength = errors.New("required upload length missing")
// ErrInvalidStatusCode is for when the status code is invalid.
ErrInvalidStatusCode = errors.New("invalid status code")
)
type TusProtocolVersion string
const (
TusProtocolVersion1_0_0 TusProtocolVersion = "1.0.0"
)
// StreamVideo represents a stream video.
type StreamVideo struct {
AllowedOrigins []string `json:"allowedOrigins,omitempty"`
Created *time.Time `json:"created,omitempty"`
Duration float64 `json:"duration,omitempty"`
Input StreamVideoInput `json:"input,omitempty"`
MaxDurationSeconds int `json:"maxDurationSeconds,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
Modified *time.Time `json:"modified,omitempty"`
UploadExpiry *time.Time `json:"uploadExpiry,omitempty"`
Playback StreamVideoPlayback `json:"playback,omitempty"`
Preview string `json:"preview,omitempty"`
ReadyToStream bool `json:"readyToStream,omitempty"`
RequireSignedURLs bool `json:"requireSignedURLs,omitempty"`
Size int `json:"size,omitempty"`
Status StreamVideoStatus `json:"status,omitempty"`
Thumbnail string `json:"thumbnail,omitempty"`
ThumbnailTimestampPct float64 `json:"thumbnailTimestampPct,omitempty"`
UID string `json:"uid,omitempty"`
Creator string `json:"creator,omitempty"`
LiveInput string `json:"liveInput,omitempty"`
Uploaded *time.Time `json:"uploaded,omitempty"`
ScheduledDeletion *time.Time `json:"scheduledDeletion,omitempty"`
Watermark StreamVideoWatermark `json:"watermark,omitempty"`
NFT StreamVideoNFTParameters `json:"nft,omitempty"`
}
// StreamVideoInput represents the video input values of a stream video.
type StreamVideoInput struct {
Height int `json:"height,omitempty"`
Width int `json:"width,omitempty"`
}
// StreamVideoPlayback represents the playback URLs for a video.
type StreamVideoPlayback struct {
HLS string `json:"hls,omitempty"`
Dash string `json:"dash,omitempty"`
}
// StreamVideoStatus represents the status of a stream video.
type StreamVideoStatus struct {
State string `json:"state,omitempty"`
PctComplete string `json:"pctComplete,omitempty"`
ErrorReasonCode string `json:"errorReasonCode,omitempty"`
ErrorReasonText string `json:"errorReasonText,omitempty"`
}
// StreamVideoWatermark represents a watermark for a stream video.
type StreamVideoWatermark struct {
UID string `json:"uid,omitempty"`
Size int `json:"size,omitempty"`
Height int `json:"height,omitempty"`
Width int `json:"width,omitempty"`
Created *time.Time `json:"created,omitempty"`
DownloadedFrom string `json:"downloadedFrom,omitempty"`
Name string `json:"name,omitempty"`
Opacity float64 `json:"opacity,omitempty"`
Padding float64 `json:"padding,omitempty"`
Scale float64 `json:"scale,omitempty"`
Position string `json:"position,omitempty"`
}
// StreamVideoNFTParameters represents a NFT for a stream video.
type StreamVideoNFTParameters struct {
AccountID string
VideoID string
Contract string `json:"contract,omitempty"`
Token int `json:"token,omitempty"`
}
// StreamUploadFromURLParameters are the parameters used when uploading a video from URL.
type StreamUploadFromURLParameters struct {
AccountID string
VideoID string
URL string `json:"url"`
Creator string `json:"creator,omitempty"`
ThumbnailTimestampPct float64 `json:"thumbnailTimestampPct,omitempty"`
AllowedOrigins []string `json:"allowedOrigins,omitempty"`
RequireSignedURLs bool `json:"requireSignedURLs,omitempty"`
Watermark UploadVideoURLWatermark `json:"watermark,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
ScheduledDeletion *time.Time `json:"scheduledDeletion,omitempty"`
}
// StreamCreateVideoParameters are parameters used when creating a video.
type StreamCreateVideoParameters struct {
AccountID string
MaxDurationSeconds int `json:"maxDurationSeconds,omitempty"`
Expiry *time.Time `json:"expiry,omitempty"`
Creator string `json:"creator,omitempty"`
ThumbnailTimestampPct float64 `json:"thumbnailTimestampPct,omitempty"`
AllowedOrigins []string `json:"allowedOrigins,omitempty"`
RequireSignedURLs bool `json:"requireSignedURLs,omitempty"`
Watermark UploadVideoURLWatermark `json:"watermark,omitempty"`
Meta map[string]interface{} `json:"meta,omitempty"`
ScheduledDeletion *time.Time `json:"scheduledDeletion,omitempty"`
}
// UploadVideoURLWatermark represents UID of an existing watermark.
type UploadVideoURLWatermark struct {
UID string `json:"uid,omitempty"`
}
// StreamVideoCreate represents parameters returned after creating a video.
type StreamVideoCreate struct {
UploadURL string `json:"uploadURL,omitempty"`
UID string `json:"uid,omitempty"`
Watermark StreamVideoWatermark `json:"watermark,omitempty"`
ScheduledDeletion *time.Time `json:"scheduledDeletion,omitempty"`
}
// StreamParameters are the basic parameters needed.
type StreamParameters struct {
AccountID string
VideoID string
}
// StreamUploadFileParameters are parameters needed for file upload of a video.
type StreamUploadFileParameters struct {
AccountID string
VideoID string
FilePath string
ScheduledDeletion *time.Time
}
// StreamListParameters represents parameters used when listing stream videos.
type StreamListParameters struct {
AccountID string
VideoID string
After *time.Time `url:"after,omitempty"`
Before *time.Time `url:"before,omitempty"`
Creator string `url:"creator,omitempty"`
IncludeCounts bool `url:"include_counts,omitempty"`
Search string `url:"search,omitempty"`
Limit int `url:"limit,omitempty"`
Asc bool `url:"asc,omitempty"`
Status string `url:"status,omitempty"`
}
// StreamSignedURLParameters represent parameters used when creating a signed URL.
type StreamSignedURLParameters struct {
AccountID string
VideoID string
ID string `json:"id,omitempty"`
PEM string `json:"pem,omitempty"`
EXP int `json:"exp,omitempty"`
NBF int `json:"nbf,omitempty"`
Downloadable bool `json:"downloadable,omitempty"`
AccessRules []StreamAccessRule `json:"accessRules,omitempty"`
}
type StreamInitiateTUSUploadParameters struct {
DirectUserUpload bool `url:"direct_user,omitempty"`
TusResumable TusProtocolVersion `url:"-"`
UploadLength int64 `url:"-"`
UploadCreator string `url:"-"`
Metadata TUSUploadMetadata `url:"-"`
}
type StreamInitiateTUSUploadResponse struct {
ResponseHeaders http.Header
}
type TUSUploadMetadata struct {
Name string `json:"name,omitempty"`
MaxDurationSeconds int `json:"maxDurationSeconds,omitempty"`
RequireSignedURLs bool `json:"requiresignedurls,omitempty"`
AllowedOrigins string `json:"allowedorigins,omitempty"`
ThumbnailTimestampPct float64 `json:"thumbnailtimestamppct,omitempty"`
ScheduledDeletion *time.Time `json:"scheduledDeletion,omitempty"`
Expiry *time.Time `json:"expiry,omitempty"`
Watermark string `json:"watermark,omitempty"`
}
func (t TUSUploadMetadata) ToTUSCsv() (string, error) {
var metadataValues []string
if t.Name != "" {
metadataValues = append(metadataValues, fmt.Sprintf("%s %s", "name", base64.StdEncoding.EncodeToString([]byte(t.Name))))
}
if t.MaxDurationSeconds != 0 {
metadataValues = append(metadataValues, fmt.Sprintf("%s %s", "maxDurationSeconds", base64.StdEncoding.EncodeToString([]byte(strconv.Itoa(t.MaxDurationSeconds)))))
}
if t.RequireSignedURLs {
metadataValues = append(metadataValues, "requiresignedurls")
}
if t.AllowedOrigins != "" {
metadataValues = append(metadataValues, fmt.Sprintf("%s %s", "allowedorigins", base64.StdEncoding.EncodeToString([]byte(t.AllowedOrigins))))
}
if t.ThumbnailTimestampPct != 0 {
metadataValues = append(metadataValues, fmt.Sprintf("%s %s", "thumbnailtimestamppct", base64.StdEncoding.EncodeToString([]byte(strconv.FormatFloat(t.ThumbnailTimestampPct, 'f', -1, 64)))))
}
if t.ScheduledDeletion != nil {
metadataValues = append(metadataValues, fmt.Sprintf("%s %s", "scheduledDeletion", base64.StdEncoding.EncodeToString([]byte(t.ScheduledDeletion.Format(time.RFC3339)))))
}
if t.Expiry != nil {
metadataValues = append(metadataValues, fmt.Sprintf("%s %s", "expiry", base64.StdEncoding.EncodeToString([]byte(t.Expiry.Format(time.RFC3339)))))
}
if t.Watermark != "" {
metadataValues = append(metadataValues, fmt.Sprintf("%s %s", "watermark", base64.StdEncoding.EncodeToString([]byte(t.Watermark))))
}
if len(metadataValues) > 0 {
return strings.Join(metadataValues, ","), nil
}
return "", nil
}
// StreamVideoResponse represents an API response of a stream video.
type StreamVideoResponse struct {
Response
Result StreamVideo `json:"result,omitempty"`
}
// StreamVideoCreateResponse represents an API response of creating a stream video.
type StreamVideoCreateResponse struct {
Response
Result StreamVideoCreate `json:"result,omitempty"`
}
// StreamListResponse represents the API response from a StreamListRequest.
type StreamListResponse struct {
Response
Result []StreamVideo `json:"result,omitempty"`
Total string `json:"total,omitempty"`
Range string `json:"range,omitempty"`
}
// StreamSignedURLResponse represents an API response for a signed URL.
type StreamSignedURLResponse struct {
Response
Result struct {
Token string `json:"token,omitempty"`
}
}
// StreamAccessRule represents the accessRules when creating a signed URL.
type StreamAccessRule struct {
Type string `json:"type"`
Country []string `json:"country,omitempty"`
Action string `json:"action"`
IP []string `json:"ip,omitempty"`
}
// StreamUploadFromURL send a video URL to it will be downloaded and made available on Stream.
//
// API Reference: https://api.cloudflare.com/#stream-videos-upload-a-video-from-a-url
func (api *API) StreamUploadFromURL(ctx context.Context, params StreamUploadFromURLParameters) (StreamVideo, error) {
if params.AccountID == "" {
return StreamVideo{}, ErrMissingAccountID
}
if params.URL == "" {
return StreamVideo{}, ErrMissingUploadURL
}
uri := fmt.Sprintf("/accounts/%s/stream/copy", params.AccountID)
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
if err != nil {
return StreamVideo{}, err
}
var streamVideoResponse StreamVideoResponse
if err := json.Unmarshal(res, &streamVideoResponse); err != nil {
return StreamVideo{}, err
}
return streamVideoResponse.Result, nil
}
// StreamUploadVideoFile uploads a video from a path to the file.
//
// API Reference: https://api.cloudflare.com/#stream-videos-upload-a-video-using-a-single-http-request
func (api *API) StreamUploadVideoFile(ctx context.Context, params StreamUploadFileParameters) (StreamVideo, error) {
if params.AccountID == "" {
return StreamVideo{}, ErrMissingAccountID
}
if params.FilePath == "" {
return StreamVideo{}, ErrMissingFilePath
}
uri := fmt.Sprintf("/accounts/%s/stream", params.AccountID)
// Create new multipart writer
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
formFile, err := writer.CreateFormFile("file", params.FilePath)
if err != nil {
return StreamVideo{}, err
}
file, err := os.Open(params.FilePath)
if err != nil {
return StreamVideo{}, err
}
if _, err := io.Copy(formFile, file); err != nil {
return StreamVideo{}, err
}
if err := writer.Close(); err != nil {
return StreamVideo{}, err
}
res, err := api.makeRequestContextWithHeaders(ctx, http.MethodPost, uri, body, http.Header{
"Accept": []string{"application/json"},
"Content-Type": []string{writer.FormDataContentType()},
})
if err != nil {
return StreamVideo{}, err
}
var streamVideoResponse StreamVideoResponse
if err := json.Unmarshal(res, &streamVideoResponse); err != nil {
return StreamVideo{}, err
}
return streamVideoResponse.Result, nil
}
// StreamCreateVideoDirectURL creates a video and returns an authenticated URL.
//
// API Reference: https://api.cloudflare.com/#stream-videos-create-a-video-and-get-authenticated-direct-upload-url
func (api *API) StreamCreateVideoDirectURL(ctx context.Context, params StreamCreateVideoParameters) (StreamVideoCreate, error) {
if params.AccountID == "" {
return StreamVideoCreate{}, ErrMissingAccountID
}
if params.MaxDurationSeconds == 0 {
return StreamVideoCreate{}, ErrMissingMaxDuration
}
uri := fmt.Sprintf("/accounts/%s/stream/direct_upload", params.AccountID)
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
if err != nil {
return StreamVideoCreate{}, err
}
var streamVideoCreateResponse StreamVideoCreateResponse
if err := json.Unmarshal(res, &streamVideoCreateResponse); err != nil {
return StreamVideoCreate{}, err
}
return streamVideoCreateResponse.Result, nil
}
// StreamListVideos list videos currently in stream.
//
// API reference: https://api.cloudflare.com/#stream-videos-list-videos
func (api *API) StreamListVideos(ctx context.Context, params StreamListParameters) ([]StreamVideo, error) {
if params.AccountID == "" {
return []StreamVideo{}, ErrMissingAccountID
}
uri := buildURI(fmt.Sprintf("/accounts/%s/stream", params.AccountID), params)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return []StreamVideo{}, err
}
var streamListResponse StreamListResponse
if err := json.Unmarshal(res, &streamListResponse); err != nil {
return []StreamVideo{}, err
}
return streamListResponse.Result, nil
}
// StreamInitiateTUSVideoUpload generates a direct upload TUS url for a video.
//
// API Reference: https://developers.cloudflare.com/api/operations/stream-videos-initiate-video-uploads-using-tus
func (api *API) StreamInitiateTUSVideoUpload(ctx context.Context, rc *ResourceContainer, params StreamInitiateTUSUploadParameters) (StreamInitiateTUSUploadResponse, error) {
if rc.Level != AccountRouteLevel {
return StreamInitiateTUSUploadResponse{}, ErrRequiredAccountLevelResourceContainer
}
headers := http.Header{}
if params.TusResumable == "" {
return StreamInitiateTUSUploadResponse{}, ErrMissingTusResumable
} else if params.TusResumable != TusProtocolVersion1_0_0 {
return StreamInitiateTUSUploadResponse{}, ErrInvalidTusResumable
} else {
headers.Set("Tus-Resumable", string(params.TusResumable))
}
if params.UploadLength == 0 {
return StreamInitiateTUSUploadResponse{}, ErrMissingUploadLength
} else {
headers.Set("Upload-Length", strconv.FormatInt(params.UploadLength, 10))
}
if params.UploadCreator != "" {
headers.Set("Upload-Creator", params.UploadCreator)
}
metadataTusCsv, err := params.Metadata.ToTUSCsv()
if err != nil {
return StreamInitiateTUSUploadResponse{}, ErrMarshallingTUSMetadata
}
if metadataTusCsv != "" {
headers.Set("Upload-Metadata", metadataTusCsv)
}
uri := buildURI(fmt.Sprintf("/accounts/%s/stream", rc.Identifier), params)
res, err := api.makeRequestWithAuthTypeAndHeadersComplete(ctx, http.MethodPost, uri, nil, api.authType, headers)
if err != nil {
return StreamInitiateTUSUploadResponse{}, err
}
if res.StatusCode != http.StatusCreated {
return StreamInitiateTUSUploadResponse{}, ErrInvalidStatusCode
}
return StreamInitiateTUSUploadResponse{ResponseHeaders: res.Headers}, nil
}
// StreamGetVideo gets the details for a specific video.
//
// API Reference: https://api.cloudflare.com/#stream-videos-video-details
func (api *API) StreamGetVideo(ctx context.Context, options StreamParameters) (StreamVideo, error) {
if options.AccountID == "" {
return StreamVideo{}, ErrMissingAccountID
}
if options.VideoID == "" {
return StreamVideo{}, ErrMissingVideoID
}
uri := fmt.Sprintf("/accounts/%s/stream/%s", options.AccountID, options.VideoID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return StreamVideo{}, err
}
var streamVideoResponse StreamVideoResponse
if err := json.Unmarshal(res, &streamVideoResponse); err != nil {
return StreamVideo{}, err
}
return streamVideoResponse.Result, nil
}
// StreamEmbedHTML gets an HTML fragment to embed on a web page.
//
// API Reference: https://api.cloudflare.com/#stream-videos-embed-code-html
func (api *API) StreamEmbedHTML(ctx context.Context, options StreamParameters) (string, error) {
if options.AccountID == "" {
return "", ErrMissingAccountID
}
if options.VideoID == "" {
return "", ErrMissingVideoID
}
uri := fmt.Sprintf("/accounts/%s/stream/%s/embed", options.AccountID, options.VideoID)
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
return "", err
}
return string(res), nil
}
// StreamDeleteVideo deletes a video.
//
// API Reference: https://api.cloudflare.com/#stream-videos-delete-video
func (api *API) StreamDeleteVideo(ctx context.Context, options StreamParameters) error {
if options.AccountID == "" {
return ErrMissingAccountID
}
if options.VideoID == "" {
return ErrMissingVideoID
}
uri := fmt.Sprintf("/accounts/%s/stream/%s", options.AccountID, options.VideoID)
if _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil); err != nil {
return err
}
return nil
}
// StreamAssociateNFT associates a video to a token and contract address.
//
// API Reference: https://api.cloudflare.com/#stream-videos-associate-video-to-an-nft
func (api *API) StreamAssociateNFT(ctx context.Context, options StreamVideoNFTParameters) (StreamVideo, error) {
if options.AccountID == "" {
return StreamVideo{}, ErrMissingAccountID
}
if options.VideoID == "" {
return StreamVideo{}, ErrMissingVideoID
}
uri := fmt.Sprintf("/accounts/%s/stream/%s", options.AccountID, options.VideoID)
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, options)
if err != nil {
return StreamVideo{}, err
}
var streamVideoResponse StreamVideoResponse
if err := json.Unmarshal(res, &streamVideoResponse); err != nil {
return StreamVideo{}, err
}
return streamVideoResponse.Result, nil
}
// StreamCreateSignedURL creates a signed URL token for a video.
//
// API Reference: https://api.cloudflare.com/#stream-videos-associate-video-to-an-nft
func (api *API) StreamCreateSignedURL(ctx context.Context, params StreamSignedURLParameters) (string, error) {
if params.AccountID == "" {
return "", ErrMissingAccountID
}
if params.VideoID == "" {
return "", ErrMissingVideoID
}
uri := fmt.Sprintf("/accounts/%s/stream/%s/token", params.AccountID, params.VideoID)
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
if err != nil {
return "", err
}
var streamSignedResponse StreamSignedURLResponse
if err := json.Unmarshal(res, &streamSignedResponse); err != nil {
return "", err
}
return streamSignedResponse.Result.Token, nil
}