worochi 0.0.14 → 0.0.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -24
  3. data/lib/worochi/agent/#example.rb +6 -0
  4. data/lib/worochi/agent/dropbox.rb +6 -8
  5. data/lib/worochi/agent/github.rb +7 -2
  6. data/lib/worochi/agent/google_drive.rb +272 -0
  7. data/lib/worochi/agent.rb +23 -2
  8. data/lib/worochi/config/google_drive.yml +3 -1
  9. data/lib/worochi/item.rb +42 -1
  10. data/lib/worochi/oauth.rb +16 -8
  11. data/lib/worochi/version.rb +1 -1
  12. data/lib/worochi.rb +1 -0
  13. data/spec/cassettes/Worochi_Agent_GoogleDrive/_delete/deletes_a_file.yml +4248 -0
  14. data/spec/cassettes/Worochi_Agent_GoogleDrive/_delete/raises_error_on_bad_token.yml +7889 -0
  15. data/spec/cassettes/Worochi_Agent_GoogleDrive/_get_item_id/retrieves_the_item_ID.yml +4148 -0
  16. data/spec/cassettes/Worochi_Agent_GoogleDrive/_get_item_id/returns_the_root_ID.yml +3889 -0
  17. data/spec/cassettes/Worochi_Agent_GoogleDrive/_insert_file/creates_a_new_folder_to_root.yml +4753 -0
  18. data/spec/cassettes/Worochi_Agent_GoogleDrive/_insert_file/uploads_a_file.yml +4492 -0
  19. data/spec/cassettes/Worochi_Agent_GoogleDrive/_push_item/creates_new_directories_as_needed.yml +5961 -0
  20. data/spec/cassettes/Worochi_Agent_GoogleDrive/_push_item/pushes_a_single_item.yml +3887 -0
  21. data/spec/cassettes/Worochi_Agent_GoogleDrive/_push_item/raises_error_on_bad_token.yml +7833 -0
  22. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_default_options/has_the_required_options.yml +3889 -0
  23. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/accepts_a_different_relative_path.yml +4294 -0
  24. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/contains_file1.yml +4236 -0
  25. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/does_not_contain_folder1.yml +4236 -0
  26. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/raises_error_on_invalid_path.yml +4236 -0
  27. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files_and_folders/contains_folder1_and_file1.yml +4583 -0
  28. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files_and_folders/shows_detailed_listing_including_the_required_fields.yml +4236 -0
  29. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_folders/accepts_a_different_relative_path.yml +4294 -0
  30. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_folders/contains_folder1.yml +4236 -0
  31. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_folders/does_not_contain_file1.yml +4236 -0
  32. data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_init_client/returns_the_client.yml +7775 -0
  33. data/spec/cassettes/Worochi_Item/_content_type/detects_the_MIME_type.yml +105 -0
  34. data/spec/cassettes/Worochi_Item/_content_type/falls_back_to_file_name_when_ruby-filemagic_is_not_loaded.yml +105 -0
  35. data/spec/spec_helper.rb +5 -0
  36. data/spec/support/shared_exampes_for_agents.rb +2 -2
  37. data/spec/worochi/agent/github_spec.rb +6 -0
  38. data/spec/worochi/agent/google_drive_spec.rb +79 -0
  39. data/spec/worochi/agent_spec.rb +13 -1
  40. data/spec/worochi/item_spec.rb +18 -0
  41. data/spec/worochi/oauth_spec.rb +1 -1
  42. data/worochi.gemspec +4 -0
  43. metadata +104 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a16bf51d367b8b62f009a425dd929ed96d38f376
4
- data.tar.gz: 9e24cbc3ebee89c808a6f1c9e5622ab78b67680f
3
+ metadata.gz: 791d8d31548bebe42edf49ba96f3e6a64dea0d14
4
+ data.tar.gz: 207531dc87e9d1158668c259fde956728fdc4195
5
5
  SHA512:
