module Doing::Util::Backup

Backup utils

Public Instance Methods

clear_redo(filename) click to toggle source

Delete all redo files

@param limit Maximum number of backups to retain

# File lib/doing/util_backup.rb, line 32
def clear_redo(filename)
  filename ||= Doing.setting('doing_file')
  backups = Dir.glob("undone*___#{File.basename(filename)}", base: backup_dir).sort.reverse
  backups.each do |file|
    FileUtils.rm(File.join(backup_dir, file))
  end
end
clear_undone(filename = nil) click to toggle source
# File lib/doing/util_backup.rb, line 103
def clear_undone(filename = nil)
  filename ||= Doing.setting('doing_file')
  # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
  Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
    FileUtils.rm(File.join(backup_dir, f))
  end
end
last_backup(filename = nil, count: 1) click to toggle source

Retrieve the most recent backup

@param filename The filename @return [String] filename

# File lib/doing/util_backup.rb, line 46
def last_backup(filename = nil, count: 1)
  filename ||= Doing.setting('doing_file')

  backup = get_backups(filename).slice(count - 1)
  backup.nil? ? nil : File.join(backup_dir, backup)
end
prune_backups(filename, limit = 10) click to toggle source

Delete all but most recent 5 backups

@param limit Maximum number of backups to retain

# File lib/doing/util_backup.rb, line 16
def prune_backups(filename, limit = 10)
  backups = get_backups(filename)
  return unless backups.count > limit

  backups[limit..-1].each do |file|
    FileUtils.rm(File.join(backup_dir, file))
  end

  clear_redo(filename)
end
redo_backup(filename = nil, count: 1) click to toggle source

Undo last undo

@param filename The filename

# File lib/doing/util_backup.rb, line 79
def redo_backup(filename = nil, count: 1)
  filename ||= Doing.setting('doing_file')
  # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
  undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort.reverse
  total = undones.count
  count = total if count > total

  skipped = undones.slice!(0, count)
  undone = skipped.pop

  raise DoingRuntimeError, 'End of redo history' if undone.nil?

  redo_file = File.join(backup_dir, undone)

  FileUtils.move(redo_file, filename)

  skipped.each do |f|
    FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
  end

  Doing.logger.warn('File update:', "restored undo step #{count}/#{total}")
  Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
end
restore_last_backup(filename = nil, count: 1) click to toggle source

Restore the most recent backup. If a filename is provided, only backups of that filename will be used.

@param filename The filename to restore, if different from default

# File lib/doing/util_backup.rb, line 60
def restore_last_backup(filename = nil, count: 1)
  Doing.logger.benchmark(:restore_backup, :start)
  filename ||= Doing.setting('doing_file')

  backup_file = last_backup(filename, count: count)
  raise DoingRuntimeError, 'End of undo history' if backup_file.nil?

  save_undone(filename)
  FileUtils.mv(backup_file, filename)
  prune_backups_after(File.basename(backup_file))
  Doing.logger.warn('File update:', "restored from #{backup_file}")
  Doing.logger.benchmark(:restore_backup, :finish)
end
select_backup(filename = nil) click to toggle source

Select from recent backups. If a filename is provided, only backups of that filename will be used.

@param filename The filename to restore

# File lib/doing/util_backup.rb, line 157
def select_backup(filename = nil)
  filename ||= Doing.setting('doing_file')

  options = get_backups(filename).each_with_object([]) do |file, arr|
    d, _base = date_of_backup(file)
    next if d.nil?
    arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
  end

  raise DoingRuntimeError, 'No backup files to load' if options.empty?

  backup_file = show_menu(options, filename)
  Util.write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
  FileUtils.mv(backup_file, filename)
  prune_backups_after(File.basename(backup_file))
  Doing.logger.warn('File update:', "restored from #{backup_file}")
end
select_redo(filename = nil) click to toggle source

Select from recent undos. If a filename is provided, only backups of that filename will be used.

@param filename The filename to restore

# File lib/doing/util_backup.rb, line 117
def select_redo(filename = nil)
  filename ||= Doing.setting('doing_file')

  undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort

  raise DoingRuntimeError, 'End of redo history' if undones.empty?

  total = undones.count
  options = undones.each_with_object([]) do |file, arr|
    d, _base = date_of_backup(file)
    next if d.nil?

    arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
  end

  raise DoingRuntimeError, 'No backup files to load' if options.empty?

  backup_file = show_menu(options, filename)
  idx = undones.index(File.basename(backup_file))
  skipped = undones.slice!(idx, undones.count - idx)
  undone = skipped.shift

  redo_file = File.join(backup_dir, undone)

  FileUtils.move(redo_file, filename)

  skipped.each do |f|
    FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
  end

  Doing.logger.warn('File update:', "restored undo step #{idx}/#{total}")
  Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
