##############################################################################
#
# Copyright (c) 2001, 2002 Zope Corporation and Manuel Amador (Rudd-O)
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################
"""Very Simple directory-based ZODB storage

This storage provides for use of dbm files and pickled files as storages that
don't support versions or Undo.  This may be useful when implementing
objects like hit counters that don't need or want to participate
in undo or versions.  Or my metadata indexing project =)

Instructions: pour in a jar, and add ReiserFS to suit your taste.
"""
__version__='$Revision: 1.4 $'[11:-2]

# FIXME need a way to delete temporary files upon start.  Pack() also needs to detect unlinked files.  Since it will be slow, consider putting in an ultrapack() method.  Perhaps it can verify if each file is mentioned in the _index map
from ZODB.MappingStorage import MappingStorage
from ZODB.BaseStorage import BaseStorage
import os
import logging
import time

class DirectoryStorage(MappingStorage):

	def __init__(self, filename):
		self.debuglog = logging.getLogger("DirectoryStorage")
		MappingStorage.__init__(self, filename)
		self.basedir = filename
		if not os.path.isdir(self.basedir):
			os.mkdir(self.basedir)
		import stat
		os.chmod(self.basedir,stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR)
		self._index=DirDict(self.basedir)
		self._tindex=[]
		
		try:
			ofilename = os.path.join(self.basedir,"max-oid")
			ofile = file(ofilename,"r")
			self._oid = ofile.read(-1)
			ofile.close()
			os.unlink(ofilename)
		except:
			logging.getLogger("DirectoryStorage").info("Recovering from a previous crash")
			keys=self._index.keys()
			if keys: self._oid=max(keys)
			logging.getLogger("DirectoryStorage").info("Recovery finished")
			
	def close(self):
		BaseStorage.close(self)
		try:
			ofilename = os.path.join(self.basedir,"max-oid")
			ofile = file(ofilename,"w")
			ofile.write(self._oid)
			ofile.close()
		except:
			raise

	def _finish(self, tid, user, desc, ext):
		thetime = time.time()
		theclock = time.clock()
		oids = [ a[0] for a in self._tindex ]
		self._index.start_transaction(tid,oids)
		MappingStorage._finish(self,tid,user,desc,ext)
		self._index.finish_transaction(tid,oids)
		self.debuglog.debug("Committed %d objects in %s sec (%s cputime).",len(self._tindex),time.time() - thetime,time.clock() - theclock)
		
# this is the persistent, transactional dictionary I am using


import cPickle

class DirDict(dict):
	
	def __init__(self,directory):
		if not os.path.isdir(directory):
			e = os.error()
			e.errno = 2
			e.filename = directory
			e.strerror = "Cannot open directory %s"%repr(directory)
			raise IOError, e
		dict.__init__(self)
		self.directory = directory
		self.transdir = os.path.join(directory,"transactions")
		self.recover_from_crash()
		
	def recover_from_crash(self):
		if not os.path.exists(self.transdir):
			os.mkdir(self.transdir)
			return
		tfiles = os.listdir(self.transdir)
		if len(tfiles) == 0: return # everything is well in the land of the lounge lizards
		tfiles = [ os.path.join(self.transdir,tfile) for tfile in tfiles ]
		# recovery procedure
		for tfile in tfiles:
			logging.getLogger("DirDict").info("Rolling transaction %s back",tfile)
			f = file(tfile,"r")
			keys = cPickle.load(f)
			f.close()
			paths = [self.__map_key_to_pathname(key) for key in keys]
			for pathname in paths:
				try: os.rename(pathname+".backup",pathname)
				except: pass
			for pathname in paths:
				try: os.unlink(pathname+".tmp")
				except: pass
			os.unlink(tfile)
			logging.getLogger("DirDict").info("%s non-atomic changes successfully rolled back",len(keys))
		
	def __map_key_to_filename(self,key):
		return key.encode("hex_codec")

		dumb = [ str(ord(byte)) for byte in key ]
		lastcomponent = dumb.pop()
		rest = "-".join(dumb)
		return os.path.join(lastcomponent,rest)
	
	def __map_filename_to_key(self,filename):
		return filename.decode("hex_codec")

		key = ""
		lastitem,arr = filename.split("/")#fixme unixism
		arr = arr.split("-")
		if len(arr) != 7: raise ValueError, "Invalid filename %s"%filename
		for char in arr + [lastitem]:
			byte = chr(int(char))
			key = key + byte
		return key
	
	def __map_key_to_pathname(self,key):
		return os.path.join(self.directory,self.__map_key_to_filename(key))
	
	def __getitem__(self,key):
		assert len(key) == 8
		pathname = self.__map_key_to_pathname(key)
		try:
			f = file(pathname,"rb",0)
			data = f.read(-1)
			f.close()
			return data
		except IOError,e:
			if e.errno == 2: raise KeyError, key
			else: raise
	
	def update(self,dictionary):
		for key,val in dictionary.iteritems():
			self[key] = val

	def __setitem__(self,key,value):
		assert len(key) == 8
		pathname = self.__map_key_to_pathname(key)
		try: os.rename(pathname,pathname+".backup")
		except: pass
		f = file(pathname+".tmp","wb",0)
		f.write(value)
		f.close()
		
	def start_transaction(self,transid,keys):
		assert type(transid) is str
		assert type(keys) is list
		transid = transid.encode("hex_codec")
		tfile = os.path.join(self.transdir,transid)
		tfile = file(tfile,"w")
		cPickle.dump(keys,tfile)
		tfile.close()
		
	def finish_transaction(self,transid,keys):
		assert type(transid) is str
		transid = transid.encode("hex_codec")
		tfile = os.path.join(self.transdir,transid)
		paths = [self.__map_key_to_pathname(key) for key in keys]
		for pathname in paths: os.rename(pathname+".tmp",pathname)
		for pathname in paths:
			try: os.unlink(pathname+".backup")
			except: pass
		os.unlink(tfile)
	
	def __delitem__(self,key):
		# where the hell is this getting called?
		# should this be in the context of a transaction as well?
		# i think this is called during pack()
		# so it needs to be transactional, so deletions don't fuck up
		# the database if interrupted at some point.
		assert len(key) == 8
		pathname = self.__map_key_to_pathname(key)
		try: os.unlink(pathname)
		except OSError,e:
			if e.errno == 2: raise KeyError, key
			else: raise
	
	def keys(self):
# 		dirs = [ str (a) for a in range(256) ]
# 		files = []
		files = os.listdir(self.directory)
# 		for directory in dirs:
# 			dirfiles = os.listdir(self.directory + "/" + directory) # fixme unixism
# 			files.extend( [ directory + "/" + dirfile for dirfile in dirfiles ] )
		keys = []
		for file in files:
			if len(file) != 16: continue # we perhaps need a more stringent test? FIXME
			try: keys.append(self.__map_filename_to_key(file))
			except ValueError: pass
		return keys
	
	def __nonzero__(self): return True

