googledrive-easy 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2e510a7a4388feec9cbfffcbf6095e2b90f73cd882b81558014b9bc9147d4ed
4
- data.tar.gz: 5940d24cf857bd29da87277dfcf0135a47890b3d37b087eddc5cbeb19084c464
3
+ metadata.gz: 1111117a9dac04b800a1429cd0ce2a7bae5cdeb04b2ea29c9d7333c40a1306a7
4
+ data.tar.gz: 518fc4f98597d3a51dcbfbe10c613c979674bde30af6d6cae4c8b2a1094f57bb
5
5
  SHA512:
6
- metadata.gz: 2f934c27d0bc9014b1f8f514e036bcfe2e22355a3e4125a11aaa287cfbec935fdef49eae7372ef18e11f76765dbb22e7b95ac466e2f432647459b7d0e8441bae
7
- data.tar.gz: b5bf2997933c7e475e8f5d254a49ed029c52ec910b9653042f59661dccd6516bb4419962a35582b466b646da1aad4c4cea2c43e637e696d2048152b0641cf577
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
- require 'googledrive-easy'
11
+ options = {
12
+ :persist=>false,
13
+ :file=>nil
14
+ }
15
+ OptionParser.new do |opts|
16
+ opts.banner = "Usage: googledrive [options]"
4
17
 
5
- if ARGV[0] == "gen_url"
6
- if ARGV.length != 2
7
- puts "gen_url requires to pass client_id as a parameter"
8
- exit -1
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
- client_id = ARGV[1]
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
- puts "Access Code must be generated by going to the following URL:"
14
- puts
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
- client_id = ARGV[1]
25
- client_secret = ARGV[2]
26
- access_code = ARGV[3]
51
+ # Handle special service mode cases
52
+ case options[:authmode]
27
53
 
