#!/usr/bin/env python

## Amazon S3 manager
## Author: Michal Ludvig <michal@logix.cz>
##         http://www.logix.cz/michal
## License: GPL Version 2

import sys
import logging
import time

from copy import copy
from optparse import OptionParser, Option, OptionValueError, IndentedHelpFormatter
from logging import debug, info, warning, error
import elementtree.ElementTree as ET

## Our modules
from S3.S3 import *
from S3.Config import Config

def output(message):
	print message

def cmd_ls(args):
	s3 = S3(Config())
	bucket = None
	if len(args) > 0:
		isuri, bucket, object = s3.parse_s3_uri(args[0])
		if not isuri:
			bucket = args[0]
	if bucket:
		cmd_bucket_list(args)
	else:
		cmd_buckets_list_all(args)

def cmd_buckets_list_all(args):
	s3 = S3(Config())
	response = s3.list_all_buckets()
	for bucket in response["list"]:
		output("%s  %s" % (
			formatDateTime(bucket["CreationDate"]),
			s3.compose_uri(bucket["Name"]),
			))

def cmd_buckets_list_all_all(args):
	s3 = S3(Config())
	response = s3.list_all_buckets()

	for bucket in response["list"]:
		cmd_bucket_list([bucket["Name"]])
		output("")


def cmd_bucket_list(args):
	s3 = S3(Config())
	isuri, bucket, object = s3.parse_s3_uri(args[0])
	if not isuri:
		bucket = args[0]
	output("Bucket '%s':" % bucket)
	if object.endswith('*'):
		object = object[:-1]
	try:
		response = s3.bucket_list(bucket, prefix = object)
	except S3Error, e:
		if S3.codes.has_key(e.Code):
			error(S3.codes[e.Code] % bucket)
			return
		else:
			raise
	for object in response["list"]:
		size, size_coeff = formatSize(object["Size"], Config().human_readable_sizes)
		output("%s  %s%s  %s" % (
			formatDateTime(object["LastModified"]),
			str(size).rjust(8), size_coeff.ljust(1),
			s3.compose_uri(bucket, object["Key"]),
			))

def cmd_bucket_create(args):
	s3 = S3(Config())
	isuri, bucket, object = s3.parse_s3_uri(args[0])
	if not isuri:
		bucket = args[0]
	try:
		response = s3.bucket_create(bucket)
	except S3Error, e:
		if S3.codes.has_key(e.Code):
			error(S3.codes[e.Code] % bucket)
			return
		else:
			raise
	output("Bucket '%s' created" % bucket)

def cmd_bucket_delete(args):
	s3 = S3(Config())
	isuri, bucket, object = s3.parse_s3_uri(args[0])
	if not isuri:
		bucket = args[0]
	try:
		response = s3.bucket_delete(bucket)
	except S3Error, e:
		if S3.codes.has_key(e.Code):
			error(S3.codes[e.Code] % bucket)
			return
		else:
			raise
	output("Bucket '%s' removed" % bucket)

def cmd_cp(args):
	raise ParameterError("Not yet implemented")

def cmd_object_put(args):
	s3 = S3(Config())

	s3uri = args.pop()
	files = args[:]

	isuri, bucket, object = s3.parse_s3_uri(s3uri)
	if not isuri:
		raise ParameterError("Expecting S3 URI instead of '%s'" % s3uri)

	if len(files) > 1 and object != "" and not Config().force:
		error("When uploading multiple files the last argument must")
		error("be a S3 URI specifying just the bucket name")
		error("WITHOUT object name!")
		error("Alternatively use --force argument and the specified")
		error("object name will be prefixed to all stored filanames.")
		exit(1)

	for file in files:
		if len(files) > 1:
			object_final = object + os.path.basename(file)
		elif object == "":
			object_final = os.path.basename(file)
		else:
			object_final = object
		response = s3.object_put(file, bucket, object_final)
		output("File '%s' stored as %s (%d bytes)" %
			(file, s3.compose_uri(bucket, object_final, force_uri = True), response["size"]))

def cmd_object_get(args):
	s3 = S3(Config())
	s3uri = args.pop(0)
	isuri, bucket, object = s3.parse_s3_uri(s3uri)
	if not isuri or not bucket or not object:
		raise ParameterError("Expecting S3 object URI instead of '%s'" % s3uri)
	destination = len(args) > 0 and args.pop(0) or object
	if os.path.isdir(destination):
		destination += ("/" + object)
	if not Config().force and os.path.exists(destination):
		raise ParameterError("File %s already exists. Use --force to overwrite it" % destination)
	response = s3.object_get(destination, bucket, object)
	output("Object %s saved as '%s' (%d bytes)" %
		(s3uri, destination, response["size"]))

