# Copyright 2002 Ben Escoto
#
# This file is part of duplicity.
#
# Duplicity 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 3 of the License, or (at your
# option) any later version.
#
# Duplicity 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 duplicity; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

"""Parse command line, check for consistency, and set globals"""

import getopt, re, sys, os
import backends, globals, log, path, selection, gpg, dup_time

select_opts = [] # Will hold all the selection options
select_files = [] # Will hold file objects when filelist given
full_backup = None # Will be set to true if -f or --full option given
list_current = None # Will be set to true if --list-current option given
collection_status = None # Will be set to true if --collection-status given
cleanup = None # Set to true if --cleanup option given
verify = None # Set to true if --verify option given

commands = ["cleanup",
			"collection-status",
			"full",
			"incremental",
			"list-current-files",
			"remove-older-than",
			"remove-all-but-n-full",
			"verify",
			]

options = ["allow-source-mismatch",
		   "archive-dir=",
		   "current-time=",
		   "encrypt-key=",
		   "exclude=",
		   "exclude-device-files",
		   "exclude-filelist=",
		   "exclude-globbing-filelist=",
		   "exclude-filelist-stdin",
		   "exclude-other-filesystems",
		   "exclude-regexp=",
		   "file-to-restore=",
		   "force",
		   "ftp-passive",
		   "ftp-regular",
		   "full-if-older-than=",
		   "gpg-options=",
		   "help",
		   "include=",
		   "include-filelist=",
		   "include-filelist-stdin",
		   "include-globbing-filelist=",
		   "include-regexp=",
		   "no-encryption",
		   "no-print-statistics",
		   "null-separator",
		   "num-retries=",
		   "restore-dir=",
		   "restore-time=",
		   "scp-command=",
		   "sftp-command=",
		   "short-filenames",
		   "sign-key=",
		   "ssh-askpass",
		   "ssh-options=",
		   "timeout=",
		   "time-separator=",
		   "verbosity=",
		   "version",
		   "volsize=",
		   ]

def parse_cmdline_options(arglist):
	"""Parse argument list"""
	global select_opts, select_files, full_backup
	global list_current, collection_status, cleanup, remove_time, verify

	def sel_fl(filename):
		"""Helper function for including/excluding filelists below"""
		try:
			return open(filename, "r")
		except IOError:
			log.FatalError("Error opening file %s" % filename)

	# expect no cmd and two positional args
	cmd = ""
	num_expect = 2
	
	# process first arg as command
	if arglist and arglist[0][0] != '-':
		cmd = arglist.pop(0)
		possible = [c for c in commands if c.startswith(cmd)]
		# no unique match, that's an error
		if len(possible) > 1:
			log.FatalError("command '%s' not unique, could be %s" % (cmd, possible))
		# only one match, that's a keeper
		elif len(possible) == 1:
			cmd = possible[0]
		# no matches, assume no cmd
		elif not possible:
			arglist.insert(0, cmd)
		
	if cmd == "cleanup":
		cleanup = True
		num_expect = 1
	elif cmd == "collection-status":
		collection_status = True
		num_expect = 1
	elif cmd == "full":
		full_backup = True
		num_expect = 2
	elif cmd == "incremental":
		globals.incremental = True
		num_expect = 2
	elif cmd == "list-current-files":
		list_current = True
		num_expect = 1
	elif cmd == "remove-older-than":
		try:
			arg = arglist.pop(0)
		except:
			command_line_error("Missing time string for remove-older-than")
		globals.remove_time = dup_time.genstrtotime(arg)
		num_expect = 1
	elif cmd == "remove-all-but-n-full":
		try:
			arg = arglist.pop(0)
		except:
			command_line_error("Missing count for remove-all-but-n-full")
		globals.keep_chains = int(arg)
		
		if not globals.keep_chains > 0:
			command_line_error("remove-all-but-n-full count must be > 0")
		
		num_expect = 1
	elif cmd == "verify":
		verify = True
		num_expect = 2

	# parse the remaining args
	try:
		optlist, args = getopt.gnu_getopt(arglist, "rt:v:V", options)
	except getopt.error, e:
		command_line_error("%s" % (str(e),))

	for opt, arg in optlist:
		if opt == "--allow-source-mismatch":
			globals.allow_source_mismatch = 1
		elif opt == "--archive-dir":
			set_archive_dir(arg)
		elif opt == "--current-time":
			dup_time.setcurtime(get_int(arg, "current-time"))
		elif opt == "--encrypt-key":
			globals.gpg_profile.recipients.append(arg)
		elif (opt == "--exclude" or
			  opt == "--exclude-regexp" or
			  opt == "--include" or
			  opt == "--include-regexp"):
			select_opts.append((opt, arg))
		elif (opt == "--exclude-device-files" or
			  opt == "--exclude-other-filesystems"):
			select_opts.append((opt, None))
		elif (opt == "--exclude-filelist" or
			  opt == "--include-filelist" or
			  opt == "--exclude-globbing-filelist" or
			  opt == "--include-globbing-filelist"):
			select_opts.append((opt, arg))
			select_files.append(sel_fl(arg))
		elif opt == "--exclude-filelist-stdin":
			select_opts.append(("--exclude-filelist", "standard input"))
			select_files.append(sys.stdin)
		elif opt == "--full-if-older-than":
			globals.full_force_time = dup_time.genstrtotime(arg)
		elif opt == "--force":
			globals.force = 1
		elif opt == "--ftp-passive":
			globals.ftp_connection = 'passive'
		elif opt == "--ftp-regular":
			globals.ftp_connection = 'regular'
		elif opt == "--gpg-options":
			gpg.gpg_options = (gpg.gpg_options + ' ' + arg).strip()
		elif opt == "--help":
			usage();
			sys.exit(1);
		elif opt == "--include-filelist-stdin":
			select_opts.append(("--include-filelist", "standard input"))
			select_files.append(sys.stdin)
		elif opt == "--no-encryption":
			globals.encryption = 0
		elif opt == "--no-print-statistics":
			globals.print_statistics = 0
		elif opt == "--null-separator":
			globals.null_separator = 1
		elif opt == "--num-retries":
			globals.num_retries = int(arg)
		elif (opt == "-r" or
			  opt == "--file-to-restore"):
			globals.restore_dir = arg
		elif (opt == "-t" or
			  opt == "--restore-time"):
			globals.restore_time = dup_time.genstrtotime(arg)
		elif opt == "--scp-command":
			backends.scp_command = arg
		elif opt == "--sftp-command":
			backends.sftp_command = arg
		elif opt == "--short-filenames":
			globals.short_filenames = 1
		elif opt == "--sign-key":
			set_sign_key(arg)
		elif opt == "--ssh-askpass":
			backends.ssh_askpass = True
		elif opt == "--ssh-options":
			backends.ssh_options = (backends.ssh_options + ' ' + arg).strip()
		elif opt == "--timeout":
			globals.timeout = int(arg)
		elif opt == "--time-separator":
			if arg == '-':
				command_line_error("Dash ('-') not valid for time-separator.")
			globals.time_separator = arg
			dup_time.curtimestr = dup_time.timetostring(dup_time.curtime)
		elif opt == "-V" or opt == "--version":
			print "duplicity", str(globals.version)
			sys.exit(0)
		elif (opt == "-v" or
			  opt == "--verbosity"):
			log.setverbosity(int(arg))
		elif opt == "--volsize":
			globals.volsize = int(arg)*1024*1024
		else:
			command_line_error("Unknown option %s" % opt)

	if len(args) != num_expect:
		command_line_error("Expected %d args, got %d" % (num_expect, len(args)))

	return args

def command_line_error(message):
	"""Indicate a command line error and exit"""
	sys.stderr.write("Command line error: %s\n" % (message,))
	sys.stderr.write("Enter 'duplicity --help' for help screen.\n")
	sys.exit(1)

