Sample code for 30+ languages & platforms
Objective-C Requires Chilkat v11.7.0+

Box: Upload a Large File in Parts and Commit the Session

See more Box Examples

Run the reusable setup once per HttpCurl object, then select names and execute the operation. The setup is repeated here only to make this published example independently runnable. Resolve documents by name, then perform the explicit chunked-upload session, part, and commit loop. Supports 20 MB through 2 GB using int. Saves recovery information and honors bounded commit Retry-After.

See the Box API guide for setup and the complete workflow.

Chilkat Objective-C Downloads

Objective-C
#import <CkoHttpCurl.h>
#import <CkoStringBuilder.h>
#import <NSString.h>
#import <CkoFileAccess.h>
#import <CkoJsonObject.h>
#import <CkoHttp.h>
#import <CkoHttpResponse.h>
#import <CkoCrypt2.h>
#import <CkoBinData.h>

BOOL success = NO;

//  Requires Chilkat 11.7.0 or later and a previously unlocked Chilkat bundle.
//  See Global Unlock Sample.
//  Guide: Box API Guide.

success = NO;

//  BEGIN ONE-TIME SETUP
//  This block is repeated so each published example can run on its own.
//  In an application, run the setup example once and pass the SAME curl object
//  to operation code below. Do not recreate it for every operation.
//  See Reusable Box HttpCurl Setup.
CkoHttpCurl *curl = [[CkoHttpCurl alloc] init];
[curl SetVar: @"access_token" varValue: @"BOX_ACCESS_TOKEN"];
[curl SetVar: @"parent_name" varValue: @"/"];
//  Box's fixed root ID is an implementation detail; callers supply names.
[curl SetVar: @"parent_id" varValue: @"0"];
CkoStringBuilder *setupCommand = [[CkoStringBuilder alloc] init];
int setupReplacements;

//  Register findParent; registration sends no request.
[setupCommand SetString: @"curl -X GET \"https://api.box.com/2.0/folders/0/items?limit=1000&fields=id,type,name\" -H \"Authorization: Bearer ACCESS_TOKEN\""];
setupReplacements = [[setupCommand Replace: @"ACCESS_TOKEN" replacement: [curl GetVar: @"access_token"]] intValue];
success = [curl AddFunction: @"findParent" curl: [setupCommand GetAsString]];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

//  Register findFolder; registration sends no request.
[setupCommand SetString: @"curl -X GET \"https://api.box.com/2.0/folders/{{parent_id}}/items?limit=1000&fields=id,type,name\" -H \"Authorization: Bearer ACCESS_TOKEN\""];
setupReplacements = [[setupCommand Replace: @"ACCESS_TOKEN" replacement: [curl GetVar: @"access_token"]] intValue];
success = [curl AddFunction: @"findFolder" curl: [setupCommand GetAsString]];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

//  Register findFile; registration sends no request.
[setupCommand SetString: @"curl -X GET \"https://api.box.com/2.0/folders/{{folder_id}}/items?limit=1000&fields=id,type,name\" -H \"Authorization: Bearer ACCESS_TOKEN\""];
setupReplacements = [[setupCommand Replace: @"ACCESS_TOKEN" replacement: [curl GetVar: @"access_token"]] intValue];
success = [curl AddFunction: @"findFile" curl: [setupCommand GetAsString]];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

//  Register findDestination; registration sends no request.
[setupCommand SetString: @"curl -X GET \"https://api.box.com/2.0/folders/0/items?limit=1000&fields=id,type,name\" -H \"Authorization: Bearer ACCESS_TOKEN\""];
setupReplacements = [[setupCommand Replace: @"ACCESS_TOKEN" replacement: [curl GetVar: @"access_token"]] intValue];
success = [curl AddFunction: @"findDestination" curl: [setupCommand GetAsString]];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

//  Register getFileDetails; registration sends no request.
[setupCommand SetString: @"curl -X GET \"https://api.box.com/2.0/files/{{file_id}}?fields=id,sha1,file_version\" -H \"Authorization: Bearer ACCESS_TOKEN\""];
setupReplacements = [[setupCommand Replace: @"ACCESS_TOKEN" replacement: [curl GetVar: @"access_token"]] intValue];
success = [curl AddFunction: @"getFileDetails" curl: [setupCommand GetAsString]];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

