dropbox 0.0.10 → 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 +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
|