Connect to Box Explorer Component using Ruby on Rails

Hello Everyone,

The target audience of this post is limited.

In this article of mine, I will write down the steps of how you can connect with Box Explorer Component to allow users to upload file into your Box account.

Requirement: We want to allow our users to upload file to somewhere in the cloud where we (me and my team) can see the files and take further actions on it

There are many solutions to this: You have AWS S3, DropBox, Google Cloud and lots more but if you do not want to spend much time on designing and engineering the front-end (by designing I mean building the entire UI for file management), you can use Box.com’s easy to integrate Box Explorer Component.

Related links:

  1. https://developer.box.com/guides/embed/ui-elements/explorer/
  2. https://app.box.com/developers/console
  3. https://app.box.com/master
  4. https://github.com/box-community/samples-docs-authenticate-with-jwt-api/blob/master/sample.rb

Prerequisites:

  1. Ruby >= 2
  2. Rails >= 5
  3. A Box.com account
  4. A Box.com App – You can create the same from Developer’s console (see Related links#2)
  5. Make sure your app is authorised. You can do the same from here: https://app.box.com/master/custom-apps
  6. A config.json file that you can download from: https://app.box.com/developers/console/app/:app_id/configuration. You’ll have to generate Public/Private keypair for this – You can do the same from the same page.
  7. You’ll have to add your domain to allowed origins via above link.

Sample Output / Demo: https://codepen.io/box-platform/pen/wdWWdN. In addition you’ll also see options to add / upload file / folder and perform operations like Search for uploads, Download, Share, Delete etc.

Once you are done setting up above things, let’s code this:

# lib/box_api.rb
require 'json'
require "openssl"
require 'securerandom'
require 'jwt'
require 'json'
require 'uri'
require 'net/https'

class BoxApi
  def initialize unique_identifier = nil
    @unique_identifier  = unique_identifier
    @access_token       = Rails.cache.fetch("users/#{@unique_identifier}/box/access_token")
  end

  def get_token
    return @access_token unless @access_token.nil?

    set_auth
    # Make the request
    uri = URI.parse(@authentication_url)
    http = Net::HTTP.start(uri.host, uri.port, use_ssl: true)
    request = Net::HTTP::Post.new(uri.request_uri)
    request.body = @params
    response = http.request(request)

    # Parse the JSON and extract the access token
    @access_token = Rails.cache.fetch("users/#{@unique_identifier}/box/access_token", expires_in: 60.minutes) do
      JSON.parse(response.body)['access_token']
    end
    return @access_token
  end

  def find_or_create_folder_by_folder_name folder_name = @unique_identifier
    root_folder     = find_folder "Client Uploads"
    root_folder     = create_folder "Client Uploads" if !root_folder
    child_folder    = find_folder folder_name, root_folder["id"]
    child_folder    = create_folder folder_name, root_folder["id"] if !child_folder

    return child_folder["id"]
  end

  def find_folder folder_name, parent_folder_id = nil
    base_url = "https://api.box.com/2.0/search?query='#{folder_name}'&type=folder"
    base_url += "&ancestor_folder_ids=#{parent_folder_id}" if parent_folder_id

    uri = URI.parse(base_url)
    request = Net::HTTP::Get.new(uri)
    request["Authorization"] = "Bearer #{@access_token}"

    req_options = {
      use_ssl: uri.scheme == "https",
    }

    response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
      http.request(request)
    end

    response_body = JSON.parse(response.body)

    return false if response_body["total_count"] == 0 && response_body["entries"].empty?
    return response_body["entries"][0]
  end

  def create_folder folder_name, parent_folder_id = nil
    uri = URI.parse("https://api.box.com/2.0/folders")
    request = Net::HTTP::Post.new(uri)
    request.content_type = "application/json"
    request["Authorization"] = "Bearer #{@access_token}"

    params = {}
    params["name"] = folder_name
    params["parent"] = {
      "id" => parent_folder_id ? parent_folder_id : "0"
    }

    request.body = JSON.dump(params)

    req_options = {
      use_ssl: uri.scheme == "https",
    }

    response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
      http.request(request)
    end

    response_body = JSON.parse(response.body)
    return response_body if response.code == "201"
  end

private
  def set_auth
    @config = JSON.parse(
      File.read("#{Rails.root.join}/config/box_config.json")
    )
    @config = Rails.env.production? ? @config["production"] : @config["base"]
    @appAuth = @config['boxAppSettings']['appAuth']
    key = OpenSSL::PKey::RSA.new(
      @appAuth['privateKey'],
      @appAuth['passphrase']
    )
    @authentication_url = 'https://api.box.com/oauth2/token'
    claims = {
      iss: @config['boxAppSettings']['clientID'],
      sub: @config['enterpriseID'],
      box_sub_type: 'enterprise',
      aud: @authentication_url,
      # This is an identifier that helps protect against
      # replay attacks
      jti: SecureRandom.hex(64),
      # We give the assertion a lifetime of 45 seconds
      # before it expires
      exp: Time.now.to_i + 45
    }
    keyId = @appAuth['publicKeyID']

    # Rather than constructing the JWT assertion manually, we are
    # using the pyjwt library.
    # The API support "RS256", "RS384", and "RS512" encryption
    assertion = JWT.encode(claims, key, 'RS512', { kid: keyId })

    # We are using the excellent axios package
    # to simplify the API call
    @params = URI.encode_www_form({
      # This specifies that we are using a JWT assertion
      # to authenticate
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      # Our JWT assertion
      assertion: assertion,
      # The OAuth 2 client ID and secret
      client_id: @config['boxAppSettings']['clientID'],
      client_secret: @config['boxAppSettings']['clientSecret']
    })
  end
end

Let’s call this api via controller’s method: `/app/controllers/home_controller.rb`

# /app/controllers/home_controller.rb
class HomeController < ApplicationController
  def connect_to_box
    api        = BoxApi.new(user.company.name)
    token      = api.get_token
    folder_id  = api.find_or_create_folder_by_folder_name

    respond_to do | wants |
      wants.json do
        render json: {
          success: true,
          access_token: token,
          folder_id: folder_id
        }
      end
    end
  end
end

and finally let’s load token and folder_id via Ajax call from view

/app/views/home/files.html.erb

<%= content_for :page_css do %>
  <link rel="stylesheet" href="https://cdn01.boxcdn.net/platform/elements/13.0.0/en-US/explorer.css" />
<% end %>

<%= content_for :page_js do %>
  <!-- polyfill.io only loads the polyfills your browser needs -->
  https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Intl
  https://cdn01.boxcdn.net/platform/elements/13.0.0/en-US/explorer.js
<% end %>

<div class=<%= "#{controller_name}_#{action_name}" %>>
  <div id="uploader-panel" class="box-light-blue">
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <div class="text-center result-overlay">
            <%= image_tag 'pageloader.svg' %>
          </div>
          <div class="box-list-container" style="width: 100%; height: 100%; margin: 0; padding: 0; min-width: 320px;"></div>
        </div>
      </div>
    </div>
  </div>
</div>

<script type="text/javascript">
  $(document).ready(function() {
    //connecting to box to fetch required token and folder
    $.ajax({
      url: '/home/connect_to_box',
      type: 'POST',
      dataType: 'json',
      success: function(response) {
        if(response['success'] === true){
          $("div.result-overlay").hide();
          const folderId = response['folder_id'];
          const accessToken = response['access_token'];
          const logoUrl = "<%= asset_path('logo-blue.jpg') %>";
          const contentExplorer = new Box.ContentExplorer();

          // for more options visit: https://developer.box.com/guides/embed/ui-elements/uploader/#options
          contentExplorer.show(folderId, accessToken, {
            container: ".box-list-container",
            canDownload: true,
            canUpload: true,
            canCreateNewFolder: true,
            sortBy: "date",
            sortDirection: "DESC",
            logoUrl: logoUrl,
            onClose: null
          });

          contentExplorer.on('upload', (data) => {
            //notify via slack
            $.ajax({
              url: '/home/upload_notify',
              type: 'POST',
              beforeSend: function(xhr) {xhr.setRequestHeader('X-CSRF-Token', $('meta[name="csrf-token"]').attr('content'))},
              dataType: "json",
              data: {files: JSON.stringify(data)}
            });
          });

          contentExplorer.on('error', (data) => {
            alert(`Error uploading file with name "${data.file.name}". The error was: "${data.error}"`);
          });
        }
      }
    })
  });
</script>

Here’s what’s happening:

  1. When the page loads we are firing Ajax call to get access_token and folder_id from Box. folder_id is the ID of the folder where the user will upload all files. We are giving different folder for each user who belongs to different company (see table below).
  2. The call to Box API checks if the access token is present for the company, it will return that else it will connect to Box, get the access token, set the cache and return the token – Important thing to note that the token returned from Box is valid for 60 minutes
  3. Based on the returned token, we will check if the folder is present for the company’s user or not!
  4. The last part of the Ajax call I have written will send the slack notification.
UserCompany NameFolder on Box
User-AABCABC
User-BXYZXYZ
User-CABCABC
User-DA123A123

That’s it. Hook it up and see it in action.

Configure Delayed Job with Elastic Beanstalk

Recently, I got a chance to work with Elastic Beanstalk and trust me, it is not as friendly as Heroku and it took me a little while to get used to with Beanstalk deployments.

I am using delayed_job in my project and the requirement was very simple, i.e. to automate background jobs. On heroku we simply add a worker dyno and jobs start working immediately. But, same is not the case with Beanstalk. I came across with various article where people suggested to add a shell script inside `.ebextensions` but I did not find this a full proof solution. Then my colleague suggested me to try `active_elastic_job` @ https://github.com/tawan/active-elastic-job

I find this gem extremely helpful and the installation instructions are super simple.

In this article, I will not dig into how to configure, install and start using this gem (because if you visit the link, the steps are defined in simplest form and should not block you). My idea, is to highlight/cover the areas which are not mentioned there or missing.

1. When you create a worker environment, skip selecting the RDS section (which means you’ll create your worker environment without a RDS DB).

2. Now you’ll have to use the existing database (which your web environment is using). For that, copy all RDS environment variables from web environment to your worker environment.

3. Once above is done, deploy your the code to your worker environment. In case your deployment fails with reason:


PG::ConnectionBad: could not connect to server: Connection timed out

Then, go to Services ~> Elastic Beanstalk ~> You Web Application ~> Configuration ~> Instances ~> Modify and note down the Security Group there (using new UI of Beanstalk)

Now, go to Services ~> Elastic Beanstalk ~> Your worker application ~> Configuration ~> Instances ~> Modify and check the security group which your Web Application is using (Note that you can check as many as you want) and Save your changes. This should allow your worker app to use the same DB.

4. Once you have successfully deployed and configured your worker app and you get:


ActiveJob::QueueAdapters::ActiveElasticJobAdapter::SerializedJobTooBig
The job contains bytes in its serialzed form, which exceeds the allowed maximum of 262144 bytes imposed by Amazon SQS

That means you are not allowed to send more than 256KB of data to delayed job. This could prove a bottleneck and you’ll have to think of a workaround to deal with this error. Although, folks on internet suggested that we could use Amazon Extended Client Library for JAVA @ https://github.com/awslabs/amazon-sqs-java-extended-client-lib but I wasn’t able to use and apply this in my Rails app.

Having said that, if anyone knows how to use this in Rails, please drop a comment or as an alternate try to send data < 256KB to SOS queue

That’s all I have for now. See you soon! Happy Coding. 🙂

Get Yourself Upto date with Ruby 1.9.2 and Rails 3

Hello Everyone,

Today, where lots of people are shifting from older versions of ruby to the newer and stable one i.e 1.9.2 and rails 3. I am also shifting to this, and would like to share my thoughts and opinions about upgrading, its advantages, hurdles and future concerns.

To try this.. I am using both Linux (Ubuntu) and Windows (windows 7).
Let’s cover each topic in detail

  • Setting up Ruby-1.9.2
  • What’s new in Ruby-1.9.2
  • Few of the working ruby code
  • Setting up Rails-3 with Ruby-1.9.2
  • Setting up Passenger or Apache with Rails-3
  • A sample application in Ruby-1.9.2 and in Rails-3, featuring:
      Authentication
      Image Upload
      Pagination
      Facebook Integration
      Twitter Integration
      Google Earth integration
      Post with Title and tags

I may cover this using different articles.

Meanwhile, there is an Rails-3 Application created by me, which is currently on Github. I would request, if someone is interested to see the changes Rails-3 has introduced, please download the application and run it on your local. Here is the path for it: https://github.com/puneetpandey/file_upload

Prerequisites:
1. Ruby 1.8.7 or higher
2. Rails-3
3. Mysql Database