# -*- coding: utf-8 -*-

# Copyright 2008-2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
import hashlib
import select
import random
import shutil
import string

import os
from os import path
import subprocess
from subprocess import Popen, PIPE, STDOUT
import stat
from shutil import copytree, rmtree, copy2
import re
import sys
from itertools import *
import tarfile
import fcntl
from tools import SingletonParam

from contextlib import contextmanager
import signal

_ = lambda x: x
from calculate.lib.cl_lang import setLocalTranslate

setLocalTranslate('cl_lib3', sys.modules[__name__])


@contextmanager
def timeout(seconds):
    def timeout_handler(signum, frame):
        pass

    original_handler = signal.signal(signal.SIGALRM, timeout_handler)

    try:
        signal.alarm(seconds)
        yield
    finally:
        signal.alarm(0)
        signal.signal(signal.SIGALRM, original_handler)


try:
    from magic import (open as type_file, MAGIC_NONE, MAGIC_CONTINUE,
                       MAGIC_MIME_TYPE, MAGIC_COMPRESS,
                       MAGIC_MIME_ENCODING, MAGIC_SYMLINK)
except ImportError as e:
    try:
        from magic import (open as type_file, NONE as MAGIC_NONE,
                           CONTINUE as MAGIC_CONTNUE,
                           MIME_TYPE as MAGIC_MIME_TYPE,
                           COMPRESS as MAGIC_COMPRESS,
                           MIME_ENCODING as MIME_ENCODING,
                           SYMLINK as MAGIC_SYMLINK)
    except (ImportError, AttributeError):
        type_file = None
        MAGIC_NONE = 0
        MAGIC_SYMLINK = 0x002
        MAGIC_COMPRESS = 0x004
        MAGIC_CONTINUE = 0x020
        MAGIC_MIME_TYPE = 0x010
        MAGIC_MIME_ENCODING = 0x400

from calculate.lib.cl_lang import setLocalTranslate

setLocalTranslate('cl_lib3', sys.modules[__name__])

reSearch = lambda pattern, listar: map(lambda x: x.groups(),
                                       filter(None,
                                              map(pattern.search, listar)))


class FilesError(Exception):
    pass


class StdoutableProcess(object):
    def __init__(self):
        self.pipe = None

    def get_stdout(self):
        return PIPE

    def close(self):
        pass


class KeyboardInputProcess(StdoutableProcess):
    def get_stdout(self):
        return None


class PipeProcess(StdoutableProcess):
    def get_stdout(self):
        return PIPE


class process(StdoutableProcess):
    """Execute system command by Popen
    
    Examples:
    
    execute program and get result:
    if process("/bin/gzip","/boot/somefile").success():
        print "Gzip success"

    unzip and process unzip data by cpio (list files):
    processGzip = process("/bin/gzip","-dc","/boot/initrd")
    processCpio = process("/bin/cpio","-tf",stdin=processGzip)
    filelist = processCpio.readlines()

    execute command and send data:
    processGrub = process("/sbin/grub")
    processGrub.write("root (hd0,0)\n")
    processGrub.write("setup (hd0)\n")
    processGrub.write("quit\n")
    isok = processGrub.success()

    union stdout and stderr:
    process("/bin/ls","/",stderr=STDOUT)
    
    result to stdout:
    process("/bin/ls",stdout=None)

    get data from keyboard:
    process("/bin/cat",stdin=None)
    """
    STDOUT = STDOUT
    PIPE = PIPE

    def __init__(self, command, *params, **kwarg):
        super(process, self).__init__()
        if "stdin" not in kwarg:
            stdin = PipeProcess()
        else:
            if kwarg["stdin"] is None:
                stdin = KeyboardInputProcess()
            else:
                stdin = kwarg["stdin"]
        self.stdout = kwarg.get("stdout", PIPE)
        self.envdict = kwarg.get("envdict", os.environ.copy())
        self.envdict["LANG"] = kwarg.get('lang', 'C')
        self.langc = "langc" in kwarg

        self.stderr = kwarg.get("stderr", PIPE)
        self.cwd = kwarg.get("cwd", None)
        self.command = getProgPath(command)
        if not self.command:
            raise FilesError(_("Command not found '%s'") %
                             command)
        self.command = [self.command] + list(params)
        self.stdin = stdin
        self.iter = None
        self.cacheresult = None
        self.cacheerr = None
        self.communicated = False
        self._cachedata = []

    def _open(self):
        """Open pipe if it not open"""
        if not self.pipe:
            self.pipe = Popen(self.command,
                              stdout=self.stdout,
                              stdin=self.stdin.get_stdout(),
                              stderr=self.stderr,
                              cwd=self.cwd,
                              close_fds=True,
                              env=self.envdict)

    def get_stdout(self):
        """Get current stdout"""
        self.close()
        self._open()
        self.cacheresult = ""
        return self.pipe.stdout

    def write(self, data):
        """Write to process stdin"""
        self._open()
        self.pipe.stdin.write(data)
        self.pipe.stdin.flush()

    def close(self):
        """Close stdin"""
        if self.pipe:
            self.pipe.poll()
            if self.pipe.stdin:
                self.pipe.stdin.close()
        if self.stdin:
            self.stdin.close()

    def readerr(self):
        self.read()
        if self.cacheerr is None:
            try:
                self.cacheerr = self.pipe.stderr.read()
            except IOError:
                self.cacheerr = ""
        return self.cacheerr

    def readByLine(self):
        _cacheerr = []
        try:
            self._open()
            if self.cacheresult is None:
                _stdout = self.pipe.stdout.fileno()
                _stderr = self.pipe.stderr.fileno()
                reads = [_stdout, _stderr]
                while True:
                    ret = select.select(reads, [], [])
                    for fd in ret[0]:
                        if fd == _stdout:
                            s = self.pipe.stdout.readline()
                            yield s
                            self._cachedata.append(s)
                        if fd == _stderr:
                            s = self.pipe.stderr.readline()
                            _cacheerr.append(s)
                    if self.pipe.poll() is not None:
                        s = " "
                        while True:
                            s = self.pipe.stdout.readline()
                            if not s:
                                break
                            yield s
                            self._cachedata.append(s)
                        while True:
                            s = self.pipe.stderr.readline()
                            if not s:
                                break
                            yield s
                            _cacheerr.append(s)
                        break
        except KeyboardInterrupt:
            self.kill()
            raise KeyboardInterrupt
        finally:
            if self.cacheresult is None:
                self.cacheresult = "\n".join(self._cachedata)
            self.cacheerr = "".join(_cacheerr)
            self.close()

    def read(self):
        """Read all data"""
        try:
            self._open()
            if self.cacheresult is None:
                self.cacheresult, self.cacheerr = self.pipe.communicate()
            return self.cacheresult
        except KeyboardInterrupt:
            self.kill()
            raise KeyboardInterrupt
        finally:
            self.close()

    def readlines(self):
        """Read lines"""
        return self.read().split('\n')

    def __iter__(self):
        """Get iterator"""
        if not self.iter:
            self.iter = iter(self.readlines())
        return self.iter

    def kill(self):
        """Kill this process"""
        if self.pipe:
            self.pipe.kill()

    def next(self):
        """Next string from stdout"""
        return self.__iter__().next()

    def returncode(self):
        """Get return code"""
        self.read()
        return self.pipe.returncode

    def success(self):
        """Success or not"""
        return self.returncode() == 0

    def failed(self):
        """Failed or not"""
        return self.returncode() != 0


