diff --git a/api2_generated_models_test.go b/api2_generated_models_test.go
new file mode 100644
index 0000000..1076144
--- /dev/null
+++ b/api2_generated_models_test.go
@@ -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)
+ }
+}
diff --git a/assembly.go b/assembly.go
index bc7ce92..a037b8d 100644
--- a/assembly.go
+++ b/assembly.go
@@ -1,13 +1,17 @@
package transloadit
import (
+ "bytes"
"context"
+ "encoding/base64"
"fmt"
"io"
"mime/multipart"
"net/http"
+ "net/url"
"os"
"strconv"
+ "strings"
"time"
)
@@ -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"`
@@ -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"`
}
@@ -233,6 +241,200 @@ func (client *Client) StartAssembly(ctx context.Context, assembly Assembly) (*As
return &info, err
}
+//
+
+// 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
+}
+
+//
+
+//
+
+// 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.
+
+// ResumeTusUpload resumes an interrupted TUS upload from the server-reported offset and waits for the Assembly to finish.
+func (client *Client) ResumeTusUpload(ctx context.Context, uploadUrl string, content []byte, assembly *AssemblyInfo) (*AssemblyInfo, error) {
+ storedUploadURL, err := url.Parse(uploadUrl)
+ if err != nil {
+ return nil, err
+ }
+
+ offsetRequest, err := http.NewRequestWithContext(ctx, "HEAD", storedUploadURL.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ offsetRequest.Header.Set("Tus-Resumable", "1.0.0")
+
+ offsetResponse, err := client.httpClient.Do(offsetRequest)
+ if err != nil {
+ return nil, err
+ }
+ defer offsetResponse.Body.Close()
+ if offsetResponse.StatusCode != 200 {
+ return nil, fmt.Errorf("TUS offset returned HTTP %d, expected 200", offsetResponse.StatusCode)
+ }
+ resumeOffsetHeader := offsetResponse.Header.Get("Upload-Offset")
+ if resumeOffsetHeader == "" {
+ return nil, fmt.Errorf("TUS offset did not return a Upload-Offset header")
+ }
+ resumeOffset, err := strconv.Atoi(resumeOffsetHeader)
+ if err != nil {
+ return nil, fmt.Errorf("TUS offset returned an invalid Upload-Offset header")
+ }
+
+ uploadRequest, err := http.NewRequestWithContext(ctx, "PATCH", storedUploadURL.String(), bytes.NewReader(content[resumeOffset:]))
+ if err != nil {
+ return nil, err
+ }
+ uploadRequest.Header.Set("Tus-Resumable", "1.0.0")
+ uploadRequest.Header.Set("Upload-Offset", strconv.Itoa(resumeOffset))
+ 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))
+ }
+
+ completedAssembly, err := client.WaitForAssembly(ctx, assembly)
+ if err != nil {
+ return nil, err
+ }
+
+ return completedAssembly, nil
+}
+
+//
+
+//
+
+// 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.AssemblySSLURL
+ 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
+}
+
+//
+
func (assembly *Assembly) makeRequest(ctx context.Context, client *Client) (*http.Request, error) {
// TODO: test with huge files
url := client.config.Endpoint + "/assemblies"
@@ -306,6 +508,12 @@ func (assembly *Assembly) makeRequest(ctx context.Context, client *Client) (*htt
return req, nil
}
+//
+
+// 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
@@ -316,6 +524,14 @@ func (client *Client) GetAssembly(ctx context.Context, assemblyURL string) (*Ass
return &info, err
}
+//
+
+//
+
+// 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.
@@ -328,6 +544,8 @@ func (client *Client) CancelAssembly(ctx context.Context, assemblyURL string) (*
return &info, err
}
+//
+
// 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:
@@ -375,6 +593,12 @@ func (client *Client) StartAssemblyReplay(ctx context.Context, assembly Assembly
return &info, nil
}
+//
+
+// 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
@@ -382,3 +606,5 @@ func (client *Client) ListAssemblies(ctx context.Context, options *ListOptions)
return assemblies, err
}
+
+//
diff --git a/assembly_test.go b/assembly_test.go
index a755514..b61ce30 100644
--- a/assembly_test.go
+++ b/assembly_test.go
@@ -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")
+ }
+}
diff --git a/examples/api2-devdock-assembly-lifecycle/main.go b/examples/api2-devdock-assembly-lifecycle/main.go
new file mode 100644
index 0000000..4804ee5
--- /dev/null
+++ b/examples/api2-devdock-assembly-lifecycle/main.go
@@ -0,0 +1,157 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "time"
+
+ transloadit "github.com/transloadit/go-sdk"
+)
+
+type assemblyLifecycleScenario struct {
+ Assembly struct {
+ FileCount int `json:"fileCount"`
+ } `json:"assembly"`
+ List struct {
+ PageSize int `json:"pageSize"`
+ } `json:"list"`
+ ScenarioID string `json:"scenarioId"`
+}
+
+func requiredEnv(name string) string {
+ value := os.Getenv(name)
+ if value == "" {
+ panic(fmt.Sprintf("%s must be set", name))
+ }
+
+ return value
+}
+
+func fail(format string, args ...interface{}) {
+ panic(fmt.Sprintf(format, args...))
+}
+
+func loadScenario() (assemblyLifecycleScenario, error) {
+ scenarioPath := os.Getenv("API2_SDK_EXAMPLE_SCENARIO")
+ if scenarioPath == "" {
+ scenarioPath = filepath.Join("examples", "api2-devdock-assembly-lifecycle", "api2-scenario.json")
+ }
+
+ contents, err := ioutil.ReadFile(scenarioPath)
+ if err != nil {
+ return assemblyLifecycleScenario{}, err
+ }
+
+ var scenario assemblyLifecycleScenario
+ if err := json.Unmarshal(contents, &scenario); err != nil {
+ return assemblyLifecycleScenario{}, err
+ }
+
+ return scenario, nil
+}
+
+func assemblyResult(info *transloadit.AssemblyInfo) map[string]interface{} {
+ return map[string]interface{}{
+ "assemblyId": info.AssemblyID,
+ "assemblySslUrl": info.AssemblySSLURL,
+ "assemblyUrl": info.AssemblyURL,
+ "ok": info.Ok,
+ }
+}
+
+func writeResult(result map[string]interface{}) error {
+ resultPath := os.Getenv("API2_SDK_EXAMPLE_RESULT")
+ if resultPath == "" {
+ return nil
+ }
+
+ contents, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return err
+ }
+
+ return ioutil.WriteFile(resultPath, append(contents, '\n'), 0o644)
+}
+
+func main() {
+ ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
+ defer cancel()
+
+ scenario, err := loadScenario()
+ if err != nil {
+ fail("load scenario: %v", err)
+ }
+
+ client := transloadit.NewClient(transloadit.Config{
+ AuthKey: requiredEnv("TRANSLOADIT_KEY"),
+ AuthSecret: requiredEnv("TRANSLOADIT_SECRET"),
+ Endpoint: requiredEnv("TRANSLOADIT_ENDPOINT"),
+ })
+
+ created, err := client.CreateTusAssembly(ctx, scenario.Assembly.FileCount)
+ if err != nil {
+ fail("create TUS assembly: %v", err)
+ }
+
+ cancelOnExit := true
+ defer func() {
+ if cancelOnExit {
+ _, _ = client.CancelAssembly(context.Background(), created.AssemblySSLURL)
+ }
+ }()
+
+ fetched, err := client.GetAssembly(ctx, created.AssemblySSLURL)
+ if err != nil {
+ fail("get assembly: %v", err)
+ }
+
+ // The Assembly list is eventually consistent: the API acknowledges creation before the
+ // list storage row lands, so poll briefly until the created Assembly shows up.
+ var assemblies transloadit.AssemblyList
+ listContainsCreated := false
+ for attempt := 0; attempt < 20; attempt++ {
+ assemblies, err = client.ListAssemblies(ctx, &transloadit.ListOptions{
+ AssemblyID: created.AssemblyID,
+ PageSize: scenario.List.PageSize,
+ })
+ if err != nil {
+ fail("list assemblies: %v", err)
+ }
+
+ for _, assembly := range assemblies.Assemblies {
+ if assembly.AssemblyID == created.AssemblyID {
+ listContainsCreated = true
+ }
+ }
+ if listContainsCreated {
+ break
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+
+ cancelled, err := client.CancelAssembly(ctx, created.AssemblySSLURL)
+ if err != nil {
+ fail("cancel assembly: %v", err)
+ }
+ cancelOnExit = false
+
+ if err := writeResult(map[string]interface{}{
+ "cancelled": assemblyResult(cancelled),
+ "created": assemblyResult(created),
+ "fetched": assemblyResult(fetched),
+ "listContainsCreated": listContainsCreated,
+ "listCount": assemblies.Count,
+ }); err != nil {
+ fail("write result: %v", err)
+ }
+
+ fmt.Printf(
+ "Go Transloadit SDK devdock scenario %s canceled Assembly %s\n",
+ scenario.ScenarioID,
+ created.AssemblyID,
+ )
+}
diff --git a/examples/api2-devdock-template-lifecycle/main.go b/examples/api2-devdock-template-lifecycle/main.go
new file mode 100644
index 0000000..7bc0451
--- /dev/null
+++ b/examples/api2-devdock-template-lifecycle/main.go
@@ -0,0 +1,226 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "time"
+
+ transloadit "github.com/transloadit/go-sdk"
+)
+
+type scenarioContent struct {
+ AdditionalProperties map[string]interface{} `json:"additionalProperties"`
+ Steps map[string]map[string]interface{} `json:"steps"`
+}
+
+type templateLifecycleScenario struct {
+ Delete struct {
+ ErrorCodeIncludes string `json:"errorCodeIncludes"`
+ } `json:"delete"`
+ List struct {
+ MinimumCount int `json:"minimumCount"`
+ PageSize int `json:"pageSize"`
+ } `json:"list"`
+ ScenarioID string `json:"scenarioId"`
+ Template struct {
+ Content scenarioContent `json:"content"`
+ NamePrefix string `json:"namePrefix"`
+ RequireSignatureAuth bool `json:"requireSignatureAuth"`
+ } `json:"template"`
+ Update struct {
+ Content scenarioContent `json:"content"`
+ NameSuffix string `json:"nameSuffix"`
+ RequireSignatureAuth bool `json:"requireSignatureAuth"`
+ } `json:"update"`
+}
+
+func requiredEnv(name string) string {
+ value := os.Getenv(name)
+ if value == "" {
+ panic(fmt.Sprintf("%s must be set", name))
+ }
+
+ return value
+}
+
+func fail(format string, args ...interface{}) {
+ panic(fmt.Sprintf(format, args...))
+}
+
+func loadScenario() (templateLifecycleScenario, error) {
+ scenarioPath := os.Getenv("API2_SDK_EXAMPLE_SCENARIO")
+ if scenarioPath == "" {
+ scenarioPath = filepath.Join(
+ "examples",
+ "api2-devdock-template-lifecycle",
+ "api2-scenario.json",
+ )
+ }
+
+ contents, err := ioutil.ReadFile(scenarioPath)
+ if err != nil {
+ return templateLifecycleScenario{}, err
+ }
+
+ var scenario templateLifecycleScenario
+ if err := json.Unmarshal(contents, &scenario); err != nil {
+ return templateLifecycleScenario{}, err
+ }
+
+ return scenario, nil
+}
+
+func applyTemplateContent(template *transloadit.Template, content scenarioContent) {
+ for stepName, step := range content.Steps {
+ template.AddStep(stepName, step)
+ }
+
+ for name, value := range content.AdditionalProperties {
+ template.Content.AdditionalProperties[name] = value
+ }
+}
+
+func newTemplate(name string, requireSignatureAuth bool, content scenarioContent) transloadit.Template {
+ template := transloadit.NewTemplate()
+ template.Name = name
+ template.RequireSignatureAuth = requireSignatureAuth
+ applyTemplateContent(&template, content)
+
+ return template
+}
+
+func main() {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ scenario, err := loadScenario()
+ if err != nil {
+ fail("load scenario: %v", err)
+ }
+
+ client := transloadit.NewClient(transloadit.Config{
+ AuthKey: requiredEnv("TRANSLOADIT_KEY"),
+ AuthSecret: requiredEnv("TRANSLOADIT_SECRET"),
+ Endpoint: requiredEnv("TRANSLOADIT_ENDPOINT"),
+ })
+
+ templateName := fmt.Sprintf("%s-%d", scenario.Template.NamePrefix, time.Now().UnixNano())
+ template := newTemplate(
+ templateName,
+ scenario.Template.RequireSignatureAuth,
+ scenario.Template.Content,
+ )
+
+ templateID, err := client.CreateTemplate(ctx, template)
+ if err != nil {
+ fail("create template: %v", err)
+ }
+ if templateID == "" {
+ fail("create template returned an empty id")
+ }
+
+ deleteTemplate := true
+ defer func() {
+ if deleteTemplate {
+ _ = client.DeleteTemplate(context.Background(), templateID)
+ }
+ }()
+
+ fetched, err := client.GetTemplate(ctx, templateID)
+ if err != nil {
+ fail("get template: %v", err)
+ }
+
+ templateList, err := client.ListTemplates(ctx, &transloadit.ListOptions{
+ PageSize: scenario.List.PageSize,
+ })
+ if err != nil {
+ fail("list templates: %v", err)
+ }
+
+ updatedTemplate := newTemplate(
+ templateName+scenario.Update.NameSuffix,
+ scenario.Update.RequireSignatureAuth,
+ scenario.Update.Content,
+ )
+
+ if err := client.UpdateTemplate(ctx, templateID, updatedTemplate); err != nil {
+ fail("update template: %v", err)
+ }
+
+ fetchedUpdated, err := client.GetTemplate(ctx, templateID)
+ if err != nil {
+ fail("get updated template: %v", err)
+ }
+
+ if err := client.DeleteTemplate(ctx, templateID); err != nil {
+ fail("delete template: %v", err)
+ }
+ deleteTemplate = false
+
+ _, err = client.GetTemplate(ctx, templateID)
+ deletedGetSucceeded := err == nil
+ deletedErrorCode := ""
+ var requestErr transloadit.RequestError
+ if err != nil && !errors.As(err, &requestErr) {
+ fail("get deleted template returned %T, expected transloadit.RequestError", err)
+ }
+ if err != nil {
+ deletedErrorCode = requestErr.Code
+ }
+
+ result := map[string]interface{}{
+ "deletedErrorCode": deletedErrorCode,
+ "deletedGetSucceeded": deletedGetSucceeded,
+ "fetched": templateResult(fetched),
+ "listCount": templateList.Count,
+ "templateId": templateID,
+ "templateName": templateName,
+ "updated": templateResult(fetchedUpdated),
+ "updatedTemplateName": updatedTemplate.Name,
+ }
+ if err := writeResult(result); err != nil {
+ fail("write result: %v", err)
+ }
+
+ fmt.Printf(
+ "Go Transloadit SDK devdock scenario %s passed for %s\n",
+ scenario.ScenarioID,
+ requiredEnv("TRANSLOADIT_ENDPOINT"),
+ )
+}
+
+func templateResult(template transloadit.Template) map[string]interface{} {
+ content := map[string]interface{}{
+ "steps": template.Content.Steps,
+ }
+ for name, value := range template.Content.AdditionalProperties {
+ content[name] = value
+ }
+
+ return map[string]interface{}{
+ "content": content,
+ "id": template.ID,
+ "name": template.Name,
+ "requireSignatureAuth": template.RequireSignatureAuth,
+ }
+}
+
+func writeResult(result map[string]interface{}) error {
+ resultPath := os.Getenv("API2_SDK_EXAMPLE_RESULT")
+ if resultPath == "" {
+ return nil
+ }
+
+ contents, err := json.MarshalIndent(result, "", " ")
+ if err != nil {
+ return err
+ }
+
+ return ioutil.WriteFile(resultPath, append(contents, '\n'), 0o644)
+}
diff --git a/examples/api2-devdock-tus-assembly/main.go b/examples/api2-devdock-tus-assembly/main.go
new file mode 100644
index 0000000..c3996d7
--- /dev/null
+++ b/examples/api2-devdock-tus-assembly/main.go
@@ -0,0 +1,152 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "time"
+
+ transloadit "github.com/transloadit/go-sdk"
+)
+
+type tusAssemblyScenario struct {
+ ExampleInput struct {
+ ScenarioID string `json:"scenarioId"`
+ SdkFeatureInputs struct {
+ UploadTusAssembly uploadTusAssemblyInput `json:"uploadTusAssembly"`
+ } `json:"sdkFeatureInputs"`
+ } `json:"exampleInput"`
+}
+
+type uploadTusAssemblyInput struct {
+ FileCount int `json:"file_count"`
+ Upload uploadConfig `json:"upload"`
+}
+
+type uploadConfig struct {
+ Content string `json:"content"`
+ Field string `json:"fieldname"`
+ Filename string `json:"filename"`
+ UserMeta map[string]string `json:"user_meta"`
+}
+
+func requiredEnv(name string) string {
+ value := os.Getenv(name)
+ if value == "" {
+ panic(fmt.Sprintf("%s must be set", name))
+ }
+
+ return value
+}
+
+func fail(format string, args ...interface{}) {
+ panic(fmt.Sprintf(format, args...))
+}
+
+func loadScenario() (tusAssemblyScenario, error) {
+ scenarioPath := os.Getenv("API2_SDK_EXAMPLE_SCENARIO")
+ if scenarioPath == "" {
+ scenarioPath = filepath.Join("examples", "api2-devdock-tus-assembly", "api2-scenario.json")
+ }
+
+ contents, err := ioutil.ReadFile(scenarioPath)
+ if err != nil {
+ return tusAssemblyScenario{}, err
+ }
+
+ var scenario tusAssemblyScenario
+ if err := json.Unmarshal(contents, &scenario); err != nil {
+ return tusAssemblyScenario{}, err
+ }
+
+ return scenario, nil
+}
+
+func asJsonObject(value interface{}, label string) (map[string]interface{}, error) {
+ contents, err := json.Marshal(value)
+ if err != nil {
+ return nil, err
+ }
+
+ var result map[string]interface{}
+ if err := json.Unmarshal(contents, &result); err != nil {
+ return nil, err
+ }
+
+ return result, nil
+}
+
+func writeResult(
+ status map[string]interface{},
+ uploadURL string,
+) error {
+ resultPath := os.Getenv("API2_SDK_EXAMPLE_RESULT")
+ if resultPath == "" {
+ return nil
+ }
+
+ contents, err := json.MarshalIndent(
+ map[string]interface{}{
+ "createResponse": status,
+ "status": status,
+ "uploadUrl": uploadURL,
+ },
+ "",
+ " ",
+ )
+ if err != nil {
+ return err
+ }
+
+ return ioutil.WriteFile(resultPath, append(contents, '\n'), 0o644)
+}
+
+func main() {
+ ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
+ defer cancel()
+
+ scenario, err := loadScenario()
+ if err != nil {
+ fail("load scenario: %v", err)
+ }
+ input := scenario.ExampleInput.SdkFeatureInputs.UploadTusAssembly
+
+ client := transloadit.NewClient(transloadit.Config{
+ AuthKey: requiredEnv("TRANSLOADIT_KEY"),
+ AuthSecret: requiredEnv("TRANSLOADIT_SECRET"),
+ Endpoint: requiredEnv("TRANSLOADIT_ENDPOINT"),
+ })
+
+ userMeta := input.Upload.UserMeta
+ if userMeta == nil {
+ userMeta = map[string]string{}
+ }
+
+ statusInfo, uploadURL, err := client.UploadTusAssembly(
+ ctx,
+ input.FileCount,
+ []byte(input.Upload.Content),
+ input.Upload.Field,
+ input.Upload.Filename,
+ userMeta,
+ )
+ if err != nil {
+ fail("upload TUS assembly: %v", err)
+ }
+ status, err := asJsonObject(statusInfo, "assembly status")
+ if err != nil {
+ fail("serialize assembly status: %v", err)
+ }
+ if err := writeResult(status, uploadURL); err != nil {
+ fail("write result: %v", err)
+ }
+
+ fmt.Printf(
+ "Go Transloadit SDK devdock scenario %s uploaded to %s\n",
+ scenario.ExampleInput.ScenarioID,
+ uploadURL,
+ )
+}
diff --git a/examples/api2-devdock-tus-resume-upload/main.go b/examples/api2-devdock-tus-resume-upload/main.go
new file mode 100644
index 0000000..7d25bd8
--- /dev/null
+++ b/examples/api2-devdock-tus-resume-upload/main.go
@@ -0,0 +1,329 @@
+// Run the API2 contract TUS resume scenario against a devdock API2 server.
+//
+// This example is intentionally checked into the SDK repository: it reads the
+// API/TUS facts from API2's injected scenario JSON, interrupts an upload like
+// an unlucky user would, and resumes it through the public SDK method.
+package main
+
+import (
+ "bytes"
+ "context"
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ transloadit "github.com/transloadit/go-sdk"
+)
+
+type resumeUploadScenario struct {
+ ExampleInput struct {
+ ScenarioID string `json:"scenarioId"`
+ } `json:"exampleInput"`
+ Prepared struct {
+ CreateResponse map[string]interface{} `json:"createResponse"`
+ } `json:"prepared"`
+ Upload struct {
+ Metadata []metadataField `json:"metadata"`
+ Resume resumePlan `json:"resume"`
+ Source uploadSource `json:"source"`
+ TusURL valueSpec `json:"tusUrl"`
+ } `json:"upload"`
+}
+
+type metadataField struct {
+ Name string `json:"name"`
+ Value valueSpec `json:"value"`
+}
+
+type resumePlan struct {
+ Fingerprint string `json:"fingerprint"`
+ RemoveFingerprintOnSuccess bool `json:"removeFingerprintOnSuccess"`
+ StopAfterAcceptedBytes int `json:"stopAfterAcceptedBytes"`
+}
+
+type uploadSource struct {
+ Encoding string `json:"encoding"`
+ Kind string `json:"kind"`
+ Value string `json:"value"`
+}
+
+type valueSpec struct {
+ Source *valueSpecSource `json:"source"`
+ Value interface{} `json:"value"`
+}
+
+type valueSpecSource struct {
+ Path []string `json:"path"`
+ Root string `json:"root"`
+}
+
+func requiredEnv(name string) string {
+ value := os.Getenv(name)
+ if value == "" {
+ panic(fmt.Sprintf("%s must be set", name))
+ }
+
+ return value
+}
+
+func fail(format string, args ...interface{}) {
+ panic(fmt.Sprintf(format, args...))
+}
+
+func loadScenario() (resumeUploadScenario, map[string]interface{}, error) {
+ scenarioPath := os.Getenv("API2_SDK_EXAMPLE_SCENARIO")
+ if scenarioPath == "" {
+ scenarioPath = filepath.Join("examples", "api2-devdock-tus-resume-upload", "api2-scenario.json")
+ }
+
+ contents, err := ioutil.ReadFile(scenarioPath)
+ if err != nil {
+ return resumeUploadScenario{}, nil, err
+ }
+
+ var scenario resumeUploadScenario
+ if err := json.Unmarshal(contents, &scenario); err != nil {
+ return resumeUploadScenario{}, nil, err
+ }
+
+ var rawScenario map[string]interface{}
+ if err := json.Unmarshal(contents, &rawScenario); err != nil {
+ return resumeUploadScenario{}, nil, err
+ }
+
+ return scenario, rawScenario, nil
+}
+
+func resolveValue(spec valueSpec, context map[string]interface{}, label string) interface{} {
+ if spec.Source == nil {
+ return spec.Value
+ }
+
+ current, ok := context[spec.Source.Root]
+ if !ok {
+ fail("%s value source root is unavailable", label)
+ }
+ for _, part := range spec.Source.Path {
+ record, ok := current.(map[string]interface{})
+ if !ok {
+ fail("%s value source cannot read %s", label, part)
+ }
+ current, ok = record[part]
+ if !ok {
+ fail("%s value source cannot read %s", label, part)
+ }
+ }
+
+ return current
+}
+
+func resolveString(spec valueSpec, context map[string]interface{}, label string) string {
+ value, ok := resolveValue(spec, context, label).(string)
+ if !ok {
+ fail("%s must be a string", label)
+ }
+
+ return value
+}
+
+func scenarioBytes(source uploadSource) []byte {
+ if source.Kind != "bytes" {
+ fail("upload.source.kind must be bytes")
+ }
+ if source.Encoding != "utf8" {
+ fail("upload.source.encoding must be utf8")
+ }
+
+ return []byte(source.Value)
+}
+
+func uploadMetadata(fields []metadataField, context map[string]interface{}) map[string]string {
+ metadata := make(map[string]string, len(fields))
+ for _, field := range fields {
+ metadata[field.Name] = fmt.Sprintf("%v", resolveValue(field.Value, context, field.Name))
+ }
+
+ return metadata
+}
+
+// createInterruptedUpload creates a TUS upload and only sends the first chunk,
+// leaving the upload interrupted the way a dropped connection would.
+func createInterruptedUpload(
+ ctx context.Context,
+ tusURL string,
+ content []byte,
+ metadata map[string]string,
+ stopAfterAcceptedBytes int,
+) string {
+ metadataNames := make([]string, 0, len(metadata))
+ for name := range metadata {
+ metadataNames = append(metadataNames, name)
+ }
+ sort.Strings(metadataNames)
+ metadataParts := make([]string, 0, len(metadata))
+ for _, name := range metadataNames {
+ encodedValue := base64.StdEncoding.EncodeToString([]byte(metadata[name]))
+ metadataParts = append(metadataParts, fmt.Sprintf("%s %s", name, encodedValue))
+ }
+
+ createRequest, err := http.NewRequestWithContext(ctx, "POST", tusURL, nil)
+ if err != nil {
+ fail("TUS create request: %v", err)
+ }
+ createRequest.Header.Set("Tus-Resumable", "1.0.0")
+ createRequest.Header.Set("Upload-Length", strconv.Itoa(len(content)))
+ createRequest.Header.Set("Upload-Metadata", strings.Join(metadataParts, ","))
+ createResponse, err := http.DefaultClient.Do(createRequest)
+ if err != nil {
+ fail("TUS create request failed: %v", err)
+ }
+ defer createResponse.Body.Close()
+ if createResponse.StatusCode != 201 {
+ fail("TUS create returned HTTP %d, expected 201", createResponse.StatusCode)
+ }
+ location := createResponse.Header.Get("Location")
+ if location == "" {
+ fail("TUS create did not return a Location header")
+ }
+ tusBase, err := url.Parse(tusURL)
+ if err != nil {
+ fail("parse TUS URL: %v", err)
+ }
+ uploadURL, err := tusBase.Parse(location)
+ if err != nil {
+ fail("resolve upload URL: %v", err)
+ }
+ uploadURLText := uploadURL.String()
+
+ patchRequest, err := http.NewRequestWithContext(
+ ctx,
+ "PATCH",
+ uploadURLText,
+ bytes.NewReader(content[:stopAfterAcceptedBytes]),
+ )
+ if err != nil {
+ fail("TUS first chunk request: %v", err)
+ }
+ patchRequest.Header.Set("Tus-Resumable", "1.0.0")
+ patchRequest.Header.Set("Upload-Offset", "0")
+ patchRequest.Header.Set("Content-Type", "application/offset+octet-stream")
+ patchResponse, err := http.DefaultClient.Do(patchRequest)
+ if err != nil {
+ fail("TUS first chunk request failed: %v", err)
+ }
+ defer patchResponse.Body.Close()
+ if patchResponse.StatusCode != 204 {
+ fail("TUS first chunk returned HTTP %d, expected 204", patchResponse.StatusCode)
+ }
+ acceptedBytes, err := strconv.Atoi(patchResponse.Header.Get("Upload-Offset"))
+ if err != nil || acceptedBytes != stopAfterAcceptedBytes {
+ fail("TUS first chunk accepted %d bytes, expected %d", acceptedBytes, stopAfterAcceptedBytes)
+ }
+
+ return uploadURLText
+}
+
+func writeResult(
+ firstUploadURL string,
+ previousUploadCount int,
+ remainingPreviousUploadCount int,
+) error {
+ resultPath := os.Getenv("API2_SDK_EXAMPLE_RESULT")
+ if resultPath == "" {
+ return nil
+ }
+
+ contents, err := json.MarshalIndent(
+ map[string]interface{}{
+ "firstUploadUrl": firstUploadURL,
+ "previousUploadCount": previousUploadCount,
+ "remainingPreviousUploadCount": remainingPreviousUploadCount,
+ "uploadUrl": firstUploadURL,
+ },
+ "",
+ " ",
+ )
+ if err != nil {
+ return err
+ }
+
+ return ioutil.WriteFile(resultPath, append(contents, '\n'), 0o644)
+}
+
+func main() {
+ ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
+ defer cancel()
+
+ scenario, rawScenario, err := loadScenario()
+ if err != nil {
+ fail("load scenario: %v", err)
+ }
+ resume := scenario.Upload.Resume
+
+ client := transloadit.NewClient(transloadit.Config{
+ AuthKey: requiredEnv("TRANSLOADIT_KEY"),
+ AuthSecret: requiredEnv("TRANSLOADIT_SECRET"),
+ Endpoint: requiredEnv("TRANSLOADIT_ENDPOINT"),
+ })
+
+ valueContext := map[string]interface{}{
+ "createResponse": scenario.Prepared.CreateResponse,
+ "scenario": rawScenario,
+ }
+ content := scenarioBytes(scenario.Upload.Source)
+ tusURL := resolveString(scenario.Upload.TusURL, valueContext, "upload.tusUrl")
+ metadata := uploadMetadata(scenario.Upload.Metadata, valueContext)
+
+ firstUploadURL := createInterruptedUpload(
+ ctx,
+ tusURL,
+ content,
+ metadata,
+ resume.StopAfterAcceptedBytes,
+ )
+
+ // Remember the interrupted upload by fingerprint, like a TUS client URL storage would.
+ storedUploads := map[string]string{resume.Fingerprint: firstUploadURL}
+ previousUploadCount := len(storedUploads)
+
+ assemblySSLURL, ok := scenario.Prepared.CreateResponse["assembly_ssl_url"].(string)
+ if !ok || assemblySSLURL == "" {
+ fail("prepared.createResponse.assembly_ssl_url must be a string")
+ }
+ completedAssembly, err := client.ResumeTusUpload(
+ ctx,
+ storedUploads[resume.Fingerprint],
+ content,
+ &transloadit.AssemblyInfo{AssemblySSLURL: assemblySSLURL},
+ )
+ if err != nil {
+ fail("resume TUS upload: %v", err)
+ }
+ if completedAssembly.Error != "" {
+ fail("resumeTusUpload returned %s: %s", completedAssembly.Error, completedAssembly.Message)
+ }
+
+ if resume.RemoveFingerprintOnSuccess {
+ delete(storedUploads, resume.Fingerprint)
+ }
+ remainingPreviousUploadCount := len(storedUploads)
+
+ if err := writeResult(firstUploadURL, previousUploadCount, remainingPreviousUploadCount); err != nil {
+ fail("write result: %v", err)
+ }
+
+ fmt.Printf(
+ "Go Transloadit SDK devdock scenario %s resumed %s\n",
+ scenario.ExampleInput.ScenarioID,
+ firstUploadURL,
+ )
+}
diff --git a/notification.go b/notification.go
index 5e14999..0532f7a 100644
--- a/notification.go
+++ b/notification.go
@@ -34,6 +34,12 @@ func (client *Client) ListNotifications(ctx context.Context, options *ListOption
return list, errors.New("transloadit: listing assembly notifications is no longer available")
}
+//
+
+// 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.
+
// ReplayNotification instructs the endpoint to replay the notification
// corresponding to the provided assembly ID.
// If notifyURL is not empty it will override the notify URL used in the
@@ -47,3 +53,5 @@ func (client *Client) ReplayNotification(ctx context.Context, assemblyID string,
return client.request(ctx, "POST", "assembly_notifications/"+assemblyID+"/replay", params, nil)
}
+
+//
diff --git a/template.go b/template.go
index 2c4f658..8549224 100644
--- a/template.go
+++ b/template.go
@@ -146,6 +146,12 @@ func (template *Template) UnmarshalJSON(b []byte) error {
return nil
}
+//
+
+// 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.
+
// CreateTemplate will save the provided template struct as a new template
// and return the ID of the new template.
func (client *Client) CreateTemplate(ctx context.Context, template Template) (string, error) {
@@ -164,6 +170,14 @@ func (client *Client) CreateTemplate(ctx context.Context, template Template) (st
return template.ID, nil
}
+//
+
+//
+
+// 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.
+
// GetTemplate will retrieve details about the template associated with the
// provided template ID.
func (client *Client) GetTemplate(ctx context.Context, templateID string) (template Template, err error) {
@@ -171,12 +185,28 @@ func (client *Client) GetTemplate(ctx context.Context, templateID string) (templ
return template, err
}
+//
+
+//
+
+// 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.
+
// DeleteTemplate will delete the template associated with the provided
// template ID.
func (client *Client) DeleteTemplate(ctx context.Context, templateID string) error {
return client.request(ctx, "DELETE", "templates/"+templateID, nil, nil)
}
+//
+
+//
+
+// 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.
+
// UpdateTemplate will update the template associated with the provided
// template ID to match the new name and new content. Please be aware that you
// are not able to change a template's ID.
@@ -195,8 +225,18 @@ func (client *Client) UpdateTemplate(ctx context.Context, templateID string, new
return client.request(ctx, "PUT", "templates/"+templateID, content, nil)
}
+//
+
+//
+
+// 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.
+
// ListTemplates will retrieve all templates matching the criteria.
func (client *Client) ListTemplates(ctx context.Context, options *ListOptions) (list TemplateList, err error) {
err = client.listRequest(ctx, "templates", options, &list)
return list, err
}
+
+//
diff --git a/template_credentials.go b/template_credentials.go
index 5c43765..4417bac 100644
--- a/template_credentials.go
+++ b/template_credentials.go
@@ -38,6 +38,12 @@ func NewTemplateCredential() TemplateCredential {
var templateCredentialPrefix = "template_credentials"
+//
+
+// 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.
+
// CreateTemplateCredential will save the provided template credential struct to the server
// and return the ID of the new template credential.
func (client *Client) CreateTemplateCredential(ctx context.Context, templateCredential TemplateCredential) (string, error) {
@@ -53,6 +59,14 @@ func (client *Client) CreateTemplateCredential(ctx context.Context, templateCred
return response.Credential.ID, nil
}
+//
+
+//
+
+// 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.
+
// GetTemplateCredential will retrieve details about the template credential associated with the
// provided template credential ID.
func (client *Client) GetTemplateCredential(ctx context.Context, templateCredentialID string) (TemplateCredential, error) {
@@ -61,18 +75,42 @@ func (client *Client) GetTemplateCredential(ctx context.Context, templateCredent
return response.Credential, err
}
+//
+
+//
+
+// 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.
+
// DeleteTemplateCredential will delete the template credential associated with the provided
// template ID.
func (client *Client) DeleteTemplateCredential(ctx context.Context, templateCredentialID string) error {
return client.request(ctx, "DELETE", templateCredentialPrefix+"/"+templateCredentialID, nil, nil)
}
+//
+
+//
+
+// 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.
+
// ListTemplateCredential will retrieve all templates credential matching the criteria.
func (client *Client) ListTemplateCredential(ctx context.Context, options *ListOptions) (list TemplateCredentialList, err error) {
err = client.listRequest(ctx, templateCredentialPrefix, options, &list)
return list, err
}
+//
+
+//
+
+// 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.
+
// UpdateTemplateCredential will update the template credential associated with the provided
// template credential ID to match the new name and new content.
func (client *Client) UpdateTemplateCredential(ctx context.Context, templateCredentialID string, templateCredential TemplateCredential) error {
@@ -83,3 +121,5 @@ func (client *Client) UpdateTemplateCredential(ctx context.Context, templateCred
}
return client.request(ctx, "PUT", templateCredentialPrefix+"/"+templateCredentialID, content, nil)
}
+
+//
diff --git a/transloadit.go b/transloadit.go
index acdb038..cba640b 100755
--- a/transloadit.go
+++ b/transloadit.go
@@ -155,6 +155,32 @@ func (client *Client) doRequest(req *http.Request, result interface{}) error {
}
func (client *Client) request(ctx context.Context, method string, path string, content map[string]interface{}, result interface{}) error {
+ return client.requestWithFormFields(ctx, method, path, content, nil, result)
+}
+
+func formFieldValue(value interface{}) string {
+ switch typed := value.(type) {
+ case nil:
+ return ""
+ case bool:
+ return strconv.FormatBool(typed)
+ case float32:
+ return strconv.FormatFloat(float64(typed), 'f', -1, 32)
+ case float64:
+ return strconv.FormatFloat(typed, 'f', -1, 64)
+ case string:
+ return typed
+ }
+
+ serialized, err := json.Marshal(value)
+ if err == nil {
+ return string(serialized)
+ }
+
+ return fmt.Sprint(value)
+}
+
+func (client *Client) requestWithFormFields(ctx context.Context, method string, path string, content map[string]interface{}, formFields map[string]interface{}, result interface{}) error {
uri := path
// Don't add host for absolute urls
if u, err := url.Parse(path); err == nil && u.Scheme == "" {
@@ -175,6 +201,9 @@ func (client *Client) request(ctx context.Context, method string, path string, c
v := url.Values{}
v.Set("params", params)
v.Set("signature", signature)
+ for name, value := range formFields {
+ v.Set(name, formFieldValue(value))
+ }
var body io.Reader
if method == "GET" {
diff --git a/transloadit_test.go b/transloadit_test.go
index d4eb73b..61f890b 100755
--- a/transloadit_test.go
+++ b/transloadit_test.go
@@ -52,6 +52,29 @@ func TestNewClient_Success(t *testing.T) {
_ = NewClient(config)
}
+func TestFormFieldValue(t *testing.T) {
+ t.Parallel()
+
+ cases := map[string]struct {
+ input interface{}
+ expected string
+ }{
+ "bool": {input: true, expected: "true"},
+ "int": {input: 3, expected: "3"},
+ "nil": {input: nil, expected: ""},
+ "object": {input: map[string]interface{}{"field": "value"}, expected: `{"field":"value"}`},
+ "string": {input: "file", expected: "file"},
+ }
+
+ for name, tc := range cases {
+ t.Run(name, func(t *testing.T) {
+ if actual := formFieldValue(tc.input); actual != tc.expected {
+ t.Fatalf("expected %q, got %q", tc.expected, actual)
+ }
+ })
+ }
+}
+
func setup(t *testing.T) Client {
config := DefaultConfig
config.AuthKey = os.Getenv("TRANSLOADIT_KEY")
diff --git a/wait.go b/wait.go
index d7118cd..b7990a2 100644
--- a/wait.go
+++ b/wait.go
@@ -5,9 +5,14 @@ import (
"time"
)
-// WaitForAssembly fetches continuously the assembly status until it has
-// finished uploading and executing or until an assembly error occurs.
-// If you want to end this loop prematurely, you can cancel the supplied context.
+//
+
+// 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.
+
+// WaitForAssembly waits for an Assembly to finish uploading and executing.
+// Use the returned assembly_ssl_url as the assembly URL.
func (client *Client) WaitForAssembly(ctx context.Context, assembly *AssemblyInfo) (*AssemblyInfo, error) {
for {
res, err := client.GetAssembly(ctx, assembly.AssemblySSLURL)
@@ -33,3 +38,5 @@ func (client *Client) WaitForAssembly(ctx context.Context, assembly *AssemblyInf
}
}
}
+
+//