28
- refresh_token = GoogleDrive.generate_refresh_token(client_id, client_secret, access_code)
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
- if refresh_token == false
31
- puts "\nError creating refresh token.\n"
32
- exit -1
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
@@ -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
- def initialize(raise_error = false)
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
- @raise_error = raise_error
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
- def self.generate_access_code_url(client_id)
22
- scope = "https://www.googleapis.com/auth/drive"
23
-
24
- return "https://accounts.google.com/o/oauth2/auth?scope=#{scope}&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&client_id=#{client_id}"
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 self.generate_refresh_token(client_id, client_secret, access_code)
28
- options = {
29
- body: {
30
- client_id: client_id,
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
- if (response.code == 400) || (response.code == 401)
44
- puts "Error: #{response.parsed_response["error"]} : #{response.parsed_response["error_description"]}"
45
- return false
46
- elsif (response.code != 200)
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
- # see if error field is present
52
- if response.parsed_response["refresh_token"].nil?
53
- puts "refresh_token field not found"
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
- puts "Refresh Token: " + response.parsed_response["refresh_token"]
58
-
59
- return response.parsed_response["refresh_token"]
60
-
61
- # Possible return values
62
- # {"error"=>"invalid_client", "error_description"=>"The OAuth client was not found."}
63
- # {"error"=>"invalid_grant", "error_description"=>"Malformed auth code."}
64
- # {"access_token"=>"XXXXXXX", "expires_in"=>3599, "refresh_token"=>"YYYYYY", "scope"=>"https://www.googleapis.com/auth/drive", "token_type"=>"Bearer"}
65
- end
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
- # refreh token may be nil if you dont have it yet.
70
- def set_api_keys(client_id, client_secret, refresh_token)
71
- @client_id = client_id
72
- @client_secret = client_secret
73
- @refresh_token = refresh_token
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
- if !client_id || !client_secret || !refresh_token
76
- if @raise_error; raise "Not all tokens provided." end
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
- return generate_access_token()
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
- def set_api_keys_from_file(json_api_key_file)
84
- if !File.file?(json_api_key_file) # file doesnt exist.
85
- if @raise_error; raise "JSON API Key '#{json_api_key_file}' does not exist." end
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
- begin
90
- api_hash = JSON.parse(File.read(json_api_key_file))
91
- rescue => error
92
- if @raise_error; raise "Unable to parse JSON: '" + error.message + "' in file '#{json_api_key_file}'." end
93
- return false
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
- # expected values in JSON
97
- @client_id = api_hash["CLIENT_ID"]
98
- @client_secret = api_hash["CLIENT_SECRET"]
99
- @refresh_token = api_hash["REFRESH_TOKEN"]
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
- if !@client_id || !@client_secret || !@refresh_token
102
- if @raise_error; raise "Not all tokens provided." end
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
- return generate_access_token()
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
- def generate_access_token
111
- # Refresh auth token from google_oauth2 and then requeue the job.
112
- # https://stackoverflow.com/questions/12792326/how-do-i-refresh-my-google-oauth2-access-token-using-my-refresh-token
113
- options = {
114
- body: {
115
- client_id: @client_id,
116
- client_secret: @client_secret,
117
- refresh_token: @refresh_token,
118
- grant_type: 'refresh_token'
119
- },
120
- headers: {
121
- 'Content-Type' => 'application/x-www-form-urlencoded'
122
- }
123
- }
124
- response = HTTParty.post('https://accounts.google.com/o/oauth2/token', options)
125
- if response.code != 200
126
- if @raise_error; raise "Non-200 HTTP error code: #{response.code}." end
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
- @access_token = response.parsed_response['access_token']
131
- return true
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
- pageSize = 100
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
- nextPageToken = nil
405
+ next_page_token = nil
145
406
  loop do
146
- headers = {
147
- "GData-Version" => "3.0",
148
- "Authorization" => "Bearer #{@access_token}"
149
- }
150
-
151
- url = "https://www.googleapis.com/drive/v3/files"
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
- # clean google drive response and only return relavent information.
208
- response.parsed_response["files"].each do |gd_file_entry|
209
- # each entry has the following fields: name, kind, mimeType, id
210
- file_hash = { }
211
- file_hash[:name] = gd_file_entry["name"]
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
- files.each do |file|
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 files
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
- def get_file_info(fileid)
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
- if !response.parsed_response.has_key?("kind")
264
- return false
265
- elsif response.parsed_response["kind"] != "drive#file"
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
- if @raise_error; raise "Directory not found." end
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
- if @raise_error; raise "ERROR: File '#{file_basename}' already exists." end
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
- begin
333
- response = JSON.load(response)
334
- rescue => e
335
- if @raise_error; raise "CURL response is not in valid JSON." end
336
- return false
337
- end
338
-
339
- # example of response.
340
- # {
341
- # "kind": "drive#file",
342
- # "id": "1Zw9YD3TXci3Ja_wU0g7f30DHpsEbk2zf",
343
- # "name": "ruby-3.1.0.tar.gz",
344
- # "mimeType": "application/gzip"
345
- # }
346
-
347
- # check that name = filename
348
- # check that kind = drive#file
349
- if !response.key?("name") # name key does not exist
350
- if @raise_error; raise "no name key specified in response." end
351
- return false
352
- elsif !response.key?("kind") # kind key does not exist
353
- if @raise_error; raise "no kind key specified in response." end
354
- return false
355
- elsif response["kind"] != "drive#file" # Not of file type
356
- if @raise_error; raise "kind is of non-file type." end
357
- return false
358
- elsif response["name"] != file_basename # file name mismatch
359
- if @raise_error; raise "file name mismatch." end
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
- # https://stackoverflow.com/questions/60608901/how-to-download-a-big-file-from-google-drive-via-curl-in-bash
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
- if @raise_error; raise "File path '#{file_path}' does not exist." end
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
- if @raise_error; raise "Multiple files with name '#{file_name_or_id}' exist. dowload_file() can only handle a single filename." end
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
- if @raise_error; raise "No file with ID '#{file_name_or_id}' exist." end
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
- file_name = file_info[:name]
399
-
544
+ output_file = "#{file_path + file_info[:name]}"
400
545
  # Delete local file if it exists
401
- `rm #{file_path + file_name} > /dev/null 2>&1`
546
+ `rm #{output_file} > /dev/null 2>&1`
402
547
 
403
- # temp file is automatically unlinked at end of function.
404
- output_file = Tempfile.new('driveapi_').path
405
-
406
- url = "https://www.googleapis.com/drive/v3/files/#{file_info[:id]}?alt=media"
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.6
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bullock
8
- autorequire:
8
+ - Michael Ketchel
9
+ autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2022-02-16 00:00:00.000000000 Z
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
- rubyforge_project:
43
- rubygems_version: 2.7.6.2
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: []