Skip to content

Commit

Permalink
Add file upload from Box (IR-30)
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Dolski committed Apr 15, 2020
1 parent 0ea504d commit 0f1f592
Show file tree
Hide file tree
Showing 17 changed files with 333 additions and 12 deletions.
1 change: 1 addition & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@
//= require bootstrap
//= require activestorage
//= require local-time
//= require_tree ../../../vendor/assets/javascripts
//= require_tree .
33 changes: 33 additions & 0 deletions app/assets/javascripts/submissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,39 @@ const SubmissionForm = function() {
}
}
});

// API docs: https://developer.box.com/guides/embed/ui-elements/picker/
const filePicker = new Box.FilePicker();
const accessToken = $("input[name=box_access_token]").val();

filePicker.addListener("choose", function(files) {
console.log(files);
files.forEach(function(file, index) { // TODO: this is obviously quite broken
$.ajax({
type: "GET",
url: file.authenticated_download_url,
headers: {
"Authorization": "Bearer " + accessToken
},
success: function() {
console.log("success");
},
error: function() {
console.log("error");
}
});
//fileTable.addFile(file);
});
});

const folderId = "0"; // the root folder of a Box account is ID 0
filePicker.show(folderId, accessToken, {
container: "#box-container",
modal: {
buttonLabel: "Select Files From Box",
buttonClassName: "btn btn-lg btn-light"
}
});
};

const fileTable = new FileTable();
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

@import "bootstrap";

@import "../../../vendor/assets/stylesheets/picker";
@import "font-awesome-sprockets";
@import "font-awesome";

Expand Down
15 changes: 15 additions & 0 deletions app/controllers/box_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

class BoxController < ApplicationController

##
# Box OAuth2 callback route. Responds to `GET /box/callback`
#
def oauth_callback
raise ActionController::BadRequest, "Missing code" if params[:code].blank?
code = params[:code].gsub(/^A-Za-z0-9/, "")[0..64]
BoxClient.new(session).new_access_token(code)
redirect_to params[:state] || redirect_path
end

end
3 changes: 3 additions & 0 deletions app/controllers/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ def destroy
#
def edit
@submission_profile = @resource.effective_submission_profile

token = BoxClient.new(session).access_token
@access_token = token['access_token'] if token
end

##
Expand Down
110 changes: 110 additions & 0 deletions app/util/box_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
##
# Convenience class used for obtaining and refreshing OAuth access tokens from
# Box and/or the session.
#
# # Usage
#
# If {access_token} returns a non-`nil` value, use it. Otherwise,
# obtain a new token via {new_access_token}.
#
class BoxClient

##
# @param callback_url [String] URL to redirect to.
# @param return_url [String] Information to add to the `state` argument.
# @return [String]
#
def self.authorization_url(callback_url:, state:)
config = ::Configuration.instance
"https://account.box.com/api/oauth2/authorize?response_type=code" +
"&client_id=#{config.box[:client_id]}" +
"&redirect_url=#{callback_url}" +
"&state=#{state}"
end

def initialize(session)
@session = session
end

##
# Fetches the access token from the session. If the token is expired, it is
# refreshed, stored, and returned. If there is no token in the session, `nil`
# is returned.
#
# @return [Hash] Hash with the same structure as the one returned from
# {new_access_token}.
#
def access_token
if @session['box'].present?
if Time.now > @session['box']['expires']
token = refresh_access_token(@session['box']['refresh_token'])
@session['box'] = token
return token
else
return @session['box']
end
end
nil
end

##
# Exchanges the OAuth access code (supplied as a query argument to the OAuth
# callback URL) for an access token, stores it in the session, and returns
# it.
#
# @param code [String]
# @return [Hash] Hash with `:access_token`, `:expires`, and `:refresh_token`
# keys.
# @see https://developer.box.com/reference/post-oauth2-token/
#
def new_access_token(code)
config = ::Configuration.instance
body = {
client_id: config.box[:client_id],
client_secret: config.box[:client_secret],
code: code,
grant_type: "authorization_code"
}
response = post("https://account.box.com/api/oauth2/token", body)
token = token_from_response(response.body)
@session['box'] = token
token
end

