googledrive-easy 0.0.6 → 0.1.1

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: a99c6d447adf5fbcead4da54fbfeeb6a101a1fec4992138f69053e2c27b261bf
4
+ data.tar.gz: 81944644cfae15757314247e1c8fd315f6ab1fcca1c276fd0f3d511a69b3f15f
5
5
  SHA512:
6
- metadata.gz: 2f934c27d0bc9014b1f8f514e036bcfe2e22355a3e4125a11aaa287cfbec935fdef49eae7372ef18e11f76765dbb22e7b95ac466e2f432647459b7d0e8441bae
7
- data.tar.gz: b5bf2997933c7e475e8f5d254a49ed029c52ec910b9653042f59661dccd6516bb4419962a35582b466b646da1aad4c4cea2c43e637e696d2048152b0641cf577
6
+ metadata.gz: 9d4e56c5d413d7da029f950898908cd74ae0a2fa6cd7a313229d48564af91c73035894aef8cdf06a7381223600a78d4de3f9c73012cc8145c895e835f0d4def8
7
+ data.tar.gz: 2a749aaffdca0407520186606d98ee10261fc470a2ab2c0c3f9f9c7e93859b54dd77b66ab22d98781b1a6b0b932329b5bc650b95e4e2a7bf646e6baf3463d28c
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,438 @@
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'
4
8
 
5
- require_relative 'googledrive/wildcard'
6
- #require 'googledrive/wildcard'
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"
18
+
19
+ # Override default retry count for long code.
20
+ Google::Apis::RequestOptions.default.retries = 10
7
21
 
8
22
  class GoogleDrive
23
+ # TODO: Test the upload, download, list methods. Write tests for them.
24
+ # TODO: Test using the gem in another program.
25
+
26
+ def initialize(raise_error: false, loglevel: Logger::ERROR )
27
+ # Config
28
+ @raise_error = raise_error
29
+ @file_fields = "id,kind,mime_type,name,md5Checksum,size,parents"
30
+ @md5_command = 'md5 -q' # Needs to output only the sum, no filename or header info.
31
+
32
+ # Init and configure logger.
33
+ # TODO: Make this smarter/respect global loggers.
34
+ @logger = Logger.new(STDOUT)
35
+ @logger.level = loglevel
36
+ @logger.formatter = proc do |severity, datetime, progname, msg|
37
+ date_format = datetime.strftime("%Y-%m-%d %H:%M:%S")
38
+ "[%s] %-5s (%s): %s\n" % [date_format, severity, self.class.name, msg ]
39
+ end
9
40
 
10
- def initialize(raise_error = false)
41
+ # Critical File Paths
42
+ @api_key_file = DRIVE_ENV_FILE
43
+ @loaded_api_key_file = @api_key_file
44
+ @drive_secret_path = "#{Dir.home}/.googledrive-secret.json" # Should contain Oauth2 data exactly as provided by GCloud Console
45
+ @drive_token_path = "#{Dir.home}/.googledrive-token.yaml" # Created by script to hold the token store
46
+
47
+ # Oauth Config
48
+ @drive_scope = 'https://www.googleapis.com/auth/drive'
49
+ @auth_url_path = '/'
50
+ @oauth_address = 'localhost' # Should be `localhost`, `127.0.0.1`, or `[::1]`
51
+ @oauth_port = 8181 # Pick anything outside the privileged range [1-1023] to avoid running SSH as root.
52
+ @oauth_loopback_base_url = "http://#{@oauth_address}:#{@oauth_port}"
53
+
54
+ # Client vars
55
+ @token_file_user = 'cliuser' # The user to store credentials as in the token file
11
56
  @client_id = nil
12
57
  @client_secret = nil
13
58
  @refresh_token = nil
14
-
15
59
  @access_token = nil
60
+ @expiration = 1665718429000
16
61
 
17
- @raise_error = raise_error
62
+ # Core runtime vars
63
+ @drive_service = nil
64
+ @authorizer = nil
65
+ @credentials = nil
18
66
  end
19
67
 
