google_drive 2.1.12 → 3.0.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a6e8bc240087cb507a378a4ff02886cad3e2b7ed
4
- data.tar.gz: 07a8f238c8cc89072248fd6db3b2ca0b7ab18d37
3
+ metadata.gz: 37e8e8318c40d62af473e05a2fbed3a4fc6c87c8
4
+ data.tar.gz: 8dce5cbb7f5bdcee6bf868099f45bf3cb508a3ce
5
5
  SHA512:
6
- metadata.gz: 55ff686961a787c15738c8177f6aada5b4df2851019116e120310e2523684fed2d389b9658ad7f7d832ce125bcaf608467e7f7244a76109c0c649491d14ac773
7
- data.tar.gz: b89df0376ae01ef915ae88193059a809eccbfa410f2a84a0e30c35415f08789f4021567d8da52a85e8f7cddd53319b084ddbc161deeb6ba19f0a0dd072e3cfec
6
+ metadata.gz: bccc7a33f9b60ec60efaaf7fe6cdacc7f5e0db6e4d5c05d083e2c1c31621871bcdac0fdf81acd1cb0c4b7ba40690145bdb63a619478c32c4638ca1c5431bd944
7
+ data.tar.gz: f827099020aef331134319588747b39e8acf7a58d4525a54038aed01b2e1f6ae2b16179a9fd757cd60d038bcffa795c667a69921548c423879431bb9f3d38fdf
data/README.md CHANGED
@@ -5,7 +5,7 @@ This is a Ruby library to read/write files/spreadsheets in Google Drive/Docs.
5
5
  NOTE: This is NOT a library to create Google Drive App.
6
6
 
7
7
 
