#!/usr/bin/env ruby
#
#= MountPoint
#
# Developers:: Morikawa Yasuhiro
# Version::    2010-01-13 10:48:52
#
#== Overview
#
#ファイルシステムをマウントするためのコマンド "mount" を支援する
#ためのクラス。現状は、Windows 領域を smbfs を用いてマウントする
#ことを重点的に考慮している。複数マウント際のパスワード入力を一度で
#済ませることが可能である。
#
#== Usage
#
#     % mount_point.rb [options]
#
#=== Options
#
#   --u                       : for umount
#   --n                       : for test
#   --ro                      : read only mount
#
#== Error Handling
#
#* 今のところ、エラー処理は特に無し。
#
#== Known Bugs
#
#* 今のところ、特に無し
#
#== Future Plans
#
#* ruby のバージョン 1.8 でならば、Process::Sys や Process::UID モジュール
#  を用い、さらにファイルに s ビットを立てることで一般ユーザからマウント
#  可能にする。
#
#== Note
#
#サンプルとして実行部分もあるが、基本的にはライブラリとしての利用を
#想定している。
#
#== History
#
#* 2008-04-01 19:13:20
#  * 文法エラーを修正 (なぜ今まで通っていたのか...)
#
#* 2006-05-14 15:35:45
#  * Moziila 用のプロファイルディレクトリをマウントするよう設定変更
#  * Opera のプロファイルはマウントから除去
#
#* 2005/03/21 18:57:52
#  * rdoc 用にコメントの形式を改訂
#
#* 2005/03/09 00:59:49  Yasuhiro Morikawa
#  * MountPoint.smbmount メソッドと MountPoint.smbumount 
#    メソッドを追加し、一般ユーザ
#    でも Samba でのマウントとアンマウントを可能にした。
#
#* 2005/03/07 19:42:37  Yasuhiro Morikawa
#  * winmount から改名して、mount_point とした。同時に構造も
#    改変した。
#
#
##################################################

require "getoptlong"      # for option_parse
require "etc"             # ユーザ ID 解析

############################################################################

#
#== IO
#
class IO
  #
  #標準入力から文字列を取得する際、入力する文字列を見えないようにする。
  #また、Ctrl-c や Ctrl-z など、interrupt や suspend シグナルを送る
  #特殊キーを一時無効にする。(途中で interrupt されると、stty -echo の
  #効果が残って入力キーが見えなくなるため)
  #
  def getshide
    system "stty -echo -isig"
    passwd = self.gets
    system "stty echo isig"
    return passwd
  end
end

