YarkonS3 API

Using the API

The Yarkon control API is exposed through the Yarkon DOM object.

It is used in a similar manner to jQuery. To get access to the API object, you need to define one method on the global yarkon object, named onReady. When the Yarkon control is ready, this function will be called, passing in as its single argument the Yarkon API object.

Events are wired to their respective event handlers by specifying a callback, again, same as when using jQuery.

The below code snippet shows how to get it done:

<script>
    yarkon.onReady = function(yarkon) {

        // Remove the application name, we don't need it here. Update the link
        // to go to the correct site
        $('a.navbar-brand').html('').attr('href', 'http://looneytunes.wikia.com/wiki/Category:ACME');

        // Wire the select folder event
        yarkon.onSelectFolder(function(e, args) {
            // Log what happened
            console.log('Event: ' + e.type + '\nArgs: ' + JSON.stringify(args));
        });

    };
</script>

Event Handling

When the user interacts with Yarkon, various events are being sent to reflect changes made. You can "wire" these events, same as done when using jQuery, to allow your code to handle user actions.

The following sample demonstrates how to keep track of the active (focused) and selected items in the interface, and how to implement a custom command on these items.

<script>
    // Wire all events that should be logged
    yarkon.onChangeActiveFolder(function(e, args) {
        $('#active-folder-log').text(getFolderLog(args));
    });

    yarkon.onChangeActiveItem(function(e, args) {
        $('#active-item-log').text(getItemLog(args));
    });

    yarkon.onChangeSelectedItems(function(e, args) {
        var log = 'Selected: ';
        switch (args.length) {
            case 0:
                log += 'no items';
                break;
            case 1:
                log += 'one item:<br/>' + getItemLog(args[0]);
                break;
            default:
                log += args.length + ' items:<br/>' + (args.map(function(item) {
                    return getItemLog(item);
                })).join('<br/>');
                break;
        }
        $('#selected-items-log').html(getLog(log));
    });

    $('#btn-active-item').click(function(e) {
        e.preventDefault();
        var activeItem = yarkon.getActiveItem();
        if (activeItem) {
            if (activeItem.isFolder) {
                alert('The S3 path of the active folder is:\n' + activeItem.id);
            } else {
                // Show the S3 attributes of this item
                yarkon.getItemAttributes(activeItem, function(err, data) {
                    if (err) {
                        alert('Error: failed getting attributes for item ' + activeItem.id + '\nError: ' + err);
                    } else {
                        alert('The S3 path of the active item is:\n' + activeItem.id + '\nAttributes: ' + JSON.stringify(data));
                    }
                });
            }
        } else {
            alert('There is no active item');
        }
    });
</script>

Configure Menus

In some cases, you would like to change the context menus displayed by Yarkon when the user right-clicks an S3 object in the UI. The Yarkon control allows you to make any modification to any of the context menus displayed, using the API. You can add your own custom actions or remove some of the existing actions.

Menu manipulation is handled using the onInitMenu event, by responding back with the modifications you require. To remove existing menu actions, identify them by the menu action text (e.g, "Cut" or "Rename"). To add a new custom menu action, specify the following:

  • menuEntry:

    • id: a unique id for your menu action
    • label: the text to display for your menu action
    • icon: any font awesome icon (e.g. 'fa-bomb')
    • click: a function that will be called when the menu item is clicked. The item clicked will be passed into the function.
    • enabler: a function that will be called when the menu is displayed. The item clicked will be passed into the function. Return true to enable the menu action and false to disable it.
  • insertBefore: the label of the menu action to insert yours before.

  • insertAfter: the label of the menu action to insert yours after.

The sample below demonstrates removal of existing actions and adding a couple of custom commands (and a separator), as well as the custom actions being called on the items clicked.

