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 +5 -0
- data/LICENSE.txt +23 -0
- data/README.md +132 -0
- data/Rakefile +35 -0
- data/TODO.txt +8 -0
- data/VERSION +1 -0
- data/bin/dbox +39 -0
- data/lib/dbox.rb +50 -0
- data/lib/dbox/api.rb +106 -0
- data/lib/dbox/db.rb +407 -0
- data/test/helper.rb +10 -0
- data/test/test_dbox.rb +7 -0
- data/vendor/dropbox-client-ruby/LICENSE +20 -0
- data/vendor/dropbox-client-ruby/README +15 -0
- data/vendor/dropbox-client-ruby/Rakefile +41 -0
- data/vendor/dropbox-client-ruby/config/testing.json.example +16 -0
- data/vendor/dropbox-client-ruby/lib/dropbox.rb +226 -0
- data/vendor/dropbox-client-ruby/manifest +9 -0
- data/vendor/dropbox-client-ruby/test/authenticator_test.rb +53 -0
- data/vendor/dropbox-client-ruby/test/client_test.rb +100 -0
- data/vendor/dropbox-client-ruby/test/util.rb +21 -0
- metadata +86 -0
data/.document
ADDED
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
|