Skip to content

Commit af8bad8

Browse files
committed
Add backup'API
1 parent 46c2c90 commit af8bad8

File tree

15 files changed

+872
-55
lines changed

15 files changed

+872
-55
lines changed

apphub/build/lib/cli/apphub_cli.py

Lines changed: 180 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import sys
22
import os
3+
import uuid
34
import json
5+
import shutil
6+
import requests
47
import subprocess
58

69
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
710

811
import click
12+
from dotenv import dotenv_values, set_key,unset_key
913
from src.services.apikey_manager import APIKeyManager
1014
from src.services.settings_manager import SettingsManager
1115
from src.core.exception import CustomException
@@ -69,30 +73,191 @@ def getconfig(section, key):
6973
raise click.ClickException(str(e))
7074

7175
@cli.command()
72-
@click.option('--appname',required=True, help='The App Name')
73-
@click.option('--appid',required=True, help='The App Id')
74-
@click.option('--github_email', help='The Github Email')
75-
@click.option('--github_user', help='The Github User')
76-
def push(appname, appid, github_email, github_user):
77-
"""Push the app to the Github"""
78-
# 从配置文件读取gitea的用户名和密码
79-
try:
80-
giteat_user = ConfigManager().get_value("gitea", "user_name")
81-
giteat_pwd = ConfigManager().get_value("gitea", "user_pwd")
76+
@click.option('--section',required=True, help='The section name')
77+
@click.option('--key', required=True, help='The key name')
78+
def getsysconfig(section, key):
79+
"""Get a system config value"""
80+
try:
81+
system_config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../config/system.ini'))
82+
settings_manager = SettingsManager()
83+
settings_manager.config_file_path = system_config_path
84+
85+
if key is None:
86+
value = SettingsManager().read_section(section)
87+
value = json.dumps(value)
88+
click.echo(f"{value}")
89+
else:
90+
value = SettingsManager().read_key(section, key)
91+
click.echo(f"{value}")
8292
except CustomException as e:
8393
raise click.ClickException(e.details)
8494
except Exception as e:
8595
raise click.ClickException(str(e))
96+
97+
@cli.command()
98+
@click.option('--appid',required=True, help='The App Id')
99+
@click.option('--github_token', required=True, help='The Github Token')
100+
def commit(appid, github_token):
101+
"""Commit the app to the Github"""
102+
try:
103+
# 从配置文件读取gitea的用户名和密码
104+
gitea_user = ConfigManager().get_value("gitea", "user_name")
105+
gitea_pwd = ConfigManager().get_value("gitea", "user_pwd")
106+
107+
# 将/tmp目录作为工作目录,如果不存在则创建,如果存在则清空
108+
work_dir = "/tmp/git"
109+
if os.path.exists(work_dir):
110+
shutil.rmtree(work_dir)
111+
os.makedirs(work_dir)
112+
os.chdir(work_dir)
113+
114+
# 执行git clone命令:将gitea仓库克隆到本地
115+
gitea_repo_url = f"http://{gitea_user}:{gitea_pwd}@websoft9-git:3000/websoft9/{appid}.git"
116+
subprocess.run(["git", "clone", gitea_repo_url], check=True)
117+
118+
# 执行git clone命令:将github仓库克隆到本地(dev分支)
119+
github_repo_url = f"https://github.com/Websoft9/docker-library.git"
120+
subprocess.run(["git", "clone", "--branch", "dev", github_repo_url], check=True)
121+
122+
# 解析gitea_repo_url下载的目录下的.env文件
123+
gitea_env_path = os.path.join(work_dir, appid, '.env')
124+
gitea_env_vars = dotenv_values(gitea_env_path)
125+
w9_app_name = gitea_env_vars.get('W9_APP_NAME')
86126

87-
# 拼接git仓库的url
88-
repo_url = f"http://{giteat_user}:{giteat_pwd}@websoft9-git:3000/websoft9/{appid}.git"
127+
if not w9_app_name:
128+
raise click.ClickException("W9_APP_NAME not found in Gitea .env file")
129+
130+
# 解析github_repo_url下载的目录下的/apps/W9_APP_NAME目录下的.env文件
131+
github_env_path = os.path.join(work_dir, 'docker-library', 'apps', w9_app_name, '.env')
132+
github_env_vars = dotenv_values(github_env_path)
89133

