Class: Dbox::Syncer::Push

Inherits:
Operation show all
Defined in:
lib/dbox/syncer.rb

Instance Attribute Summary

Attributes inherited from Operation

#database

Instance Method Summary collapse

Methods inherited from Operation

#api, #current_dir_entries_as_hash, #gather_remote_info, #generate_tmpfilename, #local_path, #lookup_id_by_path, #metadata, #process_basic_remote_props, #remote_path, #remove_dotfiles, #remove_tmpfiles, #saving_parent_timestamp, #saving_timestamp, #sort_changelist, #update_file_timestamp

Methods included from Utils

#calculate_hash, #case_insensitive_difference, #case_insensitive_equal, #case_insensitive_join, #case_insensitive_resolve, #find_nonconflicting_path, #local_to_relative_path, #parse_time, #relative_to_local_path, #relative_to_remote_path, #remote_to_relative_path, #time_to_s, #times_equal?

Methods included from Loggable

included, #log

Constructor Details

#initialize(database, api) ⇒ Push

Returns a new instance of Push.



419
420
421
# File 'lib/dbox/syncer.rb', line 419

def initialize(database, api)
  super(database, api)
end

Instance Method Details

#calculate_changes(dir) ⇒ Object

Raises:

  • (ArgumentError)


520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
# 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) ⇒ Object



593
594
595
596
597
# 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) ⇒ Object



599
600
601
602
# 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) ⇒ Object



604
605
606
607
# File 'lib/dbox/syncer.rb', line 604

def delete_file(file)
  remote_path = file[:remote_path]
  api.delete_file(remote_path)
end

#executeObject



429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# 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)
        (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])
            (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])
            (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) ⇒ Object



618
619
620
621
622
623
624
# File 'lib/dbox/syncer.rb', line 618

def (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) ⇒ Object



569
570
571
# File 'lib/dbox/syncer.rb', line 569

def is_dir(path)
  File.directory?(path)
end

#list_contents(dir) ⇒ Object



587
588
589
590
591
# 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) ⇒ Boolean

Returns:

  • (Boolean)


573
574
575
576
577
578
579
580
581
582
583
584
585
# 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) ⇒ Object



565
566
567
# File 'lib/dbox/syncer.rb', line 565

def mtime(path)
  File.mtime(path)
end

#practiceObject



423
424
425
426
427
# 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) ⇒ Object



609
610
611
612
613
614
615
616
# 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