68
+ def print_config_hints
69
+ puts <<~TEXT.chomp
70
+ To run in headless modes (environment or file configuration), you must provide either:
71
+ * A json file at #{@api_key_file} containing the following structure:
72
+ {
73
+ "CLIENT_ID": "your Google Cloud Oauth client ID",
74
+ "CLIENT_SECRET": "your Google Cloud Oauth client secret",
75
+ "REFRESH_TOKEN": "A valid refresh token from a prior Oauth completion with the ID and SECRET",
76
+ }
20
77
 
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}"
78
+ * The following environment variables:
79
+ #{ENV_KEYS[:id]}='your Google Cloud Oauth client ID'
80
+ #{ENV_KEYS[:secret]}='your Google Cloud Oauth client secret'
81
+ #{ENV_KEYS[:refresh]}='A valid refresh token from a prior Oauth completion with the ID and SECRET'
82
+
83
+ For the interactive auth modes (loopback or input) you can omit the refresh token from either of the above.
84
+ If you later switch to a headless mode, you will need to add the refresh token.
85
+ TEXT
25
86
  end
26
87
 
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)
88
+ def log_error_and_raise(msg)
89
+ @logger.error(msg)
90
+ raise "msg" if @raise_error
91
+ end
42
92
 
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}."
93
+ def set_api_keys(client_id:nil, client_secret:nil, refresh_token:nil, access_token:nil, require_refresh_token:true)
94
+ # Recommended service mode if manually called: manual
95
+ unless client_id && client_secret && (refresh_token || !require_refresh_token)
96
+ log_error_and_raise("Not all tokens provided.")
48
97
  return false
49
98
  end
99
+ @client_id = client_id if client_id
100
+ @client_secret = client_secret if client_secret
101
+ @refresh_token = refresh_token if refresh_token
102
+ @access_token = access_token if access_token
103
+ return true
104
+ end
105
+
106
+ def load_api_keys_from_file(path:nil, require_refresh_token:true)
107
+ @loaded_api_key_file = path ? path : @api_key_file
108
+ if(File.exist?(@loaded_api_key_file))
109
+ @logger.info("API key file #{@loaded_api_key_file} exists")
110
+ begin
111
+ api_hash = JSON.parse(File.read(@loaded_api_key_file))
112
+ rescue => error
113
+ log_error_and_raise("Error opening api key file: " + error.inspect)
114
+ return false
115
+ end
50
116
 
51
- # see if error field is present
52
- if response.parsed_response["refresh_token"].nil?
53
- puts "refresh_token field not found"
117
+ @logger.debug("api_hash: " + api_hash.inspect)
118
+
119
+ @client_id = api_hash["CLIENT_ID"]
120
+ @client_secret = api_hash["CLIENT_SECRET"]
121
+ @refresh_token = api_hash["REFRESH_TOKEN"]
122
+
123
+ if set_api_keys(
124
+ client_id:api_hash["CLIENT_ID"],
125
+ client_secret:api_hash["CLIENT_SECRET"],
126
+ refresh_token:api_hash["REFRESH_TOKEN"],
127
+ require_refresh_token:require_refresh_token
128
+ )
129
+ @logger.info("Using Google Drive API information from #{@loaded_api_key_file}")
130
+ else
131
+ log_error_and_raise("Not all API keys were in file #{@loaded_api_key_file}.")
132
+ return false
133
+ end
134
+ else
135
+ log_error_and_raise("Cannot find #{@loaded_api_key_file}")
54
136
  return false
55
137
  end
138
+ true
139
+ end
56
140
 
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
141
+ def load_api_keys_from_env(require_refresh_token:true)
142
+ # Google Drive Credentials from ENV if not derived from JSON file in home.
143
+ vars = [ENV_KEYS[:id], ENV_KEYS[:secret]]
144
+ vars << ENV_KEYS[:refresh] if require_refresh_token
145
+ vars.each do |v|
146
+ if ENV[v].nil?
147
+ log_error_and_raise("#{v} export variable not set.")
148
+ return false
149
+ end
150
+ end
66
151
 
152
+ # Set
153
+ return set_api_keys(
154
+ client_id:ENV[ENV_KEYS[:id]],
155
+ client_secret:ENV[ENV_KEYS[:secret]],
156
+ refresh_token:ENV[ENV_KEYS[:refresh]],
157
+ require_refresh_token:require_refresh_token
158
+ )
159
+ end
67
160
 
161
+ def generate_api_secret_hash
162
+ {
163
+ "installed" => {
164
+ "client_id" => @client_id,
165
+ "client_secret" => @client_secret,
166
+ # "project_id"=>"super-secret-project",
167
+ # "auth_uri"=>"https://accounts.google.com/o/oauth2/auth",
168
+ # "token_uri"=>"https://oauth2.googleapis.com/token",
169
+ # "auth_provider_x509_cert_url"=>"https://www.googleapis.com/oauth2/v1/certs",
170
+ # "redirect_uris"=>["http://localhost"]
171
+ }
172
+ }
173
+ end
68
174
 
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
175
+ def dump_keys
176
+ {
177
+ "client_id" => @client_id,
178
+ "client_secret" => @client_id,
179
+ "access_token" => @access_token,
180
+ "refresh_token" => @refresh_token,
181
+ }
182
+ end
74
183
 
