gen_code.py 11.4 KB
from pprint import pprint
from pathlib import Path
from difflib import Differ
import fs
import json
import os
import re
import hashlib
import time
import shutil
from jinja2 import Environment, FileSystemLoader
from resources.webcreator.log import logger

'''
流程:
一、将resources里面所有文件夹拷贝到当前目录下
二、解析json配置文件,遍历每一项生成/model/view/controller
三、自动运行项目
'''

# 比较两个文件内容是否相同,这里没有使用md5内容摘要算法,MD5算法的缺点是,针对大文件,计算耗时。虽然咱们这里不存在大文件,但是条条大路通罗马嘛,试试其他方法也挺好。
def cmp_file(file1, file2):
    f1 = os.stat(file1)
    f2 = os.stat(file2)

    if f1.st_size != f2.st_size:
        return False

    buf_size = 8 * 1024
    with open(file1, "rb") as fp1, open(file2, "rb") as fp2:
        while True:
            buf1 = fp1.read(buf_size)
            buf2 = fp2.read(buf_size)

            if buf1 != buf2:
                return False

            # 这里表示文件读完了
            if not buf1 or not buf2:
                return True


def compare_file(file1, file2):
    with open(file1) as f1,open(file2) as f2:
        content1 = f1.read().splitlines(keepends=True)
        content2 = f2.read().splitlines(keepends=True)

    d = Differ()
    result = d.compare(content1, content2)

    return list(result)

def cmp_md5(contnet1, content2):
    m1 = hashlib.md5()
    m1.update(bytearray(contnet1, 'utf-8'))

    m2 = hashlib.md5()
    m2.update(bytearray(content2, 'utf-8'))

    return m1.hexdigest() == m2.hexdigest()

# 将字符串首字母转换成大写字母
def convertFirstLetterUpper(text_str):
    # return text_str.capitalize()
    # print("////////////////////>>>>", text_str)
    return re.sub("([a-zA-Z])", lambda x: x.groups()[0].upper(), text_str, 1)

def convertDataType(t):
    if t == "Integer":
        return "int"
    elif t == "Float":
        return "float"
    elif t == "String":
        return "str"
    elif t == "Boolean":
        return "bool"

def getVariableString(var):
    return repr(var)

# ROOT = os.path.abspath(os.getcwd())
jinja_env = Environment(loader=FileSystemLoader(os.path.join(os.getcwd(), 'templates')))
jinja_env.filters['letterUpper'] = convertFirstLetterUpper
jinja_env.filters['getDataType'] = convertDataType
jinja_env.filters['getVariableString'] = getVariableString

input_dir = None
output_dir = None

events = []

def copyFiles(src_dir, dst_dir):
    if not os.path.exists(src_dir):
        logger.error("%s 目录不存在" % src_dir)
        return None

    # 复制文件之前需要判断文件是否存在
    if not os.path.exists(dst_dir):
        os.makedirs(dst_dir)

    # root 所指的是当前正在遍历的这个文件夹的本身的地址
    # dirs 是一个 list,内容是该文件夹中所有的目录的名字(不包括子目录)
    # files 同样是 list, 内容是该文件夹中所有的文件(不包括子目录)
    for root, dirs, files in os.walk(src_dir):
        save_path = Path(dst_dir)

        if os.path.basename(root) == "__pycache__":
            continue

        for file in files:
            name, suffix = os.path.splitext(file)
            # 判断目标文件夹是否存在同名文件
            # 如果是python文件,文件重命名
            # 其他文件,直接删除
            relative_path = Path(root).joinpath(file).resolve().relative_to(Path(src_dir))
            target_file = Path(dst_dir).joinpath(relative_path).resolve()
            if cmp_file(Path(root).joinpath(file).resolve().as_posix(), target_file.resolve().as_posix()):
                continue
            if target_file.exists() and target_file.suffix in [".py"]:
                new_file = "{}.{}{}".format(name, time.strftime("%Y%m%d%H%M%S", time.localtime()), suffix)
                tmp_file = target_file.parent.joinpath(new_file)
                if tmp_file.exists():
                    tmp_file.unlink()
                target_file.rename(tmp_file)
                # os.rename(os.sep.join([root, file]), os.sep.join([root, new_file]))
            else:
                logger.info("delete file")
                logger.info(target_file)
                # os.unlink(os.sep.join([root, file]))

            src_file = Path(root).joinpath(file)
            relative_path = src_file.resolve().relative_to(Path(src_dir))
            save_path = Path(dst_dir).joinpath(relative_path)
            if save_path.is_file() and not save_path.parent.exists():
                save_path.parent.mkdir()

            # if not os.path.exists(save_path):
            #     os.makedirs(save_path)

            # 目标文件是否存在?
            shutil.copyfile(src_file, save_path)

    logger.info('copy files finished!')

def handleModuleConfig(config):
    # 处理每一项配置文件
    # 入口文件名
    # 实例对象,根据这个实例连接对应请求
    # 实例方法,需要根据实例方法生成事件函数
    '''
    '''
    pass

def handleModules(config):
    global output_dir
    # 遍历modules结构,判断文件是否存在,并且enable已经打开
    # 如果文件不存在,则直接报错
    # 本质上这是一个文件拷贝的操作
    for mod in config:
        p = Path(mod.get("config"))
        if not p.exists() or not mod.get("enable"):
            continue
        shutil.copyfile(mod.get("config"),  os.sep.join([output_dir, "controllers", p.name]))
        for d in mod.get("dependencies"):
            if Path(d).exists():
                shutil.copyfile(d, os.sep.join([output_dir, "controllers", Path(d).name]))
            else:
                logger.error("文件:%s 不存在")

