Uploading a Document Using the Safelink V2 API

You can use the Safelink API to upload documents of any size to the documents area of a Safelink workspace.

In order to be able to cope with files of any size, the upload process is broken up into three stages:

  1. First, you'll need to begin an "upload session", providing the filename, size, and optionally the mime type of the document that you're uploading.
  2. Then, divide your file into 4MB chunks and upload each one in turn.  If the upload of one chunk fails, you can retry it without starting from the first chunk.
  3. Once the chunks are all uploaded, end the "upload session" to create the document.

Sample Code


Here's how you might complete this using Python 3 and its standard libraries.

For this example, you'll need to know the ID of the folder that you wish to 

Stage 1: Begin the Session

For this stage, you'll need to know the ID of the folder you wish to upload to.  If you don't know that, you can use the folder navigation API to find the ID you need.

# Construct the API path to which the chunks will be uploaded.
begin_session_path = f"{api_base}/folders/{target_folder_id}/begin_chunked_upload"

# Determine the file size and filename from the given source file path.
file_size = os.path.getsize(file_path)
file_name = os.path.basename(file_path)

# Determine the parameters to send as part of the begin session request.
begin_session_params = {
    "file_name": file_name,
    "file_size": file_size,
    "mime_type": mimetypes.guess_type(file_path)[0]
}

# Send the request as an HTTP POST, with the parameters encoded as JSON in the body.
result = post_json(connection, begin_session_path, begin_session_params)
session_id = result["session_id"]


Now that you have a `session_id`, you can begin stage 2.

Stage 2: Upload the data, in 4MB chunks

# Construct the API path to which the chunks will be uploaded.
put_chunk_path = f"{api_base}/folders/{target_folder_id}/upload_chunk/{session_id}"

# Upload in 4MB chunks.
chunk_size = 4 * 1024 * 1024 * 1024;

# The chunks themselves are just binary data.  The octet-stream content type signifies this.
chunk_content_type = "application/octet-stream"  

# We'll be uploading from the first byte of the file, which has offset zero.
start_offset = 0

# Open the input file in "read binary" mode.  The "binary" is important so that
# no character encoding is assumed.
file_object = open(file_path, "rb")

# Start an infinite loop; we'll break out of it when we run out of data.
while True:
    
    # Read 4MB more data from the file.
    more_data = file_object.read(chunk_size)
    
    # Stop if there is no more data to upload.
    if not more_data:
        break
    
    # We need to specify which bytes of the file we are uploading in each chunk.
    # First we calculate the file offset of the last byte we're about to send.
    end_offset = start_offset + len(more_data) - 1 # Inclusive endpoint.
    
    # Then, we create a Content-Range header from the start and end offsets.
    extra_headers = {
        "Content-Range": f"bytes {start_offset}-{end_offset}/{file_size}"
    }
    
    # Write the chunk to the server, as a raw put.
    result = put_raw(connection, put_chunk_path, more_data, chunk_content_type, extra_headers)

    # Update start_offset so that it's correct for the next chunk.
    start_offset += len(more_data)

    # Close the file we opened.
    file_object.close()

Stage 3: End the Upload Session

Once all the chunks have been uploaded, end the session.  This creates the document in the workspace.

You can supply the default permissions for the new document at this time, or omit them to inherit permissions from the folder.

# Construct the API path for the end session request.
end_session_path = f"{api_base}/folders/{target_folder_id}/end_chunked_upload"

# Create parameters.  The "permissions" are optional, but the session_id is required.
# These parameters will be encoded as JSON in the body of the POST request.
end_session_params = {
    "session_id": session_id,
    "permissions": {
        "view": True,
        "edit": True,
        "print": False,
        "download_original": False,
        "download_pdf": False,
        "watermark": False,
    }
}

# Send the request using the HTTP POST verb.
result = post_json(connection, end_session_path, end_session_params)


Now, the file will be live in your workspace.  If you wish to set more specific permissions for the document, you can do so using the permissions API.

Putting it Together


