#! /usr/bin/ruby1.8 -Ku
# -*- coding: utf-8 -*-
require 'digest/md5'
require 'fileutils'
require 'gettext'
require 'monitor'
require 'open-dolphin-backup-usb-storage_glade'
require 'pathname'
require 'tmpdir'

class OpenDolphinBackupUsbStorage < OpenDolphinBackupUsbStorageGlade
  include MonitorMixin

  def initialize(path_or_data, root = nil, domain = nil, localedir = nil, flag = GladeXML::FILE)
    super

    GetText.bindtextdomain(domain, localedir, nil, "UTF-8")

    @main = MainWindow.new(@glade)

    begin
      load("/etc/default/open-dolphin-backup-usb-storage")
    rescue Exception
      show_error(s_("error|error while loading /etc/default/open-dolphin-backup-usb-storage"))
      #raise
    end

    @command = Command.new
    @command.on_error(&method(:show_error))
    @command.on_question(&method(:show_question))
    @command.on_status_update(&@main.method(:set_status))
  end

  def gtk_main_quit
    synchronize do
      Gtk.main_quit
    end
  end

  def show_dialog(message, type, buttons=Gtk::MessageDialog::BUTTONS_OK)
    dialog = Gtk::MessageDialog.new(@main.window,
                                    Gtk::Dialog::DESTROY_WITH_PARENT,
                                    type,
                                    buttons,
                                    message)
    dialog.run do |response|
      if block_given? # and response != Gtk::Dialog::RESPONSE_NONE
        yield(response)
      end
    end
    dialog.destroy
  end

  def show_error(message, &block)
    show_dialog(message, Gtk::MessageDialog::ERROR, &block)
  end

  def show_info(message, &block)
    show_dialog(message, Gtk::MessageDialog::INFO, &block)
  end

  def show_question(message, &block)
    result = false
    show_dialog(message,
                Gtk::MessageDialog::QUESTION,
                Gtk::MessageDialog::BUTTONS_OK_CANCEL) do |response|
      if response == Gtk::Dialog::RESPONSE_OK
        result = true
      end
      if block_given?
        yield(response)
      end
    end
    return result
  end

  def show_warning(message, &block)
    show_dialog(message, Gtk::MessageDialog::WARNING, &block)
  end

  def protected_run
    synchronize do
      @main.buttons_set_sensitive(false)
      begin
        thread = yield
        if thread
          while thread.alive?
            Thread.pass
            while Gtk.events_pending?
              Gtk.main_iteration
            end
          end
          thread.join
        end
      rescue Exception
        if $!.is_a?(SystemCallError)
          puts $!.inspect
        else
          puts GLib.locale_from_utf8($!.inspect)
        end
        puts $!.backtrace
        message = $!.message
        if $!.is_a?(SystemCallError)
          message = GLib.locale_to_utf8(message)
        end
        @main.set_status(message)
      ensure
        @main.buttons_set_sensitive(true)
      end
    end
  end
  private :protected_run

  ## main window

  def on_button_backup_to_usb_clicked(widget)
    protected_run do
      Thread.start do
        @command.do_backup_to_usb
      end
    end
  end

  class WindowWrapper
    include GetText

    def initialize(glade, window_name)
      @glade = glade
      @window = @glade[window_name]
    end

    attr_reader :window

    def set_transient_for(parent)
      @window.set_transient_for(parent)
    end

    def show
      @window.show
    end

    def hide
      @window.hide
    end
  end

  class MainWindow < WindowWrapper
    def initialize(glade)
      super(glade, 'window_main')
      @status = @glade['label_status']
      @buttons = [
        @quit_button = @glade['button_quit'],
        @backup_to_usb_button = @glade['button_backup_to_usb'],
      ]
    end

    # set text with Pango Markup to status label.
    # see http://developer.gnome.org/doc/API/2.0/pango/PangoMarkupFormat.html
    def set_status(str)
      puts GLib.locale_from_utf8(str)
      # @status.set_text(str)
      @status.set_markup(str)
    end

    # set sensitive of all buttons
    def buttons_set_sensitive(bool)
      @buttons.each do |button|
        button.sensitive = bool
      end
    end
  end # class MainWindow

  class Command
    include GetText

    def initialize
      # callback
      @on_status_update_callback = nil
      @on_error_callback = nil
      @on_question_callback = nil

      # database
      @target_db = $target_db
      @db_user = $db_user

      # file/path
      set_dump_filename(Time.now.strftime("#{$backup_prefix}_%Y%m%d.dump"))
      @device_path = nil
      @mountpoint = Pathname.new('/mnt/usb')
      @passphrase = $passphrase || 'orcadehappy'
      @max_file = 5
      @glob_pattern = @mountpoint + "#{$backup_prefix}_*.dump.gpg"
    end

    def set_dump_filename(filename)
      tmpdir = Pathname.new(Dir.tmpdir)
      @dump_file = filename
      @gpg_file = @dump_file + '.gpg'
      @dump_path = tmpdir + @dump_file
      @gpg_path = tmpdir + @gpg_file
    end

    def on_error(&block)
      @on_error_callback = block
    end

    def on_question(&block)
      @on_question_callback = block
    end

    def on_status_update(&block)
      @on_status_update_callback = block
    end

    def do_backup_to_usb
      # set_dump_filename(Time.now.strftime('open-dolphin-backup_%Y%m%d%H%M%S.dump')) # DEBUG
      set_status(s_('status|backup to usb ...'))
      scan_device
      check_mountpoint
      umount_usb(false)
      create_backup
      encrypt_data
      begin
        mount_usb
        copy_data
        verify_data
        check_quantity
      rescue Exception
        umount_usb(false)
        raise
      else
        umount_usb(true)
      end
      set_status(s_('status|backup to usb ... done.'))
    ensure
      if @dump_path.exist?
        @dump_path.unlink rescue nil
      end
      if @gpg_path.exist?
        @gpg_path.unlink rescue nil
      end
    end

    private

    def show_error(message, &block)
      @on_error_callback.call(message, &block)
    end

    def show_question(message, &block)
      @on_question_callback.call(message, &block)
    end

    def set_status(str)
      @on_status_update_callback.call(str)
    end

    def scan_device
      set_status(s_("status|scan device ..."))
      @device_path = nil
      dmesg = `dmesg`
      dmesg.scan(/Attached scsi removable disk (sd[a-z])|\[(sd[a-z])\] Attached SCSI removable disk/) do |usb_dev,usb_dev_etchnhalf|
        usb_dev ||= usb_dev_etchnhalf
        if /\Asd.\z/ =~ usb_dev
          @device_path = Pathname.new('/dev') + usb_dev
        end
      end
      if @device_path
        set_status(s_("status|scan device ... found scsi removable disk %s" % @device_path))
      else
        raise(s_("status|error|scan device ... not found scsi removable disk."))
      end
    end

    def check_mountpoint
      set_status(s_("status|check mountpoint ..."))
      if @mountpoint.exist?
        set_status(s_("status|check mountpoint ... found."))
      else
        set_status(s_("status|check mountpoint ... not found. mkdir %s ...") % @mountpoint)
        @mountpoint.mkpath
        set_status(s_("status|check mountpoint ... not found. mkdir %s ... done.") % @mountpoint)
      end
    end

    def check_quantity
      set_status(s_("status|remove old backup files ..."))
      file_list = Pathname.glob(@glob_pattern).sort_by{|path| path.mtime }

      count = 0
      if (file_list.size > @max_file) && (file_list.size != 0)
        file_list[0...(file_list.size - @max_file)].each do |file|
          count += 1
          puts file
          file.unlink
        end
      end
      puts "check_quantity: #{count} files removed."
      set_status(s_("status|remove old backup files ... done."))
    end

    def create_backup
      set_status(s_("status|create backup ..."))
      unless system("sudo", "-u", @db_user, "pg_dump", "-O", @target_db, "-f", @dump_path)
        raise(s_("status|error|create backup ... failed."))
      end
      set_status(s_("status|create backup ... done."))
      # sleep(1)
    end

    def encrypt_data
      encrypt_data_message = s_("status|encrypt data ...")
      set_status(encrypt_data_message)
      if @gpg_path.exist?
        remove_path = @gpg_path.to_s.dump
        set_status(s_("status|remove %s ...") % remove_path)
        @gpg_path.unlink
        set_status(s_("status|remove %s ... done.") % remove_path)
        set_status(encrypt_data_message)
      end
      r, w = IO.pipe
      pid = Process.fork do
        w.close
        STDIN.reopen(r)
        STDOUT.reopen('/dev/null', 'w')
        argv = %W"gpg --batch --passphrase-fd 0 -c #{@dump_path}"
        STDERR.puts "exec(*#{argv.inspect})"
        exec(*argv)
        exit!(false)
      end
      r.close
      w.write(@passphrase)
      w.close
      # sleep(2)
      pid, status = Process.waitpid2(pid)
      if status.exitstatus != 0
        raise(s_("status|error|encrypt data ... failed."))
      end
      set_status(s_("status|encrypt data ... done."))
    end

    def copy_data
      set_status(s_("status|copy data ...\n<b>DO NOT REMOVE USB DEVICE</b>"))
      FileUtils.cp(@gpg_path, @mountpoint, :preserve => true, :verbose => true)
      system('sync')
      system('sync')
      # sleep(2)
      if $? != 0
        raise(s_("status|error|copy data ... failed."))
      end
      set_status(s_("status|copy data ... done."))
      # sleep(1)
    end

    def mount_usb
      if @device_path.exist?
        set_status(s_("status|mount usb ..."))
        1.upto(16) do |i|
          partition_path = Pathname.new("#{@device_path}#{i}")
          next unless partition_path.exist?
          if system(*%W"mount -t vfat #{partition_path} #{@mountpoint}")
            set_status(s_("status|mount usb ... done."))
            return
          end
        end
        if system(*%W"mount -t vfat #{@device_path} #{@mountpoint}")
            set_status(s_("status|mount usb ... done."))
          return
        end
        raise(s_("status|error|mount usb ... failed to mount device %s") % @device_path)
      else
        raise(s_("status|error|mount usb ... not found device %s") % @device_path)
      end
    end

    def umount_usb(verbose)
      puts_msg = proc do |msg|
        if verbose
          set_status(msg)
        else
          puts GLib.locale_from_utf8(msg)
        end
      end
      raise_msg = proc do |msg|
        if verbose
          raise(msg)
        else
          puts GLib.locale_from_utf8(msg)
        end
      end

      if @device_path.exist?
        puts_msg.call(s_("status|umount usb ..."))
        mounted = nil
        File.foreach("/etc/mtab") do |line|
          if /\A#{Regexp.quote(@device_path)}\d*/ =~ line
            mounted = $&
            break
          end
        end
        unless mounted
          puts_msg.call(s_("status|umount usb ... not mount."))
          return
        end
        if system(*%W"umount #{mounted}")
          puts_msg.call(s_("status|umount usb ... done."))
        else
          raise_msg.call(s_("status|error|umount usb ... failed to umount device %s") % @device_path)
        end
      else
        raise_msg.call(s_("status|error|umount usb ... not found device %s") % @device_path)
      end
    end

    def verify_data
      set_status(s_("status|verify data ..."))
      md5sum_src = md5sum(@gpg_path)
      md5sum_dest = md5sum(@mountpoint + @gpg_file)

      unless md5sum_src == md5sum_dest
        raise(s_("status|error|verify data ... failed. try again."))
      end

      set_status(s_("status|verify data ... done."))
      # sleep(2)
    end

    def md5sum(path)
      md5 = Digest::MD5.new
      blksize = path.stat.blksize
      path.open('rb') do |f|
        while buf = f.read(blksize)
          md5.update(buf)
        end
      end
      md5
    end

    def system(*args)
      puts GLib.locale_from_utf8("system(*#{args.inspect})")
      super(*args)
    end
  end # class Command