//  Register getEnterprise; registration sends no request.
[setupCommand SetString: @"curl -X GET \"https://api.box.com/2.0/users/me?fields=enterprise\" -H \"Authorization: Bearer ACCESS_TOKEN\""];
setupReplacements = [[setupCommand Replace: @"ACCESS_TOKEN" replacement: [curl GetVar: @"access_token"]] intValue];
success = [curl AddFunction: @"getEnterprise" curl: [setupCommand GetAsString]];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

//  Register output rules once. Chilkat 11.7.0 evaluates {{name}} variables
//  when extracting each output, using the current value at that time.
success = [curl AddOutput2: @"findParent" arrayPath: @"entries" wherePath: @"name" whereValue: @"{{parent_name}}" caseSensitive: YES subPath: @"id" varName: @"parent_id"];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

success = [curl AddOutput2: @"findFolder" arrayPath: @"entries" wherePath: @"name" whereValue: @"{{folder_name}}" caseSensitive: YES subPath: @"id" varName: @"folder_id"];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

success = [curl AddOutput2: @"findFile" arrayPath: @"entries" wherePath: @"name" whereValue: @"{{file_name}}" caseSensitive: YES subPath: @"id" varName: @"file_id"];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

success = [curl AddOutput2: @"findDestination" arrayPath: @"entries" wherePath: @"name" whereValue: @"{{destination_name}}" caseSensitive: YES subPath: @"id" varName: @"destination_id"];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

success = [curl AddOutput: @"getFileDetails" jsonPath: @"file_version.id" varName: @"file_version_id"];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

success = [curl AddOutput: @"getFileDetails" jsonPath: @"sha1" varName: @"file_sha1"];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

success = [curl AddOutput: @"getEnterprise" jsonPath: @"enterprise.id" varName: @"enterprise_id"];
if (success != YES) {
    NSLog(@"%@",curl.LastErrorText);
    return;
}

//  END ONE-TIME SETUP
//  The name lookups below assume each listing fits in one page (1000 items).
//  Parent and destination names select root children; folder_name selects a child
//  of parent_name. "/" selects the root parent. These are not arbitrary paths.

//  BEGIN OPERATION -- keep the configured object when reusing it.
//  Inputs are names. Retain cached IDs when the selected names have not changed.
NSString *parentName = @"/";
NSString *folderName = @"documents";
if (([curl GetVar: @"parent_name"] != parentName) || ((parentName == @"/") && ([curl VarDefined: @"parent_id"] == NO))) {
    [curl SetVar: @"parent_name" varValue: parentName];
    [curl ClearVar: @"parent_id"];
    [curl ClearVar: @"folder_id"];
    [curl ClearVar: @"file_id"];
    [curl ClearVar: @"file_version_id"];
    [curl ClearVar: @"file_sha1"];
    if (parentName == @"/") {
        [curl SetVar: @"parent_id" varValue: @"0"];
    }

}

if ([curl GetVar: @"folder_name"] != folderName) {
    [curl SetVar: @"folder_name" varValue: folderName];
    [curl ClearVar: @"folder_id"];
    [curl ClearVar: @"file_id"];
    [curl ClearVar: @"file_version_id"];
    [curl ClearVar: @"file_sha1"];
}

//  Resolve the named destination first. HttpCurl does not expand variables
//  inside multipart -F attributes in the tested build, so this upload is two phases.
[curl ClearTargetOutput: @"*"];
CkoStringBuilder *authorizedCommand1 = [[CkoStringBuilder alloc] init];
[authorizedCommand1 SetString: @"curl -X GET \"https://api.box.com/2.0/folders/{{folder_id}}?fields=id\" -H \"Authorization: Bearer {{access_token}}\""];
int headerReplacement1 = [[authorizedCommand1 Replace: @"{{access_token}}" replacement: [curl GetVar: @"access_token"]] intValue];
success = [curl DoYourThing: [authorizedCommand1 GetAsString]];
if ((success != YES) || ([curl.StatusCode intValue] != 200)) {
    NSLog(@"%@%d",@"Box request failed. HTTP status: ",[curl.StatusCode intValue]);
    NSLog(@"%@%d",@"Failure reason: ",[curl.FailReason intValue]);
    NSLog(@"%@",curl.ResponseBodyStr);
    return;
}

