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.
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