end # class OpenDolphinBackupUsbStorage

class RootChecker
  include GetText

  def initialize(domain, localedir)
    GetText.bindtextdomain(domain, localedir, nil, "UTF-8")
  end

  def check(data_path)
    if Process.uid != 0
      gksu = Pathname.new('/usr/bin/gksu')
      if gksu.exist?
        title = _("Running OpenDolphin Backup to USB Mass Storage ...")
        message = _("<b>I need your root password to run\nthe OpenDolphin Backup to USB Mass Storage.</b>")
        exit(system(*%W"#{gksu} -t #{title} -m #{message} -u root -- #{$0} --target_db=#{$target_db} --db_user=#{$db_user} --backup_prefix=#{$backup_prefix} #{data_path}"))
      else
        Gtk.init
        dialog = Gtk::MessageDialog.new(nil,
                                        Gtk::Dialog::DESTROY_WITH_PARENT,
                                        Gtk::MessageDialog::WARNING,
                                        Gtk::MessageDialog::BUTTONS_CLOSE)
        dialog.set_markup(_("<b>This program should be run as root and /usr/bin/gksu is not available.</b>"))
        dialog.run
        dialog.destroy
        exit(1)
      end
    end
  end
end

if __FILE__ == $0
  # Set values as your own application.
  prog_name = 'open-dolphin-backup-usb-storage'
  prog_path = 'open-dolphin-backup-usb-storage.glade'

  require 'optparse'
  ARGV.options do |opts|
    opts.banner << " data_path"
    opts.on("--target_db=DATABASE", "target database of pg_dump (default: dolphin)") do |s|
      $target_db = s
    end
    opts.on("--db_user=USER", "run pg_dump as unix user (default: postgres)") do |s|
      $db_user = s
    end
    opts.on("--backup_prefix=PREFIX", "prefix of backup files (default: TARGET_DB-box-backup)", /\A[A-Za-z0-9_\-]+\z/) do |s|
      $backup_prefix = s
    end
    opts.parse!
  end
  data_path = Pathname.new(ARGV.shift || '/usr/share')
  $target_db ||= 'dolphin'
  $db_user ||= 'postgres'
  $backup_prefix ||= "open-dolphin-backup"
  RootChecker.new(prog_name, ENV['GETTEXT_PATH']).check(data_path)
  glade_path = data_path + prog_name + prog_path
  if glade_path.exist?
    prog_path = glade_path
  else
    prog_path = Pathname.new('/usr/share') + prog_path
  end
  File.umask(077)
  Gtk.init
  OpenDolphinBackupUsbStorage.new(prog_path, nil, prog_name, ENV['GETTEXT_PATH'])
  Gtk.main
end
