#！/usr/bin/env python3
import hashlib
import os
import time
import zipfile
import logging
import fileinput
from logging.handlers import RotatingFileHandler
from datetime import datetime

hash_list1 = []  # 存放前一次文件夹内文件的哈希值
hash_list2 = []  # 存放这一次文件夹内文件的哈希值
wait_time = 30 * 60  # 等待时间，单位是s
desdir_path = "/home/luoshi/bin/controller/projects"  # 目标文件夹路径
backdir_path = "/home/luoshi/bin/controller/backup/backup_projects"  # 备份文件夹路径
min_project_size = 4 * 1024 / 10  #如果工程文件夹大小小于这个最小值，则不用备份。将kb转换成字节。经查看一个空工程的大小是4K
max_project_size = 1 * 1024 * 1024 * 1024  # 如果要备份的工程大于这个最大值，则把原来的备份文件清空再备份此工程
max_backup_size = 1 * 1024 * 1024 * 1024  # 如果备份文件夹大小大于1G，则用新的覆盖旧的
max_log_size = 50 * 1024 * 1024  # 如果日志文件大小大于50M，则删掉最早的一条

log_path = os.path.join('../', 'log')  # log_path为存放日志的路径
if not os.path.exists(log_path): os.mkdir(log_path)  # 若不存在log文件夹，则自动创建

default_formats = {
    # 终端输出格式
    'color_format': '%(log_color)s%(asctime)s-%(name)s-%(filename)s-[line:%(lineno)d]-%(levelname)s-[日志信息]: %(message)s',
    # 日志输出格式
    'log_format': '%(asctime)s-%(name)s-%(filename)s-[line:%(lineno)d]-%(levelname)s-[日志信息]: %(message)s'
}

class HandleLog:
    """
    先创建日志记录器（logging.getLogger），然后再设置日志级别（logger.setLevel），
    接着再创建日志文件，也就是日志保存的地方（logging.FileHandler），然后再设置日志格式（logging.Formatter），
    最后再将日志处理程序记录到记录器（addHandler）
    """

    def __init__(self):
        self.del_extra_files()
        self.__now_time = datetime.now().strftime('%Y-%m-%d')  # 当前日期格式化
        self.__all_log_path = os.path.join(log_path, self.__now_time + "-all" + ".log")  # 收集所有日志信息文件
        self.__error_log_path = os.path.join(log_path, self.__now_time + "-error" + ".log")  # 收集错误日志信息文件
        self.__logger = logging.getLogger()  # 创建日志记录器
        self.__logger.setLevel(logging.DEBUG)  # 设置默认日志记录器记录级别

    def del_extra_files(self):
        """
        RotatingFileHandler有个问题就是隔天重启程序则不会继续写原来的文件而是新开一个文件，时间长了文件就会越来越多，所以这里控制一下
        如果文件数量大于2，则删除时间上最早的。这样如果同一天重启则继续，隔天重启则最多会有四个文件
        """
        e_files = os.listdir(log_path)
        print("e_files:", e_files)
        while len(e_files) > 2:
            change_time = []
            file_name = []
            for file in e_files:
                filename = log_path + '/' + file
                file_name.append(filename)
                change_time.append(os.path.getmtime(filename))  # 获取文件的修改时间
                dit = {}
                dit = dict(zip(change_time, file_name))
                dit = sorted(dit.items(), key=lambda d: d[0], reverse=False)
            print(dit)
            os.remove(dit[0][1])
            e_files = os.listdir(log_path)

    @staticmethod
    def __init_logger_handler(log_path):
        """
        创建日志记录器handler，用于收集日志
        :param log_path: 日志文件路径
        :return: 日志记录器
        """
        # 写入文件，如果文件超过1M大小时，切割日志文件，仅保留3个文件
        logger_handler = RotatingFileHandler(filename=log_path, maxBytes=max_log_size, backupCount=1, encoding='utf-8')
        return logger_handler

    @staticmethod
    def __init_console_handle():
        """创建终端日志记录器handler，用于输出到控制台"""
        #console_handle = colorlog.StreamHandler()
        #return console_handle

    def __set_log_handler(self, logger_handler, level=logging.DEBUG):
        """
        设置handler级别并添加到logger收集器
        :param logger_handler: 日志记录器
        :param level: 日志记录器级别
        """
        logger_handler.setLevel(level=level)
        self.__logger.addHandler(logger_handler)

    @staticmethod
    def __set_log_formatter(file_handler):
        """
        设置日志输出格式-日志文件
        :param file_handler: 日志记录器
        """
        formatter = logging.Formatter(default_formats["log_format"], datefmt='%a, %d %b %Y %H:%M:%S')
        file_handler.setFormatter(formatter)

    @staticmethod
    def __close_handler(file_handler):
        """
        关闭handler
        :param file_handler: 日志记录器
        """
        file_handler.close()

    def __console(self, level, message):
        """构造日志收集器"""
        all_logger_handler = self.__init_logger_handler(self.__all_log_path)  # 创建日志文件
        # error_logger_handler = self.__init_logger_handler(self.__error_log_path)

        self.__set_log_formatter(all_logger_handler)  # 设置日志格式
        # self.__set_log_formatter(error_logger_handler)

        self.__set_log_handler(all_logger_handler)  # 设置handler级别并添加到logger收集器
        # self.__set_log_handler(error_logger_handler, level=logging.ERROR)

        if level == 'info':
            self.__logger.info(message)
        elif level == 'debug':
            self.__logger.debug(message)
        elif level == 'warning':
            self.__logger.warning(message)
        elif level == 'error':
            self.__logger.error(message)
        elif level == 'critical':
            self.__logger.critical(message)

        self.__logger.removeHandler(all_logger_handler)  # 避免日志输出重复问题
        # self.__logger.removeHandler(error_logger_handler)
        # self.__logger.removeHandler(console_handle)

        self.__close_handler(all_logger_handler)  # 关闭handler
        # self.__close_handler(error_logger_handler)

    def debug(self, message):
        self.__console('debug', message)

    def info(self, message):
        self.__console('info', message)

    def warning(self, message):
        self.__console('warning', message)

    def error(self, message):
        self.__console('error', message)

    def critical(self, message):
        self.__console('critical', message)