90-
# 执行git clone命令
134+
# 需要复制的变量
135+
env_vars_to_copy = ['W9_URL', 'W9_ID']
136+
port_set_vars = {key: value for key, value in github_env_vars.items() if key.endswith('PORT_SET')}
137+
138+
# 将这些值去替换gitea_repo_url目录下.env中对应项的值
139+
for key in env_vars_to_copy:
140+
if key in github_env_vars:
141+
set_key(gitea_env_path, key, github_env_vars[key])
142+
143+
for key, value in port_set_vars.items():
144+
set_key(gitea_env_path, key, value)
145+
146+
# 删除W9_APP_NAME
147+
unset_key(gitea_env_path, 'W9_APP_NAME')
148+
149+
# 将整个gitea目录覆盖到docker-library/apps/w9_app_name目录
150+
gitea_repo_dir = os.path.join(work_dir, appid)
151+
github_app_dir = os.path.join(work_dir, 'docker-library', 'apps', w9_app_name)
152+
if os.path.exists(github_app_dir):
153+
shutil.rmtree(github_app_dir)
154+
shutil.copytree(gitea_repo_dir, github_app_dir)
155+
156+
# 切换到docker-library目录
157+
os.chdir(os.path.join(work_dir, 'docker-library'))
158+
159+
# 创建一个新的分支
160+
new_branch_name = f"update-{w9_app_name}-{uuid.uuid4().hex[:8]}"
161+
subprocess.run(["git", "checkout", "-b", new_branch_name], check=True)
162+
163+
# 将修改提交到新的分支
164+
subprocess.run(["git", "add", "."], check=True)
165+
subprocess.run(["git", "commit", "-m", f"Update {w9_app_name}"], check=True)
166+
167+
# 推送新的分支到 GitHub
168+
# subprocess.run(["git", "push", "origin", new_branch_name], check=True)
169+
170+
# 推送新的分支到 GitHub
171+
github_push_url = f"https://{github_token}:x-oauth-basic@github.com/websoft9/docker-library.git"
172+
subprocess.run(["git", "push", github_push_url, new_branch_name], check=True)
173+
174+
# 创建 Pull Request 使用 GitHub API
175+
pr_data = {
176+
"title": f"Update {w9_app_name}",
177+
"head": new_branch_name,
178+
"base": "dev",
179+
"body": "Automated update"
180+
}
181+
182+
response = requests.post(
183+
f"https://api.github.com/repos/websoft9/docker-library/pulls",
184+
headers={
185+
"Authorization": f"token {github_token}",
186+
"Accept": "application/vnd.github.v3+json"
187+
},
188+
data=json.dumps(pr_data)
189+
)
190+
191+
if response.status_code != 201:
192+
raise click.ClickException(f"Failed to create Pull Request: {response.json()}")
193+
194+
click.echo(f"Pull Request created: {response.json().get('html_url')}")
195+
196+
except subprocess.CalledProcessError as e:
197+
raise click.ClickException(f"Command failed: {e}")
198+
except Exception as e:
199+
raise click.ClickException(str(e))
200+
finally:
201+
# 删除工作目录
202+
if os.path.exists(work_dir):
203+
shutil.rmtree(work_dir)
204+
205+
@cli.command()
206+
@click.argument('target', required=True, type=click.Choice(['apps'], case_sensitive=False))
207+
@click.option('--dev', is_flag=True, help='Upgrade using dev environment')
208+
def upgrade(target, dev):
209+
"""Upgrade apps"""
91210
try:
92-
subprocess.run(["git", "clone", repo_url])
211+
if target == 'apps':
212+
# 根据是否传入 --dev 参数设置 channel 和 package_name
213+
channel = "dev" if dev else "release"
214+
media_package = "media-dev.zip" if dev else "media-latest.zip"
215+
library_package = "library-dev.zip" if dev else "library-latest.zip"
216+
217+
# 执行升级 media 的命令
218+
if dev:
219+
subprocess.run(
220+
[
221+
"bash", "/websoft9/script/update_zip.sh",
222+
"--channel", channel, "--package_name", media_package, "--sync_to", "/websoft9/media"
223+
],
224+
check=True
225+
)
226+
else:
227+
subprocess.run(
228+
[
229+
"bash", "/websoft9/script/update_zip.sh",
230+
"--channel", channel, "--package_name", media_package, "--sync_to", "/websoft9/media"
231+
],
232+
check=True
233+
)
234+
click.echo(f"Media resources ({channel}) updated successfully.")
235+
236+
# 执行升级 library 的命令
237+
if dev:
238+
subprocess.run(
239+
[
240+
"bash", "/websoft9/script/update_zip.sh",
241+
"--channel", channel, "--package_name", library_package, "--sync_to", "/websoft9/library"
242+
],
243+
check=True
244+
)
245+
else:
246+
subprocess.run(
247+
[
248+
"bash", "/websoft9/script/update_zip.sh",
249+
"--channel", channel, "--package_name", library_package, "--sync_to", "/websoft9/library"
250+
],
251+
check=True
252+
)
253+
click.echo(f"Library resources ({channel}) updated successfully.")
254+
else:
255+
click.echo(f"Unknown upgrade target: {target}")
256+
except subprocess.CalledProcessError as e:
257+
raise click.ClickException(f"Upgrade command failed: {e}")
93258
except Exception as e:
94259
raise click.ClickException(str(e))
95260

