worochi 0.0.14 → 0.0.15
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.
- checksums.yaml +4 -4
- data/README.md +82 -24
- data/lib/worochi/agent/#example.rb +6 -0
- data/lib/worochi/agent/dropbox.rb +6 -8
- data/lib/worochi/agent/github.rb +7 -2
- data/lib/worochi/agent/google_drive.rb +272 -0
- data/lib/worochi/agent.rb +23 -2
- data/lib/worochi/config/google_drive.yml +3 -1
- data/lib/worochi/item.rb +42 -1
- data/lib/worochi/oauth.rb +16 -8
- data/lib/worochi/version.rb +1 -1
- data/lib/worochi.rb +1 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_delete/deletes_a_file.yml +4248 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_delete/raises_error_on_bad_token.yml +7889 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_get_item_id/retrieves_the_item_ID.yml +4148 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_get_item_id/returns_the_root_ID.yml +3889 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_insert_file/creates_a_new_folder_to_root.yml +4753 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_insert_file/uploads_a_file.yml +4492 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_push_item/creates_new_directories_as_needed.yml +5961 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_push_item/pushes_a_single_item.yml +3887 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/_push_item/raises_error_on_bad_token.yml +7833 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_default_options/has_the_required_options.yml +3889 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/accepts_a_different_relative_path.yml +4294 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/contains_file1.yml +4236 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/does_not_contain_folder1.yml +4236 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files/raises_error_on_invalid_path.yml +4236 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_files_and_folders/contains_folder1_and_file1.yml +4583 -0
- 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
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_folders/accepts_a_different_relative_path.yml +4294 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_folders/contains_folder1.yml +4236 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_folders/does_not_contain_file1.yml +4236 -0
- data/spec/cassettes/Worochi_Agent_GoogleDrive/it_should_behave_like_a_service_agent/_init_client/returns_the_client.yml +7775 -0
- data/spec/cassettes/Worochi_Item/_content_type/detects_the_MIME_type.yml +105 -0
- data/spec/cassettes/Worochi_Item/_content_type/falls_back_to_file_name_when_ruby-filemagic_is_not_loaded.yml +105 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/shared_exampes_for_agents.rb +2 -2
- data/spec/worochi/agent/github_spec.rb +6 -0
- data/spec/worochi/agent/google_drive_spec.rb +79 -0
- data/spec/worochi/agent_spec.rb +13 -1
- data/spec/worochi/item_spec.rb +18 -0
- data/spec/worochi/oauth_spec.rb +1 -1
- data/worochi.gemspec +4 -0
- metadata +104 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 791d8d31548bebe42edf49ba96f3e6a64dea0d14
|
4
|
+
data.tar.gz: 207531dc87e9d1158668c259fde956728fdc4195
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b8a5a0a98112ac258077771b0a9cb8298a3c030893633a555c3ff60780470091f3321e3f4c50887b4b8dcd579e9288b4bbc6cd315c0dc621952d11bb07219b1b
|
7
|
+
data.tar.gz: d0596891d2b02b451facb7d6e243b98c3dd0f713b8aca53515a2b29ee0b93af73ed9cc81ac9ee190cdfca8d5ac102fab76edc67d97e7bc3dbe12425b42e9fac6
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Worochi
|
2
2
|
|
3
|
-

|
3
|
+
[](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
|
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
|
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
|
-
|
56
|
+
### Specifying remote paths
|
57
57
|
|
58
|
-
|
59
|
-
|
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
|
-
|
63
|
-
|
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
|
-
# => ['
|
70
|
+
# => ['A.txt']
|
69
71
|
|
70
72
|
agent.files_and_folders
|
71
|
-
# => ['
|
72
|
-
|
73
|
-
#
|
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
|
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
|
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
|
-
|
156
|
-
|
211
|
+
```ruby
|
212
|
+
class Worochi::Agent::FooBar < Worochi::Agent
|
213
|
+
end
|
157
214
|
|
158
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
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(
|
85
|
+
@client.file_delete(abs_path)
|
88
86
|
rescue DropboxError
|
89
87
|
false
|
90
88
|
end
|
data/lib/worochi/agent/github.rb
CHANGED
@@ -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
|
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(
|
169
|
-
|
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: /
|
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
|