private

def post(url, body)
headers = { 'Content-Type': "application/x-www-form-urlencoded" }
HTTPClient.new.post(url, body, headers)
end

##
# @param refresh_token [String]
#
def refresh_access_token(refresh_token)
config = ::Configuration.instance
body = {
client_id: config.box[:client_id],
client_secret: config.box[:client_secret],
refresh_token: refresh_token,
grant_type: "refresh_token"
}
response = post("https://api.box.com/oauth2/token", body)
token_from_response(response.body)
end

##
# @param entity [String] HTTP response entity a.k.a. body.
# @return [Hash] Hash with `:access_token`, `:expires`, and `:refresh_token`
# keys.
#
def token_from_response(entity)
struct = JSON.parse(entity)
{
access_token: struct['access_token'],
expires: Time.now + struct['expires_in'],
refresh_token: struct['refresh_token']
}
end

end
32 changes: 22 additions & 10 deletions app/views/submissions/_deposit_files_form.html.haml
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
-# frozen_string_literal: true
.alert.alert-light
%i.fa.fa-exclamation-triangle
Filenames must be unique, and directory structure is not preserved.

= form_for(@resource, url: submission_path(@resource), remote: true,
html: { id: "files-form" }) do |f|
= token_tag(nil)
Expand Down Expand Up @@ -31,13 +27,29 @@
%i.fa.fa-minus
Remove
-# This input is hidden via CSS. JavaScript sends #file-dropzone click events
-# to it in order to open a file selection dialog.
%input#file-chooser{type: "file", multiple: true}
%ul.nav.nav-pills.nav-justified{role: "tablist"}
%li.nav-item
%a#browser-files-tab.nav-link.active{"data-toggle": "tab",
href: "#browser-files",
role: "tab",
"aria-controls": "browser-files",
"aria-selected": "true"} Upload From Browser
%li.nav-item
%a#box-files-tab.nav-link{"data-toggle": "tab",
href: "#box-files",
role: "tab",
"aria-controls": "box-files",
"aria-selected": "false"} Upload From Box
.tab-content
#browser-files.tab-pane.fade.show.active{role: "tabpanel",
"aria-labelledby": "browser-files-tab"}
= render partial: "deposit_files_form_browser"
#box-files.tab-pane.fade{role: "tabpanel",
"aria-labelledby": "box-files-tab"}
= render partial: "deposit_files_form_box"
#file-drop-zone.bg-light
%i.fa.fa-upload
Attach files (not folders) by dropping them here or selecting them.
.text-center.mb-3.mt-3
%button.btn.btn-light.step-3-to-2{type: "button"}
Expand Down
13 changes: 13 additions & 0 deletions app/views/submissions/_deposit_files_form_box.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- if @access_token.blank?
.text-center
= link_to BoxClient::authorization_url(callback_url: box_callback_url,
state: request.original_url),
class: "btn btn-primary" do
%i.fa.fa-sign-in-alt
Log into Box
- else
-# Read by JavaScript
= hidden_field_tag "box_access_token", @access_token

#box-container
-# JavaScript takes over from here.
7 changes: 7 additions & 0 deletions app/views/submissions/_deposit_files_form_browser.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-# This input is hidden via CSS. JavaScript sends #file-dropzone click events
-# to it in order to open a file selection dialog.
%input#file-chooser{type: "file", multiple: true}

