google_drive 0.3.0

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