6
- metadata.gz: 2cbfcb2ba4fc6d306bedfde432eae0f8cc66586a84f7a06b3b156235cf8762ada3da0ef09ee56d9dea665755b10ccea8d572bda49f76e5f8b336186b9709a8e3
7
- data.tar.gz: d28e49ee2f24e2eae8d982d3706407c4f76019d51f80f118c771e8833829d0bfa7e1dd2cab9effa292bd07563e4180e0bc57c92d006bfb2c230f2bf0241c4911
6
+ metadata.gz: b8a5a0a98112ac258077771b0a9cb8298a3c030893633a555c3ff60780470091f3321e3f4c50887b4b8dcd579e9288b4bbc6cd315c0dc621952d11bb07219b1b
7
+ data.tar.gz: d0596891d2b02b451facb7d6e243b98c3dd0f713b8aca53515a2b29ee0b93af73ed9cc81ac9ee190cdfca8d5ac102fab76edc67d97e7bc3dbe12425b42e9fac6
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Worochi
2
2
 
3
- ![Coverage Status](https://coveralls.io/repos/Pixelapse/worochi/badge.png?branch=master)
3
+ [![Coverage Status](https://coveralls.io/repos/Pixelapse/worochi/badge.png?branch=master)](https://coveralls.io/r/Pixelapse/worochi?branch=master)
4
4
 
5
5
  Worochi provides a standard way to interface with Ruby API wrappers provided
6
6
  by various cloud storage services such as Dropbox and Google Drive.
@@ -20,20 +20,20 @@ Worochi can be installed as a gem.
20
20
  ## Basic Usage
21
21
 
22
22
  Pushing files is easy. Just create an agent using the OAuth authorization
23
- token for the user and then call {Worochi::Agent#push} or {Worochi.push}. File
23
+ token for the user and then call `Worochi::Agent#push` or `Worochi.push`. File
24
24
  origins can be local, HTTP, or Amazon S3 paths.
25
25
 
26
- Pushing files to Dropbox:
26
+ ### Pushing files to Dropbox
27
27
 
28
28
  ```ruby
29
29
  token = '982n3b989az'
30
30
  agent = Worochi.create(:dropbox, token)
31
31
  agent.push('test.txt')
32
- agent.files
32
+ agent.files # lists the files in the default directory
33
33
  # => ['test.txt']
34
34
  ```
35
35
 
36
- Pushing multiple files:
36
+ ### Pushing multiple files
37
37
 
38
38
  ```ruby
39
39
  agent.push(['a.txt', 'folder1/b.txt', 'http://example.com/c.txt'])
@@ -41,7 +41,7 @@ agent.files
41
41
  # => ['a.txt', 'b.txt', 'c.txt']
42
42
  ```
43
43
 
44
- Pushing files to more than one agent at the same time:
44
+ ### Pushing to more than one service
45
45
 
46
46
  ```ruby
47
47
  a = Worochi.create(:dropbox, 'hxhrerx')
@@ -53,26 +53,34 @@ b.files
53
53
  # => ['test.txt']
54
54
  ```
55
55
 
56
- Pushing to a specific folder:
56
+ ### Specifying remote paths
57
57
 
58
- ```ruby
59
- agent = Worochi.create(:dropbox, token, { dir: '/folder1' })
60
- agent.push('a.txt')
58
+ Instead of pushing all the files to the default directory at `/`, you can
59
+ specify the default path and also the path of every file individually.
61
60
 
62
- agent.files
63
- # => ['a.txt']
61
+ ```ruby
62
+ agent = Worochi.create(:dropbox, token, { dir: '/parent' }) # default path
63
+ agent.push([
64
+ { source: 'a.txt', path: 'A.txt' },
65
+ { source: 'b.txt', path: 'folder1/B.txt' },
66
+ { source: 'c.txt', path: '/C.txt' } # absolute remote path
67
+ ])
64
68
 
65
- agent.set_dir('/')
66
- agent.push('b.txt')
67
69
  agent.files
68
- # => ['b.txt']
70
+ # => ['A.txt']
69
71
 
70
72
  agent.files_and_folders
71
- # => ['folder1', 'b.txt']
72
- agent.files('/folder1')
73
- # => ['a.txt']
74
- ```
73
+ # => ['A.txt', 'folder1']
74
+
75
+ agent.files('/parent') # same as default directory
76
+ # => ['A.txt']
75
77
 
78
+ agent.files('/parent/folder1')
79
+ # => ['B.txt']
80
+
81
+ agent.files('/') # root
82
+ # => ['C.txt']
83
+ ```
76
84
  ## Amazon S3 Support
77
85
 
78
86
  Files can be retrieved directly from their Amazon S3 location either using the
@@ -98,7 +106,7 @@ this to work.
98
106
 
99
107
  Worochi provides helper methods to assist with the OAuth2 authorization flow.
100
108
 
101
- Example Rails controller:
109
+ ### Example in Rails
102
110
 
103
111
  ```ruby
104
112
  class ApiTokensController < ApplicationController
@@ -130,9 +138,57 @@ Service-specific settings for OAuth2 are predefined in the gem, so the
130
138
  framework just needs to handle verification of session state (this is usually
131
139
  optional) and storing the retrieved access token value.
132
140
 
141
+ ### Refresh Token
142
+
143
+ Retrieved tokens can be refreshed if `refresh_token` is supported by the
144
+ service.
145
+
146
+ ```ruby
147
+ token = oauth.flow_end(code)
148
+
149
+ new_token = oauth.refresh(token)
150
+ ```
151
+
152
+ Tokens are hashes and `refresh` expects a hash containing the field
153
+ `refresh_token`. It raises an error if `refresh_token` is invalid.
154
+
155
+ ## Supported Services
156
+
157
+ Currently these services are fully supported:
158
+
159
+ **Google Drive**
160
+ - Service name `:google_drive`
161
+ - Env variables `GOOGLE_ID` `GOOGLE_SECRET` `GOOGLE_TEST_TOKEN`
162
+
163
+ **Dropbox**
164
+ - Service name `:dropbox`
165
+ - Env variables `DROPBOX_ID` `DROPBOX_SECRET` `DROPBOX_TEST_TOKEN`
166
+
167
+ **GitHub**
168
+ - Service name `:github`
169
+ - Env variables `GITHUB_ID` `GITHUB_SECRET` `GITHUB_TEST_TOKEN`
170
+
171
+ ### Environmental Variables
172
+
173
+ `ID` and `SECRET` variables are only needed for retrieving access tokens and
174
+ can be omitted if you are using other OAuth2 libraries for that purpose.
175
+
176
+ `TEST_TOKEN` is a valid user access token used for RSpec testing. The user
177
+ account being used for testing should contain these [test files]
178
+ (https://github.com/darkmirage/test) at the directory specified by the tests.
179
+
180
+ ## MIME Types
181
+
182
+ Some services such as Google Drive require Worochi to provide MIME types
183
+ for the files being uploaded. Worochi will attempt to use the file name
184
+ to determine the MIME type, but this does not work well. You can use
185
+ `ruby-filemagic` for better MIME type detection using magic numbers.
186
+
187
+ gem install ruby-filemagic
188
+
133
189
  ## Development
134
190
 
135
- Each service is implemented as an {Worochi::Agent} object. Below is an
191
+ Each service is implemented as an `Worochi::Agent` object. Below is an
136
192
  overview of the files necessary for defining an agent to support a new
137
193
  service.
138
194
 
@@ -152,10 +208,12 @@ Test file:
152
208
  Use underscore for filenames and corresponding mixed case for class name. The
153
209
  class name and service name symbol for the above example would be:
154
210
 
155
- class Worochi::Agent::FooBar < Worochi::Agent
156
- end
211
+ ```ruby
212
+ class Worochi::Agent::FooBar < Worochi::Agent
213
+ end
157
214
 
158
- Worochi.create(:foo_bar, token)
215
+ Worochi.create(:foo_bar, token)
216
+ ```
159
217
 
160
218
  RSpec tests use the [VCR](https://github.com/vcr/vcr) gem to record and
161
219
  playback real HTTP interactions. Remember to filter out API tokens in the
@@ -52,6 +52,12 @@ class Worochi
52
52
  end
53
53
  end
54
54
 
55
+ # Deletes the file at path. Raises {Error} if file deletion is not
56
+ # supported by the service.
57
+ def delete(path)
58
+ raise Error, 'Deletion is not supported'
59
+ end
60
+
55
61
  # Service specific methods
56
62
  end
57
63
  end
@@ -23,7 +23,8 @@ class Worochi
23
23
  if chunked = item.size > options[:chunk_size]
24
24
  push_item_chunked(item)
25
25
  else
26
- @client.put_file(full_path(item), item.content, options[:overwrite])
26
+ path = full_path(item.path)
27
+ @client.put_file(path, item.content, options[:overwrite])
27
28
  end
28
29
  Worochi::Log.debug "Uploaded"
29
30
  chunked
@@ -35,11 +36,7 @@ class Worochi
35
36
  # @param path [String] path to list instead of the current directory
36
37
  # @return [Array<Hash>] list of files and subdirectories
37
38
  def list(path=nil)
38
- if path
39
- remote_path = path[0] == '/' ? path : File.join(options[:dir], path)
40
- else
41
- remote_path = options[:dir]
42
- end
39
+ remote_path = list_path(path)
43
40
 
44
41
  begin
45
42
  response = @client.metadata(remote_path)
@@ -74,7 +71,7 @@ class Worochi
74
71
  end
75
72
  Worochi::Log.debug "Uploaded #{uploader.offset} bytes"
76
73
  end
77
- uploader.finish(full_path(item), options[:overwrite])
74
+ uploader.finish(full_path(item.path), options[:overwrite])
78
75
  nil
79
76
  end
80
77
 
@@ -83,8 +80,9 @@ class Worochi
83
80
  # @param path [String] path relative to current directory
84
81
  # @return [Boolean] `true` if a file was actually deleted
85
82
  def delete(path)
83
+ abs_path = full_path(path)
86
84
  begin
87
- @client.file_delete(File.join(options[:dir], path))
85
+ @client.file_delete(abs_path)
88
86
  rescue DropboxError
89
87
  false
90
88
  end
@@ -46,7 +46,7 @@ class Worochi
46
46
  # @param sha [String] list a different branch than the `:source`
47
47
  # @return [Array<Hash>] list of files and subdirectories
48
48
  def list(path=nil, sha=nil)
49
- remote_path = (path || options[:dir]).sub(/^\//, '').sub(/\/$/, '')
49
+ remote_path = list_path(path).sub(/^\//, '').sub(/\/$/, '')
50
50
 
51
51
  result = @client.tree(repo, sha || source_branch, recursive: true).tree
52
52
  result.sort! do |x, y|
@@ -89,6 +89,11 @@ class Worochi
89
89
  repos.map { |repo| repo[:full_name] } unless opts[:details]
90
90
  end
91
91
 
92
+ # Deletion is not supported by GitHub, so raises {Error}.
93
+ def delete(path)
94
+ raise Error, 'Cannot delete from GitHub'
95
+ end
96
+
92
97
  private
93
98
  # Appends an item to the existing tree.
94
99
  #
@@ -97,7 +102,7 @@ class Worochi
97
102
  # @return [String] SHA1 checksum of the resulting tree
98
103
  def tree_append(tree_sha, item)
99
104
  child = {
100
- path: full_path(item).gsub(/^\//, ''),
105
+ path: full_path(item.path).gsub(/^\//, ''),
101
106
  sha: push_blob(item),
102
107
  type: 'blob',
103
108
  mode: '100644'
@@ -0,0 +1,272 @@
1
+ require 'google/api_client'
2
+ require 'awesome_print'
3
+
4
+ class Worochi
5
+ # The {Agent} for Google Drive API.
6
+ # @see https://github.com/google/google-api-ruby-client
7
+ class Agent::GoogleDrive < Agent
8
+
9
+ # Initializes the Google Drive API client.
10
+ #
11
+ # @return [ApiClient]
12
+ # @see Agent#initialize
13
+ def init_client
14
+ clear_cache
15
+ disable_write
16
+ @client = Google::APIClient.new(
17
+ application_name: 'Worochi',
18
+ application_version: Worochi::VERSION)
19
+ @client.authorization.access_token = options[:token]
20
+ @drive = @client.discovered_api('drive', 'v2')
21
+ @client
22
+ end
23
+
24
+ # Pushes an individual item to Google Drive.
25
+ #
26
+ # @return [String] resource ID of the uploaded file.
27
+ def push_item(item)
28
+ enable_write
29
+ abs_path = full_path(item.path)
30
+ parent_id = get_parent_id(abs_path)
31
+ id = insert_file(item, parent_id)
32
+ disable_write
33
+ id
34
+ end
35
+
36
+ # Returns a list of files and subdirectories at the remote path specified
37
+ # by `options[:dir]`.
38
+ #
39
+ # @param path [String] path to list instead of the current directory
40
+ # @return [Array<Hash>] list of files and subdirectories
41
+ def list(path=nil)
42
+ remote_path = list_path(path)
43
+ result, folder_id = navigate_to(remote_path)
44
+ result.map do |elem|
45
+ if elem.mimeType == 'application/vnd.google-apps.folder'
46
+ file_type = 'folder'
47
+ else
48
+ file_type = 'file'
49
+ end
50
+ {
51
+ name: elem.title,
52
+ path: File.join(remote_path, elem.title),
53
+ type: file_type,
54
+ id: elem.id,
55
+ parent_id: folder_id
56
+ }
57
+ end
58
+ end
59
+
60
+ # Deletes the file at `path` from Google Drive.
61
+ #
62
+ # @param path [String] path relative to current directory
63
+ # @return [Boolean] `true` if a file was actually deleted
64
+ def delete(path)
65
+ abs_path = full_path(path)
66
+ item_id = get_item_id(abs_path)
67
+ return false if item_id.nil?
68
+ delete_by_id(item_id)
69
+ end
70
+
71
+ # Clears the internal resource ID cache
72
+ def clear_cache
73
+ @id_cache = {}
74
+ end
75
+
76
+ private
77
+ # Saves a resource ID in the cache.
78
+ #
79
+ # @param abs_path [String] absolute path to the file/folder
80
+ # @param id [String] resource ID of the file/folder
81
+ # @return [nil]
82
+ def set_cache(abs_path, id)
83
+ abs_path ='/' if abs_path == ''
84
+ @id_cache[abs_path] = { id: id, time: Time.now }
85
+ nil
86
+ end
87
+
88
+ # Retrieves a saved cache entry.
89
+ #
90
+ # @param abs_path [String] absolute path to the file/folder
91
+ # @return [String] resource ID of the file/folder
92
+ def get_cached(abs_path)
93
+ return nil unless @id_cache.include?(abs_path)
94
+ @id_cache[abs_path][:id]
95
+ end
96
+
97
+ # Recursive directory lookup automatically creates missing directories.
98
+ def enable_write
99
+ @write = true
100
+ end
101
+
102
+ # Disable auto-creation of missing directories.
103
+ def disable_write
104
+ @write = false
105
+ end
106
+
107
+ # @return [Boolean] `true` if auto-creation is enabled.
108
+ def write?
109
+ @write
110
+ end
111
+
112
+ # @param id [String] resource ID of file/folder to be deleted
113
+ # @return [Boolean] successfully deleted
114
+ def delete_by_id(id)
115
+ response = @client.execute(
116
+ api_method: @drive.files.delete,
117
+ parameters: { 'fileId' => id })
118
+ response.status == 204
119
+ end
120
+
121
+ # @param abs_path [String] absolute path of the child
122
+ # @return [String] parent resource ID
123
+ def get_parent_id(abs_path)
124
+ parent_path = File.dirname(abs_path)
125
+ get_item_id(parent_path)
126
+ end
127
+
128
+ # @param abs_path [String] absolute path to the item
129
+ # @return [String] Google Drive source ID for the item
130
+ def get_item_id(abs_path)
131
+ return get_cached(abs_path) if get_cached(abs_path)
132
+
133
+ file_name = File.basename(abs_path)
134
+ folder_path = File.dirname(abs_path)
135
+ return 'root' if ['/', '.', ''].include?(file_name)
136
+
137
+ parent_items, parent_id = navigate_to(folder_path)
138
+ item = find_item_by_name(parent_items, file_name)
139
+ if item
140
+ item_id = item.id
141
+ else
142
+ item_id = write? ? insert_file(file_name, parent_id) : nil
143
+ end
144
+
145
+ set_cache(abs_path, item_id)
146
+ item_id
147
+ end
148
+
149
+ # Uploads files or creates new directories.
150
+ #
151
+ # @overload insert_file(title, parent_id)
152
+ # @param title [String] new directory name
153
+ # @param parent_id [String] parent resource ID
154
+ # @overload insert_file(item, parent_id)
155
+ # @param item [Worochi::Item] file to be uploaded
156
+ # @param parent_id [String] parent resource ID
157
+ # @return [String] resource ID of the created file
158
+ def insert_file(arg, parent_id)
159
+ if arg.respond_to?(:filename)
160
+ item = arg
161
+ title = arg.filename
162
+ else
163
+ item = false
164
+ title = arg
165
+ end
166
+
167
+ body = { 'title' => title, 'parents' => [{ 'id' => parent_id }]}
168
+ opts = { api_method: @drive.files.insert }
169
+
170
+ if item
171
+ opts[:parameters] = { 'uploadType' => 'resumable' }
172
+ opts[:media] = Google::APIClient::UploadIO.new(item.content,
173
+ item.content_type, item.content.path)
174
+ else
175
+ body['mimeType'] = 'application/vnd.google-apps.folder'
176
+ end
177
+
178
+ opts[:body_object] = @drive.files.insert.request_schema.new(body)
179
+
180
+ begin
181
+ response = @client.execute(opts)
182
+ rescue NoMethodError
183
+ # Google API client does not fail correctly given an invalid token
184
+ # Need to catch and handle it here
185
+ raise Error, 'Google Error: Invalid credentials'
186
+ end
187
+
188
+ # if item
189
+ # retries = 0
190
+ # while !response.resumable_upload.complete? && retries < 10
191
+ # retries += 1
192
+ # sleep(rand(6))
193
+ # ap "retry #{retries}"
194
+ # response = @client.execute(response.resumable_upload)
195
+ # end
196
+ # end
197
+
198
+ response.data ? response.data.id : nil
199
+ end
200
+
201
+ # List the files and folders at the target absolute path.
202
+ #
203
+ # @param abs_path [String] absolute path
204
+ # @return [Array<Google::APIClient::Schema::Drive::V2::File>] children
205
+ def navigate_to(abs_path)
206
+ folders = abs_path.split('/').reject(&:empty?)
207
+ get_folder_items(folders)
208
+ end
209
+
210
+ # Recursively navigate to the target folder.
211
+ #
212
+ # @param folders [Array<String>] list of folder names
213
+ # @param parent_id [String] parent resource ID
214
+ # @param curr_path [String] absolute path to current parent
215
+ # @return [Array<Google::APIClient::Schema::Drive::V2::File>, String]
216
+ # children list and resource ID of parent
217
+ def get_folder_items(folders, parent_id='root', curr_path='')
218
+ set_cache(curr_path, parent_id)
219
+ items = retrieve_items(parent_id)
220
+ return items, parent_id if folders.empty?
221
+
222
+ name = folders.shift
223
+ item = find_item_by_name(items, name)
224
+ if item
225
+ item_id = item.id
226
+ else
227
+ raise Error, 'Invalid path specified' unless write?
228
+ item_id = insert_file(name, parent_id)
229
+ end
230
+
231
+ get_folder_items(folders, item_id, "#{curr_path}/#{name}")
232
+ end
233
+
234
+ # Returns a single item from a list of items by matching its file name.
235
+ #
236
+ # @param items [Array<Google::APIClient::Schema::Drive::V2::File>] items
237
+ # @param name [String] file name
238
+ # @return [Google::APIClient::Schema::Drive::V2::File]
239
+ def find_item_by_name(items, name)
240
+ found = items.select { |elem| elem.title == name }
241
+ raise Error, "Ambiguous file name #{name}" if found.size > 1
242
+ found.first
243
+ end
244
+
245
+ # Retrieves all the children of the specified directory.
246
+ #
247
+ # @param id [String] Google Drive resource ID of the directory
248
+ # @return [Array<Google::APIClient::Schema::Drive::V2::File>] children
249
+ def retrieve_items(id)
250
+ fields = 'items(id,mimeType,title),nextPageToken'
251
+ q = "'#{id}' in parents and trashed = false"
252
+ page_token = nil
253
+ result = []
254
+ begin
255
+ params = { 'q' => q, 'fields' => fields }
256
+ params['pageToken'] = page_token unless page_token.to_s == ''
257
+ response = @client.execute(
258
+ api_method: @drive.files.list,
259
+ parameters: params)
260
+ if response.status == 200
261
+ elems = response.data
262
+ result.concat(elems.items)
263
+ page_token = elems.next_page_token
264
+ else
265
+ error_msg = response.data['error']['message']
266
+ raise Worochi::Error, "Google Error: #{error_msg}"
267
+ end
268
+ end while page_token.to_s != ''
269
+ result
270
+ end
271
+ end
272
+ end
data/lib/worochi/agent.rb CHANGED
@@ -164,9 +164,30 @@ class Worochi
164
164
  result
165
165
  end
166
166
 
167
+ # Returns the full remote target path for the file being pushed. If the
168
+ # specified path starts with / then this is assumed to be the full
169
+ # absolute path. If not, join the path with the configured directory.
170
+ #
171
+ # @param path [String] path to the file
167
172
  # @return [String] full path combining remote directory and item path
168
- def full_path(item)
169
- File.join(options.dir, item.path)
173
+ def full_path(path)
174
+ if path[0] == '/'
175
+ path
176
+ else
177
+ File.join(options.dir, path)
178
+ end
179
+ end
180
+
181
+ # Path used for listing. Defaults to `options[:dir]` if not specified.
182
+ #
183
+ # @param path [String] relative/absolute path or nil
184
+ # @return [String] absolute path to list
185
+ def list_path(path)
186
+ if path.nil? || path.empty?
187
+ options.dir
188
+ else
189
+ return full_path(path)
190
+ end
170
191
  end
171
192
 
172
193
  # Agents should either override this or have a YAML config file at
@@ -5,7 +5,9 @@ dir: /
5
5
  oauth:
6
6
  id_env: GOOGLE_ID
7
7
  secret_env: GOOGLE_SECRET
8
- token_url: /1/oauth2/token
8
+ token_url: /o/oauth2/token
9
9
  authorize_url: /o/oauth2/auth
10
10
  site: https://accounts.google.com
11
11
  scope: https://www.googleapis.com/auth/drive
12
+ access_type: offline
13
+ approval_prompt: force
data/lib/worochi/item.rb CHANGED
@@ -1,4 +1,11 @@
1
1
  require 'tempfile'
2
+ require 'mime/types'
3
+
4
+ # Install ruby-filemagic if you want better mime-type identification
5
+ begin
6
+ require 'filemagic'
7
+ rescue LoadError
8
+ end
2
9
 
3
10
  class Worochi
4
11
  # This represents a single file that is being pushed. The {#content}
@@ -21,9 +28,15 @@ class Worochi
21
28
  def initialize(path, content)
22
29
  @path = path
23
30
  @content = content
31
+ detect_type
24
32
  @content.rewind
25
33
  end
26
34
 
35
+ # @return [String] the filename
36
+ def filename
37
+ File.basename(path)
38
+ end
39
+
27
40
  # The total size of the content in bytes.
28
41
  #
29
42
  # @return [Integer]
@@ -47,6 +60,35 @@ class Worochi
47
60
  content.rewind
48
61
  end
49
62
 
63
+ # @return [String] mime-type of the content.
64
+ def content_type
65
+ @type
66
+ end
67
+
68
+ private
69
+ # Detects the mime-type of the file. Uses file name matching if
70
+ # ruby-filemagic is not installed.
71
+ #
72
+ # @param simplified [Boolean] whether to use simplified mime-types
73
+ # @return [String] mime-type
74
+ def detect_type(simplified=true)
75
+ if defined?(FileMagic)
76
+ # Magic number matching
77
+ fm = FileMagic.mime
78
+ fm.simplified = true if simplified
79
+ @type = fm.file(@content.path)
80
+ else
81
+ # File name matching
82
+ types = MIME::Types.type_for(path)
83
+ if types.empty?
84
+ @type = 'application/octet-stream'
85
+ else
86
+ @type = simplified ? types[0].content_type : types[0].simplified
87
+ end
88
+ end
89
+ @type
90
+ end
91
+
50
92
  class << self
51
93
  # Takes in either a single file entry or a list of file entries and
52
94
  # parses them into a list of {Item} objects. Each entry can be either a
@@ -93,7 +135,6 @@ class Worochi
93
135
  end
94
136
 
95
137
  private
96
-
97
138
  # Retrieves the file content from `source`.
98
139
  #
99
140
  # @param source [String] local or remote location of the file content