# Copyright 2005 Ben Escoto
#
# This file is part of rdiff-backup.
#
# rdiff-backup is free software; you can redistribute it and/or modify
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
#
# rdiff-backup 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 rdiff-backup; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA

"""Handle long filenames

rdiff-backup sometimes wants to write filenames longer than allowed by
the destination directory.  This can happen in 3 ways:

1)  Because the destination directory has a low maximum length limit.
2)  When the source directory has a filename close to the limit, so
    that its increments would be above the limit.
3)  When quoting is enabled, so that even the mirror filenames are too
    long.

When rdiff-backup would otherwise write a file whose name is too long,
instead it either skips the operation altogether (for non-regular
files), or writes the data to a unique file in the
rdiff-backup-data/long-filename directory.  This file will have an
arbitrary basename, but if it's an increment the suffix will be the
same.  The name will be recorded in the mirror_metadata so we can find
it later.

"""

import types, errno
import log, Globals, restore, rpath, FilenameMapping, regress

long_name_dir = "long_filename_data"
rootrp = None

def get_long_rp(base = None):
	"""Return an rpath in long name directory with given base"""
	global rootrp
	if not rootrp:
		rootrp = Globals.rbdir.append(long_name_dir)
		if not rootrp.lstat(): rootrp.mkdir()
	if base: return rootrp.append(base)
	else: return rootrp


# ------------------------------------------------------------------
# These functions used mainly for backing up

# integer number of next free prefix.  Names will be created from
# integers consecutively like '1', '2', and so on.
free_name_counter = None

# Filename which holds the next available free name in it
counter_filename = "next_free"

def get_next_free():
	"""Return next free filename available in the long filename directory"""
	global free_name_counter
	def scan_next_free():
		"""Return value of free_name_counter by listing long filename dir"""
		log.Log("Setting next free from long filenames dir", 5)
		cur_high = 0
		for filename in get_long_rp().listdir():
			try: i = int(filename.split('.')[0])
			except ValueError: continue
			if i > cur_high: cur_high = i
		return cur_high + 1

	def read_next_free():
		"""Return next int free by reading the next_free file, or None"""
		rp = get_long_rp(counter_filename)
		if not rp.lstat(): return None
		return int(rp.get_data())

	def write_next_free(i):
		"""Write value i into the counter file"""
		rp = get_long_rp(counter_filename)
		if rp.lstat(): rp.delete()
		rp.write_string(str(free_name_counter))
		rp.fsync_with_dir()

	if not free_name_counter: free_name_counter = read_next_free()
	if not free_name_counter: free_name_counter = scan_next_free()
	filename = str(free_name_counter)
	rp = get_long_rp(filename)
	assert not rp.lstat(), "Unexpected file at %s found" % (rp.path,)
	free_name_counter += 1
	write_next_free(free_name_counter)
	return filename


def check_new_index(base, index, make_dirs = 0):
	"""Return new rpath with given index, or None if that is too long

	If make_dir is True, make any parent directories to assure that
	file is really too long, and not just in directories that don't exist.

	"""
	def wrap_call(func, *args):
		try: result = func(*args)
		except EnvironmentError, exc:
			if (errno.errorcode.has_key(exc[0]) and
				errno.errorcode[exc[0]] == 'ENAMETOOLONG'):
				return None
			raise
		return result

	def make_parent(rp):
		parent = rp.get_parent_rp()
		if parent.lstat(): return 1
		parent.makedirs()
		return 2

	rp = wrap_call(base.new_index, index)
	if not make_dirs or not rp or rp.lstat(): return rp

	parent_result = wrap_call(make_parent, rp)
	if not parent_result: return None
	elif parent_result == 1: return rp
	else: return wrap_call(base.new_index, index)

def get_mirror_rp(mirror_base, mirror_rorp):
	"""Get the mirror_rp for reading a regular file

	This will just be in the mirror_base, unless rorp has an alt
	mirror name specified.  Use new_rorp, unless it is None or empty,
	and mirror_rorp exists.

	"""
	if mirror_rorp.has_alt_mirror_name():
		return get_long_rp(mirror_rorp.get_alt_mirror_name())
	else:
		rp = check_new_index(mirror_base, mirror_rorp.index)
		if rp: return rp
		else: return mirror_base.new_index_empty(index)