log = HandleLog()

## 计算哈希
def cal_file_sha256(filt_path):
    with open(filt_path, "rb") as f:
        file_hash = hashlib.sha256()
        chunk = f.read(1024 * 1024)
        while chunk:
            # chunk = f.read(1024 * 1024)
            file_hash.update(chunk)
            chunk = f.read(1024 * 1024)
    return file_hash.hexdigest()

def cal_folder_hash(folder, hash_list):
    #tmp_hash_list = []
    if not os.path.exists(folder):
        print("Folder doesn't exist %s" % folder)
        return
    for file in os.listdir(folder):
        path = os.path.join(folder, file)
        if os.path.isdir(path):
            cal_folder_hash(path, hash_list)  # 递归查找文件
        else:
            #print("File: %s" % path)
            sha256 = cal_file_sha256(path)
            #print("SHA256: %s\n" % sha256)
            hash_list.append(sha256)
    #return tmp_hash_list

## 压缩文件
def getZipDir(desdir_path, outFullName):
    """
    压缩指定文件夹
    :param desdir_path: 目标文件夹路径
    :param outFullName: 压缩文件保存路径+xxxx.zip
    :return: 无
    """
    zip = zipfile.ZipFile(outFullName, "w", zipfile.ZIP_DEFLATED)
    for path, dirnames, filenames in os.walk(desdir_path):
        # 去掉目标跟路径，只对目标文件夹下边的文件及文件夹进行压缩
        fpath = path.replace(desdir_path, '')
        if not dirnames and not filenames:
            zip.write(path, fpath)
        for filename in filenames:
            zip.write(os.path.join(path, filename), os.path.join(fpath, filename))
    zip.close()

## 获取文件夹的大小
def getDirSize(path):
    dirSize = 0  # 文件夹大小  单位：字节
    # 文件夹结尾判断有没有'/'
    if path[-1].__eq__('/'):
        pass
    else:
        path = path + '/'
    fileList = os.listdir(path)  # 获得文件夹下面的所有内容
    for i in fileList:
        if os.path.isdir(path + i):  # 如果是文件夹  那就再次调用函数去递归
            dirSize = dirSize + getDirSize(path + i)  # 调用自己
        else:
            size = os.path.getsize(path + i)  # 获取文件的大小
            #print(size)
            dirSize = dirSize + size  # 叠加
#            print(dirSize)
#    print(dirSize)
    return dirSize

## 清空文件夹
def cleardir(dir):
    if not os.path.exists(dir):
        return False
    if os.path.isfile(dir):
        os.remove(dir)
        return
    for i in os.listdir(dir):
        t = os.path.join(dir, i)
        if os.path.isdir(t):
            cleardir(t)#重新调用次方法
        else:
            os.unlink(t)

