#!/usr/bin/python
# dcut --- debian command upload tool
# Copyright (c) 2004,2005,2008 Thomas Viehmann <tv@beamnet.de>
# portions ripped from dput:
#   Copyright (c) 2000,2001,2002,2003,2004 Christian Kurz
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import sys, getopt, os, tempfile, string, time, subprocess

sys.path.insert(0,'/usr/share/dput/helper')
import dputhelper

files_to_remove = []

progname = "dcut"
version = "0.2.1"

USAGE = """Usage: %s [options] [host] command [, command]
 Supported options (see man page for long forms):
   -c file Config file to parse.
   -d      Enable debug messages.
   -h      Display this help message.
   -s      Simulate the commands file creation only.
   -v      Display version information.
   -m maintaineraddress
           Use maintainer information in "Uploader:" field.
   -k keyid
           Use this keyid for signing.
   -O file Write commands to file.
   -U file Upload specified commands file (presently no checks).
   -i changes
           Upload a commands file to remove files listed in .changes.
 Supported commands: mv, rm
   (No paths or command-line options allowed on ftp-master.)
"""%(sys.argv[0])

validcommands = ('rm','cancel','reschedule')
def getoptions():
  # seed some defaults
  options = {'debug':0, 'simulate':0, 'config':None, 'host':None,
             'uploader':None, 'keyid':None, 'passive':0, 'filetocreate':None,
             'filetoupload':None, 'changes':None}
  # enable debugging very early
  if ('-d' in sys.argv[1:] or '--debug' in sys.argv[1:]):
    options['debug']=1
    print "D: %s %s" % (progname,version)

  # check environment for maintainer
  if options['debug']: print "D: trying to get maintainer email from environment"

  if os.environ.has_key('DEBEMAIL'):
    if os.environ['DEBEMAIL'].find('<')<0:
      options['uploader']=os.environ.get("DEBFULLNAME",'')
      if options['uploader']:
        options['uploader'] += ' '
      options['uploader'] += '<%s>'%(os.environ['DEBEMAIL'])
    else:
      options['uploader'] = os.environ['DEBEMAIL']
    if options['debug']: print "D: Uploader from env: %s"%(options['uploader'])
  elif os.environ.has_key('EMAIL'):
    if os.environ['EMAIL'].find('<')<0:
      options['uploader']=os.environ.get("DEBFULLNAME",'')
      if options['uploader']:
        options['uploader'] += ' '
      options['uploader'] += '<%s>'%(os.environ['EMAIL'])
    else:
      options['uploader'] = os.environ['EMAIL']
    if options['debug']: print "D: Uploader from env: %s"%(options['uploader'])
  else:
    if options['debug']: print "D: Guessing uploader"
    import pwd
    pwrec = pwd.getpwuid(os.getuid())
    try:
      s = open('/etc/mailname').read().strip()
    except IOError:
      s = ''
    if not s:
      if options['debug']: print "D: Guessing uploader: /etc/mailname was a failure"
      s = os.popen('/bin/hostname --fqdn').read().strip()
    if s:
      options['uploader'] = '%s <%s@%s>'%(pwrec[4].split(',')[0],pwrec[0],s)
      if options['debug']: print "D: Guessed uploader: %s"%(options['uploader'])
    else:
      if options['debug']: print "D: Couldn't guess uploader"
  # parse command line arguments
  (opts, arguments) = dputhelper.getopt(sys.argv[1:],
                            'c:dDhsvm:k:PU:O:i:',
                            ['config=', 'debug',
                             'help', 'simulate', 'version','host=',
                             'maintainteraddress=', 'keyid=',
                             'passive', 'upload=', 'output=', 'input='
                             ])

  for (option, arg) in opts:
    if options['debug']: print 'D: processing arg "%s", option "%s"'%(option,arg)
    if option in ('-h', '--help'):
      print USAGE
      sys.exit(0)
    elif option in ('-v', '--version'):
      print progname,version
      sys.exit(0)
    elif option in ('-d', '--debug'):
      options['debug'] = 1
    elif option in ('-c', '--config'):
      options['config'] = arg
    elif option in ('-m', '--maintaineraddress'):
      options['uploader'] = arg
    elif option in ('-k', '--keyid'):
      options['keyid'] = arg
    elif option in ('-s', '--simulate'):
      options['simulate'] = 1
    elif option in ('-P', '--passive'):
      options['passive'] = 1
    elif option in ('-U', '--upload'):
      options['filetoupload'] = arg
    elif option in ('-O', '--output'):
      options['filetocreate'] = arg
    elif option=='--host':
      options['host'] = arg
    elif option in ('-i', '--input'):
      options['changes'] = arg
    else:
      print >> sys.stderr, "%s internal error: Option %s, argument %s unknown"%(
              progname,option,arg)
      sys.exit(1)
  
  if not options['host'] and arguments and arguments[0] not in validcommands:
      options['host'] = arguments[0]
      if options['debug']: print 'D: first argument "%s" treated as host'%(options['host'])
      del arguments[0]

    
  # we don't create command files without uploader
  if not options['uploader'] and (options['filetoupload'] or options['changes']):
    print  >> sys.stderr, "%s error: command file cannot be created without maintainer email"%progname
    print  >> sys.stderr, '%s        please set $DEBEMAIL, $EMAIL or use the "-m" option'%(len(progname)*' ')
    sys.exit(1)
    
  return options, arguments

