browse-everything 1.0.0.rc2 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +93 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +43 -35
  5. data/CONTRIBUTING.md +3 -3
  6. data/Gemfile +3 -6
  7. data/LICENSE.txt +205 -22
  8. data/README.md +17 -15
  9. data/app/assets/javascripts/browse_everything/behavior.js +201 -158
  10. data/app/assets/javascripts/treetable.webpack.js +687 -0
  11. data/app/assets/stylesheets/browse_everything.scss +0 -0
  12. data/app/assets/stylesheets/{_browse_everything_bootstrap3.scss → browse_everything/_browse_everything_bootstrap3.scss} +6 -5
  13. data/app/assets/stylesheets/{_browse_everything_bootstrap4.scss → browse_everything/_browse_everything_bootstrap4.scss} +0 -0
  14. data/app/controllers/browse_everything_controller.rb +60 -60
  15. data/app/helpers/browse_everything_helper.rb +4 -0
  16. data/app/views/browse_everything/_files.html.erb +4 -2
  17. data/browse-everything.gemspec +8 -7
  18. data/lib/browse_everything.rb +2 -1
  19. data/lib/browse_everything/auth/google/credentials.rb +5 -5
  20. data/lib/browse_everything/auth/google/request_parameters.rb +38 -38
  21. data/lib/browse_everything/driver/base.rb +14 -14
  22. data/lib/browse_everything/driver/box.rb +62 -55
  23. data/lib/browse_everything/driver/dropbox.rb +37 -21
  24. data/lib/browse_everything/driver/file_system.rb +30 -18
  25. data/lib/browse_everything/driver/google_drive.rb +40 -39
  26. data/lib/browse_everything/driver/s3.rb +61 -45
  27. data/lib/browse_everything/file_entry.rb +1 -1
  28. data/lib/browse_everything/retriever.rb +72 -67
  29. data/lib/browse_everything/version.rb +1 -1
  30. data/lib/generators/browse_everything/templates/browse_everything_providers.yml.example +1 -0
  31. data/spec/features/test_compiling_stylesheets_spec.rb +1 -1
  32. data/spec/lib/browse_everything/browser_spec.rb +5 -3
  33. data/spec/lib/browse_everything/driver/dropbox_spec.rb +20 -1
  34. data/spec/lib/browse_everything/driver_spec.rb +43 -3
  35. data/spec/spec_helper.rb +9 -2
  36. data/spec/support/capybara.rb +0 -5
  37. data/spec/test_app_templates/Gemfile.extra +9 -0
  38. data/spec/test_app_templates/lib/generators/test_app_generator.rb +56 -5
  39. metadata +72 -39
  40. data/.travis.yml +0 -33
@@ -83,7 +83,8 @@ module BrowseEverything
83
83
  def contents(path = '')
84
84
  path = '/' + path unless path == ''
85
85
  response = client.list_folder(path)
86
- @entries = response.entries.map { |entry| FileEntryFactory.build(metadata: entry, key: key) }
86
+ values = response.entries.map { |entry| FileEntryFactory.build(metadata: entry, key: key) }
87
+ @entries = values.compact
87
88
  @sorter.call(@entries)
88
89
  end
89
90
 
@@ -91,7 +92,7 @@ module BrowseEverything
91
92
  return @downloaded_files[path] if @downloaded_files.key?(path)
92
93
 
93
94
  # This ensures that the name of the file its extension are preserved for user downloads
94
- temp_file_path = File.join(Dir.mktmpdir, File.basename(path))
95
+ temp_file_path = File.join(download_directory_path, File.basename(path))
95
96
  temp_file = File.open(temp_file_path, mode: 'w+', encoding: 'ascii-8bit')
96
97
  client.download(path) do |chunk|
97
98
  temp_file.write chunk
@@ -137,29 +138,44 @@ module BrowseEverything
137
138
 
138
139
  private
139
140
 
140
- def session
141
- AuthenticationFactory.new(
142
- self.class.authentication_klass,
143
- config[:client_id],
144
- config[:client_secret]
145
- )
146
- end
141
+ def session
142
+ AuthenticationFactory.new(
143
+ self.class.authentication_klass,
144
+ config[:client_id],
145
+ config[:client_secret]
146
+ )
147
+ end
147
148
 
148
- def authenticate
149
- session.authenticate
150
- end
149
+ def authenticate
150
+ session.authenticate
151
+ end
151
152
 
152
- def authenticator
153
- @authenticator ||= authenticate
154
- end
153
+ def authenticator
154
+ @authenticator ||= authenticate
155
+ end
155
156
 
