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