dbox 0.2.0 → 0.3.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/README.md CHANGED
@@ -62,42 +62,42 @@ $ DROPBOX_AUTH_KEY=v4d7l1rez1czksn DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4 dbox ..
62
62
  * This auth token will last for **10 years**, or when you choose to invalidate it, whichever comes first. So you really only need to do this once, and then keep them around.
63
63
 
64
64
 
65
- Usage
66
- -----
65
+ Using dbox from the Command-Line
66
+ --------------------------------
67
67
 
68
- ### Authorize
68
+ ### Usage
69
+
70
+ #### Authorize
69
71
 
70
72
  ```sh
71
73
  $ dbox authorize
72
74
  ```
73
75
 
74
- ### Create a new Dropbox folder
76
+ #### Create a new Dropbox folder
75
77
 
76
78
  ```sh
77
79
  $ dbox create <remote_path> [<local_path>]
78
80
  ```
79
81
 
80
- ### Clone an existing Dropbox folder
82
+ #### Clone an existing Dropbox folder
81
83
 
82
84
  ```sh
83
85
  $ dbox clone <remote_path> [<local_path>]
84
86
  ```
85
87
 
86
- ### Pull (download changes from Dropbox)
88
+ #### Pull (download changes from Dropbox)
87
89
 
88
90
  ```sh
89
91
  $ dbox pull [<local_path>]
90
92
  ```
91
93
 
92
- ### Push (upload changes to Dropbox)
94
+ #### Push (upload changes to Dropbox)
93
95
 
94
96
  ```sh
95
97
  $ dbox push [<local_path>]
96
98
  ```
97
99
 
98
-
99
- Example
100
- -------
100
+ #### Example
101
101
 
102
102
  ```sh
103
103
  $ export DROPBOX_APP_KEY=cmlrrjd3j0gbend
@@ -136,3 +136,75 @@ $ dbox pull
136
136
  $ cat hello.txt
137
137
  Oh, Hello
138
138
  ```
139
+
140
+ Using dbox from Ruby
141
+ --------------------
142
+
143
+ ### Usage
144
+
145
+ #### Setup
146
+
147
+ * Authorize beforehand with the command-line tool
148
+
149
+ ```ruby
150
+ require "dbox"
151
+ ```
152
+
153
+ #### Create a new Dropbox folder
154
+
155
+ ```ruby
156
+ Dbox.create(remote_path, local_path)
157
+ ```
158
+
159
+ #### Clone an existing Dropbox folder
160
+
161
+ ```ruby
162
+ Dbox.clone(remote_path, local_path)
163
+ ```
164
+
165
+ #### Pull (download changes from Dropbox)
166
+
167
+ ```ruby
168
+ Dbox.pull(local_path)
169
+ ```
170
+
171
+ #### Push (upload changes to Dropbox)
172
+
173
+ ```ruby
174
+ Dbox.push(local_path)
175
+ ```
176
+
177
+ #### Example
178
+
179
+ ```sh
180
+ $ export DROPBOX_APP_KEY=cmlrrjd3j0gbend
181
+ $ export DROPBOX_APP_SECRET=uvuulp75xf9jffl
182
+ ```
183
+
184
+ ```sh
185
+ $ dbox authorize
186
+ ```
187
+
188
+ ```sh
189
+ $ open http://www.dropbox.com/0/oauth/authorize?oauth_token=aaoeuhtns123456
190
+ ```
191
+
192
+ ```sh
193
+ $ export DROPBOX_AUTH_KEY=v4d7l1rez1czksn
194
+ $ export DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4
195
+ ```
196
+
197
+ ```ruby
198
+ > require "dbox"
199
+ > Dbox.clone("/Public", "/tmp/public")
200
+ > File.open("/tmp/public/hello.txt", "w") {|f| f << "Hello World" }
201
+ > Dbox.push("/tmp/public")
202
+
203
+ > File.read("#{ENV['HOME']}/Dropbox/Public/hello.txt")
204
+ => "Hello World"
205
+ > File.open("#{ENV['HOME']}/Dropbox/Public/hello.txt", "w") {|f| f << "Oh, Hello" }
206
+
207
+ > Dbox.pull("/tmp/public")
208
+ > File.read("#{ENV['HOME']}/Dropbox/Public/hello.txt")
209
+ => "Oh, Hello"
210
+ ```
data/TODO.txt CHANGED
@@ -1,7 +1,6 @@
1
- * More helpers in specs (and string style paths instead of join all over the place)
2
- * Have pull, push, etc return a list of changed files
3
1
  * Look down directory tree until you hit a .dropbox.db file