75
- if !client_id || !client_secret || !refresh_token
76
- if @raise_error; raise "Not all tokens provided." end
184
+ # Generates token yaml
185
+ def generate_token_yaml
186
+ unless @client_id && @refresh_token
187
+ api_config = {"client_id" => @client_id, "refresh_token" => @refresh_token }
188
+ @logger.debug("API Config: #{api_config}")
189
+ log_error_and_raise("Some required API config for token hasn't been configured yet")
77
190
  return false
78
191
  end
79
192
 
80
- return generate_access_token()
193
+ drive_token_hash = {
194
+ "client_id" => @client_id,
195
+ "access_token" => @access_token,
196
+ "refresh_token" => @refresh_token,
197
+ "scope" => [@drive_scope],
198
+ "expiration_time_millis" => @expiration
199
+ }
200
+ return {@token_file_user => drive_token_hash.to_json}.to_hash.to_yaml
81
201
  end
82
202
 
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
203
+ # Generates temp token file and return path to it
204
+ def generate_temp_token_file
205
+ tmpfile = Tempfile.new('drive_token')
206
+ token_yaml = generate_token_yaml
207
+ unless token_yaml
86
208
  return false
87
209
  end
210
+ tmpfile.write(token_yaml)
211
+ tmpfile.flush # flush file contents before continuing.
212
+ return @drive_token_path = File.expand_path(tmpfile.path)
213
+ end
88
214
 
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
215
+ def generate_env_file(path=@api_key_file)
216
+ File.open(path,"w") do |f|
217
+ return f.write({
218
+ "CLIENT_ID" => @client_id,
219
+ "CLIENT_SECRET" => @client_secret,
220
+ "REFRESH_TOKEN" => @refresh_token,
221
+ }.to_json)
222
+ end
223
+ end
224
+
225
+ # Sets up a webserver to receive a 'localhost' query for 3-legged OAUTH. Requires that @oauth_port is forwarded
226
+ # from the machine running the authenticating browser to the machine running this script,
227
+ # likely via `ssh user@host -L @oauth_port:@oauth_address:@oauth_port`
228
+ def get_oauth_credentials_via_loopback(authorizer)
229
+ # TODO: This a raw copy, refactor
230
+ # Print instructions and URL for user to click
231
+ # TODO: Should this be puts or @logger? it's a user interaction...
232
+ puts "Listening on #{@oauth_address}:#{@oauth_port}."
233
+ puts "If this a remote system, you need to forward this port via SSH Tunneling, if you haven't already."
234
+ puts("eg: ssh user@host -L#{@oauth_port}:#{@oauth_address}:#{@oauth_port}")
235
+ puts "After you have done so, follow this link:", authorizer.get_authorization_url(base_url: @oauth_loopback_base_url)
236
+
237
+ # Start webserver to listen for response:
238
+ socket = TCPServer.new(@oauth_address, @oauth_port)
239
+ loop do
240
+ client = socket.accept
241
+ first_line = client.gets
242
+ verb, path, _ = first_line.split
243
+
244
+ if verb == 'GET'
245
+ if result = path.match(/^\/\?code=(.*)&scope=(.*)/)
246
+ code = result[1]
247
+ scope = result[2]
248
+ response = "HTTP/1.1 200\r\n\r\nAuthorized for scope `#{scope}`! Code is #{code}"
249
+ client.puts(response)
250
+ client.close
251
+ socket.close
252
+
253
+ puts "Authorized for scope `#{scope}`! Code is #{code}"
254
+ # Extract response
255
+ puts 'Oauth flow complete. You can close the local port forward if desired.'
256
+ return authorizer.get_and_store_credentials_from_code(
257
+ user_id: @token_file_user,
258
+ code: code,
259
+ base_url: @oauth_loopback_base_url
260
+ )
261
+ else
262
+ # Default response for testing/sanity checks
263
+ response = "HTTP/1.1 200\r\n\r\nI respond to auth requests."
264
+ client.puts(response)
265
+ end
266
+ end
267
+ client.close
94
268
  end