#== MountItem
#
# デバイスとマウント先のディレクトリ、デバイスのファイルシステム、
# および書き込み情報を格納。
#
class MountItem
  #
  # デバッグ出力用メソッド。組み込み関数 $DEBUG が真の場合 (つまり、
  # プログラムを $ ruby -d ./xxxxxx.rb として呼び出した場合) に
  # debug メソッドに代入された変数を出力する。
  #
  def debug(*args)
    p [caller.first, *args] if $DEBUG
  end
  private :debug

  attr_reader :device, :dir, :mount_opt_fstype, :mount_opt_readable
  #
  # device にマウントするデバイス、もしくは共有フォルダを与える。
  # device に String クラス以外のものや空白のみのものは無視される。
  # dir にはマウント先のディレクトリを与える。与えない場合には "/mnt"
  # +「device の basename 部分」を設定する。
  # writable には読み込み専用ならば nil を、書き込みも可能とするならば
  # true を与える。デフォルトは nil である。
  #
  def initialize(device=nil, dir=nil, writable=nil, fstype=nil)
    debug(device, dir, writable, fstype)

    if !device.instance_of?(String) || !(/\w+/ =~ device.chomp.strip) then
      @device             = nil
      @dir                = nil
      @fstype             = nil
      @mount_opt_readable = nil
      return
    else
      @device  = device.chomp.strip
    end

    if !dir.instance_of?(String) then
      @dir = "/mnt/" + File.basename(@device)
    else
      @dir = dir.chomp.strip
    end

    if !writable then
      @mount_opt_readable = "ro"
    else
      @mount_opt_readable = "rw"
    end

    if !fstype.instance_of?(String) then
      @mount_opt_fstype = "-t smbfs"
    else
      if /^.*(-t)\s+.*$/ =~ fstype.chomp.strip
        @mount_opt_fstype = fstype.chomp.strip
      else
        @mount_opt_fstype = "-t " + fstype.chomp.strip
      end
    end

    debug(@device, @dir, @mount_opt_readable, @mount_opt_fstype)
  end

  #
  # device が既にマウント済みかどうかをチェックする。
  # マウントされていれば、マウント先のディレクトリと fstype を配列にして
  # 返し、されていなければ false を返す。
  # expected に true を渡すと、マウントされていない場合にメッセージを
  # 表示し、false を渡すと、マウントされている場合にメッセージを
  # 表示する。quiet に true を渡すとメッセージを表示しない。
  #
  def check_mounted(expected=nil, quiet=nil)
    deviceitem = chompslash(@device)
    debug("check #{deviceitem} is mounted.")
    mounted = false
    open("/etc/mtab", "r") { |etc_mtab|
      while line = etc_mtab.gets
        debug(line)
        if /^#{deviceitem}\s+(.+?)\s+(.+?)\s+/ =~ line then
          print "\"#{deviceitem}\" is already mounted.\n" unless quiet || expected
          dir, fstype = $1, $2
          debug(dir, fstype)
          mounted = [dir, fstype]
        end
      end
    }
    print "\"#{deviceitem}\" is not mounted.\n" if !quiet && expected && !mounted 
    debug(mounted)
    return mounted
  end

  #
  # ファイル名の末尾のスラッシュを取り除く。同時に、末尾に改行が
  # ある場合は改行も取り除く。末尾に改行が無く、またスラッシュも
  # ついていない場合は代入された文字列をそのまま返す。
  #
  def chompslash(filename)
    filename.chomp
    if /.*\/$/ =~ filename then
      len = filename.length
      len -= 1
      filename = filename.unpack("a#{len}")
    end
    return filename
  end
  private :chompslash

end


#== MountPoint
#
# デバイスとマウント先のディレクトリ、および書き込み情報を格納。
#
class MountPoint
  #
  # デバッグ出力用メソッド。組み込み関数 $DEBUG が真の場合 (つまり、
  # プログラムを $ ruby -d ./xxxxxx.rb として呼び出した場合) に
  # debug メソッドに代入された変数を出力する。
  #
  def debug(*args)
    p [caller.first, *args] if $DEBUG
  end
  private :debug

  attr_reader :mountlist
  #
  # device にマウントするデバイス、もしくは共有フォルダを与える。
  # device に String クラス以外のものや空白のみのものは無視される。
  # dir にはマウント先のディレクトリを与える。与えない場合には "/mnt"
  # +「device の basename 部分」を設定する。
  # writable には読み込み専用ならば nil を、書き込みも可能とするならば
  # true を与える。デフォルトは nil である。
  #
  def initialize(device=nil, dir=nil, writable=nil, fstype=nil)
    debug(device, dir, writable, fstype)

    mountitem = MountItem.new(device, dir, writable, fstype)

    @mountlist = Array.new
    @mountlist.push(mountitem) if mountitem.device

    self.samba(nil, nil, "")

    debug(@mountlist)
  end
  #
  # device にマウントするデバイス、もしくは共有フォルダを与える。
  # device に String クラス以外のものや空白のみのものは無視される。
  # dir にはマウント先のディレクトリを与える。与えない場合には "/mnt"
  # +「device の basename 部分」を設定する。
  # writable には読み込み専用ならば nil を、書き込みも可能とするならば
  # true を与える。デフォルトは nil である。
  #
  def add(device=nil, dir=nil, writable=nil, fstype=nil)
    debug(device, dir, writable, fstype)

    mountitem = MountItem.new(device, dir, writable, fstype)
    @mountlist.push(mountitem) if mountitem.device

    debug(@mountlist)
  end

  attr_reader :smbopts
  #
  # Samba マウントを行うよう、オプションを設定する。
  #
  def samba(username=nil, uid=nil, password=nil ,
            codepage=nil, iocharset=nil )
    smb_opt_username  = (username)  ? username.chomp.strip  : ENV["USER"]
    smb_opt_uid       = (uid)       ? uid   : Etc.getpwuid(Process.uid).uid

    smb_opt_codepage  = (codepage)  ? codepage.chomp.strip  : "cp932"
    smb_opt_iocharset = (iocharset) ? iocharset.chomp.strip : "euc-jp"
