googledrive-easy 0.0.6 → 0.1.0
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/bin/googledrive +79 -34
- data/lib/googledrive-easy-test.rb +223 -0
- data/lib/googledrive-easy.rb +432 -302
- data/lib/utilities.rb +16 -0
- metadata +9 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1111117a9dac04b800a1429cd0ce2a7bae5cdeb04b2ea29c9d7333c40a1306a7
|
4
|
+
data.tar.gz: 518fc4f98597d3a51dcbfbe10c613c979674bde30af6d6cae4c8b2a1094f57bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d89f2d745dc6d890f811eb86f9067a4c236e3b074c07a61a5ac4c8e238ffccf72677adc34b5b76c31a7dc23c45a9f61ab9c369c675f018e33e6fe942736c487e
|
7
|
+
data.tar.gz: 0b15a348a056d7e102f7c8e948957b12ad7ecf57098e0484dd02b5212bbeda408484dde604f3fb43f81d674692c5a78c1e2cf9851ea7b2da5bd6fbf856270763
|
data/bin/googledrive
CHANGED
@@ -1,46 +1,91 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'logger'
|
4
|
+
# Attempt relative import first, for development/debugging purposes.
|
5
|
+
begin
|
6
|
+
require_relative '../lib/googledrive-easy'
|
7
|
+
rescue
|
8
|
+
require 'googledrive-easy'
|
9
|
+
end
|
2
10
|
|
3
|
-
|
11
|
+
options = {
|
12
|
+
:persist=>false,
|
13
|
+
:file=>nil
|
14
|
+
}
|
15
|
+
OptionParser.new do |opts|
|
16
|
+
opts.banner = "Usage: googledrive [options]"
|
4
17
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
18
|
+
opts.on("-l", "--loopback-auth", "Complete Oauth via loopback interface (may require interaction)") { |v| options[:authmode] = 'loopback'}
|
19
|
+
opts.on("-i", "--input-auth", "Complete Oauth via copy-paste interface (may require interaction)") { |v| options[:authmode] = 'input'}
|
20
|
+
opts.on("-e", "--environment-config", "Configure Oauth via environment vars. Does nothing here, unless --persist is also passed. (headless)") { |v| options[:authmode] = 'environment'}
|
21
|
+
opts.on("-f", "--file-config FILEPATH", "Configure Oauth via file. Persists to (#{DRIVE_ENV_FILE}). Requires (headless)") do |v|
|
22
|
+
options[:authmode] = 'file'
|
23
|
+
options[:file] = v
|
9
24
|
end
|
10
25
|
|
11
|
-
|
26
|
+
opts.on("-p", "--persist", "After completing Oauth or loading existing values, persist to environment file (#{DRIVE_ENV_FILE}) for future use.") { |v| options[:persist] = true}
|
27
|
+
opts.on("-c", "--cli-config", "Configure Oauth via passed values. requires --secret, --id, and --refresh. Always persists to environment file. (headless)") { |v| options[:authmode] = 'cli'}
|
28
|
+
|
29
|
+
opts.on("--id CLIENT_ID", String, "Google Client ID for CLI config mode") { |v| options[:id] = v}
|
30
|
+
opts.on("--secret CLIENT_SECRET", String, "Google Client Secret for CLI config mode") { |v| options[:secret] = v}
|
31
|
+
opts.on("--refresh REFRESH_TOKEN", String, "Google Refresh Token for CLI config mode") { |v| options[:refresh] = v}
|
32
|
+
end.parse!
|
33
|
+
|
34
|
+
# Checks
|
35
|
+
unless options[:authmode]
|
36
|
+
puts("An auth mode must be chosen. Check `googledrive --help` for details")
|
37
|
+
exit 1
|
38
|
+
end
|
39
|
+
|
40
|
+
def print_hash_for_env_use(keys)
|
41
|
+
puts <<~TEXT
|
42
|
+
#{ENV_KEYS[:id]}='#{keys["client_id"]}'
|
43
|
+
#{ENV_KEYS[:secret]}='#{keys["client_secret"]}'
|
44
|
+
#{ENV_KEYS[:refresh]}='#{keys["refresh_token"]}'
|
45
|
+
TEXT
|
46
|
+
end
|
12
47
|
|
13
|
-
|
14
|
-
|
15
|
-
puts GoogleDrive.generate_access_code_url(client_id)
|
16
|
-
puts
|
17
|
-
puts "This will create an access token."
|
18
|
-
puts
|
19
|
-
|
20
|
-
exit 0
|
21
|
-
|
22
|
-
elsif ARGV[0] == "gen_refresh_token"
|
48
|
+
def handle_auth(options)
|
49
|
+
drive = GoogleDrive::new(raise_error: false, loglevel: Logger::ERROR)
|
23
50
|
|
24
|
-
|
25
|
-
|
26
|
-
access_code = ARGV[3]
|
51
|
+
# Handle special service mode cases
|
52
|
+
case options[:authmode]
|
27
53
|
|
28
|
-
|
54
|
+
when "file"
|
55
|
+
drive.load_api_keys_from_file(options[:file])
|
56
|
+
options[:persist] = true
|
57
|
+
service_mode="manual"
|
58
|
+
|
59
|
+
when "cli"
|
60
|
+
unless options[:id] && options[:secret] && options[:refresh]
|
61
|
+
puts "--id, --secret, and --refresh must be provided for CLI mode"
|
62
|
+
exit 1
|
63
|
+
end
|
64
|
+
options[:persist] = true
|
65
|
+
drive.set_api_keys(
|
66
|
+
client_id: options[:id],
|
67
|
+
client_secret: options[:secret],
|
68
|
+
refresh_token: options[:refresh],
|
69
|
+
)
|
70
|
+
service_mode = "manual"
|
71
|
+
|
72
|
+
else
|
73
|
+
service_mode = options[:authmode]
|
74
|
+
end
|
75
|
+
|
76
|
+
unless drive.create_service(mode: service_mode)
|
77
|
+
puts "Failure creating drive service. See above for details."
|
78
|
+
exit 1
|
79
|
+
end
|
29
80
|
|
30
|
-
|
31
|
-
|
32
|
-
|
81
|
+
print_hash_for_env_use(drive.dump_keys)
|
82
|
+
if options[:persist]
|
83
|
+
if drive.generate_env_file
|
84
|
+
puts "Configuration persisted to #{drive.instance_variable_get(:@api_key_file)}"
|
85
|
+
else
|
86
|
+
puts "Error persisting environment to #{drive.instance_variable_get(:@api_key_file)}"
|
87
|
+
end
|
33
88
|
end
|
34
|
-
puts "\nRefresh Token: #{refresh_token}\n\n"
|
35
|
-
|
36
|
-
exit 0
|
37
|
-
elsif ARGV[0] == "?" || ARGV[0] == "-?" || ARGV[0] == "-help"
|
38
|
-
puts "googledrive gen_url <client_id>"
|
39
|
-
puts "googledrive gen_refresh_token <client_id> <client_secret> <access_code>"
|
40
|
-
puts
|
41
|
-
exit 0
|
42
|
-
else
|
43
|
-
puts "Invalid argument #{ARGV[0]}"
|
44
|
-
exit -1
|
45
89
|
end
|
46
90
|
|
91
|
+
handle_auth(options)
|
@@ -0,0 +1,223 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require 'json'
|
3
|
+
require 'google/apis/drive_v3'
|
4
|
+
require_relative './googledrive-easy'
|
5
|
+
|
6
|
+
class GoogleDriveEasyTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
def configure_environment
|
9
|
+
ENV['DRIVEAPI_CLIENT_ID'] = @client_id
|
10
|
+
ENV['DRIVEAPI_CLIENT_SECRET'] = @client_secret
|
11
|
+
ENV['DRIVEAPI_REFRESH_TOKEN'] = @refresh_token
|
12
|
+
end
|
13
|
+
|
14
|
+
def deconfigure_environment
|
15
|
+
vars = %w(DRIVEAPI_CLIENT_ID DRIVEAPI_CLIENT_SECRET DRIVEAPI_REFRESH_TOKEN)
|
16
|
+
vars.each do |v|
|
17
|
+
ENV.delete(v)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def configure_env_file
|
22
|
+
# Write temp json file to load from for test
|
23
|
+
@env_file = Tempfile.new("api_var_file")
|
24
|
+
@env_file.write(JSON.generate({
|
25
|
+
"CLIENT_ID": @client_id,
|
26
|
+
"REFRESH_TOKEN": @refresh_token,
|
27
|
+
"CLIENT_SECRET": @client_secret,
|
28
|
+
}))
|
29
|
+
@env_file.flush
|
30
|
+
return @env_file
|
31
|
+
end
|
32
|
+
|
33
|
+
def deconfigure_env_file
|
34
|
+
@env_file.delete
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup
|
38
|
+
# Unset env variables before each test to keep things predictable
|
39
|
+
deconfigure_environment
|
40
|
+
|
41
|
+
# Used to omit tests that require real drive connectivity.
|
42
|
+
@test_env_vars_defined = (ENV['TEST_CLIENT_ID'] && ENV['TEST_CLIENT_SECRET'] && ENV['TEST_CLIENT_SECRET'])
|
43
|
+
|
44
|
+
# Init
|
45
|
+
@client_id = ENV['TEST_CLIENT_ID'] || 'test-id'
|
46
|
+
@client_secret = ENV['TEST_CLIENT_SECRET'] || 'test-secret'
|
47
|
+
@refresh_token = ENV['TEST_REFRESH_TOKEN'] || 'test-token'
|
48
|
+
@access_token = nil
|
49
|
+
|
50
|
+
@drive = GoogleDrive::new
|
51
|
+
@drive.instance_variable_set(:@api_key_file, "/tmp/test_drive_api_keys.json")
|
52
|
+
end
|
53
|
+
|
54
|
+
#######################
|
55
|
+
# Loading tests #
|
56
|
+
#######################
|
57
|
+
def test_loading_api_vars_from_env_success
|
58
|
+
configure_environment
|
59
|
+
|
60
|
+
result = @drive.load_api_keys_from_env
|
61
|
+
assert(result, "load_api_keys_from_env should return true")
|
62
|
+
assert_equal(@client_id, @drive.instance_variable_get(:@client_id), "Client ID should be loaded")
|
63
|
+
assert_equal(@client_secret, @drive.instance_variable_get(:@client_secret), "Client Secret should be loaded")
|
64
|
+
assert_equal(@refresh_token, @drive.instance_variable_get(:@refresh_token), "Refresh Token should be loaded")
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_loading_api_vars_from_env_missing_fail
|
68
|
+
ENV['DRIVEAPI_CLIENT_ID'] = @client_id
|
69
|
+
ENV.delete('DRIVEAPI_CLIENT_SECRET')
|
70
|
+
ENV['DRIVEAPI_REFRESH_TOKEN'] = @refresh_token
|
71
|
+
|
72
|
+
result = @drive.load_api_keys_from_env
|
73
|
+
assert(!result, "load_api_keys_from_env should return false")
|
74
|
+
assert_equal(nil, @drive.instance_variable_get(:@client_id), "Client ID should not be loaded")
|
75
|
+
assert_equal(nil, @drive.instance_variable_get(:@client_secret), "Client Secret should not be loaded")
|
76
|
+
assert_equal(nil, @drive.instance_variable_get(:@refresh_token), "Refresh Token should not be loaded")
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_loading_api_vars_from_file_success
|
80
|
+
# Write temp json file to load from for test
|
81
|
+
configure_env_file
|
82
|
+
|
83
|
+
# Override default key file
|
84
|
+
# @drive.instance_variable_set(:@api_key_file, @env_file.path)
|
85
|
+
|
86
|
+
# Attempt load
|
87
|
+
result = @drive.load_api_keys_from_file(path:@env_file.path)
|
88
|
+
assert(result, "load_api_keys_from_file should return true")
|
89
|
+
assert_equal(@client_id, @drive.instance_variable_get(:@client_id), "Client ID should be loaded")
|
90
|
+
assert_equal(@client_secret, @drive.instance_variable_get(:@client_secret), "Client Secret should be loaded")
|
91
|
+
assert_equal(@refresh_token, @drive.instance_variable_get(:@refresh_token), "Refresh Token should be loaded")
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_loading_api_vars_from_file_not_found
|
95
|
+
# Override default key file
|
96
|
+
@drive.instance_variable_set(:@api_key_file, '/tmp/bogons')
|
97
|
+
|
98
|
+
# Attempt load
|
99
|
+
result = @drive.load_api_keys_from_file
|
100
|
+
assert(!result, "load_api_keys_from_file should return false")
|
101
|
+
assert_equal(nil, @drive.instance_variable_get(:@client_id), "Client ID should be loaded")
|
102
|
+
assert_equal(nil, @drive.instance_variable_get(:@client_secret), "Client Secret should be loaded")
|
103
|
+
assert_equal(nil, @drive.instance_variable_get(:@refresh_token), "Refresh Token should be loaded")
|
104
|
+
end
|
105
|
+
|
106
|
+
###########################
|
107
|
+
# Object generation tests #
|
108
|
+
###########################
|
109
|
+
|
110
|
+
def test_generate_api_secret_hash
|
111
|
+
@drive.instance_variable_set(:@client_id, @client_id)
|
112
|
+
@drive.instance_variable_set(:@client_secret, @client_secret)
|
113
|
+
|
114
|
+
api_hash = @drive.generate_api_secret_hash
|
115
|
+
assert_equal(@client_id, api_hash['installed']['client_id'], 'Hash should contain client id')
|
116
|
+
assert_equal(@client_secret, api_hash['installed']['client_secret'], 'Hash should contain client secret')
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_generate_token_yaml_success
|
120
|
+
# TODO: Test feels partial/fragile. Improve?
|
121
|
+
# Partially Configure object
|
122
|
+
@drive.instance_variable_set(:@client_id, @client_id)
|
123
|
+
@drive.instance_variable_set(:@refresh_token, @refresh_token)
|
124
|
+
token_file_user = @drive.instance_variable_get(:@token_file_user)
|
125
|
+
|
126
|
+
yaml_string = @drive.generate_token_yaml
|
127
|
+
assert(yaml_string.include?(token_file_user), "Result should contain token_file_user")
|
128
|
+
assert(yaml_string.include?(@client_id), "Result should contain client_id")
|
129
|
+
assert(yaml_string.include?(@refresh_token), "Result should contain refresh_token")
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_generate_token_yaml_fail
|
134
|
+
# Partially Configure object
|
135
|
+
# @drive_wrapper.instance_variable_set(:@client_id, @client_id)
|
136
|
+
@drive.instance_variable_set(:@refresh_token, @refresh_token)
|
137
|
+
assert(!@drive.generate_token_yaml, "Generation of token yaml should fail with partial config")
|
138
|
+
end
|
139
|
+
|
140
|
+
def test_generate_temp_token_file
|
141
|
+
# Partially Configure object
|
142
|
+
@drive.instance_variable_set(:@client_id, @client_id)
|
143
|
+
@drive.instance_variable_set(:@refresh_token, @refresh_token)
|
144
|
+
token_file_user = @drive.instance_variable_get(:@token_file_user)
|
145
|
+
|
146
|
+
tmp_file_path = @drive.generate_temp_token_file
|
147
|
+
tmp_file_contents = File.read(tmp_file_path)
|
148
|
+
assert(tmp_file_contents.include?(token_file_user), "Result should contain token_file_user")
|
149
|
+
assert(tmp_file_contents.include?(@client_id), "Result should contain client_id")
|
150
|
+
assert(tmp_file_contents.include?(@refresh_token), "Result should contain refresh_token")
|
151
|
+
end
|
152
|
+
|
153
|
+
#########################################
|
154
|
+
# Auth tests (Require real credentials) #
|
155
|
+
#########################################
|
156
|
+
|
157
|
+
def test_create_service_environment_file
|
158
|
+
omit("Test Environment not correctly configured for live tests") unless @test_env_vars_defined
|
159
|
+
configure_env_file
|
160
|
+
|
161
|
+
# Load environment
|
162
|
+
# Use this, or just set the path directly.
|
163
|
+
@drive.load_api_keys_from_file(path:@env_file.path)
|
164
|
+
|
165
|
+
# Authorize
|
166
|
+
result = @drive.create_service(mode: 'manual')
|
167
|
+
assert(result, "Authorization from configured environment should succeed.")
|
168
|
+
assert(@drive.instance_variable_get(:@credentials), "Credentials should be set")
|
169
|
+
assert_instance_of(Google::Apis::DriveV3::DriveService, result, "Should return instance of Google Drive Service")
|
170
|
+
end
|
171
|
+
|
172
|
+
def test_create_service_environment
|
173
|
+
omit("Test Environment not correctly configured for live tests") unless @test_env_vars_defined
|
174
|
+
configure_environment
|
175
|
+
|
176
|
+
# Authorize
|
177
|
+
result = @drive.create_service(mode: 'environment')
|
178
|
+
assert(result, "Authorization from configured environment should succeed.")
|
179
|
+
assert(@drive.instance_variable_get(:@credentials), "Credentials should be set")
|
180
|
+
puts @drive.instance_variable_get(:@credentials)
|
181
|
+
assert_instance_of(Google::Apis::DriveV3::DriveService, result, "Should return instance of Google Drive Service")
|
182
|
+
end
|
183
|
+
|
184
|
+
def test_create_service_manual
|
185
|
+
omit("Test Environment not correctly configured for live tests") unless @test_env_vars_defined
|
186
|
+
@drive.set_api_keys(client_id: @client_id, client_secret: @client_secret, refresh_token: @refresh_token)
|
187
|
+
|
188
|
+
# Authorize
|
189
|
+
result = @drive.create_service(mode: 'manual')
|
190
|
+
assert(result, "Authorization from manual configuration should succeed.")
|
191
|
+
assert(@drive.instance_variable_get(:@credentials), "Credentials should be set")
|
192
|
+
assert_instance_of(Google::Apis::DriveV3::DriveService, result, "Should return instance of Google Drive Service")
|
193
|
+
end
|
194
|
+
|
195
|
+
###########################################
|
196
|
+
# Action tests (Require real credentials) #
|
197
|
+
###########################################
|
198
|
+
|
199
|
+
# def test_list_all_files
|
200
|
+
# configure_environment
|
201
|
+
# @drive_wrapper.create_service(mode:'environment')
|
202
|
+
# p @drive_wrapper.list_all_files(query:'*')
|
203
|
+
# end
|
204
|
+
|
205
|
+
# No great way to unit test the following:
|
206
|
+
# get_oauth_credentials_via_loopback
|
207
|
+
# get_oauth_credentials_via_input
|
208
|
+
# find_directory_id
|
209
|
+
# download_file
|
210
|
+
# upload_file
|
211
|
+
# list_files
|
212
|
+
|
213
|
+
def test_get_all_files
|
214
|
+
omit("Test Environment not correctly configured for live tests") unless @test_env_vars_defined
|
215
|
+
# configure_environment
|
216
|
+
|
217
|
+
@drive.set_api_keys(client_id: @client_id, client_secret: @client_secret, refresh_token: @refresh_token)
|
218
|
+
@drive.create_service(mode:'manual')
|
219
|
+
res = @drive.get_all_files(name: "rxgos*")
|
220
|
+
assert_instance_of(Array, res, "Returns list")
|
221
|
+
end
|
222
|
+
|
223
|
+
end
|
data/lib/googledrive-easy.rb
CHANGED
@@ -1,238 +1,436 @@
|
|
1
1
|
require 'json'
|
2
|
-
require 'httparty'
|
3
2
|
require 'tempfile'
|
3
|
+
require 'google/apis/drive_v3'
|
4
|
+
require 'googleauth'
|
5
|
+
require 'googleauth/stores/file_token_store'
|
6
|
+
require 'highline'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
require_relative './utilities'
|
10
|
+
require_relative './googledrive/wildcard'
|
11
|
+
|
12
|
+
ENV_KEYS = {
|
13
|
+
:id => "DRIVEAPI_CLIENT_ID",
|
14
|
+
:secret => "DRIVEAPI_CLIENT_SECRET",
|
15
|
+
:refresh => "DRIVEAPI_REFRESH_TOKEN"
|
16
|
+
}
|
17
|
+
DRIVE_ENV_FILE = "#{Dir.home}/.drive_api_keys.json"
|
4
18
|
|
5
|
-
require_relative 'googledrive/wildcard'
|
6
|
-
#require 'googledrive/wildcard'
|
7
19
|
|
8
20
|
class GoogleDrive
|
21
|
+
# TODO: Test the upload, download, list methods. Write tests for them.
|
22
|
+
# TODO: Test using the gem in another program.
|
23
|
+
|
24
|
+
def initialize(raise_error: false, loglevel: Logger::ERROR )
|
25
|
+
# Config
|
26
|
+
@raise_error = raise_error
|
27
|
+
@file_fields = "id,kind,mime_type,name,md5Checksum,size,parents"
|
28
|
+
@md5_command = 'md5 -q' # Needs to output only the sum, no filename or header info.
|
29
|
+
|
30
|
+
# Init and configure logger.
|
31
|
+
# TODO: Make this smarter/respect global loggers.
|
32
|
+
@logger = Logger.new(STDOUT)
|
33
|
+
@logger.level = loglevel
|
34
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
35
|
+
date_format = datetime.strftime("%Y-%m-%d %H:%M:%S")
|
36
|
+
"[%s] %-5s (%s): %s\n" % [date_format, severity, self.class.name, msg ]
|
37
|
+
end
|
9
38
|
|
10
|
-
|
39
|
+
# Critical File Paths
|
40
|
+
@api_key_file = DRIVE_ENV_FILE
|
41
|
+
@loaded_api_key_file = @api_key_file
|
42
|
+
@drive_secret_path = "#{Dir.home}/.googledrive-secret.json" # Should contain Oauth2 data exactly as provided by GCloud Console
|
43
|
+
@drive_token_path = "#{Dir.home}/.googledrive-token.yaml" # Created by script to hold the token store
|
44
|
+
|
45
|
+
# Oauth Config
|
46
|
+
@drive_scope = 'https://www.googleapis.com/auth/drive'
|
47
|
+
@auth_url_path = '/'
|
48
|
+
@oauth_address = 'localhost' # Should be `localhost`, `127.0.0.1`, or `[::1]`
|
49
|
+
@oauth_port = 8181 # Pick anything outside the privileged range [1-1023] to avoid running SSH as root.
|
50
|
+
@oauth_loopback_base_url = "http://#{@oauth_address}:#{@oauth_port}"
|
51
|
+
|
52
|
+
# Client vars
|
53
|
+
@token_file_user = 'cliuser' # The user to store credentials as in the token file
|
11
54
|
@client_id = nil
|
12
55
|
@client_secret = nil
|
13
56
|
@refresh_token = nil
|
14
|
-
|
15
57
|
@access_token = nil
|
58
|
+
@expiration = 1665718429000
|
16
59
|
|
17
|
-
|
60
|
+
# Core runtime vars
|
61
|
+
@drive_service = nil
|
62
|
+
@authorizer = nil
|
63
|
+
@credentials = nil
|
18
64
|
end
|
19
65
|
|
66
|
+
def print_config_hints
|
67
|
+
puts <<~TEXT.chomp
|
68
|
+
To run in headless modes (environment or file configuration), you must provide either:
|
69
|
+
* A json file at #{@api_key_file} containing the following structure:
|
70
|
+
{
|
71
|
+
"CLIENT_ID": "your Google Cloud Oauth client ID",
|
72
|
+
"CLIENT_SECRET": "your Google Cloud Oauth client secret",
|
73
|
+
"REFRESH_TOKEN": "A valid refresh token from a prior Oauth completion with the ID and SECRET",
|
74
|
+
}
|
20
75
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
76
|
+
* The following environment variables:
|
77
|
+
#{ENV_KEYS[:id]}='your Google Cloud Oauth client ID'
|
78
|
+
#{ENV_KEYS[:secret]}='your Google Cloud Oauth client secret'
|
79
|
+
#{ENV_KEYS[:refresh]}='A valid refresh token from a prior Oauth completion with the ID and SECRET'
|
80
|
+
|
81
|
+
For the interactive auth modes (loopback or input) you can omit the refresh token from either of the above.
|
82
|
+
If you later switch to a headless mode, you will need to add the refresh token.
|
83
|
+
TEXT
|
25
84
|
end
|
26
85
|
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
client_secret: client_secret,
|
32
|
-
code: access_code,
|
33
|
-
redirect_uri: 'urn:ietf:wg:oauth:2.0:oob',
|
34
|
-
grant_type: 'authorization_code'
|
35
|
-
},
|
36
|
-
headers: {
|
37
|
-
'Content-Type' => 'application/x-www-form-urlencoded'
|
38
|
-
}
|
39
|
-
}
|
40
|
-
|
41
|
-
response = HTTParty.post('https://accounts.google.com/o/oauth2/token', options)
|
86
|
+
def log_error_and_raise(msg)
|
87
|
+
@logger.error(msg)
|
88
|
+
raise "msg" if @raise_error
|
89
|
+
end
|
42
90
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
puts "Non-200 HTTP error code: #{response.code}."
|
91
|
+
def set_api_keys(client_id:nil, client_secret:nil, refresh_token:nil, access_token:nil, require_refresh_token:true)
|
92
|
+
# Recommended service mode if manually called: manual
|
93
|
+
unless client_id && client_secret && (refresh_token || !require_refresh_token)
|
94
|
+
log_error_and_raise("Not all tokens provided.")
|
48
95
|
return false
|
49
96
|
end
|
97
|
+
@client_id = client_id if client_id
|
98
|
+
@client_secret = client_secret if client_secret
|
99
|
+
@refresh_token = refresh_token if refresh_token
|
100
|
+
@access_token = access_token if access_token
|
101
|
+
return true
|
102
|
+
end
|
103
|
+
|
104
|
+
def load_api_keys_from_file(path:nil, require_refresh_token:true)
|
105
|
+
@loaded_api_key_file = path ? path : @api_key_file
|
106
|
+
if(File.exist?(@loaded_api_key_file))
|
107
|
+
@logger.info("API key file #{@loaded_api_key_file} exists")
|
108
|
+
begin
|
109
|
+
api_hash = JSON.parse(File.read(@loaded_api_key_file))
|
110
|
+
rescue => error
|
111
|
+
log_error_and_raise("Error opening api key file: " + error.inspect)
|
112
|
+
return false
|
113
|
+
end
|
50
114
|
|
51
|
-
|
52
|
-
|
53
|
-
|
115
|
+
@logger.debug("api_hash: " + api_hash.inspect)
|
116
|
+
|
117
|
+
@client_id = api_hash["CLIENT_ID"]
|
118
|
+
@client_secret = api_hash["CLIENT_SECRET"]
|
119
|
+
@refresh_token = api_hash["REFRESH_TOKEN"]
|
120
|
+
|
121
|
+
if set_api_keys(
|
122
|
+
client_id:api_hash["CLIENT_ID"],
|
123
|
+
client_secret:api_hash["CLIENT_SECRET"],
|
124
|
+
refresh_token:api_hash["REFRESH_TOKEN"],
|
125
|
+
require_refresh_token:require_refresh_token
|
126
|
+
)
|
127
|
+
@logger.info("Using Google Drive API information from #{@loaded_api_key_file}")
|
128
|
+
else
|
129
|
+
log_error_and_raise("Not all API keys were in file #{@loaded_api_key_file}.")
|
130
|
+
return false
|
131
|
+
end
|
132
|
+
else
|
133
|
+
log_error_and_raise("Cannot find #{@loaded_api_key_file}")
|
54
134
|
return false
|
55
135
|
end
|
136
|
+
true
|
137
|
+
end
|
56
138
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
139
|
+
def load_api_keys_from_env(require_refresh_token:true)
|
140
|
+
# Google Drive Credentials from ENV if not derived from JSON file in home.
|
141
|
+
vars = [ENV_KEYS[:id], ENV_KEYS[:secret]]
|
142
|
+
vars << ENV_KEYS[:refresh] if require_refresh_token
|
143
|
+
vars.each do |v|
|
144
|
+
if ENV[v].nil?
|
145
|
+
log_error_and_raise("#{v} export variable not set.")
|
146
|
+
return false
|
147
|
+
end
|
148
|
+
end
|
66
149
|
|
150
|
+
# Set
|
151
|
+
return set_api_keys(
|
152
|
+
client_id:ENV[ENV_KEYS[:id]],
|
153
|
+
client_secret:ENV[ENV_KEYS[:secret]],
|
154
|
+
refresh_token:ENV[ENV_KEYS[:refresh]],
|
155
|
+
require_refresh_token:require_refresh_token
|
156
|
+
)
|
157
|
+
end
|
67
158
|
|
159
|
+
def generate_api_secret_hash
|
160
|
+
{
|
161
|
+
"installed" => {
|
162
|
+
"client_id" => @client_id,
|
163
|
+
"client_secret" => @client_secret,
|
164
|
+
# "project_id"=>"super-secret-project",
|
165
|
+
# "auth_uri"=>"https://accounts.google.com/o/oauth2/auth",
|
166
|
+
# "token_uri"=>"https://oauth2.googleapis.com/token",
|
167
|
+
# "auth_provider_x509_cert_url"=>"https://www.googleapis.com/oauth2/v1/certs",
|
168
|
+
# "redirect_uris"=>["http://localhost"]
|
169
|
+
}
|
170
|
+
}
|
171
|
+
end
|
68
172
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
173
|
+
def dump_keys
|
174
|
+
{
|
175
|
+
"client_id" => @client_id,
|
176
|
+
"client_secret" => @client_id,
|
177
|
+
"access_token" => @access_token,
|
178
|
+
"refresh_token" => @refresh_token,
|
179
|
+
}
|
180
|
+
end
|
74
181
|
|
75
|
-
|
76
|
-
|
182
|
+
# Generates token yaml
|
183
|
+
def generate_token_yaml
|
184
|
+
unless @client_id && @refresh_token
|
185
|
+
api_config = {"client_id" => @client_id, "refresh_token" => @refresh_token }
|
186
|
+
@logger.debug("API Config: #{api_config}")
|
187
|
+
log_error_and_raise("Some required API config for token hasn't been configured yet")
|
77
188
|
return false
|
78
189
|
end
|
79
190
|
|
80
|
-
|
191
|
+
drive_token_hash = {
|
192
|
+
"client_id" => @client_id,
|
193
|
+
"access_token" => @access_token,
|
194
|
+
"refresh_token" => @refresh_token,
|
195
|
+
"scope" => [@drive_scope],
|
196
|
+
"expiration_time_millis" => @expiration
|
197
|
+
}
|
198
|
+
return {@token_file_user => drive_token_hash.to_json}.to_hash.to_yaml
|
81
199
|
end
|
82
200
|
|
83
|
-
|
84
|
-
|
85
|
-
|
201
|
+
# Generates temp token file and return path to it
|
202
|
+
def generate_temp_token_file
|
203
|
+
tmpfile = Tempfile.new('drive_token')
|
204
|
+
token_yaml = generate_token_yaml
|
205
|
+
unless token_yaml
|
86
206
|
return false
|
87
207
|
end
|
208
|
+
tmpfile.write(token_yaml)
|
209
|
+
tmpfile.flush # flush file contents before continuing.
|
210
|
+
return @drive_token_path = File.expand_path(tmpfile.path)
|
211
|
+
end
|
88
212
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
213
|
+
def generate_env_file(path=@api_key_file)
|
214
|
+
File.open(path,"w") do |f|
|
215
|
+
return f.write({
|
216
|
+
"CLIENT_ID" => @client_id,
|
217
|
+
"CLIENT_SECRET" => @client_secret,
|
218
|
+
"REFRESH_TOKEN" => @refresh_token,
|
219
|
+
}.to_json)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Sets up a webserver to receive a 'localhost' query for 3-legged OAUTH. Requires that @oauth_port is forwarded
|
224
|
+
# from the machine running the authenticating browser to the machine running this script,
|
225
|
+
# likely via `ssh user@host -L @oauth_port:@oauth_address:@oauth_port`
|
226
|
+
def get_oauth_credentials_via_loopback(authorizer)
|
227
|
+
# TODO: This a raw copy, refactor
|
228
|
+
# Print instructions and URL for user to click
|
229
|
+
# TODO: Should this be puts or @logger? it's a user interaction...
|
230
|
+
puts "Listening on #{@oauth_address}:#{@oauth_port}."
|
231
|
+
puts "If this a remote system, you need to forward this port via SSH Tunneling, if you haven't already."
|
232
|
+
puts("eg: ssh user@host -L#{@oauth_port}:#{@oauth_address}:#{@oauth_port}")
|
233
|
+
puts "After you have done so, follow this link:", authorizer.get_authorization_url(base_url: @oauth_loopback_base_url)
|
234
|
+
|
235
|
+
# Start webserver to listen for response:
|
236
|
+
socket = TCPServer.new(@oauth_address, @oauth_port)
|
237
|
+
loop do
|
238
|
+
client = socket.accept
|
239
|
+
first_line = client.gets
|
240
|
+
verb, path, _ = first_line.split
|
241
|
+
|
242
|
+
if verb == 'GET'
|
243
|
+
if result = path.match(/^\/\?code=(.*)&scope=(.*)/)
|
244
|
+
code = result[1]
|
245
|
+
scope = result[2]
|
246
|
+
response = "HTTP/1.1 200\r\n\r\nAuthorized for scope `#{scope}`! Code is #{code}"
|
247
|
+
client.puts(response)
|
248
|
+
client.close
|
249
|
+
socket.close
|
250
|
+
|
251
|
+
puts "Authorized for scope `#{scope}`! Code is #{code}"
|
252
|
+
# Extract response
|
253
|
+
puts 'Oauth flow complete. You can close the local port forward if desired.'
|
254
|
+
return authorizer.get_and_store_credentials_from_code(
|
255
|
+
user_id: @token_file_user,
|
256
|
+
code: code,
|
257
|
+
base_url: @oauth_loopback_base_url
|
258
|
+
)
|
259
|
+
else
|
260
|
+
# Default response for testing/sanity checks
|
261
|
+
response = "HTTP/1.1 200\r\n\r\nI respond to auth requests."
|
262
|
+
client.puts(response)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
client.close
|
94
266
|
end
|
267
|
+
socket.close
|
268
|
+
end
|
95
269
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
270
|
+
def get_oauth_credentials_via_input(authorizer)
|
271
|
+
# TODO: This a raw copy, refactor
|
272
|
+
puts 'Follow this url and complete the sign-in process. The login will result in an error, do not close it.'
|
273
|
+
puts 'Instead, copy and paste the value of the `code` parameter (begins with 4/)'
|
274
|
+
puts authorizer.get_authorization_url(base_url: "http://localhost:1")
|
275
|
+
puts ''
|
276
|
+
code = HighLine::ask "Please enter code:"
|
277
|
+
puts "Got code: #{code}"
|
278
|
+
|
279
|
+
return authorizer.get_and_store_credentials_from_code(
|
280
|
+
user_id: @token_file_user,
|
281
|
+
code: code,
|
282
|
+
base_url: "http://localhost:1"
|
283
|
+
)
|
284
|
+
end
|
100
285
|
|
101
|
-
|
102
|
-
|
286
|
+
def create_service(mode:'environment')
|
287
|
+
@logger.debug("Passed authorization mode: #{mode}")
|
288
|
+
|
289
|
+
unless %w(environment loopback input manual).include?(mode)
|
290
|
+
log_error_and_raise("Unknown authorization mode")
|
103
291
|
return false
|
104
292
|
end
|
105
293
|
|
106
|
-
|
107
|
-
end
|
294
|
+
# TODO: Figure out balance between always requiring env config and just reading from the token store if it already exists
|
108
295
|
|
296
|
+
interactive_mode = %w(loopback input).include?(mode)
|
297
|
+
# Attempt to load keys from environment, unless manual. Doing ahead for the benefit of both cases.
|
109
298
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
299
|
+
if mode == "manual"
|
300
|
+
key_load_result = false
|
301
|
+
else
|
302
|
+
key_load_result = (load_api_keys_from_env(require_refresh_token:!interactive_mode) || load_api_keys_from_file(require_refresh_token:!interactive_mode))
|
303
|
+
end
|
304
|
+
|
305
|
+
if interactive_mode && !key_load_result
|
306
|
+
print_config_hints
|
307
|
+
end
|
308
|
+
|
309
|
+
# If environment, we need key load to have succeeded completely
|
310
|
+
# If manual, it should pull from memory.
|
311
|
+
if %w(environment manual).include?(mode)
|
312
|
+
tmp_token_path = generate_temp_token_file
|
313
|
+
unless tmp_token_path
|
314
|
+
log_error_and_raise("Failed to generate temporary token file")
|
315
|
+
return false
|
316
|
+
end
|
317
|
+
@logger.debug(File.read(tmp_token_path))
|
318
|
+
unless key_load_result || mode=='manual'
|
319
|
+
log_error_and_raise("Unable to load api keys from environment")
|
320
|
+
return false
|
321
|
+
end
|
322
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: tmp_token_path)
|
323
|
+
else
|
324
|
+
# Otherwise, just the ID and secret are enough
|
325
|
+
unless @client_secret && @client_id
|
326
|
+
log_error_and_raise("Client Secret or ID missing.")
|
327
|
+
return false
|
328
|
+
end
|
329
|
+
token_store = Google::Auth::Stores::FileTokenStore.new(file: @drive_token_path)
|
330
|
+
end
|
331
|
+
|
332
|
+
@authorizer = Google::Auth::UserAuthorizer.new(
|
333
|
+
Google::Auth::ClientId::from_hash(generate_api_secret_hash),
|
334
|
+
@drive_scope,
|
335
|
+
token_store,
|
336
|
+
@auth_url_path
|
337
|
+
)
|
338
|
+
@logger.debug("google_authorizer: " + @authorizer.inspect)
|
339
|
+
|
340
|
+
# Attempt to retrieve credentials
|
341
|
+
@credentials = @authorizer.get_credentials(@token_file_user)
|
342
|
+
if @credentials.nil?
|
343
|
+
case mode
|
344
|
+
when 'input'
|
345
|
+
@credentials = get_oauth_credentials_via_input(@authorizer)
|
346
|
+
when 'loopback'
|
347
|
+
@credentials = get_oauth_credentials_via_loopback(@authorizer)
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
# Final cred check
|
352
|
+
if @credentials.nil?
|
353
|
+
@logger.error('Unable to retrieve credentials')
|
127
354
|
return false
|
128
355
|
end
|
129
356
|
|
130
|
-
|
131
|
-
|
357
|
+
# Update internal credentials based on credentials loaded
|
358
|
+
@client_id = @credentials.client_id
|
359
|
+
@client_secret = @credentials.client_secret
|
360
|
+
@refresh_token = @credentials.refresh_token
|
361
|
+
|
362
|
+
@drive_service = Google::Apis::DriveV3::DriveService.new
|
363
|
+
@drive_service.authorization = @credentials
|
364
|
+
return @drive_service
|
365
|
+
end
|
366
|
+
|
367
|
+
def process_file(file)
|
368
|
+
file_hash = { }
|
369
|
+
file_hash[:name] = file.name
|
370
|
+
file_hash[:id] = file.id
|
371
|
+
file_hash[:isfolder] = file.mime_type == 'application/vnd.google-apps.folder'
|
372
|
+
file_hash[:size] = file.size if file.size
|
373
|
+
file_hash[:md5] = file.md5_checksum if file.md5_checksum
|
374
|
+
file_hash[:parents] = file.parents if file.parents
|
375
|
+
return file_hash
|
132
376
|
end
|
133
377
|
|
134
378
|
# parentfolderid: "root" gets the root directory. Not all folders are under the root. Has to do with permissions
|
135
|
-
# and how Google Drive works.
|
379
|
+
# and how Google Drive works.
|
380
|
+
# https://developers.google.com/drive/api/v3/reference/query-ref
|
136
381
|
def get_all_files(justfiles: false, justfolders: false, parentfolderid: nil, name: nil)
|
382
|
+
unless @drive_service
|
383
|
+
log_error_and_raise("Drive service not initialized.")
|
384
|
+
return false
|
385
|
+
end
|
137
386
|
|
138
387
|
# Number of files/directories to be returned each call to /files.
|
139
388
|
# multiple page sizes are handled with the pageToken return value.
|
140
|
-
# 100 is default from google.
|
141
|
-
|
142
|
-
|
389
|
+
# 100 is default from google.
|
390
|
+
page_size = 100
|
391
|
+
|
392
|
+
# Fields param gives us extra juicy info like MD5 sums and file sizes
|
393
|
+
fields = "kind,incomplete_search,next_page_token,files(#{@file_fields})"
|
394
|
+
|
395
|
+
# Build query:
|
396
|
+
query = "(trashed = false)"
|
397
|
+
query +=" and (mimeType != 'application/vnd.google-apps.folder')" if justfiles && !justfolders
|
398
|
+
query += " and (mimeType = 'application/vnd.google-apps.folder')" if justfolders && !justfiles
|
399
|
+
# parent folder has to be surrounded by single quotes in query
|
400
|
+
query += " and ('#{parentfolderid}' in parents)" if parentfolderid
|
401
|
+
# filename has to be surrounded by single quotes in query
|
402
|
+
query += " and (name #{ name =~ /\*/ ? "contains" : "=" } '#{name}')" if name
|
403
|
+
|
143
404
|
files = [ ]
|
144
|
-
|
405
|
+
next_page_token = nil
|
145
406
|
loop do
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
url = url + "?pageSize=#{pageSize}"
|
153
|
-
|
154
|
-
# If a query, it must be appended to all URL
|
155
|
-
# From SO: https://stackoverflow.com/questions/62069155/how-to-filter-google-drive-api-v3-mimetype
|
156
|
-
#var query = "('GoogleDriveFolderKey01' in parents or 'GoogleDriveFolderKey02' in parents) and trashed = false and mimeType = 'application/vnd.google-apps.folder'"
|
157
|
-
|
158
|
-
# default query is non-trashed folders
|
159
|
-
query = "(trashed = false)"
|
160
|
-
|
161
|
-
if justfiles && !justfolders
|
162
|
-
query = query + " and (mimeType != 'application/vnd.google-apps.folder')"
|
163
|
-
end
|
164
|
-
|
165
|
-
if justfolders && !justfiles
|
166
|
-
query = query + " and (mimeType = 'application/vnd.google-apps.folder')"
|
167
|
-
end
|
168
|
-
|
169
|
-
if parentfolderid # parent folder has to be surrounded by single quotes in query
|
170
|
-
query = query + " and ('#{parentfolderid}' in parents)"
|
171
|
-
end
|
172
|
-
|
173
|
-
if name # filename has to be surrounded by single quotes in query
|
174
|
-
if name =~ /\*/ # if name contains wildcard
|
175
|
-
query = query + " and (name contains '#{name}')"
|
176
|
-
else
|
177
|
-
query = query + " and (name = '#{name}')"
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
url = url + "&q=" + URI.escape(query)
|
182
|
-
|
183
|
-
if nextPageToken
|
184
|
-
url = url + "&pageSize=1&pageToken=#{nextPageToken}"
|
185
|
-
end
|
186
|
-
|
187
|
-
response = HTTParty.get(url, :headers => headers)
|
188
|
-
|
189
|
-
if response.code == 404 # file not found, not an error
|
190
|
-
break
|
191
|
-
elsif response.code != 200
|
192
|
-
if @raise_error; raise "Non-200 HTTP error code: #{response.code}: " + response.parsed_response.inspect end
|
193
|
-
return false
|
194
|
-
end
|
195
|
-
|
196
|
-
if response.parsed_response.has_key?("nextPageToken")
|
197
|
-
nextPageToken = response.parsed_response["nextPageToken"]
|
198
|
-
else
|
199
|
-
nextPageToken = nil
|
200
|
-
end
|
201
|
-
|
202
|
-
if !response.parsed_response.has_key?("files")
|
203
|
-
if @raise_error; raise "Required key not in response: 'files'." end
|
204
|
-
return false
|
205
|
-
end
|
407
|
+
# TODO: Should this be converted to block form and then use that to gracefully handle failure and errors?
|
408
|
+
files_page = @drive_service.list_files(page_size: page_size, q: query, page_token: next_page_token, fields: fields)
|
409
|
+
files_page.files.each {|f| files << f}
|
410
|
+
next_page_token = files_page.next_page_token
|
411
|
+
break unless next_page_token
|
412
|
+
end
|
206
413
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
file_hash[:id] = gd_file_entry["id"]
|
213
|
-
file_hash[:isfolder] = false
|
214
|
-
if gd_file_entry["mimeType"] == 'application/vnd.google-apps.folder'
|
215
|
-
file_hash[:isfolder] = true
|
216
|
-
end
|
217
|
-
|
218
|
-
files << file_hash
|
219
|
-
end
|
220
|
-
|
221
|
-
break if nextPageToken.nil?
|
414
|
+
# Process the returned files
|
415
|
+
# Todo: Do we really need to convert these now that the API returns real objects?
|
416
|
+
processed_files = []
|
417
|
+
files.each do |file|
|
418
|
+
processed_files << process_file(file)
|
222
419
|
end
|
223
420
|
|
421
|
+
# Todo: Is this still really necessary? I think the list_files function can do this server-side. Need to research.
|
224
422
|
# we have additional processing to do it a wildcard character was passed. Because Google Drive "contains" returns all portions of it.
|
225
423
|
# so we need to filter here
|
226
424
|
if name =~ /\*/ # if name contains wildcard
|
227
425
|
ret_files = [ ]
|
228
|
-
|
426
|
+
processed_files.each do |file|
|
229
427
|
if GoogleDriveWildcard.new(name) =~ file[:name]
|
230
428
|
ret_files << file
|
231
429
|
end
|
232
430
|
end
|
233
431
|
return ret_files
|
234
432
|
else
|
235
|
-
return
|
433
|
+
return processed_files
|
236
434
|
end
|
237
435
|
end
|
238
436
|
|
@@ -241,143 +439,91 @@ class GoogleDrive
|
|
241
439
|
return get_all_files(justfiles: true, parentfolderid: parentfolderid, name: name)
|
242
440
|
end
|
243
441
|
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
headers = {
|
248
|
-
"GData-Version" => "3.0",
|
249
|
-
"Authorization" => "Bearer #{@access_token}"
|
250
|
-
}
|
251
|
-
|
252
|
-
url = "https://www.googleapis.com/drive/v3/files/#{fileid}"
|
253
|
-
response = HTTParty.get(url, :headers => headers)
|
254
|
-
|
255
|
-
if response.code == 404 # not found. Could be normal
|
256
|
-
#puts "404 not found"
|
257
|
-
return false
|
258
|
-
elsif response.code != 200
|
259
|
-
if @raise_error; raise "Non-200 HTTP error code: #{response.code}: " + response.parsed_response.inspect end
|
442
|
+
def get_file_info(file_id)
|
443
|
+
unless @drive_service
|
444
|
+
log_error_and_raise("Drive service not initialized.")
|
260
445
|
return false
|
261
446
|
end
|
262
447
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
return false
|
267
|
-
elsif !response.parsed_response.has_key?("name")
|
268
|
-
return false
|
269
|
-
end
|
270
|
-
|
271
|
-
# returns the following keys: kind, id, name, mimeType
|
272
|
-
file_hash = { }
|
273
|
-
file_hash[:name] = response.parsed_response["name"]
|
274
|
-
file_hash[:id] = response.parsed_response["id"]
|
275
|
-
file_hash[:isfolder] = false
|
276
|
-
if response.parsed_response["mimeType"] == 'application/vnd.google-apps.folder'
|
277
|
-
file_hash[:isfolder] = true
|
278
|
-
end
|
279
|
-
|
280
|
-
return file_hash
|
448
|
+
# TODO: Maybe convert this to block format and handle errors like in other places
|
449
|
+
file = @drive_service.get_file(file_id:file_id, acknowledge_abuse:true, fields: "files(#{@file_fields})")
|
450
|
+
return process_file(file)
|
281
451
|
end
|
282
452
|
|
283
453
|
def find_directory_id(directory_name, parentfolderid: nil)
|
284
|
-
|
285
454
|
file_list = get_all_files(justfolders: true, name: directory_name, parentfolderid: parentfolderid)
|
286
455
|
|
287
456
|
if !file_list || (file_list.count == 0)
|
288
|
-
|
457
|
+
log_error_and_raise("Directory not found.")
|
289
458
|
return false
|
290
459
|
end
|
291
|
-
|
292
|
-
return file_list.first[:id]
|
293
|
-
end
|
294
|
-
|
460
|
+
|
461
|
+
return file_list.first[:id]
|
462
|
+
end
|
463
|
+
|
295
464
|
def upload_file(file, directory_id: nil)
|
465
|
+
unless @drive_service
|
466
|
+
log_error_and_raise("Drive service not initialized.")
|
467
|
+
return false
|
468
|
+
end
|
296
469
|
|
297
470
|
file_basename = File.basename(file)
|
298
|
-
|
471
|
+
# TODO: If no parent directory is passed, it will deny upload if a file by that name exists in any visible folder on any visible drive. How to fix?
|
299
472
|
# see if file exists on Drive
|
300
473
|
file_list = self.get_all_files(justfiles: true, parentfolderid: directory_id, name: file_basename)
|
301
474
|
if file_list.count > 0
|
302
|
-
|
303
|
-
return false
|
304
|
-
end
|
305
|
-
|
306
|
-
metadata = "name : '#{file_basename}'"
|
307
|
-
|
308
|
-
if !directory_id.nil? # if directory if specified, add it to the parent
|
309
|
-
metadata = metadata + ", parents : [ '#{directory_id}' ]"
|
310
|
-
end
|
311
|
-
|
312
|
-
metadata = "{#{metadata}}" # wrap metadata in braces
|
313
|
-
|
314
|
-
# Upload with CURL. HTTPParty and RestClient seem to be incompatible.
|
315
|
-
cmd = "curl -X POST -L " +
|
316
|
-
"--silent " +
|
317
|
-
"-H \"Authorization: Bearer #{@access_token}\" " +
|
318
|
-
"-F \"metadata=#{metadata};type=application/json;charset=UTF-8\" " +
|
319
|
-
"-F \"file=@#{file};\" " +
|
320
|
-
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"
|
321
|
-
|
322
|
-
# puts "cmd:\n#{cmd}"
|
323
|
-
|
324
|
-
response = `#{cmd} 2>/dev/null`
|
325
|
-
exit_status = $?.exitstatus
|
326
|
-
|
327
|
-
if exit_status != 0
|
328
|
-
if @raise_error; raise "CURL existed with non-0 error code: #{exit_status}." end
|
475
|
+
log_error_and_raise("ERROR: File '#{file_basename}' already exists.")
|
329
476
|
return false
|
330
477
|
end
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
return false
|
478
|
+
|
479
|
+
file_obj = Google::Apis::DriveV3::File.new(name: file_basename)
|
480
|
+
file_obj.parents = [directory_id] if directory_id
|
481
|
+
@drive_service.create_file(
|
482
|
+
file_obj,
|
483
|
+
upload_source: file,
|
484
|
+
fields: @file_fields
|
485
|
+
) do |resfile, err|
|
486
|
+
if err
|
487
|
+
log_error_and_raise("Error: #{err}.")
|
488
|
+
return false
|
489
|
+
end
|
490
|
+
|
491
|
+
# check that name = filename
|
492
|
+
# check that kind = drive#file
|
493
|
+
if !resfile.name # name key does not exist
|
494
|
+
log_error_and_raise("no name key specified in response.")
|
495
|
+
return false
|
496
|
+
elsif !resfile.kind # kind key does not exist
|
497
|
+
log_error_and_raise("no kind key specified in response.")
|
498
|
+
return false
|
499
|
+
elsif resfile.kind != "drive#file" # Not of file type
|
500
|
+
log_error_and_raise("kind is of non-file type.")
|
501
|
+
return false
|
502
|
+
elsif resfile.name != file_basename # file name mismatch
|
503
|
+
log_error_and_raise("file name mismatch.")
|
504
|
+
return false
|
505
|
+
end
|
506
|
+
# TODO: Add MD5 check, since we're now capable.
|
361
507
|
end
|
362
|
-
|
363
508
|
return true
|
364
509
|
end
|
365
|
-
|
510
|
+
|
366
511
|
# returns full path of downloaded file
|
367
512
|
def download_file(file_name_or_id, parentfolderid: nil, file_path: nil)
|
513
|
+
unless @drive_service
|
514
|
+
log_error_and_raise("Drive service not initialized.")
|
515
|
+
return false
|
516
|
+
end
|
368
517
|
|
369
|
-
#
|
370
|
-
# curl -H "Authorization: Bearer $token" "https://www.googleapis.com/drive/v3/files/$id?alt=media" -o "$file"
|
371
|
-
|
372
|
-
# if file path passed, check it is valid.
|
518
|
+
# if file path passed, check it is valid.
|
373
519
|
if file_path && !Dir.exist?(file_path)
|
374
|
-
|
520
|
+
log_error_and_raise("File path '#{file_path}' does not exist.")
|
375
521
|
return false
|
376
522
|
elsif !file_path # no path passed, use current directory
|
377
523
|
file_path = Dir.getwd
|
378
|
-
end
|
379
|
-
|
380
|
-
# path passed and valid. Append forward slash if not already there.
|
524
|
+
end
|
525
|
+
|
526
|
+
# path passed and valid. Append forward slash if not already there.
|
381
527
|
file_path = file_path.gsub(/\/$/, '') + "/"
|
382
528
|
|
383
529
|
# 1) assume file_name_or_id is a filename
|
@@ -385,43 +531,27 @@ end
|
|
385
531
|
if files && (files.count == 1)
|
386
532
|
file_info = files.first
|
387
533
|
elsif files && (files.count > 1)
|
388
|
-
|
534
|
+
log_error_and_raise("Multiple files with name '#{file_name_or_id}' exist. download_file() can only handle a single filename.")
|
389
535
|
return false
|
390
|
-
else # either files is false or count is 0. assume file_name_or_id is an id.
|
536
|
+
else # either files is false or count is 0. assume file_name_or_id is an id.
|
391
537
|
file_info = get_file_info(file_name_or_id)
|
392
538
|
if !file_info
|
393
|
-
|
539
|
+
log_error_and_raise("No file with ID '#{file_name_or_id}' exist.")
|
394
540
|
return false
|
395
541
|
end
|
396
542
|
end
|
397
543
|
|
398
|
-
|
399
|
-
|
544
|
+
output_file = "#{file_path + file_info[:name]}"
|
400
545
|
# Delete local file if it exists
|
401
|
-
`rm #{
|
546
|
+
`rm #{output_file} > /dev/null 2>&1`
|
402
547
|
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
# --write-out \"%{http_code}\" \"$@\" returns http code in stdout
|
409
|
-
cmd = "curl --silent --write-out \"%{http_code}\" \"$@\" -H \"Authorization: Bearer #{@access_token}\" \"#{url}\" -o \"#{output_file}\""
|
410
|
-
response = `#{cmd} 2>/dev/null` # this is the http code as string.
|
411
|
-
exit_status = $?.exitstatus
|
412
|
-
|
413
|
-
if exit_status != 0
|
414
|
-
if @raise_error; raise "non-0 exit status #{exit_status}." end
|
415
|
-
return false
|
416
|
-
elsif response.to_i != 200
|
417
|
-
if @raise_error; raise "Non HTTP 200 code for download: '#{response}'" end
|
418
|
-
return false
|
548
|
+
@drive_service.get_file(file_info[:id], acknowledge_abuse:true, download_dest: output_file ) do |resfile, err|
|
549
|
+
if err
|
550
|
+
log_error_and_raise("Error: #{err}.")
|
551
|
+
return false
|
552
|
+
end
|
419
553
|
end
|
420
|
-
|
421
|
-
# file temp file to file
|
422
|
-
`cp #{output_file} #{file_path + file_name}`
|
423
|
-
|
424
|
-
return file_path + file_name
|
554
|
+
return output_file
|
425
555
|
end
|
426
556
|
end # end of class GoogleDrive
|
427
557
|
|
data/lib/utilities.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
module Utilities
|
3
|
+
# Reads files with K-V pairs in the style of
|
4
|
+
# KEY=value
|
5
|
+
# KEY2="really long value"
|
6
|
+
# and strips surrounding spaces, then surrounding double-quotes, then returns K-V pairs as a hash
|
7
|
+
def read_conf_file(filepath)
|
8
|
+
text = File.read(filepath)
|
9
|
+
data = text.scan(/(\S+)\s*=\s*(.+)/)
|
10
|
+
hash = Hash[data]
|
11
|
+
return hash.map { |k, v| [k, v.strip.delete_prefix('"').delete_suffix('"')] }.to_h
|
12
|
+
# TODO: Support comments (starting with #). Possibly non-leading # (such as after value, but outside quotes.)
|
13
|
+
# TODO: Handle quotes more elegantly. State machine?
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: googledrive-easy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Bullock
|
8
|
-
|
8
|
+
- Michael Ketchel
|
9
|
+
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2022-
|
12
|
+
date: 2022-10-19 00:00:00.000000000 Z
|
12
13
|
dependencies: []
|
13
14
|
description: Easy File interface to Google Drive
|
14
15
|
email: jmb@rgnets.com
|
@@ -18,13 +19,15 @@ extensions: []
|
|
18
19
|
extra_rdoc_files: []
|
19
20
|
files:
|
20
21
|
- bin/googledrive
|
22
|
+
- lib/googledrive-easy-test.rb
|
21
23
|
- lib/googledrive-easy.rb
|
22
24
|
- lib/googledrive/wildcard.rb
|
25
|
+
- lib/utilities.rb
|
23
26
|
homepage: https://rubygems.org/gems/googledrive-easy
|
24
27
|
licenses:
|
25
28
|
- MIT
|
26
29
|
metadata: {}
|
27
|
-
post_install_message:
|
30
|
+
post_install_message:
|
28
31
|
rdoc_options: []
|
29
32
|
require_paths:
|
30
33
|
- lib
|
@@ -39,9 +42,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
39
42
|
- !ruby/object:Gem::Version
|
40
43
|
version: '0'
|
41
44
|
requirements: []
|
42
|
-
|
43
|
-
|
44
|
-
signing_key:
|
45
|
+
rubygems_version: 3.1.6
|
46
|
+
signing_key:
|
45
47
|
specification_version: 4
|
46
48
|
summary: Easy File interface to Google Drive
|
47
49
|
test_files: []
|