<script>

    // Wire the menu initialization event
    yarkon.onInitMenu(function(e, args) {
        // We want to remove the clipboard actions from all menus
        if (args.menu === yarkon.Menu.TreeMenu ||
            args.menu === yarkon.Menu.GridDocMenu ||
            args.menu === yarkon.Menu.GridFolderMenu ||
            args.menu === yarkon.Menu.GridCanvasMenu ||
            args.menu === yarkon.Menu.SearchGridDocMenu ||
            args.menu === yarkon.Menu.SearchGridFolderMenu ||
            args.menu === yarkon.Menu.SearchGridCanvasMenu) {
            args.remove = [
                'Cut', 'Copy', 'Paste'
            ];
        }

        // We want to add two custom commands to context menus operating
        // on a document
        if (args.menu === yarkon.Menu.GridDocMenu ||
            args.menu === yarkon.Menu.SearchGridDocMenu) {
            args.add = [
                {
                    menuEntry: {
                        id: 'my_bomb_command',
                        label: 'My Bomb',
                        icon: 'fa-bomb',
                        click: function(item) {
                            alert('Bomb item: ' + JSON.stringify(item));
                        },
                        enabler: function(item) {
                            return true;
                        }
                    },
                    insertBefore: 'Download'
                }, {
                    menuEntry: {
                        id: 'my_bolt_command',
                        label: 'My Bolt',
                        icon: 'fa-bolt',
                        click: function(item) {
                            alert('Bolt item: ' + JSON.stringify(item));
                        },
                        enabler: function(item) {
                            return true;
                        }
                    },
                    insertBefore: 'Download'
                }, {
                    menuEntry: {
                        id: 'my_separator',
                        label: '-'
                    },
                    insertBefore: 'Download'
                }
            ]
        }
    });

</script>

Manage layout

The layout of the Yarkon control can be programmatically controlled using the API.

The layout consists of five areas, referred to as "panes":

  1. Toolbar - North pane; it is used for the buttons and action menus.
  2. Tree View - West pane; showing the S3 buckets and the folder structure below them.
  3. Grid View - Center pane; showing the S3 objects (documents and folders) in the current folder.
  4. Preview - East pane; showing a preview of the currently active object in the Grid View.
  5. Search Grid View - South pane; showing the search results.

A pane is said to be "closed" when it is collapsed. The user can "open" a closed pane by clicking the divider or dragging it. A pane is said to be "hidden" when it is closed and the divider is also hidden. The user cannot "show" a hidden pane, so if you want to remove a pane from the user interface, hide it.

All panes except the Toolbar and the Grid View can be either hidden/shown or opened/closed. The Toolbar can only be hidden/shown. The Grid View cannot be hidden or closed.

<script>

    // Helper function to control state of a button
    function updatePaneButtons(pane, isOpen, isShown) {
        var btnOpenClose = $('button.pane-button-open-close[data-pane=' + pane + ']');
        var elementName = btnOpenClose.text().split(' ')[1];
        btnOpenClose.text((isOpen ? 'Close ' : 'Open ') + elementName);
        var btnShowHide = $('button.pane-button-show-hide[data-pane=' + pane + ']');
        var elementName = btnShowHide.text().split(' ')[1];
        btnShowHide.text((isShown ? 'Hide ' : 'Show ') + elementName);
    }

    // Initialize the buttons
    function initPaneButtons(pane) {
        updatePaneButtons(pane, yarkon.isPaneOpen(pane), yarkon.isPaneShown(pane))
    }
    initPaneButtons(yarkon.Pane.North);
    initPaneButtons(yarkon.Pane.West);
    initPaneButtons(yarkon.Pane.East);
    initPaneButtons(yarkon.Pane.South);

    // Bind the buttons. Toggle the pane on click
    $('.pane-button-open-close').click(function(e) {
        var pane = $(this).data('pane');
        yarkon.isPaneOpen(pane) ? yarkon.closePane(pane) : yarkon.openPane(pane);
    });
    $('.pane-button-show-hide').click(function(e) {
        var pane = $(this).data('pane');
        yarkon.isPaneShown(pane) ? yarkon.hidePane(pane) : yarkon.showPane(pane);
    });

    // Wire the event handler
    yarkon.onChangeLayout(function(e, args) {
        // Update the buttons to reflect current status
        updatePaneButtons(args.pane, args.isOpen, args.isShown);
    });