def parse_queuecommands(arguments,options,config):
  commands = []
  arguments = arguments[:] # want to consume a copy of arguments
  arguments.append(0)
  curarg = []
  while arguments:
    if arguments[0] in validcommands:
      curarg = [arguments[0]]
      if arguments[0] == 'rm':
        if len(arguments)>1 and arguments[1]=='--nosearchdirs':
          del arguments[1]
        else:
          curarg.append('--searchdirs')
    else:
      if not curarg and arguments[0]!=0:
        print  >> sys.stderr, 'Error: Could not parse commands at "%s"'%(arguments[0])
        sys.exit(1)
      if str(arguments[0])[-1] in (',',';',0):
        curarg.append(arguments[0][0:-1])
        arguments[0] = ','
      if arguments[0] in (',',';',0) and curarg:
        # TV-TODO: syntax check for #args etc.
        if options['debug']: print 'D: Successfully parsed command "%s"'%(' '.join(curarg))
        commands.append(' '.join(curarg))
        curarg = []
      else:
        # TV-TODO: maybe syntax check the arguments here
        curarg.append(arguments[0])
    del arguments[0]
  if not commands:
    print >> sys.stderr, 'Error: no arguments given, see dcut -h'
    sys.exit(1)
  return commands

def write_commands(commands, options, config, tempdir):
  if options['filetocreate']:
    filename = options['filetocreate']
  else:
    translationorig =  ''.join(map(chr, range(256)))+string.ascii_letters+string.digits
    translationdest = 256*'_'+string.ascii_letters+string.digits
    uploadpartforname = string.translate(options['uploader'],
                                       string.maketrans(
      translationorig,translationdest))
    filename = (progname+'.%s.%d.%d.commands'%
                (uploadpartforname,int(time.time()),os.getpid()))
    if tempdir:
      filename = os.path.join(tempdir,filename)
      files_to_remove.append(filename)
  f = open(filename,"w")
  f.write("Uploader: %s\n"%options['uploader'])
  f.write("Commands:\n %s\n\n"%('\n '.join(commands)))
  f.close()
  debsign_cmdline = ['debsign']
  debsign_cmdline.append('-m%s' % options['uploader'])
  if options['keyid']: debsign_cmdline.append('-k%s' % options['keyid'])
  debsign_cmdline.append('%s' % filename)
  if options['debug']: print "D: calling debsign:",debsign_cmdline
  debsign_prog = subprocess.Popen(debsign_cmdline)
  if os.waitpid(debsign_prog.pid, 0)[1]:
    print >> sys.stderr, "Error: debsign failed."
    sys.exit(1)
  return filename

def upload_stolen_from_dput_main(host, upload_methods, config,debug,simulate,files_to_upload,ftp_passive_mode):
        # Check the upload methods that we have as default and per host
        if debug: print "D: Default Method: %s" % \
            config.get('DEFAULT', 'method')
        if not upload_methods.has_key(config.get('DEFAULT', 'method')):
            print >> sys.stderr, "Unknown upload method: %s" % \
                config.get('DEFAULT', 'method')
            sys.exit(1)
        if debug: print "D: Host Method: %s" % config.get(host, 'method') 
        if not upload_methods.has_key(config.get(host, 'method')):
            print >> sys.stderr, "Unknown upload method: %s" % config.get(host, 'method')
            sys.exit(1)

        # Inspect the Config and set appropriate upload method
        if not config.get(host, 'method'):
            method = config.get('DEFAULT', 'method')
        else:
            method = config.get(host, 'method')

        # Check now the login and redefine it if needed
        if config.has_option(host, 'login') and config.get(host, 'login') != 'username':
          login = config.get(host, 'login')
        elif config.has_option('DEFAULT', 'login') and config.get('DEFAULT', 'login') != 'username':
          login = config.get('DEFAULT', 'login')
        else:
          # Try to get the login from the enviroment
          if os.environ.has_key('USER'):
            login = os.environ['USER']
          else:
            print "$USER not set, will use login information."
            # Else use the current username
            login = pwd.getpwuid(os.getuid( ))[0]
            if debug: print "D: User-ID: %s" % os.getuid()
          if debug: print "D: Neither host %s nor default login used. Using %s" %(host,login)
        if debug: print "D: Login to use: %s" % login

        # Messy, yes. But it isn't referenced by the upload method anyway.
        if config.get(host, 'method') == 'local':
            fqdn = 'localhost'
        else:
            fqdn = config.get(host, 'fqdn')
        incoming = config.get(host, 'incoming')
        # Do the actual upload
        if not simulate:
            if debug:
                print "D: FQDN: %s" % fqdn
                print "D: Login: %s" % login
                print "D: Incoming: %s" % incoming
            if method == 'ftp':
                ftp_mode = config.getboolean(host, 'passive_ftp')
                if ftp_passive_mode == 1: ftp_mode = 1
                if ftp_mode == 1:
                    if debug: 
                            if ftp_passive_mode == 1:
                                print "D: Using passive ftp"
                            else:
                                print "D: Using active ftp"
                upload_methods[method](fqdn, login, incoming, \
                    files_to_upload, debug, ftp_mode)
            elif method == 'scp':
                if debug and config.getboolean(host, 'scp_compress'):
                    print "D: Setting compression for scp"
                scp_compress = config.getboolean(host, 'scp_compress')
                ssh_config_options = filter(None, map(lambda x: x.strip(),
                   config.get (host ,'ssh_config_options').split('\n')))
                upload_methods[method](fqdn, login, incoming, \
                    files_to_upload, debug, scp_compress, ssh_config_options)
            else:
                upload_methods[method](fqdn, login, incoming, \
                    files_to_upload, debug, 0)
        # Or just simulate it.
        else:
            for file in files_to_upload:
                print >> sys.stderr, 'Uploading with %s: %s to %s:%s' % (method, \
                    file, fqdn, incoming)
                os.system("cat %s"%file)