def getModeFile(nameFile, mode="all"):
    """Выдает информацию о файле
    mode=="all"
        права файла, владелец, группа файла
    mode=="mode"
        права файла
    mode=="owner"
        владелец, группа файла
    """
    fst = os.lstat(nameFile)
    if mode == "all":
        return stat.S_IMODE(fst.st_mode), fst.st_uid, fst.st_gid
    if mode == "mode":
        return stat.S_IMODE(fst.st_mode)
    if mode == "owner":
        return fst.st_uid, fst.st_gid


def chown(fn, uid=None, gid=None):
    """
    Поменять uid и gid файла если текущие отличаются
    :param fn: файл
    :param uid: UID или None
    :param gid: GID или None
    """
    f_stat = os.stat(fn)
    if uid is None:
        uid = f_stat.st_uid
    if gid is None:
        gid = f_stat.st_gid
    if uid != f_stat.st_uid or gid != f_stat.st_gid:
        os.lchown(fn, uid, gid)


class FilePermission(object):
    """
    Права на файл
    """
    UserWrite = 0200
    UserRead = 0400
    UserExecute = 0100
    UserAll = UserWrite | UserRead | UserExecute

    GroupWrite = 020
    GroupRead = 040
    GroupExecute = 010
    GroupAll = GroupWrite | GroupRead | GroupExecute

    OtherWrite = 02
    OtherRead = 04
    OtherExecute = 01
    OtherAll = OtherWrite | OtherRead | OtherExecute

    SetUid = 04000
    SetGid = 02000
    StickyBit = 01000


def chmod(fn, mode):
    """
    Поменять права на файл если текущие права отличаются
    :param fn: файл
    :param mode: права (0755)
    """
    f_stat = os.stat(fn)
    if f_stat.st_mode != mode:
        os.chmod(fn, mode)


def chownR(directory, uid, gid):
    """Recusive chown"""

    def chownPaths(rootPath, listPath, uid, gid):
        for chPath in listPath:
            chownPath = path.join(rootPath, chPath)
            statInfo = os.lstat(chownPath)[stat.ST_MODE]
            if stat.S_ISLNK(statInfo):
                os.lchown(chownPath, uid, gid)
            else:
                os.chown(chownPath, uid, gid)

    for root, dirs, files in os.walk(directory):
        # меняем владельца директории
        os.chown(root, uid, gid)
        # Меняем владельца директорий
        chownPaths(root, dirs, uid, gid)
        # Меняем владельца файлов
        chownPaths(root, files, uid, gid)
    return True