def cmd_object_del(args):
	s3 = S3(Config())
	s3uri = args.pop(0)
	isuri, bucket, object = s3.parse_s3_uri(s3uri)
	if not isuri or not bucket or not object:
		raise ParameterError("Expecting S3 object URI instead of '%s'" % s3uri)
	response = s3.object_delete(bucket, object)
	output("Object %s deleted" % s3uri)

def run_configure(config_file):
	cfg = Config()
	options = [
		("access_key", "Access Key"),
		("secret_key", "Secret Key"),
		]
	try:
		while 1:
			output("\nEnter new values or accept defaults in brackets with Enter.")
			output("Refer to user manual for detailed description of all options.\n")
			for option in options:
				prompt = option[1]
				try:
					val = getattr(cfg, option[0])
					if val not in (None, ""):
						prompt += " [%s]" % val
				except AttributeError:
					pass

				if len(option) >= 3:
					output("%s" % option[2])

				val = raw_input(prompt + ": ")
				if val != "":
					setattr(cfg, option[0], val)
			output("\nNew settings:")
			for option in options:
				output("  %s: %s" % (option[1], getattr(cfg, option[0])))
			val = raw_input("\nChange any setting? [y/N] ")
			if not val.lower().startswith("y"):
				break
		f = open(config_file, "w")
		cfg.dump_config(f)
		f.close()
		output("Configuration saved to '%s'" % config_file)

	except (EOFError, KeyboardInterrupt):
		output("\nConfiguration aborted. Changes were NOT saved.")
		return
	
	except IOError, e:
		error("Writing config file failed: %s: %s" % (config_file, e.strerror))
		exit(1)

commands = {}
commands_list = [
	{"cmd":"mb", "label":"Make bucket", "param":"s3://BUCKET", "func":cmd_bucket_create, "argc":1},
	{"cmd":"rb", "label":"Remove bucket", "param":"s3://BUCKET", "func":cmd_bucket_delete, "argc":1},
	{"cmd":"ls", "label":"List objects or buckets", "param":"[s3://BUCKET[/PREFIX]]", "func":cmd_ls, "argc":0},
	{"cmd":"la", "label":"List all object in all buckets", "param":"", "func":cmd_buckets_list_all_all, "argc":0},
	{"cmd":"cp", "label":"Copy files to / from S3 bucket", "param":"SRC DST", "func":cmd_cp, "argc":2},
	{"cmd":"put", "label":"Put file into bucket", "param":"FILE [FILE...] s3://BUCKET[/PREFIX]", "func":cmd_object_put, "argc":2},
	{"cmd":"get", "label":"Get file from bucket", "param":"s3://BUCKET/OBJECT LOCAL_FILE", "func":cmd_object_get, "argc":1},
	{"cmd":"del", "label":"Delete file from bucket", "param":"s3://BUCKET/OBJECT", "func":cmd_object_del, "argc":1},
	]

def format_commands(progname):
	help = "Commands:\n"
	for cmd in commands_list:
		help += "  %s\n      %s %s %s\n" % (cmd["label"], progname, cmd["cmd"], cmd["param"])
	return help

class OptionMimeType(Option):
	def check_mimetype(option, opt, value):
		if re.compile("^[a-z0-9]+/[a-z0-9+\.-]+$", re.IGNORECASE).match(value):
			return value
		raise OptionValueError("option %s: invalid MIME-Type format: %r" % (opt, value))

	TYPES = Option.TYPES + ("mimetype",)
	TYPE_CHECKER = copy(Option.TYPE_CHECKER)
	TYPE_CHECKER["mimetype"] = check_mimetype

class MyHelpFormatter(IndentedHelpFormatter):
	def format_epilog(self, epilog):
		if epilog:
			return "\n" + epilog + "\n"
		else:
			return ""

