#!/usr/bin/env ruby

=begin

= ftpup version 1.3.4

written by Shugo Maeda <shugo@ruby-lang.org>

This software is distributed under the terms of the Ruby license.
You can freely distribute/modify this software.

=end

require "ftplib"
require "getopts"

class FTPup
  
  VERSION = "1.3.4"
  
  def FTPup.print_version
    print "ftpup version ", VERSION, "\n"
  end

  def FTPup.usage
    $stderr.print <<EOF
usage: ftpup [options]

  -f <filename>  use <filename> instead of `~/.ftpup'
  -l             do not upload recursively
  -r             remove remote files
  -v             print version and exit
EOF
  end # `
  
  def initialize(config_file, not_recursive, remove_files)
    load(config_file)
    check_config("Config")
    check_config("Config::HOST")
    check_config("Config::USER")
    check_config("Config::PASSWORD")
    check_config("Config::REMOTE_DIR")
    check_config("Config::LOCAL_DIR")
    if defined? Config::TEXTFILE_EXT
      if Config::TEXTFILE_EXT
	@textfile_regexp =
	  Regexp.new("\\.(" + Config::TEXTFILE_EXT.join("|") + ")$") # "
      else
	@textfile_regexp = nil
      end
    else
      @textfile_regexp = /\.(txt|html|shtml)$/
    end
    if defined? Config::IGNORE_FILE_REGEXP
      @ignore_file_regexp = Config::IGNORE_FILE_REGEXP
    else
      @ignore_file_regexp = /~$/
    end
    if defined? Config::LEAVE_FILE_REGEXP
      @leave_file_regexp = Config::LEAVE_FILE_REGEXP
    else
      @leave_file_regexp = nil
    end
    if defined? Config::LIST_OPTS
      @list_opts = Config::LIST_OPTS
    else
      @list_opts = "-alL"
    end
    if defined? Config::REMOTE_TZ
      @remote_tz = Config::REMOTE_TZ * 60
    else
      g = Time.now.gmtime
      l = Time.local( g.year, g.month, g.day, g.hour, g.min, g.sec )
      @remote_tz = l.to_i - g.to_i
    end
    if defined? Config::PASSIVE
      @passive = Config::PASSIVE
    else
      @passive = false
    end
    @not_recursive = not_recursive
    @remove_files = remove_files
  end
  
  def check_config(exp)
    unless eval "defined? #{exp}"
      error "config file error: ", exp, " required"
    end
  end
  
  def start(path)
    begin
      @ftp = FTP.new
      print "connect...\n"
      @ftp.connect(Config::HOST)
      begin
	print "login...\n"
	@ftp.login(Config::USER, Config::PASSWORD)
	@ftp.passive = @passive
	@ftp.chdir(Config::REMOTE_DIR)
	Dir.chdir(File.expand_path(Config::LOCAL_DIR))
	if path.empty?
	  update(".")
	else
	  path.each do |p|
	    update(p)
	  end
	end
      ensure
	@ftp.quit
      end
    rescue
      error($!)
    end
  end
  
  def update(path)
    if path =~ /^\//
      $stderr.print "#{path}: assumed .#{path}\n"
      $stderr.flush
      path[0,1] = ""
    end
    path[0,0] = "./" if path != "."
    if not FileTest.exist? path
      $stderr.print "#{path}: not found\n"
      $stderr.flush
      return
    end
    if FileTest.directory? path
      updatedir(path)
    else
      updatefile(path)
    end
  end

  def updatedir(directory)
    print directory.sub(/^./, Config::REMOTE_DIR), ":\n"
    remote_dirs = [];
    remote_filetimes = {}
    remote_filesizes = {}
    check_remote_files(directory, remote_dirs, remote_filetimes, remote_filesizes)
    Dir.foreach(directory) do |file|
      longname = directory + "/" + file
      if file != "." && file != ".."
	if File.ftype(longname) == "directory"
	  if longname =~ @ignore_file_regexp
	    next
	  end
	  unless remote_dirs.include?(file)
	    makedir(longname)
	  end
	  updatedir(longname) unless @not_recursive
	else
	  if not longname =~ @ignore_file_regexp and
             ((not remote_filesizes.has_key?(file) or
              remote_filesizes[file] != File.size(longname)) or
	      (not remote_filetimes.has_key?(file) or
              remote_filetimes[file] < File.mtime(longname).gmtime))
	    updatefile(longname)
	  end
	  remote_filetimes.delete(file)
	end
      end
    end
    if @remove_files
      remote_filetimes.each_key do |file|
	longname = directory + "/" + file
	unless @leave_file_regexp and longname =~ @leave_file_regexp
	  print "delete ", File.basename(longname), "\n"
	  @ftp.delete(longname)
	end
      end
    end
  end
  
  def check_remote_files(directory, dirs, filetimes, filesizes)
    if @list_opts and @list_opts.length > 0
      arg = @list_opts + " " + directory
    else
      arg = directory
    end
    @ftp.list(arg) do |line|
      unless (line =~ /^total/) || (line == "")
	if (line[0] == ?d) || (line =~ /:$/)
	  line.gsub!(/:$/, '')
	  line =~ /\S+$/
         if $& != "." && $& != ".."
           dirs.push($&)
         end
	else
	  line =~ /\S+$/
	  filename = $&
	  case line
         when /(\d+) ([A-Z][a-z][a-z]) +(\d+) +(\d+):(\d\d)/
           size = $1
           time = Time.gm(Time.now.year, $2, $3, $4, $5, "59") + @remote_tz
            if time > Time.now.gmtime
             time = Time.gm(Time.now.year - 1, $2, $3, $4, $5, "59") + @remote_tz
	    end
         when /(\d+) ([A-Z][a-z][a-z]) +(\d+) +(\d\d\d\d)/
           size = $1
           time = Time.gm($4, $2, $3, "23", "59", "59") + @remote_tz
	  else
	    raise "unknown time format: " + line
	  end
	  filetimes[filename] = time
         filesizes[filename] = size.to_i
	end
      end
    end
  end
  
  def makedir(dirname)
    first_time = TRUE
    begin
      @ftp.mkdir(dirname)
    rescue
      if first_time && $! =~ /^521 /
	@ftp.delete(dirname)
	first_time = FALSE
	retry
      else
	error "can't make directory ", dirname
      end
    end
  end
  
  def updatefile(file)
    print "put ", file, ": "
    begin
      if file =~ @textfile_regexp
	i = 0
	@ftp.puttextfile(file, file) do |line|
	  if i % 60 == 0
	    print "."; $>.flush
	  end
	  i += 1
	end
      else
	@ftp.putbinaryfile(file, file, 4096) do |data|
	  print "."; $>.flush
	end
      end
    rescue
      $stderr.printf("error: %s\n", $!)
    end
    print "\n"
  end
  
  def error(*args)
    $stderr.print $0, ": ", *args
    $stderr.print "\n"
    exit(1)
  end
end

unless getopts("lrv", "f:~/.ftpup")
  FTPup.usage
  exit 1
end

if $OPT_v
  FTPup.print_version
  exit
end

ftpup = FTPup.new(File.expand_path($OPT_f), $OPT_l, $OPT_r)
ftpup.start(ARGV)

# Local variables:
# mode: Ruby
# End:
