#!/usr/bin/env python
# duplicity -- Extend rdiff functionality to directories
# Version 0.1.0 released August 26, 2002
#
# Copyright (C) 2001, 2002 Ben Escoto <bescoto@stanford.edu>
#
# This program is licensed under the GNU General Public License (GPL).
# you can redistribute it and/or modify it under the terms of the GNU
# General Public License as published by the Free Software Foundation,
# Inc., 675 Mass Ave, Cambridge MA 02139, USA; either version 2 of the
# License, or (at your option) any later version.  Distributions of
# rdiff-backup should include a copy of the GPL in a file called
# COPYING.  The GPL is also available online at
# http://www.gnu.org/copyleft/gpl.html.
#
# See http://rdiff-backup.stanford.edu/duplicity for more information.
# Please send mail to me or the mailing list if you find bugs or have
# any suggestions.

from __future__ import generators
import sys, getopt, re, gzip, os, time
from duplicity import diffdir, log, globals, selection, path, \
	 dup_time, backends, gpg, misc, robust, file_naming, manifest, \
	 collections, patchdir

# Will hold all the selection options
select_opts = []


def parse_cmdline_options(arglist):
	"""Parse argument list"""
	global select_opts
	try: optlist, args = getopt.getopt(arglist, "t:v:V",
		 ["current-time=", "exclude=", "exclude-device-files",
		  "exclude-filelist=", "exclude-filelist-stdin",
		  "exclude-other-filesystems", "exclude-regexp=", "include=",
		  "include-filelist=", "include-filelist-stdin",
		  "include-regexp=", "null-separator", "restore-time=",
		  "verbosity="])
	except getopt.error, e:
		command_line_error("Bad command line option: %s" % (str(e),))

	for opt, arg in optlist:
		if opt == "--current-time":
			globals.current_time = get_int(arg, "current-time")
		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":
			select_opts.append((opt, (arg, open(arg, "rb"))))
		elif (opt == "--exclude-filelist-stdin" or
			  opt == "--include-filelist-stdin"):
			select_opts.append((opt, ("stdin", sys.stdin)))
		elif opt == "-t" or opt == "--restore-time":
			globals.restore_time = dup_time.genstrtotime(arg)
		elif opt == "-V":
			print "duplicity version", str(globals.version)
			sys.exit(0)
		elif opt == "-v" or opt == "--verbosity": log.setverbosity(int(arg))
		else: command_line_error("Unknown option %s" % opt)

	return args

def command_line_error(message):
	"""Indicate a command line error and exit"""
	sys.stderr.write("Error: %s\n" % (message,))
	sys.stderr.write("See the duplicity manual page for instructions\n")
	sys.exit(1)

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 get_action(args):
	"""Figure out the main action from the arguments"""

	if len(args) < 3: command_line_error("Too few arguments")
	command = args[0]
	if command != "restore" and len(args) < 4:
		command_line_error("Too few arguments")
	if len(args) > 4: command_line_error("Too many arguments")

	if command == "inc" or command == "increment": command = "inc"
	return command, args[1:]

def get_selection(filename):
	"""Return selection iter starting at filename with arguments applied"""
	sel = selection.Select(path.Path(filename))
	sel.ParseArgs(select_opts)
	return sel.set_iter()

def set_archive_dir(archive_name):
	"""Verify archive dir and set globals.archive_dir exist"""
	verify_duplicity_dir()
	if not re.search("^[0-9a-zA-Z_-]*$", archive_name):
		log.FatalError("Archive name %s contains bad characters"
					   % archive_name)
	archive_dir = globals.dupdir.append(archive_name)
	if not archive_dir.exists():
		log.Log("Creating archive directory %s" % archive_dir.name, 3)
		archive_dir.mkdir()
	elif not archive_dir.isdir():
		log.FatalError("%s exists but isn't a directory" % archive_dir.name)
	globals.archive_dir = archive_dir
	return archive_dir

def verify_duplicity_dir():
	"""Create .duplicity if not already there"""
	os.umask(077)
	try: homedir = os.environ['HOME']
	except KeyError:
		log.FatalError("HOME environment variable not set, "
					   "cannot find home directory.")
	dupdir = path.Path(homedir + "/.duplicity")
	if not dupdir.exists():
		log.Log("Creating duplicity directory %s" % dupdir.name, 3)
		dupdir.mkdir()
	elif not dupdir.isdir():
		log.FatalError("%s exists but isn't a directory" % dupdir.name)
	globals.dupdir = dupdir

def get_passphrase():
	"""Get passphrase from environment"""
	try: return os.environ['PASSPHRASE']
	except KeyError:
		log.FatalError("Cannot read passphrase from environment "
					   "variable PASSPHRASE.")

def write_multivol(backup_type, archive_dir, tarblock_iter, backend):
	"""Encrypt volumes of tarblock_iter and write to backend

	backup_type should be "inc" or "full" and only matters here when
	picking the filenames.  The path_prefix will determine the names
	of the files written to backend.  Also writes manifest file.

	"""
	mf = manifest.Manifest()
	volume_number = 1
	while tarblock_iter.peek():
		# Create volume
		start_index = tarblock_iter.peek().index
		volume_path = archive_dir.append(
			file_naming.get(backup_type, volume_number, encrypted = 1))
		gpg.GPGWriteFile(tarblock_iter, volume_path.name, get_passphrase())
		volume_path.setdata()
		end_index = tarblock_iter.get_previous_index()

		# Add volume information to manifest
		vi = manifest.VolumeInfo()
		vi.set_info(volume_number, start_index, end_index)
		vi.set_hash("SHA1", gpg.get_sha_hash(volume_path))
		mf.add_volume_info(vi)

		backend.put(volume_path)
		volume_path.delete()
		volume_number += 1

	write_manifest(mf, backup_type, archive_dir, backend)

