dropbox 0.0.10 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +23 -3
- data/LICENSE +20 -0
- data/README.rdoc +84 -17
- data/Rakefile +39 -20
- data/VERSION +1 -1
- data/lib/dropbox.rb +41 -12
- 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/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 +64 -27
- data/ChangeLog.rdoc +0 -17
- data/examples/dropbox_spec.rb +0 -99
- data/lib/dropbox/dropbox.rb +0 -213
@@ -0,0 +1,96 @@
|
|
1
|
+
# Defines the Dropbox::Entry class.
|
2
|
+
|
3
|
+
nil # doc fix
|
4
|
+
|
5
|
+
module Dropbox
|
6
|
+
|
7
|
+
# A façade over a Dropbox::Session that allows the programmer to interact with
|
8
|
+
# Dropbox files in an object-oriented manner. The Dropbox::Entry instance is
|
9
|
+
# created by calling the Dropbox::API#entry method:
|
10
|
+
#
|
11
|
+
# file = session.file('remote/file.pdf')
|
12
|
+
# dir = session.directory('remote/dir') # these calls are actually identical
|
13
|
+
#
|
14
|
+
# Note that no network calls are made; this merely creates a façade that will
|
15
|
+
# delegate future calls to the session:
|
16
|
+
#
|
17
|
+
# file.move('new/path') # identical to calling session.move('remote/file.pdf', 'new/path')
|
18
|
+
#
|
19
|
+
# The internal path is updated as the file is moved and renamed:
|
20
|
+
#
|
21
|
+
# file = session.file('first_name.txt')
|
22
|
+
# file.rename('second_name.txt')
|
23
|
+
# file.rename('third_name.txt') # works as the internal path is updated with the first rename
|
24
|
+
|
25
|
+
class Entry
|
26
|
+
# The remote path of the file.
|
27
|
+
attr_reader :path
|
28
|
+
|
29
|
+
def initialize(session, path) # :nodoc:
|
30
|
+
@session = session
|
31
|
+
@path = path
|
32
|
+
end
|
33
|
+
|
34
|
+
# Delegates to Dropbox::API#metadata.
|
35
|
+
|
36
|
+
def metadata(options={})
|
37
|
+
@session.metadata path, options
|
38
|
+
end
|
39
|
+
alias :info :metadata
|
40
|
+
|
41
|
+
# Delegates to Dropbox::API#list
|
42
|
+
|
43
|
+
def list(options={})
|
44
|
+
@session.list path, options
|
45
|
+
end
|
46
|
+
alias :ls :list
|
47
|
+
|
48
|
+
# Delegates to Dropbox::API#move.
|
49
|
+
|
50
|
+
def move(dest, options={})
|
51
|
+
result = @session.move(path, dest, options)
|
52
|
+
@path = result.path.gsub(/^\//, '')
|
53
|
+
return result
|
54
|
+
end
|
55
|
+
alias :mv :move
|
56
|
+
|
57
|
+
# Delegates to Dropbox::API#rename.
|
58
|
+
|
59
|
+
def rename(name, options={})
|
60
|
+
result = @session.rename(path, name, options)
|
61
|
+
@path = result.path.gsub(/^\//, '')
|
62
|
+
return result
|
63
|
+
end
|
64
|
+
|
65
|
+
# Delegates to Dropbox::API#copy.
|
66
|
+
|
67
|
+
def copy(dest, options={})
|
68
|
+
@session.copy path, dest, options
|
69
|
+
end
|
70
|
+
alias :cp :copy
|
71
|
+
|
72
|
+
# Delegates to Dropbox::API#delete.
|
73
|
+
|
74
|
+
def delete(options={})
|
75
|
+
@session.delete path, options
|
76
|
+
end
|
77
|
+
alias :rm :delete
|
78
|
+
|
79
|
+
# Delegates to Dropbox::API#download.
|
80
|
+
|
81
|
+
def download(options={})
|
82
|
+
@session.download path, options
|
83
|
+
end
|
84
|
+
alias :body :download
|
85
|
+
|
86
|
+
# Delegates to Dropbox::API#link.
|
87
|
+
|
88
|
+
def link(options={})
|
89
|
+
@session.link path, options
|
90
|
+
end
|
91
|
+
|
92
|
+
def inspect # :nodoc:
|
93
|
+
"#<#{self.class.to_s} #{path}>"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -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
|