def usage():
	"""Print terse usage info"""
	sys.stdout.write("""
duplicity version %s running on %s.
Usage:
	duplicity [full|incremental] [options] source_dir target_url
	duplicity [restore] [options] source_url target_dir
	duplicity verify [options] source_url target_dir
	duplicity collection-status [options] target_url
	duplicity list-current-files [options] target_url
	duplicity cleanup [options] target_url
	duplicity remove-older-than time [options] target_url
	duplicity remove-all-but-n-full count [options] target_url

Backends and their URL formats:
	ssh://user@other.host:port/some_dir
	scp://user@other.host:port/some_dir
	ftp://user@other.host/some_dir
	hsi://user@other.host/some_dir
	file:///some_dir
	rsync://user@host:/module/some_dir
	rsync://user@host/some_non_module_path
	s3://host/bucket_name[/prefix]
	s3+http://bucket_name[/prefix]
	webdav://user@other.host/some_dir
	webdavs://user@other.host/some_dir

Commands:
	cleanup <target_url>
	collection-status <target_url>
	full <source_dir> <target_url>
	incr <source_dir> <target_url>
	list-current-files <target_url>
	restore <target_url> <source_dir>
	remove-older-than <time> <target_url>
	remove-all-but-n-full <count> <target_url>
	verify <target_url> <source_dir>

Options:
	--allow-source-mismatch
	--archive-dir <path>
	--encrypt-key <gpg-key-id>
	--exclude <shell_pattern>
	--exclude-device-files
	--exclude-filelist <filename>
	--exclude-filelist-stdin
	--exclude-globbing-filelist <filename>
	--exclude-other-filesystems
	--exclude-regexp <regexp>
	--file-to-restore <path>
 	--full-if-older-than <time>
	--force
	--ftp-passive
	--ftp-regular
	--gpg-options
	--include <shell_pattern>
	--include-filelist <filename>
	--include-filelist-stdin
	--include-globbing-filelist <filename>
	--include-regexp <regexp>
	--no-encryption
	--no-print-statistics
	--null-separator
	--num-retries <number>
	--scp-command <command>
	--sftp-command <command>
	--sign-key <gpg-key-id>>
	--ssh-askpass
	--ssh-options
	--short-filenames
	--timeout <seconds>
	-t<time>, --restore-time <time>
	--volsize <number>
	-v[0-9], --verbosity [0-9]
""" % (globals.version, sys.platform))


def get_int(int_string, description):
	"""Require that int_string be an integer, return int value"""
	try: return int(int_string)
	except ValueError: log.FatalError("Received '%s' for %s, need integer" %
									  (int_string, description))

def set_archive_dir(dirstring):
	"""Check archive dir and set global"""
	archive_dir = path.Path(os.path.expanduser(dirstring))
	if not archive_dir.isdir():
		log.FatalError("Specified archive directory '%s' does not exist, "
					   "or is not a directory" % (archive_dir.name,))
	globals.archive_dir = archive_dir

def set_sign_key(sign_key):
	"""Set globals.sign_key assuming proper key given"""
	if not len(sign_key) == 8 or not re.search("^[0-9A-F]*$", sign_key):
		log.FatalError("Sign key should be an 8 character hex string, like "
					   "'AA0E73D2'.\nReceived '%s' instead." % (sign_key,))
	globals.gpg_profile.sign_key = sign_key

def set_selection():
	"""Return selection iter starting at filename with arguments applied"""
	global select_opts, select_files
	sel = selection.Select(globals.local_path)
	sel.ParseArgs(select_opts, select_files)
	globals.select = sel.set_iter()