//  This example limits size to 2 GB so offsets fit all generated languages' integers.
//  The source must not change during hashing or upload. Create qa_output first.
NSString *localPath = @"qa_data/large-document.zip";
CkoFileAccess *file = [[CkoFileAccess alloc] init];
//  Check the decimal size before converting so an oversized file cannot overflow int.
NSString *sizeString = [file FileSizeStr: localPath];
if (file.LastMethodSuccess != YES) {
    NSLog(@"%@",file.LastErrorText);
    return;
}

CkoStringBuilder *sizeText = [[CkoStringBuilder alloc] init];
[sizeText SetString: sizeString];
CkoJsonObject *sizeMatch = [[CkoJsonObject alloc] init];
if ([[sizeText RegexMatch: @"^([0-9]{1,9}|1[0-9]{9}|2000000000)$" json: sizeMatch timeoutMs: [NSNumber numberWithInt: 100]] intValue] != 1) {
    NSLog(@"%@",@"Choose an input between 20 MB and 2 GB for this example.");
    return;
}

int fileSize = [sizeString intValue];
if (fileSize < 20971520) {
    NSLog(@"%@",@"Choose an input between 20 MB and 2 GB for this example.");
    return;
}

CkoHttp *http = [[CkoHttp alloc] init];
http.AuthToken = [curl GetVar: @"access_token"];
CkoHttpResponse *response = [[CkoHttpResponse alloc] init];
CkoJsonObject *request = [[CkoJsonObject alloc] init];
[request UpdateString: @"folder_id" value: [curl GetVar: @"folder_id"]];
[request UpdateString: @"file_name" value: @"large-document.zip"];
[request UpdateInt: @"file_size" value: [NSNumber numberWithInt: fileSize]];
success = [http HttpJson: @"POST" url: @"https://upload.box.com/api/2.0/files/upload_sessions" json: request contentType: @"application/json" response: response];
if (success != YES) {
    NSLog(@"%@",http.LastErrorText);
    return;
}

if ([response.StatusCode intValue] != 201) {
    NSLog(@"%@",response.BodyStr);
    return;
}

CkoJsonObject *session = [[CkoJsonObject alloc] init];
success = [session Load: response.BodyStr];
if (success != YES) {
    NSLog(@"%@",session.LastErrorText);
    return;
}

success = [session WriteFile: @"qa_output/box_upload_session.json"];
if (success != YES) {
    NSLog(@"%@",session.LastErrorText);
    return;
}

int partSize = [[session IntOf: @"part_size"] intValue];
NSString *uploadUrl = [session StringOf: @"session_endpoints.upload_part"];
NSString *commitUrl = [session StringOf: @"session_endpoints.commit"];
if ((partSize <= 0) || (uploadUrl == @"") || (commitUrl == @"")) {
    NSLog(@"%@",@"Invalid session response.");
    return;
}

CkoCrypt2 *hash = [[CkoCrypt2 alloc] init];
hash.HashAlgorithm = @"sha1";
hash.EncodingMode = @"base64";
NSString *fileDigest = [hash HashFileENC: localPath];
if (hash.LastMethodSuccess != YES) {
    NSLog(@"%@",hash.LastErrorText);
    return;
}

success = [file FileOpen: localPath accessMode: 2147483648 shareMode: 1 createDisp: 3 attr: 0];
if (success != YES) {
    NSLog(@"%@",file.LastErrorText);
    return;
}

