browse-everything 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +61 -9
  3. data/.rubocop_todo.yml +2 -15
  4. data/.travis.yml +19 -19
  5. data/CONTRIBUTING.md +6 -6
  6. data/Gemfile +12 -8
  7. data/README.md +30 -0
  8. data/Rakefile +2 -1
  9. data/app/assets/javascripts/browse_everything/behavior.js.coffee +5 -0
  10. data/app/controllers/browse_everything_controller.rb +75 -23
  11. data/app/helpers/browse_everything_helper.rb +2 -8
  12. data/app/helpers/font_awesome_version_helper.rb +9 -8
  13. data/app/services/browse_everything_session.rb +10 -0
  14. data/app/services/browse_everything_session/provider_session.rb +42 -0
  15. data/app/services/browser_factory.rb +25 -0
  16. data/app/views/browse_everything/_files.html.erb +56 -6
  17. data/browse-everything.gemspec +29 -25
  18. data/config/routes.rb +7 -2
  19. data/lib/browse-everything.rb +2 -0
  20. data/lib/browse_everything.rb +45 -12
  21. data/lib/browse_everything/auth/google/credentials.rb +28 -0
  22. data/lib/browse_everything/auth/google/request_parameters.rb +61 -0
  23. data/lib/browse_everything/browser.rb +11 -4
  24. data/lib/browse_everything/driver/authentication_factory.rb +22 -0
  25. data/lib/browse_everything/driver/base.rb +72 -19
  26. data/lib/browse_everything/driver/box.rb +46 -17
  27. data/lib/browse_everything/driver/dropbox.rb +36 -10
  28. data/lib/browse_everything/driver/file_system.rb +14 -26
  29. data/lib/browse_everything/driver/google_drive.rb +187 -54
  30. data/lib/browse_everything/driver/s3.rb +81 -75
  31. data/lib/browse_everything/engine.rb +3 -2
  32. data/lib/browse_everything/file_entry.rb +3 -1
  33. data/lib/browse_everything/retriever.rb +103 -31
  34. data/lib/browse_everything/version.rb +3 -1
  35. data/lib/generators/browse_everything/assets_generator.rb +3 -2
  36. data/lib/generators/browse_everything/config_generator.rb +11 -9
  37. data/lib/generators/browse_everything/install_generator.rb +3 -2
  38. data/lib/generators/browse_everything/templates/browse_everything_providers.yml.example +12 -11
  39. data/spec/controllers/browse_everything_controller_spec.rb +80 -0
  40. data/spec/features/select_files_spec.rb +13 -13
  41. data/spec/features/test_compiling_stylesheets_spec.rb +2 -0
  42. data/spec/fixtures/vcr_cassettes/google_drive.yml +331 -0
  43. data/spec/fixtures/vcr_cassettes/retriever.yml +93 -0
  44. data/spec/helper/browse_everything_controller_helper_spec.rb +21 -7
  45. data/spec/javascripts/jasmine_spec.rb +2 -0
  46. data/spec/javascripts/support/jasmine_helper.rb +1 -0
  47. data/spec/lib/browse_everything/auth/google/credentials_spec.rb +41 -0
  48. data/spec/{unit → lib/browse_everything}/browse_everything_helper_spec.rb +2 -0
  49. data/spec/lib/browse_everything/browser_spec.rb +109 -0
  50. data/spec/{unit → lib/browse_everything/driver}/base_spec.rb +5 -4
  51. data/spec/{unit → lib/browse_everything/driver}/box_spec.rb +20 -5
  52. data/spec/{unit → lib/browse_everything/driver}/dropbox_spec.rb +15 -18
  53. data/spec/{unit → lib/browse_everything/driver}/file_system_spec.rb +32 -26
  54. data/spec/lib/browse_everything/driver/google_drive_spec.rb +171 -0
  55. data/spec/{unit → lib/browse_everything/driver}/s3_spec.rb +38 -21
  56. data/spec/lib/browse_everything/driver_spec.rb +38 -0
  57. data/spec/{unit → lib/browse_everything}/file_entry_spec.rb +4 -1
  58. data/spec/lib/browse_everything/retriever_spec.rb +200 -0
  59. data/spec/lib/browse_everything_spec.rb +67 -0
  60. data/spec/services/browse_everything_session/provider_session_spec.rb +50 -0
  61. data/spec/services/browser_factory_spec.rb +40 -0
  62. data/spec/spec_helper.rb +39 -18
  63. data/spec/support/app/controllers/file_handler_controller.rb +4 -4
  64. data/spec/support/app/views/file_handler/main.html.erb +1 -1
  65. data/spec/support/capybara.rb +17 -0
  66. data/spec/support/rake.rb +3 -1
  67. data/spec/support/wait_for_ajax.rb +14 -0
  68. data/spec/test_app_templates/Gemfile.extra +1 -0
  69. data/spec/test_app_templates/lib/generators/test_app_generator.rb +10 -4
  70. data/spec/views/browse_everything/{_file.html.erb_spec.rb → _files.html.erb_spec.rb} +24 -18
  71. data/tasks/ci.rake +2 -0
  72. metadata +159 -107
  73. data/app/views/browse_everything/_file.html.erb +0 -52
  74. data/app/views/browse_everything/resolve.html.erb +0 -1
  75. data/spec/unit/browser_spec.rb +0 -76
  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(browse_everything.js)
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 =~ /^\.\.?$/ ? true : false
18
+ /^\.\.?$/.match(name) ? true : false
17
19
  end
