dbox 0.1.0

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