</script>

Updating Item Properties

You can use Yarkon to update item attributes such as Metadata and storage class. All attributes that can be set using the S3 copyObject command can be updated.

The following code example demonstrates how to update the server side encryption, storage class and metadata for all currently selected items.

<script>
    var selectedItems = yarkon.getSelectedItems();
    if (selectedItems && selectedItems.length) {
        yarkon.updateItemsAttributes(selectedItems, {
            ServerSideEncryption: 'AES256',
            StorageClass: 'REDUCED_REDUNDANCY',
            Metadata: {
                'updated-by': 'yarkon'
            },
            MetadataDirective: 'REPLACE'
        }, function(err, data) {
            if (!err) {
                console.log('Updated items.');
            } else {
                console.error(err);
            }
        });
    } else {
        alert('There are no selected items');
    }
</script>

Searching is a critical feature of most implementations. Yarkon allows you to customize the search function using the built-in event handlers, or even replace it completely.

You can replace the search function provided by Yarkon with your own implementation, that might be a better fit for your specific use case. Or, maybe you want to extend the search to look into file metadata as well - using an existing index you already have in DynamoDB.

The following code shows how to replace the default search function with a custom one.

<script>
    // The search handler expects an array of items.
    // For the purpose of this sample, replace the Yarkon standard
    // recursive search with a search that only looks into this
    // folder...
    var shallowSearch = function(args, callback) {
        var folder = args.folder;
        var searchFor = args.searchFor;
        console.log('Searching for "' + searchFor + '" in folder "' + folder.title + '".');

        searchFor = '.*' + searchFor.replace(/[*]/g, '.*').replace(/[?]/g, '.') + '.*';
        var reSearch = new RegExp(searchFor, 'i');
        var items = yarkon.getItems().filter(function(item) {
            return reSearch.test(item.title);
        });
        callback(null, items);
    }

    yarkon.onBeginSearch(function(e, args) {
        // Replace with our custom function
        args.onSearch = shallowSearch;
    });
</script>

You can also refine the search results, or even use the search functionality to provide your own features.

The following example shows how to enhance Yarkon with new search based functions:

  • Find the largest 3 documents in a folder.
  • Find the last 3 documents updated in a folder.

This sample also shows how to add buttons to the toolbar.

<script>
    // Implement special search functions, returning the top three
    // documents by size or recency, in the active folder

    // Add custom buttons to the toolbar, left of the "Settings" button
    $('button.navbar-btn[title="Settings"]').after([
        '<div class="btn-group navbar-left">',
        '<button type="button" class="btn btn-default navbar-btn" onclick="yarkon.findDocs(\'largest\', 3)" data-bind="koEnablerButton: { enable: function() { return true; } }" title="Find largest 3"><i class="fa fa-sort-amount-asc"></i></button>',
        '<button type="button" class="btn btn-default navbar-btn" onclick="yarkon.findDocs(\'recent\', 3)" data-bind="koEnablerButton: { enable: function() { return true; } }" title="Find recent 3"><i class="fa fa-calendar"></i></button>',
        '</div>'
    ].join(''));

    yarkon.findDocs = function(sortBy, max) {
        // When clicked, initiate standard search, looking for *.* (all).
        // Use the optional params to signal to the end search handler
        // that this is our special search
        var folder = yarkon.getActiveFolder();
        yarkon.searchInFolder(folder, '*.*', {
            field: sortBy,
            max: max
        });
    };

    // When the search starts, tell the user what is happening
    yarkon.onBeginSearch(function(e, args) {
        if (args.params && args.params.field) {
            $('span#search-results-header').html('<i class=\'fa fa-spinner fa-spin\'></i> Searching for ' +
                args.params.field + ' ' + args.params.max + ' documents in folder \'' + args.folder.title + '\'');
        }
    });

    // When done with our custom search, filter, sort and clip the results
    yarkon.onEndSearch(function(e, args) {
        if (args.params && args.params.field) {
            // This is our custom search
            var sortFn = (args.params.field === 'largest') ?
                function(i1, i2) { return i2.size - i1.size; } :
                function(i1, i2) { return i2.modified - i1.modified; };
            args.items = args.items
                // Take only documents
                .filter(function(item) { return !item.isFolder; })
                // Sort by the field we are looking at
                .sort(sortFn)
                // Take top max documents
                .slice(0, args.params.max);

            // Also update the search results title to reflect what
            // was done here... note that we have to update the UI
            // behind the default functionality, so use setTimeout.
            setTimeout(function() {
                $('span#search-results-header').html('Search for ' + args.params.field + ' ' + args.params.max +
                    ' documents in folder \'' + args.folder.title + '\'');
            }, 0);
        }
    });