269
+ socket.close
270
+ end
95
271
 
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"]
272
+ def get_oauth_credentials_via_input(authorizer)
273
+ # TODO: This a raw copy, refactor
274
+ puts 'Follow this url and complete the sign-in process. The login will result in an error, do not close it.'
275
+ puts 'Instead, copy and paste the value of the `code` parameter (begins with 4/)'
276
+ puts authorizer.get_authorization_url(base_url: "http://localhost:1")
277
+ puts ''
278
+ code = HighLine::ask "Please enter code:"
279
+ puts "Got code: #{code}"
280
+
281
+ return authorizer.get_and_store_credentials_from_code(
282
+ user_id: @token_file_user,
283
+ code: code,
284
+ base_url: "http://localhost:1"
285
+ )
286
+ end
100
287
 
101
- if !@client_id || !@client_secret || !@refresh_token
102
- if @raise_error; raise "Not all tokens provided." end
288
+ def create_service(mode:'environment')
289
+ @logger.debug("Passed authorization mode: #{mode}")
290
+
291
+ unless %w(environment loopback input manual).include?(mode)
292
+ log_error_and_raise("Unknown authorization mode")
103
293
  return false
104
294
  end
105
295
 
106
- return generate_access_token()
107
- end
296
+ # TODO: Figure out balance between always requiring env config and just reading from the token store if it already exists
108
297
 
298
+ interactive_mode = %w(loopback input).include?(mode)
299
+ # Attempt to load keys from environment, unless manual. Doing ahead for the benefit of both cases.
109
300
 
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
301
+ if mode == "manual"
302
+ key_load_result = false
303
+ else
304
+ key_load_result = (load_api_keys_from_env(require_refresh_token:!interactive_mode) || load_api_keys_from_file(require_refresh_token:!interactive_mode))
305
+ end
306
+
307
+ if interactive_mode && !key_load_result
308
+ print_config_hints
309
+ end
310
+
311
+ # If environment, we need key load to have succeeded completely
312
+ # If manual, it should pull from memory.
313
+ if %w(environment manual).include?(mode)
314
+ tmp_token_path = generate_temp_token_file
315
+ unless tmp_token_path
316
+ log_error_and_raise("Failed to generate temporary token file")
317
+ return false
318
+ end
319
+ @logger.debug(File.read(tmp_token_path))
320
+ unless key_load_result || mode=='manual'
321
+ log_error_and_raise("Unable to load api keys from environment")
322
+ return false
323
+ end
324
+ token_store = Google::Auth::Stores::FileTokenStore.new(file: tmp_token_path)
325
+ else
326
+ # Otherwise, just the ID and secret are enough
327
+ unless @client_secret && @client_id
328
+ log_error_and_raise("Client Secret or ID missing.")
329
+ return false
330
+ end
331
+ token_store = Google::Auth::Stores::FileTokenStore.new(file: @drive_token_path)
332
+ end
333
+
334
+ @authorizer = Google::Auth::UserAuthorizer.new(
335
+ Google::Auth::ClientId::from_hash(generate_api_secret_hash),
336
+ @drive_scope,
337
+ token_store,
338
+ @auth_url_path
339
+ )
340
+ @logger.debug("google_authorizer: " + @authorizer.inspect)
341
+
342
+ # Attempt to retrieve credentials
343
+ @credentials = @authorizer.get_credentials(@token_file_user)
344
+ if @credentials.nil?
345
+ case mode
346
+ when 'input'
347
+ @credentials = get_oauth_credentials_via_input(@authorizer)
348
+ when 'loopback'
349
+ @credentials = get_oauth_credentials_via_loopback(@authorizer)
350
+ end
351
+ end
352
+
353
+ # Final cred check
354
+ if @credentials.nil?
355
+ @logger.error('Unable to retrieve credentials')
127
356
  return false
128
357
  end
129
358
 
130
- @access_token = response.parsed_response['access_token']
131
- return true
359
+ # Update internal credentials based on credentials loaded
360
+ @client_id = @credentials.client_id
361
+ @client_secret = @credentials.client_secret
362
+ @refresh_token = @credentials.refresh_token
363
+
364
+ @drive_service = Google::Apis::DriveV3::DriveService.new
365
+ @drive_service.authorization = @credentials
366
+ return @drive_service
367
+ end
368
+
369
+ def process_file(file)
370
+ file_hash = { }
371
+ file_hash[:name] = file.name
372
+ file_hash[:id] = file.id
373
+ file_hash[:isfolder] = file.mime_type == 'application/vnd.google-apps.folder'
374
+ file_hash[:size] = file.size if file.size
375
+ file_hash[:md5] = file.md5_checksum if file.md5_checksum
376
+ file_hash[:parents] = file.parents if file.parents
377
+ return file_hash
132
378
  end
