google_drive 0.3.0

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,84 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "forwardable"
5
+
6
+ require "google_drive/util"
7
+ require "google_drive/error"
8
+
9
+
10
+ module GoogleDrive
11
+
12
+ # Hash-like object returned by GoogleDrive::List#[].
13
+ class ListRow
14
+
15
+ include(Enumerable)
16
+ extend(Forwardable)
17
+
18
+ def_delegators(:to_hash,
19
+ :keys, :values, :each_key, :each_value, :each, :each_pair, :hash,
20
+ :assoc, :fetch, :flatten, :key, :invert, :size, :length, :rassoc,
21
+ :merge, :reject, :select, :sort, :to_a, :values_at)
22
+
23
+ def initialize(list, index) #:nodoc:
24
+ @list = list
25
+ @index = index
26
+ end
27
+
28
+ def [](key)
29
+ return @list.get(@index, key)
30
+ end
31
+
32
+ def []=(key, value)
33
+ @list.set(@index, key, value)
34
+ end
35
+
36
+ def has_key?(key)
37
+ return @list.keys.include?(key)
38
+ end
39
+
40
+ alias include? has_key?
41
+ alias key? has_key?
42
+ alias member? has_key?
43
+
44
+ def update(hash)
45
+ for k, v in hash
46
+ self[k] = v
47
+ end
48
+ end
49
+
50
+ alias merge! update
51
+
52
+ def replace(hash)
53
+ clear()
54
+ update(hash)
55
+ end
56
+
57
+ def clear()
58
+ for key in @list.keys
59
+ self[key] = ""
60
+ end
61
+ end
62
+
63
+ def to_hash()
64
+ result = {}
65
+ for key in @list.keys
66
+ result[key] = self[key]
67
+ end
68
+ return result
69
+ end
70
+
71
+ def ==(other)
72
+ return self.class == other.class && self.to_hash() == other.to_hash()
73
+ end
74
+
75
+ alias === ==
76
+ alias eql? ==
77
+
78
+ def inspect
79
+ return "\#<%p %p>" % [self.class, to_hash()]
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -0,0 +1,26 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "rubygems"
5
+ require "oauth"
6
+
7
+
8
+ module GoogleDrive
9
+
10
+ class OAuth1Fetcher #:nodoc:
11
+
12
+ def initialize(oauth1_token)
13
+ @oauth1_token = oauth1_token
14
+ end
15
+
16
+ def request_raw(method, url, data, extra_header, auth)
17
+ if method == :delete || method == :get
18
+ return @oauth1_token.__send__(method, url, extra_header)
19
+ else
20
+ return @oauth1_token.__send__(method, url, data, extra_header)
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1,47 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "rubygems"
5
+ require "oauth2"
6
+
7
+
8
+ module GoogleDrive
9
+
10
+ class OAuth2Fetcher #:nodoc:
11
+
12
+ class Response
13
+
14
+ def initialize(raw_res)
15
+ @raw_res = raw_res
16
+ end
17
+
18
+ def code
19
+ return @raw_res.status.to_s()
20
+ end
21
+
22
+ def body
23
+ return @raw_res.body
24
+ end
25
+
26
+ def [](name)
27
+ return @raw_res.headers[name]
28
+ end
29
+
30
+ end
31
+
32
+ def initialize(oauth2_token)
33
+ @oauth2_token = oauth2_token
34
+ end
35
+
36
+ def request_raw(method, url, data, extra_header, auth)
37
+ if method == :delete || method == :get
38
+ raw_res = @oauth2_token.request(method, url, {:header => extra_header})
39
+ else
40
+ raw_res = @oauth2_token.request(method, url, {:header => extra_header, :body => data})
41
+ end
42
+ return Response.new(raw_res)
43
+ end
44
+
45
+ end
46
+
47
+ end
@@ -0,0 +1,31 @@
1
+ # Author: Hiroshi Ichikawa <http://gimite.net/>
2
+ # The license of this source is "New BSD Licence"
3
+
4
+ require "google_drive/util"
5
+ require "google_drive/error"
6
+
7
+
8
+ module GoogleDrive
9
+
10
+ # DEPRECATED: Table and Record feeds are deprecated and they will not be available after
11
+ # March 2012.
12
+ #
13
+ # Use GoogleDrive::Table#records to get GoogleDrive::Record objects.
14
+ class Record < Hash
15
+ include(Util)
16
+
17
+ def initialize(session, entry) #:nodoc:
18
+ @session = session
19
+ entry.css("gs|field").each() do |field|
20
+ self[field["name"]] = field.inner_text
21
+ end
22
+ end
23
+
24
+ def inspect #:nodoc:
25
+ content = self.map(){ |k, v| "%p => %p" % [k, v] }.join(", ")
26
+ return "\#<%p:{%s}>" % [self.class, content]
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,436 @@
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
+ # e.g.
116
+ # session.files
117
+ # session.files("title" => "hoge", "title-exact" => "true")
118
+ def files(params = {})
119
+ url = concat_url(
120
+ "https://docs.google.com/feeds/default/private/full?v=3", "?" + encode_query(params))
121
+ doc = request(:get, url, :auth => :writely)
122
+ return doc.css("feed > entry").map(){ |e| entry_element_to_file(e) }
123
+ end
124
+
125
+ # Returns GoogleDrive::File or its subclass whose title exactly matches +title+.
126
+ # Returns nil if not found. If multiple files with the +title+ are found, returns
127
+ # one of them.
128
+ def file_by_title(title)
129
+ return files("title" => title, "title-exact" => "true")[0]
130
+ end
131
+
132
+ # Returns list of spreadsheets for the user as array of GoogleDrive::Spreadsheet.
133
+ # You can specify query parameters described at
134
+ # http://code.google.com/apis/spreadsheets/docs/2.0/reference.html#Parameters
135
+ #
136
+ # e.g.
137
+ # session.spreadsheets
138
+ # session.spreadsheets("title" => "hoge")
139
+ def spreadsheets(params = {})
140
+ query = encode_query(params)
141
+ doc = request(
142
+ :get, "https://spreadsheets.google.com/feeds/spreadsheets/private/full?#{query}")
143
+ result = []
144
+ doc.css("feed > entry").each() do |entry|
145
+ title = entry.css("title").text
146
+ url = entry.css(
147
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
148
+ result.push(Spreadsheet.new(self, url, title))
149
+ end
150
+ return result
151
+ end
152
+
153
+ # Returns GoogleDrive::Spreadsheet with given +key+.
154
+ #
155
+ # e.g.
156
+ # # http://spreadsheets.google.com/ccc?key=pz7XtlQC-PYx-jrVMJErTcg&hl=ja
157
+ # session.spreadsheet_by_key("pz7XtlQC-PYx-jrVMJErTcg")
158
+ def spreadsheet_by_key(key)
159
+ url = "https://spreadsheets.google.com/feeds/worksheets/#{key}/private/full"
160
+ return Spreadsheet.new(self, url)
161
+ end
162
+
163
+ # Returns GoogleDrive::Spreadsheet with given +url+. You must specify either of:
164
+ # - URL of the page you open to access the spreadsheet in your browser
165
+ # - URL of worksheet-based feed of the spreadseet
166
+ #
167
+ # e.g.
168
+ # session.spreadsheet_by_url(
169
+ # "https://docs.google.com/spreadsheet/ccc?key=pz7XtlQC-PYx-jrVMJErTcg")
170
+ # session.spreadsheet_by_url(
171
+ # "https://spreadsheets.google.com/feeds/" +
172
+ # "worksheets/pz7XtlQC-PYx-jrVMJErTcg/private/full")
173
+ def spreadsheet_by_url(url)
174
+ # Tries to parse it as URL of human-readable spreadsheet.
175
+ uri = URI.parse(url)
176
+ if ["spreadsheets.google.com", "docs.google.com"].include?(uri.host) &&
177
+ uri.path =~ /\/ccc$/
178
+ if (uri.query || "").split(/&/).find(){ |s| s=~ /^key=(.*)$/ }
179
+ return spreadsheet_by_key($1)
180
+ end
181
+ end
182
+ # Assumes the URL is worksheets feed URL.
183
+ return Spreadsheet.new(self, url)
184
+ end
185
+
186
+ # Returns GoogleDrive::Spreadsheet with given +title+.
187
+ # Returns nil if not found. If multiple spreadsheets with the +title+ are found, returns
188
+ # one of them.
189
+ def spreadsheet_by_title(title)
190
+ return spreadsheets({"title" => title})[0]
191
+ end
192
+
193
+ # Returns GoogleDrive::Worksheet with given +url+.
194
+ # You must specify URL of cell-based feed of the worksheet.
195
+ #
196
+ # e.g.
197
+ # session.worksheet_by_url(
198
+ # "http://spreadsheets.google.com/feeds/" +
199
+ # "cells/pz7XtlQC-PYxNmbBVgyiNWg/od6/private/full")
200
+ def worksheet_by_url(url)
201
+ return Worksheet.new(self, nil, url)
202
+ end
203
+
204
+ # Returns GoogleDrive::Collection with given +url+.
205
+ # You must specify either of:
206
+ # - URL of the page you get when you go to https://docs.google.com/ with your browser and
207
+ # open a collection
208
+ # - URL of collection (folder) feed
209
+ #
210
+ # e.g.
211
+ # session.collection_by_url(
212
+ # "https://drive.google.com/#folders/" +
213
+ # "0B9GfDpQ2pBVUODNmOGE0NjIzMWU3ZC00NmUyLTk5NzEtYaFkZjY1MjAyxjMc")
214
+ # session.collection_by_url(
215
+ # "http://docs.google.com/feeds/default/private/full/folder%3A" +
216
+ # "0B9GfDpQ2pBVUODNmOGE0NjIzMWU3ZC00NmUyLTk5NzEtYaFkZjY1MjAyxjMc")
217
+ def collection_by_url(url)
218
+ uri = URI.parse(url)
219
+ if ["docs.google.com", "drive.google.com"].include?(uri.host) &&
220
+ uri.fragment =~ /^folders\/(.+)$/
221
+ # Looks like a URL of human-readable collection page. Converts to collection feed URL.
222
+ url = "https://docs.google.com/feeds/default/private/full/folder%3A#{$1}"
223
+ end
224
+ return Collection.new(self, url)
225
+ end
226
+
227
+ # Creates new spreadsheet and returns the new GoogleDrive::Spreadsheet.
228
+ #
229
+ # e.g.
230
+ # session.create_spreadsheet("My new sheet")
231
+ def create_spreadsheet(
232
+ title = "Untitled",
233
+ feed_url = "https://docs.google.com/feeds/documents/private/full")
234
+
235
+ xml = <<-"EOS"
236
+ <atom:entry
237
+ xmlns:atom="http://www.w3.org/2005/Atom"
238
+ xmlns:docs="http://schemas.google.com/docs/2007">
239
+ <atom:category
240
+ scheme="http://schemas.google.com/g/2005#kind"
241
+ term="http://schemas.google.com/docs/2007#spreadsheet"
242
+ label="spreadsheet"/>
243
+ <atom:title>#{h(title)}</atom:title>
244
+ </atom:entry>
245
+ EOS
246
+
247
+ doc = request(:post, feed_url, :data => xml, :auth => :writely)
248
+ ss_url = doc.css(
249
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]["href"]
250
+ return Spreadsheet.new(self, ss_url, title)
251
+
252
+ end
253
+
254
+ # Uploads a file with the given +title+ and +content+.
255
+ # Returns a GoogleSpreadsheet::File object.
256
+ #
257
+ # e.g.
258
+ # # Uploads and converts to a Google Docs document:
259
+ # session.upload_from_string(
260
+ # "Hello world.", "Hello", :content_type => "text/plain")
261
+ #
262
+ # # Uploads without conversion:
263
+ # session.upload_from_string(
264
+ # "Hello world.", "Hello", :content_type => "text/plain", :convert => false)
265
+ def upload_from_string(content, title = "Untitled", params = {})
266
+ return upload_from_io(StringIO.new(content), title, params)
267
+ end
268
+
269
+ # Uploads a local file.
270
+ # Returns a GoogleSpreadsheet::File object.
271
+ #
272
+ # e.g.
273
+ # # Uploads and converts to a Google Docs document:
274
+ # session.upload_from_file("/path/to/hoge.txt")
275
+ #
276
+ # # Uploads without conversion:
277
+ # session.upload_from_file("/path/to/hoge.txt", "Hoge", :convert => false)
278
+ #
279
+ # # Uploads with explicit content type:
280
+ # session.upload_from_file("/path/to/hoge", "Hoge", :content_type => "text/plain")
281
+ def upload_from_file(path, title = nil, params = {})
282
+ file_name = ::File.basename(path)
283
+ params = {:file_name => file_name}.merge(params)
284
+ open(path, "rb") do |f|
285
+ return upload_from_io(f, title || file_name, params)
286
+ end
287
+ end
288
+
289
+ # Uploads a file. Reads content from +io+.
290
+ # Returns a GoogleSpreadsheet::File object.
291
+ def upload_from_io(io, title = "Untitled", params = {})
292
+ doc = request(:get, "https://docs.google.com/feeds/default/private/full?v=3",
293
+ :auth => :writely)
294
+ initial_url = doc.css(
295
+ "link[rel='http://schemas.google.com/g/2005#resumable-create-media']")[0]["href"]
296
+ return upload_raw(:post, initial_url, io, title, params)
297
+ end
298
+
299
+ def upload_raw(method, url, io, title = "Untitled", params = {}) #:nodoc:
300
+
301
+ params = {:convert => true}.merge(params)
302
+ pos = io.pos
303
+ io.seek(0, IO::SEEK_END)
304
+ total_bytes = io.pos - pos
305
+ io.pos = pos
306
+ content_type = params[:content_type]
307
+ if !content_type && params[:file_name]
308
+ content_type = EXT_TO_CONTENT_TYPE[::File.extname(params[:file_name]).downcase]
309
+ end
310
+ if !content_type
311
+ content_type = "application/octet-stream"
312
+ end
313
+
314
+ initial_xml = <<-"EOS"
315
+ <entry xmlns="http://www.w3.org/2005/Atom"
316
+ xmlns:docs="http://schemas.google.com/docs/2007">
317
+ <title>#{h(title)}</title>
318
+ </entry>
319
+ EOS
320
+
321
+ default_initial_header = {
322
+ "Content-Type" => "application/atom+xml",
323
+ "X-Upload-Content-Type" => content_type,
324
+ "X-Upload-Content-Length" => total_bytes.to_s(),
325
+ }
326
+ initial_full_url = concat_url(url, params[:convert] ? "?convert=true" : "?convert=false")
327
+ initial_response = request(method, initial_full_url,
328
+ :header => default_initial_header.merge(params[:header] || {}),
329
+ :data => initial_xml,
330
+ :auth => :writely,
331
+ :response_type => :response)
332
+ upload_url = initial_response["location"]
333
+
334
+ sent_bytes = 0
335
+ while data = io.read(UPLOAD_CHUNK_SIZE)
336
+ content_range = "bytes %d-%d/%d" % [
337
+ sent_bytes,
338
+ sent_bytes + data.bytesize - 1,
339
+ total_bytes,
340
+ ]
341
+ upload_header = {
342
+ "Content-Type" => content_type,
343
+ "Content-Range" => content_range,
344
+ }
345
+ doc = request(
346
+ :put, upload_url, :header => upload_header, :data => data, :auth => :writely)
347
+ sent_bytes += data.bytesize
348
+ end
349
+
350
+ return entry_element_to_file(doc.root)
351
+
352
+ end
353
+
354
+ def entry_element_to_file(entry) #:nodoc:
355
+ title = entry.css("title").text
356
+ worksheets_feed_link = entry.css(
357
+ "link[rel='http://schemas.google.com/spreadsheets/2006#worksheetsfeed']")[0]
358
+ if worksheets_feed_link
359
+ return Spreadsheet.new(self, worksheets_feed_link["href"], title)
360
+ else
361
+ return GoogleDrive::File.new(self, entry)
362
+ end
363
+ end
364
+
365
+ def request(method, url, params = {}) #:nodoc:
366
+
367
+ # Always uses HTTPS.
368
+ url = url.gsub(%r{^http://}, "https://")
369
+ data = params[:data]
370
+ auth = params[:auth] || :wise
371
+ if params[:header]
372
+ extra_header = params[:header]
373
+ elsif data
374
+ extra_header = {"Content-Type" => "application/atom+xml"}
375
+ else
376
+ extra_header = {}
377
+ end
378
+ response_type = params[:response_type] || :xml
379
+
380
+ while true
381
+ response = @fetcher.request_raw(method, url, data, extra_header, auth)
382
+ if response.code == "401" && @on_auth_fail && @on_auth_fail.call()
383
+ next
384
+ end
385
+ if !(response.code =~ /^[23]/)
386
+ raise(
387
+ response.code == "401" ? AuthenticationError : GoogleDrive::Error,
388
+ "Response code #{response.code} for #{method} #{url}: " +
389
+ CGI.unescapeHTML(response.body))
390
+ end
391
+ return convert_response(response, response_type)
392
+ end
393
+
394
+ end
395
+
396
+ def inspect
397
+ return "#<%p:0x%x>" % [self.class, self.object_id]
398
+ end
399
+
400
+ private
401
+
402
+ def convert_response(response, response_type)
403
+ case response_type
404
+ when :xml
405
+ return Nokogiri.XML(response.body)
406
+ when :raw
407
+ return response.body
408
+ when :response
409
+ return response
410
+ else
411
+ raise(GoogleDrive::Error,
412
+ "Unknown params[:response_type]: %s" % response_type)
413
+ end
414
+ end
415
+
416
+ def authenticate(mail, password, auth)
417
+ params = {
418
+ "accountType" => "HOSTED_OR_GOOGLE",
419
+ "Email" => mail,
420
+ "Passwd" => password,
421
+ "service" => auth.to_s(),
422
+ "source" => "Gimite-RubyGoogleDrive-1.00",
423
+ }
424
+ header = {"Content-Type" => "application/x-www-form-urlencoded"}
425
+ response = request(:post,
426
+ "https://www.google.com/accounts/ClientLogin",
427
+ :data => encode_query(params),
428
+ :auth => :none,
429
+ :header => header,
430
+ :response_type => :raw)
431
+ return response.slice(/^Auth=(.*)$/, 1)
432
+ end
433
+
434
+ end
435
+
436
+ end