def write_manifest(mf, backup_type, archive_dir, backend):
	"""Write manifest to file in archive_dir and encrypted to backend"""
	manifest_path = archive_dir.append(
		file_naming.get(backup_type, manifest = 1))
	mf.write_to_path(manifest_path)

	encrypted_mf_path = archive_dir.append(
		file_naming.get(backup_type, manifest = 1, encrypted = 1))
	assert not encrypted_mf_path.exists()
	fout = gpg.GPGFile(1, encrypted_mf_path, get_passphrase())
	fout.write(str(mf))
	assert not fout.close()
	backend.put(encrypted_mf_path)
	encrypted_mf_path.delete()

def full_backup(sel, backend):
	"""Do full backup of directory to backend, using archive_dir"""
	tmp_sig_path = robust.get_tmpfile(globals.archive_dir)
	sig_outfp = gzip.GzipFile(tmp_sig_path.name, "wb")

	tarblock_iter = diffdir.DirFull_WriteSig(sel, sig_outfp)

	write_multivol("full", globals.archive_dir, tarblock_iter, backend)
	tmp_sig_path.rename(
		globals.archive_dir.append(file_naming.get("full-sig", gzipped = 1)))
	
def incremental_backup(col_stats, sel, backend):
	"""Do incremental backup of directory to backend, using archive_dir"""
	if not col_stats.matched_chain_pair:
		log.FatalError("Unable to start incremental backup, old signatures "
					   "not found.")
	sig_chain = col_stats.matched_chain_pair[0]		
	dup_time.setprevtime(sig_chain.end_time)

	tmp_sig_path = robust.get_tmpfile(globals.archive_dir)
	new_sig_outfp = gzip.GzipFile(tmp_sig_path.name, "wb")

	tarblock_iter = diffdir.DirDelta_WriteSig(sel, sig_chain.get_fileobjs(),
											  new_sig_outfp)
	write_multivol("inc", globals.archive_dir, tarblock_iter, backend)
	tmp_sig_path.rename(
		globals.archive_dir.append(file_naming.get("new-sig", gzipped = 1)))


def restore(backend, restrict_pathname, target_pathname):
	"""Restore archive in backend to target_pathname.

	If restrict_pathname is given, only backup files starting it.

	"""
	if restrict_pathname: index = tuple(restrict_pathname.split("/"))
	else: index = ()
	target = path.Path(target_pathname)

	for backup_set in restore_get_setlist(backend,
										  globals.restore_time or
										  globals.current_time):
		log.Log("Patching from backup set at time " +
				backup_set.get_timestr(), 4)
		patchdir.Patch_from_iter(target,
								 restore_fileobj_iter(backup_set, index),
								 index)

def restore_get_setlist(backend, time):
	"""Get list of relevant backup sets"""
	col_stats = collections.CollectionsStatus(backend, None)
	col_stats.set_values(no_archive_dir = 1)
	chain = col_stats.get_backup_chain_at_time(time)
	return chain.get_sets_at_time(time)

def restore_fileobj_iter(backup_set, index = ()):
	"""Get file object iterator from backup_set contain given index"""
	manifest = restore_get_manifest(backup_set)
	for vol_num in manifest.get_containing_volumes(index):
		yield restore_get_enc_fileobj(backup_set.backend,
									  backup_set.volume_name_dict[vol_num])

def restore_get_manifest(backup_set):
	"""Return manifest from given backup set"""
	temp = robust.get_tmpfile()
	fp = restore_get_enc_fileobj(backup_set.backend,
								 backup_set.remote_manifest_name)
	buf = fp.read()
	assert not fp.close()
	return manifest.Manifest().from_string(buf)

def restore_get_enc_fileobj(backend, filename, encrypted = 1):
	"""Return plaintext fileobj from encrypted filename on backend"""
	class FileLike:
		"""Like file but add extra closing hook"""
		def __init__(self, fileobj, closing_hook):
			self.fileobj = fileobj
			self.closing_hook = closing_hook
		def read(self, length = -1): return self.fileobj.read(length)
		def close(self):
			result = self.fileobj.close()
			self.closing_hook()
			return result

	temp = robust.get_tmpfile(globals.archive_dir)
	backend.get(filename, temp)
	if not encrypted: temp.open("rb")
	else: fileobj = gpg.GPGFile(0, temp, get_passphrase())
	return FileLike(fileobj, temp.delete)

def main():
	"""Start/end here"""
	args = parse_cmdline_options(sys.argv[1:])
	action, other_args = get_action(args)
	dup_time.setcurtime(globals.current_time)

	if action == "restore":
		if len(other_args) == 2:
			backup_url, target_pathname = other_args
			restrict_pathname = None
		else: backup_url,  restrict_pathname, target_pathname = other_args
		backend = backends.get_backend(backup_url)
		restore(backend, restrict_pathname, target_pathname)
	else:
		# Full or incremental backup
		archive_name, directory, dest_url = other_args
		archive_dir = set_archive_dir(archive_name)
		backend = backends.get_backend(dest_url)

		col_stats = collections.CollectionsStatus(backend, archive_dir)
		col_stats.set_values()
		sel = get_selection(directory)

		if action == "inc":
			incremental_backup(col_stats, sel, backend)
		elif action == "full":
			full_backup(sel, backend)
			if col_stats.matched_chain_pair:
				col_stats.matched_chain_pair[0].delete()
		else: command_line_error("Bad action " + action)

		
if __name__ == "__main__": main()
