dbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Ken Pratt <ken@kenpratt.net>
4
+ Copyright (c) 2011 Ruboss Technology Corp <peter@ruboss.com>
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining
7
+ a copy of this software and associated documentation files (the
8
+ "Software"), to deal in the Software without restriction, including
9
+ without limitation the rights to use, copy, modify, merge, publish,
10
+ distribute, sublicense, and/or sell copies of the Software, and to
11
+ permit persons to whom the Software is furnished to do so, subject to
12
+ the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be
15
+ included in all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
21
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
22
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
23
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,132 @@
1
+ dbox
2
+ ====
3
+
4
+ An easy way to push and pull your Dropbox folders, with fine-grained control over what folder you are syncing, where you are syncing it to, and when you are doing it.
5
+
6
+ **IMPORTANT:** This is **not** an automated Dropbox client. It will exit after sucessfully pushing/pulling, so if you want regular updates, you can run it in cron, a loop, etc.
7
+
8
+
9
+ Installation
10
+ ------------
11
+
12
+ ### Get developer keys
13
+
14
+ * Follow the instructions at https://www.dropbox.com/developers/quickstart to create a Dropbox development application, and copy the application keys.
15
+
16
+ * Now either set the keys as environment variables:
17
+
18
+ ```sh
19
+ $ export DROPBOX_APP_KEY=cmlrrjd3j0gbend
20
+ $ export DROPBOX_APP_SECRET=uvuulp75xf9jffl
21
+ ```
22
+
23
+ * Or include them in calls to dbox:
24
+
25
+ ```sh
26
+ $ DROPBOX_APP_KEY=cmlrrjd3j0gbend DROPBOX_APP_SECRET=uvuulp75xf9jffl dbox ...
27
+ ```
28
+ ### Generate an auth token
29
+
30
+ * Make an authorize request:
31
+
32
+ ```sh
33
+ $ dbox authorize
34
+ Please visit the following URL in your browser, log into Dropbox, and authorize the app you created.
35
+
36
+ http://www.dropbox.com/0/oauth/authorize?oauth_token=j2kuzfvobcpqh0g
37
+
38
+ When you have done so, press [ENTER] to continue.
39
+ ```
40
+
41
+ * Visit the given URL in your browser, and then go back to the terminal and press Enter.
42
+
43
+ * Now either set the keys as environment variables:
44
+
45
+ ```sh
46
+ $ export DROPBOX_AUTH_KEY=v4d7l1rez1czksn
47
+ $ export DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4
48
+ ```
49
+
50
+ * Or include them in calls to dbox:
51
+
52
+ ```sh
53
+ $ DROPBOX_AUTH_KEY=v4d7l1rez1czksn DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4 dbox ...
54
+ ```
55
+
56
+ * 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.
57
+
58
+
59
+ Usage
60
+ -----
61
+
62
+ ### Authorize
63
+
64
+ ```sh
65
+ $ dbox authorize
66
+ ```
67
+
68
+ ### Clone an existing Dropbox folder
69
+
70
+ ```sh
71
+ $ dbox clone <remote_path> [<local_path>]
72
+ ```
73
+
74
+ ### Create a new Dropbox folder
75
+
76
+ ```sh
77
+ $ dbox create <remote_path> [<local_path>]
78
+ ```
79
+
80
+ ### Pull (download changes from Dropbox)
81
+
82
+ ```sh
83
+ $ dbox pull [<local_path>]
84
+ ```
85
+
86
+ ### Push (upload changes to Dropbox)
87
+
88
+ ```sh
89
+ $ dbox push [<local_path>]
90
+ ```
91
+
92
+
93
+ Example
94
+ -------
95
+
96
+ ```sh
97
+ $ export DROPBOX_APP_KEY=cmlrrjd3j0gbend
98
+ $ export DROPBOX_APP_SECRET=uvuulp75xf9jffl
99
+ ```
100
+
101
+ ```sh
102
+ $ dbox authorize
103
+ ```
104
+
105
+ ```sh
106
+ $ open http://www.dropbox.com/0/oauth/authorize?oauth_token=aaoeuhtns123456
107
+ ```
108
+
109
+ ```sh
110
+ $ export DROPBOX_AUTH_KEY=v4d7l1rez1czksn
111
+ $ export DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4
112
+ ```
113
+
114
+ ```sh
115
+ $ cd /tmp
116
+ $ dbox clone Public
117
+ $ cd Public
118
+ $ echo "Hello World" > hello.txt
119
+ $ dbox push
120
+ ```
121
+
122
+ ```sh
123
+ $ cat ~/Dropbox/Public/hello.txt
124
+ Hello World
125
+ $ echo "Oh, Hello" > ~/Dropbox/Public/hello.txt
126
+ ```
127
+
128
+ ```sh
129
+ $ dbox pull
130
+ $ cat hello.txt
131
+ Oh, Hello
132
+ ```
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ require 'jeweler'
7
+ Jeweler::Tasks.new do |gem|
8
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
9
+ gem.name = "dbox"
10
+ gem.homepage = "http://github.com/kenpratt/dbox"
11
+ gem.license = "MIT"
12
+ gem.summary = "Dropbox made easy."
13
+ gem.description = "An easy-to-use Dropbox client with fine-grained control over syncs."
14
+ gem.email = "ken@kenpratt.net"
15
+ gem.authors = ["Ken Pratt"]
16
+ gem.executables = ["dbox"]
17
+ end
18
+ Jeweler::RubygemsDotOrgTasks.new
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/test_*.rb'
31
+ test.verbose = true
32
+ test.rcov_opts << '--exclude "gems/*"'
33
+ end
34
+
35
+ task :default => :test
data/TODO.txt ADDED
@@ -0,0 +1,8 @@
1
+ * Add tests
2
+ * Add proper logger
3
+ * See if pull re-creates locally-deleted files (it shouldn't)
4
+ * Look down directory tree until you hit a .dropbox.db file
5
+ * Solve upload -> re-download issue
6
+ * Add rename_remote command
7
+ * Add a "sync" command that pushes and pulls in one go
8
+ * Add support for partial push/pull? (also, make it defoult behaviour from subdir?)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/bin/dbox ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
4
+ require "dbox"
5
+
6
+ # usage line
7
+ def usage
8
+ "Usage:
9
+ dbox authorize
10
+ export DROPBOX_AUTH_KEY=abcdef012345678
11
+ export DROPBOX_AUTH_SECRET=876543210fedcba
12
+ dbox create <remote_path> [<local_path>]
13
+ dbox clone <remote_path> [<local_path>]
14
+ dbox pull
15
+ dbox push"
16
+ end
17
+ def print_usage_and_quit; puts usage; exit 1; end
18
+
19
+ # ensure that push/pull arg was given
20
+ print_usage_and_quit unless ARGV.size >= 1
21
+
22
+ command = ARGV[0]
23
+ rest = ARGV[1..-1]
24
+
25
+ # execute the command
26
+ case command
27
+ when "authorize"
28
+ Dbox.authorize
29
+ when "create", "clone"
30
+ unless rest.size >= 1
31
+ puts "Error: Please provide a remote path to clone"
32
+ print_usage_and_quit
33
+ end
34
+ Dbox.send(command, *rest)
35
+ when "pull", "push"
36
+ Dbox.send(command, *rest)
37
+ else
38
+ print_usage_and_quit
39
+ end
data/lib/dbox.rb ADDED
@@ -0,0 +1,50 @@
1
+ ROOT_PATH = File.expand_path(File.join(File.dirname(__FILE__), ".."))
2
+ $:.unshift File.join(ROOT_PATH, "lib")
3
+ $:.unshift File.join(ROOT_PATH, "vendor", "dropbox-client-ruby", "lib")
4
+
5
+ require "dropbox"
6
+ require "fileutils"
7
+ require "time"
8
+ require "yaml"
9
+
10
+ require "dbox/api"
11
+ require "dbox/db"
12
+
13
+ module Dbox
14
+ def self.authorize
15
+ Dbox::API.authorize
16
+ end
17
+
18
+ def self.create(remote_path, local_path = nil)
19
+ remote_path = clean_remote_path(remote_path)
20
+ local_path ||= remote_path.split("/").last
21
+ Dbox::DB.create(remote_path, local_path)
22
+ end
23
+
24
+ def self.clone(remote_path, local_path = nil)
25
+ remote_path = clean_remote_path(remote_path)
26
+ local_path ||= remote_path.split("/").last
27
+ Dbox::DB.clone(remote_path, local_path)
28
+ end
29
+
30
+ def self.pull(local_path = nil)
31
+ local_path ||= "."
32
+ Dbox::DB.pull(local_path)
33
+ end
34
+
35
+ def self.push(local_path = nil)
36
+ local_path ||= "."
37
+ Dbox::DB.push(local_path)
38
+ end
39
+
40
+ private
41
+
42
+ def self.clean_remote_path(path)
43
+ if path
44
+ path.sub(/\/$/,'')
45
+ path[0] == "/" ? path : "/#{path}"
46
+ else
47
+ raise "Missing remote path"
48
+ end
49
+ end
50
+ end
data/lib/dbox/api.rb ADDED
@@ -0,0 +1,106 @@
1
+ module Dbox
2
+ class API
3
+ def self.authorize
4
+ puts conf.inspect
5
+ auth = Authenticator.new(conf)
6
+ puts auth.inspect
7
+ authorize_url = auth.get_request_token
8
+ puts "Please visit the following URL in your browser, log into Dropbox, and authorize the app you created.\n\n#{authorize_url}\n\nWhen you have done so, press [ENTER] to continue."
9
+ STDIN.readline
10
+ res = auth.get_access_token
11
+ puts "export DROPBOX_AUTH_KEY=#{res.token}"
12
+ puts "export DROPBOX_AUTH_SECRET=#{res.secret}"
13
+ puts
14
+ puts "This auth token will last for 10 years, or when you choose to invalidate it, whichever comes first."
15
+ puts
16
+ puts "Now either include these constants in yours calls to dbox, or set them as environment variables."
17
+ puts "In bash, including them in calls looks like:"
18
+ puts "$ DROPBOX_AUTH_KEY=#{res.token} DROPBOX_AUTH_SECRET=#{res.secret} dbox ..."
19
+ end
20
+
21
+ def self.connect
22
+ api = new()
23
+ api.connect
24
+ api
25
+ end
26
+
27
+ # IMPORTANT: API.new is private. Please use API.authorize or API.connect as the entry point.
28
+ private_class_method :new
29
+ def initialize
30
+ @conf = self.class.conf
31
+ end
32
+
33
+ def connect
34
+ auth_key = ENV["DROPBOX_AUTH_KEY"]
35
+ auth_secret = ENV["DROPBOX_AUTH_SECRET"]
36
+
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
39
+
40
+ @auth = Authenticator.new(@conf, auth_key, auth_secret)
41
+ @client = DropboxClient.new(@conf["server"], @conf["content_server"], @conf["port"], @auth)
42
+ end
43
+
44
+ def metadata(path = "/")
45
+ path = escape_path(path)
46
+ puts "[api] fetching metadata for #{path}"
47
+ @client.metadata(@conf["root"], path)
48
+ end
49
+
50
+ def create_dir(path)
51
+ path = escape_path(path)
52
+ puts "[api] creating #{path}"
53
+ @client.file_create_folder(@conf["root"], path)
54
+ end
55
+
56
+ def delete_dir(path)
57
+ path = escape_path(path)
58
+ puts "[api] deleting #{path}"
59
+ @client.file_delete(@conf["root"], path)
60
+ end
61
+
62
+ def get_file(path)
63
+ path = escape_path(path)
64
+ puts "[api] downloading #{path}"
65
+ @client.get_file(@conf["root"], path)
66
+ end
67
+
68
+ def put_file(path, file_obj)
69
+ path = escape_path(path)
70
+ puts "[api] uploading #{path}"
71
+ dir = File.dirname(path)
72
+ name = File.basename(path)
73
+ @client.put_file(@conf["root"], dir, name, file_obj)
74
+ end
75
+
76
+ def delete_file(path)
77
+ path = escape_path(path)
78
+ puts "[api] deleting #{path}"
79
+ @client.file_delete(@conf["root"], path)
80
+ end
81
+
82
+ def escape_path(path)
83
+ URI.escape(path)
84
+ end
85
+
86
+ def self.conf
87
+ app_key = ENV["DROPBOX_APP_KEY"]
88
+ app_secret = ENV["DROPBOX_APP_SECRET"]
89
+
90
+ raise("Please set the DROPBOX_APP_KEY environment variable to a Dropbox application key") unless app_key
91
+ raise("Please set the DROPBOX_APP_SECRET environment variable to a Dropbox application secret") unless app_secret
92
+
93
+ {
94
+ "server" => "api.dropbox.com",
95
+ "content_server" => "api-content.dropbox.com",
96
+ "port" => 80,
97
+ "request_token_url" => "http://api.dropbox.com/0/oauth/request_token",
98
+ "access_token_url" => "http://api.dropbox.com/0/oauth/access_token",
99
+ "authorization_url" => "http://www.dropbox.com/0/oauth/authorize",
100
+ "root" => "dropbox",
101
+ "consumer_key" => app_key,
102
+ "consumer_secret" => app_secret
103
+ }
104
+ end
105
+ end
106
+ end
data/lib/dbox/db.rb ADDED
@@ -0,0 +1,407 @@
1
+ module Dbox
2
+ class DB
3
+ DB_FILE = ".dropbox.db"
4
+
5
+ attr_accessor :local_path
6
+
7
+ def self.create(remote_path, local_path)
8
+ puts "[db] Creating remote folder: #{remote_path}"
9
+ api.create_dir(remote_path)
10
+ clone(remote_path, local_path)
11
+ end
12
+
13
+ def self.clone(remote_path, local_path)
14
+ puts "[db] Cloning #{remote_path} into #{local_path}"
15
+ case res = api.metadata(remote_path)
16
+ when Hash
17
+ raise "Remote path error" unless remote_path == res["path"]
18
+ db = new(local_path, res)
19
+ db.pull
20
+ when Net::HTTPNotFound
21
+ raise "Remote path does not exist"
22
+ else
23
+ raise "Clone failed: #{res.inspect}"
24
+ end
25
+ end
26
+
27
+ def self.load(local_path)
28
+ db_file = db_file(local_path)
29
+ if File.exists?(db_file)
30
+ db = File.open(db_file, "r") {|f| YAML::load(f.read) }
31
+ db.local_path = File.expand_path(local_path)
32
+ db
33
+ else
34
+ raise "No DB file found in #{local_path}"
35
+ end
36
+ end
37
+
38
+ def self.pull(local_path)
39
+ load(local_path).pull
40
+ end
41
+
42
+ def self.push(local_path)
43
+ load(local_path).push
44
+ end
45
+
46
+ # IMPORTANT: DropboxDb.new is private. Please use DropboxDb.create, DropboxDb.clone, or DropboxDb.load as the entry point.
47
+ private_class_method :new
48
+ def initialize(local_path, res)
49
+ @local_path = File.expand_path(local_path)
50
+ @remote_path = res["path"]
51
+ FileUtils.mkdir_p(@local_path)
52
+ @root = DropboxDir.new(self, res)
53
+ save
54
+ end
55
+
56
+ def save
57
+ self.class.saving_timestamp(@local_path) do
58
+ File.open(db_file, "w") {|f| f << YAML::dump(self) }
59
+ end
60
+ end
61
+
62
+ def pull
63
+ @root.pull
64
+ save
65
+ end
66
+
67
+ def push
68
+ @root.push
69
+ save
70
+ end
71
+
72
+ def local_to_relative_path(path)
73
+ if path.include?(@local_path)
74
+ path.sub(@local_path, "").sub(/^\//, "")
75
+ else
76
+ raise "Not a local path: #{path}"
77
+ end
78
+ end
79
+
80
+ def remote_to_relative_path(path)
81
+ if path.include?(@remote_path)
82
+ path.sub(@remote_path, "").sub(/^\//, "")
83
+ else
84
+ raise "Not a remote path: #{path}"
85
+ end
86
+ end
87
+
88
+ def relative_to_local_path(path)
89
+ if path.any?
90
+ File.join(@local_path, path)
91
+ else
92
+ @local_path
93
+ end
94
+ end
95
+
96
+ def relative_to_remote_path(path)
97
+ if path.any?
98
+ File.join(@remote_path, path)
99
+ else
100
+ @remote_path
101
+ end
102
+ end
103
+
104
+ def self.saving_timestamp(path)
105
+ mtime = File.mtime(path)
106
+ yield
107
+ File.utime(Time.now, mtime, path)
108
+ end
109
+
110
+ def self.api
111
+ @api ||= API.connect
112
+ end
113
+
114
+ def api
115
+ self.class.api
116
+ end
117
+
118
+ def self.db_file(local_path)
119
+ File.join(local_path, DB_FILE)
120
+ end
121
+
122
+ def db_file
123
+ self.class.db_file(@local_path)
124
+ end
125
+
126
+ class DropboxBlob
127
+ attr_reader :path, :revision, :modified_at
128
+
129
+ def initialize(db, res)
130
+ @db = db
131
+ @path = @db.remote_to_relative_path(res["path"])
132
+ update_modification_info(res)
133
+ end
134
+
135
+ def update_modification_info(res)
136
+ last_modified_at = @modified_at
137
+ @modified_at = case t = res["modified"]
138
+ when Time
139
+ t
140
+ when String
141
+ Time.parse(t)
142
+ end
143
+ if res.has_key?("revision")
144
+ @revision = res["revision"]
145
+ else
146
+ @revision = -1 if @modified_at != last_modified_at
147
+ end
148
+ end
149
+
150
+ def smart_new(res)
151
+ if res["is_dir"]
152
+ DropboxDir.new(@db, res)
153
+ else
154
+ DropboxFile.new(@db, res)
155
+ end
156
+ end
157
+
158
+ def update(res)
159
+ raise "bad path (#{remote_path} != #{res["path"]})" unless remote_path == res["path"]
160
+ 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
+ update_modification_info(res)
162
+ end
163
+
164
+ def local_path
165
+ @db.relative_to_local_path(@path)
166
+ end
167
+
168
+ def remote_path
169
+ @db.relative_to_remote_path(@path)
170
+ end
171
+
172
+ def dir?
173
+ raise "not implemented"
174
+ end
175
+
176
+ def create_local; raise "not implemented"; end
177
+ def delete_local; raise "not implemented"; end
178
+ def update_local; raise "not implemented"; end
179
+
180
+ def create_remote; raise "not implemented"; end
181
+ def delete_remote; raise "not implemented"; end
182
+ def update_remote; raise "not implemented"; end
183
+
184
+ def modified?(last)
185
+ !(revision == last.revision && modified_at == last.modified_at)
186
+ end
187
+
188
+ def update_file_timestamp
189
+ File.utime(Time.now, modified_at, local_path)
190
+ end
191
+
192
+ def saving_parent_timestamp(&proc)
193
+ parent = File.dirname(local_path)
194
+ DB.saving_timestamp(parent, &proc)
195
+ end
196
+
197
+ def api
198
+ @db.api
199
+ end
200
+ end
201
+
202
+ class DropboxDir < DropboxBlob
203
+ attr_reader :contents_hash, :contents
204
+
205
+ def initialize(db, res)
206
+ @contents_hash = nil
207
+ @contents = {}
208
+ super(db, res)
209
+ end
210
+
211
+ def update(res)
212
+ raise "not a directory" unless res["is_dir"]
213
+ super(res)
214
+ @contents_hash = res["hash"] if res.has_key?("hash")
215
+ if res.has_key?("contents")
216
+ old_contents = @contents
217
+ new_contents_arr = remove_dotfiles(res["contents"]).map do |c|
218
+ if last_entry = old_contents[c["path"]]
219
+ new_entry = last_entry.clone
220
+ last_entry.freeze
221
+ new_entry.update(c)
222
+ [c["path"], new_entry]
223
+ else
224
+ [c["path"], smart_new(c)]
225
+ end
226
+ end
227
+ @contents = Hash[new_contents_arr]
228
+ end
229
+ end
230
+
231
+ def remove_dotfiles(contents)
232
+ contents.reject {|c| File.basename(c["path"]).start_with?(".") }
233
+ end
234
+
235
+ def pull
236
+ prev = self.clone
237
+ prev.freeze
238
+ puts "[db] pulling"
239
+ res = api.metadata(remote_path)
240
+ update(res)
241
+ if contents_hash != prev.contents_hash
242
+ reconcile(prev, :down)
243
+ end
244
+ subdirs.each {|d| d.pull }
245
+ end
246
+
247
+ def push
248
+ prev = self.clone
249
+ prev.freeze
250
+ puts "[db] pushing"
251
+ res = gather_info(@path)
252
+ update(res)
253
+ reconcile(prev, :up)
254
+ subdirs.each {|d| d.push }
255
+ end
256
+
257
+ def reconcile(prev, direction)
258
+ old_paths = prev.contents.keys
259
+ new_paths = contents.keys
260
+
261
+ deleted_paths = old_paths - new_paths
262
+
263
+ created_paths = new_paths - old_paths
264
+
265
+ kept_paths = old_paths & new_paths
266
+ stale_paths = kept_paths.select {|p| contents[p].modified?(prev.contents[p]) }
267
+
268
+ case direction
269
+ when :down
270
+ deleted_paths.each {|p| prev.contents[p].delete_local }
271
+ created_paths.each {|p| contents[p].create_local }
272
+ stale_paths.each {|p| contents[p].update_local }
273
+ when :up
274
+ deleted_paths.each {|p| prev.contents[p].delete_remote }
275
+ created_paths.each {|p| contents[p].create_remote }
276
+ stale_paths.each {|p| contents[p].update_remote }
277
+ else
278
+ raise "Invalid direction: #{direction.inspect}"
279
+ end
280
+ end
281
+
282
+ def gather_info(rel, list_contents=true)
283
+ full = @db.relative_to_local_path(rel)
284
+ remote = @db.relative_to_remote_path(rel)
285
+
286
+ attrs = {
287
+ "path" => remote,
288
+ "is_dir" => File.directory?(full),
289
+ "modified" => File.mtime(full)
290
+ }
291
+
292
+ if attrs["is_dir"] && list_contents
293
+ contents = Dir[File.join(full, "*")]
294
+ attrs["contents"] = contents.map do |f|
295
+ r = @db.local_to_relative_path(f)
296
+ gather_info(r, false)
297
+ end
298
+ end
299
+
300
+ attrs
301
+ end
302
+
303
+ def dir?
304
+ true
305
+ end
306
+
307
+ def create_local
308
+ puts "[fs] creating dir #{local_path}"
309
+ saving_parent_timestamp do
310
+ FileUtils.mkdir_p(local_path)
311
+ update_file_timestamp
312
+ end
313
+ end
314
+
315
+ def delete_local
316
+ puts "[fs] deleting dir #{local_path}"
317
+ saving_parent_timestamp do
318
+ FileUtils.rm_r(local_path)
319
+ end
320
+ end
321
+
322
+ def update_local
323
+ puts "[fs] updating dir #{local_path}"
324
+ update_file_timestamp
325
+ end
326
+
327
+ def create_remote
328
+ api.create_dir(remote_path)
329
+ end
330
+
331
+ def delete_remote
332
+ api.delete_dir(remote_path)
333
+ end
334
+
335
+ def update_remote
336
+ # do nothing
337
+ end
338
+
339
+ def subdirs
340
+ @contents.values.select {|c| c.dir? }
341
+ end
342
+
343
+ def print
344
+ puts
345
+ puts "#{path} (v#{revision}, #{modified_at})"
346
+ contents.each do |path, c|
347
+ puts " #{c.path} (v#{c.revision}, #{c.modified_at})"
348
+ end
349
+ puts
350
+ end
351
+ end
352
+
353
+ class DropboxFile < DropboxBlob
354
+ def dir?
355
+ false
356
+ end
357
+
358
+ def create_local
359
+ puts "[fs] creating file #{local_path}"
360
+ saving_parent_timestamp do
361
+ download
362
+ update_file_timestamp
363
+ end
364
+ end
365
+
366
+ def delete_local
367
+ puts "[fs] deleting file #{local_path}"
368
+ saving_parent_timestamp do
369
+ FileUtils.rm_rf(local_path)
370
+ end
371
+ end
372
+
373
+ def update_local
374
+ puts "[fs] updating file #{local_path}"
375
+ download
376
+ update_file_timestamp
377
+ end
378
+
379
+ def create_remote
380
+ upload
381
+ end
382
+
383
+ def delete_remote
384
+ api.delete_file(remote_path)
385
+ end
386
+
387
+ def update_remote
388
+ upload
389
+ end
390
+
391
+ def download
392
+ res = api.get_file(remote_path)
393
+
394
+ File.open(local_path, "w") do |f|
395
+ f << res
396
+ end
397
+ update_file_timestamp
398
+ end
399
+
400
+ def upload
401
+ File.open(local_path) do |f|
402
+ res = api.put_file(remote_path, f)
403
+ end
404
+ end
405
+ end
406
+ end
407
+ end