Spaces:
Sleeping
Sleeping
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) |