end
show_menu(options, filename) click to toggle source
# File lib/doing/util_backup.rb, line 175
def show_menu(options, filename)
  if TTY::Which.which('colordiff')
    preview = 'colordiff -U 1'
    pipe = '| awk "(NR>2)"'
  elsif TTY::Which.which('git')
    preview = 'git --no-pager diff -U1 --color=always --minimal --word-diff'
    pipe = ' | awk "(NR>4)"'
  else
    preview = 'diff -U 1'
    pipe = if TTY::Which.which('delta')
             ' | delta --no-gitconfig --syntax-theme=1337'
           elsif TTY::Which.which('diff-so-fancy')
             ' | diff-so-fancy'
           elsif TTY::Which.which('ydiff')
             ' | ydiff -c always --wrap < /dev/tty'
           else
             cmd = 'sed -e "s/^-/`echo -e "\033[31m"`-/;s/^+/`echo -e "\033[32m"`+/;s/^@/`echo -e "\033[34m"`@/;s/\$/`echo -e "\033[0m"`/"'
             "| bash -c #{Shellwords.escape(cmd)}"
           end
    pipe += ' | awk "(NR>2)"'
  end

  result = Doing::Prompt.choose_from(options,
                                     prompt: 'Select a backup to restore',
                                     sorted: false,
                                     fzf_args: [
                                       '--delimiter="\t"',
                                       '--with-nth=1',
                                       %(--preview='#{preview} "#{filename}" {2} #{pipe}'),
                                       '--disabled',
                                       '--height=10',
                                       '--preview-window="right,70%,nowrap,follow"',
                                       '--header="Select a revision to restore"'
                                     ])
  raise UserCancelled unless result

  result.strip.split(/\t/).last
end
write_backup(filename = nil) click to toggle source

Writes a copy of the content to a dated backup file in a hidden directory

@param content The data to back up

# File lib/doing/util_backup.rb, line 220
def write_backup(filename = nil)
  Doing.logger.benchmark(:_write_backup, :start)
  filename ||= Doing.setting('doing_file')

  unless File.exist?(filename)
    Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
    return
  end

  backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
  # compressed = Zlib::Deflate.deflate(content)
  # Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
  #   gz.write(IO.read(filename))
  # end

  FileUtils.cp(filename, backup_file)

  prune_backups(filename, Doing.setting('history_size').to_i)
  clear_undone(filename)
  Doing.logger.benchmark(:_write_backup, :finish)
end

Private Instance Methods

backup_dir() click to toggle source

Return a location for storing backups, creating if needed

@return Path to backup directory

# File lib/doing/util_backup.rb, line 277
def backup_dir
  @backup_dir ||= create_backup_dir
end
create_backup_dir() click to toggle source
# File lib/doing/util_backup.rb, line 281
def create_backup_dir
  dir = File.expand_path(Doing.setting('backup_dir')) || File.join(user_home, '.doing_backup')
  if File.exist?(dir) && !File.directory?(dir)
    raise DoingRuntimeError, "Backup error: #{dir} is not a directory"

  end

  unless File.exist?(dir)
    FileUtils.mkdir_p(dir)
    Doing.logger.warn('Backup:', "backup directory created at #{dir}")
  end

  dir
end
date_of_backup(filename) click to toggle source

Retrieve date from backup filename

@param filename The filename

# File lib/doing/util_backup.rb, line 265
def date_of_backup(filename)
  m = filename.match(/^(?:undone)?(?<date>\d{4}-\d{2}-\d{2})_(?<time>\d{2}\.\d{2}\.\d{2})___(?<file>.*?)$/)
  return nil if m.nil?

  [Time.parse("#{m['date']} #{m['time'].gsub(/\./, ':')}"), m['file']]
end
get_backups(filename = nil, include_forward: false) click to toggle source
# File lib/doing/util_backup.rb, line 248
def get_backups(filename = nil, include_forward: false)
  filename ||= Doing.setting('doing_file')
  backups = Dir.glob("*___#{File.basename(filename)}", base: backup_dir).sort.reverse
  backups.delete_if { |f| f =~ /^undone/ } unless include_forward
end
prune_backups_after(filename) click to toggle source

Delete backups newer than selected filename

@param filename The filename

# File lib/doing/util_backup.rb, line 301
def prune_backups_after(filename)
  target_date, base = date_of_backup(filename)
  return if target_date.nil?

  counter = 0
  get_backups(base).each do |file|
    date, _base = date_of_backup(file)
    if date && target_date < date
      FileUtils.mv(File.join(backup_dir, file), File.join(backup_dir, "undone#{file}"))
      counter += 1
    end
  end
  Doing.logger.debug('Backup:', "deleted #{counter} files newer than restored backup")
end
save_undone(filename = nil) click to toggle source
# File lib/doing/util_backup.rb, line 254
def save_undone(filename = nil)
  filename ||= Doing.setting('doing_file')
  undone_file = File.join(backup_dir, "undone#{timestamp_filename}___#{File.basename(filename)}")
  FileUtils.cp(filename, undone_file)
end
timestamp_filename() click to toggle source
# File lib/doing/util_backup.rb, line 244
def timestamp_filename
  Time.now.strftime('%Y-%m-%d_%H.%M.%S')
end