googledrive-easy 0.0.6 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []