Blog post Technical, Tridion

Leveling up the Aprimo and Tridion Integration-Part 2: Upload a video file to Aprimo DAM via the API

In my previous blog in this series, you were able to see how to connect to the Aprimo DAM API by generating a token and using it to authenticate and connect to the Aprimo API. In this blog, we are advancing to the next level in our "game" by uploading a video to the Aprimo DAM via their API.

The procedure for uploading files is more complicated than it might seem. You can read more about it here in the documentation. However, the explanation in the docs leaves a lot to be desired, so hopefully, this will help simplify things and address any unanswered questions.

For any file that is smaller than 20MB, you can upload it directly to the Aprimo Upload Service as one large binary without an issue. However, files that are over 20MB must be uploaded in segments. So, to simplify life, it is easiest to always upload files in segments regardless of the file size since most assets will be high-resolution images and videos that exceed the file size constraint.

Currently, only multipart form-data content is supported. This means that you cannot simply POST binary stream to the /uploads endpoint. Instead, you must produce a proper multipart form-data request. Luckily, multipart form-data is the default format for modern HTTP clients, so crafting such a request should not be a problem.

TIP:

Uploading files in segments requires you to use the https://mydomain.aprimo.com domain, the same one that is used for retrieving the token, but still different from the API domain. Moreover, segments are uploaded to a different endpoint: /uploads/segments

Uploading files in segments consists of:

  • Prepare file segments to upload
  • Set up the upload
  • Upload each segment
  • Commit the upload or Cancel the upload if issues arise

1. Prepare the file segments to upload

To prepare the file for segmented upload, we need to read the file from the file system (e.g., when the upload button is clicked in our application), then split the binary file (bytes) into chunks; I decided to use 15MB chunk size. Here is some example code that chunks our binary file from its byte stream:

const int MAX_BUFFER = 16485760; //15MB 

Stream fileContentStream = inputSelectFile.FileContent;
byte[] buffer = new byte[MAX_BUFFER]; 
int bytesRead, index = 0;
using (BufferedStream bs = new BufferedStream(fileContentStream))
{
    while ((bytesRead = bs.Read(buffer, 0, MAX_BUFFER)) != 0) //reading 15mb chunks at a time
    {…

2. Set up the upload

This is meant to notify the server that an upload flow is initiated by the client.

POST https://mydomain.aprimo.com/uploads/segments HTTP/1.1 
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) ... 
api-version: 1 
Authorization: Token ##token_placeholder##
Accept: */* 
Content-Type: application/json 
{ 
       "filename":"marko_test_video.mp4"
}

When successful, the response will be a 200-OK HTTP Response and will contain a URI, which can be used to upload the file segments in the next step.

{
	"uri":"/uploads/segments/NzVmMDY5YTA4MGU1NDBhMDkyY2FmYzQ3YzQwMzNjMmQ%3d" 
}

The code to execute this step is:

InitiateUploadPayload initiatedPayload;
//NOTE: Always use HttpClient factory (do not Dispose of HttpClients)... 
var httpClient = _httpClientFactory.CreateClient();
httpClient.BaseAddress = new Uri(UploadServiceUrl);
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {accessToken");

//NOTE: We need to serialize our file name into a Json paylaod with a 'filename' property.
string payloadJson = JsonConvert.SerializeObject(new { filename = fileName });

using (StringContent bodyContent = new StringContent(payloadJson, Encoding.UTF8, "application/json"))
using (HttpResponseMessage responseMessage = await client.PostAsync(UploadServiceUrl, bodyContent, cancellationToken))
{
    responseMessage.EnsureSuccessStatusCode();
    string response = await responseMessage.Content.ReadAsStringAsync();
    initiatedPayload = JsonConvert.DeserializeObject<InitiateUploadPayload>(response);
}

Note, the InitiateUploadPayload class is used to deserialize the response and to retrieve URI, allowing us to return an object to consuming code; it is a quite simple class as follows:

public class InitiateUploadPayload
{
    [JsonProperty("uri", NullValueHandling = NullValueHandling.Ignore)]
    public string Uri { get; set; }
}

Once we have received the URI, we can continue uploading all the segments/chunks to it as our upload endpoint.

3. Upload each segment

In this step, each segment of the file will be uploaded to the URI returned in the response in the previous Step 2.

The file name provided in the upload form is not important. However, the client must specify the index of the uploaded segment as a query string parameter. The index of the segment is ZERO based; there should be one segment with index=0, and there should be no gaps between indexes.

As noted above, I have chosen to use a chunk size of 15MB. Once the binary stream is split into chunks, we now perform an HTTP POST for every segment, sending it to the previously retrieved URL.

In this case, my request looks something like this:

POST https://mydomain.aprimo.com/uploads/segments/NzVmMDY5YTA4MGU1NDBhMDkyY2FmYzQ3YzQwMzNjMmQ%3d?index=0 HTTP/1.1
Content-Length: 24947
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) ...
api-version: 1
Authorization: Token ##token_placeholder##
Content-Type: multipart/form-data; boundary=----BoundaryzZaqkP0Sxn4p9XFF
Accept: */*
------BoundaryzZaqkP0Sxn4p9XFF
Content-Disposition: form-data; name="segment0"; filename=" marko_test_video.mp4.segment0"
Content-Type: image/png
[binary content]

This step needs to be repeated for every chunk. It is possible to upload file segments in parallel (async) requests. However, for simplicity, this example uploads every chunk serially; check out Parallel.ForEachAsync with AsyncEnumerable if you are interested in learning more.

When successful, the response will be a 202-Accepted HTTP Response.

Here is a (slightly) more fleshed-out example that pulls the previous steps together:

Stream fileContentStream = inputSelectFile.FileContent;
byte[] buffer = new byte[MAX_BUFFER]; 
int bytesRead, index = 0;
using (BufferedStream bs = new BufferedStream(fileContentStream))
{
    while ((bytesRead = bs.Read(buffer, 0, MAX_BUFFER)) != 0) //reading 15mb chunks at a time
    {
        //NOTE: Always use HttpClient factory (do not Dispose of HttpClients)... 
        var httpClient = _httpClientFactory.CreateClient();
        httpClient.BaseAddress = new Uri(UploadServiceUrl);
        httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + accessToken);

        using (MultipartFormDataContent multipartFormDataContent = new MultipartFormDataContent("----BoundaryzZaqkP0Sxn4p9XFF"))
        using (Stream memoryStream = new MemoryStream(buffer))
        using (StreamContent streamContent = new StreamContent(memoryStream))
        {
            multipartFormDataContent.Add(streamContent, name: $"segment{index}", fileName: $"{fileName}.segment{index}");

            //NOTE: Because our index is an int we do not have an encoding risk, 
            //  otherwise the awesome Flurl library would make constructing a well-formed Uri easier. 
            using (HttpResponseMessage responseMessage = await client.PostAsync($"{initiatePayload.Uri}?index={index}", multipartFormDataContent, cancellationToken))
            {
                responseMessage.EnsureSuccessStatusCode();
                string responseContent = await responseMessage.Content.ReadAsStringAsync();
            }
        }
    }

    index++;
}

