google_drive2 3.0.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,733 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require 'cgi'
5
+ require 'stringio'
6
+
7
+ require 'rubygems'
8
+ require 'nokogiri'
9
+ require 'googleauth'
10
+
11
+ require 'google_drive/util'
12
+ require 'google_drive/api_client_fetcher'
13
+ require 'google_drive/error'
14
+ require 'google_drive/authentication_error'
15
+ require 'google_drive/response_code_error'
16
+ require 'google_drive/spreadsheet'
17
+ require 'google_drive/worksheet'
18
+ require 'google_drive/collection'
19
+ require 'google_drive/file'
20
+ require 'google_drive/config'
21
+ require 'google_drive/access_token_credentials'
22
+
23
+ module GoogleDrive
24
+ # A session for Google Drive operations.
25
+ #
26
+ # Use from_credentials, from_access_token, from_service_account_key or
27
+ # from_config class method to construct a GoogleDrive::Session object.
28
+ class Session
29
+ include(Util)
30
+ extend(Util)
31
+
32
+ DEFAULT_SCOPE = [
33
+ 'https://www.googleapis.com/auth/drive',
34
+ 'https://spreadsheets.google.com/feeds/'
35
+ ].freeze
36
+
37
+ # Equivalent of either from_credentials or from_access_token.
38
+ def self.login_with_oauth(credentials_or_access_token, proxy = nil)
39
+ Session.new(credentials_or_access_token, proxy)
40
+ end
41
+
42
+ # Creates a dummy GoogleDrive::Session object for testing.
43
+ def self.new_dummy
44
+ Session.new(nil)
45
+ end
46
+
47
+ # Constructs a GoogleDrive::Session object from OAuth2 credentials such as
48
+ # Google::Auth::UserRefreshCredentials.
49
+ #
50
+ # See
51
+ # https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md
52
+ # for a usage example.
53
+ #
54
+ # See from_config documentation for an explanation of client_options and
55
+ # request_options.
56
+ def self.from_credentials(credentials, client_options = nil, request_options = nil)
57
+ Session.new(credentials, nil, client_options, request_options)
58
+ end
59
+
60
+ # Constructs a GoogleDrive::Session object from OAuth2 access token string.
61
+ #
62
+ # See from_config documentation for an explanation of client_options and
63
+ # request_options.
64
+ def self.from_access_token(access_token, client_options = nil, request_options = nil)
65
+ Session.new(access_token, nil, client_options, request_options)
66
+ end
67
+
68
+ # Constructs a GoogleDrive::Session object from a service account key JSON.
69
+ #
70
+ # You can pass either the path to a JSON file, or an IO-like object with the
71
+ # JSON.
72
+ #
73
+ # See
74
+ # https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md
75
+ # for a usage example.
76
+ #
77
+ # As with from_config, you can configure Google API client behavior with
78
+ # +client_options+ and +request_options+. Unlike in from_config, these
79
+ # are passed as positional arguments.
80
+ def self.from_service_account_key(
81
+ json_key_path_or_io, scope = DEFAULT_SCOPE, client_options = nil,
82
+ request_options = nil
83
+ )
84
+ if json_key_path_or_io.is_a?(String)
85
+ open(json_key_path_or_io) do |f|
86
+ from_service_account_key(f, scope, client_options, request_options)
87
+ end
88
+ else
89
+ credentials = Google::Auth::ServiceAccountCredentials.make_creds(
90
+ json_key_io: json_key_path_or_io, scope: scope
91
+ )
92
+ Session.new(credentials, nil, client_options, request_options)
93
+ end
94
+ end
95
+
96
+ # Returns GoogleDrive::Session constructed from a config JSON file at
97
+ # +config+, potenially modified with +options+.
98
+ #
99
+ # +config+ is the path to the config file.
100
+ #
101
+ # This will prompt the credential via command line for the first time and
102
+ # save it to +config+ for later usages.
103
+ #
104
+ # See
105
+ # https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md
106
+ # for a usage example.
107
+ #
108
+ # You can also provide a config object that must respond to:
109
+ # client_id
110
+ # client_secret
111
+ # refesh_token
112
+ # refresh_token=
113
+ # scope
114
+ # scope=
115
+ # save
116
+ #
117
+ # +options+ is a hash which may contain the following keys:
118
+ # client_secret: if present, this will overwrite config.client_secret
119
+ # client_id: if present, this will overwrite config.client_id
120
+ # client_options: a hash or ::Google::Apis::ClientOptions. See
121
+ # https://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/ClientOptions
122
+ # for an explanation of legal sub-fields.
123
+ # request_options: a hash or ::Google::Apis::RequestOptions. See
124
+ # https://www.rubydoc.info/github/google/google-api-ruby-client/Google/Apis/RequestOptions
125
+ # for an explanation of legal sub-fields.
126
+ def self.from_config(config, options = {})
127
+ if config.is_a?(String)
128
+ config_path = config
129
+ config = Config.new(config_path)
130
+ if config.type == 'service_account'
131
+ return from_service_account_key(
132
+ config_path, options[:scope] || DEFAULT_SCOPE, options[:client_options],
133
+ options[:request_options]
134
+ )
135
+ end
136
+ end
137
+
138
+ config.scope ||= DEFAULT_SCOPE
139
+
140
+ if options[:client_id] && options[:client_secret]
141
+ config.client_id = options[:client_id]
142
+ config.client_secret = options[:client_secret]
143
+ end
144
+ if !config.client_id || !config.client_secret
145
+ raise(
146
+ ArgumentError,
147
+ 'client_id or client_secret is missing in the config. Follow ' \
148
+ 'https://github.com/gimite/google-drive-ruby/blob/master/doc/authorization.md ' \
149
+ 'to provide a valid config. google_drive library no longer provides ' \
150
+ 'the default credential due to a limitation of Google API.'
151
+ )
152
+ end
153
+
154
+ credentials = Google::Auth::UserRefreshCredentials.new(
155
+ client_id: config.client_id,
156
+ client_secret: config.client_secret,
157
+ scope: config.scope,
158
+ redirect_uri: 'urn:ietf:wg:oauth:2.0:oob'
159
+ )
160
+
161
+ if config.refresh_token
162
+ credentials.refresh_token = config.refresh_token
163
+ credentials.fetch_access_token!
164
+ else
165
+ $stderr.print(
166
+ format("\n1. Open this page:\n%s\n\n", credentials.authorization_uri)
167
+ )
168
+ $stderr.print('2. Enter the authorization code shown in the page: ')
169
+ credentials.code = $stdin.gets.chomp
170
+ credentials.fetch_access_token!
171
+ config.refresh_token = credentials.refresh_token
172
+ end
173
+
174
+ config.save
175
+
176
+ Session.new(
177
+ credentials, nil, options[:client_options], options[:request_options]
178
+ )
179
+ end
180
+
181
+ def initialize(
182
+ credentials_or_access_token, proxy = nil, client_options = nil,
183
+ request_options = nil
184
+ )
185
+ if proxy
186
+ raise(
187
+ ArgumentError,
188
+ 'Specifying a proxy object is no longer supported. ' \
189
+ 'Set ENV["http_proxy"] instead.'
190
+ )
191
+ end
192
+
193
+ if credentials_or_access_token
194
+ if credentials_or_access_token.is_a?(String)
195
+ credentials = AccessTokenCredentials.new(credentials_or_access_token)
196
+ # Equivalent of credentials_or_access_token.is_a?(OAuth2::AccessToken),
197
+ # without adding dependency to "oauth2" library.
198
+ elsif credentials_or_access_token
199
+ .class
200
+ .ancestors
201
+ .any? { |m| m.name == 'OAuth2::AccessToken' }
202
+ credentials =
203
+ AccessTokenCredentials.new(credentials_or_access_token.token)
204
+ else
205
+ credentials = credentials_or_access_token
206
+ end
207
+ @fetcher = ApiClientFetcher.new(
208
+ credentials, client_options, request_options
209
+ )
210
+ else
211
+ @fetcher = nil
212
+ end
213
+ end
214
+
215
+ # Proc or Method called when authentication has failed.
216
+ # When this function returns +true+, it tries again.
217
+ attr_accessor :on_auth_fail
218
+
219
+ # Returns an instance of Google::Apis::DriveV3::DriveService.
220
+ def drive_service
221
+ @fetcher.drive
222
+ end
223
+
224
+ alias drive drive_service
225
+
226
+ # Returns an instance of Google::Apis::SheetsV4::SheetsService.
227
+ def sheets_service
228
+ @fetcher.sheets
229
+ end
230
+
231
+ # Returns list of files for the user as array of GoogleDrive::File or its
232
+ # subclass. You can specify parameters documented at
233
+ # https://developers.google.com/drive/v3/web/search-parameters
234
+ #
235
+ # e.g.
236
+ # session.files
237
+ # session.files(q: "name = 'hoge'")
238
+ # # Same as above with a placeholder
239
+ # session.files(q: ["name = ?", "hoge"])
240
+ #
241
+ # By default, it returns the first 100 files. You can get all files by
242
+ # calling with a block:
243
+ # session.files do |file|
244
+ # p file
245
+ # end
246
+ # Or passing "pageToken" parameter:
247
+ # page_token = nil
248
+ # begin
249
+ # (files, page_token) = session.files(page_token: page_token)
250
+ # p files
251
+ # end while page_token
252
+ def files(params = {}, &block)
253
+ params = convert_params(params)
254
+ execute_paged!(
255
+ method: drive_service.method(:list_files),
256
+ parameters: { fields: '*', supports_all_drives: true, include_items_from_all_drives: true }.merge(params),
257
+ items_method_name: :files,
258
+ converter: proc { |af| wrap_api_file(af) },
259
+ &block
260
+ )
261
+ end
262
+
263
+ # Returns a file (including a spreadsheet and a folder) whose title exactly
264
+ # matches +title+.
265
+ #
266
+ # Returns an instance of GoogleDrive::File or its subclass
267
+ # (GoogleDrive::Spreadsheet, GoogleDrive::Collection). Returns nil if not
268
+ # found. If multiple files with the +title+ are found, returns one of them.
269
+ #
270
+ # If given an Array, traverses folders by title. e.g.:
271
+ # session.file_by_title(
272
+ # ["myfolder", "mysubfolder/even/w/slash", "myfile"])
273
+ def file_by_title(title)
274
+ if title.is_a?(Array)
275
+ root_collection.file_by_title(title)
276
+ else
277
+ files(q: ['name = ?', title], page_size: 1)[0]
278
+ end
279
+ end
280
+
281
+ alias file_by_name file_by_title
282
+
283
+ # Returns a file (including a spreadsheet and a folder) with a given +id+.
284
+ #
285
+ # Returns an instance of GoogleDrive::File or its subclass
286
+ # (GoogleDrive::Spreadsheet, GoogleDrive::Collection).
287
+ def file_by_id(id)
288
+ api_file = drive_service.get_file(id, fields: '*', supports_all_drives: true)
289
+ wrap_api_file(api_file)
290
+ end
291
+
292
+ # Returns a file (including a spreadsheet and a folder) with a given +url+.
293
+ # +url+ must be the URL of the page you open to access a
294
+ # document/spreadsheet in your browser.
295
+ #
296
+ # Returns an instance of GoogleDrive::File or its subclass
297
+ # (GoogleDrive::Spreadsheet, GoogleDrive::Collection).
298
+ def file_by_url(url)
299
+ file_by_id(url_to_id(url))
300
+ end
301
+
302
+ # Returns list of spreadsheets for the user as array of
303
+ # GoogleDrive::Spreadsheet.
304
+ # You can specify parameters documented at
305
+ # https://developers.google.com/drive/v3/web/search-parameters
306
+ #
307
+ # e.g.
308
+ # session.spreadsheets
309
+ # session.spreadsheets(q: "name = 'hoge'")
310
+ # # Same as above with a placeholder
311
+ # session.spreadsheets(q: ["name = ?", "hoge"])
312
+ #
313
+ # By default, it returns the first 100 spreadsheets. See document of files
314
+ # method for how to get all spreadsheets.
315
+ def spreadsheets(params = {}, &block)
316
+ params = convert_params(params)
317
+ query = construct_and_query(
318
+ [
319
+ "mimeType = 'application/vnd.google-apps.spreadsheet'",
320
+ params[:q]
321
+ ]
322
+ )
323
+ files(params.merge(q: query), &block)
324
+ end
325
+
326
+ # Returns GoogleDrive::Spreadsheet with given +key+.
327
+ #
328
+ # e.g.
329
+ # # https://docs.google.com/spreadsheets/d/1L3-kvwJblyW_TvjYD-7pE-AXxw5_bkb6S_MljuIPVL0/edit
330
+ # session.spreadsheet_by_key(
331
+ # "1L3-kvwJblyW_TvjYD-7pE-AXxw5_bkb6S_MljuIPVL0")
332
+ def spreadsheet_by_key(key)
333
+ file = file_by_id(key)
334
+ unless file.is_a?(Spreadsheet)
335
+ raise(
336
+ GoogleDrive::Error,
337
+ format('The file with the ID is not a spreadsheet: %s', key)
338
+ )
339
+ end
340
+ file
341
+ end
342
+
343
+ # Returns GoogleDrive::Spreadsheet with given +url+. You must specify either
344
+ # of:
345
+ # - URL of the page you open to access the spreadsheet in your browser
346
+ # - URL of worksheet-based feed of the spreadseet
347
+ #
348
+ # e.g.
349
+ # session.spreadsheet_by_url(
350
+ # "https://docs.google.com/spreadsheets/d/" \
351
+ # "1L3-kvwJblyW_TvjYD-7pE-AXxw5_bkb6S_MljuIPVL0/edit")
352
+ def spreadsheet_by_url(url)
353
+ file = file_by_url(url)
354
+ unless file.is_a?(Spreadsheet)
355
+ raise(
356
+ GoogleDrive::Error,
357
+ format('The file with the URL is not a spreadsheet: %s', url)
358
+ )
359
+ end
360
+ file
361
+ end
362
+
363
+ # Returns GoogleDrive::Spreadsheet with given +title+.
364
+ # Returns nil if not found. If multiple spreadsheets with the +title+ are
365
+ # found, returns one of them.
366
+ def spreadsheet_by_title(title)
367
+ spreadsheets(q: ['name = ?', title], page_size: 1)[0]
368
+ end
369
+
370
+ alias spreadsheet_by_name spreadsheet_by_title
371
+
372
+ # Returns GoogleDrive::Worksheet with given +url+.
373
+ # You must specify URL of either worksheet feed or cell-based feed of the
374
+ # worksheet.
375
+ #
376
+ # e.g.:
377
+ # # Worksheet feed URL
378
+ # session.worksheet_by_url(
379
+ # "https://spreadsheets.google.com/feeds/worksheets/" \
380
+ # "1smypkyAz4STrKO4Zkos5Z4UPUJKvvgIza32LnlQ7OGw/private/full/od7")
381
+ # # Cell-based feed URL
382
+ # session.worksheet_by_url(
383
+ # "https://spreadsheets.google.com/feeds/cells/" \
384
+ # "1smypkyAz4STrKO4Zkos5Z4UPUJKvvgIza32LnlQ7OGw/od7/private/full")
385
+ def worksheet_by_url(url)
386
+ case url
387
+ when %r{^https?://spreadsheets.google.com/feeds/worksheets/(.*)/.*/full/(.*)$}
388
+ spreadsheet_id = Regexp.last_match(1)
389
+ worksheet_feed_id = Regexp.last_match(2)
390
+ when %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full(\?.*)?$}
391
+ spreadsheet_id = Regexp.last_match(1)
392
+ worksheet_feed_id = Regexp.last_match(2)
393
+ else
394
+ raise(
395
+ GoogleDrive::Error,
396
+ 'URL is neither a worksheet feed URL nor a cell-based feed URL: ' \
397
+ "#{url}"
398
+ )
399
+ end
400
+
401
+ spreadsheet = spreadsheet_by_key(spreadsheet_id)
402
+ worksheet = spreadsheet.worksheets.find{ |ws| ws.worksheet_feed_id == worksheet_feed_id }
403
+ unless worksheet
404
+ raise(
405
+ GoogleDrive::Error,
406
+ "Worksheet not found for the given URL: #{url}"
407
+ )
408
+ end
409
+ worksheet
410
+ end
411
+
412
+ # Returns the root folder.
413
+ def root_collection
414
+ @root_collection ||= file_by_id('root')
415
+ end
416
+
417
+ alias root_folder root_collection
418
+
419
+ # Returns the top-level folders (direct children of the root folder).
420
+ #
421
+ # By default, it returns the first 100 folders. See document of files method
422
+ # for how to get all folders.
423
+ def collections(params = {}, &block)
424
+ root_collection.subcollections(params, &block)
425
+ end
426
+
427
+ alias folders collections
428
+
429
+ # Returns a top-level folder whose title exactly matches +title+ as
430
+ # GoogleDrive::Collection.
431
+ # Returns nil if not found. If multiple folders with the +title+ are found,
432
+ # returns one of them.
433
+ def collection_by_title(title)
434
+ root_collection.subcollection_by_title(title)
435
+ end
436
+
437
+ alias folders_by_name collection_by_title
438
+
439
+ # Returns GoogleDrive::Collection with given +url+.
440
+ #
441
+ # You must specify the URL of the page you get when you go to
442
+ # https://drive.google.com/ with your browser and open a folder.
443
+ #
444
+ # e.g.
445
+ # session.collection_by_url(
446
+ # "https://drive.google.com/drive/folders/" \
447
+ # "1u99gpfHIk08RVK5q_vXxUqkxR1r6FUJH")
448
+ def collection_by_url(url)
449
+ file = file_by_url(url)
450
+ unless file.is_a?(Collection)
451
+ raise(
452
+ GoogleDrive::Error,
453
+ format('The file with the URL is not a folder: %s', url)
454
+ )
455
+ end
456
+ file
457
+ end
458
+
459
+ alias folder_by_url collection_by_url
460
+
461
+ # Returns GoogleDrive::Collection with given +id+.
462
+ #
463
+ # e.g.
464
+ # # https://drive.google.com/drive/folders/1rPPuzAew4tO3ORc88Vz1JscPCcnrX7-J
465
+ # session.collection_by_id("1rPPuzAew4tO3ORc88Vz1JscPCcnrX7-J")
466
+ def collection_by_id(id)
467
+ file = file_by_id(id)
468
+ unless file.is_a?(Collection)
469
+ raise(
470
+ GoogleDrive::Error,
471
+ format('The file with the ID is not a folder: %s', id)
472
+ )
473
+ end
474
+ file
475
+ end
476
+
477
+ alias folder_by_id collection_by_id
478
+
479
+ # Creates a top-level folder with given title. Returns GoogleDrive::Collection
480
+ # object.
481
+ def create_collection(title, file_properties = {})
482
+ create_file(title, file_properties.merge(mime_type: 'application/vnd.google-apps.folder'))
483
+ end
484
+
485
+ alias create_folder create_collection
486
+
487
+ # Creates a spreadsheet with given title. Returns GoogleDrive::Spreadsheet
488
+ # object.
489
+ #
490
+ # e.g.
491
+ # session.create_spreadsheet("My new sheet")
492
+ def create_spreadsheet(title = 'Untitled', file_properties = {})
493
+ create_file(title, file_properties.merge(mime_type: 'application/vnd.google-apps.spreadsheet'))
494
+ end
495
+
496
+ # Creates a file with given title and properties. Returns objects
497
+ # with the following types: GoogleDrive::Spreadsheet, GoogleDrive::File,
498
+ # GoogleDrive::Collection
499
+ #
500
+ # You can pass a MIME Type using the file_properties-function parameter,
501
+ # for example: create_file('Document Title', mime_type: 'application/vnd.google-apps.document')
502
+ #
503
+ # A list of available Drive MIME Types can be found here:
504
+ # https://developers.google.com/drive/v3/web/mime-types
505
+ def create_file(title, file_properties = {})
506
+ file_metadata = {
507
+ name: title,
508
+ }.merge(file_properties)
509
+
510
+ file = drive_service.create_file(
511
+ file_metadata, fields: '*', supports_all_drives: true
512
+ )
513
+
514
+ wrap_api_file(file)
515
+ end
516
+
517
+ # Uploads a file with the given +title+ and +content+.
518
+ # Returns a GoogleSpreadsheet::File object.
519
+ #
520
+ # e.g.
521
+ # # Uploads and converts to a Google Docs document:
522
+ # session.upload_from_string(
523
+ # "Hello world.", "Hello", content_type: "text/plain")
524
+ #
525
+ # # Uploads without conversion:
526
+ # session.upload_from_string(
527
+ # "Hello world.", "Hello", content_type: "text/plain", convert: false)
528
+ #
529
+ # # Uploads and converts to a Google Spreadsheet:
530
+ # session.upload_from_string(
531
+ # "hoge\tfoo\n", "Hoge", content_type: "text/tab-separated-values")
532
+ # session.upload_from_string(
533
+ # "hoge,foo\n", "Hoge", content_type: "text/tsv")
534
+ def upload_from_string(content, title = 'Untitled', params = {})
535
+ upload_from_source(StringIO.new(content), title, params)
536
+ end
537
+
538
+ # Uploads a local file.
539
+ # Returns a GoogleSpreadsheet::File object.
540
+ #
541
+ # e.g.
542
+ # # Uploads a text file and converts to a Google Docs document:
543
+ # session.upload_from_file("/path/to/hoge.txt")
544
+ #
545
+ # # Uploads without conversion:
546
+ # session.upload_from_file("/path/to/hoge.txt", "Hoge", convert: false)
547
+ #
548
+ # # Uploads with explicit content type:
549
+ # session.upload_from_file(
550
+ # "/path/to/hoge", "Hoge", content_type: "text/plain")
551
+ #
552
+ # # Uploads a text file and converts to a Google Spreadsheet:
553
+ # session.upload_from_file("/path/to/hoge.csv", "Hoge")
554
+ # session.upload_from_file(
555
+ # "/path/to/hoge", "Hoge", content_type: "text/csv")
556
+ def upload_from_file(path, title = nil, params = {})
557
+ # TODO: Add a feature to upload to a folder.
558
+ file_name = ::File.basename(path)
559
+ default_content_type =
560
+ EXT_TO_CONTENT_TYPE[::File.extname(file_name).downcase] ||
561
+ 'application/octet-stream'
562
+ upload_from_source(
563
+ path,
564
+ title || file_name,
565
+ { content_type: default_content_type }.merge(params)
566
+ )
567
+ end
568
+
569
+ # Uploads a file. Reads content from +io+.
570
+ # Returns a GoogleDrive::File object.
571
+ def upload_from_io(io, title = 'Untitled', params = {})
572
+ upload_from_source(io, title, params)
573
+ end
574
+
575
+ # @api private
576
+ def wrap_api_file(api_file)
577
+ case api_file.mime_type
578
+ when 'application/vnd.google-apps.folder'
579
+ Collection.new(self, api_file)
580
+ when 'application/vnd.google-apps.spreadsheet'
581
+ Spreadsheet.new(self, api_file)
582
+ else
583
+ File.new(self, api_file)
584
+ end
585
+ end
586
+
587
+ # @api private
588
+ def execute_paged!(opts, &block)
589
+ if block
590
+ page_token = nil
591
+ loop do
592
+ parameters =
593
+ (opts[:parameters] || {}).merge(page_token: page_token)
594
+ (items, page_token) =
595
+ execute_paged!(opts.merge(parameters: parameters))
596
+ items.each(&block)
597
+ break unless page_token
598
+ end
599
+
600
+ elsif opts[:parameters] && opts[:parameters].key?(:page_token)
601
+ response = opts[:method].call(**opts[:parameters])
602
+ items = response.__send__(opts[:items_method_name]).map do |item|
603
+ opts[:converter] ? opts[:converter].call(item) : item
604
+ end
605
+ [items, response.next_page_token]
606
+
607
+ else
608
+ parameters = (opts[:parameters] || {}).merge(page_token: nil)
609
+ (items,) = execute_paged!(opts.merge(parameters: parameters))
610
+ items
611
+ end
612
+ end
613
+
614
+ # @api private
615
+ def request(method, url, params = {})
616
+ # Always uses HTTPS.
617
+ url = url.gsub(%r{^http://}, 'https://')
618
+ data = params[:data]
619
+ auth = params[:auth] || :wise
620
+ response_type = params[:response_type] || :xml
621
+
622
+ extra_header = if params[:header]
623
+ params[:header]
624
+ elsif data
625
+ {
626
+ 'Content-Type' => 'application/atom+xml;charset=utf-8'
627
+ }
628
+ else
629
+ {}
630
+ end
631
+ extra_header = { 'GData-Version' => '3.0' }.merge(extra_header)
632
+
633
+ loop do
634
+ response = @fetcher.request_raw(method, url, data, extra_header, auth)
635
+ next if response.code == '401' && @on_auth_fail && @on_auth_fail.call
636
+ unless response.code =~ /^2/
637
+ raise(
638
+ (response.code == '401' ? AuthenticationError : ResponseCodeError)
639
+ .new(response.code, response.body, method, url)
640
+ )
641
+ end
642
+ return convert_response(response, response_type)
643
+ end
644
+ end
645
+
646
+ def inspect
647
+ format('#<%p:0x%x>', self.class, object_id)
648
+ end
649
+
650
+ private
651
+
652
+ def upload_from_source(source, title, params = {})
653
+ api_params = {
654
+ upload_source: source,
655
+ content_type: 'application/octet-stream',
656
+ fields: '*',
657
+ supports_all_drives: true
658
+ }
659
+ for k, v in params
660
+ unless %i[convert convert_mime_type parents].include?(k)
661
+ api_params[k] = v
662
+ end
663
+ end
664
+
665
+ file_metadata = { name: title }
666
+ content_type = api_params[:content_type]
667
+ if params[:convert_mime_type]
668
+ file_metadata[:mime_type] = params[:convert_mime_type]
669
+ elsif params.fetch(:convert, true) &&
670
+ IMPORTABLE_CONTENT_TYPE_MAP.key?(content_type)
671
+ file_metadata[:mime_type] = IMPORTABLE_CONTENT_TYPE_MAP[content_type]
672
+ end
673
+ file_metadata[:parents] = params[:parents] if params[:parents]
674
+
675
+ file = drive_service.create_file(file_metadata, **api_params)
676
+ wrap_api_file(file)
677
+ end
678
+
679
+ def convert_response(response, response_type)
680
+ case response_type
681
+ when :xml
682
+ Nokogiri.XML(response.body)
683
+ when :raw
684
+ response.body
685
+ when :response
686
+ response
687
+ else
688
+ raise(GoogleDrive::Error,
689
+ format('Unknown params[:response_type]: %s', response_type))
690
+ end
691
+ end
692
+
693
+ def url_to_id(url)
694
+ uri = URI.parse(url)
695
+ if ['spreadsheets.google.com', 'docs.google.com', 'drive.google.com']
696
+ .include?(uri.host)
697
+ case uri.path
698
+ # Document feed.
699
+ when /^\/feeds\/\w+\/private\/full\/\w+%3A(.*)$/
700
+ return Regexp.last_match(1)
701
+ # Worksheets feed of a spreadsheet.
702
+ when /^\/feeds\/worksheets\/([^\/]+)/
703
+ return Regexp.last_match(1)
704
+ # Human-readable new spreadsheet/document.
705
+ when /\/d\/([^\/]+)/
706
+ return Regexp.last_match(1)
707
+ # Human-readable new folder page.
708
+ when /^\/drive\/[^\/]+\/([^\/]+)/
709
+ return Regexp.last_match(1)
710
+ # Human-readable old folder view.
711
+ when /\/folderview$/
712
+ if (uri.query || '').split(/&/).find { |s| s =~ /^id=(.*)$/ }
713
+ return Regexp.last_match(1)
714
+ end
715
+ # Human-readable old spreadsheet.
716
+ when /\/ccc$/
717
+ if (uri.query || '').split(/&/).find { |s| s =~ /^key=(.*)$/ }
718
+ return Regexp.last_match(1)
719
+ end
720
+ end
721
+ case uri.fragment
722
+ # Human-readable old folder page.
723
+ when /^folders\/(.+)$/
724
+ return Regexp.last_match(1)
725
+ end
726
+ end
727
+ raise(
728
+ GoogleDrive::Error,
729
+ format('The given URL is not a known Google Drive URL: %s', url)
730
+ )
731
+ end
732
+ end
733
+ end