if __name__ == '__main__':
	if float("%d.%d" %(sys.version_info[0], sys.version_info[1])) < 2.5:
		sys.stderr.write("ERROR: Python 2.5 or higher required, sorry.\n")
		exit(1)

	## Populate "commands" from "commands_list"
	for cmd in commands_list:
		if cmd.has_key("cmd"):
			commands[cmd["cmd"]] = cmd

	default_verbosity = Config().verbosity
	optparser = OptionParser(option_class=OptionMimeType, formatter=MyHelpFormatter())
	#optparser.disable_interspersed_args()
	optparser.set_defaults(config=os.getenv("HOME")+"/.s3cfg")
	optparser.add_option("-c", "--config", dest="config", metavar="FILE", help="Config file name. Defaults to %default")
	optparser.set_defaults(verbosity = default_verbosity)
	optparser.add_option("-d", "--debug", dest="verbosity", action="store_const", const=logging.DEBUG, help="Enable debug output.")
	optparser.add_option("-v", "--verbose", dest="verbosity", action="store_const", const=logging.INFO, help="Enable verbose output.")
	optparser.add_option("-H", "--human-readable-sizes", dest="human_readable_sizes", action="store_true", help="Print sizes in human readable form.")
	optparser.add_option("-f", "--force", dest="force", action="store_true", help="Force overwrite and other dangerous operations.")
	optparser.add_option("-u", "--show-uri", dest="show_uri", action="store_true", help="Show complete S3 URI in listings.")
	optparser.add_option("-P", "--acl-public", dest="acl_public", action="store_true", help="Store objects with ACL allowing read by anyone.")
	optparser.add_option("-m", "--mime-type", dest="default_mime_type", type="mimetype", metavar="MIME/TYPE", help="Default MIME-type to be set for objects stored.")
	optparser.add_option("-M", "--guess-mime-type", dest="guess_mime_type", action="store_true", help="Guess MIME-type of files by their extension. Falls back to default MIME-Type as specified by --mime-type option")
	optparser.add_option(      "--dump-config", dest="dump_config", action="store_true", help="Dump current configuration after parsin config files and command line options and exit.")
	optparser.add_option(      "--configure", dest="run_configure", action="store_true", help="Invoke interactive (re)configuration tool.")

	optparser.set_usage(optparser.usage + " COMMAND [parameters]")
	optparser.set_description('S3cmd is a tool to manage objects in '+
		'Amazon S3 storage. It allows for making and removing '+
		'"buckets" and uploading, downloading and removing '+
		'"objects" from these buckets.')
	optparser.epilog = format_commands(optparser.get_prog_name())
	optparser.epilog += '\nSee program homepage for more information at\nhttp://www.logix.cz/michal/devel/s3tools\n'

	(options, args) = optparser.parse_args()

	## Some mucking with logging levels to enable 
	## debugging/verbose output for config file parser on request
	logging.basicConfig(level=options.verbosity, format='%(levelname)s: %(message)s')
	
	## Now finally parse the config file
	try:
		cfg = Config(options.config)
	except IOError, e:
		if options.run_configure:
			cfg = Config()
		else:
			error("%s: %s"  % (options.config, e.strerror))
			error("Configuration file not available.")
			error("Consider using --configure parameter to create one.")
			exit(1)

	## And again some logging level adjustments
	## according to configfile and command line parameters
	if options.verbosity != default_verbosity:
		cfg.verbosity = options.verbosity
	logging.root.setLevel(cfg.verbosity)

	## Update Config with other parameters
	for option in cfg.option_list():
		try:
			if getattr(options, option) != None:
				debug("Updating %s -> %s" % (option, getattr(options, option)))
				cfg.update_option(option, getattr(options, option))
		except AttributeError:
			## Some Config() options are not settable from command line
			pass

	if options.dump_config:
		cfg.dump_config(sys.stdout)
		exit(0)

	if options.run_configure:
		run_configure(options.config)
		exit(0)

	if len(args) < 1:
		error("Missing command. Please run with --help for more information.")
		exit(1)

	command = args.pop(0)
	try:
		debug("Command: " + commands[command]["cmd"])
		## We must do this lookup in extra step to 
		## avoid catching all KeyError exceptions
		## from inner functions.
		cmd_func = commands[command]["func"]
	except KeyError, e:
		error("Invalid command: %s" % e)
		exit(1)

	if len(args) < commands[command]["argc"]:
		error("Not enough paramters for command '%s'" % command)
		exit(1)

	try:
		cmd_func(args)
	except S3Error, e:
		error("S3 error: " + str(e))
	except ParameterError, e:
		error("Parameter problem: " + str(e))