156
- def client
157
- DropboxApi::Client.new(token)
158
- end
157
+ def client
158
+ DropboxApi::Client.new(token)
159
+ end
159
160
 
160
- def redirect_uri(url_options)
161
- connector_response_url(**url_options)
162
- end
161
+ def redirect_uri(url_options)
162
+ connector_response_url(**url_options)
163
+ end
164
+
165
+ # Ensures that the "tmp" directory is used if there is no default download
166
+ # directory specified in the configuration
167
+ # @return [String]
168
+ def default_download_directory
169
+ Rails.root.join('tmp')
170
+ end
171
+
172
+ # Retrieves the directory path for downloads used when retrieving the
173
+ # resource from Dropbox
174
+ # @return [String]
175
+ def download_directory_path
176
+ dir_path = config[:download_directory] || default_download_directory
177
+ File.expand_path(dir_path)
178
+ end
163
179
  end
164
180
  end
165
181
  end
@@ -11,13 +11,17 @@ module BrowseEverything
11
11
  raise BrowseEverything::InitializationError, 'FileSystem driver requires a :home argument' if config[:home].blank?
12
12
  end
13
13
 
14
+ # Retrieve the contents of a directory
15
+ # @param path [String] the path to a file system resource
16
+ # @return [Array<BrowseEverything::FileEntry>]
14
17
  def contents(path = '')
15
18
  real_path = File.join(config[:home], path)
16
- @entries = if File.directory?(real_path)
17
- make_directory_entry real_path
18
- else
19
- [details(real_path)]
20
- end
19
+ values = if File.directory?(real_path)
20
+ make_directory_entry real_path
21
+ else
22
+ [details(real_path)]
23
+ end
24
+ @entries = values.compact
21
25
 
22
26
  @sorter.call(@entries)
23
27
  end
@@ -32,6 +36,10 @@ module BrowseEverything
32
36
  true
33
37
  end
34
38
 
39
+ # Construct a FileEntry objects for a file-system resource
40
+ # @param path [String] path to the file
41
+ # @param display [String] display label for the resource
42
+ # @return [BrowseEverything::FileEntry]
35
43
  def details(path, display = File.basename(path))
36
44
  return nil unless File.exist? path
37
45
  info = File::Stat.new(path)
@@ -47,21 +55,25 @@ module BrowseEverything
47
55
 
48
56
  private
49
57
 
50
- def make_directory_entry(real_path)
51
- entries = []
52
- entries + Dir[File.join(real_path, '*')].collect { |f| details(f) }
53
- end
58
+ # Construct an array of FileEntry objects for the contents of a
59
+ # directory
60
+ # @param real_path [String] path to the file system directory
61
+ # @return [Array<BrowseEverything::FileEntry>]
62
+ def make_directory_entry(real_path)
63
+ entries = []
64
+ entries + Dir[File.join(real_path, '*')].collect { |f| details(f) }
65
+ end
54
66
 
55
- def make_pathname(path)
56
- Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(config[:home]))
57
- end
67
+ def make_pathname(path)
68
+ Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(config[:home]))
69
+ end
58
70
 
59
- def file_size(path)
60
- File.size(path).to_i
61
- rescue StandardError => error
62
- Rails.logger.error "Failed to find the file size for #{path}: #{error}"
63
- 0
64
- end
71
+ def file_size(path)
72
+ File.size(path).to_i
73
+ rescue StandardError => error
74
+ Rails.logger.error "Failed to find the file size for #{path}: #{error}"
75
+ 0
76
+ end
65
77
  end
66
78
  end
67
79
  end
@@ -79,9 +79,10 @@ module BrowseEverything
79
79
  raise error
80
80
  end
81
81
 
82
- @entries += file_list.files.map do |gdrive_file|
82
+ values = file_list.files.map do |gdrive_file|
83
83
  details(gdrive_file, path)
84
84
  end
85
+ @entries += values.compact
85
86
 
86
87
  request_params.page_token = file_list.next_page_token
87
88
  end
@@ -190,50 +191,50 @@ module BrowseEverything
190
191
 
191
192
  private
192
193
 