</script>

Advanced samples

Code Browser

Suppose you have an S3 folder where you keep some code project you would like to browse. Using Yarkon, let's create a code browser with preview.

The following steps are demonstrated:

  1. Customize the Yarkon layout to only show the Tree view and the Grid view.
  2. Add a generic code preview in HTML.
  3. Update the preview when the user clicks a document in Yarkon.

This sample uses "microlight.js":http://asvd.github.io/microlight/ for the syntax highlighting.

<script>
    // Set an arbitrary size for the preview limit. Use 100Kb for this demo.
    var PREVIEW_MAX_FILE_SIZE = 100 * 1024;

    // Keep the preview control in a var for convenience
    var $preview = $('div.microlight');

    // Hide the toolbar, search and preview
    yarkon.hidePane(yarkon.Pane.North);
    yarkon.hidePane(yarkon.Pane.South);
    yarkon.hidePane(yarkon.Pane.East);

    // Helper function to update the preview pane
    function updatePreview() {

        // Get the active item; if a file, update the preview with the
        // content, otherwise, clear the preview.
        var item = yarkon.getActiveItem();
        if (!item || item.isFolder) {
            // Clear
            console.log('Clearing preview');
            $preview.text('Please select a code file.');
            microlight.reset();
        } else {
            // Update
            console.log('Update preview for ' + item.id);

            // Check that the item is not too large.
            if (item.size > PREVIEW_MAX_FILE_SIZE) {
                // Too big for this humble demo
                console.warn('File size is too large for this preview');
                $preview.text('Sorry, too large. This preview is limited to ' + PREVIEW_MAX_FILE_SIZE + ' bytes.');
                microlight.reset();
            } else {
                // Get the item data from S3 using Yarkon
                yarkon.getItemData(item, function(err, data) {
                    if (err) {
                        console.error('Failed getting item data: ' + error);
                        $preview.text('Oops, error: ' + error);
                    } else {
                        // Try to show this file content.
                        $preview.text(String(data.Body));
                    }
                    microlight.reset();
                })
            }
        }
    }

    // Wire all events that might affect the preview
    yarkon.onSelectFolder(function(e, args) {
        updatePreview();
    });

    yarkon.onChangeActiveItem(function(e, args) {
        updatePreview();
    });
</script>

File Download

With Yarkon you can download files and folders from S3. The API is flexible and will use sensible defaults when input parameters are not provided:

  • downloadItems - will download the specified items. If none are provided, will download the current selection in the grid. If there is no selection, will download the active item in the grid, if there is one.
  • downloadFolder - will download the specified folder. If not is provided, will download the content of the current active folder.
<script>
    // Wire the download button
    $('#btn-download-selection').click(function(e) {
        e.preventDefault();
        // Download the current selection
        yarkon.downloadItems();
    });

    $('#btn-download-folder').click(function(e) {
        e.preventDefault();
        // Download the current active folder
        yarkon.downloadFolder();
    });

    // Wire the download event. This will fire for the above commands, as well
    // as for user initiated downloads from the Yarkon UI.
    yarkon.onDownload(function(e, args) {
        var firstItem = $.isArray(args.items) ? args.items[0] : args.items;
        appendLog('Begin download:<br/>Bucket: ' + args.bucket + '<br/>First item: ' + firstItem.id);
    });
