worochi 0.0.14 → 0.0.15
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
![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
|
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
|