def get_mirror_inc_rps(rorp_pair, mirror_root, inc_root = None):
	"""Get (mirror_rp, inc_rp) pair, possibly making new longname base

	To test inc_rp, pad incbase with 50 random (non-quoted) characters
	and see if that raises an error.

	"""
	if not inc_root: # make fake inc_root if not available
		inc_root = mirror_root.append_path('rdiff-backup-data/increments')

	def mir_triple_old(old_rorp):
		"""Return (mirror_rp, alt_mirror, alt_inc) from old_rorp"""
		if old_rorp.has_alt_mirror_name():
			alt_mirror = old_rorp.get_alt_mirror_name()
			return (get_long_rp(alt_mirror), alt_mirror, None)
		else:
			mirror_rp = mirror_root.new_index(old_rorp.index)
			if old_rorp.has_alt_inc_name():
				return (mirror_rp, None, old_rorp.get_alt_inc_name())
			else: return (mirror_rp, None, None)

	def mir_triple_new(new_rorp):
		"""Return (mirror_rp, alt_mirror, None) from new_rorp"""
		mirror_rp = check_new_index(mirror_root, new_rorp.index)
		if mirror_rp: return (mirror_rp, None, None)
		alt_mirror = get_next_free()
		return (get_long_rp(alt_mirror), alt_mirror, None)

	def update_rorp(new_rorp, alt_mirror, alt_inc):
		"""Update new_rorp with alternate mirror/inc information"""
		if not new_rorp or not new_rorp.lstat(): return
		if alt_mirror: new_rorp.set_alt_mirror_name(alt_mirror)
		elif alt_inc: new_rorp.set_alt_inc_name(alt_inc)

	def find_inc_pair(index, mirror_rp, alt_mirror, alt_inc):
		"""Return (alt_inc, inc_rp) pair"""
		if alt_mirror: return (None, mirror_rp)
		elif alt_inc: return (alt_inc, get_long_rp(alt_inc))
		elif not index: return (None, inc_root)

		trial_inc_index = index[:-1] + (index[-1] + ('a'*50),)
		if check_new_index(inc_root, trial_inc_index, make_dirs = 1):
			return (None, inc_root.new_index(index))
		alt_inc = get_next_free()
		return (alt_inc, get_long_rp(alt_inc))
		
	(new_rorp, old_rorp) = rorp_pair
	if old_rorp and old_rorp.lstat():
		mirror_rp, alt_mirror, alt_inc = mir_triple_old(old_rorp)
		index = old_rorp.index
	else:
		assert new_rorp and new_rorp.lstat(), (old_rorp, new_rorp)
		mirror_rp, alt_mirror, alt_inc = mir_triple_new(new_rorp)
		index = new_rorp.index

	alt_inc, inc_rp = find_inc_pair(index, mirror_rp, alt_mirror, alt_inc)
	update_rorp(new_rorp, alt_mirror, alt_inc)
	return mirror_rp, inc_rp


# ------------------------------------------------------------------
# The following section is for restoring

# This holds a dictionary {incbase: inclist}.  The keys are increment
# bases like '1' or '23', and the values are lists containing the
# associated increments.
restore_inc_cache = None

def set_restore_cache():
	"""Initialize restore_inc_cache based on long filename dir"""
	global restore_inc_cache
	restore_inc_cache = {}
	root_rf = restore.RestoreFile(get_long_rp(), get_long_rp(), [])
	for incbase_rp, inclist in root_rf.yield_inc_complexes(get_long_rp()):
		restore_inc_cache[incbase_rp.index[-1]] = inclist

def get_inclist(inc_base_name):
	if not restore_inc_cache: set_restore_cache()
	try: return restore_inc_cache[inc_base_name]
	except KeyError: return []

def update_rf(rf, rorp, mirror_root):
	"""Return new or updated restorefile based on alt name info in rorp"""
	def update_incs(rf, inc_base):
		"""Swap inclist in rf with those with base inc_base and return"""
		log.Log("Restoring with increment base %s for file %s" %
				(inc_base, rorp.get_indexpath()), 6)		
		rf.inc_rp = get_long_rp(inc_base)
		rf.inc_list = get_inclist(inc_base)
		rf.set_relevant_incs()

	def update_existing_rf(rf, rorp):
		"""Update rf based on rorp, don't make new one"""
		if rorp.has_alt_mirror_name():
			inc_name = rorp.get_alt_mirror_name()
			rf.mirror_rp = get_long_rp(mirror_name)
		elif rorp.has_alt_inc_name(): inc_name = rorp.get_alt_inc_name()
		else: inc_name = None

		if inc_name: update_incs(rf, inc_name)

	def make_new_rf(rorp, mirror_root):
		"""Make a new rf when long name info is available"""
		if rorp.has_alt_mirror_name():
			inc_name = rorp.get_alt_mirror_name()
			mirror_rp = get_long_rp(inc_name)
		else:
			mirror_rp = mirror_root.new_index(rorp.index)
			if rorp.has_alt_inc_name(): inc_name = rorp.get_alt_inc_name()
			else: return restore.RestoreFile(mirror_rp, None, [])

		rf = restore.RestoreFile(mirror_rp, None, [])
		update_incs(rf, inc_name)
		return rf
		
	if not rorp: return rf
	if rf and not rorp.has_alt_mirror_name() and not rorp.has_alt_inc_name():
		return rf # Most common case
	if rf:
		update_existing_rf(rf, rorp)
		return rf
	else: return make_new_rf(rorp, mirror_root)

def update_regressfile(rf, rorp, mirror_root):
	"""Like update_rf except return a regress file object"""
	rf = update_rf(rf, rorp, mirror_root)
	if isinstance(rf, regress.RegressFile): return rf
	return regress.RegressFile(rf.mirror_rp, rf.inc_rp, rf.inc_list)


syntax highlighted by Code2HTML, v. 0.9.1