#    smb_opt_iocharset = (iocharset) ? iocharset.chomp.strip : "sjis"

    smb_opt_password  = (password)  ? password.chomp.strip  : get_smbpasswd(smb_opt_username)

    @smbopts   =  "-o"
    @smbopts   << " username=#{smb_opt_username}"
    @smbopts   << ",password=#{smb_opt_password}"
    @smbopts   << ",uid=#{smb_opt_uid},gid=#{smb_opt_uid}"
    @smbopts   << ",iocharset=#{smb_opt_iocharset}"
    @smbopts   << ",codepage=#{smb_opt_codepage}"
    @smbopts   << ",fmask=664,dmask=755"

    debug(@smbopts)
  end

  def get_smbpasswd(username)
    print "Password for \"#{username}\" : "
    password = $stdin.getshide.chomp
    print "\n"
    return password
  end
  private :get_smbpasswd

  #
  # マウントを実行する、noexec に true を与えると、実際にはコマンドを
  # 実行せず、実行するコマンドを標準出力に出力する。
  #
  def mount(noexec=nil)
    @mountlist.each{ |mountitem|
      next if mountitem.check_mounted

      cmdstr   =  "/usr/bin/env mount"
      cmdstr   << " #{mountitem.mount_opt_fstype}"

      debug(mountitem.mount_opt_fstype.chomp.strip)
      if "-t smbfs" == mountitem.mount_opt_fstype.chomp.strip
        cmdstr << " #{@smbopts}"
        cmdstr << ",#{mountitem.mount_opt_readable}"
      else
        cmdstr << " -o #{mountitem.mount_opt_readable}"
      end

      cmdstr   << " #{mountitem.device} #{mountitem.dir}"

      debug(cmdstr)
      if /^(.*?)password=.*?,(.*)$/ =~ cmdstr then
        cmdstr_nopass = $1 + "password=********," + $2
      else
        cmdstr_nopass = cmdstr
      end

      debug("uid=", Process.uid, " euid=", Process.euid)
      print "#{cmdstr_nopass}\n"
      system "#{cmdstr}" unless noexec
    }
  end


  #
  # アンマウントを実行する、noexec に true を与えると、実際にはコマンドを
  # 実行せず、実行するコマンドを標準出力に出力する。
  #
  def umount(noexec=nil)
    @mountlist.each{ |mountitem|
      next unless mountitem.check_mounted(true)

      cmdstr =  "/usr/bin/env umount"
      cmdstr << " #{mountitem.device}"

      debug(cmdstr)
      debug("uid=", Process.uid, " euid=", Process.euid)
      print "#{cmdstr}\n"
      system "#{cmdstr}" unless noexec
    }
  end

  #
  # smbmount を実行する、noexec に true を与えると、実際にはコマンドを
  # 実行せず、実行するコマンドを標準出力に出力する。
  # 結果的には MountPoint.mount メソッドと変わらないが、
  # このメソッドは一般ユーザでも実行可能である。
  #
  def smbmount(noexec=nil)
    @mountlist.each{ |mountitem|
      next if mountitem.check_mounted

      unless "-t smbfs" == mountitem.mount_opt_fstype.chomp.strip then
        print "Not Mount MountItem [\"#{mountitem.device}\" ->"
        print " \"#{mountitem.dir}\"] so it is not \"smbfs\". "
        return nil
      end

      cmdstr   =  "/usr/bin/env smbmount"
      cmdstr   << " #{mountitem.device} #{mountitem.dir}"
      cmdstr   << " #{@smbopts}"
      cmdstr   << ",#{mountitem.mount_opt_readable}"

      debug(cmdstr)
      if /^(.*?)password=.*?,(.*)$/ =~ cmdstr then
        cmdstr_nopass = $1 + "password=********," + $2
      else
        cmdstr_nopass = cmdstr
      end

      debug("uid=", Process.uid, " euid=", Process.euid)
      print "#{cmdstr_nopass}\n"
      system "#{cmdstr}" unless noexec
    }
  end


  #
  # アンマウントを実行する、noexec に true を与えると、実際にはコマンドを
  # 実行せず、実行するコマンドを標準出力に出力する。
  #
  def smbumount(noexec=nil)
    @mountlist.each{ |mountitem|
      next unless mountitem.check_mounted(true)

      unless "-t smbfs" == mountitem.mount_opt_fstype.chomp.strip then
        print "Not Uount MountItem [\"#{mountitem.device}\" ->"
        print " \"#{mountitem.dir}\"] so it is not \"smbfs\". "
        return nil
      end

      cmdstr =  "/usr/bin/env smbumount"
      cmdstr << " #{mountitem.dir}"

      debug(cmdstr)
      debug("uid=", Process.uid, " euid=", Process.euid)
      print "#{cmdstr}\n"
      system "#{cmdstr}" unless noexec
    }
  end

  #
  # 設定された全ての device が既にマウント済みかどうかをチェックする。
  # all に true を与えた場合、一つでもマウントされていないものがあれば、
  # false を返す。全てがマウントされていれば true を返す。
  # 一方、all に false を与えた場合、一つでもマウントされていれば、
  # true を返す。一つもマウントされていなければ false を返す。
  # (これは、「全てアンマウントされているか」を調べるのに有効である)
  #
  # quiet に false を渡すと、all が true の際はマウントされていないもの
  # に関して、all が false の場合はマウントされているものに関して
  # メッセージを表示する。
  #
  def check_mounted(all=true, quiet=true)
    allmounted    = true
    partlymounted = false

    @mountlist.each{ |mountitem|
      if mountitem.check_mounted(all, quiet) then
        partlymounted = true
      else
        allmounted = false
      end
    }

    debug(allmounted, partlymounted)
    return allmounted    if all
    return partlymounted unless all
  end