8
- * [Migration from ver. 0.x.x / 1.x.x](#migration)
8
+ * [Migration from ver. 2.x.x or before](#migration)
9
9
  * [How to install](#install)
10
10
  * [How to use](#use)
11
11
  * [API documentation](http://www.rubydoc.info/gems/google_drive)
@@ -16,7 +16,7 @@ NOTE: This is NOT a library to create Google Drive App.
16
16
  * [Author](#author)
17
17
 
18
18
 
19
- ## <a name="migration">Migration from ver. 0.x.x / 1.x.x</a>
19
+ ## <a name="migration">Migration from ver. 2.x.x or before</a>
20
20
 
21
21
  There are some incompatible API changes. See
22
22
  [MIGRATING.md](https://github.com/gimite/google-drive-ruby/blob/master/MIGRATING.md).
@@ -21,7 +21,7 @@ module GoogleDrive
21
21
  def initialize(session, file)
22
22
  @session = session
23
23
  @file = file
24
- api_permissions = @session.drive.list_permissions(
24
+ api_permissions = @session.drive_service.list_permissions(
25
25
  @file.id, fields: '*', supports_team_drives: true
26
26
  )
27
27
  @entries =
@@ -70,7 +70,7 @@ module GoogleDrive
70
70
  def push(params_or_entry, options = {})
71
71
  entry = params_or_entry.is_a?(AclEntry) ?
72
72
  params_or_entry : AclEntry.new(params_or_entry)
73
- api_permission = @session.drive.create_permission(
73
+ api_permission = @session.drive_service.create_permission(
74
74
  @file.id,
75
75
  entry.params,
76
76
  { fields: '*', supports_team_drives: true }.merge(options)
@@ -85,7 +85,7 @@ module GoogleDrive
85
85
  # e.g.
86
86
  # spreadsheet.acl.delete(spreadsheet.acl[1])
87
87
  def delete(entry)
88
- @session.drive.delete_permission(
88
+ @session.drive_service.delete_permission(
89
89
  @file.id, entry.id, supports_team_drives: true
90
90
  )
91
91
  @entries.delete(entry)
@@ -93,7 +93,7 @@ module GoogleDrive
93
93
 
94
94
  # @api private
95
95
  def update_role(entry)
96
- api_permission = @session.drive.update_permission(
96
+ api_permission = @session.drive_service.update_permission(
97
97
  @file.id,
98
98
  entry.id,
99
99
  { role: entry.role },
@@ -4,6 +4,7 @@
4
4
  require 'net/https'
5
5
  require 'uri'
6
6
  require 'google/apis/drive_v3'
7
+ require 'google/apis/sheets_v4'
7
8
  Net::HTTP.version_1_2
8
9
 
9
10
  module GoogleDrive
@@ -19,30 +20,35 @@ module GoogleDrive
19
20
 
20
21
  def initialize(authorization, client_options, request_options)
21
22
  @drive = Google::Apis::DriveV3::DriveService.new
22
- @drive.authorization = authorization
23
-
24
- # Make the timeout virtually infinite because some of the operations
25
- # (e.g., uploading a large file) can take very long.
26
- # This value is the maximal allowed timeout in seconds on JRuby.
27
- t = (2**31 - 1) / 1000
28
- @drive.client_options.open_timeout_sec = t
29
- @drive.client_options.read_timeout_sec = t
30
- @drive.client_options.send_timeout_sec = t
31
-
32
- if client_options
33
- @drive.client_options.members.each do |name|
34
- if !client_options[name].nil?
35
- @drive.client_options[name] = client_options[name]
23
+ @sheets = Google::Apis::SheetsV4::SheetsService.new
24
+
25
+ [@drive, @sheets].each do |service|
26
+ service.authorization = authorization
27
+
28
+ # Make the timeout virtually infinite because some of the operations
29
+ # (e.g., uploading a large file) can take very long.
30
+ # This value is the maximal allowed timeout in seconds on JRuby.
31
+ t = (2**31 - 1) / 1000
32
+ service.client_options.open_timeout_sec = t
33
+ service.client_options.read_timeout_sec = t
34
+ service.client_options.send_timeout_sec = t
35
+
36
+ if client_options
37
+ service.client_options.members.each do |name|
38
+ if !client_options[name].nil?
39
+ service.client_options[name] = client_options[name]
40
+ end
36
41
  end
37
42
  end
38
- end
39
43
 
40
- if request_options
41
- @drive.request_options = @drive.request_options.merge(request_options)
44
+ if request_options
45
+ service.request_options = service.request_options.merge(request_options)
46
+ end
42
47
  end
43
48
  end
44
49
 
45
50
  attr_reader(:drive)
51
+ attr_reader(:sheets)
46
52
 
47
53
  def request_raw(method, url, data, extra_header, _auth)
48
54
  options = @drive.request_options.merge(header: extra_header)
@@ -19,7 +19,7 @@ module GoogleDrive
19
19
 
20
20
  # Adds the given GoogleDrive::File to the folder.
21
21
  def add(file)
22
- @session.drive.update_file(
22
+ @session.drive_service.update_file(
23
23
  file.id, add_parents: id, fields: '', supports_team_drives: true
24
24
  )
25
25
  nil
@@ -27,7 +27,7 @@ module GoogleDrive
27
27
 
28
28
  # Removes the given GoogleDrive::File from the folder.
29
29
  def remove(file)
30
- @session.drive.update_file(
30
+ @session.drive_service.update_file(
31
31
  file.id, remove_parents: id, fields: '', supports_team_drives: true
32
32
  )
33
33
  end
@@ -61,7 +61,7 @@ module GoogleDrive
61
61
  parents: [id]
62
62
  }.merge(file_properties)
63
63
 
64
- file = @session.drive.create_file(
64
+ file = @session.drive_service.create_file(
65
65
  file_metadata, fields: '*', supports_team_drives: true
66
66
  )
67
67
 
@@ -38,7 +38,7 @@ module GoogleDrive
38
38
 
39
39
  # Reloads file metadata such as title and acl.
40
40
  def reload_metadata
41
- @api_file = @session.drive.get_file(
41
+ @api_file = @session.drive_service.get_file(
42
42
  id, fields: '*', supports_team_drives: true
43
43
  )
44
44
  @acl = Acl.new(@session, self) if @acl
@@ -94,7 +94,7 @@ module GoogleDrive
94
94
  #
95
95
  # To export the file in other formats, use export_as_file.
96
96
  def download_to_file(path, params = {})
97
- @session.drive.get_file(
97
+ @session.drive_service.get_file(
98
98
  id,
99
99
  { download_dest: path, supports_team_drives: true }.merge(params)
100
100
  )
@@ -113,7 +113,7 @@ module GoogleDrive
113
113
  #
114
114
  # To export the file in other formats, use export_to_io.
115
115
  def download_to_io(io, params = {})
116
- @session.drive.get_file(
116
+ @session.drive_service.get_file(
117
117
  id,
118
118
  { download_dest: io, supports_team_drives: true }.merge(params)
119
119
  )
@@ -185,7 +185,7 @@ module GoogleDrive
185
185
  # Reads content from +io+ and updates the file with the content.
186
186
  def update_from_io(io, params = {})
187
187
  params = { upload_source: io, supports_team_drives: true }.merge(params)
188
- @session.drive.update_file(id, nil, params)
188
+ @session.drive_service.update_file(id, nil, params)
189
189
  nil
190
190
  end
191
191
 
@@ -193,9 +193,9 @@ module GoogleDrive
193
193
  # If +permanent+ is +true+, deletes the file permanently.
194
194
  def delete(permanent = false)
195
195
  if permanent
196
- @session.drive.delete_file(id, supports_team_drives: true)
196
+ @session.drive_service.delete_file(id, supports_team_drives: true)
197
197
  else
198
- @session.drive.update_file(
198
+ @session.drive_service.update_file(
199
199
  id, { trashed: true }, supports_team_drives: true
200
200
  )
201
201
  end
@@ -204,7 +204,7 @@ module GoogleDrive
204
204
 
205
205
  # Renames title of the file.
206
206
  def rename(title)
207
- @session.drive.update_file(
207
+ @session.drive_service.update_file(
208
208
  id, { name: title }, supports_team_drives: true
209
209
  )
210
210
  nil
@@ -214,7 +214,7 @@ module GoogleDrive
214
214
 
215
215
  # Creates copy of this file with the given title.
216
216
  def copy(title, file_properties = {})
217
- api_file = @session.drive.copy_file(
217
+ api_file = @session.drive_service.copy_file(
218
218
  id, { name: title }.merge(file_properties), fields: '*', supports_team_drives: true
219
219
  )
220
220
  @session.wrap_api_file(api_file)
@@ -259,7 +259,7 @@ module GoogleDrive
259
259
 
260
260
  def export_to_dest(dest, format)
261
261
  mime_type = EXT_TO_CONTENT_TYPE['.' + format] || format
262
- @session.drive.export_file(id, mime_type, download_dest: dest)
262
+ @session.drive_service.export_file(id, mime_type, download_dest: dest)
263
263
  nil
264
264
  end
265
265
  end
@@ -219,10 +219,17 @@ module GoogleDrive
219
219
  attr_accessor :on_auth_fail
220
220
 
221
221
  # Returns an instance of Google::Apis::DriveV3::DriveService.
222
- def drive
222
+ def drive_service
223
223
  @fetcher.drive
224
224
  end
225
225
 
226
+ alias drive drive_service
227
+
228
+ # Returns an instance of Google::Apis::SheetsV4::SheetsService.
229
+ def sheets_service
230
+ @fetcher.sheets
231
+ end
232
+
226
233
  # Returns list of files for the user as array of GoogleDrive::File or its
227
234
  # subclass. You can specify parameters documented at
228
235
  # https://developers.google.com/drive/v3/web/search-parameters
@@ -247,7 +254,7 @@ module GoogleDrive
247
254
  def files(params = {}, &block)
248
255
  params = convert_params(params)
249
256
  execute_paged!(
250
- method: drive.method(:list_files),
257
+ method: drive_service.method(:list_files),
251
258
  parameters: { fields: '*', supports_team_drives: true }.merge(params),
252
259
  items_method_name: :files,
253
260
  converter: proc { |af| wrap_api_file(af) },
@@ -280,7 +287,7 @@ module GoogleDrive
280
287
  # Returns an instance of GoogleDrive::File or its subclass
281
288
  # (GoogleDrive::Spreadsheet, GoogleDrive::Collection).
282
289
  def file_by_id(id)
283
- api_file = drive.get_file(id, fields: '*', supports_team_drives: true)
290
+ api_file = drive_service.get_file(id, fields: '*', supports_team_drives: true)
284
291
  wrap_api_file(api_file)
285
292
  end
286
293
 
@@ -379,13 +386,12 @@ module GoogleDrive
379
386
  # "1smypkyAz4STrKO4Zkos5Z4UPUJKvvgIza32LnlQ7OGw/od7/private/full")
380
387
  def worksheet_by_url(url)
381
388
  case url
382
- when %r{^https?://spreadsheets.google.com/feeds/worksheets/.*/.*/full/.*$}
383
- worksheet_feed_url = url
384
- when %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full((\?.*)?)$}
385
- worksheet_feed_url =
386
- 'https://spreadsheets.google.com/feeds/worksheets/' \
387
- "#{Regexp.last_match(1)}/private/full/" \
388
- "#{Regexp.last_match(2)}#{Regexp.last_match(3)}"
389
+ when %r{^https?://spreadsheets.google.com/feeds/worksheets/(.*)/.*/full/(.*)$}
390
+ spreadsheet_id = Regexp.last_match(1)
391
+ worksheet_feed_id = Regexp.last_match(2)
392
+ when %r{^https?://spreadsheets.google.com/feeds/cells/(.*)/(.*)/private/full(\?.*)?$}
393
+ spreadsheet_id = Regexp.last_match(1)
394
+ worksheet_feed_id = Regexp.last_match(2)
389
395
  else
390
396
  raise(
391
397
  GoogleDrive::Error,
@@ -394,8 +400,15 @@ module GoogleDrive
394
400
  )
395
401
  end
396
402
 
397
- worksheet_feed_entry = request(:get, worksheet_feed_url)
398
- Worksheet.new(self, nil, worksheet_feed_entry)
403
+ spreadsheet = spreadsheet_by_key(spreadsheet_id)
404
+ worksheet = spreadsheet.worksheets.find{ |ws| ws.worksheet_feed_id == worksheet_feed_id }
405
+ unless worksheet
406
+ raise(
407
+ GoogleDrive::Error,
408
+ "Worksheet not found for the given URL: #{url}"
409
+ )
410
+ end
411
+ worksheet
399
412
  end
400
413
 
401
414
  # Returns the root folder.
@@ -496,7 +509,7 @@ module GoogleDrive
496
509
  name: title,
497
510
  }.merge(file_properties)
498
511
 
499
- file = drive.create_file(
512
+ file = drive_service.create_file(
500
513
  file_metadata, fields: '*', supports_team_drives: true
501
514
  )
502
515
 
@@ -661,7 +674,7 @@ module GoogleDrive
661
674
  end
662
675
  file_metadata[:parents] = params[:parents] if params[:parents]
663
676
 
664
- file = drive.create_file(file_metadata, api_params)
677
+ file = drive_service.create_file(file_metadata, api_params)
665
678
  wrap_api_file(file)
666
679
  end
667
680
 
@@ -16,6 +16,10 @@ module GoogleDrive
16
16
  # create_spreadsheet in GoogleDrive::Session to get GoogleDrive::Spreadsheet
17
17
  # object.
18
18
  class Spreadsheet < GoogleDrive::File
19
+
20
+ # TODO: Bump up the major version before switching the existing methods to
21
+ # v4 API because it requires to turn on a new API in the API console.
22
+
19
23
  include(Util)
20
24
 
21
25
  SUPPORTED_EXPORT_FORMAT = Set.new(%w[xlsx csv pdf])
@@ -45,16 +49,8 @@ module GoogleDrive
45
49
 
46
50
  # Returns worksheets of the spreadsheet as array of GoogleDrive::Worksheet.
47
51
  def worksheets
48
- doc = @session.request(:get, worksheets_feed_url)
49
- if doc.root.name != 'feed'
50
- raise(GoogleDrive::Error,
51
- format(
52
- "%s doesn't look like a worksheets feed URL because its root " \
53
- 'is not <feed>.',
54
- worksheets_feed_url
55
- ))
56
- end
57
- doc.css('entry').map { |e| Worksheet.new(@session, self, e) }.freeze
52
+ api_spreadsheet = @session.sheets_service.get_spreadsheet(id, fields: 'sheets.properties')
53
+ api_spreadsheet.sheets.map{ |s| Worksheet.new(@session, self, s.properties) }
58
54
  end
59
55
 
60
56
  # Returns a GoogleDrive::Worksheet with the given title in the spreadsheet.
@@ -68,24 +64,32 @@ module GoogleDrive
68
64
  # Returns a GoogleDrive::Worksheet with the given gid.
69
65
  #
70
66
  # Returns nil if not found.
71
- def worksheet_by_gid(gid)
72
- gid = gid.to_s
73
- worksheets.find { |ws| ws.gid == gid }
67
+ def worksheet_by_sheet_id(sheet_id)
68
+ sheet_id = sheet_id.to_i
69
+ worksheets.find { |ws| ws.sheet_id == sheet_id }
74
70
  end
75
71
 
72
+ alias worksheet_by_gid worksheet_by_sheet_id
73
+
76
74
  # Adds a new worksheet to the spreadsheet. Returns added
77
75
  # GoogleDrive::Worksheet.
78
- def add_worksheet(title, max_rows = 100, max_cols = 20)
79
- xml = <<-"EOS"
80
- <entry xmlns='http://www.w3.org/2005/Atom'
81
- xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
82
- <title>#{h(title)}</title>
83
- <gs:rowCount>#{h(max_rows)}</gs:rowCount>
84
- <gs:colCount>#{h(max_cols)}</gs:colCount>
85
- </entry>
86
- EOS
87
- doc = @session.request(:post, worksheets_feed_url, data: xml)
88
- Worksheet.new(@session, self, doc.root)
76
+ #
77
+ # When +index+ is specified, the worksheet is inserted at the given
78
+ # +index+.
79
+ def add_worksheet(title, max_rows = 100, max_cols = 20, index: nil)
80
+ (response,) = batch_update([{
81
+ add_sheet: {
82
+ properties: {
83
+ title: title,
84
+ index: index,
85
+ grid_properties: {
86
+ row_count: max_rows,
87
+ column_count: max_cols,
88
+ },
89
+ },
90
+ },
91
+ }])
92
+ Worksheet.new(@session, self, response.add_sheet.properties)
89
93
  end
90
94
 
91
95
  # Not available for GoogleDrive::Spreadsheet. Use export_as_file instead.
@@ -114,5 +118,18 @@ module GoogleDrive
114
118
  'Use export_to_io instead.'
115
119
  )
116
120
  end
121
+
122
+ # Performs batch update of the spreadsheet.
123
+ #
124
+ # +requests+ is an Array of Google::Apis::SheetsV4::Request or its Hash
125
+ # equivalent. Returns an Array of Google::Apis::SheetsV4::Response.
126
+ def batch_update(requests)
127
+ batch_request =
128
+ Google::Apis::SheetsV4::BatchUpdateSpreadsheetRequest.new(
129
+ requests: requests)
130
+ batch_response =
131
+ @session.sheets_service.batch_update_spreadsheet(id, batch_request)
132
+ batch_response.replies
133
+ end
117
134
  end
118
135
  end
@@ -16,6 +16,37 @@ module GoogleDrive
16
16
  class Worksheet
17
17
  include(Util)
18
18
 
19
+ # A few default color instances that match the colors from the Google Sheets web UI.
20
+ #
21
+ # TODO: Add more colors from
22
+ # https://github.com/denilsonsa/gimp-palettes/blob/master/palettes/Google-Drive.gpl
23
+ module Colors
24
+ RED = Google::Apis::SheetsV4::Color.new(red: 1.0)
25
+ DARK_RED_1 = Google::Apis::SheetsV4::Color.new(red: 0.8)
26
+ RED_BERRY = Google::Apis::SheetsV4::Color.new(red: 0.596)
27
+ DARK_RED_BERRY_1 = Google::Apis::SheetsV4::Color.new(red: 0.659, green: 0.11)
28
+ ORANGE = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 0.6)
29
+ DARK_ORANGE_1 = Google::Apis::SheetsV4::Color.new(red: 0.9, green: 0.569, blue: 0.22)
30
+ YELLOW = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 1.0)
31
+ DARK_YELLOW_1 = Google::Apis::SheetsV4::Color.new(red: 0.945, green: 0.76, blue: 0.196)
32
+ GREEN = Google::Apis::SheetsV4::Color.new(green: 1.0)
33
+ DARK_GREEN_1 = Google::Apis::SheetsV4::Color.new(red: 0.416, green: 0.659, blue: 0.31)
34
+ CYAN = Google::Apis::SheetsV4::Color.new(green: 1.0, blue: 1.0)
35
+ DARK_CYAN_1 = Google::Apis::SheetsV4::Color.new(red: 0.27, green: 0.506, blue: 0.557)
36
+ CORNFLOWER_BLUE = Google::Apis::SheetsV4::Color.new(red: 0.29, green: 0.525, blue: 0.91)
37
+ DARK_CORNFLOWER_BLUE_1 = Google::Apis::SheetsV4::Color.new(red: 0.235, green: 0.47, blue: 0.847)
38
+ BLUE = Google::Apis::SheetsV4::Color.new(blue: 1.0)
39
+ DARK_BLUE_1 = Google::Apis::SheetsV4::Color.new(red: 0.239, green: 0.522, blue: 0.776)
40
+ PURPLE = Google::Apis::SheetsV4::Color.new(red: 0.6, blue: 1.0)
41
+ DARK_PURPLE_1 = Google::Apis::SheetsV4::Color.new(red: 0.404, green: 0.306, blue: 0.655)
42
+ MAGENTA = Google::Apis::SheetsV4::Color.new(red: 1.0, blue: 1.0)
43
+ DARK_MAGENTA_1 = Google::Apis::SheetsV4::Color.new(red: 0.651, green: 0.302, blue: 0.475)
44
+ WHITE = Google::Apis::SheetsV4::Color.new(red: 1.0, green: 1.0, blue: 1.0)
45
+ BLACK = Google::Apis::SheetsV4::Color.new(red: 0.0, green: 0.0, blue: 0.0)
46
+ GRAY = Google::Apis::SheetsV4::Color.new(red: 0.8, green: 0.8, blue: 0.8)
47
+ DARK_GRAY_1 = Google::Apis::SheetsV4::Color.new(red: 0.714, green: 0.714, blue: 0.714)
48
+ end
49
+
19
50
  # @api private
20
51
  # A regexp which matches an invalid character in XML 1.0:
21
52
  # https://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0
@@ -23,44 +54,63 @@ module GoogleDrive
23
54
  /[^\u0009\u000a\u000d\u0020-\ud7ff\ue000-\ufffd\u{10000}-\u{10ffff}]/
24
55
 
25
56
  # @api private
26
- def initialize(session, spreadsheet, worksheet_feed_entry)
57
+ def initialize(session, spreadsheet, properties)
27
58
  @session = session
28
59
  @spreadsheet = spreadsheet
29
- set_worksheet_feed_entry(worksheet_feed_entry)
30
-
60
+ set_properties(properties)
31
61
  @cells = nil
32
62
  @input_values = nil
33
63
  @numeric_values = nil
34
64
  @modified = Set.new
35
65
  @list = nil
66
+ @v4_requests = []
36
67
  end
37
68
 
38
69
  # Nokogiri::XML::Element object of the <entry> element in a worksheets feed.
39
- attr_reader(:worksheet_feed_entry)
70
+ #
71
+ # DEPRECATED: This method is deprecated, and now requires additional
72
+ # network fetch. Consider using properties instead.
73
+ def worksheet_feed_entry
74
+ @worksheet_feed_entry ||= @session.request(:get, worksheet_feed_url).root
75
+ end
76
+
77
+ # Google::Apis::SheetsV4::SheetProperties object for this worksheet.
78
+ attr_reader :properties
40
79
 
41
80
  # Title of the worksheet (shown as tab label in Web interface).
42
- attr_reader(:title)
81
+ attr_reader :title
82
+
83
+ # GoogleDrive::Spreadsheet which this worksheet belongs to.
84
+ attr_reader :spreadsheet
43
85
 
44
86
  # Time object which represents the time the worksheet was last updated.
45
- attr_reader(:updated)
87
+ #
88
+ # DEPRECATED: From google_drive 3.0.0, it returns the time the
89
+ # *spreadsheet* was last updated, instead of the worksheet. This is because
90
+ # it looks the information is not available in Sheets v4 API.
91
+ def updated
92
+ spreadsheet.modified_time.to_time
93
+ end
46
94
 
47
95
  # URL of cell-based feed of the worksheet.
96
+ #
97
+ # DEPRECATED: This method is deprecated, and now requires additional
98
+ # network fetch.
48
99
  def cells_feed_url
49
- @worksheet_feed_entry.css(
100
+ worksheet_feed_entry.css(
50
101
  "link[rel='http://schemas.google.com/spreadsheets/2006#cellsfeed']"
51
102
  )[0]['href']
52
103
  end
53
104
 
54
105
  # URL of worksheet feed URL of the worksheet.
55
106
  def worksheet_feed_url
56
- @worksheet_feed_entry.css("link[rel='self']")[0]['href']
107
+ return '%s/%s' % [spreadsheet.worksheets_feed_url, worksheet_feed_id]
57
108
  end
58
109
 
59
110
  # URL to export the worksheet as CSV.
60
111
  def csv_export_url
61
- @worksheet_feed_entry.css(
62
- "link[rel='http://schemas.google.com/spreadsheets/2006#exportcsv']"
63
- )[0]['href']
112
+ 'https://docs.google.com/spreadsheets/d/%s/export?gid=%s&format=csv' %
113
+ [spreadsheet.id, gid]
64
114
  end
65
115
 
66
116
  # Exports the worksheet as String in CSV format.
@@ -74,10 +124,14 @@ module GoogleDrive
74
124
  open(path, 'wb') { |f| f.write(data) }
75
125
  end
76
126
 
77
- # gid of the worksheet.
127
+ # ID of the worksheet.
128
+ def sheet_id
129
+ @properties.sheet_id
130
+ end
131
+
132
+ # Returns sheet_id.to_s.
78
133
  def gid
79
- # A bit tricky but couldn't find a better way.
80
- CGI.parse(URI.parse(csv_export_url).query)['gid'].last
134
+ sheet_id.to_s
81
135
  end
82
136
 
83
137
  # URL to view/edit the worksheet in a Web browser.
@@ -85,20 +139,6 @@ module GoogleDrive
85
139
  format("%s\#gid=%s", spreadsheet.human_url, gid)
86
140
  end
87
141
 
88
- # GoogleDrive::Spreadsheet which this worksheet belongs to.
89
- def spreadsheet
90
- unless @spreadsheet
91
- unless worksheet_feed_url =~
92
- %r{https?://spreadsheets\.google\.com/feeds/worksheets/(.*)/(.*)$}
93
- raise(GoogleDrive::Error,
94
- 'Worksheet feed URL is in unknown format: ' \
95
- "#{worksheet_feed_url}")
96
- end
97
- @spreadsheet = @session.file_by_id(Regexp.last_match(1))
98
- end
99
- @spreadsheet
100
- end
101
-
102
142
  # Returns content of the cell as String. Arguments must be either
103
143
  # (row number, column number) or cell name. Top-left cell is [1, 1].
104
144
  #
@@ -314,8 +354,19 @@ module GoogleDrive
314
354
  # Note that changes you made by []= etc. is discarded if you haven't called
315
355
  # save().
316
356
  def reload
317
- set_worksheet_feed_entry(@session.request(:get, worksheet_feed_url).root)
318
- reload_cells
357
+ api_spreadsheet =
358
+ @session.sheets_service.get_spreadsheet(
359
+ spreadsheet.id,
360
+ ranges: "'%s'" % @title,
361
+ fields:
362
+ 'sheets(properties,data.rowData.values' \
363
+ '(formattedValue,userEnteredValue,effectiveValue))'
364
+ )
365
+ api_sheet = api_spreadsheet.sheets[0]
366
+ set_properties(api_sheet.properties)
367
+ update_cells_from_api_sheet(api_sheet)
368
+ @v4_requests = []
369
+ @worksheet_feed_entry = nil
319
370
  true
320
371
  end
321
372
 
@@ -324,122 +375,58 @@ module GoogleDrive
324
375
  sent = false
325
376
 
326
377
  if @meta_modified
378
+ add_request({
379
+ update_sheet_properties: {
380
+ properties: {
381
+ sheet_id: sheet_id,
382
+ title: title,
383
+ grid_properties: {row_count: max_rows, column_count: max_cols},
384
+ },
385
+ fields: '*',
386
+ },
387
+ })
388
+ end
327
389
 
328
- edit_url = @worksheet_feed_entry.css("link[rel='edit']")[0]['href']
329
- xml = <<-"EOS"
330
- <entry xmlns='http://www.w3.org/2005/Atom'
331
- xmlns:gs='http://schemas.google.com/spreadsheets/2006'>
332
- <title>#{h(title)}</title>
333
- <gs:rowCount>#{h(max_rows)}</gs:rowCount>
334
- <gs:colCount>#{h(max_cols)}</gs:colCount>
335
- </entry>
336
- EOS
337
-
338
- result = @session.request(
339
- :put,
340
- edit_url,
341
- data: xml,
342
- header: {
343
- 'Content-Type' => 'application/atom+xml;charset=utf-8',
344
- 'If-Match' => '*'
345
- }
346
- )
347
- set_worksheet_feed_entry(result.root)
348
-
390
+ if !@v4_requests.empty?
391
+ self.spreadsheet.batch_update(@v4_requests)
392
+ @v4_requests = []
349
393
  sent = true
350
394
  end
395
+
396
+ @remote_title = @title
351
397
 
352
398
  unless @modified.empty?
353
- # Gets id and edit URL for each cell.
354
- # Note that return-empty=true is required to get those info for empty cells.
355
- cell_entries = {}
356
- rows = @modified.map { |r, _c| r }
357
- cols = @modified.map { |_r, c| c }
358
- url = concat_url(
359
- cells_feed_url,
360
- "?return-empty=true&min-row=#{rows.min}&max-row=#{rows.max}" \
361
- "&min-col=#{cols.min}&max-col=#{cols.max}"
362
- )
363
- doc = @session.request(:get, url)
364
-
365
- doc.css('entry').each do |entry|
366
- row = entry.css('gs|cell')[0]['row'].to_i
367
- col = entry.css('gs|cell')[0]['col'].to_i
368
- cell_entries[[row, col]] = entry
369
- end
370
-
371
- xml = <<-EOS
372
- <feed xmlns="http://www.w3.org/2005/Atom"
373
- xmlns:batch="http://schemas.google.com/gdata/batch"
374
- xmlns:gs="http://schemas.google.com/spreadsheets/2006">
375
- <id>#{h(cells_feed_url)}</id>
376
- EOS
377
- @modified.each do |row, col|
378
- value = @cells[[row, col]]
379
- entry = cell_entries[[row, col]]
380
- id = entry.css('id').text
381
- edit_link = entry.css("link[rel='edit']")[0]
382
- unless edit_link
383
- raise(
384
- GoogleDrive::Error,
385
- format(
386
- "The user doesn't have write permission to the spreadsheet: %p",
387
- spreadsheet
388
- )
389
- )
390
- end
391
- edit_url = edit_link['href']
392
- xml << <<-EOS
393
- <entry>
394
- <batch:id>#{h(row)},#{h(col)}</batch:id>
395
- <batch:operation type="update"/>
396
- <id>#{h(id)}</id>
397
- <link
398
- rel="edit"
399
- type="application/atom+xml"
400
- href="#{h(edit_url)}"/>
401
- <gs:cell
402
- row="#{h(row)}"
403
- col="#{h(col)}"
404
- inputValue="#{h(value)}"/>
405
- </entry>
406
- EOS
399
+ min_modified_row = 1.0 / 0.0
400
+ max_modified_row = 0
401
+ min_modified_col = 1.0 / 0.0
402
+ max_modified_col = 0
403
+ @modified.each do |r, c|
404
+ min_modified_row = r if r < min_modified_row
405
+ max_modified_row = r if r > max_modified_row
406
+ min_modified_col = c if c < min_modified_col
407
+ max_modified_col = c if c > max_modified_col
407
408
  end
408
- xml << <<-"EOS"
409
- </feed>
410
- EOS
411
-
412
- batch_url = concat_url(cells_feed_url, '/batch')
413
- result = @session.request(
414
- :post,
415
- batch_url,
416
- data: xml,
417
- header: {
418
- 'Content-Type' => 'application/atom+xml;charset=utf-8',
419
- 'If-Match' => '*'
420
- }
421
- )
422
- result.css('entry').each do |entry|
423
- interrupted = entry.css('batch|interrupted')[0]
424
- if interrupted
425
- raise(
426
- GoogleDrive::Error,
427
- format('Update has failed: %s', interrupted['reason'])
428
- )
409
+
410
+ # Uses update_spreadsheet_value instead batch_update_spreadsheet with
411
+ # update_cells. batch_update_spreadsheet has benefit that the request
412
+ # can be batched with other requests. But it has drawback that the
413
+ # type of the value (string_value, number_value, etc.) must be
414
+ # explicitly specified in user_entered_value. Since I don't know exact
415
+ # logic to determine the type from text, I chose to use
416
+ # update_spreadsheet_value here.
417
+ range = "'%s'!R%dC%d:R%dC%d" %
418
+ [@title, min_modified_row, min_modified_col, max_modified_row, max_modified_col]
419
+ values = (min_modified_row..max_modified_row).map do |r|
420
+ (min_modified_col..max_modified_col).map do |c|
421
+ @modified.include?([r, c]) ? (@cells[[r, c]] || '') : nil
429
422
  end
430
- next if entry.css('batch|status').first['code'] =~ /^2/
431
- raise(
432
- GoogleDrive::Error,
433
- format(
434
- 'Updating cell %s has failed: %s',
435
- entry.css('id').text, entry.css('batch|status')[0]['reason']
436
- )
437
- )
438
423
  end
424
+ value_range = Google::Apis::SheetsV4::ValueRange.new(values: values)
425
+ @session.sheets_service.update_spreadsheet_value(
426
+ spreadsheet.id, range, value_range, value_input_option: 'USER_ENTERED')
439
427
 
440
428
  @modified.clear
441
429
  sent = true
442
-
443
430
  end
444
431
 
445
432
  sent
@@ -454,17 +441,20 @@ module GoogleDrive
454
441
  # Deletes this worksheet. Deletion takes effect right away without calling
455
442
  # save().
456
443
  def delete
457
- ws_doc = @session.request(:get, worksheet_feed_url)
458
- edit_url = ws_doc.css("link[rel='edit']")[0]['href']
459
- @session.request(:delete, edit_url)
444
+ spreadsheet.batch_update([{
445
+ delete_sheet: Google::Apis::SheetsV4::DeleteSheetRequest.new(sheet_id: sheet_id),
446
+ }])
460
447
  end
461
448
 
462
- # Returns true if you have changes made by []= which haven't been saved.
449
+ # Returns true if you have changes made by []= etc. which haven't been saved.
463
450
  def dirty?
464
- !@modified.empty?
451
+ !@modified.empty? || !@v4_requests.empty?
465
452
  end
466
453
 
467
454
  # List feed URL of the worksheet.
455
+ #
456
+ # DEPRECATED: This method is deprecated, and now requires additional
457
+ # network fetch.
468
458
  def list_feed_url
469
459
  @worksheet_feed_entry.css(
470
460
  "link[rel='http://schemas.google.com/spreadsheets/2006#listfeed']"
@@ -517,7 +507,7 @@ module GoogleDrive
517
507
  end
518
508
 
519
509
  def inspect
520
- fields = { worksheet_feed_url: worksheet_feed_url }
510
+ fields = { spreadsheet_id: spreadsheet.id, gid: gid }
521
511
  fields[:title] = @title if @title
522
512
  format(
523
513
  "\#<%p %s>",
@@ -526,39 +516,165 @@ module GoogleDrive
526
516
  )
527
517
  end
528
518
 
529
- private
519
+ # Merges a range of cells together. "MERGE_COLUMNS" is another option for merge_type
520
+ def merge_cells(top_row, left_col, num_rows, num_cols, merge_type: 'MERGE_ALL')
521
+ range = v4_range_object(top_row, left_col, num_rows, num_cols)
522
+ add_request({
523
+ merge_cells:
524
+ Google::Apis::SheetsV4::MergeCellsRequest.new(
525
+ range: range, merge_type: merge_type),
526
+ })
527
+ end
528
+
529
+ # Changes the formatting of a range of cells to match the given number format.
530
+ # For example to change A1 to a percentage with 1 decimal point:
531
+ # worksheet.set_number_format(1, 1, 1, 1, "##.#%")
532
+ # Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#numberformat
533
+ def set_number_format(top_row, left_col, num_rows, num_cols, pattern, type: "NUMBER")
534
+ number_format = Google::Apis::SheetsV4::NumberFormat.new(type: type, pattern: pattern)
535
+ format = Google::Apis::SheetsV4::CellFormat.new(number_format: number_format)
536
+ fields = 'userEnteredFormat(numberFormat)'
537
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
538
+ end
539
+
540
+ # Changes text alignment of a range of cells.
541
+ # Horizontal alignment can be "LEFT", "CENTER", or "RIGHT".
542
+ # Vertical alignment can be "TOP", "MIDDLE", or "BOTTOM".
543
+ # Google API reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#HorizontalAlign
544
+ def set_text_alignment(
545
+ top_row, left_col, num_rows, num_cols,
546
+ horizontal: nil, vertical: nil)
547
+ return if horizontal.nil? && vertical.nil?
548
+
549
+ format = Google::Apis::SheetsV4::CellFormat.new(
550
+ horizontal_alignment: horizontal, vertical_alignment: vertical)
551
+ subfields =
552
+ (horizontal.nil? ? [] : ['horizontalAlignment']) +
553
+ (vertical.nil? ? [] : ['verticalAlignment'])
554
+
555
+ fields = 'userEnteredFormat(%s)' % subfields.join(',')
556
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
557
+ end
558
+
559
+ # Changes the background color on a range of cells. e.g.:
560
+ # worksheet.set_background_color(1, 1, 1, 1, GoogleDrive::Worksheet::Colors::DARK_YELLOW_1)
561
+ #
562
+ # background_color is an instance of Google::Apis::SheetsV4::Color.
563
+ def set_background_color(top_row, left_col, num_rows, num_cols, background_color)
564
+ format = Google::Apis::SheetsV4::CellFormat.new(background_color: background_color)
565
+ fields = 'userEnteredFormat(backgroundColor)'
566
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
567
+ end
568
+
569
+ # Change the text formatting on a range of cells. e.g., To set cell
570
+ # A1 to have red text that is bold and italic:
571
+ # worksheet.set_text_format(
572
+ # 1, 1, 1, 1,
573
+ # bold: true,
574
+ # italic: true,
575
+ # foreground_color: GoogleDrive::Worksheet::Colors::RED_BERRY)
576
+ #
577
+ # foreground_color is an instance of Google::Apis::SheetsV4::Color.
578
+ # Google API reference:
579
+ # https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets#textformat
580
+ def set_text_format(top_row, left_col, num_rows, num_cols, bold: false,
581
+ italic: false, strikethrough: false, font_size: nil,
582
+ font_family: nil, foreground_color: nil)
583
+ text_format = Google::Apis::SheetsV4::TextFormat.new(
584
+ bold: bold,
585
+ italic: italic,
586
+ strikethrough: strikethrough,
587
+ font_size: font_size,
588
+ font_family: font_family,
589
+ foreground_color: foreground_color
590
+ )
530
591
 
531
- def set_worksheet_feed_entry(entry)
532
- @worksheet_feed_entry = entry
533
- @title = entry.css('title').text
534
- set_max_values(entry)
535
- @updated = Time.parse(entry.css('updated').text)
536
- @meta_modified = false
592
+ format = Google::Apis::SheetsV4::CellFormat.new(text_format: text_format)
593
+ fields = 'userEnteredFormat(textFormat)'
594
+ format_cells(top_row, left_col, num_rows, num_cols, format, fields)
595
+ end
596
+
597
+ # Update the border styles for a range of cells.
598
+ # borders is a Hash of Google::Apis::SheetsV4::Border keyed with the
599
+ # following symbols: :top, :bottom, :left, :right, :innerHorizontal, :innerVertical
600
+ # e.g., To set a black double-line on the bottom of A1:
601
+ # update_borders(
602
+ # 1, 1, 1, 1,
603
+ # {bottom: Google::Apis::SheetsV4::Border.new(
604
+ # style: "DOUBLE", color: GoogleDrive::Worksheet::Colors::BLACK)})
605
+ def update_borders(top_row, left_col, num_rows, num_cols, borders)
606
+ request = Google::Apis::SheetsV4::UpdateBordersRequest.new(borders)
607
+ request.range = v4_range_object(top_row, left_col, num_rows, num_cols)
608
+ add_request({update_borders: request})
609
+ end
610
+
611
+ # Add an instance of Google::Apis::SheetsV4::Request (or its Hash
612
+ # equivalent) which will be applied on the next call to the save method.
613
+ def add_request(request)
614
+ @v4_requests.push(request)
537
615
  end
538
616
 
539
- def set_max_values(entry)
540
- @max_rows = entry.css('gs|rowCount').text.to_i
541
- @max_cols = entry.css('gs|colCount').text.to_i
617
+ # @api private
618
+ def worksheet_feed_id
619
+ gid_int = sheet_id
620
+ xor_val = gid_int > 31578 ? 474 : 31578
621
+ letter = gid_int > 31578 ? 'o' : ''
622
+ letter + (gid_int ^ xor_val).to_s(36)
623
+ end
624
+
625
+ private
626
+
627
+ def format_cells(top_row, left_col, num_rows, num_cols, format, fields)
628
+ add_request({
629
+ repeat_cell:
630
+ Google::Apis::SheetsV4::RepeatCellRequest.new(
631
+ range: v4_range_object(top_row, left_col, num_rows, num_cols),
632
+ cell: Google::Apis::SheetsV4::CellData.new(user_entered_format: format),
633
+ fields: fields
634
+ ),
635
+ })
636
+ end
637
+
638
+ def set_properties(properties)
639
+ @properties = properties
640
+ @title = @remote_title = properties.title
641
+ @max_rows = properties.grid_properties.row_count
642
+ @max_cols = properties.grid_properties.column_count
643
+ @meta_modified = false
542
644
  end
543
645
 
544
646
  def reload_cells
545
- doc = @session.request(:get, cells_feed_url)
647
+ response =
648
+ @session.sheets_service.get_spreadsheet(
649
+ spreadsheet.id,
650
+ ranges: "'%s'" % @remote_title,
651
+ fields: 'sheets.data.rowData.values(formattedValue,userEnteredValue,effectiveValue)'
652
+ )
653
+ update_cells_from_api_sheet(response.sheets[0])
654
+ end
546
655
 
547
- @num_cols = nil
548
- @num_rows = nil
656
+ def update_cells_from_api_sheet(api_sheet)
657
+ rows_data = api_sheet.data[0].row_data || []
549
658
 
659
+ @num_rows = rows_data.size
660
+ @num_cols = 0
550
661
  @cells = {}
551
662
  @input_values = {}
552
663
  @numeric_values = {}
553
- doc.css('feed > entry').each do |entry|
554
- cell = entry.css('gs|cell')[0]
555
- row = cell['row'].to_i
556
- col = cell['col'].to_i
557
- @cells[[row, col]] = cell.inner_text
558
- @input_values[[row, col]] = cell['inputValue'] || cell.inner_text
559
- numeric_value = cell['numericValue']
560
- @numeric_values[[row, col]] = numeric_value ? numeric_value.to_f : nil
664
+
665
+ rows_data.each_with_index do |row_data, r|
666
+ next if !row_data.values
667
+ @num_cols = row_data.values.size if row_data.values.size > @num_cols
668
+ row_data.values.each_with_index do |cell_data, c|
669
+ k = [r + 1, c + 1]
670
+ @cells[k] = cell_data.formatted_value || ''
671
+ @input_values[k] = extended_value_to_str(cell_data.user_entered_value)
672
+ @numeric_values[k] =
673
+ cell_data.effective_value && cell_data.effective_value.number_value ?
674
+ cell_data.effective_value.number_value.to_f : nil
675
+ end
561
676
  end
677
+
562
678
  @modified.clear
563
679
  end
564
680
 
@@ -596,5 +712,25 @@ module GoogleDrive
596
712
  )
597
713
  end
598
714
  end
715
+
716
+ def v4_range_object(top_row, left_col, num_rows, num_cols)
717
+ Google::Apis::SheetsV4::GridRange.new(
718
+ sheet_id: sheet_id,
719
+ start_row_index: top_row - 1,
720
+ start_column_index: left_col - 1,
721
+ end_row_index: top_row + num_rows - 1,
722
+ end_column_index: left_col + num_cols - 1
723
+ )
724
+ end
725
+
726
+ def extended_value_to_str(extended_value)
727
+ return '' if !extended_value
728
+ value =
729
+ extended_value.number_value ||
730
+ extended_value.string_value ||
731
+ extended_value.bool_value ||
732
+ extended_value.formula_value
733
+ value.to_s
734
+ end
599
735
  end
600
736
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google_drive
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.12
4
+ version: 3.0.0.pre1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hiroshi Ichikawa
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-09-10 00:00:00.000000000 Z
11
+ date: 2018-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -164,9 +164,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
164
164
  version: 2.0.0
165
165
  required_rubygems_version: !ruby/object:Gem::Requirement
166
166
  requirements:
167
- - - ">="
167
+ - - ">"
168
168
  - !ruby/object:Gem::Version
169
- version: '0'
169
+ version: 1.3.1
170
170
  requirements: []
171
171
  rubyforge_project:
172
172
  rubygems_version: 2.6.14