#!/usr/local/bin/ruby
# -*- ruby -*-
#
# Copyright (c) 2000-2001 Akinori MUSHA
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#

RCS_ID = %q$Idaemons: /home/cvs/pkgtools/bin/pkg_fetch,v 1.20 2002/02/04 13:24:13 knu Exp $
RCS_REVISION = RCS_ID.split[2]
MYNAME = File.basename($0)

require 'ftools'
require "optparse"
require "pkgtools"
require "uri"

def init_global
  $force = false
  $noconfig = false
  #$sanity_check = true
  $upward_recursive = false
end

COLUMNSIZE = 24
NEXTLINE = "\n%*s" % [5 + COLUMNSIZE, '']

def main(argv)
  usage = <<-"EOF"
usage: #{MYNAME} [-hfqRv] {pkgname|URI} ...
  EOF

  banner = <<-"EOF"
#{MYNAME} rev.#{RCS_REVISION}

#{usage}
  EOF

  dry_parse = true

  OptionParser.new(banner, COLUMNSIZE) do |opts|
    opts.def_option("-h", "--help", "Show this message") {
      print opts
      exit 0
    }

    opts.def_option("-f", "--force", "Download a package even if recorded as installed;#{NEXTLINE}Remove existing packages if they are corrupt") {
      |$force|
    }

#    opts.def_option("-O", "--omit-check", "Omit sanity checks for dependencies.") {
#      $sanity_check = false
#    }

    opts.def_option("-q", "--noconfig", "Do not read pkgtools.conf") {
      |$noconfig|
    }

    opts.def_option("-R", "--upward-recursive", "Download the packages required by the given#{NEXTLINE}packages as well") {
      |$upward_recursive|
    }

    opts.def_option("-v", "--verbose", "Be verbose") {
      |$verbose|
    }

    opts.def_tail_option '
pkgname is a full pkgname, a pkgname w/o version followed by an @,
or a full URI.