end

############################################################################


##################################################
## +++             Main Routine             +++ ##

## parse options
parser = GetoptLong.new
parser.set_options(
                   ###    global option   ###
                   ['--u',  GetoptLong::NO_ARGUMENT], # for umount
                   ['--n',  GetoptLong::NO_ARGUMENT], # for test
                   ['--ro', GetoptLong::NO_ARGUMENT]  # read only mount
                   )
begin
  parser.each_option do |name, arg|
    eval "$OPT_#{name.sub(/^--/, '').gsub(/-/, '_')} = '#{arg}'"  # strage option value to $OPT_val
  end
rescue
  exit(1)
end

if $0 == __FILE__

  mountobject = MountPoint.new

  password = $OPT_u ? "" : nil
  mountobject.samba("morikawa", "morikawa", password)

  hostname = "ZERO"
  #hostname = "CROSS"
  #hostname = "CODE"
  #hostname = "192.168.8.1"

  mount_opt_readable = $OPT_ro ? "ro" : "rw"
  mountobject.add("//#{hostname}/Install_Files",
                  "/home/morikawa/Windows/Install_Files", mount_opt_readable)
  mountobject.add("//#{hostname}/IMJP12",
                  "/home/morikawa/Windows/IMJP12")
  mountobject.add("//#{hostname}/Free_Program",
                  "/home/morikawa/Windows/Free_Program", mount_opt_readable)
  mountobject.add("//#{hostname}/My_Documents",
                  "/home/morikawa/Windows/My_Documents", mount_opt_readable)
  mountobject.add("//#{hostname}/Nict",
                  "/home/morikawa/Windows/Nict", mount_opt_readable)
  mountobject.add("//#{hostname}/Data",
                  "/home/morikawa/Windows/Data", mount_opt_readable)
#  mountobject.add("//#{hostname}/Mozilla_Profiles",
#                  "/home/morikawa/Windows/Mozilla_Profiles")
#  mountobject.add("//#{hostname}/Opera_profile",
#                  "/home/morikawa/Windows/Opera_profile")

  mountobject.smbmount($OPT_n)  unless $OPT_u
  mountobject.smbumount($OPT_n) if $OPT_u
  #mountobject.mount($OPT_n)  unless $OPT_u
  #mountobject.umount($OPT_n) if $OPT_u


end