133
379
 
134
380
  # parentfolderid: "root" gets the root directory. Not all folders are under the root. Has to do with permissions
135
- # and how Google Drive works.
381
+ # and how Google Drive works.
382
+ # https://developers.google.com/drive/api/v3/reference/query-ref
136
383
  def get_all_files(justfiles: false, justfolders: false, parentfolderid: nil, name: nil)
384
+ unless @drive_service
385
+ log_error_and_raise("Drive service not initialized.")
386
+ return false
387
+ end
137
388
 
138
389
  # Number of files/directories to be returned each call to /files.
139
390
  # multiple page sizes are handled with the pageToken return value.
140
- # 100 is default from google.
141
- pageSize = 100
142
-
391
+ # 100 is default from google.
392
+ page_size = 100
393
+
394
+ # Fields param gives us extra juicy info like MD5 sums and file sizes
395
+ fields = "kind,incomplete_search,next_page_token,files(#{@file_fields})"
396
+
397
+ # Build query:
398
+ query = "(trashed = false)"
399
+ query +=" and (mimeType != 'application/vnd.google-apps.folder')" if justfiles && !justfolders
400
+ query += " and (mimeType = 'application/vnd.google-apps.folder')" if justfolders && !justfiles
401
+ # parent folder has to be surrounded by single quotes in query
402
+ query += " and ('#{parentfolderid}' in parents)" if parentfolderid
403
+ # filename has to be surrounded by single quotes in query
404
+ query += " and (name #{ name =~ /\*/ ? "contains" : "=" } '#{name}')" if name
405
+
143
406
  files = [ ]
144
- nextPageToken = nil
407
+ next_page_token = nil
145
408
  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
409
+ # TODO: Should this be converted to block form and then use that to gracefully handle failure and errors?
410
+ files_page = @drive_service.list_files(page_size: page_size, q: query, page_token: next_page_token, fields: fields)
411
+ files_page.files.each {|f| files << f}
412
+ next_page_token = files_page.next_page_token
413
+ break unless next_page_token
414
+ end
206
415
 
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?
416
+ # Process the returned files
417
+ # Todo: Do we really need to convert these now that the API returns real objects?
418
+ processed_files = []
419
+ files.each do |file|
420
+ processed_files << process_file(file)
222
421
  end
223
422
 
423
+ # Todo: Is this still really necessary? I think the list_files function can do this server-side. Need to research.
224
424
  # we have additional processing to do it a wildcard character was passed. Because Google Drive "contains" returns all portions of it.
225
425
  # so we need to filter here
226
426
  if name =~ /\*/ # if name contains wildcard
227
427
  ret_files = [ ]
228
- files.each do |file|
428
+ processed_files.each do |file|
229
429
  if GoogleDriveWildcard.new(name) =~ file[:name]
230
430
  ret_files << file
231
431
  end
232
432
  end
233
433
  return ret_files
234
434
  else
235
- return files
435
+ return processed_files
236
436
  end
237
437
  end
238
438
 
@@ -241,143 +441,91 @@ class GoogleDrive
241
441
  return get_all_files(justfiles: true, parentfolderid: parentfolderid, name: name)
242
442
  end
243
443
 
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
444
+ def get_file_info(file_id)
445
+ unless @drive_service
446
+ log_error_and_raise("Drive service not initialized.")
260
447
  return false
261
448
  end
262
449
 
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
450
+ # TODO: Maybe convert this to block format and handle errors like in other places
451
+ file = @drive_service.get_file(file_id:file_id, acknowledge_abuse:true, fields: "files(#{@file_fields})")
452
+ return process_file(file)
281
453
  end
282
454
 
283
455
  def find_directory_id(directory_name, parentfolderid: nil)
284
-
285
456
  file_list = get_all_files(justfolders: true, name: directory_name, parentfolderid: parentfolderid)
286
457
 
287
458
  if !file_list || (file_list.count == 0)
288
- if @raise_error; raise "Directory not found." end
459
+ log_error_and_raise("Directory not found.")
289
460
  return false
290
461
  end