4
- * Solve upload -> re-download issue
5
- * Add rename_remote command
2
+ * Put pull, push, etc in begin blocks and rescue => save to avoid half-baked repos
3
+ * Detect old db format and migrate
4
+ * Add "move" command (that renames remote)
6
5
  * Add a "sync" command that pushes and pulls in one go
7
- * Add support for partial push/pull? (also, make it defoult behaviour from subdir?)
6
+ * Add support for partial push/pull
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.0
data/bin/dbox CHANGED
@@ -32,20 +32,30 @@ def print_usage_and_quit; puts usage; exit 1; end
32
32
  print_usage_and_quit unless ARGV.size >= 1
33
33
 
34
34
  command = ARGV[0]
35
- rest = ARGV[1..-1]
35
+ args = ARGV[1..-1]
36
36
 
37
37
  # execute the command
38
38
  case command
39
39
  when "authorize"
40
40
  Dbox.authorize
41
41
  when "create", "clone"
42
- unless rest.size >= 1
42
+ unless args.size >= 1
43
43
  puts "Error: Please provide a remote path to clone"
44
44
  print_usage_and_quit
45
45
  end
46
- Dbox.send(command, *rest)
46
+
47
+ remote_path = args[0]
48
+
49
+ # default to creating a directory inside the current directory with
50
+ # the same name of the directory being created/cloned
51
+ local_path = args[1] || remote_path.split("/").last
52
+
53
+ Dbox.send(command, remote_path, local_path)
47
54
  when "pull", "push"
48
- Dbox.send(command, *rest)
55
+ # default to current directory
56
+ local_path = args[0] || "."
57
+
58
+ Dbox.send(command, local_path)
49
59
  else
50
60
  print_usage_and_quit
51
61
  end
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dbox}
8
- s.version = "0.2.0"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = [%q{Ken Pratt}]
@@ -17,36 +17,38 @@ module Dbox
17
17
  Dbox::API.authorize
18
18
  end
19
19
 
20
- def self.create(remote_path, local_path = nil)
20
+ def self.create(remote_path, local_path)
21
21
  remote_path = clean_remote_path(remote_path)
22
- local_path ||= remote_path.split("/").last
22
+ local_path = clean_local_path(local_path)
23
23
  Dbox::DB.create(remote_path, local_path)
24
24
  end
25
25
 
26
- def self.clone(remote_path, local_path = nil)
26
+ def self.clone(remote_path, local_path)
27
27
  remote_path = clean_remote_path(remote_path)
28
- local_path ||= remote_path.split("/").last
28
+ local_path = clean_local_path(local_path)
29
29
  Dbox::DB.clone(remote_path, local_path)
30
30
  end
31
31
 
32
- def self.pull(local_path = nil)
33
- local_path ||= "."
32
+ def self.pull(local_path)
33
+ local_path = clean_local_path(local_path)
34
34
  Dbox::DB.pull(local_path)
35
35
  end
36
36
 
37
- def self.push(local_path = nil)
38
- local_path ||= "."
37
+ def self.push(local_path)
38
+ local_path = clean_local_path(local_path)
39
39
  Dbox::DB.push(local_path)
40
40
  end
41
41
 
42
42
  private
43
43
 
44
44
  def self.clean_remote_path(path)
45
- if path
46
- path.sub(/\/$/,'')
47
- path[0].chr == "/" ? path : "/#{path}"
48
- else
49
- raise "Missing remote path"
50
- end
45
+ raise(ArgumentError, "Missing remote path") unless path
46
+ path.sub(/\/$/,'')
47
+ path[0].chr == "/" ? path : "/#{path}"
48
+ end
49
+
50
+ def self.clean_local_path(path)
51
+ raise(ArgumentError, "Missing local path") unless path
52
+ File.expand_path(path)
51
53
  end
52
54
  end
@@ -1,4 +1,10 @@
1
1
  module Dbox
2
+ class ConfigurationError < RuntimeError; end
3
+ class ServerError < RuntimeError; end
4
+ class RemoteMissing < RuntimeError; end
5
+ class RemoteAlreadyExists < RuntimeError; end
6
+ class RequestDenied < RuntimeError; end
7
+
2
8
  class API
3
9
  include Loggable
4
10
 
@@ -34,60 +40,84 @@ module Dbox
34
40
  auth_key = ENV["DROPBOX_AUTH_KEY"]
35
41
  auth_secret = ENV["DROPBOX_AUTH_SECRET"]
36
42
 
