dbox 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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