class Girror::Application

Application logic container.

Public Class Methods

debug(string) click to toggle source

Writes a debug message to @log.

# File lib/girror.rb, line 156
def debug string
  @log.debug string if @debug
end
dl_if_needed(name) click to toggle source

On-demand fetcher. Recursively fetches a directory entry ‘name’ (String) if there’s no local copy of it or the remote mtime is newer or the attributes are to be updated.

# File lib/girror.rb, line 168
def dl_if_needed name
  debug "RNA: #{name}"
  lname = econv(File.join '.', name.gsub(/^#{@path}/,'')); debug "LNA: #{lname}"

  # get and hold the current direntry's stat in here
  begin
    rs  = @sftp.stat!(name); s_rs = [Time.at(rs.mtime), Time.at(rs.atime), rs.uid, rs.gid, "%o" % rs.permissions].inspect
  rescue Net::SFTP::StatusException => detail
    return if detail.code == 2 # silently ignore the broken remote link
  end
  debug "Remote stat for #{name} => #{s_rs}"

  # remote type filter: we only work with types 1..2 (regular, dir)
  begin
    debug "Remote file type #{rs.type} isn't supported, ignoring."
    return
  end if rs.type > 2

  # remove the local entry if local/remote entry type differ;
  # else compare remote/local owner/mode and schedule the update.
  if File.exist? lname
    if (
        rs.type != case File.ftype lname
        when "file"      then 1
        when "directory" then 2
        end
      )
      remove_entry_secure lname, :force => true
    else
      lrs = File.stat(lname)
      # we do mode comparison on Unices only,
      # and owner compaison only if we are root
      unless ENV['OS'] == "Windows_NT"
        set_attrs = true unless (
          if ENV['EUID'] == 0
            debug "Comparing: #{[rs.permissions, rs.uid, rs.gid].inspect} <=> #{[lrs.mode, lrs.uid, lrs.gid].inspect}"
            [lrs.mode, lrs.uid, lrs.gid] == [rs.permissions, rs.uid, rs.gid]
          else
            debug "Comparing: #{rs.permissions} <=> #{lrs.mode}"
            lrs.mode == rs.permissions
          end
        )
      end
    end
  end

  # do the type-specific fetch operations
  case rs.type
  when 1
    if (lrs.nil? or (lrs.mtime.to_i < rs.mtime))
      log "Fetching file #{name} -> #{lname.force_encoding("BINARY")} (#{rs.size} bytes)"
      @sftp.download! name, lname
      set_attrs = true
    end
  when 2
    # here we've got a dir
    # create the dir locally if needed
    unless File.exist?(lname)
      log "Fetching directory #{name} -> #{lname.force_encoding("BINARY")} | #{s_rs}"
      mkdir lname
      set_attrs = true
    end
    # recurse into the dir; get the remote list
    rlist = @sftp.dir.entries(name).map do |e|
      unless e.name =~ FILTER_RE
        dl_if_needed(File.join(name, e.name))
        Iconv.conv("utf-8", @renc, e.name)
      end
    end . compact

    # get the local list
    llist = Dir.entries(lname).map do |n|
      Iconv.conv("utf-8", @lenc, n) unless n =~ FILTER_RE
    end . compact

    # differentiate the lists; remove what's needed from local repo
    diff = llist - rlist
    diff.each do |n|
      # the string should be converted back to local encoding before any
      # operations
      n = Iconv.conv(@lenc, "utf-8", File.join(lname, n))
      log "Removing #{n}"
      begin
        @git.remove n, :recursive => true
      rescue Git::GitExecuteError => detail
        case detail.message
        when /did not match/
          log "#{n} has no match in the git repo: removing it forcefully!"
          rm_rf n
        else
          log detail.message
        end
      end
    end
  end

  # do the common after-fetch tasks (chown, chmod, utime)
  unless lname == "./"
    unless ENV['OS'] == "Windows_NT"     # chmod/chown issues on that platform
      if ENV['EUID'] == 0
        log "Setting owner: #{lname} => #{[rs.uid, rs.gid].inspect}"
        File.chown rs.uid, rs.gid, lname
      end
      log "Setting mode: #{lname} => #{"%o" % rs.permissions}"
      File.chmod rs.permissions, lname
    end
    log "Setting mtime: #{lname} => #{[rs.atime, rs.mtime].map{|t| Time.at(t).strftime("%Y-%m-%d %H:%M:%S")}.inspect}"
    File.utime rs.atime, rs.mtime, lname
  end if set_attrs
end
econv(str) click to toggle source

Converts the String str from @renc to @lenc if both @renc and @lenc are set and aren’t equal.

Returns the converted String.

# File lib/girror.rb, line 283
def econv str
  ((@lenc == @renc) or (@lenc.nil? or @renc.nil?)) ?
    str : Iconv.conv(@lenc, @renc, str)
end
log(string) click to toggle source

Writes a log message to @log.

# File lib/girror.rb, line 161
def log string
  @log.info string
end
run(ops) click to toggle source

Runs the app. Much like the C’s main().

# File lib/girror.rb, line 58
def run ops
  # Logging setup
  @log = case ops[:log]
  when 'syslog'
    unless ENV['OS'] == 'Windows_NT'
      require 'syslog_logger'
      SyslogLogger.new('girror')
    else
      Logger.new STDERR
    end
  when nil
    Logger.new STDERR
  else
    Logger.new ops[:log]
  end
  @log.datetime_format = "%Y-%m-%d %H:%M:%S " if Logger.class == Logger
  log "Starting"
  @debug = true if ops[:verbose]
  debug "Current options are: #{ops.inspect}"

  # check the validity of a local directory
  @lpath = ops[:to]       # local save path
  log "Opening local git repo at #{@lpath}"
  @git = Git.open(@lpath) # local git repo

  cd ops[:to]; log "Changed to #{pwd}"

  # read the config and use CLI ops to override it
  $:.unshift(File.join(".", "_girror"))
  begin
    require 'config'
    ops = Config::OPTIONS.merge ops

    begin
      debug "Program options:"
      ops.each do |pair|
        debug pair.inspect
      end
    end

  rescue LoadError => d
    log "Not using stored config: #{d.message}"
  end

  # set commit message for git
  ops[:commit_msg].nil? ? @commit_msg = Proc.new { Time.now.to_s } : @commit_msg = ops[:commit_msg]

  # name conversion encodings for Iconv
  ops[:renc].nil? ? @renc = "utf-8" :  @renc = ops[:renc]
  ops[:lenc].nil? ? @lenc = "utf-8" :  @lenc = ops[:lenc]

  # Check the validity of a remote url and run the remote connection
  if ops[:from] =~ /^((\w+)(:(\w+))?@)?(.+):(.*)$/
    $2.nil? ? @user = ENV["USERNAME"] : @user = $2
    @pass = $4
    @host = $5
    @path = $6

    debug "Remote data specified as: login: #{@user}; pass: #{@pass.inspect}; host: #{@host}; path: #{@path}"
    Net::SFTP.start(@host, @user,
                    :password => @pass,
                    :keys => ops[:ssh][:keys],
                    :compression => ops[:ssh][:compression]
                   ) do |s|
      @sftp = s
      log "Connected to remote #{@host} as #{@user}"

      dl_if_needed @path

      log "Disconnected from remote #{@host}"

      # fix the local tree in the git repo
      begin
        log "Committing changes to local git repo"
        @git.add
        msg = if @commit_msg.class == Proc then @commit_msg.call
        else @commit_msg
        end . to_s
        @git.commit msg, :add_all => true
      rescue Git::GitExecuteError => detail
        case detail.message
        when /nothing to commit/
          log "Nothing to commit"
        else
          log detail.message
        end
      end

    end

  else
    raise "Bad remote specification!"
  end

  log "Finishing"
end