google_drive 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +89 -0
- data/doc_src/google_drive/acl.rb +20 -0
- data/doc_src/google_drive/acl_entry.rb +33 -0
- data/lib/google_drive.rb +120 -0
- data/lib/google_drive/acl.rb +124 -0
- data/lib/google_drive/acl_entry.rb +58 -0
- data/lib/google_drive/authentication_error.rb +14 -0
- data/lib/google_drive/client_login_fetcher.rb +56 -0
- data/lib/google_drive/collection.rb +54 -0
- data/lib/google_drive/error.rb +12 -0
- data/lib/google_drive/file.rb +217 -0
- data/lib/google_drive/list.rb +115 -0
- data/lib/google_drive/list_row.rb +84 -0
- data/lib/google_drive/oauth1_fetcher.rb +26 -0
- data/lib/google_drive/oauth2_fetcher.rb +47 -0
- data/lib/google_drive/record.rb +31 -0
- data/lib/google_drive/session.rb +436 -0
- data/lib/google_drive/spreadsheet.rb +220 -0
- data/lib/google_drive/table.rb +60 -0
- data/lib/google_drive/util.rb +55 -0
- data/lib/google_drive/worksheet.rb +445 -0
- metadata +121 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
|
+
# The license of this source is "New BSD Licence"
|
3
|
+
|
4
|
+
require "google_drive/error"
|
5
|
+
|
6
|
+
|
7
|
+
module GoogleDrive
|
8
|
+
|
9
|
+
# Raised when GoogleDrive.login has failed.
|
10
|
+
class AuthenticationError < GoogleDrive::Error
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
|
+
# The license of this source is "New BSD Licence"
|
3
|
+
|
4
|
+
require "net/https"
|
5
|
+
require "uri"
|
6
|
+
Net::HTTP.version_1_2
|
7
|
+
|
8
|
+
|
9
|
+
module GoogleDrive
|
10
|
+
|
11
|
+
class ClientLoginFetcher #:nodoc:
|
12
|
+
|
13
|
+
def initialize(auth_tokens, proxy)
|
14
|
+
@auth_tokens = auth_tokens
|
15
|
+
if proxy
|
16
|
+
@proxy = proxy
|
17
|
+
elsif ENV["http_proxy"] && !ENV["http_proxy"].empty?
|
18
|
+
proxy_url = URI.parse(ENV["http_proxy"])
|
19
|
+
@proxy = Net::HTTP.Proxy(proxy_url.host, proxy_url.port)
|
20
|
+
else
|
21
|
+
@proxy = Net::HTTP
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_accessor(:auth_tokens)
|
26
|
+
|
27
|
+
def request_raw(method, url, data, extra_header, auth)
|
28
|
+
uri = URI.parse(url)
|
29
|
+
http = @proxy.new(uri.host, uri.port)
|
30
|
+
http.use_ssl = true
|
31
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
32
|
+
http.start() do
|
33
|
+
path = uri.path + (uri.query ? "?#{uri.query}" : "")
|
34
|
+
header = auth_header(auth).merge(extra_header)
|
35
|
+
if method == :delete || method == :get
|
36
|
+
return http.__send__(method, path, header)
|
37
|
+
else
|
38
|
+
return http.__send__(method, path, data, header)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def auth_header(auth)
|
46
|
+
token = auth == :none ? nil : @auth_tokens[auth]
|
47
|
+
if token
|
48
|
+
return {"Authorization" => "GoogleLogin auth=#{token}"}
|
49
|
+
else
|
50
|
+
return {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,54 @@
|
|
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
|
+
require "google_drive/spreadsheet"
|
7
|
+
|
8
|
+
|
9
|
+
module GoogleDrive
|
10
|
+
|
11
|
+
# Use GoogleDrive::Session#collection_by_url to get GoogleDrive::Collection object.
|
12
|
+
class Collection
|
13
|
+
|
14
|
+
include(Util)
|
15
|
+
|
16
|
+
def initialize(session, collection_feed_url) #:nodoc:
|
17
|
+
@session = session
|
18
|
+
@collection_feed_url = collection_feed_url
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader(:collection_feed_url)
|
22
|
+
|
23
|
+
# Adds the given GoogleDrive::File to the collection.
|
24
|
+
def add(file)
|
25
|
+
contents_url = concat_url(@collection_feed_url, "/contents")
|
26
|
+
header = {"GData-Version" => "3.0", "Content-Type" => "application/atom+xml"}
|
27
|
+
xml = <<-"EOS"
|
28
|
+
<entry xmlns="http://www.w3.org/2005/Atom">
|
29
|
+
<id>#{h(file.document_feed_url)}</id>
|
30
|
+
</entry>
|
31
|
+
EOS
|
32
|
+
@session.request(
|
33
|
+
:post, contents_url, :data => xml, :header => header, :auth => :writely)
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns all the files in the collection.
|
38
|
+
def files
|
39
|
+
contents_url = concat_url(@collection_feed_url, "/contents")
|
40
|
+
header = {"GData-Version" => "3.0", "Content-Type" => "application/atom+xml"}
|
41
|
+
doc = @session.request(:get, contents_url, :header => header, :auth => :writely)
|
42
|
+
return doc.css("feed > entry").map(){ |e| @session.entry_element_to_file(e) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns all the spreadsheets in the collection.
|
46
|
+
def spreadsheets
|
47
|
+
return self.files.select(){ |f| f.is_a?(Spreadsheet) }
|
48
|
+
end
|
49
|
+
|
50
|
+
# TODO Add other operations.
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# Author: Hiroshi Ichikawa <http://gimite.net/>
|
2
|
+
# The license of this source is "New BSD Licence"
|
3
|
+
|
4
|
+
require "stringio"
|
5
|
+
|
6
|
+
require "google_drive/util"
|
7
|
+
require "google_drive/acl"
|
8
|
+
|
9
|
+
|
10
|
+
module GoogleDrive
|
11
|
+
|
12
|
+
# A file in Google Drive, including Google Docs document/spreadsheet/presentation.
|
13
|
+
#
|
14
|
+
# Use GoogleDrive::Session#files or GoogleDrive::Session#file_by_title to
|
15
|
+
# get this object.
|
16
|
+
class File
|
17
|
+
|
18
|
+
include(Util)
|
19
|
+
|
20
|
+
def initialize(session, entry) #:nodoc:
|
21
|
+
@session = session
|
22
|
+
@document_feed_entry = entry
|
23
|
+
@document_feed_url = entry ? entry.css("link[rel='self']")[0]["href"] : nil
|
24
|
+
@title = entry ? entry.css("title")[0].text : nil
|
25
|
+
@acl = nil
|
26
|
+
end
|
27
|
+
|
28
|
+
# URL of feed used in document list feed API.
|
29
|
+
attr_reader(:document_feed_url)
|
30
|
+
|
31
|
+
# <entry> element of document list feed as Nokogiri::XML::Element.
|
32
|
+
attr_reader(:document_feed_entry)
|
33
|
+
|
34
|
+
# Title of the file.
|
35
|
+
attr_reader(:title)
|
36
|
+
|
37
|
+
# URL to view/edit the file in a Web browser.
|
38
|
+
#
|
39
|
+
# e.g. "https://docs.google.com/file/d/xxxx/edit"
|
40
|
+
def human_url
|
41
|
+
return self.document_feed_entry.css("link[rel='alternate']")[0]["href"]
|
42
|
+
end
|
43
|
+
|
44
|
+
# ACL feed URL of the file.
|
45
|
+
def acl_feed_url
|
46
|
+
orig_acl_feed_url = self.document_feed_entry.css(
|
47
|
+
"gd|feedLink[rel='http://schemas.google.com/acl/2007#accessControlList']")[0]["href"]
|
48
|
+
case orig_acl_feed_url
|
49
|
+
when %r{^https?://docs.google.com/feeds/default/private/full/.*/acl(\?.*)?$}
|
50
|
+
return orig_acl_feed_url
|
51
|
+
when %r{^https?://docs.google.com/feeds/acl/private/full/([^\?]*)(\?.*)?$}
|
52
|
+
# URL of old API version. Converts to v3 URL.
|
53
|
+
return "https://docs.google.com/feeds/default/private/full/#{$1}/acl"
|
54
|
+
else
|
55
|
+
raise(GoogleDrive::Error,
|
56
|
+
"ACL feed URL is in unknown format: #{orig_acl_feed_url}")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Content types you can specify in methods download_to_file, download_to_string,
|
61
|
+
# download_to_io .
|
62
|
+
def available_content_types
|
63
|
+
return self.document_feed_entry.css("content").map(){ |c| c["type"] }
|
64
|
+
end
|
65
|
+
|
66
|
+
# Downloads the file to a local file.
|
67
|
+
#
|
68
|
+
# e.g.
|
69
|
+
# file.download_to_file("/path/to/hoge.txt")
|
70
|
+
# file.download_to_file("/path/to/hoge", :content_type => "text/plain")
|
71
|
+
def download_to_file(path, params = {})
|
72
|
+
params = params.dup()
|
73
|
+
if !params[:content_type]
|
74
|
+
params[:content_type] = EXT_TO_CONTENT_TYPE[::File.extname(path).downcase]
|
75
|
+
params[:content_type_is_hint] = true
|
76
|
+
end
|
77
|
+
open(path, "wb") do |f|
|
78
|
+
download_to_io(f, params)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Downloads the file and returns as a String.
|
83
|
+
#
|
84
|
+
# e.g.
|
85
|
+
# file.download_to_string() #=> "Hello world."
|
86
|
+
# file.download_to_string(:content_type => "text/plain") #=> "Hello world."
|
87
|
+
def download_to_string(params = {})
|
88
|
+
sio = StringIO.new()
|
89
|
+
download_to_io(sio, params)
|
90
|
+
return sio.string
|
91
|
+
end
|
92
|
+
|
93
|
+
# Downloads the file and writes it to +io+.
|
94
|
+
def download_to_io(io, params = {})
|
95
|
+
all_contents = self.document_feed_entry.css("content")
|
96
|
+
if params[:content_type] && (!params[:content_type_is_hint] || all_contents.size > 1)
|
97
|
+
contents = all_contents.select(){ |c| c["type"] == params[:content_type] }
|
98
|
+
else
|
99
|
+
contents = all_contents
|
100
|
+
end
|
101
|
+
if contents.size == 1
|
102
|
+
url = contents[0]["src"]
|
103
|
+
else
|
104
|
+
if contents.empty?
|
105
|
+
raise(GoogleDrive::Error,
|
106
|
+
("Downloading with content type %p not supported for this file. " +
|
107
|
+
"Specify one of these to content_type: %p") %
|
108
|
+
[params[:content_type], self.available_content_types])
|
109
|
+
else
|
110
|
+
raise(GoogleDrive::Error,
|
111
|
+
("Multiple content types are available for this file. " +
|
112
|
+
"Specify one of these to content_type: %p") %
|
113
|
+
[self.available_content_types])
|
114
|
+
end
|
115
|
+
end
|
116
|
+
# TODO Use streaming if possible.
|
117
|
+
body = @session.request(:get, url, :response_type => :raw, :auth => :writely)
|
118
|
+
io.write(body)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Updates the file with the content of the local file.
|
122
|
+
#
|
123
|
+
# e.g.
|
124
|
+
# file.update_from_file("/path/to/hoge.txt")
|
125
|
+
def update_from_file(path, params = {})
|
126
|
+
params = {:file_name => ::File.basename(path)}.merge(params)
|
127
|
+
open(path, "rb") do |f|
|
128
|
+
update_from_io(f, params)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Updates the file with +content+.
|
133
|
+
#
|
134
|
+
# e.g.
|
135
|
+
# file.update_from_string("Good bye, world.")
|
136
|
+
def update_from_string(content, params = {})
|
137
|
+
update_from_io(StringIO.new(content), params)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Reads content from +io+ and updates the file with the content.
|
141
|
+
def update_from_io(io, params = {})
|
142
|
+
params = {:header => {"If-Match" => "*"}}.merge(params)
|
143
|
+
initial_url = self.document_feed_entry.css(
|
144
|
+
"link[rel='http://schemas.google.com/g/2005#resumable-edit-media']")[0]["href"]
|
145
|
+
@document_feed_entry = @session.upload_raw(
|
146
|
+
:put, initial_url, io, self.title, params)
|
147
|
+
end
|
148
|
+
|
149
|
+
# If +permanent+ is +false+, moves the file to the trash.
|
150
|
+
# If +permanent+ is +true+, deletes the file permanently.
|
151
|
+
def delete(permanent = false)
|
152
|
+
@session.request(:delete,
|
153
|
+
self.document_feed_url + (permanent ? "?delete=true" : ""),
|
154
|
+
:auth => :writely, :header => {"If-Match" => "*"})
|
155
|
+
end
|
156
|
+
|
157
|
+
# Renames title of the file.
|
158
|
+
def rename(title)
|
159
|
+
|
160
|
+
doc = @session.request(:get, self.document_feed_url, :auth => :writely)
|
161
|
+
edit_url = doc.css("link[rel='edit']").first["href"]
|
162
|
+
xml = <<-"EOS"
|
163
|
+
<atom:entry
|
164
|
+
xmlns:atom="http://www.w3.org/2005/Atom"
|
165
|
+
xmlns:docs="http://schemas.google.com/docs/2007">
|
166
|
+
<atom:title>#{h(title)}</atom:title>
|
167
|
+
</atom:entry>
|
168
|
+
EOS
|
169
|
+
|
170
|
+
@session.request(
|
171
|
+
:put, edit_url, :data => xml, :auth => :writely,
|
172
|
+
:header => {"Content-Type" => "application/atom+xml", "If-Match" => "*"})
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
alias title= rename
|
177
|
+
|
178
|
+
# Returns GoogleDrive::Acl object for the file.
|
179
|
+
#
|
180
|
+
# With the object, you can see and modify people who can access the file.
|
181
|
+
# Modifications take effect immediately.
|
182
|
+
#
|
183
|
+
# Set <tt>params[:reload]</tt> to true to force reloading the title.
|
184
|
+
#
|
185
|
+
# e.g.
|
186
|
+
# # Dumps people who have access:
|
187
|
+
# for entry in file.acl
|
188
|
+
# p [entry.scope_type, entry.scope, entry.role]
|
189
|
+
# # => e.g. ["user", "example1@gmail.com", "owner"]
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# # Shares the file with new people:
|
193
|
+
# # NOTE: This sends email to the new people.
|
194
|
+
# file.acl.push(
|
195
|
+
# {:scope_type => "user", :scope => "example2@gmail.com", :role => "reader"})
|
196
|
+
# file.acl.push(
|
197
|
+
# {:scope_type => "user", :scope => "example3@gmail.com", :role => "writer"})
|
198
|
+
#
|
199
|
+
# # Changes the role of a person:
|
200
|
+
# file.acl[1].role = "writer"
|
201
|
+
#
|
202
|
+
# # Deletes an ACL entry:
|
203
|
+
# file.acl.delete(file.acl[1])
|
204
|
+
def acl(params = {})
|
205
|
+
if !@acl || params[:reload]
|
206
|
+
@acl = Acl.new(@session, self.acl_feed_url)
|
207
|
+
end
|
208
|
+
return @acl
|
209
|
+
end
|
210
|
+
|
211
|
+
def inspect
|
212
|
+
return "\#<%p document_feed_url=%p>" % [self.class, self.document_feed_url]
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
@@ -0,0 +1,115 @@
|
|
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
|
+
require "google_drive/list_row"
|
7
|
+
|
8
|
+
|
9
|
+
module GoogleDrive
|
10
|
+
|
11
|
+
# Provides access to cells using column names.
|
12
|
+
# Use GoogleDrive::Worksheet#list to get GoogleDrive::List object.
|
13
|
+
#--
|
14
|
+
# This is implemented as wrapper of GoogleDrive::Worksheet i.e. using cells
|
15
|
+
# feed, not list feed. In this way, we can easily provide consistent API as
|
16
|
+
# GoogleDrive::Worksheet using save()/reload().
|
17
|
+
class List
|
18
|
+
|
19
|
+
include(Enumerable)
|
20
|
+
|
21
|
+
def initialize(worksheet) #:nodoc:
|
22
|
+
@worksheet = worksheet
|
23
|
+
end
|
24
|
+
|
25
|
+
# Number of non-empty rows in the worksheet excluding the first row.
|
26
|
+
def size
|
27
|
+
return @worksheet.num_rows - 1
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns Hash-like object (GoogleDrive::ListRow) for the row with the
|
31
|
+
# index. Keys of the object are colum names (the first row).
|
32
|
+
# The second row has index 0.
|
33
|
+
#
|
34
|
+
# Note that updates to the returned object are not sent to the server until
|
35
|
+
# you call GoogleDrive::Worksheet#save().
|
36
|
+
def [](index)
|
37
|
+
return ListRow.new(self, index)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Updates the row with the index with the given Hash object.
|
41
|
+
# Keys of +hash+ are colum names (the first row).
|
42
|
+
# The second row has index 0.
|
43
|
+
#
|
44
|
+
# Note that update is not sent to the server until
|
45
|
+
# you call GoogleDrive::Worksheet#save().
|
46
|
+
def []=(index, hash)
|
47
|
+
self[index].replace(hash)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterates over Hash-like object (GoogleDrive::ListRow) for each row
|
51
|
+
# (except for the first row).
|
52
|
+
# Keys of the object are colum names (the first row).
|
53
|
+
def each(&block)
|
54
|
+
for i in 0...self.size
|
55
|
+
yield(self[i])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Column names i.e. the contents of the first row.
|
60
|
+
# Duplicates are removed.
|
61
|
+
def keys
|
62
|
+
return (1..@worksheet.num_cols).map(){ |i| @worksheet[1, i] }.uniq()
|
63
|
+
end
|
64
|
+
|
65
|
+
# Updates column names i.e. the contents of the first row.
|
66
|
+
#
|
67
|
+
# Note that update is not sent to the server until
|
68
|
+
# you call GoogleDrive::Worksheet#save().
|
69
|
+
def keys=(ary)
|
70
|
+
for i in 1..ary.size
|
71
|
+
@worksheet[1, i] = ary[i - 1]
|
72
|
+
end
|
73
|
+
for i in (ary.size + 1)..@worksheet.num_cols
|
74
|
+
@worksheet[1, i] = ""
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Adds a new row to the bottom.
|
79
|
+
# Keys of +hash+ are colum names (the first row).
|
80
|
+
# Returns GoogleDrive::ListRow for the new row.
|
81
|
+
#
|
82
|
+
# Note that update is not sent to the server until
|
83
|
+
# you call GoogleDrive::Worksheet#save().
|
84
|
+
def push(hash)
|
85
|
+
row = self[self.size]
|
86
|
+
row.update(hash)
|
87
|
+
return row
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns all rows (except for the first row) as Array of Hash.
|
91
|
+
# Keys of Hash objects are colum names (the first row).
|
92
|
+
def to_hash_array()
|
93
|
+
return self.map(){ |r| r.to_hash() }
|
94
|
+
end
|
95
|
+
|
96
|
+
def get(index, key) #:nodoc:
|
97
|
+
return @worksheet[index + 2, key_to_col(key)]
|
98
|
+
end
|
99
|
+
|
100
|
+
def set(index, key, value) #:nodoc:
|
101
|
+
@worksheet[index + 2, key_to_col(key)] = value
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def key_to_col(key)
|
107
|
+
key = key.to_s()
|
108
|
+
col = (1..@worksheet.num_cols).find(){ |c| @worksheet[1, c] == key }
|
109
|
+
raise(GoogleDrive::Error, "Colunm doesn't exist: %p" % key) if !col
|
110
|
+
return col
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|