class proxy_type_file:
    def __init__(self, flags):
        self.flags = flags

    def load(self):
        pass

    def setflags(self, flags):
        self.flags = flags

    def close(self):
        pass

    def _get_cmd_by_flags(self, flags):
        """
        Получить команду для выполнения по флагам
        """
        paramdict = {MAGIC_CONTINUE: "-k",
                     MAGIC_SYMLINK: "-L",
                     MAGIC_MIME_TYPE: "--mime-type",
                     MAGIC_MIME_ENCODING: "--mime-encoding",
                     MAGIC_COMPRESS: "-z"}
        appendParam = map(lambda x: paramdict[x],
                          filter(lambda x: flags & x,
                                 sorted(paramdict.keys())))
        fileCmd = getProgPath('/usr/bin/file')
        return [fileCmd, '-b'] + appendParam

    def file(self, filename):
        if path.exists(filename):
            processFile = process(
                *(self._get_cmd_by_flags(self.flags) + [filename]))
            if processFile.success():
                return processFile.read().rstrip()
        return None


class typeFile(object):
    """Получение типа файла"""

    def __init__(self, magic=MAGIC_MIME_ENCODING | MAGIC_MIME_TYPE):
        if type_file is None:
            self.magicObject = proxy_type_file(MAGIC_NONE)
        else:
            self.magicObject = type_file(MAGIC_NONE)
        self.magicObject.load()
        self.magicObject.setflags(magic)

    def __del__(self):
        """Закрываем magic"""
        self.magicObject.close()

    def getMType(self, filename):
        """Информация о типе файла"""
        try:
            ret = self.magicObject.file(filename)
        except UnicodeDecodeError:
            try:
                ret = self.magicObject.file(filename.decode('utf-8'))
            except UnicodeDecodeError:
                return None
        # fix for kernel 3.7.7 (bad work samba)
        if ret is None and self.magicObject.errno() == 5:
            r, w = os.pipe()
            devnull = os.open(os.devnull, os.O_WRONLY)
            cat = subprocess.Popen(['/bin/cat', filename], stdout=w,
                                   stderr=devnull, close_fds=True)
            ret = self.magicObject.descriptor(r)
            os.close(w)
            os.close(devnull)
            cat.wait()
        return ret

    def isBinary(self, filename):
        """является ли файл бинарным"""
        mime = self.getMType(filename)
        if mime:
            if mime.startswith("text"):
                return False
            else:
                return True
        return None


class processProgress(process):
    """
    Generator like process progress
    """

    def __init__(self, command, *params, **kwarg):
        process.__init__(self, command, *params, **kwarg)
        self.readsize = kwarg.get("readsize", 10)
        self.delimeter = re.compile("\n")
        self.init(**kwarg)
        self._cachedata = []
        self.buf = ""

    def init(self, *args, **kwarg):
        pass

    def processInit(self):
        pass

    def processEnd(self):
        pass

    def processString(self, strdata):
        self._cachedata.append(strdata)
        return strdata

    def processBuffer(self, buf):
        return ""

    def progress(self):
        try:
            res = self.processInit()
            if res is not None:
                yield res
            self._open()
            if self.cacheresult is None:
                self.buf = ""
                fd = self.pipe.stdout.fileno()
                fl = fcntl.fcntl(fd, fcntl.F_GETFL)
                fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
                hasData = True
                while hasData or self.pipe.poll() is None:
                    hasData = False
                    try:
                        part = self.pipe.stdout.read(self.readsize)
                        if not part:
                            continue
                    except IOError as er:
                        # re-raise not "Resource temporary unavailable"
                        if er.errno != 11:
                            raise
                        continue
                    hasData = True
                    if self.buf:
                        self.buf += part
                    else:
                        self.buf = part
                    res = self.processBuffer(part)
                    if res:
                        yield res
                    while self.delimeter.search(self.buf):
                        strdata, self.buf = self.delimeter.split(self.buf, 1)
                        res = self.processString(strdata)
                        if res:
                            yield res
            res = self.processEnd()
            if res is not None:
                yield res
        except KeyboardInterrupt:
            self.pipe.kill()
            raise
        finally:
            if self._cachedata:
                self.cacheresult = "\n".join(self._cachedata)
            else:
                self.cacheresult = ""


def countFiles(dirpath, onefilesystem=True):
    """
    Count files in specified dirpath
    """
    num = 1
    for dirpath, dirnames, filenames in os.walk(dirpath):
        num += len(set(dirnames) | set(filenames))
        if onefilesystem:
            mountDirs = filter(lambda x: path.ismount(path.join(dirpath, x)),
                               dirnames)
            for dirname in mountDirs:
                dirnames.remove(dirname)
    return num


def getFilesCount(directory):
    """Alias for compatibility"""
    return countFiles(directory, onefilesystem=False)


def copyDir(srcDir, destDir):
    """Копируем директорию srcDir в destDir

    При копировании сохраняются владелец, группа, права
    """

    def ignoreFile(pathname, names):
        """Игнорирование сокетов при копировании"""
        ignore = []
        for name in names:
            if stat.S_ISSOCK(os.lstat(path.join(pathname, name))[stat.ST_MODE]):
                ignore.append(name)
        return ignore

    copytree(srcDir, destDir, ignore=ignoreFile)
    return True


def removeDir(rmDir):
    """Рекурсивное удаление директории"""
    rmtree(rmDir)
    return True


