Initial commit: rdgen with GitHub/Gitea dual build backend
This commit is contained in:
@@ -0,0 +1,695 @@
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote
|
||||
|
||||
import base64
|
||||
import pyzipper
|
||||
from PIL import Image
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files.base import ContentFile
|
||||
from django.db.models import Q
|
||||
from django.http import (
|
||||
HttpResponse,
|
||||
JsonResponse,
|
||||
HttpResponseForbidden,
|
||||
Http404,
|
||||
)
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from . import app_settings, build_backends, versions as rdeskVersions
|
||||
from .forms import GenerateForm
|
||||
from .models import GithubRun, STATUS_LABELS, STATUS_BADGE
|
||||
|
||||
|
||||
def _webhook_denied(request):
|
||||
"""配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。"""
|
||||
secret = app_settings.get_value('WEBHOOK_SECRET')
|
||||
if secret and request.headers.get('X-Webhook-Secret') != secret:
|
||||
return HttpResponseForbidden('无效的 Webhook 密钥')
|
||||
return None
|
||||
|
||||
|
||||
def _status_extra(status_code):
|
||||
return (
|
||||
STATUS_LABELS.get(status_code, status_code),
|
||||
STATUS_BADGE.get(status_code, 'badge-secondary'),
|
||||
)
|
||||
|
||||
|
||||
def _can_access_run(request, run):
|
||||
"""任务提交者本人或管理员可访问;历史任务无归属人时登录用户均可访问。"""
|
||||
if request.user.is_staff:
|
||||
return True
|
||||
return not run.created_by_id or run.created_by_id == request.user.id
|
||||
|
||||
|
||||
def generate_custom_client(params, full_url, user=None):
|
||||
"""
|
||||
网页表单与 JSON API 共用的核心生成逻辑。
|
||||
|
||||
Args:
|
||||
params: 包含全部配置项的字典(键名与 GenerateForm 字段一致)
|
||||
full_url: 本服务的完整 URL(协议 + 主机名)
|
||||
user: 提交任务的登录用户(可为 None,如匿名 API 调用)
|
||||
|
||||
Returns:
|
||||
成功时包含 success/uuid/filename/platform/log_url;
|
||||
失败时包含 success=False、error(中文提示)及 status_code。
|
||||
"""
|
||||
user_secret = params.get('sh_secret_field', '')
|
||||
selfhosted = (app_settings.get_value('SH_SECRET') == user_secret)
|
||||
platform = params.get('platform', 'windows')
|
||||
version = params.get('version', '1.4.9')
|
||||
delayFix = params.get('delayFix', True)
|
||||
xOffline = params.get('xOffline', False)
|
||||
hidecm = params.get('hidecm', False)
|
||||
removeNewVersionNotif = params.get('removeNewVersionNotif', False)
|
||||
server = params.get('serverIP', '')
|
||||
serverPort = params.get('serverPort', '')
|
||||
key = params.get('key', '')
|
||||
apiServer = params.get('apiServer', '')
|
||||
urlLink = params.get('urlLink', '')
|
||||
downloadLink = params.get('downloadLink', '')
|
||||
if not server:
|
||||
server = 'rs-ny.rustdesk.com' # RustDesk 默认服务器
|
||||
if not serverPort:
|
||||
serverPort = '21116' # RustDesk 默认会合端口
|
||||
if not key:
|
||||
key = 'OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=' # RustDesk 默认公钥
|
||||
if not apiServer:
|
||||
apiServer = server+":21114"
|
||||
if not urlLink:
|
||||
urlLink = "https://rustdesk.com"
|
||||
if not downloadLink:
|
||||
downloadLink = "https://rustdesk.com/download"
|
||||
direction = params.get('direction', 'both')
|
||||
installation = params.get('installation', 'installationY')
|
||||
settings = params.get('settings', 'settingsY')
|
||||
appname = params.get('appname', '')
|
||||
if not appname:
|
||||
appname = "rustdesk"
|
||||
filename = params.get('exename', 'rustdesk')
|
||||
compname = params.get('compname', '')
|
||||
if not compname:
|
||||
compname = "Purslane Ltd"
|
||||
androidappid = params.get('androidappid', '')
|
||||
if not androidappid:
|
||||
androidappid = "com.carriez.flutter_hbb"
|
||||
compname = compname.replace("&","\\&")
|
||||
permPass = params.get('permanentPassword', '')
|
||||
theme = params.get('theme', 'system')
|
||||
themeDorO = params.get('themeDorO', 'default')
|
||||
passApproveMode = params.get('passApproveMode', 'password-click')
|
||||
denyLan = params.get('denyLan', False)
|
||||
enableDirectIP = params.get('enableDirectIP', False)
|
||||
autoClose = params.get('autoClose', False)
|
||||
permissionsDorO = params.get('permissionsDorO', 'default')
|
||||
permissionsType = params.get('permissionsType', 'custom')
|
||||
enableKeyboard = params.get('enableKeyboard', True)
|
||||
enableClipboard = params.get('enableClipboard', True)
|
||||
enableFileTransfer = params.get('enableFileTransfer', True)
|
||||
enableAudio = params.get('enableAudio', True)
|
||||
enableTCP = params.get('enableTCP', True)
|
||||
enableRemoteRestart = params.get('enableRemoteRestart', True)
|
||||
enableRecording = params.get('enableRecording', True)
|
||||
enableBlockingInput = params.get('enableBlockingInput', True)
|
||||
enableRemoteModi = params.get('enableRemoteModi', False)
|
||||
removeWallpaper = params.get('removeWallpaper', True)
|
||||
defaultManual = params.get('defaultManual', '')
|
||||
overrideManual = params.get('overrideManual', '')
|
||||
enablePrinter = params.get('enablePrinter', True)
|
||||
enableCamera = params.get('enableCamera', True)
|
||||
enableTerminal = params.get('enableTerminal', True)
|
||||
|
||||
if all(char.isascii() for char in filename):
|
||||
filename = re.sub(r'[^\w\s-]', '_', filename).strip()
|
||||
filename = filename.replace(" ","_")
|
||||
else:
|
||||
filename = "rustdesk"
|
||||
if not all(char.isascii() for char in appname):
|
||||
appname = "rustdesk"
|
||||
myuuid = str(uuid.uuid4())
|
||||
|
||||
try:
|
||||
iconfile = params.get('iconfile')
|
||||
if not iconfile:
|
||||
iconfile = params.get('iconbase64')
|
||||
iconlink_url, iconlink_uuid, iconlink_file = save_png(iconfile,myuuid,full_url,"icon.png")
|
||||
except Exception:
|
||||
print("获取图标失败,使用默认图标")
|
||||
iconlink_url = "false"
|
||||
iconlink_uuid = "false"
|
||||
iconlink_file = "false"
|
||||
try:
|
||||
logofile = params.get('logofile')
|
||||
if not logofile:
|
||||
logofile = params.get('logobase64')
|
||||
logolink_url, logolink_uuid, logolink_file = save_png(logofile,myuuid,full_url,"logo.png")
|
||||
except Exception:
|
||||
print("获取 Logo 失败")
|
||||
logolink_url = "false"
|
||||
logolink_uuid = "false"
|
||||
logolink_file = "false"
|
||||
try:
|
||||
privacyfile = params.get('privacyfile')
|
||||
if not privacyfile:
|
||||
privacyfile = params.get('privacybase64')
|
||||
privacylink_url, privacylink_uuid, privacylink_file = save_png(privacyfile,myuuid,full_url,"privacy.png")
|
||||
except Exception:
|
||||
print("获取隐私屏图片失败")
|
||||
privacylink_url = "false"
|
||||
privacylink_uuid = "false"
|
||||
privacylink_file = "false"
|
||||
|
||||
### 生成 custom.txt 配置 JSON,作为工作流输入
|
||||
decodedCustom = {}
|
||||
if direction != "Both":
|
||||
decodedCustom['conn-type'] = direction
|
||||
if installation == "installationN":
|
||||
decodedCustom['disable-installation'] = 'Y'
|
||||
if settings == "settingsN":
|
||||
decodedCustom['disable-settings'] = 'Y'
|
||||
if appname.lower() != "rustdesk" and appname != "":
|
||||
decodedCustom['app-name'] = appname
|
||||
decodedCustom['override-settings'] = {}
|
||||
decodedCustom['default-settings'] = {}
|
||||
if permPass != "":
|
||||
decodedCustom['password'] = permPass
|
||||
if theme != "system":
|
||||
if themeDorO == "default":
|
||||
if platform == "windows-x86":
|
||||
decodedCustom['default-settings']['allow-darktheme'] = 'Y' if theme == "dark" else 'N'
|
||||
else:
|
||||
decodedCustom['default-settings']['theme'] = theme
|
||||
elif themeDorO == "override":
|
||||
if platform == "windows-x86":
|
||||
decodedCustom['override-settings']['allow-darktheme'] = 'Y' if theme == "dark" else 'N'
|
||||
else:
|
||||
decodedCustom['override-settings']['theme'] = theme
|
||||
decodedCustom['enable-lan-discovery'] = 'N' if denyLan else 'Y'
|
||||
#decodedCustom['direct-server'] = 'Y' if enableDirectIP else 'N'
|
||||
decodedCustom['allow-auto-disconnect'] = 'Y' if autoClose else 'N'
|
||||
|
||||
if permissionsDorO == "default":
|
||||
decodedCustom['default-settings']['access-mode'] = permissionsType
|
||||
decodedCustom['default-settings']['enable-keyboard'] = 'Y' if enableKeyboard else 'N'
|
||||
decodedCustom['default-settings']['enable-clipboard'] = 'Y' if enableClipboard else 'N'
|
||||
decodedCustom['default-settings']['enable-file-transfer'] = 'Y' if enableFileTransfer else 'N'
|
||||
decodedCustom['default-settings']['enable-audio'] = 'Y' if enableAudio else 'N'
|
||||
decodedCustom['default-settings']['enable-tunnel'] = 'Y' if enableTCP else 'N'
|
||||
decodedCustom['default-settings']['enable-remote-restart'] = 'Y' if enableRemoteRestart else 'N'
|
||||
decodedCustom['default-settings']['enable-record-session'] = 'Y' if enableRecording else 'N'
|
||||
decodedCustom['default-settings']['enable-block-input'] = 'Y' if enableBlockingInput else 'N'
|
||||
decodedCustom['default-settings']['allow-remote-config-modification'] = 'Y' if enableRemoteModi else 'N'
|
||||
decodedCustom['default-settings']['direct-server'] = 'Y' if enableDirectIP else 'N'
|
||||
decodedCustom['default-settings']['verification-method'] = 'use-permanent-password' if hidecm else 'use-both-passwords'
|
||||
decodedCustom['default-settings']['approve-mode'] = passApproveMode
|
||||
decodedCustom['default-settings']['allow-hide-cm'] = 'Y' if hidecm else 'N'
|
||||
decodedCustom['default-settings']['allow-remove-wallpaper'] = 'Y' if removeWallpaper else 'N'
|
||||
decodedCustom['default-settings']['enable-remote-printer'] = 'Y' if enablePrinter else 'N'
|
||||
decodedCustom['default-settings']['enable-camera'] = 'Y' if enableCamera else 'N'
|
||||
decodedCustom['default-settings']['enable-terminal'] = 'Y' if enableTerminal else 'N'
|
||||
|
||||
|
||||
else:
|
||||
decodedCustom['override-settings']['access-mode'] = permissionsType
|
||||
decodedCustom['override-settings']['enable-keyboard'] = 'Y' if enableKeyboard else 'N'
|
||||
decodedCustom['override-settings']['enable-clipboard'] = 'Y' if enableClipboard else 'N'
|
||||
decodedCustom['override-settings']['enable-file-transfer'] = 'Y' if enableFileTransfer else 'N'
|
||||
decodedCustom['override-settings']['enable-audio'] = 'Y' if enableAudio else 'N'
|
||||
decodedCustom['override-settings']['enable-tunnel'] = 'Y' if enableTCP else 'N'
|
||||
decodedCustom['override-settings']['enable-remote-restart'] = 'Y' if enableRemoteRestart else 'N'
|
||||
decodedCustom['override-settings']['enable-record-session'] = 'Y' if enableRecording else 'N'
|
||||
decodedCustom['override-settings']['enable-block-input'] = 'Y' if enableBlockingInput else 'N'
|
||||
decodedCustom['override-settings']['allow-remote-config-modification'] = 'Y' if enableRemoteModi else 'N'
|
||||
decodedCustom['override-settings']['direct-server'] = 'Y' if enableDirectIP else 'N'
|
||||
decodedCustom['override-settings']['verification-method'] = 'use-permanent-password' if hidecm else 'use-both-passwords'
|
||||
decodedCustom['override-settings']['approve-mode'] = passApproveMode
|
||||
decodedCustom['override-settings']['allow-hide-cm'] = 'Y' if hidecm else 'N'
|
||||
decodedCustom['override-settings']['allow-remove-wallpaper'] = 'Y' if removeWallpaper else 'N'
|
||||
decodedCustom['override-settings']['enable-remote-printer'] = 'Y' if enablePrinter else 'N'
|
||||
decodedCustom['override-settings']['enable-camera'] = 'Y' if enableCamera else 'N'
|
||||
decodedCustom['override-settings']['enable-terminal'] = 'Y' if enableTerminal else 'N'
|
||||
if direction == 'incoming':
|
||||
decodedCustom['override-settings']['custom-rendezvous-server'] = server
|
||||
decodedCustom['override-settings']['api-server'] = apiServer
|
||||
|
||||
if defaultManual:
|
||||
for line in defaultManual.splitlines():
|
||||
if '=' in line:
|
||||
k, value = line.split('=', 1)
|
||||
decodedCustom['default-settings'][k.strip()] = value.strip()
|
||||
|
||||
if overrideManual:
|
||||
for line in overrideManual.splitlines():
|
||||
if '=' in line:
|
||||
k, value = line.split('=', 1)
|
||||
decodedCustom['override-settings'][k.strip()] = value.strip()
|
||||
|
||||
decodedCustomJson = json.dumps(decodedCustom)
|
||||
|
||||
string_bytes = decodedCustomJson.encode("ascii")
|
||||
base64_bytes = base64.b64encode(string_bytes)
|
||||
encodedCustom = base64_bytes.decode("ascii")
|
||||
|
||||
#### 触发构建平台工作流(GitHub / Gitea 可在后台「构建平台配置」切换)
|
||||
backend = build_backends.get_backend()
|
||||
workflow_file = backend.workflow_file_for(platform, selfhosted=bool(selfhosted))
|
||||
|
||||
inputs_raw = {
|
||||
"server":server,
|
||||
"serverPort":serverPort,
|
||||
"key":key,
|
||||
"apiServer":apiServer,
|
||||
"custom":encodedCustom,
|
||||
"uuid":myuuid,
|
||||
"iconlink_url":iconlink_url,
|
||||
"iconlink_uuid":iconlink_uuid,
|
||||
"iconlink_file":iconlink_file,
|
||||
"logolink_url":logolink_url,
|
||||
"logolink_uuid":logolink_uuid,
|
||||
"logolink_file":logolink_file,
|
||||
"privacylink_url":privacylink_url,
|
||||
"privacylink_uuid":privacylink_uuid,
|
||||
"privacylink_file":privacylink_file,
|
||||
"appname":appname,
|
||||
"genurl":app_settings.get_value('GENURL'),
|
||||
"urlLink":urlLink,
|
||||
"downloadLink":downloadLink,
|
||||
"delayFix": 'true' if delayFix else 'false',
|
||||
"rdgen":'true',
|
||||
"xOffline": 'true' if xOffline else 'false',
|
||||
"removeNewVersionNotif": 'true' if removeNewVersionNotif else 'false',
|
||||
"compname": compname,
|
||||
"androidappid":androidappid,
|
||||
"filename":filename
|
||||
}
|
||||
|
||||
# ZIP_PASSWORD 为空时 pyzipper 的 AES 加密会直接抛 RuntimeError(500),
|
||||
# 这里前置校验,返回可操作的中文提示(网页与 API 共用此函数)。
|
||||
zip_password = app_settings.get_value('ZIP_PASSWORD')
|
||||
if not zip_password.strip():
|
||||
return {
|
||||
"success": False,
|
||||
"error": (
|
||||
"配置压缩包密码(ZIP_PASSWORD)未配置,无法加密构建配置包。"
|
||||
"请联系管理员在后台「GitHub 构建配置 - 回调与加密」中设置,"
|
||||
"并确保与 GitHub 仓库 Secret「ZIP_PASSWORD」完全一致。"
|
||||
),
|
||||
"status_code": 500,
|
||||
}
|
||||
|
||||
temp_json_path = f"data_{uuid.uuid4()}.json"
|
||||
zip_filename = f"secrets_{uuid.uuid4()}.zip"
|
||||
zip_path = "temp_zips/%s" % (zip_filename)
|
||||
Path("temp_zips").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(temp_json_path, "w") as f:
|
||||
json.dump(inputs_raw, f)
|
||||
|
||||
with pyzipper.AESZipFile(zip_path, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as zf:
|
||||
zf.setpassword(zip_password.encode())
|
||||
zf.write(temp_json_path, arcname="secrets.json")
|
||||
|
||||
if os.path.exists(temp_json_path):
|
||||
os.remove(temp_json_path)
|
||||
|
||||
zipJson = {}
|
||||
zipJson['url'] = full_url
|
||||
zipJson['file'] = zip_filename
|
||||
|
||||
zip_url = json.dumps(zipJson)
|
||||
|
||||
dispatch_inputs = {
|
||||
"version": version,
|
||||
"zip_url": zip_url,
|
||||
}
|
||||
# 保存提交时的配置快照,供终态任务「一键重试」。
|
||||
# 仅保留 JSON 安全类型(上传文件对象跳过——网页提交时图片已转为
|
||||
# iconbase64/logobase64/privacybase64 data URL 字符串,不会丢失)。
|
||||
config_snapshot = {}
|
||||
for _k, _v in params.items():
|
||||
if isinstance(_v, (str, int, float, bool)) or _v is None:
|
||||
config_snapshot[_k] = _v
|
||||
|
||||
new_github_run = GithubRun(
|
||||
uuid=myuuid,
|
||||
status="Starting generator...please wait",
|
||||
platform=platform,
|
||||
filename=filename,
|
||||
config_data=config_snapshot,
|
||||
backend=backend.name,
|
||||
created_by=(user if user is not None and getattr(user, 'is_authenticated', False) else None),
|
||||
)
|
||||
try:
|
||||
dispatch_result = backend.dispatch(workflow_file, dispatch_inputs, timeout=20)
|
||||
new_github_run.github_run_id = dispatch_result.run_id
|
||||
new_github_run.status = "in_progress"
|
||||
new_github_run.save()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"uuid": myuuid,
|
||||
"filename": filename,
|
||||
"platform": platform,
|
||||
"log_url": dispatch_result.html_url,
|
||||
"run": new_github_run,
|
||||
}
|
||||
except build_backends.base.BackendError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": e.message,
|
||||
"status_code": 502
|
||||
}
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"触发构建服务时发生未预期的错误:{str(e)}",
|
||||
"status_code": 502
|
||||
}
|
||||
|
||||
|
||||
def _get_run_status(uuid_val):
|
||||
"""
|
||||
查询构建状态(网页与 JSON API 共用)。
|
||||
"""
|
||||
try:
|
||||
gh_run = GithubRun.objects.get(uuid=uuid_val)
|
||||
except GithubRun.DoesNotExist:
|
||||
return {"found": False}
|
||||
|
||||
# 历史任务按其记录的构建平台查询,平台切换后不影响在途任务
|
||||
backend = build_backends.get_backend(gh_run.backend or 'github')
|
||||
github_log_url = backend.log_url(gh_run.github_run_id) if gh_run.github_run_id else ''
|
||||
|
||||
if gh_run.status not in ['success', 'failure', 'cancelled', 'timed_out', 'skipped'] and gh_run.github_run_id:
|
||||
try:
|
||||
info = backend.get_run(gh_run.github_run_id)
|
||||
if info is not None and info.finished:
|
||||
gh_run.status = info.status
|
||||
gh_run.save()
|
||||
except Exception as e:
|
||||
print(f"查询构建状态出错:{e}")
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
"status": gh_run.status,
|
||||
"github_log_url": github_log_url,
|
||||
"gh_run": gh_run
|
||||
}
|
||||
|
||||
|
||||
@login_required
|
||||
def generator_view(request):
|
||||
# 版本下拉始终刷新为官方仓库最新 tags(内部带缓存与内置兜底,不会拖慢页面)
|
||||
version_choices = rdeskVersions.version_choices()
|
||||
if request.method == 'POST':
|
||||
form = GenerateForm(request.POST, request.FILES)
|
||||
form.fields['version'].choices = version_choices
|
||||
if form.is_valid():
|
||||
params = form.cleaned_data
|
||||
full_url = f"{app_settings.get_value('PROTOCOL')}://{request.get_host()}"
|
||||
result = generate_custom_client(params, full_url, user=request.user)
|
||||
if result['success']:
|
||||
status_label, status_badge = _status_extra("Starting generator...please wait")
|
||||
return render(request, 'waiting.html', {
|
||||
'filename': result['filename'],
|
||||
'uuid': result['uuid'],
|
||||
'status': "Starting generator...please wait",
|
||||
'status_label': status_label,
|
||||
'status_badge': status_badge,
|
||||
'platform': result['platform'],
|
||||
'log_url': result['log_url']
|
||||
})
|
||||
else:
|
||||
messages.error(request, result['error'])
|
||||
else:
|
||||
messages.error(request, "表单校验未通过,请根据下方提示修正后重新提交。")
|
||||
else:
|
||||
form = GenerateForm()
|
||||
form.fields['version'].choices = version_choices
|
||||
|
||||
_, version_source, version_ts = rdeskVersions.get_versions()
|
||||
profiles = request.user.saved_configs.all().values('id', 'name')
|
||||
return render(request, 'generator.html', {
|
||||
'form': form,
|
||||
'profiles': profiles,
|
||||
'version_source': version_source,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def check_for_file(request):
|
||||
filename = request.GET.get('filename')
|
||||
uuid_val = request.GET.get('uuid')
|
||||
platform = request.GET.get('platform')
|
||||
|
||||
result = _get_run_status(uuid_val)
|
||||
if not result['found']:
|
||||
raise Http404("未找到对应的构建任务")
|
||||
|
||||
gh_run = result['gh_run']
|
||||
if not _can_access_run(request, gh_run):
|
||||
raise PermissionDenied("无权查看该构建任务")
|
||||
|
||||
github_log_url = result['github_log_url']
|
||||
status_label, status_badge = _status_extra(gh_run.status)
|
||||
|
||||
context = {
|
||||
'filename': filename,
|
||||
'uuid': uuid_val,
|
||||
'platform': platform,
|
||||
'status': gh_run.status,
|
||||
'status_label': status_label,
|
||||
'status_badge': status_badge,
|
||||
'log_url': github_log_url,
|
||||
}
|
||||
|
||||
if gh_run.status == "success":
|
||||
return render(request, 'generated.html', context)
|
||||
elif gh_run.status in ['failure', 'cancelled', 'timed_out', 'skipped', 'action_required']:
|
||||
return render(request, 'failure.html', context)
|
||||
else:
|
||||
return render(request, 'waiting.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def download(request):
|
||||
filename = request.GET.get('filename', '')
|
||||
uuid_val = request.GET.get('uuid', '')
|
||||
if not filename or not uuid_val:
|
||||
raise Http404
|
||||
|
||||
gh_run = GithubRun.objects.filter(uuid=uuid_val).first()
|
||||
if gh_run is None:
|
||||
raise Http404("未找到对应的构建任务")
|
||||
if not _can_access_run(request, gh_run):
|
||||
raise PermissionDenied("无权下载该文件")
|
||||
|
||||
# 防路径穿越:最终路径必须位于 exe/<uuid>/ 目录内
|
||||
base_dir = os.path.abspath(os.path.join('exe', uuid_val))
|
||||
file_path = os.path.abspath(os.path.join(base_dir, os.path.basename(filename)))
|
||||
if not file_path.startswith(base_dir + os.sep) or not os.path.isfile(file_path):
|
||||
raise Http404("文件不存在或尚未生成完成")
|
||||
|
||||
from django.http import FileResponse
|
||||
return FileResponse(
|
||||
open(file_path, 'rb'),
|
||||
content_type='application/octet-stream',
|
||||
as_attachment=True,
|
||||
filename=os.path.basename(filename),
|
||||
)
|
||||
|
||||
|
||||
def get_png(request):
|
||||
"""供 GitHub Actions 工作流下载构建所用的图片资源。"""
|
||||
filename = request.GET.get('filename', '')
|
||||
uuid_val = request.GET.get('uuid', '')
|
||||
if not filename or not uuid_val:
|
||||
raise Http404
|
||||
|
||||
base_dir = os.path.abspath(os.path.join('png', uuid_val))
|
||||
file_path = os.path.abspath(os.path.join(base_dir, os.path.basename(filename)))
|
||||
if not file_path.startswith(base_dir + os.sep) or not os.path.isfile(file_path):
|
||||
raise Http404("图片不存在")
|
||||
|
||||
from django.http import FileResponse
|
||||
return FileResponse(open(file_path, 'rb'), content_type='image/png')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def update_github_run(request):
|
||||
"""GitHub Actions 回调:更新构建状态。"""
|
||||
denied = _webhook_denied(request)
|
||||
if denied:
|
||||
return denied
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return HttpResponse(status=400)
|
||||
myuuid = data.get('uuid')
|
||||
mystatus = data.get('status')
|
||||
if not myuuid or not mystatus:
|
||||
return HttpResponse(status=400)
|
||||
GithubRun.objects.filter(Q(uuid=myuuid)).update(status=mystatus)
|
||||
return HttpResponse('')
|
||||
|
||||
|
||||
def resize_and_encode_icon(imagefile):
|
||||
maxWidth = 200
|
||||
try:
|
||||
with io.BytesIO() as image_buffer:
|
||||
for chunk in imagefile.chunks():
|
||||
image_buffer.write(chunk)
|
||||
image_buffer.seek(0)
|
||||
|
||||
img = Image.open(image_buffer)
|
||||
imgcopy = img.copy()
|
||||
except (IOError, OSError):
|
||||
raise ValueError("上传的文件不是有效的图片格式。")
|
||||
|
||||
# 无需缩放时直接返回
|
||||
if img.size[0] <= maxWidth:
|
||||
with io.BytesIO() as image_buffer:
|
||||
imgcopy.save(image_buffer, format=imagefile.content_type.split('/')[1])
|
||||
image_buffer.seek(0)
|
||||
return_image = ContentFile(image_buffer.read(), name=imagefile.name)
|
||||
return base64.b64encode(return_image.read())
|
||||
|
||||
# 等比例缩放
|
||||
wpercent = (maxWidth / float(img.size[0]))
|
||||
hsize = int((float(img.size[1]) * float(wpercent)))
|
||||
|
||||
# LANCZOS 高质量重采样
|
||||
imgcopy = imgcopy.resize((maxWidth, hsize), Image.Resampling.LANCZOS)
|
||||
|
||||
with io.BytesIO() as resized_image_buffer:
|
||||
imgcopy.save(resized_image_buffer, format=imagefile.content_type.split('/')[1])
|
||||
resized_image_buffer.seek(0)
|
||||
|
||||
resized_imagefile = ContentFile(resized_image_buffer.read(), name=imagefile.name)
|
||||
|
||||
resized64 = base64.b64encode(resized_imagefile.read())
|
||||
return resized64
|
||||
|
||||
# 以下接口供外部来源(如自建 RustDesk API 服务器)调用
|
||||
@csrf_exempt
|
||||
def startgh(request):
|
||||
denied = _webhook_denied(request)
|
||||
if denied:
|
||||
return denied
|
||||
data_ = json.loads(request.body)
|
||||
#### 触发构建平台工作流(GitHub / Gitea 按后台配置切换)
|
||||
backend = build_backends.get_backend()
|
||||
workflow_file = 'generator-' + str(data_.get('platform')) + '.yml'
|
||||
inputs = {
|
||||
"server":data_.get('server'),
|
||||
"key":data_.get('key'),
|
||||
"apiServer":data_.get('apiServer'),
|
||||
"custom":data_.get('custom'),
|
||||
"uuid":data_.get('uuid'),
|
||||
"iconlink":data_.get('iconlink'),
|
||||
"logolink":data_.get('logolink'),
|
||||
"appname":data_.get('appname'),
|
||||
"extras":data_.get('extras'),
|
||||
"filename":data_.get('filename')
|
||||
}
|
||||
try:
|
||||
result = backend.dispatch(workflow_file, inputs, timeout=20)
|
||||
print(result)
|
||||
except build_backends.base.BackendError as e:
|
||||
# 历史行为:本端点无论成败都返回 204,仅记录日志
|
||||
print(f"startgh 触发失败:{e.message}")
|
||||
return HttpResponse(status=204)
|
||||
|
||||
def save_png(file, uuid, domain, name):
|
||||
file_save_path = "png/%s/%s" % (uuid, name)
|
||||
Path("png/%s" % uuid).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if isinstance(file, str): # base64 字符串
|
||||
try:
|
||||
header, encoded = file.split(';base64,')
|
||||
decoded_img = base64.b64decode(encoded)
|
||||
file = ContentFile(decoded_img, name=name) # 类文件对象
|
||||
except ValueError:
|
||||
print("base64 数据无效")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"base64 解码出错:{e}")
|
||||
return None
|
||||
|
||||
with open(file_save_path, "wb+") as f:
|
||||
for chunk in file.chunks():
|
||||
f.write(chunk)
|
||||
return domain, uuid, name
|
||||
|
||||
@csrf_exempt
|
||||
def save_custom_client(request):
|
||||
"""GitHub Actions 回调:上传构建产物。"""
|
||||
denied = _webhook_denied(request)
|
||||
if denied:
|
||||
return denied
|
||||
file = request.FILES.get('file')
|
||||
myuuid = request.POST.get('uuid')
|
||||
if not file or not myuuid:
|
||||
return HttpResponse("缺少文件或任务 UUID", status=400)
|
||||
file_save_path = "exe/%s/%s" % (myuuid, os.path.basename(file.name))
|
||||
Path("exe/%s" % myuuid).mkdir(parents=True, exist_ok=True)
|
||||
with open(file_save_path, "wb+") as f:
|
||||
for chunk in file.chunks():
|
||||
f.write(chunk)
|
||||
|
||||
return HttpResponse("文件保存成功")
|
||||
|
||||
@csrf_exempt
|
||||
def cleanup_secrets(request):
|
||||
"""GitHub Actions 回调:清理加密的临时配置包。"""
|
||||
denied = _webhook_denied(request)
|
||||
if denied:
|
||||
return denied
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return HttpResponse("请求体不是有效的 JSON", status=400)
|
||||
my_uuid = data.get('uuid')
|
||||
|
||||
if not my_uuid:
|
||||
return HttpResponse("缺少任务 UUID", status=400)
|
||||
|
||||
temp_dir = os.path.join('temp_zips')
|
||||
if not os.path.isdir(temp_dir):
|
||||
return HttpResponse("清理完成", status=200)
|
||||
|
||||
for filename in os.listdir(temp_dir):
|
||||
if my_uuid in filename and filename.endswith('.zip'):
|
||||
file_path = os.path.join(temp_dir, filename)
|
||||
try:
|
||||
os.remove(file_path)
|
||||
print(f"已删除 {file_path}")
|
||||
except OSError as e:
|
||||
print(f"删除文件失败:{e}")
|
||||
|
||||
return HttpResponse("清理完成", status=200)
|
||||
|
||||
def get_zip(request):
|
||||
"""供 GitHub Actions 工作流下载 AES 加密的构建配置包。"""
|
||||
filename = request.GET.get('filename', '')
|
||||
base_dir = os.path.abspath('temp_zips')
|
||||
file_path = os.path.abspath(os.path.join(base_dir, os.path.basename(filename)))
|
||||
if not file_path.startswith(base_dir + os.sep) or not os.path.isfile(file_path):
|
||||
return HttpResponseForbidden("无效的文件名")
|
||||
from django.http import FileResponse
|
||||
return FileResponse(
|
||||
open(file_path, 'rb'),
|
||||
content_type='application/zip',
|
||||
as_attachment=True,
|
||||
filename=os.path.basename(filename),
|
||||
)
|
||||
Reference in New Issue
Block a user