class Dbox::Syncer::Push

Public Class Methods

new(database, api) click to toggle source
Calls superclass method Dbox::Syncer::Operation::new
# File lib/dbox/syncer.rb, line 419
def initialize(database, api)
  super(database, api)
end

Public Instance Methods

calculate_changes(dir) click to toggle source
# File lib/dbox/syncer.rb, line 520
def calculate_changes(dir)
  raise(ArgumentError, "Not a directory: #{dir.inspect}") unless dir[:is_dir]

  out = []
  recur_dirs = []

  existing_entries = current_dir_entries_as_hash(dir)
  child_paths = list_contents(dir).sort

  child_paths.each do |p|
    local_path = relative_to_local_path(p)
    remote_path = relative_to_remote_path(p)
    c = {
      :path => p,
      :local_path => local_path,
      :remote_path => remote_path,
      :modified => mtime(local_path),
      :is_dir => is_dir(local_path),
      :parent_path => dir[:path],
      :local_hash => calculate_hash(local_path)
    }
    if entry = existing_entries[p]
      c[:id] = entry[:id]
      recur_dirs << c if c[:is_dir] # queue dir for later
      out << [:update, c] if modified?(entry, c) # update iff modified
    else
      # create
      out << [:create, c]
      recur_dirs << c if c[:is_dir]
    end
  end

  # add any deletions
  out += case_insensitive_difference(existing_entries.keys, child_paths).map do |p|
    [:delete, existing_entries[p]]
  end

  # recursively process new & existing subdirectories
  recur_dirs.each do |dir|
    out += calculate_changes(dir)
  end

  out
end
create_dir(dir) click to toggle source
# File lib/dbox/syncer.rb, line 593
def create_dir(dir)
  remote_path = dir[:remote_path]
  log.info "Creating #{remote_path}"
  api.create_dir(remote_path)
end
delete_dir(dir) click to toggle source
# File lib/dbox/syncer.rb, line 599
def delete_dir(dir)
  remote_path = dir[:remote_path]
  api.delete_dir(remote_path)
end
delete_file(file) click to toggle source
# File lib/dbox/syncer.rb, line 604
def delete_file(file)
  remote_path = file[:remote_path]
  api.delete_file(remote_path)
end
execute() click to toggle source
# File lib/dbox/syncer.rb, line 429
def execute
  dir = database.root_dir
  changes = calculate_changes(dir)
  log.debug "Executing changes:\n" + changes.map {|c| c.inspect }.join("\n")
  changelist = { :created => [], :deleted => [], :updated => [], :failed => [] }

  changes.each do |op, c|
    case op
    when :create
      c[:parent_id] ||= lookup_id_by_path(c[:parent_path])

      if c[:is_dir]
        # create the remote directiory
        create_dir(c)
        database.add_entry(c[:path], true, c[:parent_id], nil, nil, nil, nil)
        force_metadata_update_from_server(c)
        changelist[:created] << c[:path]
      else
        # upload a new file
        begin
          local_hash = calculate_hash(c[:local_path])
          res = upload_file(c)
          database.add_entry(c[:path], false, c[:parent_id], nil, nil, nil, local_hash)
          if case_insensitive_equal(c[:path], res[:path])
            force_metadata_update_from_server(c)
            changelist[:created] << c[:path]
          else
            log.warn "#{c[:path]} had a conflict and was renamed to #{res[:path]} on the server"
            changelist[:conflicts] ||= []
            changelist[:conflicts] << { :original => c[:path], :renamed => res[:path] }
          end
        rescue => e
          log.error "Error while uploading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
          changelist[:failed] << { :operation => :create, :path => c[:path], :error => e }
        end
      end
    when :update
      existing = database.find_by_path(c[:path])
      unless existing[:is_dir] == c[:is_dir]
        raise(RuntimeError, "Mode on #{c[:path]} changed between file and dir -- not supported yet")
      end

      # only update files -- nothing to do to update a dir
      if !c[:is_dir]
        # upload changes to a file
        begin
          local_hash = calculate_hash(c[:local_path])
          res = upload_file(c)
          database.update_entry_by_path(c[:path], :local_hash => local_hash)
          if case_insensitive_equal(c[:path], res[:path])
            force_metadata_update_from_server(c)
            changelist[:updated] << c[:path]
          else
            log.warn "#{c[:path]} had a conflict and was renamed to #{res[:path]} on the server"
            changelist[:conflicts] ||= []
            changelist[:conflicts] << { :original => c[:path], :renamed => res[:path] }
          end
        rescue => e
          log.error "Error while uploading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
          changelist[:failed] << { :operation => :update, :path => c[:path], :error => e }
        end
      end
    when :delete
      # delete a remote file/directory
      begin
        begin
          if c[:is_dir]
            delete_dir(c)
          else
            delete_file(c)
          end
        rescue Dbox::RemoteMissing
          # safe to delete even if remote is already gone
        end
        database.delete_entry_by_path(c[:path])
        changelist[:deleted] << c[:path]
      rescue => e
        log.error "Error while deleting #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
        changelist[:failed] << { :operation => :delete, :path => c[:path], :error => e }
      end
    when :failed
      changelist[:failed] << { :operation => c[:operation], :path => c[:path], :error => c[:error] }
    else
      raise(RuntimeError, "Unknown operation type: #{op}")
    end
  end

  # sort & return output
  sort_changelist(changelist)
end
force_metadata_update_from_server(entry) click to toggle source
# File lib/dbox/syncer.rb, line 618
def force_metadata_update_from_server(entry)
  res = gather_remote_info(entry)
  unless res == :not_modified
    database.update_entry_by_path(entry[:path], :modified => res[:modified], :revision => res[:revision], :remote_hash => res[:remote_hash])
  end
  update_file_timestamp(database.find_by_path(entry[:path]))
end
is_dir(path) click to toggle source
# File lib/dbox/syncer.rb, line 569
def is_dir(path)
  File.directory?(path)
end
list_contents(dir) click to toggle source
# File lib/dbox/syncer.rb, line 587
def list_contents(dir)
  local_path = dir[:local_path]
  paths = Dir.entries(local_path).reject {|s| s == "." || s == ".." || s.start_with?(".") }
  paths.map {|p| local_to_relative_path(File.join(local_path, p)) }
end
modified?(entry, res) click to toggle source
# File lib/dbox/syncer.rb, line 573
def modified?(entry, res)
  out = true
  if entry[:is_dir]
    out = !times_equal?(entry[:modified], res[:modified])
    log.debug "#{entry[:path]} modified? t#{time_to_s(entry[:modified])} vs. t#{time_to_s(res[:modified])} => #{out}"
  else
    eh = entry[:local_hash]
    rh = res[:local_hash]
    out = !(eh && rh && eh == rh)
    log.debug "#{entry[:path]} modified? #{eh} vs. #{rh} => #{out}"
  end
  out
end
mtime(path) click to toggle source
# File lib/dbox/syncer.rb, line 565
def mtime(path)
  File.mtime(path)
end
practice() click to toggle source
# File lib/dbox/syncer.rb, line 423
def practice
  dir = database.root_dir
  changes = calculate_changes(dir)
  log.debug "Changes that would be executed:\n" + changes.map {|c| c.inspect }.join("\n")
end
upload_file(file) click to toggle source
# File lib/dbox/syncer.rb, line 609
def upload_file(file)
  local_path = file[:local_path]
  remote_path = file[:remote_path]
  db_entry = database.find_by_path(file[:path])
  last_revision = db_entry ? db_entry[:revision] : nil
  res = api.put_file(remote_path, local_path, last_revision)
  process_basic_remote_props(res)
end