#! /usr/bin/env python

## Copyright 2001 by LivingLogic AG, Bayreuth, Germany.
## Copyright 2001 by Walter Dörwald
##
## All Rights Reserved
##
## Permission to use, copy, modify, and distribute this software and its documentation
## for any purpose and without fee is hereby granted, provided that the above copyright
## notice appears in all copies and that both that copyright notice and this permission
## notice appear in supporting documentation, and that the name of LivingLogic AG or
## the author not be used in advertising or publicity pertaining to distribution of the
## software without specific, written prior permission.
##
## LIVINGLOGIC AG AND THE AUTHOR DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
## INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
## LIVING LOGIC AG OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
## DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER
## IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
## IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""
<par>A module that provides some tools for file i/o related stuff.</par>

<par>The first is a class <pyref class="Filename"><class>Filename</class></pyref>, that
provides several methods (e.g. a method <pyref class="Filename" method="open"><method>open</method></pyref>,
that automatically creates any non exiting directories).</par>

<par>The second is a class named <pyref class="SafeFile"><class>SafeFile</class></pyref>,
that encapsulates a normal file. Nothing will be written to the file with the
specified name. All writes go to a temporary file instead. Only when the
method <pyref class="SafeFile" method="close"><method>close</method></pyref> is called, will the
temporary file be closed and the file renamed to the original name.</par>

