rdropbox 1.0.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.
- data/.document +5 -0
- data/.gitignore +24 -0
- data/LICENSE +20 -0
- data/README.rdoc +84 -0
- data/Rakefile +54 -0
- data/VERSION +1 -0
- data/dropbox.gemspec +87 -0
- data/lib/dropbox/api.rb +530 -0
- data/lib/dropbox/entry.rb +96 -0
- data/lib/dropbox/event.rb +109 -0
- data/lib/dropbox/memoization.rb +98 -0
- data/lib/dropbox/revision.rb +197 -0
- data/lib/dropbox/session.rb +160 -0
- data/lib/dropbox.rb +43 -0
- data/lib/extensions/array.rb +9 -0
- data/lib/extensions/hash.rb +61 -0
- data/lib/extensions/module.rb +22 -0
- data/lib/extensions/object.rb +5 -0
- data/lib/extensions/string.rb +9 -0
- data/lib/extensions/to_bool.rb +17 -0
- data/spec/dropbox/api_spec.rb +778 -0
- data/spec/dropbox/entry_spec.rb +144 -0
- data/spec/dropbox/event_spec.rb +122 -0
- data/spec/dropbox/revision_spec.rb +367 -0
- data/spec/dropbox/session_spec.rb +148 -0
- data/spec/dropbox_spec.rb +57 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- metadata +150 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
# Defines the Dropbox::Event class.
|
2
|
+
|
3
|
+
nil # doc fix
|
4
|
+
|
5
|
+
module Dropbox
|
6
|
+
|
7
|
+
# The Dropbox::Event class stores information about which entries were
|
8
|
+
# modified during a pingback event. You initialize this class from the JSON
|
9
|
+
# string given to you by Dropbox during a pingback:
|
10
|
+
#
|
11
|
+
# event = Dropbox::Event.new(params[:target_events])
|
12
|
+
#
|
13
|
+
# Once this is complete, the Dropbox::Event instance contains references for
|
14
|
+
# each of the entries, with the basic information included in the pingback:
|
15
|
+
#
|
16
|
+
# event.user_ids #=> [ 1, 2, 3 ]
|
17
|
+
# event.entries(1).first #=> #<Dropbox::Revision 1:10:100>
|
18
|
+
#
|
19
|
+
# For any of these entries, you can load its content and metadata:
|
20
|
+
#
|
21
|
+
# event.entries(1).first.load(dropbox_session)
|
22
|
+
# event.entries(1).first.content #=> "Content of file..."
|
23
|
+
# event.entries(1).first.size #=> 2245
|
24
|
+
#
|
25
|
+
# You can also load only the metadata for all of a user's entries:
|
26
|
+
#
|
27
|
+
# event.load_metadata(first_users_dropbox_session)
|
28
|
+
# event.entries(1).first.size #=> 154365
|
29
|
+
|
30
|
+
class Event
|
31
|
+
def initialize(json_pingback) # :nodoc:
|
32
|
+
@json_pingback = json_pingback
|
33
|
+
begin
|
34
|
+
@metadata = JSON.parse(json_pingback).stringify_keys_recursively
|
35
|
+
rescue JSON::ParserError
|
36
|
+
raise Dropbox::ParseError, "Invalid pingback event data"
|
37
|
+
end
|
38
|
+
|
39
|
+
process_pingback
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns an array of Dropbox user ID's involved in this pingback.
|
43
|
+
|
44
|
+
def user_ids
|
45
|
+
@entries_by_user_id.keys
|
46
|
+
end
|
47
|
+
|
48
|
+
# When given no arguments, returns an array of all entries (as
|
49
|
+
# Dropbox::Revision instances). When given a user ID, filters the list
|
50
|
+
# to only entries belonging to that Dropbox user.
|
51
|
+
|
52
|
+
def entries(user_id=nil)
|
53
|
+
user_id ? (@entries_by_user_id[user_id.to_i] || []).dup : @entries.dup
|
54
|
+
end
|
55
|
+
|
56
|
+
# Loads the metadata for all entries belonging to a given Dropbox session.
|
57
|
+
# Does not load data for files that do not belong to the user owning the
|
58
|
+
# given Dropbox session.
|
59
|
+
#
|
60
|
+
# Future calls to this method will result in additional network requests,
|
61
|
+
# though the Dropbox::Revision instances do cache their metadata values.
|
62
|
+
#
|
63
|
+
# Options:
|
64
|
+
#
|
65
|
+
# +mode+:: Temporarily changes the API mode. See the Dropbox::API::MODES
|
66
|
+
# array.
|
67
|
+
|
68
|
+
def load_metadata(session, options={})
|
69
|
+
process_metadata session.event_metadata(@json_pingback, options).stringify_keys_recursively
|
70
|
+
end
|
71
|
+
|
72
|
+
def inspect # :nodoc:
|
73
|
+
"#<#{self.class.to_s} (#{@entries.size} entries)>"
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def process_pingback
|
79
|
+
@entries = Array.new
|
80
|
+
@entries_by_user_id = Hash.new
|
81
|
+
@entries_hashed = Hash.new
|
82
|
+
|
83
|
+
@metadata.each do |user_id, namespaces|
|
84
|
+
@entries_hashed[user_id.to_i] = Hash.new
|
85
|
+
@entries_by_user_id[user_id.to_i] = Array.new
|
86
|
+
namespaces.each do |namespace_id, journals|
|
87
|
+
@entries_hashed[user_id.to_i][namespace_id.to_i] = Hash.new
|
88
|
+
journals.each do |journal_id|
|
89
|
+
entry = Dropbox::Revision.new(user_id.to_i, namespace_id.to_i, journal_id.to_i)
|
90
|
+
@entries << entry
|
91
|
+
@entries_by_user_id[user_id.to_i] << entry
|
92
|
+
@entries_hashed[user_id.to_i][namespace_id.to_i][journal_id.to_i] = entry
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def process_metadata(metadata)
|
99
|
+
p metadata
|
100
|
+
metadata.each do |user_id, namespaces|
|
101
|
+
namespaces.each do |namespace_id, journals|
|
102
|
+
journals.each do |journal_id, attributes|
|
103
|
+
@entries_hashed[user_id.to_i][namespace_id.to_i][journal_id.to_i].process_metadata(attributes.symbolize_keys)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# Defines the Dropbox::Memoization module.
|
2
|
+
|
3
|
+
nil # doc fix
|
4
|
+
|
5
|
+
module Dropbox
|
6
|
+
|
7
|
+
# Adds methods to the Dropbox::Session class to support the temporary local
|
8
|
+
# storage of API results to reduce the number of network calls and simplify
|
9
|
+
# code.
|
10
|
+
#
|
11
|
+
# Memoization is <b>opt-in</b>; you must explicitly indicate that you want
|
12
|
+
# this functionality by calling the enable_memoization method on your
|
13
|
+
# Scribd::Session instance. Once memoization is enabled, subsequent calls to
|
14
|
+
# memoized methods will hit an in-memory cache as opposed to making identical
|
15
|
+
# network calls.
|
16
|
+
#
|
17
|
+
# If you would like to use your own caching strategy (for instance, your own
|
18
|
+
# memcache instance), set the +cache_proc+ and +cache_clear_proc+ attributes.
|
19
|
+
#
|
20
|
+
# Enabling memoization makes removes an instance's thread-safety.
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
#
|
24
|
+
# session.metadata('file1') # network
|
25
|
+
#
|
26
|
+
# session.enable_memoization
|
27
|
+
# session.metadata('file1') # network
|
28
|
+
# session.metadata('file1') # cache
|
29
|
+
#
|
30
|
+
# session.metadata('file2') # network
|
31
|
+
# session.metadata('file2') # cache
|
32
|
+
#
|
33
|
+
# session.disable_memoization
|
34
|
+
# session.metadata('file2') # network
|
35
|
+
|
36
|
+
module Memoization
|
37
|
+
def self.included(base) # :nodoc:
|
38
|
+
base.extend ClassMethods
|
39
|
+
end
|
40
|
+
|
41
|
+
# The cache_proc is a proc with two arguments, the cache identifier and the
|
42
|
+
# proc to call and store in the event of a cache miss:
|
43
|
+
#
|
44
|
+
# instance.cache_proc = Proc.new do |identifier, calculate_proc|
|
45
|
+
# Rails.cache.fetch(identifier) { calculate_proc.call }
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# The Cache identifier will always be 64 lowercase hexadecimal characters.
|
49
|
+
# The second argument is a curried proc including all arguments to the
|
50
|
+
# original method call.
|
51
|
+
|
52
|
+
def cache_proc=(prc)
|
53
|
+
@_memo_cache_proc = prc
|
54
|
+
end
|
55
|
+
|
56
|
+
# The cache_clear_proc takes an identifier and should invalidate it from the
|
57
|
+
# cache:
|
58
|
+
#
|
59
|
+
# instance.cache_clear_proc = Proc.new { |identifier| Rails.cache.delete identifier }
|
60
|
+
|
61
|
+
def cache_clear_proc=(prc)
|
62
|
+
@_memo_cache_clear_proc = prc
|
63
|
+
end
|
64
|
+
|
65
|
+
# Begins memoizing the results of API calls. Memoization is off by default
|
66
|
+
# for new instances.
|
67
|
+
|
68
|
+
def enable_memoization
|
69
|
+
@_memoize = true
|
70
|
+
@_memo_identifiers ||= Set.new
|
71
|
+
end
|
72
|
+
|
73
|
+
# Halts memoization of API calls and clears the memoization cache.
|
74
|
+
|
75
|
+
def disable_memoization
|
76
|
+
@_memoize = false
|
77
|
+
@_memo_identifiers.each { |identifier| (@_memo_cache_clear_proc || Proc.new { |ident| eval "@_memo_#{ident} = nil" }).call(identifier) }
|
78
|
+
@_memo_identifiers.clear
|
79
|
+
end
|
80
|
+
|
81
|
+
module ClassMethods # :nodoc:
|
82
|
+
def memoize(*method_names) # :nodoc:
|
83
|
+
method_names.each do |meth|
|
84
|
+
define_method :"#{meth}_with_memo" do |*args|
|
85
|
+
if @_memoize then
|
86
|
+
identifier = Digest::SHA1.hexdigest(meth.to_s + ":" + args.to_yaml)
|
87
|
+
@_memo_identifiers << identifier
|
88
|
+
(@_memo_cache_proc || Proc.new { |ident, calculate_proc| eval "@_memo_#{ident} ||= calculate_proc.call" }).call identifier, Proc.new { send :"#{meth}_without_memo", *args }
|
89
|
+
else
|
90
|
+
send :"#{meth}_without_memo", *args
|
91
|
+
end
|
92
|
+
end
|
93
|
+
alias_method_chain meth, :memo
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
# Defines the Dropbox::Revision class.
|
2
|
+
|
3
|
+
nil # doc fix
|
4
|
+
|
5
|
+
module Dropbox
|
6
|
+
|
7
|
+
# A file or folder at a point in time as referenced by a Dropbox::Event
|
8
|
+
# pingback event. Instances start out as "shells" only storing enough
|
9
|
+
# information to uniquely identify a file/folder belonging to a user at a
|
10
|
+
# certain revision.
|
11
|
+
#
|
12
|
+
# Instances of this class only appear in a Dropbox::Event object. To load the
|
13
|
+
# metadata for a revision, use the Dropbox::Event#load_metadata method.
|
14
|
+
# To load the content of the file at this revision, use the load_content
|
15
|
+
# method on this class.
|
16
|
+
#
|
17
|
+
# Once the metadata has been loaded, you can access it directly:
|
18
|
+
#
|
19
|
+
# revision.size #=> 2962
|
20
|
+
#
|
21
|
+
# The +mtime+ attribute will be a Time instance. The +mtime+ and +size+
|
22
|
+
# attributes will be +nil+ (not -1) if the file was deleted. All other
|
23
|
+
# attributes are as defined in
|
24
|
+
# http://developers.getdropbox.com/base.html#event-metadata
|
25
|
+
#
|
26
|
+
# If the metadata could not be read for whatever reason, the HTTP error code
|
27
|
+
# will be stored in the +error+ attribute.
|
28
|
+
|
29
|
+
class Revision
|
30
|
+
# The ID of the Dropbox user that owns this file.
|
31
|
+
attr_reader :user_id
|
32
|
+
# The namespace ID of the file (Dropbox internal).
|
33
|
+
attr_reader :namespace_id
|
34
|
+
# The journal ID of the file (Dropbox internal).
|
35
|
+
attr_reader :journal_id
|
36
|
+
# The HTTP error code received when trying to load metadata, or +nil+ if no
|
37
|
+
# error has yet been received.
|
38
|
+
attr_reader :error
|
39
|
+
|
40
|
+
def initialize(uid, nid, jid) # :nodoc:
|
41
|
+
@user_id = uid
|
42
|
+
@namespace_id = nid
|
43
|
+
@journal_id = jid
|
44
|
+
end
|
45
|
+
|
46
|
+
# The unique identifier string used by some Dropbox event API methods.
|
47
|
+
|
48
|
+
def identifier
|
49
|
+
"#{user_id}:#{namespace_id}:#{journal_id}"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Uses the given Dropbox::Session to load the content and metadata for a
|
53
|
+
# file at a specific revision.
|
54
|
+
#
|
55
|
+
# Options:
|
56
|
+
#
|
57
|
+
# +mode+:: Temporarily changes the API mode. See the Dropbox::API::MODES
|
58
|
+
# array.
|
59
|
+
|
60
|
+
def load(session, options={})
|
61
|
+
@content, @metadata = session.event_content(identifier, options)
|
62
|
+
@metadata.symbolize_keys!
|
63
|
+
|
64
|
+
postprocess_metadata
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns true if the content for this revision has been previously loaded
|
68
|
+
# and is cached in this object.
|
69
|
+
|
70
|
+
def content_loaded?
|
71
|
+
@content.to_bool
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns true if the metadata for this revision has been previously loaded
|
75
|
+
# and is cached in this object.
|
76
|
+
|
77
|
+
def metadata_loaded?
|
78
|
+
@metadata.to_bool
|
79
|
+
end
|
80
|
+
|
81
|
+
# Sugar for the +latest+ attribute.
|
82
|
+
|
83
|
+
def latest?
|
84
|
+
raise NotLoadedError.new(:metadata) unless metadata_loaded?
|
85
|
+
self.latest
|
86
|
+
end
|
87
|
+
|
88
|
+
# Sugar for the +is_dir+ attribute.
|
89
|
+
|
90
|
+
def directory?
|
91
|
+
raise NotLoadedError.new(:metadata) unless metadata_loaded?
|
92
|
+
self.is_dir
|
93
|
+
end
|
94
|
+
|
95
|
+
# Synonym for the +mtime+ attribute, for "duck" compatibility with the
|
96
|
+
# Dropbox +metadata+ API.
|
97
|
+
|
98
|
+
def modified
|
99
|
+
raise NotLoadedError.new(:metadata) unless metadata_loaded?
|
100
|
+
self.mtime
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns true if an error occurred when trying to load metadata.
|
104
|
+
|
105
|
+
def error?
|
106
|
+
error.to_bool
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns true if this change represents the file being deleted.
|
110
|
+
|
111
|
+
def deleted?
|
112
|
+
raise NotLoadedError.new(:metadata) unless metadata_loaded?
|
113
|
+
self.mtime.nil? and self.size.nil?
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns the contents of the file as a string. Returns nil for directories.
|
117
|
+
# You must call load first to retrieve the content from the network.
|
118
|
+
|
119
|
+
def content
|
120
|
+
raise NotLoadedError.new(:content) unless content_loaded?
|
121
|
+
@content
|
122
|
+
end
|
123
|
+
|
124
|
+
def inspect # :nodoc:
|
125
|
+
"#<#{self.class.to_s} #{identifier}>"
|
126
|
+
end
|
127
|
+
|
128
|
+
# Allows you to access metadata attributes directly:
|
129
|
+
#
|
130
|
+
# revision.size #=> 10526
|
131
|
+
#
|
132
|
+
# A NoMethodError will be raised if the metadata has not yet been loaded for
|
133
|
+
# this revision, so be sure to call metadata_loaded? beforehand.
|
134
|
+
|
135
|
+
def method_missing(meth, *args)
|
136
|
+
if args.empty? then
|
137
|
+
if @metadata and @metadata.include?(meth) then
|
138
|
+
return @metadata[meth]
|
139
|
+
else
|
140
|
+
super
|
141
|
+
end
|
142
|
+
else
|
143
|
+
super
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Loads the metadata for the latest revision of the entry and returns it as
|
148
|
+
# as <tt>Struct</tt> object. Uses the given session and calls
|
149
|
+
# Dropbox::API.metadata.
|
150
|
+
#
|
151
|
+
# If the metadata for this object has not yet been loaded, raises an error.
|
152
|
+
# Options are passed to Dropbox::API.metadata.
|
153
|
+
|
154
|
+
def metadata_for_latest_revision(session, options={})
|
155
|
+
raise NotLoadedError.new(:metadata) unless metadata_loaded?
|
156
|
+
session.metadata self.path, options
|
157
|
+
end
|
158
|
+
|
159
|
+
def process_metadata(metadata) # :nodoc:
|
160
|
+
if metadata[:error] then
|
161
|
+
@error = metadata[:error] unless @metadata
|
162
|
+
return
|
163
|
+
end
|
164
|
+
|
165
|
+
@error = nil
|
166
|
+
@metadata = Hash.new
|
167
|
+
metadata.each { |key, value| @metadata[key.to_sym] = value }
|
168
|
+
|
169
|
+
postprocess_metadata
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def postprocess_metadata
|
175
|
+
@metadata[:size] = nil if @metadata[:size] == -1
|
176
|
+
@metadata[:mtime] = (@metadata[:mtime] == -1 ? nil : Time.at(@metadata[:mtime])) if @metadata[:mtime]
|
177
|
+
@metadata[:ts] = Time.parse(@metadata[:ts]) if @metadata[:ts]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Raised when trying to access content metadata before it has been loaded.
|
182
|
+
|
183
|
+
class NotLoadedError < StandardError
|
184
|
+
|
185
|
+
# What data did you attempt to access before it was loaded? Either
|
186
|
+
# <tt>:content</tt> or <tt>:metadata</tt>.
|
187
|
+
attr_reader :data
|
188
|
+
|
189
|
+
def initialize(data) # :nodoc:
|
190
|
+
@data = data
|
191
|
+
end
|
192
|
+
|
193
|
+
def to_s # :nodoc:
|
194
|
+
"#{data.capitalize} not yet loaded -- call #load on the Dropbox::Revision instance beforehand"
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# Defines the Dropbox::Session class.
|
2
|
+
|
3
|
+
require 'oauth'
|
4
|
+
|
5
|
+
module Dropbox
|
6
|
+
|
7
|
+
# This class is a portal to the Dropbox API and a façade over the Ruby OAuth
|
8
|
+
# gem allowing developers to authenticate their user's Dropbox accounts.
|
9
|
+
#
|
10
|
+
# == Authenticating a user
|
11
|
+
#
|
12
|
+
# You start by creating a new instance and providing your OAuth consumer key
|
13
|
+
# and secret. You then call the authorize_url method on your new instance to
|
14
|
+
# receive the authorization URL.
|
15
|
+
#
|
16
|
+
# Once your user visits the URL, it will complete the authorization process on
|
17
|
+
# the server side. You should call the authorize method:
|
18
|
+
#
|
19
|
+
# session = Dropbox::Session.new(my_key, my_secret)
|
20
|
+
# puts "Now visit #{session.authorize_url}. Hit enter when you have completed authorization."
|
21
|
+
# gets
|
22
|
+
# session.authorize
|
23
|
+
#
|
24
|
+
# The authorize method must be called on the same instance of Dropbox::Session
|
25
|
+
# that gave you the URL. If this is unfeasible (for instance, you are doing
|
26
|
+
# this in a stateless Rails application), you can serialize the Session for
|
27
|
+
# storage (e.g., in your Rails session):
|
28
|
+
#
|
29
|
+
# def authorize
|
30
|
+
# if params[:oauth_token] then
|
31
|
+
# dropbox_session = Dropbox::Session.deserialize(session[:dropbox_session])
|
32
|
+
# dropbox_session.authorize(params)
|
33
|
+
# session[:dropbox_session] = dropbox_session.serialize # re-serialize the authenticated session
|
34
|
+
#
|
35
|
+
# redirect_to :action => 'upload'
|
36
|
+
# else
|
37
|
+
# dropbox_session = Dropbox::Session.new('your_consumer_key', 'your_consumer_secret')
|
38
|
+
# session[:dropbox_session] = dropbox_session.serialize
|
39
|
+
# redirect_to dropbox_session.authorize_url(:oauth_callback => root_url)
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# == Working with the API
|
44
|
+
#
|
45
|
+
# This class includes the methods of the Dropbox::API module. See that module
|
46
|
+
# to learn how to continue using the API.
|
47
|
+
|
48
|
+
class Session
|
49
|
+
include API
|
50
|
+
|
51
|
+
# Begins the authorization process. Provide the OAuth key and secret of your
|
52
|
+
# API account, assigned by Dropbox. This is the first step in the
|
53
|
+
# authorization process.
|
54
|
+
#
|
55
|
+
# Options:
|
56
|
+
#
|
57
|
+
# +ssl+:: If true, uses SSL to connect to the Dropbox API server.
|
58
|
+
|
59
|
+
def initialize(oauth_key, oauth_secret, options={})
|
60
|
+
@ssl = options[:ssl].to_bool
|
61
|
+
@consumer = OAuth::Consumer.new(oauth_key, oauth_secret,
|
62
|
+
:site => (@ssl ? Dropbox::SSL_HOST : Dropbox::HOST),
|
63
|
+
:request_token_path => "/#{Dropbox::VERSION}/oauth/request_token",
|
64
|
+
:authorize_path => "/#{Dropbox::VERSION}/oauth/authorize",
|
65
|
+
:access_token_path => "/#{Dropbox::VERSION}/oauth/access_token")
|
66
|
+
@request_token = @consumer.get_request_token
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a URL that is used to complete the authorization process. Visiting
|
70
|
+
# this URL is the second step in the authorization process, after creating
|
71
|
+
# the Session instance.
|
72
|
+
|
73
|
+
def authorize_url(*args)
|
74
|
+
if authorized? then
|
75
|
+
raise AlreadyAuthorizedError, "You have already been authorized; no need to get an authorization URL."
|
76
|
+
else
|
77
|
+
return @request_token.authorize_url(*args)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Authorizes a user from the information returned by Dropbox. This is the
|
82
|
+
# third step in the authorization process, after sending the user to the
|
83
|
+
# authorize_url.
|
84
|
+
#
|
85
|
+
# You can pass to this method a hash containing the keys and values of the
|
86
|
+
# OAuth parameters returned by Dropbox. An example in Rails:
|
87
|
+
#
|
88
|
+
# session.authorize :oauth_verifier => params[:oauth_verifier]
|
89
|
+
#
|
90
|
+
# Returns a boolean indicating if authentication was successful.
|
91
|
+
|
92
|
+
def authorize(options={})
|
93
|
+
@access_token = @request_token.get_access_token(options)
|
94
|
+
@request_token = nil if @access_token
|
95
|
+
return @access_token.to_bool
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns true if this session has been authorized.
|
99
|
+
|
100
|
+
def authorized?
|
101
|
+
@access_token.to_bool
|
102
|
+
end
|
103
|
+
|
104
|
+
# Serializes this object into a string that can then be recreated with the
|
105
|
+
# Dropbox::Session.deserialize method.
|
106
|
+
|
107
|
+
def serialize
|
108
|
+
if authorized? then
|
109
|
+
[ @consumer.key, @consumer.secret, authorized?, @access_token.token, @access_token.secret ].to_yaml
|
110
|
+
else
|
111
|
+
[ @consumer.key, @consumer.secret, authorized?, @request_token.token, @request_token.secret ].to_yaml
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Deserializes an instance from a string created from the serialize method.
|
116
|
+
# Returns the recreated instance.
|
117
|
+
|
118
|
+
def self.deserialize(data)
|
119
|
+
consumer_key, consumer_secret, authorized, token, token_secret = YAML.load(StringIO.new(data))
|
120
|
+
raise ArgumentError, "Must provide a properly serialized #{self.to_s} instance" unless [ consumer_key, consumer_secret, token, token_secret ].all? and authorized == true or authorized == false
|
121
|
+
|
122
|
+
session = self.new(consumer_key, consumer_secret)
|
123
|
+
if authorized then
|
124
|
+
session.instance_variable_set :@access_token, OAuth::AccessToken.new(session.instance_variable_get(:@consumer), token, token_secret)
|
125
|
+
else
|
126
|
+
session.instance_variable_set :@request_token, OAuth::RequestToken.new(session.instance_variable_get(:@consumer), token, token_secret)
|
127
|
+
end
|
128
|
+
|
129
|
+
return session
|
130
|
+
end
|
131
|
+
|
132
|
+
def inspect # :nodoc:
|
133
|
+
"#<#{self.class.to_s} #{@consumer.key} (#{'un' unless authorized?}authorized)>"
|
134
|
+
end
|
135
|
+
|
136
|
+
private
|
137
|
+
|
138
|
+
def access_token
|
139
|
+
@access_token || raise(UnauthorizedError, "You need to authorize the Dropbox user before you can call API methods")
|
140
|
+
end
|
141
|
+
|
142
|
+
def clone_with_host(host)
|
143
|
+
session = dup
|
144
|
+
consumer = OAuth::Consumer.new(@consumer.key, @consumer.secret, :site => host)
|
145
|
+
session.instance_variable_set :@consumer, consumer
|
146
|
+
session.instance_variable_set :@access_token, OAuth::AccessToken.new(consumer, @access_token.token, @access_token.secret)
|
147
|
+
return session
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Raised when trying to call Dropbox API methods without yet having completed
|
152
|
+
# the OAuth process.
|
153
|
+
|
154
|
+
class UnauthorizedError < StandardError; end
|
155
|
+
|
156
|
+
# Raised when trying to call Dropbox::Session#authorize_url on an already
|
157
|
+
# authorized session.
|
158
|
+
|
159
|
+
class AlreadyAuthorizedError < StandardError; end
|
160
|
+
end
|
data/lib/dropbox.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# Defines the Dropbox module.
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
require 'yaml'
|
5
|
+
require 'digest/sha1'
|
6
|
+
require 'thread'
|
7
|
+
require 'set'
|
8
|
+
require 'time'
|
9
|
+
require 'tempfile'
|
10
|
+
|
11
|
+
Dir.glob("#{File.expand_path File.dirname(__FILE__)}/extensions/*.rb") { |file| require file }
|
12
|
+
Dir.glob("#{File.expand_path File.dirname(__FILE__)}/dropbox/*.rb") { |file| require file }
|
13
|
+
|
14
|
+
# Container module for the all Dropbox API classes.
|
15
|
+
|
16
|
+
module Dropbox
|
17
|
+
# The API version this client works with.
|
18
|
+
VERSION = "0"
|
19
|
+
# The host serving API requests.
|
20
|
+
HOST = "http://api.dropbox.com"
|
21
|
+
# The SSL host serving API requests.
|
22
|
+
SSL_HOST = "https://api.dropbox.com"
|
23
|
+
# Alternate hosts for other API requests.
|
24
|
+
ALTERNATE_HOSTS = { 'event_content' => 'http://api-content.dropbox.com', 'files' => "http://api-content.dropbox.com" }
|
25
|
+
# Alternate SSL hosts for other API requests.
|
26
|
+
ALTERNATE_SSL_HOSTS = { 'event_content' => 'https://api-content.dropbox.com', 'files' => "https://api-content.dropbox.com" }
|
27
|
+
|
28
|
+
def self.api_url(*paths_and_options) # :nodoc:
|
29
|
+
params = paths_and_options.extract_options!
|
30
|
+
ssl = params.delete(:ssl)
|
31
|
+
host = (ssl ? ALTERNATE_SSL_HOSTS[paths_and_options.first] : ALTERNATE_HOSTS[paths_and_options.first]) || (ssl ? SSL_HOST : HOST)
|
32
|
+
url = "#{host}/#{VERSION}/#{paths_and_options.map { |path_elem| CGI.escape path_elem.to_s }.join('/')}"
|
33
|
+
url.gsub! '+', '%20' # dropbox doesn't really like plusses
|
34
|
+
url << "?#{params.map { |k,v| CGI.escape(k.to_s) + "=" + CGI.escape(v.to_s) }.join('&')}" unless params.empty?
|
35
|
+
return url
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.check_path(path) # :nodoc:
|
39
|
+
raise ArgumentError, "Backslashes are not allowed in Dropbox paths" if path.include?('\\')
|
40
|
+
raise ArgumentError, "Dropbox paths are limited to 256 characters in length" if path.size > 256
|
41
|
+
return path
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Hash # :nodoc:
|
2
|
+
def slice(*keys) #:nodoc:
|
3
|
+
keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
4
|
+
hash = self.class.new
|
5
|
+
keys.each { |k| hash[k] = self[k] if has_key?(k) }
|
6
|
+
hash
|
7
|
+
end unless method_defined?(:slice)
|
8
|
+
|
9
|
+
def symbolize_keys # :nodoc:
|
10
|
+
inject({}) do |options, (key, value)|
|
11
|
+
options[(key.to_sym rescue key) || key] = value
|
12
|
+
options
|
13
|
+
end
|
14
|
+
end unless method_defined?(:symbolize_keys)
|
15
|
+
|
16
|
+
def symbolize_keys! # :nodoc:
|
17
|
+
self.replace(self.symbolize_keys)
|
18
|
+
end unless method_defined?(:symbolize_keys!)
|
19
|
+
|
20
|
+
def symbolize_keys_recursively # :nodoc:
|
21
|
+
hsh = symbolize_keys
|
22
|
+
hsh.each { |k, v| hsh[k] = v.symbolize_keys_recursively if v.kind_of?(Hash) }
|
23
|
+
hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.symbolize_keys_recursively : i } if v.kind_of?(Array) }
|
24
|
+
return hsh
|
25
|
+
end
|
26
|
+
|
27
|
+
def stringify_keys # :nodoc:
|
28
|
+
inject({}) do |options, (key, value)|
|
29
|
+
options[(key.to_s rescue key) || key] = value
|
30
|
+
options
|
31
|
+
end
|
32
|
+
end unless method_defined?(:stringify_keys)
|
33
|
+
|
34
|
+
def stringify_keys! # :nodoc:
|
35
|
+
self.replace(self.stringify_keys)
|
36
|
+
end unless method_defined?(:stringify_keys!)
|
37
|
+
|
38
|
+
def stringify_keys_recursively # :nodoc:
|
39
|
+
hsh = stringify_keys
|
40
|
+
hsh.each { |k, v| hsh[k] = v.stringify_keys_recursively if v.kind_of?(Hash) }
|
41
|
+
hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.stringify_keys_recursively : i } if v.kind_of?(Array) }
|
42
|
+
return hsh
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_struct # :nodoc:
|
46
|
+
struct = Struct.new(*keys).new(*values)
|
47
|
+
# attach methods for any predicate keys, since Struct.new doesn't seem to do that
|
48
|
+
pred_keys = slice(*(keys.select { |key| key.to_s.ends_with?('?') }))
|
49
|
+
pred_keys.each do |key, val|
|
50
|
+
struct.eigenclass.send(:define_method, key.to_sym) { return val }
|
51
|
+
end
|
52
|
+
return struct
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_struct_recursively # :nodoc:
|
56
|
+
hsh = dup
|
57
|
+
hsh.each { |k, v| hsh[k] = v.to_struct_recursively if v.kind_of?(Hash) }
|
58
|
+
hsh.each { |k, v| hsh[k] = v.map { |i| i.kind_of?(Hash) ? i.to_struct_recursively : i } if v.kind_of?(Array) }
|
59
|
+
return hsh.to_struct
|
60
|
+
end
|
61
|
+
end
|