browse-everything 0.15.1 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +61 -9
- data/.rubocop_todo.yml +2 -15
- data/.travis.yml +19 -19
- data/CONTRIBUTING.md +6 -6
- data/Gemfile +12 -8
- data/README.md +30 -0
- data/Rakefile +2 -1
- data/app/assets/javascripts/browse_everything/behavior.js.coffee +5 -0
- data/app/controllers/browse_everything_controller.rb +75 -23
- data/app/helpers/browse_everything_helper.rb +2 -8
- data/app/helpers/font_awesome_version_helper.rb +9 -8
- data/app/services/browse_everything_session.rb +10 -0
- data/app/services/browse_everything_session/provider_session.rb +42 -0
- data/app/services/browser_factory.rb +25 -0
- data/app/views/browse_everything/_files.html.erb +56 -6
- data/browse-everything.gemspec +29 -25
- data/config/routes.rb +7 -2
- data/lib/browse-everything.rb +2 -0
- data/lib/browse_everything.rb +45 -12
- data/lib/browse_everything/auth/google/credentials.rb +28 -0
- data/lib/browse_everything/auth/google/request_parameters.rb +61 -0
- data/lib/browse_everything/browser.rb +11 -4
- data/lib/browse_everything/driver/authentication_factory.rb +22 -0
- data/lib/browse_everything/driver/base.rb +72 -19
- data/lib/browse_everything/driver/box.rb +46 -17
- data/lib/browse_everything/driver/dropbox.rb +36 -10
- data/lib/browse_everything/driver/file_system.rb +14 -26
- data/lib/browse_everything/driver/google_drive.rb +187 -54
- data/lib/browse_everything/driver/s3.rb +81 -75
- data/lib/browse_everything/engine.rb +3 -2
- data/lib/browse_everything/file_entry.rb +3 -1
- data/lib/browse_everything/retriever.rb +103 -31
- data/lib/browse_everything/version.rb +3 -1
- data/lib/generators/browse_everything/assets_generator.rb +3 -2
- data/lib/generators/browse_everything/config_generator.rb +11 -9
- data/lib/generators/browse_everything/install_generator.rb +3 -2
- data/lib/generators/browse_everything/templates/browse_everything_providers.yml.example +12 -11
- data/spec/controllers/browse_everything_controller_spec.rb +80 -0
- data/spec/features/select_files_spec.rb +13 -13
- data/spec/features/test_compiling_stylesheets_spec.rb +2 -0
- data/spec/fixtures/vcr_cassettes/google_drive.yml +331 -0
- data/spec/fixtures/vcr_cassettes/retriever.yml +93 -0
- data/spec/helper/browse_everything_controller_helper_spec.rb +21 -7
- data/spec/javascripts/jasmine_spec.rb +2 -0
- data/spec/javascripts/support/jasmine_helper.rb +1 -0
- data/spec/lib/browse_everything/auth/google/credentials_spec.rb +41 -0
- data/spec/{unit → lib/browse_everything}/browse_everything_helper_spec.rb +2 -0
- data/spec/lib/browse_everything/browser_spec.rb +109 -0
- data/spec/{unit → lib/browse_everything/driver}/base_spec.rb +5 -4
- data/spec/{unit → lib/browse_everything/driver}/box_spec.rb +20 -5
- data/spec/{unit → lib/browse_everything/driver}/dropbox_spec.rb +15 -18
- data/spec/{unit → lib/browse_everything/driver}/file_system_spec.rb +32 -26
- data/spec/lib/browse_everything/driver/google_drive_spec.rb +171 -0
- data/spec/{unit → lib/browse_everything/driver}/s3_spec.rb +38 -21
- data/spec/lib/browse_everything/driver_spec.rb +38 -0
- data/spec/{unit → lib/browse_everything}/file_entry_spec.rb +4 -1
- data/spec/lib/browse_everything/retriever_spec.rb +200 -0
- data/spec/lib/browse_everything_spec.rb +67 -0
- data/spec/services/browse_everything_session/provider_session_spec.rb +50 -0
- data/spec/services/browser_factory_spec.rb +40 -0
- data/spec/spec_helper.rb +39 -18
- data/spec/support/app/controllers/file_handler_controller.rb +4 -4
- data/spec/support/app/views/file_handler/main.html.erb +1 -1
- data/spec/support/capybara.rb +17 -0
- data/spec/support/rake.rb +3 -1
- data/spec/support/wait_for_ajax.rb +14 -0
- data/spec/test_app_templates/Gemfile.extra +1 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +10 -4
- data/spec/views/browse_everything/{_file.html.erb_spec.rb → _files.html.erb_spec.rb} +24 -18
- data/tasks/ci.rake +2 -0
- metadata +159 -107
- data/app/views/browse_everything/_file.html.erb +0 -52
- data/app/views/browse_everything/resolve.html.erb +0 -1
- data/spec/unit/browser_spec.rb +0 -76
- data/spec/unit/retriever_spec.rb +0 -109
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module BrowseEverything
|
2
4
|
class Engine < ::Rails::Engine
|
3
5
|
config.assets.paths << config.root.join('vendor', 'assets', 'javascripts')
|
4
6
|
config.assets.paths << config.root.join('vendor', 'assets', 'stylesheets')
|
5
|
-
config.assets.precompile += %w
|
6
|
-
config.assets.precompile += %w(browse_everything.css)
|
7
|
+
config.assets.precompile += %w[browse_everything.js browse_everything.css]
|
7
8
|
end
|
8
9
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module BrowseEverything
|
2
4
|
class FileEntry
|
3
5
|
attr_reader :id, :location, :name, :size, :mtime, :type
|
@@ -13,7 +15,7 @@ module BrowseEverything
|
|
13
15
|
end
|
14
16
|
|
15
17
|
def relative_parent_path?
|
16
|
-
name
|
18
|
+
/^\.\.?$/.match(name) ? true : false
|
17
19
|
end
|
18
20
|
|
19
21
|
def container?
|
@@ -1,15 +1,49 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
3
|
require 'addressable'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'typhoeus'
|
4
6
|
|
5
7
|
module BrowseEverything
|
8
|
+
# Class for raising errors when a download is invalid
|
9
|
+
class DownloadError < StandardError
|
10
|
+
attr_reader :response
|
11
|
+
|
12
|
+
# Constructor
|
13
|
+
# @param msg [String]
|
14
|
+
# @param response [Typhoeus::Response] response from the server
|
15
|
+
def initialize(msg, response)
|
16
|
+
@response = response
|
17
|
+
super(msg)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generate the message for the exception
|
21
|
+
# @return [String]
|
22
|
+
def message
|
23
|
+
"#{super}: #{response.body}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Class for retrieving a file or resource from a storage provider
|
6
28
|
class Retriever
|
29
|
+
CHUNK_SIZE = 16384
|
30
|
+
|
7
31
|
attr_accessor :chunk_size
|
8
32
|
|
33
|
+
class << self
|
34
|
+
def can_retrieve?(uri)
|
35
|
+
Typhoeus.get(uri, headers: { Range: 'bytes=0-0' }).success?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Constructor
|
9
40
|
def initialize
|
10
|
-
@chunk_size =
|
41
|
+
@chunk_size = CHUNK_SIZE
|
11
42
|
end
|
12
43
|
|
44
|
+
# Download a file or resource
|
45
|
+
# @param options [Hash]
|
46
|
+
# @param target [String, nil] system path to the downloaded file (defaults to a temporary file)
|
13
47
|
def download(spec, target = nil)
|
14
48
|
if target.nil?
|
15
49
|
ext = File.extname(spec['file_name'])
|
@@ -26,65 +60,103 @@ module BrowseEverything
|
|
26
60
|
target
|
27
61
|
end
|
28
62
|
|
29
|
-
|
30
|
-
|
31
|
-
|
63
|
+
# Retrieve the resource from the storage service
|
64
|
+
# @param options [Hash]
|
65
|
+
def retrieve(options, &block)
|
66
|
+
expiry_time_value = options.fetch('expires', nil)
|
67
|
+
if expiry_time_value
|
68
|
+
expiry_time = Time.parse(expiry_time_value)
|
69
|
+
raise ArgumentError, "Download expired at #{expiry_time}" if expiry_time < Time.now
|
32
70
|
end
|
33
71
|
|
34
|
-
|
72
|
+
download_options = extract_download_options(options)
|
73
|
+
url = download_options.fetch(:url)
|
35
74
|
|
36
|
-
case
|
75
|
+
case url.scheme
|
37
76
|
when 'file'
|
38
|
-
retrieve_file(
|
77
|
+
retrieve_file(download_options, &block)
|
39
78
|
when /https?/
|
40
|
-
retrieve_http(
|
79
|
+
retrieve_http(download_options, &block)
|
41
80
|
else
|
42
|
-
raise URI::BadURIError, "Unknown URI scheme: #{
|
81
|
+
raise URI::BadURIError, "Unknown URI scheme: #{url.scheme}"
|
43
82
|
end
|
44
83
|
end
|
45
84
|
|
46
85
|
private
|
47
86
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
87
|
+
# Extract and parse options used to download a file or resource from an HTTP API
|
88
|
+
# @param options [Hash]
|
89
|
+
# @return [Hash]
|
90
|
+
def extract_download_options(options)
|
91
|
+
url = options.fetch('url')
|
92
|
+
|
93
|
+
# This avoids the potential for a KeyError
|
94
|
+
headers = options.fetch('auth_header', {}) || {}
|
95
|
+
headers.each_pair { |k, v| headers[k] = v.tr('+', ' ') }
|
96
|
+
|
97
|
+
file_size_value = options.fetch('file_size', 0)
|
98
|
+
file_size = file_size_value.to_i
|
99
|
+
|
100
|
+
output = {
|
101
|
+
url: ::Addressable::URI.parse(url),
|
102
|
+
headers: headers,
|
103
|
+
file_size: file_size
|
53
104
|
}
|
54
105
|
|
55
|
-
|
56
|
-
|
57
|
-
result
|
106
|
+
output[:file_size] = get_file_size(output) if output[:file_size] < 1
|
107
|
+
output
|
58
108
|
end
|
59
109
|
|
60
|
-
|
110
|
+
# Retrieve the file from the file system
|
111
|
+
# @param options [Hash]
|
112
|
+
def retrieve_file(options)
|
113
|
+
file_uri = options.fetch(:url)
|
114
|
+
file_size = options.fetch(:file_size)
|
115
|
+
|
61
116
|
retrieved = 0
|
62
|
-
File.open(
|
117
|
+
File.open(file_uri.path, 'rb') do |f|
|
63
118
|
until f.eof?
|
64
119
|
chunk = f.read(chunk_size)
|
65
120
|
retrieved += chunk.length
|
66
|
-
yield(chunk, retrieved,
|
121
|
+
yield(chunk, retrieved, file_size)
|
67
122
|
end
|
68
123
|
end
|
69
124
|
end
|
70
125
|
|
71
|
-
|
126
|
+
# Retrieve a resource over the HTTP
|
127
|
+
# @param options [Hash]
|
128
|
+
def retrieve_http(options)
|
129
|
+
file_size = options.fetch(:file_size)
|
130
|
+
headers = options.fetch(:headers)
|
131
|
+
url = options.fetch(:url)
|
72
132
|
retrieved = 0
|
73
|
-
stream_body = parsed_spec[:file_size] > 500.megabytes
|
74
133
|
|
75
|
-
|
76
|
-
|
77
|
-
|
134
|
+
request = Typhoeus::Request.new(url.to_s)
|
135
|
+
request.on_headers do |response|
|
136
|
+
raise DownloadError.new("#{self.class}: Failed to download #{url}", response) unless response.code == 200
|
78
137
|
end
|
138
|
+
request.on_body do |chunk|
|
139
|
+
retrieved += chunk.bytesize
|
140
|
+
yield(chunk, retrieved, file_size)
|
141
|
+
end
|
142
|
+
request.run
|
79
143
|
end
|
80
144
|
|
81
|
-
|
82
|
-
|
145
|
+
# Retrieve the file size
|
146
|
+
# @param options [Hash]
|
147
|
+
# @return [Integer] the size of the requested file
|
148
|
+
def get_file_size(options)
|
149
|
+
url = options.fetch(:url)
|
150
|
+
headers = options.fetch(:headers)
|
151
|
+
file_size = options.fetch(:file_size)
|
152
|
+
|
153
|
+
case url.scheme
|
83
154
|
when 'file'
|
84
155
|
File.size(url.path)
|
85
156
|
when /https?/
|
86
|
-
response =
|
87
|
-
|
157
|
+
response = Typhoeus.head(url.to_s, headers: headers)
|
158
|
+
length_value = response.headers['Content-Length'] || file_size
|
159
|
+
length_value.to_i
|
88
160
|
else
|
89
161
|
raise URI::BadURIError, "Unknown URI scheme: #{url.scheme}"
|
90
162
|
end
|
@@ -1,10 +1,11 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'rails/generators'
|
3
4
|
|
4
5
|
class BrowseEverything::AssetsGenerator < Rails::Generators::Base
|
5
6
|
desc 'This generator installs the browse_everything CSS assets into your application'
|
6
7
|
|
7
|
-
source_root File.expand_path('
|
8
|
+
source_root File.expand_path('templates', __dir__)
|
8
9
|
|
9
10
|
def inject_css
|
10
11
|
copy_file 'browse_everything.scss', 'app/assets/stylesheets/browse_everything.scss'
|
@@ -1,13 +1,14 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'rails/generators'
|
3
4
|
|
4
5
|
class BrowseEverything::ConfigGenerator < Rails::Generators::Base
|
5
|
-
desc <<-
|
6
|
-
This generator makes the following changes to your application:
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
source_root File.expand_path('
|
6
|
+
desc <<-DESC
|
7
|
+
This generator makes the following changes to your application:
|
8
|
+
1. Creates config/browse_everything_providers.yml with a placeholder value
|
9
|
+
2. Modifies your app's routes.rb to mount BrowseEverything at /browse
|
10
|
+
DESC
|
11
|
+
source_root File.expand_path('templates', __dir__)
|
11
12
|
|
12
13
|
def inject_routes
|
13
14
|
insert_into_file 'config/routes.rb', after: '.draw do' do
|
@@ -17,12 +18,13 @@ This generator makes the following changes to your application:
|
|
17
18
|
end
|
18
19
|
|
19
20
|
def copy_example_config
|
20
|
-
|
21
|
+
FileUtils.rm 'config/browse_everything_providers.yml', force: true if File.exists? 'config/browse_everything_providers.yml'
|
22
|
+
copy_file 'browse_everything_providers.yml.example', 'config/browse_everything_providers.yml', force: true
|
21
23
|
end
|
22
24
|
|
23
25
|
def insert_file_system_path
|
24
26
|
insert_into_file 'config/browse_everything_providers.yml', before: '# dropbox:' do
|
25
|
-
|
27
|
+
"file_system:\n home: #{Rails.root}\n"
|
26
28
|
end
|
27
29
|
end
|
28
30
|
end
|
@@ -1,4 +1,5 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'rails/generators'
|
3
4
|
|
4
5
|
class BrowseEverything::InstallGenerator < Rails::Generators::Base
|
@@ -6,7 +7,7 @@ class BrowseEverything::InstallGenerator < Rails::Generators::Base
|
|
6
7
|
|
7
8
|
desc 'This generator installs the browse everything configuration and assets into your application'
|
8
9
|
|
9
|
-
source_root File.expand_path('
|
10
|
+
source_root File.expand_path('templates', __dir__)
|
10
11
|
|
11
12
|
def inject_config
|
12
13
|
generate 'browse_everything:config'
|
@@ -3,18 +3,19 @@
|
|
3
3
|
# The file_system provider can be a path to any directory on the server where your application is running.
|
4
4
|
#
|
5
5
|
# dropbox:
|
6
|
-
#
|
7
|
-
#
|
6
|
+
# app_key: YOUR_DROPBOX_APP_KEY
|
7
|
+
# app_secret: YOUR_DROPBOX_APP_SECRET
|
8
8
|
# box:
|
9
|
-
#
|
10
|
-
#
|
9
|
+
# client_id: YOUR_BOX_CLIENT_ID
|
10
|
+
# client_secret: YOUR_BOX_CLIENT_SECRET
|
11
11
|
# google_drive:
|
12
|
-
#
|
13
|
-
#
|
12
|
+
# client_id: YOUR_GOOGLE_API_CLIENT_ID
|
13
|
+
# client_secret: YOUR_GOOGLE_API_CLIENT_SECRET
|
14
14
|
# s3:
|
15
|
-
#
|
16
|
-
#
|
17
|
-
# :
|
18
|
-
# :
|
19
|
-
# :
|
15
|
+
# bucket: YOUR_AWS_S3_BUCKET
|
16
|
+
# response_type: signed_url # set to :public_url for public urls or :s3_uri for an s3://BUCKET/KEY uri
|
17
|
+
# expires_in: 14400 # for signed_url response_type, number of seconds url will be valid for.
|
18
|
+
# app_key: YOUR_AWS_S3_KEY # :app_key, :app_secret, and :region can be specified
|
19
|
+
# app_secret: YOUR_AWS_S3_SECRET # explicitly here, or left out to use system-configured
|
20
|
+
# region: YOUR_AWS_S3_REGION # defaults.
|
20
21
|
# See https://aws.amazon.com/blogs/security/a-new-and-standardized-way-to-manage-credentials-in-the-aws-sdks/
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'signet/errors'
|
5
|
+
|
6
|
+
RSpec.describe BrowseEverythingController, type: :controller do
|
7
|
+
routes { Rails.application.class.routes }
|
8
|
+
|
9
|
+
let(:provider) { instance_double(BrowseEverything::Driver::Base) }
|
10
|
+
|
11
|
+
before do
|
12
|
+
allow(provider).to receive(:authorized?).and_return(true)
|
13
|
+
allow(controller).to receive(:provider).and_return(provider)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#auth' do
|
17
|
+
let(:provider_session) { instance_double(BrowseEverythingSession::ProviderSession) }
|
18
|
+
|
19
|
+
before do
|
20
|
+
allow(controller).to receive(:params).and_return('code' => 'test-code')
|
21
|
+
allow(provider_session).to receive(:data).and_return(nil)
|
22
|
+
allow(provider).to receive(:connect)
|
23
|
+
controller.auth
|
24
|
+
end
|
25
|
+
it 'retrieves the authorization code from the parameters' do
|
26
|
+
expect(provider).to have_received(:connect).with({ 'code' => 'test-code' }, nil)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#show' do
|
31
|
+
let(:file1) { instance_double(BrowseEverything::FileEntry) }
|
32
|
+
let(:file2) { instance_double(BrowseEverything::FileEntry) }
|
33
|
+
|
34
|
+
before do
|
35
|
+
allow(provider).to receive(:contents).and_return([file1, file2])
|
36
|
+
allow(controller).to receive(:render).with(partial: 'files', layout: true)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'renders the files retrieved by the provider' do
|
40
|
+
allow(provider).to receive(:contents).and_return([file1, file2])
|
41
|
+
controller.show
|
42
|
+
expect(controller).to have_received(:render).with(partial: 'files', layout: true)
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when an authentication error occurs while retrieving the files' do
|
46
|
+
let(:provider_session) { instance_double(BrowseEverythingSession::ProviderSession) }
|
47
|
+
|
48
|
+
before do
|
49
|
+
controller.instance_variable_set(:@provider_session, provider_session)
|
50
|
+
allow(controller).to receive(:render).with(partial: 'auth', layout: true)
|
51
|
+
allow(controller).to receive(:render).with(partial: 'files', layout: true).and_raise(Signet::AuthorizationError)
|
52
|
+
allow(provider_session).to receive(:token=)
|
53
|
+
allow(provider_session).to receive(:code=)
|
54
|
+
allow(provider_session).to receive(:data=)
|
55
|
+
controller.show
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'clears the Rails session of auth. data' do
|
59
|
+
expect(provider_session).to have_received(:token=).with(nil)
|
60
|
+
expect(provider_session).to have_received(:code=).with(nil)
|
61
|
+
expect(provider_session).to have_received(:data=).with(nil)
|
62
|
+
expect(provider_session.instance_variable_get(:@provider_session)).to be_nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
context 'when a remote API error occurs while retrieving the files' do
|
67
|
+
before do
|
68
|
+
allow(controller).to receive(:render).with(partial: 'auth', layout: true)
|
69
|
+
allow(controller).to receive(:render).with(partial: 'files', layout: true).and_raise(StandardError)
|
70
|
+
allow(controller).to receive(:reset_provider_session!)
|
71
|
+
controller.show
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'directs the user to reauthenticate after attempting to render the files' do
|
75
|
+
expect(controller).to have_received(:render).with(partial: 'auth', layout: true)
|
76
|
+
expect(controller).to have_received(:reset_provider_session!)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -1,30 +1,30 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
describe 'Choosing files', type: :feature do
|
3
|
+
describe 'Choosing files', type: :feature, js: true do
|
4
4
|
before do
|
5
|
-
Capybara.register_driver :poltergeist do |app|
|
6
|
-
Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: 90)
|
7
|
-
end
|
8
|
-
Capybara.current_driver = :poltergeist
|
9
5
|
visit '/'
|
10
6
|
end
|
11
7
|
|
12
8
|
shared_examples 'browseable files' do
|
13
9
|
it 'selects files from the filesystem' do
|
14
10
|
click_button('Browse')
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
wait_for_ajax
|
12
|
+
|
13
|
+
expect(page).to have_selector '#browse-everything'
|
14
|
+
expect(page).to have_link 'config.ru'
|
15
|
+
find(:css, '#config-ru').set(true)
|
16
|
+
|
18
17
|
within('.modal-footer') do
|
19
|
-
expect(page).to have_selector('span', text: '2 files selected')
|
20
18
|
click_button('Submit')
|
21
19
|
end
|
22
|
-
|
23
|
-
|
20
|
+
|
21
|
+
wait_for_ajax
|
22
|
+
|
23
|
+
expect(page).to have_selector('#status', text: '1 items selected')
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
context 'when Turbolinks are enabled' do
|
27
|
+
context 'when Turbolinks are enabled', fail: true do
|
28
28
|
before { click_link('Enter Test App (Turbolinks)') }
|
29
29
|
it_behaves_like 'browseable files'
|
30
30
|
end
|