Coding in PowerBuilder

Downloading

A great majority of the requests done through HTTP result in a response; there are two different ways of accessing the response depending on its size:

  • GetResponseData for small data (less than 20MB) and

  • ReadData for anything larger

Small data

The GetResponseData is mostly used for simple requests that don't return a lot of data. It's regularly used for API requests that expect short responses, for example:

HTTPClient ino_client
​
'''' rem n_cst_azurefs.of_getcontainers:13
li_res = ino_client.Sendrequest( "GET", of_geturl(URL_LIST_CONTAINERS) )
li_responseStatus = ino_client.GetResponseStatuscode( )
​
if li_responseStatus <> 200 then
    as_error = ino_client.GetResponseStatusText( )
    return 1
end if
​
ino_client.GetResponseBody(ref lblb_responseBytes)
ls_response = string(lblb_responseBytes, EncodingUTF8!)

The GetResponseData can read data onto both a string or a blob, so it's perfect for small data.

However, this method does not support working with data larger than 20MB.

Large data

The ReadData method is used to read the response in chunks, thus allowing the read of arbitrarily large responses. This is the method used to download the files in the sample application. Since the data to be read may be larger than what a Blob object can hold, it's recommended that the chunks be written to a file on disk as they're read.

Note: It is necessary to stop the HTTPClient object from automatically loading the data:

''' n_cst_filetransferhelper.of_download:25
lb_autoread = ino_client.Autoreaddata
ino_client.Autoreaddata = false

Then we can send the request for the data:

li_res = ino_client.SendRequest( "GET", as_url)

Finally, we read and send to disk the received data, reading chunk by chunk. The ReadData method automatically reads the data following what was previously read. The FileWriteEx too writes at the end of what was previously written.

''' n_cst_filetransferhelper.of_download:29
​
do while true
    lblb_buffer = Blob("")
    li_ret = ino_client.readdata(ref lblb_buffer, CHUNK_SIZE)
    
    FileWriteEx(al_file, lblb_buffer)
    ll_chunksWritten++
​
    ...
loop

Don't forget to restore the value of AutoReadData:

ino_client.Autoreaddata = lb_autoread

Note: Reading and writing to disk by chunks might take a long time, so it's recommended to yield() between chunks to prevent the application from freezing.

Uploading

For the Azure example, the Azure REST API currently only allows to upload files through PUT requests, and currently the HTTP Client doesn't have a supported way to upload files larger than 20MB with a single call. Thus, to upload our files we have to upload files in chunks, making use of Azure's Put Block and Put Block List endpoints to submit the files in parts.

Reading the file

To read the file in parts the FileReadEx in PowerScript Reference method is used.

''' n_cst_azurefs.of_uploadfile:28
​
    ll_filesize = FileLength64(as_path)
    li_file = FileOpen(as_path, StreamMode!, Read!, LockRead!)
    
    if li_file < 1 then
        as_error = "Could not open file for reading"
        return -1
    end if
    
    li_chunk = 0
    do while true
        ll_read = FileReadEx(li_file, ref lblb_buffer, CHUNK_SIZE)
        if ll_read < 1 then exit
        lblb_pathHash = lno_crypter.SHA(SHA512!, Blob(as_path + string(li_chunk), EncodingUTF8!))
        ls_blockId = lno_coderobject.base64encode(lblb_pathHash)
        ls_blockId = Left(ls_blockId, 64)
        li_res = of_putblock(&
            lblb_buffer,&
            ls_sanitizedObjectPath,&
            lno_coderobject.urlencode(Blob(ls_blockId, EncodingUTF8!)),&
            ref ls_error)

Each chunk of the file will have an ID generated (this is needed to commit all the pieces into a single file on Azure) and immediately after, the chunk will be uploaded.

Uploading the file

To upload a chunk of the file, all that's necessary is to send a PUT request to the URL appending the Blob as an argument to the SendRequest method:

''' n_cst_filetransferhelper.of_uploadPut:7
int li_res
int li_responseCode
string ls_responseText
​
ino_client.SetRequestheader( "Content-Length", String(Len(ablb_blob)), true)
​
li_res = ino_client.SendRequest("PUT", as_url, ablb_blob)
​
if li_res <> 1 then 
    as_error = "couldn't send request"
    
    return -1
end if
​
li_responseCode = ino_client.GetResponseStatuscode( )
ls_responseText = ino_client.GetResponseStatusText( )
​
if li_responseCode < 200 or li_responseCode > 300 then
    as_error = "Received non-success status code " + string(li_responseCode) + ": " + ls_responseText
    return -1 
end if
​
return 1

Once all the chunks have been uploaded and the IDs collected, we have to send another request indicating the instruction to compile all the chunks into a single file. The list of chunks is built as an XML document, thus we have to indicate such in the request header (with SetRequestHeader).

''' n_cst_azurefs.of_putblocklist:8
​
ls_xml = "<?xml version='1.0' encoding='utf-8'?><BlockList>"
for i = 1 to UpperBound(as_blockids) 
    ls_xml += "<Uncommitted>" + as_blockids[i] + "</Uncommitted>"
next
ls_xml += "</BlockList>"
​
of_prepareclient( "PUT", len(ls_xml), "/" + is_accname + "/" + is_container + "/" + as_destination, "comp:blocklist", "application/xml")
ino_client.SetRequestheader( "Content-Type", "application/xml", true)
​
li_res = ino_client.SendRequest("PUT", &
    ls_url,&
    ls_xml)

Finally we send the PUT request to the URL and pass the XML as the body of the request.

Uploading large files in parts

The procedure above is used to work with Azure's particular constraints; however a more general approach is to upload a file in chunks using a POST method. We use this other approach with the C# file server.

    /// n_cst_genericfileserverbrowser.of_uploadfile:37 
​
    li_res = ino_client.postdatastart(ls_url) // initiate the chunked requeset
​
    if li_res <> 1 then
        as_error = "Error attempting file transfer"
        return -1
    end if
    
    do while lll_transferred < ll_fileSize
        lblb_buffer = Blob("")
        li_res = FileReadEx(li_handle, lblb_buffer, CHUNK_SIZE) // read a chunk of the data
        
        if li_res = 0 then exit
        if li_res < 1 then 
            as_error = "Error reading file chunk"
            return -1
        end if
        
        ino_client.postdata( lblb_buffer, li_res) // send the chunk of the data
        
        lll_transferred += li_res
    loop // repeat until all the file has been read
    
    li_res = ino_client.postdataend() 

This is the way to send very large files to an endpoint expecting a POST action, there's currently not an equivalent operation for the PUT method.

Helper Tool

The demo application contains an NVO with the logic required to download and upload files through HTTP called n_cst_filetransferhelper. This tool will help you work with downloading/uploading files by taking care of the nitty-gritty and providing a single method for each operation. Feel free to download it and use it in your own projects and/or modify it to better accommodate your requirements.