def check_rw(dn):
    """
    Проверить досупена ли указанная директория на запись
    :param dn:
    :return:
    """

    def _random_string(l=10):
        return "".join(
            random.choice(string.ascii_letters) for i in xrange(0, l))

    mark_file = None
    while mark_file is None or path.exists(mark_file):
        mark_file = path.join(dn, "_mark_%s" % _random_string())
    try:
        open(mark_file, 'w')
        os.unlink(mark_file)
        return True
    except (OSError, IOError):
        return False


def removeFileWithEmptyDirectory(rmName, stopDirectory="/"):
    """
    Удалить файл и все пустые родительские директории
    :param rmName: путь до файла или директории
    """
    if rmName != stopDirectory and path.exists(rmName):
        if path.isdir(rmName):
            if not listDirectory(rmName, fullPath=True):
                os.rmdir(rmName)
                removeFileWithEmptyDirectory(path.dirname(rmName),
                                             stopDirectory=stopDirectory)
        else:
            os.unlink(rmName)
            removeFileWithEmptyDirectory(path.dirname(rmName),
                                         stopDirectory=stopDirectory)


def getRunCommands(not_chroot=False, chroot=None, uid=None):
    """List run program"""

    def getCmd(procNum):
        cmdLineFile = '/proc/%s/cmdline' % procNum
        try:
            if uid is not None:
                fstat = os.stat("/proc/%s"%procNum)
                if fstat.st_uid != uid:
                    return ""
            if path.exists(cmdLineFile):
                if not_chroot:
                    root_link = '/proc/%s/root' % procNum
                    if os.readlink(root_link) != "/":
                        return ""
                if chroot is not None:
                    root_link = '/proc/%s/root' % procNum
                    if os.readlink(root_link) != chroot:
                        return ""
                return open(cmdLineFile, 'r').read().strip()
        except Exception:
            pass
        return ""

    if not os.access('/proc', os.R_OK):
        return []
    return filter(None,
                  map(getCmd,
                      filter(lambda x: x.isdigit(),
                             listDirectory('/proc'))))


def isMount(dn):
    """
    Возвращает путь примонтированного dn
    """

    def find_names(old_dn):
        dn = path.abspath(old_dn)
        yield dn
        if dn.startswith('/dev'):
            info = device.getUdevDeviceInfo(name=dn)
            if 'DM_VG_NAME' in info and 'DM_LV_NAME' in info:
                yield '/dev/mapper/{vg}-{lv}'.format(vg=info['DM_VG_NAME'],
                                                     lv=info['DM_LV_NAME'])

    def get_overlay_mounts(line):
        mounts = line.split(' ')
        yield mounts[1]
        for dn in re.findall("(?:lowerdir=|upperdir=|workdir=)([^,]+)",
                             mounts[3]):
            yield dn

    find_data = set(find_names(dn))
    ret = ""
    for line in readLinesFile('/etc/mtab'):
        if " overlay " not in line:
            if " " in line:
                mounts = set(line.split(' ')[:2])
                if mounts & find_data:
                    ret = (mounts - find_data).pop()
        else:
            mounts = set(get_overlay_mounts(line))
            dest = line.split(' ')[1]
            if mounts & find_data:
                if dest in find_data:
                    ret = "overlay"
                else:
                    return dest
    return ret


def commonPath(*paths):
    """Return common path from list of paths"""
    paths = map(lambda x: path.normpath(x).split('/'), paths)
    res = map(lambda x: x[0],
              filter(lambda x: filter(lambda y: x[0] == y, x[1:]), zip(*paths)))
    return "/".join(res)


def childMounts(pathname):
    """Get all mount points which contain path"""
    if pathname != "none":
        absPath = path.abspath(pathname)
    else:
        absPath = pathname
    mtabFile = '/etc/mtab'
    if not os.access(mtabFile, os.R_OK):
        return ""
    return reduce(lambda x, y: x + [y],
                  filter(lambda x: commonPath(absPath, x[0]) == absPath or \
                                   commonPath(absPath, x[1]) == absPath,
                         map(lambda x: [x[0], x[1]],
                             map(lambda x: x.split(" "),
                                 open(mtabFile)))),
                  [])


def pathJoin(*paths):
    """Складывает пути, в отличии от os.path.join, складывает абсолютные пути"""
    if len(paths) == 1:
        return paths[0]
    return reduce(path.join,
                  filter(lambda x: x and x != "/",
                         map(lambda x: x.startswith("/") and x[1:] or x,
                             paths[1:])), paths[0])


def checkDigestFile(digestfile):
    """Check digest by digestfile"""
    reEntry = re.compile(r"# (\S+) HASH\n(\S+)  (\S+)", re.S)
    result = []
    for alg, hashdata, filename in reEntry.findall(
            open(digestfile, 'r').read()):
        if hasattr(hashlib, alg.lower()):
            hashobj = getattr(hashlib, alg.lower())
            filename = path.join(path.dirname(digestfile), filename)
            if os.path.exists(filename):
                digest = hashobj(open(filename, 'r').read())
                result.append((alg,
                               digest.hexdigest().upper() == hashdata.upper()))
    return result


def listDirectory(directory, fullPath=False, onlyDir=False):
    """Get files from directory, if it exists"""
    if not path.exists(directory):
        return []
    try:
        if fullPath:
            if onlyDir:
                return filter(lambda x: path.isdir(x),
                              map(lambda x: path.join(directory, x),
                                  os.listdir(directory)))
            else:
                return map(lambda x: path.join(directory, x),
                           os.listdir(directory))
        else:
            if onlyDir:
                return filter(lambda x: path.isdir(path.join(directory, x)),
                              os.listdir(directory))
            else:
                return os.listdir(directory)
    except OSError:
        pass
    return []