</script>

File Upload

Yarkon allows you to modify the meta-data for documents being uploaded. This sample shows you how.

For this sample to actually upload documents to S3, you will have to use your own bucket and access credentials. The credentials provided with this SDK are READ-ONLY and will not allow file upload.

<script>
    // Wire the upload button
    $('#btn-upload').click(function(e) {
        e.preventDefault();
        // Upload into the current active folder
        yarkon.uploadIntoFolder();
    });

    // Wire all events that should be logged
    yarkon.onBeginUpload(function(e, args) {
        appendLog('Begin upload:<br/>Bucket: ' + args.bucket + '<br/>Folder: ' + args.folder + '<br/>First file: ' + args.files[0].name + '<br/>Params: ' + JSON.stringify(args.params));

        // Add the upload timestamp to the files
        if (!args.params.metadata) {
            args.params.metadata = {};
        }
        args.params.metadata['uploaded-by'] = 'Yarkon demo';
        args.params.metadata['uploaded-on'] = (new Date()).toUTCString();
    });

    yarkon.onBeginUploadFile(function(e, args) {
        appendLog('Begin upload file:<br/>Bucket: ' + args.bucket + '<br/>Path: ' + args.path + '<br/>Content type: ' + args.contentType + '<br/>Params: ' + JSON.stringify(args.params));

        // Do not upload files that have a "json" extension
        var ext = args.path.substr(args.path.lastIndexOf('.') + 1).toLowerCase();
        if (ext === 'json') {
            args.params.skip = true;
        }
    });

    yarkon.onEndUploadFile(function(e, args) {
        appendLog('End upload file: ' + JSON.stringify(args));
    });

    yarkon.onEndUpload(function(e, args) {
        appendLog('End upload: ' + JSON.stringify(args));
    });
</script>

Image Preview

Yarkon shows a preview when an image is selected in the main view. Instead of showing the actual image there, you can show a thumbnail, thus improving the responsiveness of the user experience.

You will have to implement some mechanism to have these thumbnails available - the common approach is to generate a thumbnail for each image uploaded to your S3 storage and store these thumbnails in a central location. Amazon provided a helpful example showing one way of getting it done using an AWS lambda function: Create thumbnails using lambda.

For the purpose of this example, we created thumbnails for all images, and are stored in a dedicated bucket named yarkon-sdk-thumbnails. To make sure there is no name collision of images, we named the thumbnails using an MD5 hash of the original S3 url.

The following code will then use the thumbnail image instead of the original:

<script>
    // For this sample to work, we created thumbnails for all images
    // we have in the "yarkon-sdk-images" bucket. To make the solution
    // generic, we use the MD5 of the S3 path of the original images for
    // the file names of the thumbnails, so we can store them all in the
    // same S3 bucket.

    // In a production environment, you'd have to add some code that
    // creates thumbnails whenever a new image is added to a bucket.

    yarkon.onShowPreview(function(e, args) {
        // We use our prepared thumbnails instead of the actual images.
        // Our thumbnails are stored in a single flat folder, using the
        // MD5 hash of the original S3 path.
        args.params.bucket = 'yarkon-sdk-thumbnails';
        args.params.key = CryptoJS.MD5(args.bucket + '/' + args.key).toString() + '.jpg';

        // Since the thumbnail is smaller than the original, tell the
        // viewer what the original dimensions were, so it displays
        // correctly. In this example, all our images have the same
        // dimensions. If you have images with different dimensions,
        // the original sizes have to be kept somewhere - a good place
        // would be the S3 metadata associated with the thumbnail.
        args.params.imageWidth = 979;
        args.params.imageHeight = 734;
    });
</script>