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..2592c88 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,130 @@ 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. + +// 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 +} + +// + func (assembly *Assembly) makeRequest(ctx context.Context, client *Client) (*http.Request, error) { // TODO: test with huge files url := client.config.Endpoint + "/assemblies" @@ -306,6 +438,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 +454,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 +474,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 +523,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 +536,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-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/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 } } } + +//