lib/dbox/db.rb in dbox-0.2.0 vs lib/dbox/db.rb in dbox-0.3.0
- old
+ new
@@ -1,35 +1,37 @@
module Dbox
+ class MissingDatabase < RuntimeError; end
+ class BadPath < RuntimeError; end
+
class DB
include Loggable
DB_FILE = ".dropbox.db"
attr_accessor :local_path
def self.create(remote_path, local_path)
- log.info "Creating remote folder: #{remote_path}"
api.create_dir(remote_path)
clone(remote_path, local_path)
end
def self.clone(remote_path, local_path)
log.info "Cloning #{remote_path} into #{local_path}"
res = api.metadata(remote_path)
- raise "Remote path error" unless remote_path == res["path"]
+ raise(BadPath, "Remote path error") unless remote_path == res["path"]
db = new(local_path, res)
db.pull
end
def self.load(local_path)
db_file = db_file(local_path)
if File.exists?(db_file)
db = File.open(db_file, "r") {|f| YAML::load(f.read) }
- db.local_path = File.expand_path(local_path)
+ db.local_path = local_path
db
else
- raise "No DB file found in #{local_path}"
+ raise MissingDatabase, "No DB file found in #{local_path}"
end
end
def self.pull(local_path)
load(local_path).pull
@@ -40,11 +42,11 @@
end
# IMPORTANT: DropboxDb.new is private. Please use DropboxDb.create, DropboxDb.clone, or DropboxDb.load as the entry point.
private_class_method :new
def initialize(local_path, res)
- @local_path = File.expand_path(local_path)
+ @local_path = local_path
@remote_path = res["path"]
FileUtils.mkdir_p(@local_path)
@root = DropboxDir.new(self, res)
save
end
@@ -54,32 +56,34 @@
File.open(db_file, "w") {|f| f << YAML::dump(self) }
end
end
def pull
- @root.pull
+ res = @root.pull
save
+ res
end
def push
- @root.push
+ res = @root.push
save
+ res
end
def local_to_relative_path(path)
if path.include?(@local_path)
path.sub(@local_path, "").sub(/^\//, "")
else
- raise "Not a local path: #{path}"
+ raise BadPath, "Not a local path: #{path}"
end
end
def remote_to_relative_path(path)
if path.include?(@remote_path)
path.sub(@remote_path, "").sub(/^\//, "")
else
- raise "Not a remote path: #{path}"
+ raise BadPath, "Not a remote path: #{path}"
end
end
def relative_to_local_path(path)
if path.any?
@@ -152,12 +156,12 @@
DropboxFile.new(@db, res)
end
end
def update(res)
- raise "bad path (#{remote_path} != #{res["path"]})" unless remote_path == res["path"]
- 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?
+ raise(BadPath, "Bad path (#{remote_path} != #{res["path"]})") unless remote_path == res["path"]
+ raise(RuntimeError, "Mode on #{@path} changed between file and dir -- not supported yet") unless dir? == res["is_dir"]
update_modification_info(res)
end
def local_path
@db.relative_to_local_path(@path)
@@ -166,29 +170,38 @@
def remote_path
@db.relative_to_remote_path(@path)
end
def dir?
- raise "not implemented"
+ raise RuntimeError, "Not implemented"
end
- def create_local; raise "not implemented"; end
- def delete_local; raise "not implemented"; end
- def update_local; raise "not implemented"; end
+ def create_local; raise RuntimeError, "Not implemented"; end
+ def delete_local; raise RuntimeError, "Not implemented"; end
+ def update_local; raise RuntimeError, "Not implemented"; end
- def create_remote; raise "not implemented"; end
- def delete_remote; raise "not implemented"; end
- def update_remote; raise "not implemented"; end
+ def create_remote; raise RuntimeError, "Not implemented"; end
+ def delete_remote; raise RuntimeError, "Not implemented"; end
+ def update_remote; raise RuntimeError, "Not implemented"; end
def modified?(last)
!(revision == last.revision && modified_at == last.modified_at)
end
def update_file_timestamp
File.utime(Time.now, modified_at, local_path)
end
+ # this downloads the metadata about this blob from the server and
+ # overwrites the metadata & timestamp
+ # IMPORTANT: should only be called if you are CERTAIN the file is up to date
+ def force_metadata_update_from_server
+ res = api.metadata(remote_path)
+ update_modification_info(res)
+ update_file_timestamp
+ end
+
def saving_parent_timestamp(&proc)
parent = File.dirname(local_path)
DB.saving_timestamp(parent, &proc)
end
@@ -205,23 +218,25 @@
@contents = {}
super(db, res)
end
def update(res)
- raise "not a directory" unless res["is_dir"]
+ raise(ArgumentError, "Not a directory: #{res.inspect}") unless res["is_dir"]
super(res)
@contents_hash = res["hash"] if res.has_key?("hash")
if res.has_key?("contents")
old_contents = @contents
new_contents_arr = remove_dotfiles(res["contents"]).map do |c|
- if last_entry = old_contents[c["path"]]
+ p = @db.remote_to_relative_path(c["path"])
+ if last_entry = old_contents[p]
new_entry = last_entry.clone
last_entry.freeze
new_entry.update(c)
- [c["path"], new_entry]
+ [new_entry.path, new_entry]
else
- [c["path"], smart_new(c)]
+ new_entry = smart_new(c)
+ [new_entry.path, new_entry]
end
end
@contents = Hash[new_contents_arr]
end
end
@@ -231,32 +246,32 @@
end
def pull
prev = self.clone
prev.freeze
- log.info "Pulling changes"
res = api.metadata(remote_path)
update(res)
if contents_hash != prev.contents_hash
- reconcile(prev, :down)
+ changes = reconcile(prev, :down)
+ else
+ changes = { :created => [], :deleted => [], :updated => [] }
end
- subdirs.each {|d| d.pull }
+ subdirs.inject(changes) {|c, d| merge_changes(c, d.pull) }
end
def push
prev = self.clone
prev.freeze
- log.info "Pushing changes"
res = gather_info(@path)
update(res)
- reconcile(prev, :up)
- subdirs.each {|d| d.push }
+ changes = reconcile(prev, :up)
+ subdirs.inject(changes) {|c, d| merge_changes(c, d.push) }
end
def reconcile(prev, direction)
- old_paths = prev.contents.keys
- new_paths = contents.keys
+ old_paths = prev.contents.keys.sort
+ new_paths = contents.keys.sort
deleted_paths = old_paths - new_paths
created_paths = new_paths - old_paths
@@ -266,19 +281,25 @@
case direction
when :down
deleted_paths.each {|p| prev.contents[p].delete_local }
created_paths.each {|p| contents[p].create_local }
stale_paths.each {|p| contents[p].update_local }
+ { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
when :up
deleted_paths.each {|p| prev.contents[p].delete_remote }
created_paths.each {|p| contents[p].create_remote }
stale_paths.each {|p| contents[p].update_remote }
+ { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
else
- raise "Invalid direction: #{direction.inspect}"
+ raise(ArgumentError, "Invalid sync direction: #{direction.inspect}")
end
end
+ def merge_changes(old, new)
+ old.merge(new) {|k, v1, v2| v1 + v2 }
+ end
+
def gather_info(rel, list_contents=true)
full = @db.relative_to_local_path(rel)
remote = @db.relative_to_remote_path(rel)
attrs = {
@@ -301,11 +322,10 @@
def dir?
true
end
def create_local
- log.info "Creating dir: #{local_path}"
saving_parent_timestamp do
FileUtils.mkdir_p(local_path)
update_file_timestamp
end
end
@@ -316,16 +336,16 @@
FileUtils.rm_r(local_path)
end
end
def update_local
- log.info "Updating dir: #{local_path}"
update_file_timestamp
end
def create_remote
api.create_dir(remote_path)
+ force_metadata_update_from_server
end
def delete_remote
api.delete_dir(remote_path)
end
@@ -352,14 +372,12 @@
def dir?
false
end
def create_local
- log.info "Creating file: #{local_path}"
saving_parent_timestamp do
download
- update_file_timestamp
end
end
def delete_local
log.info "Deleting file: #{local_path}"
@@ -367,13 +385,11 @@
FileUtils.rm_rf(local_path)
end
end
def update_local
- log.info "Updating file: #{local_path}"
download
- update_file_timestamp
end
def create_remote
upload
end
@@ -397,9 +413,10 @@
def upload
File.open(local_path) do |f|
res = api.put_file(remote_path, f)
end
+ force_metadata_update_from_server
end
end
end
end