Skip to content
Open
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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,39 @@ func main() {

For fully working examples on how to use templates, non-blocking processing and more, take a look at [`examples/`](https://github.com/transloadit/go-sdk/tree/main/examples).

## Assembly Notifications (webhooks)

If an assembly's `NotifyURL` is set, Transloadit submits the assembly's final status to that
URL once it finishes. Use `ParseAssemblyNotification` in your HTTP handler to verify the
request's signature and decode the payload:

```go
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
notification, err := transloadit.ParseAssemblyNotification(r, options.AuthSecret)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

// notification.Ok / notification.Error describe the assembly's outcome.
// notification.AssemblyInfo has the same fields as GetAssembly's result.
})
```

See [`examples/webhook`](https://github.com/transloadit/go-sdk/tree/main/examples/webhook) for a
full example.

## Bearer tokens

`Client.IssueBearerToken` exchanges your Auth Key and Auth Secret for a short-lived, scoped
bearer token:

```go
token, err := client.IssueBearerToken(context.Background(), transloadit.BearerTokenRequest{
Scope: "assemblies:read assemblies:write",
})
```

## Documentation

See <a href="https://pkg.go.dev/github.com/transloadit/go-sdk">Godoc</a> for full API documentation.
Expand Down
94 changes: 91 additions & 3 deletions assembly.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,97 @@ type AssemblyInfo struct {
// Since 7 March 2018, the user agent, IP and referer are no longer
// stored by Transloadit (see https://transloadit.com/blog/2018/03/gdpr/)
// Therefore, these properties will always hold empty strings.
ClientAgent string
ClientIp string
ClientReferer string
ClientAgent string `json:"client_agent"`
ClientIp string `json:"client_ip"`
ClientReferer string `json:"client_referer"`

// The following fields were added to match the Assembly Status schema
// published at https://api2.transloadit.com/openapi.json, which is also
// used for the notify_url webhook payload (see AssemblyNotificationPayload).
AccountID string `json:"account_id"`
AccountName string `json:"account_name"`
AccountSlug string `json:"account_slug"`
APIAuthKeyID string `json:"api_auth_key_id"`
BuildID string `json:"build_id"`
CompanionURL string `json:"companion_url"`
ExpectedTusUploads int `json:"expected_tus_uploads"`
FinishedTusUploads int `json:"finished_tus_uploads"`
IgnoredErrorCount int `json:"ignored_error_count"`
IgnoredErrors []AssemblyIgnoredError `json:"ignored_errors"`
Info map[string]interface{} `json:"info"`
Instance string `json:"instance"`
JobsQueueDuration float32 `json:"jobs_queue_duration"`
MergedParams string `json:"merged_params"`
NotifyError string `json:"notify_error"`
NotifyResponseCode Integer `json:"notify_response_code"`
NotifyResponseData string `json:"notify_response_data"`
NumInputFiles int `json:"num_input_files"`
QueueDuration float32 `json:"queue_duration"`
Region string `json:"region"`
RunningJobs []string `json:"running_jobs"`
StartedTusUploads int `json:"started_tus_uploads"`
Template string `json:"template"`
TemplateID string `json:"template_id"`
TemplateName string `json:"template_name"`
TransloaditClient string `json:"transloadit_client"`
TusUploads []TusUpload `json:"tus_uploads"`
TusURL string `json:"tus_url"`
UpdateStreamURL string `json:"update_stream_url"`
UploadMetaDataExtracted bool `json:"upload_meta_data_extracted"`
UppyServerURL string `json:"uppyserver_url"`
UsageTags string `json:"usage_tags"`
VirusName string `json:"virusname"`
Warnings []AssemblyWarning `json:"warnings"`
WebsocketURL string `json:"websocket_url"`

// The following fields are only present when Error is set, giving
// additional diagnostic detail about the processing job that failed.
// Use Error for programmatic handling; these are for troubleshooting only.
Cmd interface{} `json:"cmd"`
ExitCode Integer `json:"exitCode"`
ExitSignal string `json:"exitSignal"`
File string `json:"file"`
Headers map[string]interface{} `json:"headers"`
IsPrivateAddress bool `json:"is_private_address"`
Name string `json:"name"`
NumRetries int `json:"numRetries"`
PlaywrightErrorCode string `json:"playwright_error_code"`
Reason interface{} `json:"reason"`
Retries int `json:"retries"`
Retryable bool `json:"retryable"`
ResponseCode Integer `json:"response_code"`
Stderr string `json:"stderr"`
Stdout string `json:"stdout"`
URL string `json:"url"`
URLHost string `json:"url_host"`
}

// AssemblyIgnoredError describes a non-fatal error that occurred while
// processing an assembly but did not cause it to fail.
type AssemblyIgnoredError struct {
Error interface{} `json:"error"`
Message string `json:"message"`
Phase string `json:"phase"`
Step string `json:"step"`
}

// AssemblyWarning describes a non-fatal warning raised while processing an
// assembly.
type AssemblyWarning struct {
Level string `json:"level"`
Msg string `json:"msg"`
}

// TusUpload contains details about a single resumable (tus) upload belonging
// to an assembly.
type TusUpload struct {
Fieldname string `json:"fieldname"`
Filename string `json:"filename"`
Finished bool `json:"finished"`
Offset float64 `json:"offset"`
Size float64 `json:"size"`
UploadURL string `json:"upload_url"`
UserMeta map[string]interface{} `json:"user_meta"`
}

// FileInfo contains details about a file which was either uploaded or is the
Expand Down
59 changes: 59 additions & 0 deletions bearer_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package transloadit

import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
)

// BearerTokenRequest contains options for exchanging a Client's Auth Key and
// Auth Secret for a scoped bearer token via Client.IssueBearerToken.
type BearerTokenRequest struct {
// Scope restricts the token to a space-separated list of scopes, e.g.
// "assemblies:read assemblies:write". If empty, the token inherits all
// scopes granted to the Auth Key.
Scope string
// Audience sets the optional token audience. If empty, the deployment's
// default audience is used.
Audience string
}

// BearerToken contains a bearer token issued by the Transloadit API. Details
// about each value can be found at https://transloadit.com/docs/api/tokens/.
type BearerToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
TokenType string `json:"token_type"`
}

// IssueBearerToken exchanges the Client's Auth Key and Auth Secret for a
// scoped bearer token by calling POST /token. Unlike other requests made by
// this Client, the token endpoint is authenticated using HTTP Basic auth
// rather than the usual HMAC request signature.
func (client *Client) IssueBearerToken(ctx context.Context, tokenRequest BearerTokenRequest) (*BearerToken, error) {
form := url.Values{"grant_type": {"client_credentials"}}
if tokenRequest.Scope != "" {
form.Set("scope", tokenRequest.Scope)
}
if tokenRequest.Audience != "" {
form.Set("aud", tokenRequest.Audience)
}

req, err := http.NewRequest("POST", client.config.Endpoint+"/token", strings.NewReader(form.Encode()))
if err != nil {
return nil, fmt.Errorf("issue bearer token: %s", err)
}
req = req.WithContext(ctx)
req.SetBasicAuth(client.config.AuthKey, client.config.AuthSecret)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

var token BearerToken
if err := client.doRequest(req, &token); err != nil {
return nil, err
}

return &token, nil
}
85 changes: 85 additions & 0 deletions bearer_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package transloadit

import (
"context"
"net/http"
"net/http/httptest"
"testing"
)

func TestIssueBearerToken_SendsBasicAuthAndFormBody(t *testing.T) {
client := NewClient(Config{
AuthKey: "foo_key",
AuthSecret: "foo_secret",
})

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/token" {
t.Errorf("expected path /token, got %q", r.URL.Path)
}

user, pass, ok := r.BasicAuth()
if !ok || user != "foo_key" || pass != "foo_secret" {
t.Errorf("expected basic auth foo_key/foo_secret, got %q/%q (ok=%v)", user, pass, ok)
}

if err := r.ParseForm(); err != nil {
t.Fatal(err)
}
if got := r.Form.Get("grant_type"); got != "client_credentials" {
t.Errorf("expected grant_type=client_credentials, got %q", got)
}
if got := r.Form.Get("scope"); got != "assemblies:read" {
t.Errorf("expected scope=assemblies:read, got %q", got)
}

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"access_token":"opaque-token","expires_in":21600,"scope":"assemblies:read","token_type":"Bearer"}`))
}))
defer server.Close()

client.config.Endpoint = server.URL

token, err := client.IssueBearerToken(context.Background(), BearerTokenRequest{Scope: "assemblies:read"})
if err != nil {
t.Fatal(err)
}

if token.AccessToken != "opaque-token" {
t.Errorf("expected access token %q, got %q", "opaque-token", token.AccessToken)
}
if token.ExpiresIn != 21600 {
t.Errorf("expected expires_in 21600, got %d", token.ExpiresIn)
}
if token.TokenType != "Bearer" {
t.Errorf("expected token_type Bearer, got %q", token.TokenType)
}
}

func TestIssueBearerToken_ReturnsRequestError(t *testing.T) {
client := NewClient(Config{
AuthKey: "foo_key",
AuthSecret: "foo_secret",
})

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"GET_ACCOUNT_UNKNOWN_AUTH_KEY","message":"unknown auth key"}`))
}))
defer server.Close()

client.config.Endpoint = server.URL

_, err := client.IssueBearerToken(context.Background(), BearerTokenRequest{})
if err == nil {
t.Fatal("expected an error")
}

reqErr, ok := err.(RequestError)
if !ok {
t.Fatalf("expected RequestError, got %T: %s", err, err)
}
if reqErr.Code != "GET_ACCOUNT_UNKNOWN_AUTH_KEY" {
t.Errorf("expected error code GET_ACCOUNT_UNKNOWN_AUTH_KEY, got %q", reqErr.Code)
}
}
Loading