"""A thread-safe file operation package for python"""
import os
import shutil
import tempfile
from hashlib import md5
from contextlib import contextmanager
from threading import Lock
from filelock import FileLock
import cmdy

MULTILOCK = Lock()
TMPDIR    = os.path.join(tempfile.gettempdir(), 'fsutil.locks')
if not os.path.exists(TMPDIR): # pragma: no cover
	os.makedirs(TMPDIR)

class TargetExistsError(OSError):
	"""Raise when target exists and not able to overwrite"""

class TargetNotExistsError(OSError):
	"""Raise when target does not exist and not able to ignore"""

def _getLockFile(path):
	basename = md5(str(path).encode()).hexdigest()
	return os.path.join(TMPDIR, basename + '.lock')

@contextmanager
def lock(*files, resolve = False):
	"""Lock file context"""
	files = [os.path.realpath(path) if resolve else os.path.abspath(path) for path in files]
	locks = [FileLock(_getLockFile(path)) for path in files]
	if len(files) == 1:
		with locks[0]:
			yield locks[0]
	else:
		with MULTILOCK:
			_ = [lock.acquire() for lock in locks]
			yield locks
		_ = [lock.release() for lock in locks]

def autolock(func, *filekws, resolve = False):
	"""Regiester function for auto locking operating files"""
	def realfunc(*args, **kwargs):
		files = [path for i, path in enumerate(args) if i in filekws]
		files.extend([path for kw, path in kwargs.items() if kw in filekws])
		files = set(files or [args[0]])
		with lock(*files, resolve = resolve) as locks:
			func.lock = locks
			return func(*args, **kwargs)
	return realfunc

exists = os.path.exists # pylint: disable=invalid-name
isfile = os.path.isfile # pylint: disable=invalid-name
isdir  = os.path.isdir  # pylint: disable=invalid-name
islink = os.path.islink # pylint: disable=invalid-name

# haven't figured out a way to mimic this
def _removeBusyDir(path): # pragma: no cover
	"""Try to remove directory with files being ocupied by open process"""
	for root, _, files in os.walk(path):
		for fname in files:
			if not fname.startswith('.'):
				continue
			fpath = os.path.join(root, fname)
			lsof = cmdy.lsof(fpath, _raise = False)
			if lsof.rc != 0:
				continue
			lsof = lsof.splitlines()
			if len(lsof) < 2:
				continue
			pid = lsof[-1].split()[1]
			cmdy.kill({'9': pid})
	shutil.rmtree(path)

def remove(path, ignore_nonexist = True):
	"""Remove anything. If ignore_nonexist is False and path does not exists,
	a TargetNotExistsError raises."""
	if os.path.islink(path):
		os.remove(path)
	if os.path.isdir(path):
		try:
			shutil.rmtree(path)
		except OSError as ex: # pragma: no cover
			if 'busy' not in str(ex):
				raise
			_removeBusyDir(path)
	if not exists(path):
		if not ignore_nonexist:
			raise TargetNotExistsError(path)
		return
	os.remove(path)

def move(src, dst, overwrite = True):
	"""Move src to dst. If overwrite is False and dst exists, a TargetExistsError raises"""
	if not islink(dst) and samefile(src, dst):
		remove(src)
		return
	if overwrite:
		remove(dst)
	elif exists(dst):
		raise TargetExistsError(dst)
	shutil.move(src, dst)

def copy(src, dst, overwrite = True):
	"""Copy src to dst. If overwrite is False and dst exists, a TargetExistsError raises"""
	if not islink(dst) and samefile(src, dst):
		return
	if overwrite:
		remove(dst)
	elif exists(dst):
		raise TargetExistsError(dst)
	if isdir(src):
		shutil.copytree(src, dst)
	else:
		shutil.copy2(src, dst)

def link(src, dst, overwrite = True):
	"""Symbolically link src to dst.
	If overwrite is False and dst exists, a TargetExistsError raises"""
	if not islink(dst) and samefile(src, dst):
		move(dst, src)
		link(src, dst)
		return
	if overwrite:
		remove(dst)
	elif exists(dst):
		raise TargetExistsError(dst)
	os.symlink(src, dst)

def samefile(path1, path2):
	"""Tell if two paths are pointing to the same file
	If both of them don't exists, they have to be the same string.
	Otherwise, they have to point to the same file
	"""
	exist1 = exists(path1)
	exist2 = exists(path2)
	if not exist1 and not exist2:
		return path1 == path2
	if not exist1 or not exist2:
		return False
	return os.path.samefile(path1, path2)

def makedirs(path, overwrite = True):
	"""Create directory. If overwrite is False and path exists, a TargetExistsError raises.
	Important: if overwrite is True and path exists, content of the directory will lost!"""
	if overwrite:
		remove(path)
	elif exists(path):
		raise TargetExistsError(path)
	os.makedirs(path)
mkdir = makedirs # pylint: disable=invalid-name

def gzip(src, dst, overwrite = True):
	"""Gzip a file or a directory (using tar zip)"""
	if overwrite:
		remove(dst)
	elif exists(dst):
		raise TargetExistsError(dst)
	if os.path.isdir(src): # tar.gz
		import tarfile
		from glob import glob
		cwd = os.getcwd()
		os.chdir(src)
		with tarfile.open(dst, 'w:gz') as tar:
			for name in glob('./*'):
				tar.add(name)
		os.chdir(cwd)
	else:
		import gzip as _gzip
		with open(src, 'rb') as srcf, _gzip.open(dst, 'wb') as dstf:
			shutil.copyfileobj(srcf, dstf)


def gunzip(src, dst, overwrite = True):
	"""Gunzip a gzip file or a tar.gz file"""
	if overwrite:
		remove(dst)
	elif exists(dst):
		raise TargetExistsError(dst)
	if str(src).endswith('.tgz') or str(src).endswith('.tar.gz'):
		import tarfile
		makedirs(dst, overwrite)
		with tarfile.open(src, 'r:gz') as tar:
			tar.extractall(dst)
	else:
		import gzip as _gzip
		with _gzip.open(src, 'rb') as srcf, open(dst, 'wb') as dstf:
			shutil.copyfileobj(srcf, dstf)

@@ -249,16 +249,50 @@
 DIR    =
+ tempfile.gettempdir()%0A#TMPDIR   =
  os.path
@@ -336,16 +336,42 @@
 locks')%0A
+## not threading-safe!!%0A# 
 if not o
@@ -412,16 +412,18 @@
 o cover%0A
+# 
 %09os.make

Use TEMPDIR instead for _fsutil to avoid creating a subdirectory non-threading-safely.