Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions api2_generated_models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package transloadit

// This file is generated from Transloadit API2 contracts. If it looks wrong,
// please report the issue instead of editing this file by hand; the source fix
// belongs in the contract generator so all SDKs stay in sync.

import (
"reflect"
"strings"
"testing"
)

func TestGeneratedApi2ContractModelFields(t *testing.T) {
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "AssemblyID", "assembly_id", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "AssemblySSLURL", "assembly_ssl_url", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "AssemblyURL", "assembly_url", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "Error", "error", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "Ok", "ok", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "Results", "results", reflect.TypeOf((*map[string][]*FileInfo)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "TUSURL", "tus_url", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(AssemblyInfo{}), "Uploads", "uploads", reflect.TypeOf((*[]*FileInfo)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(FileInfo{}), "Field", "field", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(FileInfo{}), "IsTUSFile", "is_tus_file", reflect.TypeOf((*bool)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(FileInfo{}), "Name", "name", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(FileInfo{}), "TUSUploadURL", "tus_upload_url", reflect.TypeOf((*string)(nil)).Elem())
assertGeneratedApi2ContractModelField(t, reflect.TypeOf(FileInfo{}), "UserMeta", "user_meta", reflect.TypeOf((*map[string]interface{})(nil)).Elem())
}

func assertGeneratedApi2ContractModelField(t *testing.T, modelType reflect.Type, fieldName string, jsonField string, expectedType reflect.Type) {
t.Helper()

field, ok := modelType.FieldByName(fieldName)
if !ok {
t.Fatalf("%s.%s is missing", modelType.Name(), fieldName)
}
if field.Type != expectedType {
t.Fatalf("%s.%s has type %s, expected %s", modelType.Name(), fieldName, field.Type, expectedType)
}

jsonTag := field.Tag.Get("json")
if jsonTag != jsonField && !strings.HasPrefix(jsonTag, jsonField+",") {
t.Fatalf("%s.%s has json tag %q, expected %q", modelType.Name(), fieldName, jsonTag, jsonField)
}
}
156 changes: 156 additions & 0 deletions assembly.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package transloadit

import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)

Expand Down Expand Up @@ -86,6 +90,7 @@ type AssemblyInfo struct {
ParentID string `json:"parent_id"`
AssemblyURL string `json:"assembly_url"`
AssemblySSLURL string `json:"assembly_ssl_url"`
TUSURL string `json:"tus_url"`
BytesReceived int `json:"bytes_received"`
BytesExpected Integer `json:"bytes_expected"`
StartDate string `json:"start_date"`
Expand Down Expand Up @@ -135,9 +140,12 @@ type FileInfo struct {
OriginalMd5Hash string `json:"original_md5hash"`
OriginalID string `json:"original_id"`
OriginalBasename string `json:"original_basename"`
IsTUSFile bool `json:"is_tus_file"`
TUSUploadURL string `json:"tus_upload_url"`
URL string `json:"url"`
SSLURL string `json:"ssl_url"`
Meta map[string]interface{} `json:"meta"`
UserMeta map[string]interface{} `json:"user_meta"`
Cost int `json:"cost"`
}

Expand Down Expand Up @@ -233,6 +241,130 @@ func (client *Client) StartAssembly(ctx context.Context, assembly Assembly) (*As
return &info, err
}

// <api2-generated-feature createTusAssembly>

// This block is generated from Transloadit API2 contracts. If it looks wrong,
// please report the issue instead of editing this block by hand; the source fix
// belongs in the contract generator so all SDKs stay in sync.

// CreateTusAssembly creates a TUS-ready Assembly that waits for the requested number of resumable uploads before execution continues.
func (client *Client) CreateTusAssembly(ctx context.Context, fileCount int) (*AssemblyInfo, error) {
content := map[string]interface{}{
"await": false,
"steps": map[string]interface{}{
":original": map[string]interface{}{
"output_meta": true,
"result": "debug",
"robot": "/upload/handle",
},
},
}
formFields := map[string]interface{}{
"num_expected_upload_files": fileCount,
}

var assembly AssemblyInfo
err := client.requestWithFormFields(ctx, "POST", "assemblies", content, formFields, &assembly)

return &assembly, err
}

// </api2-generated-feature createTusAssembly>

// <api2-generated-feature uploadTusAssembly>

// This block is generated from Transloadit API2 contracts. If it looks wrong,
// please report the issue instead of editing this block by hand; the source fix
// belongs in the contract generator so all SDKs stay in sync.

// UploadTusAssembly creates a TUS-ready Assembly, uploads one file with the TUS protocol, and waits for the Assembly to finish.
func (client *Client) UploadTusAssembly(ctx context.Context, fileCount int, content []byte, fieldname string, filename string, userMeta map[string]string) (*AssemblyInfo, string, error) {
createdAssembly, err := client.CreateTusAssembly(ctx, fileCount)
if err != nil {
return nil, "", err
}

endpointURL, err := url.Parse(createdAssembly.TUSURL)
if err != nil {
return nil, "", err
}

metadataMap := make(map[string]string)
for name, value := range userMeta {
metadataMap[name] = value
}
metadataMap["assembly_url"] = createdAssembly.AssemblyURL
metadataMap["fieldname"] = fieldname
metadataMap["filename"] = filename

createRequest, err := http.NewRequestWithContext(ctx, "POST", endpointURL.String(), nil)
if err != nil {
return nil, "", err
}
createRequest.Header.Set("Tus-Resumable", "1.0.0")
createRequest.Header.Set("Upload-Length", strconv.Itoa(len(content)))
metadataParts := make([]string, 0, len(metadataMap))
for name, value := range metadataMap {
metadataParts = append(metadataParts, fmt.Sprintf("%s %s", name, base64.StdEncoding.EncodeToString([]byte(value))))
}
createRequest.Header.Set("Upload-Metadata", strings.Join(metadataParts, ","))

createResponse, err := client.httpClient.Do(createRequest)
if err != nil {
return nil, "", err
}
defer createResponse.Body.Close()
if createResponse.StatusCode != 201 {
return nil, "", fmt.Errorf("TUS create returned HTTP %d, expected 201", createResponse.StatusCode)
}
uploadURLLocation := createResponse.Header.Get("Location")
if uploadURLLocation == "" {
return nil, "", fmt.Errorf("TUS create did not return a Location header")
}
uploadURL, err := endpointURL.Parse(uploadURLLocation)
if err != nil {
return nil, "", err
}
uploadURLText := uploadURL.String()

uploadRequest, err := http.NewRequestWithContext(ctx, "PATCH", uploadURLText, bytes.NewReader(content))
if err != nil {
return nil, "", err
}
uploadRequest.Header.Set("Tus-Resumable", "1.0.0")
uploadRequest.Header.Set("Upload-Offset", "0")
uploadRequest.Header.Set("Content-Type", "application/offset+octet-stream")

uploadResponse, err := client.httpClient.Do(uploadRequest)
if err != nil {
return nil, "", err
}
defer uploadResponse.Body.Close()
if uploadResponse.StatusCode != 204 {
return nil, "", fmt.Errorf("TUS upload returned HTTP %d, expected 204", uploadResponse.StatusCode)
}
uploadOffset, err := strconv.Atoi(uploadResponse.Header.Get("Upload-Offset"))
if err != nil {
return nil, "", err
}
if uploadOffset != len(content) {
return nil, "", fmt.Errorf("TUS upload offset %d, expected %d", uploadOffset, len(content))
}

createdAssemblyAssemblySSLURL := createdAssembly.AssemblySSLURL
if createdAssemblyAssemblySSLURL == "" {
return nil, "", fmt.Errorf("uploadTusAssembly needs createdAssembly.assembly_ssl_url")
}
completedAssembly, err := client.WaitForAssembly(ctx, createdAssembly)
if err != nil {
return nil, "", err
}

return completedAssembly, uploadURLText, nil
}

// </api2-generated-feature uploadTusAssembly>

func (assembly *Assembly) makeRequest(ctx context.Context, client *Client) (*http.Request, error) {
// TODO: test with huge files
url := client.config.Endpoint + "/assemblies"
Expand Down Expand Up @@ -306,6 +438,12 @@ func (assembly *Assembly) makeRequest(ctx context.Context, client *Client) (*htt
return req, nil
}

// <api2-generated-endpoint getAssemblyStatus>

// This block is generated from Transloadit API2 contracts. If it looks wrong,
// please report the issue instead of editing this block by hand; the source fix
// belongs in the contract generator so all SDKs stay in sync.

// GetAssembly fetches the full assembly status from the provided URL.
// The assembly URL must be absolute, for example:
// https://api2-amberly.transloadit.com/assemblies/15a6b3701d3811e78d7bfba4db1b053e
Expand All @@ -316,6 +454,14 @@ func (client *Client) GetAssembly(ctx context.Context, assemblyURL string) (*Ass
return &info, err
}

// </api2-generated-endpoint getAssemblyStatus>

// <api2-generated-endpoint cancelAssembly>

// This block is generated from Transloadit API2 contracts. If it looks wrong,
// please report the issue instead of editing this block by hand; the source fix
// belongs in the contract generator so all SDKs stay in sync.

// CancelAssembly cancels an assembly which will result in all corresponding
// uploads and encoding jobs to be aborted. Finally, the updated assembly
// information after the cancellation will be returned.
Expand All @@ -328,6 +474,8 @@ func (client *Client) CancelAssembly(ctx context.Context, assemblyURL string) (*
return &info, err
}

// </api2-generated-endpoint cancelAssembly>

// NewAssemblyReplay will create a new AssemblyReplay struct which can be used
// to replay an assemblie's execution using Client.StartAssemblyReplay.
// The assembly URL must be absolute, for example:
Expand Down Expand Up @@ -375,10 +523,18 @@ func (client *Client) StartAssemblyReplay(ctx context.Context, assembly Assembly
return &info, nil
}

// <api2-generated-endpoint listAssemblies>

// This block is generated from Transloadit API2 contracts. If it looks wrong,
// please report the issue instead of editing this block by hand; the source fix
// belongs in the contract generator so all SDKs stay in sync.

// ListAssemblies will fetch all assemblies matching the provided criteria.
func (client *Client) ListAssemblies(ctx context.Context, options *ListOptions) (AssemblyList, error) {
var assemblies AssemblyList
err := client.listRequest(ctx, "assemblies", options, &assemblies)

return assemblies, err
}

// </api2-generated-endpoint listAssemblies>
47 changes: 47 additions & 0 deletions assembly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,50 @@ func TestInteger_MarshalJSON(t *testing.T) {
t.Fatal("wrong default value for string")
}
}

func TestAssemblyInfo_TusFields(t *testing.T) {
t.Parallel()

var info AssemblyInfo
err := json.Unmarshal([]byte(`{
"tus_url": "https://api2.example/resumable/files/",
"uploads": [
{
"is_tus_file": true,
"tus_upload_url": "https://api2.example/resumable/files/upload-id",
"user_meta": {
"hello": "world"
}
}
],
"results": {
":original": [
{
"is_tus_file": false,
"user_meta": {
"hello": "world"
}
}
]
}
}`), &info)
if err != nil {
t.Fatal(err)
}

if info.TUSURL != "https://api2.example/resumable/files/" {
t.Fatal("wrong tus url")
}
if len(info.Uploads) != 1 || !info.Uploads[0].IsTUSFile {
t.Fatal("wrong TUS upload marker")
}
if info.Uploads[0].TUSUploadURL != "https://api2.example/resumable/files/upload-id" {
t.Fatal("wrong TUS upload url")
}
if info.Uploads[0].UserMeta["hello"] != "world" {
t.Fatal("wrong upload user meta")
}
if info.Results[":original"][0].UserMeta["hello"] != "world" {
t.Fatal("wrong result user meta")
}
}
Loading