google_drive2 3.0.8

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.
@@ -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