CkoJsonObject *commit = [[CkoJsonObject alloc] init];
CkoJsonObject *partResponse = [[CkoJsonObject alloc] init];
CkoBinData *chunk = [[CkoBinData alloc] init];
CkoStringBuilder *range = [[CkoStringBuilder alloc] init];
CkoStringBuilder *digestHeader = [[CkoStringBuilder alloc] init];
int offset = 0;
int partIndex = 0;
int partLength;
int lastByte;
NSString *partDigest = 0;
while (offset < fileSize) {
    [chunk Clear];
    success = [file FileReadBd: [NSNumber numberWithInt: partSize] binData: chunk];
    if (success != YES) {
        NSLog(@"%@",file.LastErrorText);
        [file FileClose];
        return;
    }

    partLength = [chunk.NumBytes intValue];
    if (partLength <= 0) {
        NSLog(@"%@",@"Unexpected end of file.");
        [file FileClose];
        return;
    }

    lastByte = offset + (partLength - 1);
    [range Clear];
    [range Append: @"bytes "];
    [range AppendInt: [NSNumber numberWithInt: offset]];
    [range Append: @"-"];
    [range AppendInt: [NSNumber numberWithInt: lastByte]];
    [range Append: @"/"];
    [range AppendInt: [NSNumber numberWithInt: fileSize]];
    partDigest = [hash HashBdENC: chunk];
    if (hash.LastMethodSuccess != YES) {
        NSLog(@"%@",hash.LastErrorText);
        [file FileClose];
        return;
    }

    [digestHeader SetString: @"sha="];
    [digestHeader Append: partDigest];
    [http SetRequestHeader: @"Digest" value: [digestHeader GetAsString]];
    [http SetRequestHeader: @"Content-Range" value: [range GetAsString]];
    success = [http HttpBd: @"PUT" url: uploadUrl bd: chunk contentType: @"application/octet-stream" response: response];
    if ((success != YES) || ([response.StatusCode intValue] != 200)) {
        NSLog(@"%@%d",@"Part failed. HTTP status: ",[response.StatusCode intValue]);
        NSLog(@"%@",response.BodyStr);
        NSLog(@"%@",@"Inspect the saved session and server list_parts before retrying this part.");
        [file FileClose];
        return;
    }

    success = [partResponse Load: response.BodyStr];
    if (success != YES) {
        [file FileClose];
        return;
    }

    commit.I = [NSNumber numberWithInt: partIndex];
    //  Copy the server's acknowledged part fields into the commit manifest.
    [commit UpdateString: @"parts[i].part_id" value: [partResponse StringOf: @"part.part_id"]];
    [commit UpdateInt: @"parts[i].offset" value: [partResponse IntOf: @"part.offset"]];
    [commit UpdateInt: @"parts[i].size" value: [partResponse IntOf: @"part.size"]];
    [commit UpdateString: @"parts[i].sha1" value: [partResponse StringOf: @"part.sha1"]];
    success = [commit WriteFile: @"qa_output/box_upload_parts.json"];
    if (success != YES) {
        NSLog(@"%@",commit.LastErrorText);
        [file FileClose];
        return;
    }

    offset = offset + partLength;
    partIndex = partIndex + 1;
}

[file FileClose];
[http RemoveRequestHeader: @"Content-Range"];
[digestHeader SetString: @"sha="];
[digestHeader Append: fileDigest];
[http SetRequestHeader: @"Digest" value: [digestHeader GetAsString]];
int attempts = 0;
int waitSeconds;
CkoStringBuilder *retryHeader = [[CkoStringBuilder alloc] init];
CkoJsonObject *retryMatch = [[CkoJsonObject alloc] init];
BOOL completed = NO;
while ((attempts < 10) && (completed == NO)) {
    success = [http HttpJson: @"POST" url: commitUrl json: commit contentType: @"application/json" response: response];
    if (success != YES) {
        NSLog(@"%@%d",@"Commit failed: ",[response.StatusCode intValue]);
        NSLog(@"%@",response.BodyStr);
        return;
    }

    if ([response.StatusCode intValue] == 201) {
        completed = YES;
    }
    else {
        if ([response.StatusCode intValue] != 202) {
            NSLog(@"%@%d",@"Unexpected commit status: ",[response.StatusCode intValue]);
            return;
        }

        //  Box's asynchronous commit response supplies Retry-After in seconds.
        [retryHeader SetString: [response GetHeaderField: @"Retry-After"]];
        if ([[retryHeader RegexMatch: @"^[0-9]{1,2}$" json: retryMatch timeoutMs: [NSNumber numberWithInt: 100]] intValue] != 1) {
            NSLog(@"%@%@",@"Commit pending. Preserve the session and inspect Retry-After before resuming: ",[retryHeader GetAsString]);
            return;
        }

        waitSeconds = [[retryHeader GetAsString] intValue];
        if ((waitSeconds < 1) || (waitSeconds > 60)) {
            NSLog(@"%@%@",@"Commit pending. Resume commit after Retry-After: ",[response GetHeaderField: @"Retry-After"]);
            return;
        }

        [http SleepMs: [NSNumber numberWithInt: (waitSeconds * 1000)]];
    }

    attempts = attempts + 1;
}

if (completed == NO) {
    NSLog(@"%@",@"Commit still pending; retain session and part manifests to resume commit later.");
    return;
}

NSLog(@"%@%@",@"Upload committed: ",response.BodyStr);