<par>The third tool is a class <pyref class="LogFile"><class>LogFile</class></pyref>. Writing
to a log file prepends a timestamp to each line.</par>
"""

__version__ = tuple(map(int,"$Revision: 1.29 $"[11:-2].split(".")))
# $Source: /data/cvsroot/LivingLogic/Python/fileutils/fileutils.py,v $

import sys, os, os.path, time, UserString, types

try:
	import datetime
	def timestamp2dt(t):
		return datetime.datetime.utcfromtimestamp(t)
except ImportError:
	from mx import DateTime
	def timestamp2dt(t):
		return DateTime.mktime(time.gmtime(t))

from ll import url

class Filename(UserString.UserString):
	def __init__(self, name):
		"""
		<par>create a new <class>Filename</class> instance.
		<arg>name</arg> may be a <class>Filename</class>
		instance or a string.</par>
		"""
		if isinstance(name, Filename):
			UserString.UserString.__init__(self, name.data)
		else:
			if name.startswith("file:"):
				name = name[5:]
			name = os.path.expanduser(os.path.normpath(name))
			UserString.UserString.__init__(self, name)

	def clone(self):
		"""
		<par>return a (deep) copy of <self/>.</par>
		"""
		return self.__class__(self)

	def __cmp__(self, other):
		if type(other) is type(self):
			othertype = other.__class__
		else:
			othertype = type(other)
		res = cmp(self.__class__.__name__, othertype.__name__)
		if not res:
			res = cmp(self.data, other.data)
		return res

	def __hash__(self):
		return hash(self.data)

	def url(self):
		"""
		<par>return <self/> as an URL (i.e. with a leading <lit>file:</lit>.</par>
		"""
		return "file:" + self.data

	def ext(self):
		"""
		return the file extension.
		"""
		return os.path.splitext(self.data)[1]

	def withoutExt(self):
		"""
		return a copy of <self/> without an extension.
		"""
		return Filename(os.path.splitext(self.data)[0])

	def withExt(self, ext):
		"""
		return a copy of <self/> with the new extension <arg>ext</arg>.
		"""
		return Filename(os.path.splitext(self.data)[0] + ext)

	def open(self, mode="rb", buffering=-1):
		"""
		Works like the builtin function open, but is able to create
		non existing directories.
		"""
		name = self.data
		try:
			return open(name, mode, buffering)
		except IOError, ex:
			if "w" in mode:
				if ex[0] != 2: # didn't work for some other reason than a non existing directory
					raise
				(splitpath, splitname) = os.path.split(name)
				if splitpath!="": # we really have a parent
					os.makedirs(splitpath)
					return open(name, mode, buffering)
			raise # we don't have a directory to make (or open was for reading), so pass the error on

	def openread(self, headers=None, data=None):
		return url.Filename(self.data).openread(headers=headers, data=data)

	def openwrite(self):
		return url.Filename(self.data).openwrite()

	def remove(self):
		"""
		delete the file
		"""
		os.remove(self.data)

	def rename(self, other):
		"""
		rename the file. <arg>other</arg> must be a <pyref class="Filename"><class>Filename</class></pyref> instance.
		"""
		os.rename(self.data, other.data)

	def getmtime(self):
		"""
		return the time of the last modification timestamp to the file object.
		"""
		timestamp = os.path.getmtime(self.data)
		return timestamp2dt(timestamp)

	def mkdir(self):
		"""
		create a directory with the name of <self/>.
		"""
		os.makedirs(self.data)

	def mkparentdir(self):
		"""
		create the parent directory of <self/>.
		"""
		name = self.data
		(parent, dummy) = os.path.split(name)
		if parent != name:
			os.makedirs(parent)

	def files(self, withDir=0):
		"""
		return a list of files in this directory (and all subdirectories)
		"""
		files = {}
		name = self.data
		os.path.walk(name, self.__walk, (withDir, files))
		files = files.keys()
		files.sort()
		return [ Filename(file) for file in files ]

	def __walk(self, (withDir, files), dirname, filenames):
		"""
		internal method used by <pyref method="files"><method>files</method></pyref>
		"""
		try:
			del files[dirname] # don't collect directories
		except KeyError:
			pass
		l = len(self.data) + len(os.sep)
		for filename in filenames:
			filename = os.path.join(dirname, filename)
			if not withDir:
				filename = filename[l:]
			files[filename] = None

	def __div__(self, other):
		"""
		<par>contatenate two filenames. <arg>other</arg> may be a
		<pyref class="Filename"><class>Filename</class></pyref> or a string.</par>
		"""
		if isinstance(other, Filename):
			other = other.data
		return self.__class__(os.path.join(self.data, other))

	def cd(self, other):
		"""
		<par>synonym for <lit>self/other</lit>.</par>
		"""
		return self/other

	def __repr__(self):
		return "%s(%r)" % (self.__class__.__name__, self.data)

def cwd():
	"""
	<par>return the current working directory as a <pyref class="Filename"><class>Filename</class></pyref>.</par>
	"""
	return Filename(os.getcwd())

class SafeFile:
	"""
	encapsulates a normal file.
	When the file will be opened, all writes go to
	a temporary file. After all writes are finished
	the file will be renamed to its final name.
	"""

	def __init__(self, name, mode="w", buffering=0):
		self.name = Filename(name)
		self.tmpname = self.name + ("_%d.tmp" % os.getpid())
		self.mode = mode
		self.buffering = buffering
		self.file = None

	def write(self, *texts):
		self.__open()
		for text in texts:
			self.file.write(text)

	def writelines(self, *alllines):
		self.__open()
		for lines in alllines:
			self.file.writelines(lines)

	def __open(self):
		if self.file is None:
			self.file = self.tmpname.open(self.mode, self.buffering)

	def close(self):
		"""
		This method can be called by the user when all writes to the file
		are done. The file will be closed and moved to the final destination.
		"""
		if self.file is not None:
			self.file.close()
			self.tmpname.rename(self.name)
			self.file = None

class LogFile:
	"""
	A log file. All lines written to the file will be prepended with a time stamp.
	"""
	def __init__(self, name, mode="a", buffering=1, encoding="iso-8859-1"):
		"""
		<par>create a new log file (which will be opened on the first write). Arguments are:</par>
		<ulist>
		<item><arg>name</arg>: the filename (either as a string or a 
		<pyref class="Filename"><class>Filename</class></pyref> instance).</item>
		<item><arg>mode</arg>: The mode for opening the file (should be <lit>"w"</lit> or <lit>"a"</lit>)</item>
		<item><arg>buffering</arg>: the buffering for the file (<lit>0</lit> is unbuffered, <lit>1</lit> is line buffered, any other integer specifies the buffersize)</item>
		<item><arg>encoding</arg>: the encoding to use for the strings written to the file</item>
		</ulist>
		"""
		self.startTime = time.time()
		self.name = name
		self.mode = mode
		self.buffering = buffering
		self.encoding = encoding
		self.file = None

	def _formatTime(self, timestamp):
		"""
		format <arg>timestamp</arg> into a string.
		"""
		return time.strftime("%d/%b/%Y %H:%M:%S", time.localtime(timestamp))

	def __open(self):
		if self.file is None:
			self.file = Filename(self.name).open(self.mode, self.buffering)

	def write(self, *texts):
		"""
		write <arg>texts</arg> to the log file.
		"""
		now = time.time()
		pid = os.getpid()
		self.__open()
		for text in texts:
			if type(text) is types.UnicodeType:
				text = text.encode(self.encoding, "replace")
			lines = text.split("\n")
			if not len(lines[-1]):
				del lines[-1]
			lines = [ "[pid=%d][%s]=[t+%.2fsec] %s\n" % (pid, self._formatTime(now), now-self.startTime, line) for line in lines ]
			self.file.write("".join(lines))

def getFirst(names):
	"""
	<par>Return the first file or directory from the sequence <arg>names</arg> that does exist.
	<lit>None</lit> entries will be ignored. If none of the names exists <lit>None</lit> will be
	returned.</par>
	"""
	for name in names:
		if name is not None:
			if os.path.exists(name):
				return name
	return None