193
- def client_secrets
194
- {
195
- Google::Auth::ClientId::WEB_APP => {
196
- Google::Auth::ClientId::CLIENT_ID => config[:client_id],
197
- Google::Auth::ClientId::CLIENT_SECRET => config[:client_secret]
198
- }
194
+ def client_secrets
195
+ {
196
+ Google::Auth::ClientId::WEB_APP => {
197
+ Google::Auth::ClientId::CLIENT_ID => config[:client_id],
198
+ Google::Auth::ClientId::CLIENT_SECRET => config[:client_secret]
199
199
  }
200
- end
200
+ }
201
+ end
201
202
 
202
- # This is required for using the googleauth Gem
203
- # @see http://www.rubydoc.info/gems/googleauth/Google/Auth/Stores/FileTokenStore FileTokenStore for googleauth
204
- # @return [Tempfile] temporary file within which to cache credentials
205
- def file_token_store_path
206
- Tempfile.new('gdrive.yaml')
207
- end
203
+ # This is required for using the googleauth Gem
204
+ # @see http://www.rubydoc.info/gems/googleauth/Google/Auth/Stores/FileTokenStore FileTokenStore for googleauth
205
+ # @return [Tempfile] temporary file within which to cache credentials
206
+ def file_token_store_path
207
+ Tempfile.new('gdrive.yaml')
208
+ end
208
209
 
209
- def scope
210
- Google::Apis::DriveV3::AUTH_DRIVE
211
- end
210
+ def scope
211
+ Google::Apis::DriveV3::AUTH_DRIVE
212
+ end
212
213
 
213
- # Provides the user ID for caching access tokens
214
- # (This is a hack which attempts to anonymize the access tokens)
215
- # @return [String] the ID for the user
216
- def user_id
217
- 'current_user'
218
- end
214
+ # Provides the user ID for caching access tokens
215
+ # (This is a hack which attempts to anonymize the access tokens)
216
+ # @return [String] the ID for the user
217
+ def user_id
218
+ 'current_user'
219
+ end
219
220
 
220
- # Please see https://developers.google.com/drive/v3/web/manage-downloads
221
- # @param id [String] the ID for the Google Drive File
222
- # @return [String] the URL for the file download
223
- def download_url(id)
224
- "https://www.googleapis.com/drive/v3/files/#{id}?alt=media"
225
- end
221
+ # Please see https://developers.google.com/drive/v3/web/manage-downloads
222
+ # @param id [String] the ID for the Google Drive File
223
+ # @return [String] the URL for the file download
224
+ def download_url(id)
225
+ "https://www.googleapis.com/drive/v3/files/#{id}?alt=media"
226
+ end
226
227
 
227
- # Restore the credentials for the Google API
228
- # @param access_token [String] the access token redeemed using an authorization code
229
- # @return Credentials credentials restored from a cached access token
230
- def restore_credentials(access_token)
231
- client = Auth::Google::Credentials.new
232
- client.client_id = client_id.id
233
- client.client_secret = client_id.secret
234
- client.update_token!('access_token' => access_token)
235
- @credentials = client
236
- end
228
+ # Restore the credentials for the Google API
229
+ # @param access_token [String] the access token redeemed using an authorization code
230
+ # @return Credentials credentials restored from a cached access token
231
+ def restore_credentials(access_token)
232
+ client = Auth::Google::Credentials.new
233
+ client.client_id = client_id.id
234
+ client.client_secret = client_id.secret
235
+ client.update_token!('access_token' => access_token)
236
+ @credentials = client
237
+ end
237
238
  end
238
239
  end
239
240
  end
@@ -79,63 +79,79 @@ module BrowseEverything
79
79
 
80
80
  private
81
81
 
82
- def strip(path)
83
- path.sub %r{^/?(.+?)/?$}, '\1'
84
- end
82
+ def strip(path)
83
+ path.sub %r{^/?(.+?)/?$}, '\1'
84
+ end
85
85
 
86
- def from_base(key)
87
- Pathname.new(key).relative_path_from(Pathname.new(config[:base].to_s)).to_s
88
- end
86
+ def from_base(key)
87
+ Pathname.new(key).relative_path_from(Pathname.new(config[:base].to_s)).to_s
88
+ end
89
89
 
90
- def full_path(path)
91
- config[:base].present? ? File.join(config[:base], path) : path
92
- end
90
+ def full_path(path)
91
+ config[:base].present? ? File.join(config[:base], path) : path
92
+ end
93
93
 
94
- def aws_config
95
- result = {}
96
- result[:credentials] = Aws::Credentials.new(config[:app_key], config[:app_secret]) if config[:app_key].present?
97
- result[:region] = config[:region] if config.key?(:region)
98
- result
99
- end
94
+ def aws_config
95
+ result = {}
96
+ result[:credentials] = Aws::Credentials.new(config[:app_key], config[:app_secret]) if config[:app_key].present?
97
+ result[:region] = config[:region] if config.key?(:region)
98
+ result
99
+ end
100
100
 
101
- def session
102
- AuthenticationFactory.new(
103
- self.class.authentication_klass,
104
- aws_config
105
- )
106
- end
101
+ def session
102
+ AuthenticationFactory.new(
103
+ self.class.authentication_klass,
104
+ aws_config
105
+ )
106
+ end
107
107
 
108
- def authenticate
109
- session.authenticate
110
- end
108
+ def authenticate
109
+ session.authenticate
110
+ end
111
111
 
112
- def client
113
- @client ||= authenticate
114
- end
112
+ def client
113
+ @client ||= authenticate
114
+ end
115
115
 
116
- def entry_for(name, size, date, dir)
117
- BrowseEverything::FileEntry.new(name, [key, name].join(':'), File.basename(name), size, date, dir)
118
- end
116
+ # Construct a BrowseEverything::FileEntry object
117
+ # @param name [String]
118
+ # @param size [String]
119
+ # @param date [DateTime]
120
+ # @param dir [String]
121
+ # @return [BrowseEverything::FileEntry]
122
+ def entry_for(name, size, date, dir)
123
+ BrowseEverything::FileEntry.new(name, [key, name].join(':'), File.basename(name), size, date, dir)
124
+ end
119
125
 
120
- def add_directories(listing)
121
- listing.common_prefixes.each do |prefix|
122
- @entries << entry_for(from_base(prefix.prefix), 0, Time.current, true)
123
- end
126
+ # Populate the entries with FileEntry objects from an S3 listing
127
+ # @param listing [Seahorse::Client::Response]
128
+ def add_directories(listing)
129
+ listing.common_prefixes.each do |prefix|
130
+ new_entry = entry_for(from_base(prefix.prefix), 0, Time.current, true)
131
+ @entries << new_entry unless new_entry.nil?
124
132
  end
133
+ end
125
134
 
126
- def add_files(listing, path)
127
- listing.contents.each do |entry|
128
- key = from_base(entry.key)
129
- @entries << entry_for(key, entry.size, entry.last_modified, false) unless strip(key) == strip(path)
130
- end
135
+ # Given a listing and a S3 listing and path, populate the entries
136
+ # @param listing [Seahorse::Client::Response]
137
+ # @param path [String]
138
+ def add_files(listing, path)
139
+ listing.contents.each do |entry|
140
+ key = from_base(entry.key)
141
+ new_entry = entry_for(key, entry.size, entry.last_modified, false)
142
+ @entries << new_entry unless strip(key) == strip(path) || new_entry.nil?
131
143
  end
144
+ end
132
145
 
133
- def generate_listing(path)
134
- client
135
- listing = client.list_objects(bucket: config[:bucket], delimiter: '/', prefix: full_path(path))
136
- add_directories(listing)
137
- add_files(listing, path)
138
- end
146
+ # For a given path to a S3 resource, retrieve the listing object and
147
+ # construct the file entries
148
+ # @param path [String]
149
+ def generate_listing(path)
150
+ client
151
+ listing = client.list_objects(bucket: config[:bucket], delimiter: '/', prefix: full_path(path))
152
+ add_directories(listing)
153
+ add_files(listing, path)
154
+ end
139
155
  end
140
156
  end
141
157
  end
@@ -15,7 +15,7 @@ module BrowseEverything
15
15
  end
16
16
 
17
17
  def relative_parent_path?
18
- name =~ /^\.\.?$/ ? true : false
18
+ name.match?(/^\.\.?$/)
19
19
  end
20
20
 
21
21
  def container?
@@ -31,8 +31,13 @@ module BrowseEverything
31
31
  attr_accessor :chunk_size
32
32
 
33
33
  class << self
34
- def can_retrieve?(uri)
35
- Typhoeus.get(uri, headers: { Range: 'bytes=0-0' }).success?
34
+ # Determines whether or not a remote resource can be retrieved
35
+ # @param uri [String] URI for the remote resource (usually a URL)
36
+ # @param headers [Hash] any custom headers required to transit the request
37
+ def can_retrieve?(uri, headers = {})
38
+ request_headers = headers.merge(Range: 'bytes=0-0')
39
+ response = Typhoeus.get(uri, headers: request_headers)
40
+ response.success?
36
41
  end
37
42
  end
38
43
 
@@ -84,81 +89,81 @@ module BrowseEverything
84
89
 
85
90
  private
86
91
 
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
+ # Extract and parse options used to download a file or resource from an HTTP API
93
+ # @param options [Hash]
94
+ # @return [Hash]
95
+ def extract_download_options(options)
96
+ url = options.fetch('url')
92
97
 
93
- # This avoids the potential for a KeyError
94
- headers = options.fetch('headers', {}) || {}
98
+ # This avoids the potential for a KeyError
99
+ headers = options.fetch('headers', {}) || {}
95
100
 
96
- file_size_value = options.fetch('file_size', 0)
97
- file_size = file_size_value.to_i
101
+ file_size_value = options.fetch('file_size', 0)
102
+ file_size = file_size_value.to_i
98
103
 
99
- output = {
100
- url: ::Addressable::URI.parse(url),
101
- headers: headers,
102
- file_size: file_size
103
- }
104
+ output = {
105
+ url: ::Addressable::URI.parse(url),
106
+ headers: headers,
107
+ file_size: file_size
108
+ }
104
109
 
105
- output[:file_size] = get_file_size(output) if output[:file_size] < 1
106
- output
107
- end
110
+ output[:file_size] = get_file_size(output) if output[:file_size] < 1
111
+ output
112
+ end
108
113
 
109
- # Retrieve the file from the file system
110
- # @param options [Hash]
111
- def retrieve_file(options)
112
- file_uri = options.fetch(:url)
113
- file_size = options.fetch(:file_size)
114
-
115
- retrieved = 0
116
- File.open(file_uri.path, 'rb') do |f|
117
- until f.eof?
118
- chunk = f.read(chunk_size)
119
- retrieved += chunk.length
120
- yield(chunk, retrieved, file_size)
121
- end
114
+ # Retrieve the file from the file system
115
+ # @param options [Hash]
116
+ def retrieve_file(options)
117
+ file_uri = options.fetch(:url)
118
+ file_size = options.fetch(:file_size)
119
+
120
+ retrieved = 0
121
+ File.open(file_uri.path, 'rb') do |f|
122
+ until f.eof?
123
+ chunk = f.read(chunk_size)
124
+ retrieved += chunk.length
125
+ yield(chunk, retrieved, file_size)
122
126
  end
123
127
  end
128
+ end
124
129
 
125
- # Retrieve a resource over the HTTP
126
- # @param options [Hash]
127
- def retrieve_http(options)
128
- file_size = options.fetch(:file_size)
129
- headers = options.fetch(:headers)
130
- url = options.fetch(:url)
131
- retrieved = 0
132
-
133
- request = Typhoeus::Request.new(url.to_s, method: :get, headers: headers)
134
- request.on_headers do |response|
135
- raise DownloadError.new("#{self.class}: Failed to download #{url}: Status Code: #{response.code}", response) unless response.code == 200
136
- end
137
- request.on_body do |chunk|
138
- retrieved += chunk.bytesize
139
- yield(chunk, retrieved, file_size)
140
- end
141
- request.run
130
+ # Retrieve a resource over the HTTP
131
+ # @param options [Hash]
132
+ def retrieve_http(options)
133
+ file_size = options.fetch(:file_size)
134
+ headers = options.fetch(:headers)
135
+ url = options.fetch(:url)
136
+ retrieved = 0
137
+
138
+ request = Typhoeus::Request.new(url.to_s, method: :get, headers: headers)
139
+ request.on_headers do |response|
140
+ raise DownloadError.new("#{self.class}: Failed to download #{url}: Status Code: #{response.code}", response) unless response.code == 200
141
+ end
142
+ request.on_body do |chunk|
143
+ retrieved += chunk.bytesize
144
+ yield(chunk, retrieved, file_size)
142
145
  end
146
+ request.run
147
+ end
143
148
 
144
- # Retrieve the file size
145
- # @param options [Hash]
146
- # @return [Integer] the size of the requested file
147
- def get_file_size(options)
148
- url = options.fetch(:url)
149
- headers = options.fetch(:headers)
150
- file_size = options.fetch(:file_size)
151
-
152
- case url.scheme
153
- when 'file'
154
- File.size(url.path)
155
- when /https?/
156
- response = Typhoeus.head(url.to_s, headers: headers)
157
- length_value = response.headers['Content-Length'] || file_size
158
- length_value.to_i
159
- else
160
- raise URI::BadURIError, "Unknown URI scheme: #{url.scheme}"
161
- end
149
+ # Retrieve the file size
150
+ # @param options [Hash]
151
+ # @return [Integer] the size of the requested file
152
+ def get_file_size(options)
153
+ url = options.fetch(:url)
154
+ headers = options.fetch(:headers)
155
+ file_size = options.fetch(:file_size)
156
+
157
+ case url.scheme
158
+ when 'file'
159
+ File.size(url.path)
160
+ when /https?/
161
+ response = Typhoeus.head(url.to_s, headers: headers)
162
+ length_value = response.headers['Content-Length'] || file_size
163
+ length_value.to_i
164
+ else
165
+ raise URI::BadURIError, "Unknown URI scheme: #{url.scheme}"
162
166
  end
167
+ end
163
168
  end
164
169
  end