To run the above code, you'll need a `connection` from the http.client library, which might look like this:

safelink_hostname = "ci.safelink.cloud" # Customise as needed.
api_base = "/api/v2"
connection = http.client.HTTPSConnection(safelink_hostname)
upload_file_from_path(connection, "/path/to/some/file.docx", "SF12345678")
connection.close()

Error Handling


This example does not perform any error handling.  In your real code, you will need to check for errors after each request, deal with timeouts, and retry when necessary.

Helper Functions

# A function to make a HTTP POST request with a JSON body,
# parsing the JSON response.
def post_json(connection, path, data):
    request_body = json.dumps(data)
    return post_raw(connection, path, request_body, "application/json")
    
# A function to make a HTTP POST request and parse the JSON response.
def post_raw(connection, path, request_body, content_type):
    print("Request path: " + path)
    headers = {
        "Content-Type": content_type,
        "Accept": "application/json"
    }
    headers.update(get_auth_headers())
    
    connection.request("POST", path, request_body, headers)
    response = connection.getresponse()
    response_json = response.read()
    return json.loads(response_json)

# A function to make a HTTP POST request and parse the JSON response.
def put_raw(connection, path, request_body, content_type, extra_headers = {}):
    print("Request path: " + path)
    headers = {
        "Content-Type": content_type,
        "Accept": "application/json"
    }
    headers.update(get_auth_headers())
    headers.update(extra_headers)
    
    connection.request("PUT", path, request_body, headers)
    response = connection.getresponse()
    response_json = response.read()
    return json.loads(response_json)

Authentication

You can HTTP Basic Authentication using your user ID and password, or Bearer authentication.  In both cases, the credentials are secured by HTTPS.

Permissions are applied when you access the Safelink API.  You will need to have permission to upload to the relevant folder, in order to do so.

Here is a function that generates an `Authorization` header that you can pass with each request.

# A function to build the HTTP headers required to authenticate yourself.
def get_auth_headers():
    # We join the username and password using a ":", then encode the
    # result as base64.  We then construct a header with
    # name "Authorization" and value "Basic <base64_encoded_value_here>".
    
    username_plus_password = (username + ":" + password).encode("ascii")
    encoded_username_plus_password = b64encode(username_plus_password).decode("ascii")
    
    return { "Authorization": "Basic " + encoded_username_plus_password }

Dependencies

# Imports from the Python3 standard libraries.
import http.client
from base64 import b64encode
import json
import pprint
import os.path
import mimetypes

Complete Example

#!/usr/bin/python3

# Imports from the Python3 standard libraries.
import http.client
from base64 import b64encode
import json
import pprint
import os.path
import mimetypes

# Basic connection information.
safelink_hostname = "ci.safelink.cloud" # Customise as needed.
api_base = "/api/v2"
username = "insert_username_here"
password = "insert_password_here"

# A function to build the HTTP headers required to authenticate yourself.
def get_auth_headers():
    # Authentication uses HTTP Basic Authentication, secured over HTTPS.
    # We join the username and password using a ":", then encode the
    # result as base64.  We then construct a header with
    # name "Authorization" and value "Basic <base64_encoded_value_here>".
    username_plus_password = (username + ":" + password).encode("ascii")
    encoded_username_plus_password = b64encode(username_plus_password).decode("ascii")
    return { "Authorization": "Basic " + encoded_username_plus_password }

# A function to make an HTTP GET request and parse the JSON response.
def get_json(connection, path):
    request_body = None
    headers = get_auth_headers()
    connection.request("GET", path, request_body, headers)
    response = connection.getresponse()
    response_json = response.read()
    # print("Body is {}".format(response_json))
    # print("Room list is {}".format(room_list))
    # print("Status: {} and reason: {}".format(response.status, response.reason))
    return json.loads(response_json)

# A function to make a HTTP POST request with a JSON body,
# parsing the JSON response.
def post_json(connection, path, data):
    request_body = json.dumps(data)
    return post_raw(connection, path, request_body, "application/json")
    