291
-
292
- return file_list.first[:id]
293
- end
294
-
462
+
463
+ return file_list.first[:id]
464
+ end
465
+
295
466
  def upload_file(file, directory_id: nil)
467
+ unless @drive_service
468
+ log_error_and_raise("Drive service not initialized.")
469
+ return false
470
+ end
296
471
 
297
472
  file_basename = File.basename(file)
298
-
473
+ # 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
474
  # see if file exists on Drive
300
475
  file_list = self.get_all_files(justfiles: true, parentfolderid: directory_id, name: file_basename)
301
476
  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
477
+ log_error_and_raise("ERROR: File '#{file_basename}' already exists.")
329
478
  return false
330
479
  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
480
+
481
+ file_obj = Google::Apis::DriveV3::File.new(name: file_basename)
482
+ file_obj.parents = [directory_id] if directory_id
483
+ @drive_service.create_file(
484
+ file_obj,
485
+ upload_source: file,
486
+ fields: @file_fields
487
+ ) do |resfile, err|
488
+ if err
489
+ log_error_and_raise("Error: #{err}.")
490
+ return false
491
+ end
492
+
493
+ # check that name = filename
494
+ # check that kind = drive#file
495
+ if !resfile.name # name key does not exist
496
+ log_error_and_raise("no name key specified in response.")
497
+ return false
498
+ elsif !resfile.kind # kind key does not exist
499
+ log_error_and_raise("no kind key specified in response.")
500
+ return false
501
+ elsif resfile.kind != "drive#file" # Not of file type
502
+ log_error_and_raise("kind is of non-file type.")
503
+ return false
504
+ elsif resfile.name != file_basename # file name mismatch
505
+ log_error_and_raise("file name mismatch.")
506
+ return false
507
+ end
508
+ # TODO: Add MD5 check, since we're now capable.
361
509
  end
362
-
363
510
  return true
364
511
  end
365
-
512
+
366
513
  # returns full path of downloaded file
367
514
  def download_file(file_name_or_id, parentfolderid: nil, file_path: nil)
515
+ unless @drive_service
516
+ log_error_and_raise("Drive service not initialized.")
517
+ return false
518
+ end
368
519
 
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.
520
+ # if file path passed, check it is valid.
373
521
  if file_path && !Dir.exist?(file_path)
374
- if @raise_error; raise "File path '#{file_path}' does not exist." end
522
+ log_error_and_raise("File path '#{file_path}' does not exist.")
375
523
  return false
376
524
  elsif !file_path # no path passed, use current directory
377
525
  file_path = Dir.getwd
378
- end
379
-
380
- # path passed and valid. Append forward slash if not already there.
526
+ end
527
+
528
+ # path passed and valid. Append forward slash if not already there.
381
529
  file_path = file_path.gsub(/\/$/, '') + "/"
382
530
 
383
531
  # 1) assume file_name_or_id is a filename
@@ -385,43 +533,27 @@ end
385
533
  if files && (files.count == 1)
386
534
  file_info = files.first
387
535
  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
536
+ log_error_and_raise("Multiple files with name '#{file_name_or_id}' exist. download_file() can only handle a single filename.")
389
537
  return false
390
- else # either files is false or count is 0. assume file_name_or_id is an id.
538
+ else # either files is false or count is 0. assume file_name_or_id is an id.
391
539
  file_info = get_file_info(file_name_or_id)
392
540
  if !file_info
393
- if @raise_error; raise "No file with ID '#{file_name_or_id}' exist." end
541
+ log_error_and_raise("No file with ID '#{file_name_or_id}' exist.")
394
542
  return false
395
543
  end
396
544
  end
397
545
 
398
- file_name = file_info[:name]
399
-
546
+ output_file = "#{file_path + file_info[:name]}"
400
547
  # Delete local file if it exists
401
- `rm #{file_path + file_name} > /dev/null 2>&1`
548
+ `rm #{output_file} > /dev/null 2>&1`
402
549
 
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
550
+ @drive_service.get_file(file_info[:id], acknowledge_abuse:true, download_dest: output_file ) do |resfile, err|
551
+ if err
552
+ log_error_and_raise("Error: #{err}.")
553
+ return false
554
+ end
419
555
  end
420
-
421
- # file temp file to file
422
- `cp #{output_file} #{file_path + file_name}`
423
-
424
- return file_path + file_name
556
+ return output_file
425
557
  end
426
558
  end # end of class GoogleDrive
427
559
 
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.1
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-20 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: []