def makeDirectory(pathname):
    """Make directory and parent.

    Return False on error"""
    try:
        parent = path.split(path.normpath(pathname))[0]
        if not path.exists(parent):
            makeDirectory(parent)
        else:
            if path.exists(pathname):
                return True
        os.mkdir(pathname)
        return True
    except (OSError, IOError):
        return False


def readLinesFile(filename):
    """Read file by line"""
    try:
        if path.exists(filename):
            for line in open(filename, 'r'):
                yield line.rstrip('\n')
    except (OSError, IOError):
        pass
    finally:
        raise StopIteration


def readFile(filename, tailbyte=None, headbyte=None):
    """
    Прочитать целый файл или вернуть пустую строку.
    tailbyte: прочитать только последнее указанное количество байт
    headbyte: прочитать только первое указанное количество байт
    """
    try:
        if path.exists(filename):
            with open(filename, 'r') as f:
                if tailbyte:
                    seeksize = max(0, os.stat(filename).st_size - tailbyte)
                    if seeksize:
                        f.seek(seeksize)
                if headbyte:
                    return f.read(headbyte)
                else:
                    return f.read()
    except (OSError, IOError):
        pass
    return ""


def writeFile(filename):
    """
    Открыть файл на запись и создать необходимые каталоги
    """
    dn = path.dirname(filename)
    if not path.exists(dn):
        os.makedirs(dn)
    return open(filename, 'w')


class GetProgPath(object):
    """Get full path of program or False"""
    cache = {}

    def __call__(self, progname, prefix="/"):
        baseprogname = path.basename(progname)
        key = (baseprogname, prefix)
        if key in self.cache:
            return self.cache[key]
        bindir = ('/bin', '/sbin', '/usr/sbin', '/usr/bin')
        if progname.startswith('/'):
            if path.exists(pathJoin(prefix, progname)):
                self.cache[key] = progname
                return progname
        for progname in (pathJoin(x, baseprogname) for x in bindir):
            if path.exists(pathJoin(prefix, progname)):
                self.cache[key] = progname
                return progname
        return False
        # env = {"LANG":"C"}
        # env.update(os.environ.items() + [("PATH",common.getpathenv())] +\
        #           env.items())
        # res = runOsCommand("which %s"%baseprogname,env_dict=env)
        # if path.isabs(progname) and path.exists(progname):
        #    self.cache[baseprogname] = progname
        #    return self.cache[baseprogname]
        # elif res[0] == 0:
        #    self.cache[baseprogname] = res[1][0].strip()
        #    return self.cache[baseprogname]
        # else:
        #    return False


getProgPath = GetProgPath()


def checkUtils(*utils):
    """Check utils, exit if it not found and return fullpath"""
    retval = []
    for util in utils:
        utilPath = getProgPath(util)
        if not utilPath:
            raise FilesError(_("Command not found '%s'") %
                             path.basename(util))
        retval.append(utilPath)
    if len(retval) == 1:
        return retval[0]
    else:
        return retval


def getCmdLineParam(paramName):
    """Get value of param /proc/cmdline. If param not found then empty.
    """
    cmdLine = '/proc/cmdline'
    paramName = "%s=" % paramName
    params = \
        map(lambda x: x.partition('=')[2],
            filter(lambda x: x.startswith(paramName),
                   readFile(cmdLine).split(' ')))
    if params:
        return params[-1]
    else:
        return ""


from device import getUUIDDict
import device


class MountHelper(object):
    """Data reader for fstab"""
    data_file = '/etc/fstab'
    NAME, DIR, TYPE, OPTS, FREQ, PASSNO = range(0, 6)

    def __init__(self, data_file=None, devs=()):
        if data_file:
            self.data_file = data_file
        self.cache = []
        self.rotateCache = []
        self.dictUUID = getUUIDDict(devs=devs)
        self.rebuildCache()

    def rebuildCache(self):
        """Rebuild cache from fstab file"""

        def setlen(ar):
            return ar[:6] + [""] * (6 - len(ar))

        self.cache = \
            map(lambda x: setlen(map(lambda y: y.strip(), x.split())),
                filter(lambda x: x.strip() and not x.lstrip().startswith("#"),
                       open(self.data_file, 'r').read().split('\n')))
        for data in self.cache:
            convertDev = lambda x: path.realpath(x) if x.startswith('/') else x
            data[0] = device.getUdevDeviceInfo(
                name=convertDev(self.dictUUID.get(data[0], data[0]))
            ).get('DEVNAME', data[0])
            data[1] = data[1] if data[2] != "swap" else "swap"
        self.rotateCache = zip(*self.cache)

    def getBy(self, what=DIR, where=NAME, _in=None, eq=None,
              contains=None, noteq=None, allentry=False):
        """Get data from fstab"""
        if eq is not None:
            filterfunc = lambda x: x[where] == eq
        elif _in is not None:
            filterfunc = lambda x: x[where] in _in
        elif contains is not None:
            filterfunc = lambda x: contains in x[where]
        else:
            filterfunc = lambda x: x[where] != noteq
        res = map(lambda x: x[what], filter(filterfunc, self.cache))
        if allentry:
            return res
        else:
            return "" if not res else res[-1]

    def getFields(self, *fields):
        """Get all data by specifie fields"""
        return zip(*reduce(lambda x, y: x + [self.rotateCache[y]], fields, []))

    def isExists(self, what=DIR, eq=None, noteq=None):
        """Field with condition exist in fstab"""
        if not eq is None:
            filterfunc = lambda x: x[what] == eq
        else:
            filterfunc = lambda x: x[what] != noteq
        return bool(filter(filterfunc, self.cache))


