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 +82 -10
- data/TODO.txt +4 -5
- data/VERSION +1 -1
- data/bin/dbox +14 -4
- data/dbox.gemspec +1 -1
- data/lib/dbox.rb +16 -14
- data/lib/dbox/api.rb +52 -22
- data/lib/dbox/db.rb +54 -37
- data/spec/dbox_spec.rb +120 -56
- data/spec/spec_helper.rb +21 -1
- metadata +3 -3
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
|
-
|
66
|
-
|
65
|
+
Using dbox from the Command-Line
|
66
|
+
--------------------------------
|
67
67
|
|
68
|
-
###
|
68
|
+
### Usage
|
69
|
+
|
70
|
+
#### Authorize
|
69
71
|
|
70
72
|
```sh
|
71
73
|
$ dbox authorize
|
72
74
|
```
|
73
75
|
|
74
|
-
|
76
|
+
#### Create a new Dropbox folder
|
75
77
|
|
76
78
|
```sh
|
77
79
|
$ dbox create <remote_path> [<local_path>]
|
78
80
|
```
|
79
81
|
|
80
|
-
|
82
|
+
#### Clone an existing Dropbox folder
|
81
83
|
|
82
84
|
```sh
|
83
85
|
$ dbox clone <remote_path> [<local_path>]
|
84
86
|
```
|
85
87
|
|
86
|
-
|
88
|
+
#### Pull (download changes from Dropbox)
|
87
89
|
|
88
90
|
```sh
|
89
91
|
$ dbox pull [<local_path>]
|
90
92
|
```
|
91
93
|
|
92
|
-
|
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
|
-
*
|
5
|
-
*
|
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
|
6
|
+
* Add support for partial push/pull
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
data/dbox.gemspec
CHANGED
data/lib/dbox.rb
CHANGED
@@ -17,36 +17,38 @@ module Dbox
|
|
17
17
|
Dbox::API.authorize
|
18
18
|
end
|
19
19
|
|
20
|
-
def self.create(remote_path, local_path
|
20
|
+
def self.create(remote_path, local_path)
|
21
21
|
remote_path = clean_remote_path(remote_path)
|
22
|
-
local_path
|
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
|
26
|
+
def self.clone(remote_path, local_path)
|
27
27
|
remote_path = clean_remote_path(remote_path)
|
28
|
-
local_path
|
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
|
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
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
data/lib/dbox/api.rb
CHANGED
@@ -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
|
50
|
+
def run(path)
|
45
51
|
path = escape_path(path)
|
46
|
-
log.debug "Fetching metadata for #{path}"
|
47
52
|
begin
|
48
|
-
|
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 "
|
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
|
66
|
+
raise RuntimeError, "Unexpected result: #{res.inspect}"
|
55
67
|
end
|
56
68
|
rescue DropboxError => e
|
57
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
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",
|
data/lib/dbox/db.rb
CHANGED
@@ -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 =
|
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 =
|
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 "
|
158
|
-
raise "
|
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 "
|
175
|
+
raise RuntimeError, "Not implemented"
|
172
176
|
end
|
173
177
|
|
174
|
-
def create_local; raise "
|
175
|
-
def delete_local; raise "
|
176
|
-
def update_local; raise "
|
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 "
|
179
|
-
def delete_remote; raise "
|
180
|
-
def update_remote; raise "
|
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 "
|
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
|
-
|
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
|
-
[
|
234
|
+
[new_entry.path, new_entry]
|
221
235
|
else
|
222
|
-
|
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.
|
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.
|
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
|
data/spec/dbox_spec.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
23
|
+
Dbox.create(@remote, @local)
|
24
|
+
@local.should exist
|
17
25
|
end
|
18
26
|
|
19
|
-
|
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(
|
23
|
-
|
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
|
-
|
32
|
-
Dbox.clone(@remote)
|
33
|
-
|
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(
|
38
|
-
|
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(
|
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(
|
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
|
-
|
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
|
-
|
67
|
+
Dbox.create(@remote, @local)
|
68
|
+
"#{@local}/hello.txt".should_not exist
|
67
69
|
|
68
|
-
|
69
|
-
Dbox.clone(@remote)
|
70
|
-
|
71
|
-
|
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
|
-
|
75
|
-
|
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
|
-
|
81
|
-
|
82
|
-
|
83
|
-
Dbox.pull
|
84
|
-
|
85
|
-
|
86
|
-
|
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(
|
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
|
-
|
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
|
109
|
-
|
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
|
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
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -11,7 +11,13 @@ FileUtils.mkdir_p(ALTERNATE_LOCAL_TEST_PATH)
|
|
11
11
|
|
12
12
|
REMOTE_TEST_PATH = "/dbox_test_dirs"
|
13
13
|
|
14
|
-
|
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