37
- raise("Please set the DROPBOX_AUTH_KEY environment variable to an authenticated Dropbox session key") unless auth_key
38
- raise("Please set the DROPBOX_AUTH_SECRET environment variable to an authenticated Dropbox session secret") unless auth_secret
43
+ raise(ConfigurationError, "Please set the DROPBOX_AUTH_KEY environment variable to an authenticated Dropbox session key") unless auth_key
44
+ raise(ConfigurationError, "Please set the DROPBOX_AUTH_SECRET environment variable to an authenticated Dropbox session secret") unless auth_secret
39
45
 
40
46
  @auth = Authenticator.new(@conf, auth_key, auth_secret)
41
47
  @client = DropboxClient.new(@conf["server"], @conf["content_server"], @conf["port"], @auth)
42
48
  end
43
49
 
44
- def metadata(path = "/")
50
+ def run(path)
45
51
  path = escape_path(path)
46
- log.debug "Fetching metadata for #{path}"
47
52
  begin
48
- case res = @client.metadata(@conf["root"], path)
53
+ res = yield path
54
+ log.debug "Result: #{res.inspect}"
55
+
56
+ case res
49
57
  when Hash
50
58
  res
59
+ when String
60
+ res
51
61
  when Net::HTTPNotFound
52
- raise "Remote path does not exist"
62
+ raise RemoteMissing, "#{path} does not exist on Dropbox"
63
+ when Net::HTTPForbidden
64
+ raise RequestDenied, "Operation on #{path} denied"
53
65
  else
54
- raise "Unexpected result from GET /metadata: #{res.inspect}"
66
+ raise RuntimeError, "Unexpected result: #{res.inspect}"
55
67
  end
56
68
  rescue DropboxError => e
57
- raise "Server error -- might be a hiccup, please try your request again (#{e.message})"
69
+ log.debug e.inspect
70
+ raise ServerError, "Server error -- might be a hiccup, please try your request again (#{e.message})"
71
+ end
72
+ end
73
+
74
+ def metadata(path = "/")
75
+ log.debug "Fetching metadata for #{path}"
76
+ run(path) do |path|
77
+ @client.metadata(@conf["root"], path)
58
78
  end
59
79
  end
60
80
 
61
81
  def create_dir(path)
62
- path = escape_path(path)
63
82
  log.info "Creating #{path}"
64
- @client.file_create_folder(@conf["root"], path)
83
+ run(path) do |path|
84
+ case res = @client.file_create_folder(@conf["root"], path)
85
+ when Net::HTTPForbidden
86
+ raise RemoteAlreadyExists, "The directory at #{path} already exists"
87
+ else
88
+ res
89
+ end
90
+ end
65
91
  end
66
92
 
67
93
  def delete_dir(path)
68
- path = escape_path(path)
69
94
  log.info "Deleting #{path}"
70
- @client.file_delete(@conf["root"], path)
95
+ run(path) do |path|
96
+ @client.file_delete(@conf["root"], path)
97
+ end
71
98
  end
72
99
 
73
100
  def get_file(path)
74
- path = escape_path(path)
75
101
  log.info "Downloading #{path}"
76
- @client.get_file(@conf["root"], path)
102
+ run(path) do |path|
103
+ @client.get_file(@conf["root"], path)
104
+ end
77
105
  end
78
106
 
79
107
  def put_file(path, file_obj)
80
- path = escape_path(path)
81
108
  log.info "Uploading #{path}"
82
- dir = File.dirname(path)
83
- name = File.basename(path)
84
- @client.put_file(@conf["root"], dir, name, file_obj)
109
+ run(path) do |path|
110
+ dir = File.dirname(path)
111
+ name = File.basename(path)
112
+ @client.put_file(@conf["root"], dir, name, file_obj)
113
+ end
85
114
  end
86
115
 
87
116
  def delete_file(path)
88
- path = escape_path(path)
89
117
  log.info "Deleting #{path}"
90
- @client.file_delete(@conf["root"], path)
118
+ run(path) do |path|
119
+ @client.file_delete(@conf["root"], path)
120
+ end
91
121
  end
92
122
 
93
123
  def escape_path(path)
@@ -98,8 +128,8 @@ module Dbox
98
128
  app_key = ENV["DROPBOX_APP_KEY"]
99
129
  app_secret = ENV["DROPBOX_APP_SECRET"]
100
130
 
101
- raise("Please set the DROPBOX_APP_KEY environment variable to a Dropbox application key") unless app_key
102
- raise("Please set the DROPBOX_APP_SECRET environment variable to a Dropbox application secret") unless app_secret
131
+ raise(ConfigurationError, "Please set the DROPBOX_APP_KEY environment variable to a Dropbox application key") unless app_key
132
+ raise(ConfigurationError, "Please set the DROPBOX_APP_SECRET environment variable to a Dropbox application secret") unless app_secret
103
133
 