TIP:

initiatePayload.Uri is Uri retrieved from the previous step. Uploading large videos can take quite some time, so make sure to:

  • have all chucks uploaded with successful state
  • if any fail, re-attempt the upload of the segment
  • take a closer look at multipartFormDataContent.Add(streamContent, name: $"segment{index}", fileName: $"{fileName}.segment{index}"). We are adding the binary file with a specific name, as requested above.
  • take a look at: await client.PostAsync($"{initiatePayload.Uri}?index={index}". Each request has a URL querystring parameter index=N, where N is the current chunk index. This allows the Aprimo API to correctly assemble the binary file.

4a. Commit the upload

After all the segments have been uploaded, the upload flow can be committed. To do this, the client needs to provide the total number of segments that have been uploaded and the name of the original file, as follows:

POST https://mydomain.aprimo.com/uploads/segments/NzVmMDY5YTA4MGU1NDBhMDkyY2FmYzQ3YzQwMzNjMmQ%3d/commit HTTP/1.1 
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) ... 
api-version: 1 
Authorization: Token ##token_placeholder##
Accept: */* 
Content-Type: application/json 
{ 
       "filename":" marko_test_video.mp4", 
       "segmentcount":"12" 
} 

In this step, validation will occur, which will make sure that all the file segments have been uploaded to the server. If successful, the file will be re-assembled from fragments and will persist on the server. The response will be 200-OK, and it will contain a file upload token which can then be used later in the edit record file request.

{ 
token":"NzVmMDY5YTA4MGU1NDBhMDkyY2FmYzQ3YzQwMzNjMmQ="  
}

4b. Cancel the upload (if issues arise

An upload can be canceled at any moment after its initial setup by issuing a DELETE REST request to the original Uri returned as follows:

DELETE https://mydomain.aprimo.com/uploads/segments/NzVmMDY5YTA4MGU1NDBhMDkyY2FmYzQ3YzQwMzNjMmQ%3d HTTP/1.1 
 
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) ... 
api-version: 1 
Authorization: Token ##token_placeholder## 
Accept: */*

When successful, the response will be a 204-No Content HTTP Response.

And there you have it. Suppose the upload is successful in step 3. In that case, you will obtain an upload token that we can use in the payload to create an associated record in Aprimo. I will provide this example in the next part of the blog series, so stay tuned.

If you have any questions or if you are interested in getting firsthand experience from us about APRIMO and/or Tridion integrations, feel free to contact us.

Contact us to discuss your project.
We're ready to work with you.
Let's talk