import zipfile import yaml from pywebio.input import * from pywebio.output import * from pywebio.platform import config from pywebio.platform.tornado import start_server from pathlib import Path import shutil import logging import os import io import re # 环境变量 APPS_DIR = Path("apps") DEFAULT_LOGO = Path("default_logo.png") # 初始化logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # 校验key是否为英文字符串 def is_valid_key(key): return bool(re.match(r'^[a-zA-Z]+$', key)) # 校验基本信息 def check_base_info(data): required_fields = [ "name", "key", "tags", "shortDescZh", "shortDescEn", "type", "crossVersionUpdate", "website", "github", "document" ] for field in required_fields: if not data[field]: return (field, f"{field} 不能为空") if len(data["shortDescZh"]) > 30: return ("shortDescZh", "中文描述不能超过30个字") if not is_valid_key(data["key"]): return ("key", "key 必须是纯英文字符串") return None # 保存文件 def save_file(path, content, mode='w', encoding=None): try: if 'b' in mode: # 二进制模式 with open(path, mode) as f: f.write(content) else: # 文本模式 with open(path, mode, encoding=encoding or 'utf-8') as f: f.write(content) logging.info(f"File saved successfully: {path}") except IOError as e: logging.error(f"Error saving file {path}: {e}") raise # 复制文件 def copy_file(src, dst): try: shutil.copy(src, dst) logging.info(f"File copied successfully from {src} to {dst}") except IOError as e: logging.error(f"Error copying file from {src} to {dst}: {e}") raise # 创建目录 def create_directory(path): try: path.mkdir(parents=True, exist_ok=True) logging.info(f"Directory created: {path}") except OSError as e: logging.error(f"Error creating directory {path}: {e}") raise # 创建版本 def create_version(app_dir, existing_versions): while True: version = input("请输入应用的版本 (不要以v开头)") if version in existing_versions: put_error(f"版本 {version} 已存在,请输入一个新的版本号") else: break version_dir = app_dir / version create_directory(version_dir) version_info = input_group("版本信息", [ textarea("请编写docker-compose.yml", name="docker_compose", code={"mode": "yaml", "theme": ""}), textarea("请编写data.yml", name="data", code={"mode": "yaml", "theme": ""}), ]) save_file(version_dir / "data.yml", version_info["data"]) save_file(version_dir / "docker-compose.yml", version_info["docker_compose"]) put_success(f"已成功创建版本 {version}") return version # 压缩文件夹 def zip_folder(folder_path, output_path): with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zipf: for root, _, files in os.walk(folder_path): for file in files: file_path = os.path.join(root, file) arcname = os.path.relpath(file_path, folder_path) zipf.write(file_path, arcname) # 主函数 def main(): base_info = input_group( "自助创建 1Panel 应用", [ input("1. 请输入应用名称* ", name="name", type=TEXT), input("2. 请输入应用的key* (仅限英文,用于创建文件夹)", name="key", type=TEXT), checkbox("3. 选择应用标签*(可以有多个)", inline=True, options=[ {"label": "建站", "value": "WebSite"}, {"label": "Web 服务器", "value": "Server"}, {"label": "运行环境", "value": "Runtime"}, {"label": "数据库", "value": "Database"}, {"label": "工具", "value": "Tool"}, {"label": "CI/CD", "value": "CI/CD"}, {"label": "本地", "value": "Local"}, ], name="tags"), input("4. 请输入应用中文描述*(不要超过30个字)", name="shortDescZh", type=TEXT), input("5. 请输入应用英文描述*", name="shortDescEn", type=TEXT), select("6. 选择应用类型*", options=[ {"label": "工具类应用,如 phpMyAdmin redis-commander jenkins", "value": "tool"}, {"label": "支持一键部署的站点类应用类型,如 wordpress halo", "value": "website"}, {"label": "服务类型的运行时应用,如 mysql openresty redis", "value": "runtime"}, ], name="type"), select("7. 是否可跨大版本升级*", options=[ {"label": "是", "value": True}, {"label": "否", "value": False}, ], name="crossVersionUpdate"), slider("8. 应用安装数量限制,(0 代表无限制)*", name="limit", min=0, max=100, step=1, value=0), input("9. 官网地址*", name="website", type=URL), input("10. Github 地址*", name="github", type=URL), input("11. 文档地址*", name="document", type=URL), file_upload("上传应用Logo图片(最好是 180 * 180 px)(可选): ", name="logo", accept=[".png", ".jpg", ".jpeg"], max_size="5M"), ], validate=check_base_info, ) app_dir = APPS_DIR / base_info["key"] create_directory(app_dir) app_info = { "additionalProperties": { "key": base_info["key"], "name": base_info["name"], "tags": base_info["tags"], "shortDescZh": base_info["shortDescZh"], "shortDescEn": base_info["shortDescEn"], "type": base_info["type"], "crossVersionUpdate": base_info["crossVersionUpdate"], "limit": base_info["limit"], "website": base_info["website"], "github": base_info["github"], "document": base_info["document"], } } save_file(app_dir / "data.yml", yaml.dump(app_info, allow_unicode=True)) if base_info["logo"]: _, file_extension = os.path.splitext(base_info["logo"]["filename"]) logo_filename = f"logo{file_extension.lower()}" save_file(app_dir / logo_filename, base_info["logo"]["content"], mode='wb') else: copy_file(DEFAULT_LOGO, app_dir / "logo.png") put_success("已成功创建基本信息") readme = textarea("请编写README", code={"mode": "markdown", "theme": ""}) save_file(app_dir / "README.md", readme) put_success("已成功创建README") versions = [] while True: version = create_version(app_dir, versions) versions.append(version) if not actions("是否继续创建新版本?", [ {"label": "是", "value": "yes"}, {"label": "否", "value": "no"}, ]) == "yes": break # 压缩应用文件夹 zip_buffer = io.BytesIO() zip_folder(app_dir, zip_buffer) zip_buffer.seek(0) # 美化下载按钮 put_button( f"下载 {base_info['name']} 应用文件", onclick=lambda: put_file(f"{base_info['key']}.zip", zip_buffer.getvalue()), color="success", outline=True ) if __name__ == "__main__": config(title="自助创建 1Panel 应用") start_server(main, debug=False, port=8080, cdn=False)