#!/usr/bin/env 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/pkg_fetch,v 1.6 2001/09/06 09:11:12 knu Exp $
RCS_REVISION = RCS_ID.split[2]
MYNAME = File.basename($0)

require "optparse"
require "pkgtools"
require "uri"

DEFAULT_PACKAGEROOT = 'ftp://ftp.FreeBSD.org/'
PKG_FETCH = ENV['PKG_FETCH'] || "/usr/bin/fetch -o %2$s %1$s"

$force = false
$upward_recursive = false

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

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

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

#{usage}
  EOF

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

    opts.def_option("-f", "--force", "Download a package even if it is recorded as#{NEXTLINE}installed.") {
      |$force|
    }

    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 or a pkgname w/o version.

Environment Variables [default]:
    PKG_TMPDIR           temporary directory for download [$TMPDIR]
    TMPDIR               temporary directory [/var/tmp]
    PORTSDIR             ports directory [/usr/ports]
    PKGREPOSITORY        packages directory to save files [$PORTSDIR/packages]
    PKGREPOSITORYSUBDIR  packages subdirectory to save files [All]
    PKG_DBDIR            packages DB directory [/var/db/pkg]
    PKG_FETCH            command to fetch files [/usr/bin/fetch -ao %2$s %1$s]
    PACKAGEBASE          URI of the packages directory [$PACKAGESITE/../]
    PACKAGESITE          URI of the "Latest" directory of the site [none]
    PACKAGEROOT          URI of the root of the site [ftp://ftp.FreeBSD.org/]'

    begin
      opts.order!(argv)

      if argv.empty?
	print opts
	return 0
      end

      $uri_base = uri_base()

      results = []

      opts.order(*argv) do |arg|
	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 +(.*)$/
	pkgname = $1
      when /^@pkgdep +(.*)$/
	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)

    results << [pkgname, downloaded ? nil : 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 try_fetch_pkg(pkgname, path)
  flag = false

  pattern = /^#{Regexp.quote(pkgname)}-[^\-]+$/

  if not $portsdb.glob(pattern).empty?
    uri = $uri_base + 'Latest/' + (pkgname + '.tgz')

    fetch(uri, path) and return true

    flag = true
  end

  if pkgname.include?('-')
    uri = $uri_base + 'All/' + (pkgname + '.tgz')

    fetch(uri, path) and return true
  end

  unless flag
    progress_message "Assuming '#{pkgname}' is a package name without version"

    uri = $uri_base + 'Latest/' + (pkgname + '.tgz')

    fetch(uri, path) and return true
  end

  false
end

def do_fetch_pkg(pkgname)
  tgz = pkgname + '.tgz'

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

  save_path = File.join($packages_dir, tgz)

  if File.exists?(save_path)
    progress_message "Identifying the package #{save_path}" if $verbose

    pkgname, origin, pkgdep = identify_pkg(save_path)

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

    return false, pkgdep
  end

  progress_message "Fetching #{tgz}"

  temp_path = File.join($tmpdir, tgz)

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

  progress_message "Downloaded as #{tgz}" if $verbose

  progress_message "Identifying the package #{save_path}" if $verbose

  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)

  system('/bin/mv', '-f', 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}" if $verbose

  return true, pkgdep
rescue => e
  warning_message "Error: #{e.message}"
  raise e
end

def uri_base()
  if ENV.key?('PACKAGEBASE')
    return URI.parse(ENV['PACKAGEBASE'])
  end

  if ENV.key?('PACKAGESITE')
    return URI.parse(ENV['PACKAGESITE']) + '../'
  end

  return URI.parse(ENV['PACKAGEROOT'] || DEFAULT_PACKAGEROOT) +
    "/pub/FreeBSD/ports/#{`uname -m`.chomp}/packages-#{`uname -r`.chomp.downcase}/"
rescue => e
  warning_message e.message
  return nil
end

def fetch(uri, path = nil)
  if path.nil?
    path = File.basename(uri.path)
  end

  if path.empty?
    warning_message 'Missing filename'
    return false
  end

  cmdline = format(PKG_FETCH, 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.exists?(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