18
20
 
19
21
  def container?
@@ -1,15 +1,49 @@
1
- require 'httparty'
2
- require 'tempfile'
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 = 16384
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
- def retrieve(spec, &block)
30
- if spec.key?('expires') && Time.parse(spec['expires']) < Time.now
31
- raise ArgumentError, "Download spec expired at #{spec['expires']}"
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
- parsed_spec = parse_spec(spec)
72
+ download_options = extract_download_options(options)
73
+ url = download_options.fetch(:url)
35
74
 
36
- case parsed_spec[:url].scheme
75
+ case url.scheme
37
76
  when 'file'
38
- retrieve_file(parsed_spec, &block)
77
+ retrieve_file(download_options, &block)
39
78
  when /https?/
40
- retrieve_http(parsed_spec, &block)
79
+ retrieve_http(download_options, &block)
41
80
  else
42
- raise URI::BadURIError, "Unknown URI scheme: #{parsed_spec[:url].scheme}"
81
+ raise URI::BadURIError, "Unknown URI scheme: #{url.scheme}"
43
82
  end
44
83
  end
45
84
 
46
85
  private
47
86
 
48
- def parse_spec(spec)
49
- result = {
50
- url: ::Addressable::URI.parse(spec['url']),
51
- headers: spec['auth_header'] || {},
52
- file_size: spec.fetch('file_size', 0).to_i
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
- result[:headers].each_pair { |k, v| result[:headers][k] = v.tr('+', ' ') }
56
- result[:file_size] = get_file_size(result) if result[:file_size] < 1
57
- result
106
+ output[:file_size] = get_file_size(output) if output[:file_size] < 1
107
+ output
58
108
  end
59
109
 
60
- def retrieve_file(parsed_spec)
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(parsed_spec[:url].path, 'rb') do |f|
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, parsed_spec[:file_size])
121
+ yield(chunk, retrieved, file_size)
67
122
  end
68
123
  end
69
124
  end
70
125
 
71
- def retrieve_http(parsed_spec)
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
- HTTParty.get(parsed_spec[:url].to_s, stream_body: stream_body, headers: parsed_spec[:headers]) do |chunk|
76
- retrieved += chunk.length
77
- yield(chunk, retrieved, parsed_spec[:file_size])
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
- def get_file_size(parsed_spec)
82
- case parsed_spec[:url].scheme
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 = HTTParty.head(parsed_spec[:url].to_s, headers: parsed_spec[:headers])
87
- (response.content_length || parsed_spec[:file_size]).to_i
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module BrowseEverything
2
- VERSION = '0.15.1'.freeze
4
+ VERSION = '0.16.0'
3
5
  end
@@ -1,10 +1,11 @@
1
- # -*- encoding : utf-8 -*-
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('../templates', __FILE__)
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
- # -*- encoding : utf-8 -*-
1
+ # frozen_string_literal: true
2
+
2
3
  require 'rails/generators'
3
4
 
4
5
  class BrowseEverything::ConfigGenerator < Rails::Generators::Base
5
- desc <<-END_OF_DESC
6
- This generator makes the following changes to your application:
7
- 1. Creates config/browse_everything_providers.yml with a placeholder value
8
- 2. Modifies your app's routes.rb to mount BrowseEverything at /browse
9
- END_OF_DESC
10
- source_root File.expand_path('../templates', __FILE__)
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
- copy_file 'browse_everything_providers.yml.example', 'config/browse_everything_providers.yml'
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
- YAML.dump('file_system' => { home: Rails.root.to_s })
27
+ "file_system:\n home: #{Rails.root}\n"
26
28
  end
27
29
  end
28
30
  end
@@ -1,4 +1,5 @@
1
- # -*- encoding : utf-8 -*-
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('../templates', __FILE__)
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
- # :app_key: YOUR_DROPBOX_APP_KEY
7
- # :app_secret: YOUR_DROPBOX_APP_SECRET
6
+ # app_key: YOUR_DROPBOX_APP_KEY
7
+ # app_secret: YOUR_DROPBOX_APP_SECRET
8
8
  # box:
9
- # :client_id: YOUR_BOX_CLIENT_ID
10
- # :client_secret: YOUR_BOX_CLIENT_SECRET
9
+ # client_id: YOUR_BOX_CLIENT_ID
10
+ # client_secret: YOUR_BOX_CLIENT_SECRET
11
11
  # google_drive:
12
- # :client_id: YOUR_GOOGLE_API_CLIENT_ID
13
- # :client_secret: YOUR_GOOGLE_API_CLIENT_SECRET
12
+ # client_id: YOUR_GOOGLE_API_CLIENT_ID
13
+ # client_secret: YOUR_GOOGLE_API_CLIENT_SECRET
14
14
  # s3:
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
- # :app_key: YOUR_AWS_S3_KEY # :app_key, :app_secret, and :region can be specified
18
- # :app_secret: YOUR_AWS_S3_SECRET # explicitly here, or left out to use system-configured
19
- # :region: YOUR_AWS_S3_REGION # defaults.
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
- require 'capybara/poltergeist'
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
- sleep(5)
16
- click_link('Gemfile.lock')
17
- check('config-ru')
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
- sleep(5)
23
- expect(page).to have_selector('#status', text: '2 items selected')
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