def set_backend(arg1, arg2):
	"""Figure out which arg is url, set backend

	Return value is pair (path_first, path) where is_first is true iff
	path made from arg1.

	"""
	backend1, backend2 = backends.get_backend(arg1), backends.get_backend(arg2)
	if not backend1 and not backend2:
		log.FatalError(
"""One of the arguments must be an URL.  Examples of URL strings are
"scp://user@host.net:1234/path" and "file:///usr/local".  See the man
page for more information.""")
	if backend1 and backend2:
		command_line_error("Two URLs specified.  "
						   "One argument should be a path.")
	if backend1:
		globals.backend = backend1
		return (None, arg2)
	elif backend2:
		globals.backend = backend2
		return (1, arg1)

def process_local_dir(action, local_pathname):
	"""Check local directory, set globals.local_path"""
	local_path = path.Path(path.Path(local_pathname).get_canonical())
	if action == "restore":
		if local_path.exists() and not local_path.isemptydir():
			log.FatalError("Restore destination directory %s already "
						   "exists.\nWill not overwrite." % (local_pathname,))
	elif action == "verify":
		if not local_path.exists():
			log.FatalError("Verify directory %s does not exist" %
						   (local_path.name,))
	else:
		assert action == "full" or action == "inc"
		if not local_path.exists():
			log.FatalError("Backup source directory %s does not exist."
						   % (local_path.name,))
	
	globals.local_path = local_path

def check_consistency(action):
	"""Final consistency check, see if something wrong with command line"""
	global full_backup, select_opts, list_current
	def assert_only_one(arglist):
		"""Raises error if two or more of the elements of arglist are true"""
		n = 0
		for m in arglist:
			if m: n+=1
		assert n <= 1, "Invalid syntax, two conflicting modes specified"
	if action in ["list-current", "collection-status",
				  "cleanup", "remove-old", "remove-all-but-n-full"]:
		assert_only_one([list_current, collection_status, cleanup,
						 globals.remove_time is not None])
	elif action == "restore" or action == "verify":
		if full_backup:
			command_line_error("--full option cannot be used when "
							   "restoring or verifying")
		elif globals.incremental:
			command_line_error("--incremental option cannot be used when "
							   "restoring or verifying")
		if select_opts and action == "restore":
			command_line_error("Selection options --exclude/--include\n"
							   "currently work only when backing up, "
							   "not restoring.")
	else:
		assert action == "inc" or action == "full"
		if verify: command_line_error("--verify option cannot be used "
									  "when backing up")
		if globals.restore_dir:
			log.FatalError("--restore-dir option incompatible with %s backup"
						   % (action,))

def ProcessCommandLine(cmdline_list):
	"""Process command line, set globals, return action

	action will be "list-current", "collection-status", "cleanup",
	"remove-old", "restore", "verify", "full", or "inc".

	"""
	globals.gpg_profile = gpg.GPGProfile()

	args = parse_cmdline_options(cmdline_list)
	if len(args) < 1: command_line_error("Too few arguments")
	elif len(args) == 1:
		if list_current: action = "list-current"
		elif collection_status: action = "collection-status"
		elif cleanup: action = "cleanup"
		elif globals.remove_time is not None: action = "remove-old"
		elif globals.keep_chains is not None: action = "remove-all-but-n-full"
		else: command_line_error("Too few arguments")
		globals.backend = backends.get_backend(args[0])
		if not globals.backend: log.FatalError("""Bad URL '%s'.
Examples of URL strings are "scp://user@host.net:1234/path" and
"file:///usr/local".  See the man page for more information.""" % (args[0],))
	elif len(args) == 2: # Figure out whether backup or restore
		backup, local_pathname = set_backend(args[0], args[1])
		if backup:
			if full_backup: action = "full"
			else: action = "inc"
		else:
			if verify: action = "verify"
			else: action = "restore"

		process_local_dir(action, local_pathname)
		if action in ['full', 'inc', 'verify']: set_selection()
	elif len(args) > 2: command_line_error("Too many arguments")

	check_consistency(action)
	log.Log("Main action: " + action, 7)
	return action


syntax highlighted by Code2HTML, v. 0.9.1