96261

97262
if __name__ == "__main__":
98-
cli()
263+
cli()

apphub/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
setup(
66
name='apphub',
7-
version='0.3',
7+
version='0.4',
88
packages=find_packages(where='src'),
99
package_dir={'': 'src'},
1010
install_requires=[

apphub/src/api/v1/routers/backup.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from fastapi import APIRouter, Query, Path, Body
2+
from typing import Dict, List
3+
from src.schemas.errorResponse import ErrorResponse
4+
from src.services.back_manager import BackupManager
5+
from src.schemas.backupsnapshot import BackupSnapshot
6+
7+
router = APIRouter()
8+
9+
@router.post(
10+
"/backup/{app_id}",
11+
summary="Create Backup",
12+
description="Create a backup for the specified app",
13+
responses={
14+
200: {"description": "Backup created successfully"},
15+
400: {"model": ErrorResponse},
16+
500: {"model": ErrorResponse},
17+
},
18+
)
19+
def create_backup(
20+
app_id: str = Path(..., description="App ID to create backup for")
21+
):
22+
BackupManager().create_backup(app_id)
23+
return {"message": f"Backup created successfully for app: {app_id}"}
24+
25+
@router.get(
26+
"/backup/snapshots",
27+
summary="List Snapshots",
28+
description="List all snapshots or filter by app ID",
29+
responses={
30+
200: {"model": List[BackupSnapshot]},
31+
400: {"model": ErrorResponse},
32+
500: {"model": ErrorResponse},
33+
},
34+
)
35+
def list_snapshots(
36+
app_id: str = Query(None, description="App ID to filter snapshots by (optional)"),
37+
):
38+
snapshots = BackupManager().list_snapshots(app_id)
39+
return snapshots
40+
41+
@router.delete(
42+
"/backup/snapshots/{snapshot_id}",
43+
summary="Delete Snapshot",
44+
description="Delete a snapshot by its ID",
45+
status_code=204,
46+
responses={
47+
204: {"description": "Snapshot deleted successfully"},
48+
400: {"model": ErrorResponse},
49+
500: {"model": ErrorResponse},
50+
},
51+
)
52+
def delete_snapshot(
53+
snapshot_id: str = Path(..., description="Snapshot ID to delete"),
54+
):
55+
BackupManager().delete_snapshot(snapshot_id)
56+
return {"message": f"Snapshot {snapshot_id} deleted successfully"}
57+
58+
@router.post(
59+
"/backup/restore/{app_id}/{snapshot_id}",
60+
summary="Restore Snapshot",
61+
description="Restore a snapshot to the specified locations",
62+
responses={
63+
200: {"description": "Snapshot restored successfully"},
64+
400: {"model": ErrorResponse},
65+
500: {"model": ErrorResponse},
66+
},
67+
)
68+
def restore_snapshot(
69+
app_id: str = Path(..., description="App ID to restore"),
70+
snapshot_id: str = Path(..., description="Snapshot ID to restore")
71+
):
72+
BackupManager().restore_backup(app_id,snapshot_id)
73+
return {"message": f"Snapshot {snapshot_id} restored successfully"}

apphub/src/apphub.egg-info/PKG-INFO

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
Metadata-Version: 2.1
22
Name: apphub
3-
Version: 0.3
3+
Version: 0.4

apphub/src/cli/apphub_cli.py

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import configparser
12
import sys
23
import os
34
import uuid
@@ -55,17 +56,51 @@ def setconfig(section, key, value):
5556
raise click.ClickException(str(e))
5657

5758
@cli.command()
58-
@click.option('--section',required=True, help='The section name')
59+
@click.option('--section', help='The section name')
5960
@click.option('--key', help='The key name')
6061
def getconfig(section, key):
61-
"""Get a config value"""
62-
try:
63-
if key is None:
64-
value = SettingsManager().read_section(section)
65-
value = json.dumps(value)
62+
"""Get a config value or all config as JSON"""
63+
try:
64+
config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../config/config.ini'))
65+
config = configparser.ConfigParser()
66+
config.read(config_path, encoding="utf-8")
67+
if section is None:
68+
# 返回整个配置文件内容
69+
all_config = {s: dict(config.items(s)) for s in config.sections()}
70+
click.echo(json.dumps(all_config))
71+
elif key is None:
72+
# 返回指定 section 的内容
73+
value = dict(config.items(section)) if section in config.sections() else {}
74+
click.echo(json.dumps(value))
75+
else:
76+
# 返回指定 section 和 key 的内容
77+
value = config.get(section, key) if config.has_option(section, key) else ""
6678
click.echo(f"{value}")
79+
except CustomException as e:
80+
raise click.ClickException(e.details)
81+
except Exception as e:
82+
raise click.ClickException(str(e))
83+
84+
@cli.command()
85+
@click.option('--section', help='The section name')
86+
@click.option('--key', help='The key name')
87+
def getsysconfig(section, key):
88+
"""Get a system config value or all system config as JSON"""
89+
try:
90+
system_config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../config/system.ini'))
91+
config = configparser.ConfigParser()
92+
config.read(system_config_path, encoding="utf-8")
93+
if section is None:
94+
# 返回整个 system.ini 文件内容
95+
all_config = {s: dict(config.items(s)) for s in config.sections()}
96+
click.echo(json.dumps(all_config))
97+
elif key is None:
98+
# 返回指定 section 的内容
99+
value = dict(config.items(section)) if section in config.sections() else {}
100+
click.echo(json.dumps(value))
67101
else:
68-
value = SettingsManager().read_key(section, key)
102+
# 返回指定 section 和 key 的内容
103+
value = config.get(section, key) if config.has_option(section, key) else ""
69104
click.echo(f"{value}")
70105
except CustomException as e:
71106
raise click.ClickException(e.details)
@@ -236,6 +271,23 @@ def upgrade(target, dev):
236271
except Exception as e:
237272
raise click.ClickException(str(e))
238273

274+
@cli.command()
275+
def getallconfig():
276+
"""Get all config.ini and system.ini data as JSON"""
277+
try:
278+
config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../config/config.ini'))
279+
system_config_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '../config/system.ini'))
280+
config = configparser.ConfigParser()
281+
system_config = configparser.ConfigParser()
282+
config.read(config_path, encoding="utf-8")
283+
system_config.read(system_config_path, encoding="utf-8")
284+
result = {
285+
"config": {s: dict(config.items(s)) for s in config.sections()},
286+
"system": {s: dict(system_config.items(s)) for s in system_config.sections()}
287+
}
288+
click.echo(json.dumps(result))
289+
except Exception as e:
290+
raise click.ClickException(str(e))
239291

240292
if __name__ == "__main__":
241293
cli()

0 commit comments

Comments
 (0)