parallel588_google_drive 0.3.3

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,485 @@
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
+
10
+ require "google_drive/util"
11
+ require "google_drive/client_login_fetcher"
12
+ require "google_drive/oauth1_fetcher"
13
+ require "google_drive/oauth2_fetcher"
14
+ require "google_drive/error"
15
+ require "google_drive/authentication_error"
16
+ require "google_drive/spreadsheet"
17
+ require "google_drive/worksheet"
18
+ require "google_drive/collection"
19
+ require "google_drive/file"
20
+
21
+
22
+ module GoogleDrive
23
+
24
+ # Use GoogleDrive.login or GoogleDrive.saved_session to get
25
+ # GoogleDrive::Session object.
26
+ class Session
27
+
28
+ include(Util)
29
+ extend(Util)
30
+
31
+ UPLOAD_CHUNK_SIZE = 512 * 1024
32
+
33
+ # The same as GoogleDrive.login.
34
+ def self.login(mail, password, proxy = nil)
35
+ session = Session.new(nil, ClientLoginFetcher.new({}, proxy))
36
+ session.login(mail, password)
37
+ return session
38
+ end
39
+
40
+ # The same as GoogleDrive.login_with_oauth.
41
+ def self.login_with_oauth(oauth_token)
42
+ case oauth_token
43
+ when OAuth::AccessToken
44
+ fetcher = OAuth1Fetcher.new(oauth_token)
45
+ when OAuth2::AccessToken
46
+ fetcher = OAuth2Fetcher.new(oauth_token)
47
+ else
48
+ raise(GoogleDrive::Error,
49
+ "oauth_token is neither OAuth::Token nor OAuth2::Token: %p" % oauth_token)
50
+ end
51
+ return Session.new(nil, fetcher)
52
+ end
53
+
54
+ # The same as GoogleDrive.restore_session.
55
+ def self.restore_session(auth_tokens, proxy = nil)
56
+ return Session.new(auth_tokens, nil, proxy)
57
+ end
58
+
59
+ # Creates a dummy GoogleDrive::Session object for testing.
60
+ def self.new_dummy()
61
+ return Session.new(nil, Object.new())
62
+ end
63
+
64
+ # DEPRECATED: Use GoogleDrive.restore_session instead.
65
+ def initialize(auth_tokens = nil, fetcher = nil, proxy = nil)
66
+ if fetcher
67
+ @fetcher = fetcher
68
+ else
69
+ @fetcher = ClientLoginFetcher.new(auth_tokens || {}, proxy)
70
+ end
71
+ end
72
+
73
+ # Authenticates with given +mail+ and +password+, and updates current session object
74
+ # if succeeds. Raises GoogleDrive::AuthenticationError if fails.
75
+ # Google Apps account is supported.
76
+ def login(mail, password)
77
+ if !@fetcher.is_a?(ClientLoginFetcher)
78
+ raise(GoogleDrive::Error,
79
+ "Cannot call login for session created by login_with_oauth.")
80
+ end
81
+ begin
82
+ @fetcher.auth_tokens = {
83
+ :wise => authenticate(mail, password, :wise),
84
+ :writely => authenticate(mail, password, :writely),
85
+ }
86
+ rescue GoogleDrive::Error => ex
87
+ return true if @on_auth_fail && @on_auth_fail.call()
88
+ raise(AuthenticationError, "Authentication failed for #{mail}: #{ex.message}")
89
+ end
90
+ end
91
+
92
+ # Authentication tokens.
93
+ def auth_tokens
94
+ if !@fetcher.is_a?(ClientLoginFetcher)
95
+ raise(GoogleDrive::Error,
96
+ "Cannot call auth_tokens for session created by " +
97
+ "login_with_oauth.")
98
+ end
99
+ return @fetcher.auth_tokens
100
+ end
101
+
102
+ # Authentication token.
103
+ def auth_token(auth = :wise)
104
+ return self.auth_tokens[auth]
105
+ end
106
+
107
+ # Proc or Method called when authentication has failed.
108
+ # When this function returns +true+, it tries again.
109
+ attr_accessor :on_auth_fail
110
+
111
+ # Returns list of files for the user as array of GoogleDrive::File or its subclass.
112
+ # You can specify query parameters described at
113
+ # https://developers.google.com/google-apps/documents-list/#getting_a_list_of_documents_and_files
114
+ #
115
+ # files doesn't return collections unless "showfolders" => true is specified.
116
+ #
117
+ # e.g.
118
+ # session.files
119
+ # session.files("title" => "hoge", "title-exact" => "true")
120
+ def files(params = {})
121
+ url = concat_url(
122
+ "#{DOCS_BASE_URL}?v=3", "?" + encode_query(params))
123
+ doc = request(:get, url, :auth => :writely)
124
+ return doc.css("feed > entry").map(){ |e| entry_element_to_file(e) }
125
+ end
126
+
127
+ # Returns GoogleDrive::File or its subclass whose title exactly matches +title+.
128
+ # Returns nil if not found. If multiple files with the +title+ are found, returns
129
+ # one of them.
130
+ def file_by_title(title)
131
+ return files("title" => title, "title-exact" => "true")[0]
132
+ end
133
+
134
+ # Returns GoogleDrive::File.
135
+ # Returns nil if not found
136
+ def find_by_id(resource_id)
137
+ doc = request(:get, "https://docs.google.com/feeds/documents/private/full/#{resource_id}?v=3", :auth => :writely)
138
+ return entry_element_to_file(doc)
139
+ end
140
+
141
+ # Returns list of spreadsheets for the user as array of GoogleDrive::Spreadsheet.
142
+ # You can specify query parameters e.g. "title", "title-exact".
143
+ #
144
+ # e.g.
145
+ # session.spreadsheets
146
+ # session.spreadsheets("title" => "hoge")
147
+ # session.spreadsheets("title" => "hoge", "title-exact" => "true")
148
+ def spreadsheets(params = {})
149
+ query = encode_query(params)
150
+ doc = request(
151
+ :get, "https://spreadsheets.google.com/feeds/spreadsheets/private/full?#{query}")
152
+ result = []
153
+ doc.css("feed > entry").each() do |entry|
154
+ title = entry.css("title").text
155
+ url = entry.css(
156
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
157
+ result.push(Spreadsheet.new(self, url, title))
158
+ end
159
+ return result
160
+ end
161
+
162
+ # Returns GoogleDrive::Spreadsheet with given +key+.
163
+ #
164
+ # e.g.
165
+ # # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
166
+ # session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg")
167
+ def spreadsheet_by_key(key)
168
+ url = "https://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
169
+ return Spreadsheet.new(self, url)
170
+ end
171
+
172
+ # Returns GoogleDrive::Spreadsheet with given +url+. You must specify either of:
173
+ # - URL of the page you open to access the spreadsheet in your browser
174
+ # - URL of worksheet-based feed of the spreadseet
175
+ #
176
+ # e.g.
177
+ # session.spreadsheet_by_url(
178
+ # "https://docs.google.com/spreadsheet/ccc?key=pz7XtlQC-PYx-jrVMJErTcg")
179
+ # session.spreadsheet_by_url(
180
+ # "https://spreadsheets.google.com/feeds/" +
181
+ # "worksheets/pz7XtlQC-PYx-jrVMJErTcg/private/full")
182
+ def spreadsheet_by_url(url)
183
+ # Tries to parse it as URL of human-readable spreadsheet.
184
+ uri = URI.parse(url)
185
+ if ["spreadsheets.google.com", "docs.google.com"].include?(uri.host) &&
186
+ uri.path =~ /\/ccc$/
187
+ if (uri.query || "").split(/&/).find(){ |s| s=~ /^key=(.*)$/ }
188
+ return spreadsheet_by_key($1)
189
+ end
190
+ end
191
+ # Assumes the URL is worksheets feed URL.
192
+ return Spreadsheet.new(self, url)
193
+ end
194
+
195
+ # Returns GoogleDrive::Spreadsheet with given +title+.
196
+ # Returns nil if not found. If multiple spreadsheets with the +title+ are found, returns
197
+ # one of them.
198
+ def spreadsheet_by_title(title)
199
+ return spreadsheets({"title" => title, "title-exact" => "true"})[0]
200
+ end
201
+
202
+ # Returns GoogleDrive::Worksheet with given +url+.
203
+ # You must specify URL of cell-based feed of the worksheet.
204
+ #
205
+ # e.g.
206
+ # session.worksheet_by_url(
207
+ # "http://spreadsheets.google.com/feeds/" +
208
+ # "cells/pz7XtlQC-PYxNmbBVgyiNWg/od6/private/full")
209
+ def worksheet_by_url(url)
210
+ return Worksheet.new(self, nil, url)
211
+ end
212
+
213
+ # Returns the root collection.
214
+ def root_collection
215
+ return Collection.new(self, Collection::ROOT_URL)
216
+ end
217
+
218
+ # Returns the top-level collections (direct children of the root collection).
219
+ def collections
220
+ return self.root_collection.subcollections
221
+ end
222
+
223
+ # Returns a top-level collection whose title exactly matches +title+ as
224
+ # GoogleDrive::Collection.
225
+ # Returns nil if not found. If multiple collections with the +title+ are found, returns
226
+ # one of them.
227
+ def collection_by_title(title)
228
+ return self.root_collection.subcollection_by_title(title)
229
+ end
230
+
231
+ # Returns GoogleDrive::Collection with given +url+.
232
+ # You must specify either of:
233
+ # - URL of the page you get when you go to https://docs.google.com/ with your browser and
234
+ # open a collection
235
+ # - URL of collection (folder) feed
236
+ #
237
+ # e.g.
238
+ # session.collection_by_url(
239
+ # "https://drive.google.com/#folders/" +
240
+ # "0B9GfDpQ2pBVUODNmOGE0NjIzMWU3ZC00NmUyLTk5NzEtYaFkZjY1MjAyxjMc")
241
+ # session.collection_by_url(
242
+ # "http://docs.google.com/feeds/default/private/full/folder%3A" +
243
+ # "0B9GfDpQ2pBVUODNmOGE0NjIzMWU3ZC00NmUyLTk5NzEtYaFkZjY1MjAyxjMc")
244
+ def collection_by_url(url)
245
+ uri = URI.parse(url)
246
+ if ["docs.google.com", "drive.google.com"].include?(uri.host) &&
247
+ uri.fragment =~ /^folders\/(.+)$/
248
+ # Looks like a URL of human-readable collection page. Converts to collection feed URL.
249
+ url = "#{DOCS_BASE_URL}/folder%3A#{$1}"
250
+ end
251
+ return Collection.new(self, url)
252
+ end
253
+
254
+ # Creates new spreadsheet and returns the new GoogleDrive::Spreadsheet.
255
+ #
256
+ # e.g.
257
+ # session.create_spreadsheet("My new sheet")
258
+ def create_spreadsheet(
259
+ title = "Untitled",
260
+ feed_url = "https://docs.google.com/feeds/documents/private/full")
261
+
262
+ xml = <<-"EOS"
263
+ <atom:entry
264
+ xmlns:atom="http://www.w3.org/2005/Atom"
265
+ xmlns:docs="http://schemas.google.com/docs/2007">
266
+ <atom:category
267
+ scheme="http://schemas.google.com/g/2005#kind"
268
+ term="http://schemas.google.com/docs/2007#spreadsheet"
269
+ label="spreadsheet"/>
270
+ <atom:title>#{h(title)}</atom:title>
271
+ </atom:entry>
272
+ EOS
273
+
274
+ doc = request(:post, feed_url, :data => xml, :auth => :writely)
275
+ ss_url = doc.css(
276
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
277
+ return Spreadsheet.new(self, ss_url, title)
278
+
279
+ end
280
+
281
+ # Uploads a file with the given +title+ and +content+.
282
+ # Returns a GoogleSpreadsheet::File object.
283
+ #
284
+ # e.g.
285
+ # # Uploads and converts to a Google Docs document:
286
+ # session.upload_from_string(
287
+ # "Hello world.", "Hello", :content_type => "text/plain")
288
+ #
289
+ # # Uploads without conversion:
290
+ # session.upload_from_string(
291
+ # "Hello world.", "Hello", :content_type => "text/plain", :convert => false)
292
+ #
293
+ # # Uploads and converts to a Google Spreadsheet:
294
+ # session.upload_from_string("hoge\tfoo\n", "Hoge", :content_type => "text/tab-separated-values")
295
+ # session.upload_from_string("hoge,foo\n", "Hoge", :content_type => "text/tsv")
296
+ def upload_from_string(content, title = "Untitled", params = {})
297
+ return upload_from_io(StringIO.new(content), title, params)
298
+ end
299
+
300
+ # Uploads a local file.
301
+ # Returns a GoogleSpreadsheet::File object.
302
+ #
303
+ # e.g.
304
+ # # Uploads a text file and converts to a Google Docs document:
305
+ # session.upload_from_file("/path/to/hoge.txt")
306
+ #
307
+ # # Uploads without conversion:
308
+ # session.upload_from_file("/path/to/hoge.txt", "Hoge", :convert => false)
309
+ #
310
+ # # Uploads with explicit content type:
311
+ # session.upload_from_file("/path/to/hoge", "Hoge", :content_type => "text/plain")
312
+ #
313
+ # # Uploads a text file and converts to a Google Spreadsheet:
314
+ # session.upload_from_file("/path/to/hoge.tsv", "Hoge")
315
+ # session.upload_from_file("/path/to/hoge.csv", "Hoge")
316
+ # session.upload_from_file("/path/to/hoge", "Hoge", :content_type => "text/tab-separated-values")
317
+ # session.upload_from_file("/path/to/hoge", "Hoge", :content_type => "text/csv")
318
+ def upload_from_file(path, title = nil, params = {})
319
+ file_name = ::File.basename(path)
320
+ params = {:file_name => file_name}.merge(params)
321
+ open(path, "rb") do |f|
322
+ return upload_from_io(f, title || file_name, params)
323
+ end
324
+ end
325
+
326
+ # Uploads a file. Reads content from +io+.
327
+ # Returns a GoogleSpreadsheet::File object.
328
+ def upload_from_io(io, title = "Untitled", params = {})
329
+ doc = request(:get, "#{DOCS_BASE_URL}?v=3",
330
+ :auth => :writely)
331
+ initial_url = doc.css(
332
+ "link[rel='http://schemas.google.com/g/2005#resumable-create-media']")[0]["href"]
333
+ return upload_raw(:post, initial_url, io, title, params)
334
+ end
335
+
336
+ def upload_raw(method, url, io, title = "Untitled", params = {}) #:nodoc:
337
+
338
+ params = {:convert => true}.merge(params)
339
+ pos = io.pos
340
+ io.seek(0, IO::SEEK_END)
341
+ total_bytes = io.pos - pos
342
+ io.pos = pos
343
+ content_type = params[:content_type]
344
+ if !content_type && params[:file_name]
345
+ content_type = EXT_TO_CONTENT_TYPE[::File.extname(params[:file_name]).downcase]
346
+ end
347
+ if !content_type
348
+ content_type = "application/octet-stream"
349
+ end
350
+
351
+ initial_xml = <<-"EOS"
352
+ <entry xmlns="http://www.w3.org/2005/Atom"
353
+ xmlns:docs="http://schemas.google.com/docs/2007">
354
+ <title>#{h(title)}</title>
355
+ </entry>
356
+ EOS
357
+
358
+ default_initial_header = {
359
+ "Content-Type" => "application/atom+xml",
360
+ "X-Upload-Content-Type" => content_type,
361
+ "X-Upload-Content-Length" => total_bytes.to_s(),
362
+ }
363
+ initial_full_url = concat_url(url, params[:convert] ? "?convert=true" : "?convert=false")
364
+ initial_response = request(method, initial_full_url,
365
+ :header => default_initial_header.merge(params[:header] || {}),
366
+ :data => initial_xml,
367
+ :auth => :writely,
368
+ :response_type => :response)
369
+ upload_url = initial_response["location"]
370
+
371
+ if total_bytes > 0
372
+ sent_bytes = 0
373
+ while data = io.read(UPLOAD_CHUNK_SIZE)
374
+ content_range = "bytes %d-%d/%d" % [
375
+ sent_bytes,
376
+ sent_bytes + data.bytesize - 1,
377
+ total_bytes,
378
+ ]
379
+ upload_header = {
380
+ "Content-Type" => content_type,
381
+ "Content-Range" => content_range,
382
+ }
383
+ doc = request(
384
+ :put, upload_url, :header => upload_header, :data => data, :auth => :writely)
385
+ sent_bytes += data.bytesize
386
+ end
387
+ else
388
+ upload_header = {
389
+ "Content-Type" => content_type,
390
+ }
391
+ doc = request(
392
+ :put, upload_url, :header => upload_header, :data => "", :auth => :writely)
393
+ end
394
+
395
+ return entry_element_to_file(doc.root)
396
+
397
+ end
398
+
399
+ def entry_element_to_file(entry) #:nodoc:
400
+ type, resource_id = entry.css("gd|resourceId").text.split(/:/)
401
+ title = entry.css("title").text
402
+ case type
403
+ when "folder"
404
+ return Collection.new(self, entry)
405
+ when "spreadsheet"
406
+ worksheets_feed_link = entry.css(
407
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]
408
+ return Spreadsheet.new(self, worksheets_feed_link["href"], title)
409
+ else
410
+ return GoogleDrive::File.new(self, entry)
411
+ end
412
+ end
413
+
414
+ def request(method, url, params = {}) #:nodoc:
415
+
416
+ # Always uses HTTPS.
417
+ url = url.gsub(%r{^http://}, "https://")
418
+ data = params[:data]
419
+ auth = params[:auth] || :wise
420
+ if params[:header]
421
+ extra_header = params[:header]
422
+ elsif data
423
+ extra_header = {"Content-Type" => "application/atom+xml"}
424
+ else
425
+ extra_header = {}
426
+ end
427
+ response_type = params[:response_type] || :xml
428
+
429
+ while true
430
+ response = @fetcher.request_raw(method, url, data, extra_header, auth)
431
+ if response.code == "401" && @on_auth_fail && @on_auth_fail.call()
432
+ next
433
+ end
434
+ if !(response.code =~ /^[23]/)
435
+ raise(
436
+ response.code == "401" ? AuthenticationError : GoogleDrive::Error,
437
+ "Response code #{response.code} for #{method} #{url}: " +
438
+ CGI.unescapeHTML(response.body))
439
+ end
440
+ return convert_response(response, response_type)
441
+ end
442
+
443
+ end
444
+
445
+ def inspect
446
+ return "#<%p:0x%x>" % [self.class, self.object_id]
447
+ end
448
+
449
+ private
450
+
451
+ def convert_response(response, response_type)
452
+ case response_type
453
+ when :xml
454
+ return Nokogiri.XML(response.body)
455
+ when :raw
456
+ return response.body
457
+ when :response
458
+ return response
459
+ else
460
+ raise(GoogleDrive::Error,
461
+ "Unknown params[:response_type]: %s" % response_type)
462
+ end
463
+ end
464
+
465
+ def authenticate(mail, password, auth)
466
+ params = {
467
+ "accountType" => "HOSTED_OR_GOOGLE",
468
+ "Email" => mail,
469
+ "Passwd" => password,
470
+ "service" => auth.to_s(),
471
+ "source" => "Gimite-RubyGoogleDrive-1.00",
472
+ }
473
+ header = {"Content-Type" => "application/x-www-form-urlencoded"}
474
+ response = request(:post,
475
+ "https://www.google.com/accounts/ClientLogin",
476
+ :data => encode_query(params),
477
+ :auth => :none,
478
+ :header => header,
479
+ :response_type => :raw)
480
+ return response.slice(/^Auth=(.*)$/, 1)
481
+ end
482
+
483
+ end
484
+
485
+ end