Environment Variables [default]:
    PACKAGEROOT      URI of the root of the site [ftp://ftp.FreeBSD.org]
    PACKAGESITE      URI of the directory to fetch packages from [none]
                     (overrides PACKAGEROOT and PKG_SITES)
    PACKAGES         packages directory to save files [$PORTSDIR/packages]
    PKGTOOLS_CONF    configuration file [$PREFIX/etc/pkgtools.conf]
    PKG_DBDIR        packages DB directory [/var/db/pkg]
    PKG_FETCH        command to fetch files [/usr/bin/fetch -ao %2$s %1$s]
    PKG_SITES        list of URIs to fetch packages from [none]
    PKG_TMPDIR       temporary directory for download [$TMPDIR]
    PORTSDIR         ports directory [/usr/ports]
    TMPDIR           temporary directory [/var/tmp]'

    begin
      init_global

      rest = opts.order(*argv)

      unless $noconfig
	init_global
	load_config
      else
	argv = rest
      end

      dry_parse = false

      opts.order!(argv)

      if argv.empty?
	print opts
	return 0
      end

      results = []

      opts.order(*argv) do |arg|
	set_uri_base(arg)

	arg = File.basename(arg, '.tgz')

	fetch_pkg(arg, $upward_recursive, results)
      end

      return show_results(results, 'downloaded')
    rescue OptionParser::ParseError => e
      STDERR.puts "#{MYNAME}: #{e}", usage
      exit 64
    end
  end
end

def identify_pkg(path)
  dir, file = File.split(path)

  pkgname = nil
  origin = nil
  pkgdep = []

  IO.popen("cd #{dir} && #{PkgDB::CMD[:pkg_info]} -qfo #{file}") do |r|
    r.each do |line|
      case line
      when /^@name\s+(\S*)/
	pkgname = $1
      when /^@pkgdep\s+(\S*)/
	pkgdep << $1
      when /^(\S+\/\S+)$/		# /
	origin = $1
      end
    end
  end

  return pkgname, origin, pkgdep
rescue => e
  warning_message e.message
  return nil
end

def fetch_pkg(pkgname, recursive = false, results = [])
  begin
    downloaded, pkgdep = do_fetch_pkg(pkgname)

    $subdir = nil

    results << [pkgname, downloaded ? true : false]

    if pkgdep.is_a?(Array)
      if recursive
	pkgdep.each do |dep|
	  if results.find { |item, | item == dep }
	    next
	  end
      
	  fetch_pkg(dep, true, results)
	end
      end
    end
  rescue => e
    results << [pkgname, e]
  end

  results
end

def do_fetch_pkg(pkgname)
  pkgname = pkgname.dup

  latest = pkgname.chomp!('@') || !pkgname.include?('-')

  tgz = pkgname + '.tgz'

  if !latest && !$force && $pkgdb.installed?(pkgname)
    progress_message "Skipping #{pkgname} (already installed)"
    return false, nil
  end

  save_path = File.join($packages_dir, tgz)

  if File.exist?(save_path)
    progress_message "Identifying the package #{save_path}"

    id_pkgname, origin, pkgdep = identify_pkg(save_path)

    return false, pkgdep if not id_pkgname.nil?

    warning_message "Failed to extract information from #{tgz}"

    raise "corrupt package" unless $force

    warning_message "Removing the corrupt package #{save_path}"

    File.unlink(save_path)
  end

  progress_message "Fetching #{tgz}"

  temp_path = File.join($tmpdir, tgz)

  File.makedirs $tmpdir, $packages_dir

  if not real_fetch_pkg(pkgname, temp_path, latest)
    warning_message "Failed to fetch #{tgz}"
    raise "fetch error"
  end

  progress_message "Downloaded as #{tgz}"

  progress_message "Identifying the package #{save_path}"

  pkgname, origin, pkgdep = identify_pkg(temp_path)

  if pkgname.nil?
    warning_message "Failed to extract information from #{tgz}"
    raise "corrupt package"
  end

  tgz = pkgname + '.tgz'
  save_path = File.join($packages_dir, tgz)

  File.move(temp_path, save_path)

  if $?.nonzero?
    warning_message "Failed to save the dowloaded tarball as #{save_path}"
    raise "permission denied"
  end

  progress_message "Saved as #{save_path}"

  return true, pkgdep
end

def set_uri_base(uri_s)
  $subdir = nil

  begin
    uri = URI.parse(uri_s)

    if not uri.scheme.nil?
      $subdir = File.basename((uri + './').path.chomp('/'))

      case $subdir
      when 'All', 'Latest'
	$pkg_site_uris = [uri + '../']
      else
	$subdir = '.'
	$pkg_site_uris = [uri]
      end

      return true
    end
  rescue => e
    # not a remote URI
  end

  if ENV.key?('PACKAGESITE')
    $pkg_site_uris = [URI.parse(ENV['PACKAGESITE']) + '../']
  else
    $pkg_site_uris = $pkg_sites.map { |str|
      URI.parse(str)
    }
  end

  return true
rescue => e
  warning_message e.message
  $pkg_site_uris = []
  return false
end

def real_fetch_pkg(pkgname, path, latest = false)
  if latest
    subdir = $subdir || 'Latest'
  else
    subdir = $subdir || 'All'
  end

  if $verbose
    information_message 'Will try the following sites in the order named:'

    $pkg_site_uris.each do |site|
      STDERR.puts "\t#{site}"
    end
  end

  $pkg_site_uris.each do |uri_base|
    uri = uri_base + (subdir + '/' + pkgname + '.tgz')

    fetch(uri, path) and return true
  end

  false
end

def fetch(uri, path = File.basename(uri.path))
  if path.empty?
    warning_message 'Missing filename'
    return false
  end

  cmdline = format(ENV['PKG_FETCH'] || "/usr/bin/fetch -o '%2$s' '%1$s'", uri, path)

  progress_message "Invoking a command: #{cmdline}" if $verbose

  system(cmdline)
  status = $? >> 8

  if status.nonzero?
    warning_message format("The command returned a non-zero exit status: %d", status)
  end

  if File.zero?(path)
    warning_message "Got a zero-sized file #{path} (removing)"
    File.unlink(path)
  end

  if !File.exist?(path)
    warning_message "Failed to fetch #{uri}"
    return false
  end

  return true
end

def signal_handler(sig)
  puts "\nInterrupted."

  stty_sane

  exit
end

if $0 == __FILE__
  for sig in [2, 3, 15]
    trap(sig) do
      signal_handler(sig)
    end
  end

  exit(main(ARGV) || 1)
end