# A function to make a HTTP POST request and parse the JSON response.
def post_raw(connection, path, request_body, content_type):
    print("Request path: " + path)
    headers = {
        "Content-Type": content_type,
        "Accept": "application/json"
    }
    headers.update(get_auth_headers())
    
    connection.request("POST", path, request_body, headers)
    response = connection.getresponse()
    response_json = response.read()
    return json.loads(response_json)

# A function to make a HTTP POST request and parse the JSON response.
def put_raw(connection, path, request_body, content_type, extra_headers = {}):
    print("Request path: " + path)
    headers = {
        "Content-Type": content_type,
        "Accept": "application/json"
    }
    headers.update(get_auth_headers())
    headers.update(extra_headers)
    
    connection.request("PUT", path, request_body, headers)
    response = connection.getresponse()
    response_json = response.read()
    return json.loads(response_json)

# A function to get a list of rooms to which you have access.
def get_rooms(connection):
    return get_json(connection, api_base + "/rooms")

# Upload a file to Safelink, given the ID of a target
def upload_file_from_path(connection, file_path, target_folder_id):
    file_size = os.path.getsize(file_path)
    
    # Stage 1: Begin the upload session.
    # The goal here is to obtain a session_id, which you need for stage 2.
    begin_session_path = f"{api_base}/folders/{target_folder_id}/begin_chunked_upload"
    begin_session_params = {
        "file_name": os.path.basename(file_path),
        "file_size": file_size,
        "mime_type": mimetypes.guess_type(file_path)[0]
    }
    result = post_json(connection, begin_session_path, begin_session_params)
    session_id = result["session_id"]
    
    # Print the server's response for stage 1.
    pretty_print(result)
    
    # Stage 2: Upload the data, in 4MB chunks.
    put_chunk_path = f"{api_base}/folders/{target_folder_id}/upload_chunk/{session_id}"
    chunk_size = 4 * 1024 * 1024 * 1024;             # Upload in 4MB chunks.
    chunk_content_type = "application/octet-stream"  # Signifies binary data.
    start_offset = 0
    
    file_object = open(file_path, "rb")
    
    while True:
        # Read 4MB more data from the file.
        more_data = file_object.read(chunk_size)
        
        # Stop if there is no more data to upload.
        if not more_data:
            break
        
        # We need to specify which bytes of the file we are uploading in each chunk.
        # First we calculate the file offset of the last byte we're about to send.
        end_offset = start_offset + len(more_data) - 1 # Inclusive endpoint.
        
        # Then, we create a Content-Range header from the start and end offsets.
        extra_headers = {
            "Content-Range": f"bytes {start_offset}-{end_offset}/{file_size}"
        }
        
        # Write the chunk to the server, as a raw put.
        result = put_raw(connection, put_chunk_path, more_data, chunk_content_type, extra_headers)
        start_offset += len(more_data)
        
        # Print the server's response for the chunk upload.
        pretty_print(result)
        
    file_object.close()

    # Stage 3: End Session.  Once all the chunks have been uploaded,
    # end the session.  This creates the document in the workspace.
    # You can supply the default permissions for the new document at this time,
    # or omit them to inherit permissions from the folder.
    end_session_path = f"{api_base}/folders/{target_folder_id}/end_chunked_upload"
    end_session_params = {
        "session_id": session_id,
        "permissions": {
            "view": True,
            "edit": True,
            "print": False,
            "download_original": False,
            "download_pdf": False,
            "watermark": False,
        }
    }
    result = post_json(connection, end_session_path, end_session_params)
    
    # Print the server's final response.
    pretty_print(result)

# A function that prints some data in a human-readable way.
def pretty_print(data):
    pprint.PrettyPrinter(indent=4).pprint(data)
    
connection = http.client.HTTPSConnection(safelink_hostname)
rooms = get_rooms(connection)
pretty_print(rooms)
upload_file_from_path(connection, "./upload_file.py", "SF5360374")
connection.close()