class FStab(MountHelper):
    """Data reader for fstab"""
    __metaclass__ = SingletonParam
    data_file = '/etc/fstab'


class Mounts(MountHelper):
    """Data reader for fstab"""
    data_file = '/etc/mtab'


def tarLinks(rootpath, archpath, skip=()):
    """
    Поместить все симлинки в архив
    """
    links = []
    lenprefix = len(path.normpath(rootpath)) + 1
    if path.exists(archpath):
        os.unlink(archpath)
    # create arch
    tar = tarfile.open(archpath, "w:bz2")
    # find links
    if skip:
        reSkip = re.compile("|".join(map(lambda x: x.replace("*", ".*"),
                                         skip))).search
    else:
        reSkip = lambda x: False
    for link in ifilterfalse(reSkip,
                             find(rootpath, filetype=FindFileType.SymbolicLink,
                                  onefilesystem=True)):
        ti = tar.gettarinfo(link)
        ti.name = link[lenprefix:]
        tar.addfile(ti)
        links.append(link)
    tar.close()
    return links


def getch():
    """
    Get char from keyboard
    """
    import sys, tty, termios

    try:
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch
    except Exception:
        return ""


class FindFileType(object):
    """
    Типы файлов для find
    """
    RegularFile = "f"
    Directory = "d"
    SymbolicLink = "l"
    Block = "b"
    Character = "c"
    NamedPipe = "p"


def find(directory, onefilesystem=False, filetype=None,
         fullpath=True, depth=None, downfilter=None):
    """
    Поиск файлов в каталоге
    :param directory: каталог в котором производится поиск
    :param onefilesystem: искать только в текущей файловой системе
    :param filetype: тип файла (FindFileType)
    :param fullpath: выдавать полный путь
    :param depth: глубина поиска
    :param downfilter: фильтр поиска
    :return: генератор списка файлов
    """
    directory = path.normpath(directory)
    ldirectory = len(directory)
    if downfilter or onefilesystem or depth:
        _downfilter = lambda dp: (
            (depth is None or dp[ldirectory:].count('/') < depth) and
            (not downfilter or downfilter(dp)) and
            (not onefilesystem or not path.ismount(dp)))
    else:
        _downfilter = None

    if filetype:
        def dirfilter(dp):
            link = path.islink(dp)
            return ((FindFileType.SymbolicLink in filetype and link)
                    or (FindFileType.Directory in filetype and not link))

        _dirfilter = dirfilter
    else:
        _dirfilter = None

    _dirfullpath = lambda root, fn: path.join(root, fn)

    if filetype:
        def filefilter(fp):
            link = path.islink(fp)
            return ((FindFileType.SymbolicLink in filetype and link)
                    or (FindFileType.RegularFile in filetype and not link))

        _filefilter = filefilter
    else:
        _filefilter = None

    _filefullpath = lambda root, fn: path.join(root, fn)

    if directory.endswith('/'):
        clearlen = len(directory)
    else:
        clearlen = len(directory) + 1
    shortpath = lambda fp: fp[clearlen:]

    for root, directories, files in os.walk(directory):
        for fn in files:
            fp = _filefullpath(root, fn)
            if not _filefilter or _filefilter(fp):
                yield fp if fullpath else shortpath(fp)
        for dn in directories[:]:
            dp = _dirfullpath(root, dn)
            if not _dirfilter or _dirfilter(dp):
                yield dp if fullpath else shortpath(dp)
            if _downfilter and not _downfilter(dp):
                directories.remove(dn)


def isEmpty(dn):
    """
    Проверить пустая ли директория
    :param dn: директория
    :return:
    """
    return not listDirectory(dn)


def copyWithPath(fn, dest, prefix=None):
    """
    Копировать файл в dst с созданием каталогов

    dest: каталог для копирования
    fn: имя файла
    prefix: префикс исходного файла

    Пример:
    copyWithPath("/etc/conf.d/net", "/tmp/conf", prefix="/etc")
    /etc/conf.d/net -> /tmp/conf/conf.d/net
    """
    if prefix:
        destFile = pathJoin(dest, fn[len(prefix):])
    else:
        destFile = pathJoin(dest, path.basename(fn))
    try:
        destDir = path.dirname(destFile)
        if not path.exists(destDir):
            os.makedirs(destDir)
        copy2(fn, destFile)
    except Exception:
        raise FilesError(
            _("Failed to copy '%(src)s' to '%(dst)s'") %
            {'src': fn,
             'dst': destFile})