if __name__ == "__main__":
    ## 先创建需要的文件夹
    mkpath = "/home/luoshi/bin/controller/backup"
    isExists = os.path.exists(mkpath)
    if not isExists:
        os.makedirs(mkpath)

    mkpath1 = "/home/luoshi/bin/controller/backup/backup_projects"
    isExists1 = os.path.exists(mkpath1)    
    if not isExists1:
        os.makedirs(mkpath1)

    mkpath2 = "/home/luoshi/bin/controller/backup/log"
    isExists2 = os.path.exists(mkpath2)
    if not isExists2:
        os.makedirs(mkpath2)

    ## 开机先备份一次
    project_size = getDirSize(desdir_path)
    if project_size > min_project_size:  # 工程文件夹大小大于极小值，才备份
        # 按命名规则压缩并放到备份文件夹下
        now = datetime.now()
        outFullName = backdir_path + "/projects_" + str(now.year) + str(now.strftime('%m')) + str(
            now.strftime('%d')) + "_" + str(now.strftime('%H')) + str(now.strftime('%M')) + ".zip";
        outProName = "projects_" + str(now.year) + str(now.strftime('%m')) + str(now.strftime('%d')) + "_" + str(
            now.strftime('%H')) + str(now.strftime('%M')) + ".zip";
        # 判断备份文件大小是否超过1G及project文件夹是否小于1kb以及文件数量是否超过5个
        backup_size = getDirSize(backdir_path)
        count = 0
        for root, dirs, files in os.walk(backdir_path):  # 遍历统计文件夹下文件的个数
            for each in files:
                count += 1  # 统计文件夹下文件个数

        ## 如果当前要备份的工程大小超过1G，则先清空再备份此工程
        if project_size > max_project_size:
            cleardir(backdir_path)
            count = 0
            log.warning("要备份的工程太大，将清空备份文件夹后再备份" + outProName)
        # 按命名规则压缩并放到备份文件夹下
        getZipDir(desdir_path, outFullName);
        if count != 0:
            fileList = os.listdir(backdir_path)
            fileList = sorted(fileList, key=lambda x: os.path.getmtime(os.path.join(backdir_path, x)))
            if (cal_file_sha256(backdir_path + "/" + fileList[count]) == cal_file_sha256(backdir_path + "/" + fileList[count - 1])):
                os.remove(backdir_path + "/" + fileList[count])  # 先按修改时间排序，再删掉最新的
                log.info("工程文件自上次关机前最后一次备份后无修改，此次开机不备份工程")
            else:
                if backup_size > max_backup_size or count >= 5:
                    ## 如果备份文件大小超过1G或者数量达到5个，则删掉最早的那个
                    fileList = os.listdir(backdir_path)
                    fileList = sorted(fileList, key=lambda x: os.path.getmtime(os.path.join(backdir_path, x)))
                    os.remove(backdir_path + "/" + fileList[0])  # 先按修改时间排序，再删掉第一个
                    log.warning("备份工程太大或数目太多，将用新文件" + outProName + "覆盖旧文件" + fileList[0])
                log.info("开机备份工程，文件名为" + outProName)
        else:
            log.info("开机备份工程，文件名为" + outProName)
    else:
        # 文件夹大小小于极小值，尚未建立工程或工程已经丢失，不用备份，写日志记录
        log.warning("尚未建立工程或者工程可能已经丢失，此次不备份！")
    ## 开机备份完毕

    ## 计算一次哈希并在一段时间后再计算一次
    cal_folder_hash(desdir_path, hash_list1)
    #print(hash_list1)
    time.sleep(wait_time)
    cal_folder_hash(desdir_path, hash_list2)
    while 1:
        if hash_list1 != hash_list2:
            # 备份
            project_size = getDirSize(desdir_path)
            #print("工程文件夹大小：",project_size)
            if project_size > min_project_size:  # 工程文件夹大小大于极小值，才备份
                # 按命名规则压缩并放到备份文件夹下
                now = datetime.now()
                outFullName = backdir_path + "/projects_" + str(now.year) + str(now.strftime('%m')) + str(now.strftime('%d')) + "_" + str(now.strftime('%H')) + str(now.strftime('%M')) + ".zip";
                outProName = "projects_" + str(now.year) + str(now.strftime('%m')) + str(now.strftime('%d')) + "_" + str(now.strftime('%H')) + str(now.strftime('%M')) + ".zip";
                # 判断备份文件大小是否超过1G及project文件夹是否小于1kb以及文件数量是否超过5个
                backup_size = getDirSize(backdir_path)
                count = 0
                for root, dirs, files in os.walk(backdir_path):  # 遍历统计文件夹下文件的个数
                    for each in files:
                        count += 1  # 统计文件夹下文件个数
                if (backup_size > max_backup_size or count >= 5) and project_size < max_project_size :
                    ## 如果备份文件大小超过1G或者数量达到5个，则删掉最早的那个
                    fileList = os.listdir(backdir_path)
                    fileList = sorted(fileList, key=lambda x: os.path.getmtime(os.path.join(backdir_path, x)))
                    os.remove(backdir_path + "/" + fileList[0])   # 先按修改时间排序，再删掉第一个
                    log.warning("备份工程太大或数目太多，将用新文件" + outProName + "覆盖旧文件" + fileList[0])

                ## 如果当前要备份的工程大小超过1G，则先清空再备份此工程
                elif project_size > max_project_size:
                    cleardir(backdir_path)
                    log.warning("要备份的工程太大，将清空备份文件夹后再备份" + outProName)

                # 按命名规则压缩并放到备份文件夹下
                getZipDir(desdir_path, outFullName)
                log.info("成功备份工程，文件名为" + outProName)
            else:
                # 文件夹大小小于极小值，认为工程已经丢失，不用备份，写日志记录
                log.warning("工程可能已经丢失，此次不备份！")
            # 备份完毕，重新计算哈希值
            hash_list1 = []  # 每次重新计算之前清空一下保存哈希值的list
            cal_folder_hash(desdir_path, hash_list1)
            time.sleep(wait_time)
            hash_list2 = []
            cal_folder_hash(desdir_path, hash_list2)
        else:
            log.info("工程没有变化，此次不备份！")
            hash_list1 = hash_list2
            time.sleep(wait_time)
            hash_list2 = []
            cal_folder_hash(desdir_path, hash_list2)
