parallel588_google_drive 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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