def load_dput(options):
  if options['debug']: print 'D: loading dput'
  d = {}
  execfile("/usr/bin/dput",d)
  return d


def dcut():
  options,arguments = getoptions()
  dput = load_dput(options)
  # dput read_configs sets dput.config
  if options['debug']: print 'D: calling dput.read_configs'
  dput['read_configs'](options['config'], options['debug'])
  config = dput['config']
  if not options['host'] and config.has_option('DEFAULT', 'default_host_main'):
    options['host'] = config.get('DEFAULT', 'default_host_main')
    if options['debug']: print 'D: Using host "%s" (default_host_main)'%(options['host'])
    if not options['host']:
      options['host'] = 'ftp-master'
      if options['debug']: print 'D: Using host "%s" (hardcoded)'%(options['host'])
  tempdir = None
  filename = None
  try:
    if not (options['filetoupload'] or options['filetocreate']):
      tempdir = tempfile.mkdtemp(prefix=progname+'.')
    if not options['filetocreate']:
      if not options['host']:
        print "Error: No host specified and no default found in config"
        sys.exit(1)
      if not config.has_section(options['host']):
        print "No host %s found in config" % (options['host'])
        sys.exit(1)
      else:
        if config.has_option(options['host'], 'allow_dcut'):
          dcut_allowed = config.getboolean(options['host'], 'allow_dcut')
        else:
          dcut_allowed = config.getboolean('DEFAULT', 'allow_dcut')
        if not dcut_allowed:
          print 'Error: dcut is not supported for this upload queue.'
          sys.exit(1)
    if options['filetoupload']:
      if arguments:
        print 'Error: cannot take commands when uploading existing file,'
        print '       "%s" found'%(' '.join(arguments))
        sys.exit(1)
      commands = None
      filename = options['filetoupload']
      if not filename.endswith(".commands"):
        print 'Error: I\'m insisting on the .commands extension, which'
        print '       "%s" doesnt seem to have.'%filename
      # TV-TODO: check file to be readable?
    elif options['changes']:
      parse_changes = dput['parse_changes']
      removecommands = create_commands(options, config, parse_changes)
      filename = write_commands(removecommands, options, config, tempdir)
    else:
      commands = parse_queuecommands(arguments,options,config)
      filename = write_commands(commands, options, config, tempdir)
    if not options['filetocreate']:
      # hack to get upload_methods
      myglobals={'dput':dput}
      dput['import_upload_functions']()
      upload_methods = dput['upload_methods']
      upload_stolen_from_dput_main(options['host'],upload_methods, config,options['debug'],options['simulate'],[filename],options['passive'])
  finally:
    # we use sys.exit, so we need to clean up here
    if tempdir:
      # file is temporary iff in tempdir
      for filename in files_to_remove:
        os.unlink(filename)
      os.rmdir(tempdir)

# Parses a .changes file and returns commands to remove files named in it
def create_commands(options, config, parse_changes):
  changes_file = options['changes']
  if options['debug']:
    print "D: Parsing changes file (%s) for files to remove" % changes_file
  try:
    chg_fd = open(changes_file, 'r')
  except IOError:
    print "Can't open changes file: %s" % changes_file
    sys.exit(1)
  the_changes = parse_changes(chg_fd)
  chg_fd.close
  removecommands = ['rm --searchdirs ' + os.path.basename(changes_file)]
  for file in the_changes.dict['files'].split('\n'):
    fn = string.split(file)[4] # filename only
    rm = 'rm --searchdirs ' + fn
    if options['debug']: print "D: Will remove %s with '%s'" % (fn, rm)
    removecommands.append(rm)
  return removecommands

if __name__=="__main__":
  try:
    dcut()
  except dputhelper.DputException, e:
    print >> sys.stderr, e
    sys.exit(1)
