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