def getLoopFromPath(directory):
    """
    Получить loop, использующие файлы в данной директории
    """
    losetup = getProgPath('losetup')
    p = process(losetup, '-a')
    rePattern = re.compile('(^/dev/loop[^:]+)[^(]+\(([^)]+)\).*')
    return map(lambda x: x[0],
               filter(lambda x: x[1].startswith(directory),
                      reSearch(rePattern,
                               p.readlines())))


def getMdRaidDevices():
    """
    Получить словарь: какой mdraid какие использует устройства
    """
    reMdInfo = re.compile('^(\w+)\s+:\s+active.*?raid\d+\s+(.*)')
    return dict(map(lambda x: (x[0], map(lambda x: x.partition('[')[0],
                                         x[1].split())),
                    reSearch(reMdInfo, readLinesFile('/proc/mdstat'))))


def sambaPasswordCheck(username, password, server, resource):
    """
    Подключиться к указанному samba ресурсу сервера по логину паролю
    """
    smbclient = getProgPath('/usr/sbin/smbclient')
    if not smbclient:
        raise FilesError(_("Command not found '%s'") % "smbclint")
    p = process(smbclient, "-U", username, "//%s/%s" % (server, resource),
                envdict={'PASSWD': password})
    return p.success()


class PercentProgress(processProgress):
    """
    Объект выдает прогресс, ища в выводе \d+%
    
    Args:
        part: количество прогрессов в программе
        delimeter: разделители строк по умолчанию \n и \r
        cachefilter: фильтр вывода программы (регулярная строка)
        startpart: используется для вывода процентов прогрессбар
                   с определенного места (0 по умолчанию)
        end:       конечный прогрессбар (по умолчанию)
        atty: для получения данных создается pty
    """

    def init(self, *args, **kwargs):
        self.rePerc = re.compile("(\d+(?:\.\d+)?)%", re.S)
        self.part = kwargs.get("part", 1)
        if self.part < 1:
            self.part = 1
        self.add_offset = 100 / self.part
        self.offset = 0 + kwargs.get("startpart", 0) * self.add_offset
        self.is_end = kwargs.get("end", True)
        self.stderr = STDOUT
        self.delimeter = re.compile("[%s]" % kwargs.get("delimeter", "\n\r"))
        self.cachedata = re.compile(kwargs.get("cachefilter",
                                               "((?:\[31;01m\*|\[33;01m\*|"
                                               "Bad Option|No space left|FATAL ERROR|SYNTAX:|mkisofs:|"
                                               "error:|warning:|fatal:).*)"))
        self.atty = kwargs.get("atty", False)
        self.alldata = ""

    def _open(self):
        self.master, self.slave = None, None
        if self.atty:
            if not self.pipe:
                self.master, self.slave = os.openpty()
                import termios, struct, fcntl

                TIOCSWINSZ = getattr(termios, 'TIOCSWINSZ', -2146929561)
                s = struct.pack('HHHH', 25, 80, 0, 0)
                fcntl.ioctl(self.slave, TIOCSWINSZ, s)
                self.pipe = Popen(self.command,
                                  stdout=self.slave,
                                  stderr=self.slave,
                                  cwd=self.cwd,
                                  close_fds=True,
                                  env=self.envdict)
                self.pipe.stdout = os.fdopen(self.master, 'r')
        else:
            process._open(self)

    def processInit(self):
        self.percent = 0
        self.showval = 0
        if not self.offset:
            return 0

    def processEnd(self):
        if not self.slave is None:
            os.close(self.slave)
        if self.is_end:
            self.percent = 100
            return 100

    def processString(self, strdata):
        self.alldata += strdata
        match = self.rePerc.search(strdata)
        resSearch = self.cachedata.search(strdata)
        if resSearch:
            self._cachedata.append(
                re.sub("\[31;01m\*|\[33;01m\*|\[.*?m",
                       "", resSearch.group(1)))
        if match:
            percent = int(float(match.group(1)))
            if percent < self.percent:
                self.offset += self.add_offset
            percent /= self.part
            if percent != self.percent:
                self.percent = percent
                showval = min(99, self.percent + self.offset)
                if showval != self.showval:
                    self.showval = showval
                    return self.showval


def set_active_tty(ttynum):
    """
    Установить активный терминал
    """
    chvt = getProgPath("/usr/bin/chvt")
    if chvt:
        os.system('chvt %d &>/dev/null' % ttynum)


def get_active_tty():
    """
    Получить активный терминал
    """
    getvt = getProgPath("/usr/bin/fgconsole")
    if getvt:
        p = process(getvt)
        if p.success():
            return p.read().strip()
    return None