104
134
  {
105
135
  "server" => "api.dropbox.com",
@@ -1,4 +1,7 @@
1
1
  module Dbox
2
+ class MissingDatabase < RuntimeError; end
3
+ class BadPath < RuntimeError; end
4
+
2
5
  class DB
3
6
  include Loggable
4
7
 
@@ -7,7 +10,6 @@ module Dbox
7
10
  attr_accessor :local_path
8
11
 
9
12
  def self.create(remote_path, local_path)
10
- log.info "Creating remote folder: #{remote_path}"
11
13
  api.create_dir(remote_path)
12
14
  clone(remote_path, local_path)
13
15
  end
@@ -15,7 +17,7 @@ module Dbox
15
17
  def self.clone(remote_path, local_path)
16
18
  log.info "Cloning #{remote_path} into #{local_path}"
17
19
  res = api.metadata(remote_path)
18
- raise "Remote path error" unless remote_path == res["path"]
20
+ raise(BadPath, "Remote path error") unless remote_path == res["path"]
19
21
  db = new(local_path, res)
20
22
  db.pull
21
23
  end
@@ -24,10 +26,10 @@ module Dbox
24
26
  db_file = db_file(local_path)
25
27
  if File.exists?(db_file)
26
28
  db = File.open(db_file, "r") {|f| YAML::load(f.read) }
27
- db.local_path = File.expand_path(local_path)
29
+ db.local_path = local_path
28
30
  db
29
31
  else
30
- raise "No DB file found in #{local_path}"
32
+ raise MissingDatabase, "No DB file found in #{local_path}"
31
33
  end
32
34
  end
33
35
 
@@ -42,7 +44,7 @@ module Dbox
42
44
  # IMPORTANT: DropboxDb.new is private. Please use DropboxDb.create, DropboxDb.clone, or DropboxDb.load as the entry point.
43
45
  private_class_method :new
44
46
  def initialize(local_path, res)
45
- @local_path = File.expand_path(local_path)
47
+ @local_path = local_path
46
48
  @remote_path = res["path"]
47
49
  FileUtils.mkdir_p(@local_path)
48
50
  @root = DropboxDir.new(self, res)
@@ -56,20 +58,22 @@ module Dbox
56
58
  end
57
59
 
58
60
  def pull
59
- @root.pull
61
+ res = @root.pull
60
62
  save
63
+ res
61
64
  end
62
65
 
63
66
  def push
64
- @root.push
67
+ res = @root.push
65
68
  save
69
+ res
66
70
  end
67
71
 
68
72
  def local_to_relative_path(path)
69
73
  if path.include?(@local_path)
70
74
  path.sub(@local_path, "").sub(/^\//, "")
71
75
  else
72
- raise "Not a local path: #{path}"
76
+ raise BadPath, "Not a local path: #{path}"
73
77
  end
74
78
  end
75
79
 
@@ -77,7 +81,7 @@ module Dbox
77
81
  if path.include?(@remote_path)
78
82
  path.sub(@remote_path, "").sub(/^\//, "")
79
83
  else
80
- raise "Not a remote path: #{path}"
84
+ raise BadPath, "Not a remote path: #{path}"
81
85
  end
82
86
  end
83
87
 
@@ -154,8 +158,8 @@ module Dbox
154
158
  end
155
159
 
156
160
  def update(res)
157
- raise "bad path (#{remote_path} != #{res["path"]})" unless remote_path == res["path"]
158
- raise "mode on #{@path} changed between file and dir -- not supported yet" unless dir? == res["is_dir"] # TODO handle change from dir to file or vice versa?
161
+ raise(BadPath, "Bad path (#{remote_path} != #{res["path"]})") unless remote_path == res["path"]
162
+ raise(RuntimeError, "Mode on #{@path} changed between file and dir -- not supported yet") unless dir? == res["is_dir"]
159
163
  update_modification_info(res)
160
164
  end
161
165
 
@@ -168,16 +172,16 @@ module Dbox
168
172
  end
169
173
 
170
174
  def dir?
171
- raise "not implemented"
175
+ raise RuntimeError, "Not implemented"
172
176
  end
173
177
 
174
- def create_local; raise "not implemented"; end
175
- def delete_local; raise "not implemented"; end
176
- def update_local; raise "not implemented"; end
178
+ def create_local; raise RuntimeError, "Not implemented"; end
179
+ def delete_local; raise RuntimeError, "Not implemented"; end
180
+ def update_local; raise RuntimeError, "Not implemented"; end
177
181
 
178
- def create_remote; raise "not implemented"; end
179
- def delete_remote; raise "not implemented"; end
180
- def update_remote; raise "not implemented"; end
182
+ def create_remote; raise RuntimeError, "Not implemented"; end
183
+ def delete_remote; raise RuntimeError, "Not implemented"; end
184
+ def update_remote; raise RuntimeError, "Not implemented"; end
181
185
 
182
186
  def modified?(last)
183
187
  !(revision == last.revision && modified_at == last.modified_at)
@@ -187,6 +191,15 @@ module Dbox
187
191
  File.utime(Time.now, modified_at, local_path)
188
192
  end
189
193
 
194
+ # this downloads the metadata about this blob from the server and
195
+ # overwrites the metadata & timestamp
196
+ # IMPORTANT: should only be called if you are CERTAIN the file is up to date
197
+ def force_metadata_update_from_server
198
+ res = api.metadata(remote_path)
199
+ update_modification_info(res)
200
+ update_file_timestamp
201
+ end
202
+
190
203
  def saving_parent_timestamp(&proc)
191
204
  parent = File.dirname(local_path)
192
205
  DB.saving_timestamp(parent, &proc)
@@ -207,19 +220,21 @@ module Dbox
207
220
  end
208
221
 
209
222
  def update(res)
210
- raise "not a directory" unless res["is_dir"]
223
+ raise(ArgumentError, "Not a directory: #{res.inspect}") unless res["is_dir"]
211
224
  super(res)
212
225
  @contents_hash = res["hash"] if res.has_key?("hash")
213
226
  if res.has_key?("contents")
214
227
  old_contents = @contents
215
228
  new_contents_arr = remove_dotfiles(res["contents"]).map do |c|
216
- if last_entry = old_contents[c["path"]]
229
+ p = @db.remote_to_relative_path(c["path"])
230
+ if last_entry = old_contents[p]
217
231
  new_entry = last_entry.clone
218
232
  last_entry.freeze
219
233
  new_entry.update(c)
220
- [c["path"], new_entry]
234
+ [new_entry.path, new_entry]
221
235
  else
222
- [c["path"], smart_new(c)]
236
+ new_entry = smart_new(c)
237
+ [new_entry.path, new_entry]
223
238
  end
224
239
  end
225
240
  @contents = Hash[new_contents_arr]
@@ -233,28 +248,28 @@ module Dbox
233
248
  def pull
234
249
  prev = self.clone
235
250
  prev.freeze
236
- log.info "Pulling changes"
237
251
  res = api.metadata(remote_path)
238
252
  update(res)
239
253
  if contents_hash != prev.contents_hash
240
- reconcile(prev, :down)
254
+ changes = reconcile(prev, :down)
255
+ else
256
+ changes = { :created => [], :deleted => [], :updated => [] }
241
257
  end
242
- subdirs.each {|d| d.pull }
258
+ subdirs.inject(changes) {|c, d| merge_changes(c, d.pull) }
243
259
  end
244
260
 
245
261
  def push
246
262
  prev = self.clone
247
263
  prev.freeze
248
- log.info "Pushing changes"
249
264
  res = gather_info(@path)
250
265
  update(res)
251
- reconcile(prev, :up)
252
- subdirs.each {|d| d.push }
266
+ changes = reconcile(prev, :up)
267
+ subdirs.inject(changes) {|c, d| merge_changes(c, d.push) }
253
268
  end
254
269
 
255
270
  def reconcile(prev, direction)
256
- old_paths = prev.contents.keys
257
- new_paths = contents.keys
271
+ old_paths = prev.contents.keys.sort
272
+ new_paths = contents.keys.sort
258
273
 
259
274
  deleted_paths = old_paths - new_paths
260
275
 
@@ -268,15 +283,21 @@ module Dbox
268
283
  deleted_paths.each {|p| prev.contents[p].delete_local }
269
284
  created_paths.each {|p| contents[p].create_local }
270
285
  stale_paths.each {|p| contents[p].update_local }
286
+ { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
271
287
  when :up
272
288
  deleted_paths.each {|p| prev.contents[p].delete_remote }
273
289
  created_paths.each {|p| contents[p].create_remote }
274
290
  stale_paths.each {|p| contents[p].update_remote }
291
+ { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
275
292
  else
276
- raise "Invalid direction: #{direction.inspect}"
293
+ raise(ArgumentError, "Invalid sync direction: #{direction.inspect}")
277
294
  end
278
295
  end
279
296
 
297
+ def merge_changes(old, new)
298
+ old.merge(new) {|k, v1, v2| v1 + v2 }
299
+ end
300
+
280
301
  def gather_info(rel, list_contents=true)
281
302
  full = @db.relative_to_local_path(rel)
282
303
  remote = @db.relative_to_remote_path(rel)
@@ -303,7 +324,6 @@ module Dbox
303
324
  end
304
325
 
305
326
  def create_local
306
- log.info "Creating dir: #{local_path}"
307
327
  saving_parent_timestamp do
308
328
  FileUtils.mkdir_p(local_path)
309
329
  update_file_timestamp
@@ -318,12 +338,12 @@ module Dbox
318
338
  end
319
339
 
320
340
  def update_local
321
- log.info "Updating dir: #{local_path}"
322
341
  update_file_timestamp
323
342
  end
324
343
 
325
344
  def create_remote
326
345
  api.create_dir(remote_path)
346
+ force_metadata_update_from_server
327
347
  end
328
348
 
329
349
  def delete_remote
@@ -354,10 +374,8 @@ module Dbox
354
374
  end
355
375
 
356
376
  def create_local
357
- log.info "Creating file: #{local_path}"
358
377
  saving_parent_timestamp do
359
378
  download
360
- update_file_timestamp
361
379
  end
362
380
  end
363
381
 
@@ -369,9 +387,7 @@ module Dbox
369
387
  end
370
388
 
371
389
  def update_local
372
- log.info "Updating file: #{local_path}"
373
390
  download
374
- update_file_timestamp
375
391
  end
376
392
 
377
393
  def create_remote
@@ -399,6 +415,7 @@ module Dbox
399
415
  File.open(local_path) do |f|
400
416
  res = api.put_file(remote_path, f)
401
417
  end
418
+ force_metadata_update_from_server
402
419
  end
403
420
  end
404
421
  end
@@ -3,119 +3,183 @@ require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
3
3
  include FileUtils
4
4
 
5
5
  describe Dbox do
6
+ before(:all) do
7
+ clear_test_log
8
+ end
9
+
6
10
  before(:each) do
7
- cd LOCAL_TEST_PATH
11
+ log.info example.full_description
8
12
  @name = randname()
9
13
  @local = File.join(LOCAL_TEST_PATH, @name)
10
14
  @remote = File.join(REMOTE_TEST_PATH, @name)
11
15
  end
12
16
 
17
+ after(:each) do
18
+ log.info ""
19
+ end
20
+
13
21
  describe "#create" do
14
22
  it "creates the local directory" do
15
- Dbox.create(@remote)
16
- File.exists?(@local).should be_true
23
+ Dbox.create(@remote, @local)
24
+ @local.should exist
17
25
  end
18
26
 
19
- xit "should fail if the remote already exists" do
20
- Dbox.create(@remote)
27
+ it "should fail if the remote already exists" do
28
+ Dbox.create(@remote, @local)
21
29
  rm_rf @local
22
- expect { Dbox.create(@remote) }.to raise_error("Remote path already exists")
23
- File.exists?(@local).should be_false
30
+ expect { Dbox.create(@remote, @local) }.to raise_error(Dbox::RemoteAlreadyExists)
31
+ @local.should_not exist
24
32
  end
25
33
  end
26
34
 
27
35
  describe "#clone" do
28
36
  it "creates the local directory" do
29
- Dbox.create(@remote)
37
+ Dbox.create(@remote, @local)
30
38
  rm_rf @local
31
- File.exists?(@local).should be_false
32
- Dbox.clone(@remote)
33
- File.exists?(@local).should be_true
39
+ @local.should_not exist
40
+ Dbox.clone(@remote, @local)
41
+ @local.should exist
34
42
  end
35
43
 
36
44
  it "should fail if the remote does not exist" do
37
- expect { Dbox.clone(@remote) }.to raise_error("Remote path does not exist")
38
- File.exists?(@local).should be_false
45
+ expect { Dbox.clone(@remote, @local) }.to raise_error(Dbox::RemoteMissing)
46
+ @local.should_not exist
39
47
  end
40
48
  end
41
49
 
42
50
  describe "#pull" do
43
51
  it "should fail if the local dir is missing" do
44
- expect { Dbox.pull(@local) }.to raise_error(/No DB file found/)
52
+ expect { Dbox.pull(@local) }.to raise_error(Dbox::MissingDatabase)
45
53
  end
46
54
 
47
55
  it "should fail if the remote dir is missing" do
48
- Dbox.create(@remote)
56
+ Dbox.create(@remote, @local)
49
57
  modify_dbfile {|s| s.sub(/^remote_path: \/.*$/, "remote_path: /#{randname()}") }
50
- expect { Dbox.pull(@local) }.to raise_error("Remote path does not exist")
58
+ expect { Dbox.pull(@local) }.to raise_error(Dbox::RemoteMissing)
51
59
  end
52
60
 
53
61
  it "should be able to pull" do
54
- Dbox.create(@remote)
55
- expect { Dbox.pull(@local) }.to_not raise_error
56
- end
57
-
58
- it "should be able to pull from inside the dir" do
59
- Dbox.create(@remote)
60
- cd @local
61
- expect { Dbox.pull }.to_not raise_error
62
+ Dbox.create(@remote, @local)
63
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
62
64
  end
63
65
 
64
66
  it "should be able to pull changes" do
65
- Dbox.create(@remote)
66
- File.exists?("#{@local}/hello.txt").should be_false
67
+ Dbox.create(@remote, @local)
68
+ "#{@local}/hello.txt".should_not exist
67
69
 
68
- cd ALTERNATE_LOCAL_TEST_PATH
69
- Dbox.clone(@remote)
70
- cd @name
71
- touch "hello.txt"
72
- Dbox.push
70
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
71
+ Dbox.clone(@remote, @alternate)
72
+ touch "#{@alternate}/hello.txt"
73
+ Dbox.push(@alternate).should eql(:created => ["hello.txt"], :deleted => [], :updated => [])
73
74
 
74
- expect { Dbox.pull(@local) }.to_not raise_error
75
- File.exists?("#{@local}/hello.txt").should be_true
75
+ Dbox.pull(@local).should eql(:created => ["hello.txt"], :deleted => [], :updated => [])
76
+ "#{@local}/hello.txt".should exist
76
77
  end
77
78
 
78
79
  it "should be able to pull after deleting a file and not have the file re-created" do
79
- Dbox.create(@remote)
80
- cd @name
81
- touch "hello.txt"
82
- Dbox.push
83
- Dbox.pull
84
- rm "hello.txt"
85
- Dbox.pull
86
- File.exists?("#{@local}/hello.txt").should be_false
80
+ Dbox.create(@remote, @local)
81
+ touch "#{@local}/hello.txt"
82
+ Dbox.push(@local).should eql(:created => ["hello.txt"], :deleted => [], :updated => [])
83
+ rm "#{@local}/hello.txt"
84
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
85
+ "#{@local}/hello.txt".should_not exist
86
+ end
87
+
88
+ it "should handle a complex set of changes" do
89
+ Dbox.create(@remote, @local)
90
+
91
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
92
+ Dbox.clone(@remote, @alternate)
93
+
94
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
95
+
96
+ touch "#{@alternate}/foo.txt"
97
+ touch "#{@alternate}/bar.txt"
98
+ touch "#{@alternate}/baz.txt"
99
+ Dbox.push(@alternate).should eql(:created => ["bar.txt", "baz.txt", "foo.txt"], :deleted => [], :updated => [])
100
+
101
+ Dbox.pull(@local).should eql(:created => ["bar.txt", "baz.txt", "foo.txt"], :deleted => [], :updated => [])
102
+
103
+ sleep 1
104
+ mkdir "#{@alternate}/subdir"
105
+ touch "#{@alternate}/subdir/one.txt"
106
+ rm "#{@alternate}/foo.txt"
107
+ File.open("#{@alternate}/baz.txt", "w") {|f| f << "baaz" }
108
+ Dbox.push(@alternate).should eql(:created => ["subdir", "subdir/one.txt"], :deleted => ["foo.txt"], :updated => ["baz.txt"])
109
+
110
+ Dbox.pull(@local).should eql(:created => ["subdir", "subdir/one.txt"], :deleted => ["foo.txt"], :updated => ["baz.txt"])
87
111
  end
88
112
  end
89
113
 
90
114
  describe "#push" do
91
115
  it "should fail if the local dir is missing" do
92
- expect { Dbox.push(@local) }.to raise_error(/No DB file found/)
116
+ expect { Dbox.push(@local) }.to raise_error(Dbox::MissingDatabase)
93
117
  end
94
118
 
95
119
  it "should be able to push" do
96
- Dbox.create(@remote)
97
- expect { Dbox.push(@local) }.to_not raise_error
98
- end
99
-
100
- it "should be able to push from inside the dir" do
101
- Dbox.create(@remote)
102
- cd @local
103
- expect { Dbox.push }.to_not raise_error
120
+ Dbox.create(@remote, @local)
121
+ Dbox.push(@local).should eql(:created => [], :deleted => [], :updated => [])
104
122
  end
105
123
 
106
124
  it "should be able to push new file" do
107
- Dbox.create(@remote)
108
- touch File.join(@local, "foo.txt")
109
- expect { Dbox.push(@local) }.to_not raise_error
125
+ Dbox.create(@remote, @local)
126
+ touch "#{@local}/foo.txt"
127
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
110
128
  end
111
129
 
112
130
  it "should create the remote dir if it is missing" do
113
- Dbox.create(@remote)
114
- touch File.join(@local, "foo.txt")
131
+ Dbox.create(@remote, @local)
132
+ touch "#{@local}/foo.txt"
115
133
  @new_name = randname()
116
134
  @new_remote = File.join(REMOTE_TEST_PATH, @new_name)
117
135
  modify_dbfile {|s| s.sub(/^remote_path: \/.*$/, "remote_path: #{@new_remote}") }
118
- expect { Dbox.push(@local) }.to_not raise_error
136
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
137
+ end
138
+
139
+ it "should be able to push nested content" do
140
+ Dbox.create(@remote, @local)
141
+ mkdir "#{@local}/subdir"
142
+ touch "#{@local}/subdir/foo.txt"
143
+ Dbox.push(@local).should eql(:created => ["subdir", "subdir/foo.txt"], :deleted => [], :updated => [])
144
+ end
145
+
146
+ it "should not re-download the file after creating" do
147
+ Dbox.create(@remote, @local)
148
+ touch "#{@local}/foo.txt"
149
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
150
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
151
+ end
152
+
153
+ it "should not re-download the file after updating" do
154
+ Dbox.create(@remote, @local)
155
+ touch "#{@local}/foo.txt"
156
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
157
+ sleep 1
158
+ File.open("#{@local}/foo.txt", "w") {|f| f << "fooz" }
159
+ Dbox.push(@local).should eql(:created => [], :deleted => [], :updated => ["foo.txt"])
160
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
161
+ end
162
+
163
+ it "should not re-download the dir after creating" do
164
+ Dbox.create(@remote, @local)
165
+ mkdir "#{@local}/subdir"
166
+ Dbox.push(@local).should eql(:created => ["subdir"], :deleted => [], :updated => [])
167
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
168
+ end
169
+
170
+ it "should handle a complex set of changes" do
171
+ Dbox.create(@remote, @local)
172
+ touch "#{@local}/foo.txt"
173
+ touch "#{@local}/bar.txt"
174
+ touch "#{@local}/baz.txt"
175
+ Dbox.push(@local).should eql(:created => ["bar.txt", "baz.txt", "foo.txt"], :deleted => [], :updated => [])
176
+ sleep 1
177
+ mkdir "#{@local}/subdir"
178
+ touch "#{@local}/subdir/one.txt"
179
+ rm "#{@local}/foo.txt"
180
+ touch "#{@local}/baz.txt"
181
+ Dbox.push(@local).should eql(:created => ["subdir", "subdir/one.txt"], :deleted => ["foo.txt"], :updated => ["baz.txt"])
182
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
119
183
  end
120
184
  end
121
185
  end
@@ -11,7 +11,13 @@ FileUtils.mkdir_p(ALTERNATE_LOCAL_TEST_PATH)
11
11
 
12
12
  REMOTE_TEST_PATH = "/dbox_test_dirs"
13
13
 
14
- LOGGER = Logger.new(File.expand_path(File.join(File.dirname(__FILE__), "..", "tmp", "test.log")))
14
+ $started_at ||= Time.now
15
+
16
+ LOGFILE = File.expand_path(File.join(File.dirname(__FILE__), "..", "tmp", "test.log"))
17
+ LOGGER = Logger.new(LOGFILE)
18
+ LOGGER.formatter = proc do |severity, datetime, progname, msg|
19
+ format "[%4.1fs] [%s] %s\n", (Time.now - $started_at), severity, msg
20
+ end
15
21
 
16
22
  def randname
17
23
  u = `uuidgen`.chomp
@@ -24,3 +30,17 @@ def modify_dbfile
24
30
  s = yield s
25
31
  File.open(dbfile, "w") {|f| f << s }
26
32
  end
33
+
34
+ def clear_test_log
35
+ File.open(LOGFILE, "w") {|f| f << "" }
36
+ end
37
+
38
+ def log
39
+ LOGGER
40
+ end
41
+
42
+ RSpec::Matchers.define :exist do
43
+ match do |actual|
44
+ File.exists?(actual) == true
45
+ end
46
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbox
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
8
+ - 3
9
9
  - 0
10
- version: 0.2.0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ken Pratt