dewey 0.1.2
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.
- data/CHANGELOG.md +13 -0
- data/README.md +32 -0
- data/TODO.md +4 -0
- data/lib/dewey.rb +232 -0
- data/lib/dewey/https.rb +11 -0
- data/lib/dewey/mime.rb +84 -0
- data/lib/dewey/utils.rb +19 -0
- metadata +85 -0
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
Dewey allows you to simply upload, download and delete files from your Google
|
2
|
+
Docs account. Let Google do all of the hard work of converting your documents!
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
|
6
|
+
First, create a new Dewey::Document instance using your google docs account and
|
7
|
+
password.
|
8
|
+
|
9
|
+
dewey = Dewey::Document.new(:account => 'example.account', :password => 'example')
|
10
|
+
|
11
|
+
Next, choose a local document to upload or convert.
|
12
|
+
|
13
|
+
document = File.new('my_document.doc')
|
14
|
+
spreadsheet = File.new('my_spreadsheet.xls')
|
15
|
+
|
16
|
+
Finally, handle the upload, download, and delete manually or use the convenient
|
17
|
+
`convert` method.
|
18
|
+
|
19
|
+
dewey.convert(document, :html)
|
20
|
+
dewey.convert(spreadsheet, :csv)
|
21
|
+
|
22
|
+
## Testing
|
23
|
+
|
24
|
+
Until testing is converted to an offline solution you will have to provide some
|
25
|
+
credentials for testing. All that is required is a valid Google Docs account, no
|
26
|
+
fussing about with getting an application API.
|
27
|
+
|
28
|
+
By default the spec will look for a file called dewey.yml inside of the spec
|
29
|
+
folder. The file should contain two lines:
|
30
|
+
|
31
|
+
email: <your gmail account>
|
32
|
+
password: <your gmail password>
|
data/TODO.md
ADDED
data/lib/dewey.rb
ADDED
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/https'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'dewey/https'
|
7
|
+
require 'dewey/mime'
|
8
|
+
require 'dewey/utils'
|
9
|
+
|
10
|
+
module Dewey
|
11
|
+
GOOGLE_BASE_URL = "https://www.google.com"
|
12
|
+
GOOGLE_DOCS_URL = "https://docs.google.com"
|
13
|
+
GOOGLE_SPRD_URL = "https://spreadsheets.google.com"
|
14
|
+
GOOGLE_LOGIN_URL = GOOGLE_BASE_URL + "/accounts/ClientLogin"
|
15
|
+
GOOGLE_FEED_URL = GOOGLE_DOCS_URL + "/feeds/default/private/full"
|
16
|
+
|
17
|
+
GOOGLE_DOCUMENT_URL = GOOGLE_DOCS_URL + "/feeds/download/documents/Export"
|
18
|
+
GOOGLE_SPREADSHEET_URL = GOOGLE_SPRD_URL + "/feeds/download/spreadsheets/Export"
|
19
|
+
|
20
|
+
GOOGLE_SERVICES = { :document => 'writely', :spreadsheet => 'wise' }
|
21
|
+
|
22
|
+
DOCUMENT_EXPORT_FORMATS = %w(txt odt pdf html rtf doc png zip)
|
23
|
+
PRESENTATION_EXPORT_FORMATS = %w(swf pdf png ppt)
|
24
|
+
SPREADSHEET_EXPORT_FORMATS = %W(xls csv pdf ods tsv html)
|
25
|
+
|
26
|
+
class DeweyException < Exception
|
27
|
+
end
|
28
|
+
|
29
|
+
# Doc
|
30
|
+
# This base class handles authentication and requests
|
31
|
+
#
|
32
|
+
class Document
|
33
|
+
attr_accessor :account, :password, :service, :token, :cached
|
34
|
+
|
35
|
+
# Determine wether or not a format is available for download.
|
36
|
+
#
|
37
|
+
# * format - The file format to check, i.e. 'txt', 'doc', 'pdf'
|
38
|
+
# * service - The service that would be used. Must be document, presentation,
|
39
|
+
# or spreadsheet.
|
40
|
+
def self.valid_upload_format?(format, service = :document)
|
41
|
+
case service
|
42
|
+
when :document then Dewey::DOCUMENT_MIMETYPES.has_key?(format.to_sym)
|
43
|
+
when :presentation then Dewey::PRESENTATION_MIMETYPES.has_key?(format.to_sym)
|
44
|
+
when :spreadsheet then Dewey::SPREADSHEET_MIMETYPES.has_key?(format.to_sym)
|
45
|
+
else
|
46
|
+
raise DeweyException, "Unknown service: #{service}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Determine whether or not a format is available for export.
|
51
|
+
#
|
52
|
+
# * format - The file format to check, i.e. 'txt', 'doc', 'pdf'
|
53
|
+
# * service - The service that would be used. Must be document, presentation,
|
54
|
+
# or spreadsheet.
|
55
|
+
def self.valid_export_format?(format, service = :document)
|
56
|
+
case service
|
57
|
+
when :document then DOCUMENT_EXPORT_FORMATS.include?(format)
|
58
|
+
when :presentation then PRESENTATION_EXPORT_FORMATS.include?(format)
|
59
|
+
when :spreadsheet then SPREADSHEET_EXPORT_FORMATS.include?(format)
|
60
|
+
else
|
61
|
+
raise DeweyException, "Unknown service: #{service}"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Create a new Doc object
|
66
|
+
# Options specified in +opts+ consist of:
|
67
|
+
#
|
68
|
+
# * :account - The Google Doc's account that will be used for authentication.
|
69
|
+
# This will most typically be a gmail account, i.e. +example@gmail.com+
|
70
|
+
# * :password - The password for the Google Doc's account.
|
71
|
+
# * :service - The default service to connect to, either :document or :spreadsheet
|
72
|
+
def initialize(options = {})
|
73
|
+
@account = options[:account]
|
74
|
+
@password = options[:password]
|
75
|
+
@service = options[:service] || :document
|
76
|
+
@cached = {}
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns true if this instance has been authorized
|
80
|
+
def authorized?
|
81
|
+
!! @token
|
82
|
+
end
|
83
|
+
|
84
|
+
# Gets an authorization token for this instance. Raises an error if no
|
85
|
+
# credentials have been provided, +false+ if authorization fails, and +true+ if
|
86
|
+
# authorization is successful.
|
87
|
+
def authorize!
|
88
|
+
if @account.nil? || @password.nil?
|
89
|
+
raise DeweyException, "Account or password missing."
|
90
|
+
end
|
91
|
+
|
92
|
+
url = URI.parse(GOOGLE_LOGIN_URL)
|
93
|
+
params = { 'accountType' => 'HOSTED_OR_GOOGLE', 'Email' => @account,
|
94
|
+
'Passwd' => @password, 'service'=> GOOGLE_SERVICES[@service] }
|
95
|
+
|
96
|
+
response = Net::HTTPS.post_form(url, params)
|
97
|
+
|
98
|
+
case response
|
99
|
+
when Net::HTTPSuccess
|
100
|
+
@token = response.body.split(/=/).last
|
101
|
+
true
|
102
|
+
when Net::HTTPForbidden
|
103
|
+
false
|
104
|
+
else
|
105
|
+
raise DeweyException, "Unexpected response: #{response}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def upload(file, title = nil)
|
110
|
+
extension = File.extname(file.path).sub('.', '')
|
111
|
+
basename = File.basename(file.path, ".#{extension}")
|
112
|
+
mimetype = Dewey::Mime.mime_type(file)
|
113
|
+
service = Dewey::Mime.guess_service(mimetype)
|
114
|
+
|
115
|
+
title ||= basename
|
116
|
+
|
117
|
+
raise DeweyException, "Invalid file type: #{extension}" unless Dewey::Document.valid_upload_format?(extension, service)
|
118
|
+
|
119
|
+
headers = base_headers
|
120
|
+
headers['Content-Length'] = File.size?(file).to_s
|
121
|
+
headers['Content-Type'] = mimetype
|
122
|
+
headers['Slug'] = Dewey::Utils.escape(title)
|
123
|
+
|
124
|
+
# Rewind the file in the case of multiple uploads, or conversions
|
125
|
+
file.rewind
|
126
|
+
|
127
|
+
response = post_request(GOOGLE_FEED_URL, file.read.to_s, headers)
|
128
|
+
|
129
|
+
case response
|
130
|
+
when Net::HTTPCreated
|
131
|
+
rid = @cached[basename] = extract_rid(response.body)
|
132
|
+
else
|
133
|
+
nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Download, or export more accurately, a file to a specified format
|
138
|
+
# * id - A resource id or exact file name matching a document in the account
|
139
|
+
# * format - The output format, see *_EXPORT_FORMATS for possiblibilites
|
140
|
+
def download(id, format = nil)
|
141
|
+
spreadsheet = !! id.match(/spreadsheet/)
|
142
|
+
|
143
|
+
url = (spreadsheet ? GOOGLE_SPREADSHEET_URL : GOOGLE_DOCUMENT_URL)
|
144
|
+
url << (spreadsheet ? "?key=#{id}" : "?docID=#{id}")
|
145
|
+
url << "&exportFormat=#{format.to_s}" unless format.nil?
|
146
|
+
|
147
|
+
headers = base_headers
|
148
|
+
|
149
|
+
file = Tempfile.new([id, format].join('.'))
|
150
|
+
file.binmode
|
151
|
+
|
152
|
+
open(url, headers) { |data| file.write data.read }
|
153
|
+
|
154
|
+
file
|
155
|
+
end
|
156
|
+
|
157
|
+
# Deletes a document referenced either by resource id or by name.
|
158
|
+
def delete(id)
|
159
|
+
headers = base_headers
|
160
|
+
headers['If-Match'] = '*' # We don't care if others have modified
|
161
|
+
|
162
|
+
url = GOOGLE_FEED_URL + "/#{Dewey::Utils.escape(id)}?delete=true"
|
163
|
+
response = delete_request(url, headers)
|
164
|
+
|
165
|
+
case response
|
166
|
+
when Net::HTTPOK
|
167
|
+
true
|
168
|
+
else
|
169
|
+
false
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Convenience method for +upload+, +download+, +delete+. Returns a Tempfile
|
174
|
+
# with in the provided type.
|
175
|
+
# * file - The file that will be converted
|
176
|
+
# * options - Takes :title and :format. See +upload+ for title, and +download+
|
177
|
+
# for format.
|
178
|
+
def convert(file, options = {})
|
179
|
+
rid = upload(file, options[:title])
|
180
|
+
con = download(rid, options[:format])
|
181
|
+
|
182
|
+
delete(rid)
|
183
|
+
|
184
|
+
con
|
185
|
+
end
|
186
|
+
|
187
|
+
private
|
188
|
+
|
189
|
+
def post_request(url, data, headers) #:nodoc:
|
190
|
+
http_request(:post, url, headers, data)
|
191
|
+
end
|
192
|
+
|
193
|
+
def delete_request(url, headers) #:nodoc:
|
194
|
+
http_request(:delete, url, headers)
|
195
|
+
end
|
196
|
+
|
197
|
+
def http_request(method, url, headers, data = nil) #:nodoc:
|
198
|
+
url = URI.parse(url) if url.kind_of? String
|
199
|
+
|
200
|
+
connection = Net::HTTP.new(url.host, url.port)
|
201
|
+
connection.use_ssl = (url.scheme == 'https')
|
202
|
+
|
203
|
+
case method
|
204
|
+
when :post
|
205
|
+
connection.post(url.path, data, headers)
|
206
|
+
when :delete
|
207
|
+
connection.delete(url.path, headers)
|
208
|
+
else
|
209
|
+
raise DeweyException, "Invalid request type. Valid options are :post and :delete"
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def base_headers #:nodoc:
|
214
|
+
base = {}
|
215
|
+
base['GData-Version'] = '3.0'
|
216
|
+
base['Content-Type'] = 'application/x-www-form-urlencoded'
|
217
|
+
base['Authorization'] = "GoogleLogin auth=#{@token}" if authorized?
|
218
|
+
|
219
|
+
base
|
220
|
+
end
|
221
|
+
|
222
|
+
def extract_rid(source) #:nodoc:
|
223
|
+
xml = REXML::Document.new(source)
|
224
|
+
|
225
|
+
begin
|
226
|
+
"#{$1}:#{$2}" if xml.elements['//id'].text =~ /.+(document|spreadsheet|presentation)%3A([0-9a-zA-Z]+)/
|
227
|
+
rescue
|
228
|
+
raise DeweyException, "id could not be extracted from: #{body}"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
data/lib/dewey/https.rb
ADDED
data/lib/dewey/mime.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# (c) Copyright 2010 Parker Selbert <parker@sorentwo.com>
|
2
|
+
#
|
3
|
+
# Dewey is freely distributable under the terms of an MIT-style license.
|
4
|
+
# See LICENSE or http://www.opensource.org/licenses/mit-license.php
|
5
|
+
|
6
|
+
module Dewey
|
7
|
+
|
8
|
+
DOCUMENT_MIMETYPES = {
|
9
|
+
:doc => 'application/msword',
|
10
|
+
:docx => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
11
|
+
:htm => 'text/html',
|
12
|
+
:html => 'text/html',
|
13
|
+
:odt => 'application/vnd.oasis.opendocument.text',
|
14
|
+
:pdf => 'application/pdf',
|
15
|
+
:rtf => 'application/rtf',
|
16
|
+
:sxw => 'application/vnd.sun.xml.writer',
|
17
|
+
:txt => 'text/plain'
|
18
|
+
}
|
19
|
+
|
20
|
+
PRESENTATION_MIMETYPES = {
|
21
|
+
:pps => 'application/vnd.ms-powerpoint',
|
22
|
+
:ppt => 'application/vnd.ms-powerpoint'
|
23
|
+
}
|
24
|
+
|
25
|
+
SPREADSHEET_MIMETYPES = {
|
26
|
+
:csv => 'text/csv',
|
27
|
+
:ods => 'application/x-vnd.oasis.opendocument.spreadsheet',
|
28
|
+
:tab => 'text/tab-separated-values',
|
29
|
+
:tsv => 'text/tab-separated-values',
|
30
|
+
:xls => 'application/vnd.ms-excel',
|
31
|
+
:xlsx => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
32
|
+
}
|
33
|
+
|
34
|
+
class Mime
|
35
|
+
# Attempts to provide a mime_type that Google Docs finds acceptable. Certain
|
36
|
+
# types, when gathered from +file+ require extra coercion and will be handled
|
37
|
+
# automatically.
|
38
|
+
def self.mime_type(file)
|
39
|
+
type = (file.path.match(/\.(\w+)/)[1]).downcase
|
40
|
+
case type
|
41
|
+
when /csv/ then SPREADSHEET_MIMETYPES[:csv]
|
42
|
+
when /doc$/ then DOCUMENT_MIMETYPES[:doc]
|
43
|
+
when /docx/ then DOCUMENT_MIMETYPES[:docx]
|
44
|
+
when /htm/ then DOCUMENT_MIMETYPES[:html]
|
45
|
+
when /ods/ then SPREADSHEET_MIMETYPES[:ods]
|
46
|
+
when /odt/ then DOCUMENT_MIMETYPES[:odt]
|
47
|
+
when /pdf/ then DOCUMENT_MIMETYPES[:pdf]
|
48
|
+
when /pps/ then PRESENTATION_MIMETYPES[:pps]
|
49
|
+
when /ppt/ then PRESENTATION_MIMETYPES[:ppt]
|
50
|
+
when /rtf/ then DOCUMENT_MIMETYPES[:rtf]
|
51
|
+
when /sxw/ then DOCUMENT_MIMETYPES[:sxw]
|
52
|
+
when /tab/ then SPREADSHEET_MIMETYPES[:tab]
|
53
|
+
when /tsv/ then SPREADSHEET_MIMETYPES[:tsv]
|
54
|
+
when /txt/ then DOCUMENT_MIMETYPES[:txt]
|
55
|
+
when /xls$/ then SPREADSHEET_MIMETYPES[:xls]
|
56
|
+
when /xlsx/ then SPREADSHEET_MIMETYPES[:xlsx]
|
57
|
+
else
|
58
|
+
Mime.coerce((`file --mime-type #{file.path}`).split(':').last.strip) rescue "application/#{type}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Attempt to take mime types that are known to be guessed incorrectly and
|
63
|
+
# cast them to one that Google Docs will accept.
|
64
|
+
def self.coerce(mime_type)
|
65
|
+
case mime_type
|
66
|
+
when /vnd.ms-office/ then 'application/msword'
|
67
|
+
when /x-docx/ then 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
68
|
+
else mime_type
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.guess_service(mime_type)
|
73
|
+
services = { :document => DOCUMENT_MIMETYPES,
|
74
|
+
:presentation => PRESENTATION_MIMETYPES,
|
75
|
+
:spreadsheet => SPREADSHEET_MIMETYPES }
|
76
|
+
|
77
|
+
services.each_key do |service|
|
78
|
+
return service if services[service].has_value?(mime_type)
|
79
|
+
end
|
80
|
+
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/dewey/utils.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Dewey
|
2
|
+
module Utils #:nodoc:
|
3
|
+
# Performs URI escaping. (Stolen from Camping via Rake).
|
4
|
+
def escape(s)
|
5
|
+
s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
|
6
|
+
'%'+$1.unpack('H2' * bytesize($1)).join('%').upcase
|
7
|
+
}.tr(' ', '+')
|
8
|
+
end
|
9
|
+
|
10
|
+
module_function :escape
|
11
|
+
|
12
|
+
# Return the bytesize of String; uses String#length under Ruby 1.8 and
|
13
|
+
def bytesize(string)
|
14
|
+
string.bytesize
|
15
|
+
end
|
16
|
+
|
17
|
+
module_function :bytesize
|
18
|
+
end
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dewey
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 2
|
9
|
+
version: 0.1.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Parker Selbert
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-06-29 00:00:00 -04:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :development
|
32
|
+
version_requirements: *id001
|
33
|
+
description: Dewey is a no-frills tool for utilizing Google Docs ability to convert documents between formats
|
34
|
+
email:
|
35
|
+
- parker@sorentwo.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- lib/dewey/https.rb
|
44
|
+
- lib/dewey/mime.rb
|
45
|
+
- lib/dewey/utils.rb
|
46
|
+
- lib/dewey.rb
|
47
|
+
- README.md
|
48
|
+
- CHANGELOG.md
|
49
|
+
- TODO.md
|
50
|
+
has_rdoc: true
|
51
|
+
homepage: http://github.com/sorentwo/dewey
|
52
|
+
licenses: []
|
53
|
+
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
segments:
|
65
|
+
- 0
|
66
|
+
version: "0"
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
segments:
|
73
|
+
- 1
|
74
|
+
- 3
|
75
|
+
- 6
|
76
|
+
version: 1.3.6
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project: dewey
|
80
|
+
rubygems_version: 1.3.7
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Google Docs fueled document conversion
|
84
|
+
test_files: []
|
85
|
+
|