def handleResources(config):
    # 处理路由页面
    # 遍历config文件,收集所有的action和name,action和name的首字母必须大写
    # 然后才生成路由配置页面
    target_file = os.sep.join(["views", "__init__.py"])
    handleRender(config, 'router.tpl', target_file)

def handleSignal(config):
    # 生成信号槽模块
    if config.get("framework").get("signal").get("regenerate"):
        target_file = os.sep.join(["application", "signal_manager.py"])
        handleRender(config.get("apis"), 'signal_manager.tpl', target_file)

    if config.get("framework").get("controllerInit").get("regenerate"):
        target_file = os.sep.join(["controllers", "__init__.py"])
        handleRender(config.get("apis"), 'signal_manager_init.tpl', target_file)

def handleModel(config, application):
    # 判断是否有model字段,没有直接退出
    if not config.get("model"):
        return None

    # 将所有有默认值的字段分为一组,没有默认值的字段分为另一组
    # 生成模板代码时,无默认值的字段在前,有默认值的字段字在后
    # 收集表字段信息
    fields = []
    extend = False
    for m in config.get("model").get("fields"):
        fields.append(m.get("name"))
        extend = True
        print(m)

    target_file = os.sep.join(["models", "{}.py".format(config.get("name"))])
    handleRender(config, 'model.tpl', target_file, fields=fields, extend=extend, application=application)
    # 多次复制时,会报文件已存在错误,因此要先删除
    target_file = os.sep.join(["models", "base.py"])
    if os.path.exists(target_file):
        os.remove(target_file)
    handleRender(config, 'base.tpl', target_file, application=application)

def handleView(config):
    target_file = os.sep.join(["views", "{}.py".format(config.get("name"))])
    handleRender(config, 'view.tpl', target_file)

def handleController(config):
    # 根据模型字段自动从models列表中取出file name信息和class name信息
    target_file = os.sep.join(["controllers", "{}.py".format(config.get("name"))])
    handleRender(config, 'controller.tpl', target_file)

def handleRender(result, tpl, target_file, **kwargs):
    global output_dir
    # print("=========>", result.get("name"), "{}.py".format(result.get("name")))
    jinja_tpl = jinja_env.get_template(tpl)
    content = jinja_tpl.render({ "config": result, **kwargs })
    # print("############", output_dir)
    target_file = os.sep.join([output_dir, target_file])
    if not os.path.exists(os.path.dirname(target_file)):
        os.makedirs(os.path.dirname(target_file))
    
    # 这里需要比较目标文件夹是否已经存在同名文件,如果存在,则需要比较两个文件内容是否一致
    # 如果不一致时,则需要给旧的文件打上时间戳,作为备份文件

    # 具体流程:
    # 先将原来文件重命名,然后新的内容写入到重命名的文件中
    # 最后比较两个文件内容是否一致,如果一致,则删除重命名的那个旧的备份文件

    tmp_file = ""
    if os.path.exists(target_file) and os.stat(target_file).st_size > 0:
        n, e = os.path.splitext(target_file)
        tmp_file = "{}.{}{}".format(n, time.strftime("%Y%m%d%H%M%S", time.localtime()), e)
        if os.path.exists(tmp_file):
            os.remove(tmp_file)
        os.rename(target_file, tmp_file)

    with open(target_file, 'w', encoding='utf-8') as f:
        f.write(content)

    if len(tmp_file) > 0 and cmp_file(tmp_file, target_file) == True:
        os.remove(tmp_file)

def parseConfig(config):
    # 解析配置文件
    for cfg in config.get("apis"):
        if not cfg.get("enable"):
            continue
        handleModel(cfg, config.get("application"))
        handleView(cfg)
        handleController(cfg)
    # 全局配置
    handleResources(config.get("apis"))
    handleSignal(config)
    handleModules(config.get("modules"))

def readConfig():
    result = {}
    with open("config.json", "r+") as f:
        result = json.loads(f.read())
    return result

# 备份数据库,判断目标文件夹下是否有.db文件,有的话,先备份到临时目录,待文件复制完成后,再放回原来位置
def backup_database():
    global output_dir
    target_dir = os.sep.join([os.path.dirname(output_dir), "tmp"])
    if not os.path.exists(target_dir):
        os.makedirs(target_dir)
    home_fs = fs.open_fs(output_dir)
    for file in home_fs.glob('*.db', namespaces=['details']):
        # print(file.path, os.path.normpath(os.sep.join([output_dir, file.path])))
        # copyFiles(os.path.normpath(os.sep.join([output_dir, file.path])), target_dir)
        shutil.copy(os.path.normpath(os.sep.join([output_dir, file.path])), target_dir)

# 恢复数据库,代码生成完成后,需要把之前复制的数据库文件,恢复到原来位置
def restore_database():
    global output_dir
    target_dir = os.sep.join([os.path.dirname(output_dir), "tmp"])

    if not os.path.exists(target_dir):
        return

    home_fs = fs.open_fs(target_dir)
    for file in home_fs.glob('*.db', namespaces=['details']):
        # copyFiles(os.path.normpath(os.sep.join([target_dir, file.path])), output_dir)
        shutil.copy(os.path.normpath(os.sep.join([target_dir, file.path])), output_dir)

    home_fs = fs.open_fs(target_dir)
    home_fs.removetree("/")
    if home_fs.exists("/"):
        os.removedirs(target_dir)

def run():
    global input_dir
    global output_dir
    input_dir = os.sep.join([os.getcwd(), "resources"])
    output_dir = os.sep.join([os.getcwd(), "build_out"])
    backup_database()
    # 复制文件到输出目录
    copyFiles(input_dir, output_dir)
    config = readConfig()
    parseConfig(config)
    restore_database()
    print("success ......")

if __name__ == "__main__":
    run()