#file-drop-zone.bg-light
%i.fa.fa-upload
Attach files (not folders) by dropping them here or selecting them.
3 changes: 3 additions & 0 deletions config/credentials/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ aws:
access_key_id: my-access-key
secret_access_key: my-secret-key
bucket: ideals-test
box:
client_id:
client_secret:
primary_db:
host: localhost
port: 5432
Expand Down
2 changes: 1 addition & 1 deletion config/credentials/demo.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Qr3MvjqlnpbgLS3J7EZdPMf89uzTPO/LEB6W8RSeeOTWQpxx77n48jLcsnpaqfFcHuLFn14FBWPNG/sQA1jEsM829R+Yc963kBTv7iTI38tKocyt6gsNM5z5fCOdeq+Uo1pt6fbcI6MKjBl3BCmWPepqoG1GMUMo5GPZo5lAErJeo38uvJEehCluiIyJ5KH74WMK/nwFFFF8rq+tms+RCzOC5UjkD/YA0eF/gEw3N0zxr2td4hwAVL8o8uO2YTVHj532tdcWdDFHyb9SFUC8grzlCCCsuwrO3ew7iERlrax18TXE6i8J+cp+VQ6wA3kbFkV0D70f0teVGBzLDoAs1dke4/k3DGlYorfwT6aupp7PqXofefgr0cSOh2QqbVif9D5owhG0jAtQyGBYo0uLdNXCAoIwPqT3Bu4rhKA3iJqxKQ3kykWtPI7lofVgl6a8ISItQt/H1+7wD5MiYQGVRwPbSG/VjBBeswjCPSVSBm5XNlVnxU9M8ZJ8cdeaCEoyXgUewGekedB/37oZGo+7E0aXom15WuryGaazhSIWbXvWugDVt90LcCQmKVMk/q6ULwerIDH+2xA+ta7ZRmch6bXVy9FdA7HzEUGAsRKK3w1u1EcyRa76PiYeG3TFbUNQdIc58xdtx2E5OD82lanA626+/7VC47Sq5yovTd1Rf3sHsMwPywohoqdts0iNYjyOviocfRAlNSlYNotFVsw4hqCdDG3kgsrLqCbhWV8lkrYc7V9p146LskYDJ1XV2OwIPzLrLSHLdgkCJRFrxfKwcf+LHQeZA0PV23qyPpJ+aKqO1UkmIVDvd2F1jIPB7PiZKGwhWTfkbInU8QNzzcRPyipTLeRVYRUw78fXTr1cNgiXOng+XL3m7Jo9OjegFfINOQpw5aJcu6lLScKqunZ+ByXizAjEDdKF5KY+MV/HJZLzX+DbrkWwvTxb16rJQsfqfEHZSvqMvm0oeA4hlJGa60fj+Km5851I1UCXkzYKhlp9J5q93LqcKoWGsbeAj7M3RU/zFJrcJvigZhwuJ2q9QgfqD83q+hWGJfooNDBVyi7LRI1OmRLPBltkZTip7mjcOkh+pAv6ZubSwWXEyrjI05tQd7tEut6Y8o3NtNm0vS7vscahDunAiMtGc/xa9+JaqCBvtPAWta68AcmB5w==--2DZrSin+e1ccXOGI--ezuvk+QXegtA8cH/dR4WvA==
a2rYDieRk9yez7p4GpoVITHrholn16shU00OmmIl2gpS5Rh3oE/XpspGOugRL98crg8cWeO3Nuf4D5zm2mQ1l8gILYKEQkUZyNoNBgtHcQOqhYAf0APcfWgLVj8TMb7GV0qljzr5DF3jrjmWuEjRh0Y9TC+n9cEQ30nwPjZxNNaHg3zZtMeHP7xx6ywP6KDaqHm4YRxI4plD6vQGJb9N92wjB8EwNiW6B0rOvhsntCbflUj8PVuqZRqZF6lgKwL6vqR7q8LVZzaNGta10qlhDjJFAu/P3omWVzumqaSt7kxwe1Md6pvyNz66Umj/PjkBQKD7/YBYXR92h/mQNdPkcZawjadumJUT+8LFyEb7L9FF+pz10D4a255hdY0JgZQOAKPgcd8YnYrhCa6BeVyIx4BRCnfeLH1YhlZZggPcXxCgE+2W27A+rVklBmvC4ye5YNvHw0gekOVyFjX4WGmvlyPYb8Omdprk/dM3t1F6cgPOq2bAgId7izxiJT/YmMC7tf2WW6DY17KtOvcNbMxVMRi2GA3Dg5grKkE9SqEHPdgRGq0z3ouz9lRUkyNDJB9t0RFRe9JG2OX2E730PI54AClKSmUsoSPtM0rIlXFS5sf/3RXmgmLLi9qta4QZ17etrTIOQ5Y4pGcUIS1qzqtekHQdhYYPacLmEPOCSZx8jYChEdBeklVMldw0sQeJdLQXdFO0mZGThiUxY0hYktec+kMulE0McC9wtKIUGK2hUx/eFttLLkh6C20195QouAUn9XCZ7+QRsCxkTw7iNe0/4OpF7a948uuc8l0n6/Clre6/Z+fyoqxK3whH1endMftLfDOyMM5gfkctpRmh/52w1XrDq3t7+EjcvofpX6h++MrJhiT++OkOiI7Z0Lj4bgmdELIfkRnILC68re3x54TPMwG/es0DpDyhhWwXXfjKjGw6R4JdLihuwIKu1bkboJbrXhQ2ZPxqb3KE5cjRMbKNWqrP2geR3o9adqQSA9Q1Z/Beqrnt6TiHy5/oipR/wUxyn85HYfRjy1GByurzeYX4+H1CsgN/uBdcQf8vtvOqVwrUHVjcVzYi/Kflkln8PmIf1VsvypiDWxFRS445S1xDCgBuHq6Yeg79qKlxBxFp73Ks7L7ay+f/0atIiJHDFgODEse4zjOQrth4if9zXPN2YkTKWhjZTb7pXhQkh3WMD83vWQTl11FdP7TRY1kJaCW46kT4OA8wIPPK7TnOh9vzcm1M5X/bF8zLu7e4UpRZPztppRzR/ZAwdp7whHhnEUOKNiglQbjPebsVyqe/KwNtoyyX--PS8v2vwpvTyszM4I--mVBHpN0ZxzChPZ26AEzCbA==
2 changes: 1 addition & 1 deletion config/credentials/production.yml.enc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
qnjJHlT7UNhA1yAeNmffuqPdkDxfY7y0fuIP7uZPhEuFF3tcmKud1OL43PGop/zEFeZM3m9Z3IGFx/H8Mu8iOykdVyeiyV0wc7HpwOagg3WaSChTL0RrfgR8UR/TLM5jfxigN97CjJ8RsoVJr1J2OdaQyS/ZKCp5aPvHsnYTT+BuDTL2zMB1WWzrN+OvMSAwgVkycwYJ6fcqCXakzNxv85D+Zf+mt6/WpDIuIooM/rMaar0Aj2t6+0Wldp+MxG1TCX8PQlISVNMWXMpboEwOTxcPQmMDUsruCHeHb/0Jdh9zhKsenfKeaKQpSYA0aoTZ4ZHfjb+xgRWd8KUw6CbO6h2V3DdCTHh6/f+5sCg9jdhPJCCE9RKP0x/W8I9PqWON0T6q3XD1YBFijGKedtOXrbuaA+DW4IsUWUsfN69Y5PPz/Yd06czJMdMGmMyE9y4K/bCnfhY7qkY/dwjGTNJSC9zzCUkuuouDjfbeCGJjKUqyHE9rmLf21ViJdUQV/CuwoG642o3r6vT/7rNgV4fe2EeWvwQUw+QhaKD656wn2onk8TSBPxFVNN4ReC/gAVWs9bKyWEt0t4XNkkVCy6+zK+7xH/nnEQMQARJbvokOmUBIDZdIW8XQ73t9q6oMMOFXV0mwnXIJkWvZnpFsSzxGnjiXrsn2RWZeH40AGLYKTO9oDHt7g+STnOqTGz3IdTBydMU5ilzOZDwCvKKka2YovWKmQjNHsHpx5ODj+q9mq0Rt43kSUrh77dB6/M/Q61qvZ4NfE3G0szkHuXlS+mMJJHjLn3EcSBXNpvuNigpk3X7IxPwf4PSowgW3/OM4PcF+0ZjeffO47eNyGxBLOsKLqKVfs8XNRY4+6BeTwl3IIdI6hpe/7v9cGRfBk6pcxbZ6rk7l/MsF6Oc5/bKsXydrrsHTERWJYnPZXak9tpdE/rWdzaa+AfXOKXTI2BwY4OUzN3jMQG/uNwLhsx1BPt/fg0DO0ZBmG3x30uZ0KvLeMG5AZUOiGF13e0yYxB6zlb/eT60HTxioHLHhJZPuBqLnLaZU0sHnENuTsrHojJZBPaSAdjtRVZQdV5eW/ww/1cw=--mkKJyPBx48IuCU0C--UdB9VcGoHu8ODh7CDAPvCA==
mMsv9Fk2fBcBkTiCQ9Rcsl8IvSCdSmFGoNWKCFFMH/f0R5f0UxWr47entCMPPojUkvGk+V9bUZvb3XIuGDz1mpluhpAZLrp1iPRsNNrfkTBOxrR3tPFLxkQd4q+YMsXRDT384QZQNqMor6Hcz8xzgTDdnMM3CRg5t6s6q5h2JTLqDvinVypfw8Jo0UZwyLh81sIVJUjLQlyXPQd2vFJqOZfJhHSS46L379ara+6/zdVB0DFoFs42D1XvoEHvTRUBG3I70tKlA1v1xAgKgk93f/SvK46hlx0AU5ZG965/Zk77WmTpPcPJxyIrM8HejcLPJI8G0QVNNYNDy/h240S0FgKgT++ZXAWVvu4QCg1kToRRNUPpi8BkDC3UY8YZtmk5vin3jEO0rjrubWib9CXrspeRMqbU8pesGGZLQqZqtOhZpqmfU9frIGVRWndJTjAVBW52e0AX2uKhh3SoLJsR/JfBbEQq3S/1ULnuluYN05nxb3E+2WtFoJxbM6EUXebK1mQqzCZsQ2h7jgj2PkTYCv1TO+POtloY+9vUJD/ifn263hdumYnCJ+BPQ0lb3kzuImNODeiEgG8bWOmxzPQWHZ+yXbj1sR1y4NthbUUh/xV4XM5LbbuU6tz5SL1+rPMeNOFlIyK0TgUgw0dnT3kzpMKoYx+IjLXVlX1QlBMW60oSjojD65A8kHK6pzhmNRGoLksf2XbBcfeo9Y5SWjNkeE/xz8eRUl9gWhYmRwbTQfgavUWk9shmYy1zPU5a+aO8L9GM34QcEYDWqTyl0W6uPq4RkHK8mbhxKE+TxiRdiqXBxBzTym3CtrdG4VvyD09TloM5o0Kdx4ieqUfKogj4oPcyih6UXWv62tJ2VOAEPR+bffcYefh6A+N/+IevIQAz/oMAg9w8TgjvsdaXKRZ8HyQL822a3qk4Uju7bWewFU8W7qVrzoDO/+Azm7fWmj1v14crPbCDgUs+hAYRU/G1RnJ3QWUFomiJFEEMzTRm5LblgzzsYiHpcIBD8LoEXujXyagiS/lmMat3k51y9RVgq9eXtvrhoYln4z+CqaKIUUKl1g8pFi1S77uTlGiSOwzlqbNCVSdzEhU9co719l5WDx2K4XDNdR0NIJ0QBHuW1uplUDv6tM5PphR8BI+N8M4w13PJm3l3G23IzswaaWrhq3P0U4zQLR5Cz8cESLeswTcao2ukNCe2fwHhy5tG2rmMm6Ieyg==--Iaf0ykNNbkSr3SXx--XFFu7MogA+7xdGA1lNd4/A==
3 changes: 3 additions & 0 deletions config/credentials/template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ aws:
secret_access_key:
# This key is used only in demo & production.
region: us-east-2
box:
client_id:
client_secret:
# Database connection settings, which will get injected into
# config/database.yml.
primary_db:
Expand Down
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
get '/handle/:prefix/:suffix', to: 'handles#resolve'

resources :account_activations, only: [:edit]
match "/box/callback", to: "box#oauth_callback", via: :get
resources :collections, except: [:edit, :new] do
match "/children", to: "collections#children", via: :get,
constraints: lambda { |request| request.xhr? }
Expand Down
93 changes: 93 additions & 0 deletions vendor/assets/javascripts/picker.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions vendor/assets/javascripts/polyfill.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions vendor/assets/stylesheets/picker.css

Large diffs are not rendered by default.

0 comments on commit 0f1f592

Please sign in to comment.