Storing Large Files (Blobs)


The basic design principle of WebWidgets is that most datasets are relatively small, so they can be stored easily in a SQLite DB and sent to the browser on page load. This is true for a lot of applications, but it causes a problem when an app wants to store large files like images, videos, or audio clips. If an app is managing thousands of images, each of which is 10 megabytes, it would be very awkward and slow to try to send the entire dataset to the browser, especially if the user just wants to look at a single file. The Blob Storage mechanism enables users to upload and retrieve large files, without storing the entire contents in the Widget DB.

Important note: the API for blob storage in WWIO is contained in the core API, but the implementation is not. If you are running the open-source version on your own machines, you will need to implement a simple storage interface to connect the data from WWIO to your network's storage utility.

To store blob data in your Widget system, use the following procedure:

To retrieve the stored Blob data, do this:

The WWIO Blob data manager performs a couple of simple tasks. When it sees a create or update request come in from the user, it transforms the base64 data in the request into a file, and uploads the result to a storage location that depends on the username, widget name, table name, and record ID. Unlike most other Widget sync operations, it does NOT store the blob data in the SQLite DB. Instead, it stores the magic tag follow by the file length. The getBlobStoreUrl is a special endpoint that retrieves the blob from the storage location and servers it to the browser.

It is important to detect the completion of the syncItem call because large file uploads may take a significant amount of time, unlike most other WWIO syncs. If you close the page before the upload has completed, your blob data may be truncated. The example code below shows how to detect the request completion.

Example Code

This example shows how to use the Blob storage facility to implement a simple photo manager. The full code, called Photos, can be viewed in the Widgets Gallery. The schema for this Widget is:

CREATE TABLE photo_main (id int, tag_list varchar(30), photo_date varchar(10), 
            base64_blob_data varchar(1000), blob_file_name varchar(100), f
            ull_desc varchar(300), rotation int default 0, primary key(id));

As you can see, this table as the two required columns base64_blob_data and blob_file_name that mark it as a blob-enabled table, as well as a few other utility columns. We create a record with a standard Widget call:

function updateAttachmentContent(base64data, filename)
{
    const newrec = {
        "short_name" : "NotYetSet",
        "full_desc" : "...",
        "tag_list" : "",
        "photo_date" : getTodayCode().getDateString(),
        "description" : "---",
        "base64_blob_data" : base64data,
        "blob_file_name" : filename,
        "rotation" : 0
    };


    const item = W.buildItem("photo_main", newrec);
    SYNC_TARGET_ID = item.getId();
    item.syncItem();
}

To get the base-64 data, we use the FileReader utility:

function checkUploadAndGo()
{
    // get the HTML input element for the file
    const uploadel = getUniqElementByName("uploadMe");
    const filename = uploadel.files[0].name;
    
    if(!confirm(`Okay to upload file ${filename}?`))
        { return; }

    const reader = new FileReader();
    reader.readAsDataURL(uploadel.files[0]);
    reader.onload = function () {
        updateAttachmentContent(reader.result, filename);
    };
}

The input element is defined as follows:

    <input id="file-upload" type="file" name="uploadMe" onChange="javascript:checkUploadAndGo()"/>

Finally, to detect the completion of the request, we define a special method called ajaxRequestUserCallBack. When a sync operation completes, the WWIO JavaScript code checks to see if this function is present, and calls it if so. Thus, this method can be defined to request actions to happen on completion of sync calls. In this case, we just reload the page.

function ajaxRequestUserCallBack(opurl, opname, itemid)
{
  if(itemid == SYNC_TARGET_ID)
  {
    alert("Upload complete, going to reload page");
    window.location.reload();
  }
}

To view the photos, we use the special method on the Widget records:


  const item = W.lookupItem("photo_main", EDIT_STUDY_ITEM);
  const blobstore = item.getBlobStoreUrl();
  const rotate90 = 90 * item.getRotation();

  pageinfo += `
    <img src="${blobstore}&download=false" style="transform: rotate(${rotate90}deg);" width="60%"/>
  `;

The rotation logic is a simple trick to deal with the fact that sometimes photos uploaded from a phone or tablet with the wrong orientation.