lib/dbox/db.rb in dbox-0.4.2 vs lib/dbox/db.rb in dbox-0.4.3

- old
+ new

@@ -63,19 +63,15 @@ File.open(db_file, "w") {|f| f << YAML::dump(self) } end end def pull - res = @root.pull - save - res + @root.pull end def push - res = @root.push - save - res + @root.push end def move(new_remote_path) api.move(@remote_path, new_remote_path) @remote_path = new_remote_path @@ -146,38 +142,30 @@ @path = @db.remote_to_relative_path(res["path"]) update_modification_info(res) end def update_modification_info(res) + 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"] last_modified_at = @modified_at - @modified_at = case t = res["modified"] - when Time - t - when String - Time.parse(t) - end - if res.has_key?("revision") + @modified_at = parse_time(res["modified"]) + if res["revision"] @revision = res["revision"] else @revision = -1 if @modified_at != last_modified_at end + log.debug "updated modification info on #{path.inspect}: r#{@revision}, #{@modified_at}" end def smart_new(res) if res["is_dir"] DropboxDir.new(@db, res) else DropboxFile.new(@db, res) end end - def update(res) - 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) end def remote_path @@ -186,24 +174,62 @@ def dir? raise RuntimeError, "Not implemented" end + def create(direction) + case direction + when :down + create_local + when :up + create_remote + end + end + + def update(direction) + case direction + when :down + update_local + when :up + update_remote + end + end + + def delete(direction) + case direction + when :down + delete_local + when :up + delete_remote + end + 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 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) + def modified?(res) + out = !(@revision == res["revision"] && @modified_at == parse_time(res["modified"])) + log.debug "#{path}.modified? r#{@revision} =? r#{res["revision"]}, #{@modified_at} =? #{parse_time(res["modified"])} => #{out}" + out end + def parse_time(t) + case t + when Time + t + when String + Time.parse(t) + end + end + def update_file_timestamp - File.utime(Time.now, modified_at, local_path) + 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 @@ -230,124 +256,154 @@ @contents_hash = nil @contents = {} super(db, res) end - def update(res) - 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| - 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) - [new_entry.path, new_entry] - else - new_entry = smart_new(c) - [new_entry.path, new_entry] - end - end - @contents = Hash[new_contents_arr] - end - end - - def remove_dotfiles(contents) - contents.reject {|c| File.basename(c["path"]).start_with?(".") } - end - def pull - prev = self.clone - prev.freeze + # calculate changes on this dir res = api.metadata(remote_path) - update(res) - if contents_hash != prev.contents_hash - changes = reconcile(prev, :down) - else - changes = { :created => [], :deleted => [], :updated => [] } - end - subdirs.inject(changes) {|c, d| merge_changes(c, d.pull) } + changes = calculate_changes(res) + + # execute changes on this dir + changelist = execute_changes(changes, :down) + + # recur on subdirs, expanding changelist as we go + changelist = subdirs.inject(changelist) {|c, d| merge_changelists(c, d.pull) } + + # only update the modification info on the directory once all descendants are updated + update_modification_info(res) + + # return changes + @db.save + changelist end def push - prev = self.clone - prev.freeze - res = gather_info(@path) - update(res) - changes = reconcile(prev, :up) - subdirs.inject(changes) {|c, d| merge_changes(c, d.push) } - end + # calculate changes on this dir + res = gather_local_info(@path) + changes = calculate_changes(res) - def reconcile(prev, direction) - old_paths = prev.contents.keys.sort - new_paths = contents.keys.sort + # execute changes on this dir + changelist = execute_changes(changes, :up) - deleted_paths = old_paths - new_paths + # recur on subdirs, expanding changelist as we go + changelist = subdirs.inject(changelist) {|c, d| merge_changelists(c, d.push) } - created_paths = new_paths - old_paths + # only update the modification info on the directory once all descendants are updated + update_modification_info(res) - kept_paths = old_paths & new_paths - stale_paths = kept_paths.select {|p| contents[p].modified?(prev.contents[p]) } + # return changes + @db.save + changelist + end - 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 } + def calculate_changes(res) + raise(ArgumentError, "Not a directory: #{res.inspect}") unless res["is_dir"] + + if @contents_hash && res["hash"] && @contents_hash == res["hash"] + # dir hash hasn't changed -- no need to calculate changes + [] + elsif res["contents"] + # dir has changed -- calculate changes on contents + out = [] + got_paths = [] + + remove_dotfiles(res["contents"]).each do |c| + p = @db.remote_to_relative_path(c["path"]) + c["rel_path"] = p + got_paths << p + + if @contents.has_key?(p) + # only update file if it's been modified + if @contents[p].modified?(c) + out << [:update, c] + end + else + out << [:create, c] + end + end + out += (@contents.keys.sort - got_paths.sort).map {|p| [:delete, { "rel_path" => p }] } + out else - raise(ArgumentError, "Invalid sync direction: #{direction.inspect}") + raise(RuntimeError, "Trying to calculate dir changes without any contents") end end - def merge_changes(old, new) - old.merge(new) {|k, v1, v2| v1 + v2 } + def execute_changes(changes, direction) + log.debug "executing changes: #{changes.inspect}" + changelist = { :created => [], :deleted => [], :updated => [] } + changes.each do |op, c| + case op + when :create + e = smart_new(c) + e.create(direction) + @contents[e.path] = e + changelist[:created] << e.path + when :update + e = @contents[c["rel_path"]] + e.update_modification_info(c) if direction == :down + e.update(direction) + changelist[:updated] << e.path + when :delete + e = @contents[c["rel_path"]] + e.delete(direction) + @contents.delete(e.path) + changelist[:deleted] << e.path + else + raise(RuntimeError, "Unknown operation type: #{op}") + end + @db.save + end + changelist.keys.each {|k| changelist[k].sort! } + changelist end - def gather_info(rel, list_contents=true) + def merge_changelists(old, new) + old.merge(new) {|k, v1, v2| (v1 + v2).sort } + end + + def gather_local_info(rel, list_contents=true) full = @db.relative_to_local_path(rel) remote = @db.relative_to_remote_path(rel) attrs = { "path" => remote, "is_dir" => File.directory?(full), - "modified" => File.mtime(full) + "modified" => File.mtime(full), + "revision" => @contents[rel] ? @contents[rel].revision : nil } if attrs["is_dir"] && list_contents contents = Dir.entries(full).reject {|s| s == "." || s == ".." } attrs["contents"] = contents.map do |s| p = File.join(full, s) r = @db.local_to_relative_path(p) - gather_info(r, false) + gather_local_info(r, false) end end attrs end + def remove_dotfiles(contents) + contents.reject {|c| File.basename(c["path"]).start_with?(".") } + end + def dir? true end def create_local + log.info "Creating #{local_path}" saving_parent_timestamp do FileUtils.mkdir_p(local_path) update_file_timestamp end end def delete_local - log.info "Deleting dir: #{local_path}" + log.info "Deleting #{local_path}" saving_parent_timestamp do FileUtils.rm_r(local_path) end end @@ -372,11 +428,11 @@ @contents.values.select {|c| c.dir? } end def print puts - puts "#{path} (v#{revision}, #{modified_at})" + puts "#{path} (v#{@revision}, #{@modified_at})" contents.each do |path, c| puts " #{c.path} (v#{c.revision}, #{c.modified_at})" end puts end @@ -425,10 +481,10 @@ update_file_timestamp end def upload File.open(local_path) do |f| - res = api.put_file(remote_path, f) + api.put_file(remote_path, f) end force_metadata_update_from_server end end end