class scanDirectory(object):
    """Класс для cканирования директории"""

    def processingFile(self, pathname, prefix):
        """Обработка в случае файла"""
        return True

    def processingDirectory(self, pathname, prefix):
        """Обработка в случае директории если возвращаем None то пропуск дир."""
        return True

    def scanningDirectory(self, scanDir, skipFile=(), skipDir=(),
                          prefix=None, flagDir=False):
        """Сканирование и обработка шаблонов в директории scanDir"""
        ret = True
        if not prefix:
            prefix = path.join(scanDir, "")[:-1]
        if not flagDir:
            # проверка корневой директории
            retDir = self.processingDirectory(scanDir, scanDir)
            if retDir is None:
                return None
            elif retDir is False:
                return False
        if flagDir or stat.S_ISDIR(os.lstat(scanDir)[stat.ST_MODE]):
            for absPath in sorted(listDirectory(scanDir, fullPath=True)):
                relPath = absPath.split(prefix)[1]
                stInfo = os.lstat(absPath)
                statInfo = stInfo[stat.ST_MODE]
                if stat.S_ISREG(statInfo):
                    # Обработка файла
                    if relPath in skipFile:
                        continue
                    if not self.processingFile(absPath, prefix):
                        return False
                elif stat.S_ISDIR(statInfo):
                    # Обработка директории
                    if relPath in skipDir:
                        continue
                    retDir = self.processingDirectory(absPath, prefix)
                    if retDir is None:
                        continue
                    elif retDir is False:
                        return False
                    ret = self.scanningDirectory(absPath, skipFile,
                                                 skipDir, prefix, True)
                    if ret is False:
                        return False
        return ret


class DiskSpaceError(Exception):
    pass


class DiskSpace(object):
    def __init__(self):
        self.df_cmd = getProgPath('/bin/df')

    def get_free(self, dev=None, dn=None):
        if dev:
            mp = isMount(dev)
            if not mp:
                raise DiskSpaceError(_("Device %s must be mounted") % dev)
            dn = dev
        p = process(self.df_cmd, dn, "-B1")
        if p.success():
            data = p.read().strip()
            lines = data.split('\n')
            if len(lines) >= 2:
                cols = filter(None, lines[1].split())
                if len(cols) == 6:
                    return int(cols[3])
            raise DiskSpaceError(_("Wrong df output:\n%s") % data)
        else:
            raise DiskSpaceError(str(p.readerr()))


def calculate_exclude(base_dn, exclude=(), include=()):
    """
    Вычисление исключений с использованием фильтров include
    """
    # корректируем входные данные, чтобы не учитывался первый /
    exclude = [x[1:] if x.startswith('/') else x for x in exclude]
    include = [x[1:] if x.startswith('/') else x for x in include]
    # перебираем все исключения
    for exclude_dn in exclude:
        # ищем фильтры для текущего исключения
        f_include = [x for x in include
                     if x == exclude_dn or x.startswith("%s/" % exclude_dn)]
        # если фильтра нет - то исключение добавляется полностью
        if not f_include:
            yield exclude_dn
        else:
            # пребираем все файлы в указанно исключении
            for root, dirs, files in os.walk(path.join(base_dn, exclude_dn)):
                rel_root = root[len(base_dn) + 1:]
                # если файл указан в фильтре, пропускаем его
                for fn in (path.join(rel_root, x) for x in files):
                    if fn not in f_include:
                        yield fn
                filtered_dn = []
                for dn in dirs:
                    rel_dn = path.join(rel_root, dn)
                    # если директория не используется как часть фильтра - возвращем её
                    if all(not x.startswith("%s/" % rel_dn) and x != rel_dn
                           for x in f_include):
                        filtered_dn.append(dn)
                        yield rel_dn
                    elif rel_dn in f_include:
                        filtered_dn.append(dn)

                # удаляем из обрабатываемых дирекоторий не использующиеся в фильтре
                # или указанную конечную директорию
                for dn in filtered_dn:
                    dirs.remove(dn)


def get_free_dirname(dn):
    """
    Возвращает имя указанной директорий если оно отсутствует либо добавляются
    цифры
    :param base_dn:
    :param name:
    :return:
    """
    if not path.exists(dn):
        return dn
    base_dn, name = path.split(dn)
    for n in range(0, 9999):
        dn = path.join(base_dn, "%s.%02d" % (name, n))
        if not path.exists(dn):
            return dn
    raise FilesError(_("No directory name available for %s") % dn)


def dir_sync(src, dst):
    """
    Скопировать файлы из src в dst, заменяя одинаковые файлы
    :return:
    """
    if not path.isdir(src):
        raise FilesError(_("%s is not directory") % src)
    if path.lexists(dst):
        dst_stat = os.lstat(dst)
        if stat.S_ISDIR(dst_stat.st_mode):
            for fn in listDirectory(src):
                src_fn = path.join(src, fn)
                dst_fn = path.join(dst, fn)
                fn_stat = os.lstat(src_fn)
                if stat.S_ISDIR(fn_stat.st_mode):
                    dir_sync(src_fn, dst_fn)
                    continue
                if path.lexists(dst_fn):
                    dst_stat = os.lstat(dst_fn)
                    if stat.S_ISDIR(dst_stat.st_mode):
                        shutil.rmtree(dst_fn)
                    else:
                        os.unlink(dst_fn)
                shutil.copy2(src_fn, dst_fn)
            return
        else:
            os.unlink(dst)
    shutil.copytree(src, dst)


def tar_directory(dn, archfile):
    """
    Заархивировать указанную директорию
    :param dn:
    :param archfile:
    :return:
    """
    try:
        with tarfile.open(archfile, "w:bz2") as tar:
            for x in listDirectory(dn, fullPath=True):
                tar.add(x, path.basename(x))
    except (IOError, OSError) as e:
        raise FilesError(_("Failed to create tarball: %s") % str(e))
