65 lines
3.1 KiB
Plaintext
65 lines
3.1 KiB
Plaintext
# project_file.dspy - 按项目ID提供产出文件的打开/下载(待办里跨项目直达交付件文件)
|
||
# 入参:project_id=<项目id>,path=<文件路径>(项目目录内相对路径 或 项目目录内的绝对路径),
|
||
# download=1 强制下载。
|
||
# 安全:仅登录用户;解析后校验文件真实路径必须落在该项目目录内(防路径穿越/跨项目越权)。
|
||
|
||
import os
|
||
from urllib.parse import quote
|
||
from aiohttp.web_fileresponse import FileResponse
|
||
|
||
uid = await get_user()
|
||
if not uid:
|
||
return {"widgettype": "Message", "options": {"title": "未登录", "message": "请先登录"}}
|
||
|
||
project_id = ((params_kw or {}).get('project_id') or '').strip()
|
||
fpath = ((params_kw or {}).get('path') or '').strip()
|
||
download = ((params_kw or {}).get('download') or '').strip()
|
||
|
||
if not project_id:
|
||
return {"widgettype": "Message", "options": {"title": "错误", "message": "缺少 project_id"}}
|
||
if not fpath:
|
||
return {"widgettype": "Message", "options": {"title": "错误", "message": "缺少文件路径"}}
|
||
|
||
dbname = get_module_dbname('pipeline-sdlc')
|
||
|
||
# 按项目ID解析项目目录(不依赖会话上下文),并做机构隔离校验
|
||
async with DBPools().sqlorContext(dbname) as sor:
|
||
precs = await sor.sqlExe(
|
||
"SELECT id, name, org_id FROM sd_projects WHERE id=${p}$ LIMIT 1", {"p": project_id})
|
||
await sor.sqlExe("COMMIT", {})
|
||
if not precs:
|
||
return {"widgettype": "Message", "options": {"title": "错误", "message": "项目不存在"}}
|
||
proj_org = getattr(precs[0], 'org_id', '0') or '0'
|
||
project_dir, workspace_base = await get_project_dir_by_id(sor, project_id)
|
||
|
||
if not project_dir or not os.path.isdir(project_dir):
|
||
return {"widgettype": "Message", "options": {"title": "错误", "message": "项目目录不可用"}}
|
||
|
||
# 机构隔离:非超管(0)只能访问本机构项目(项目为通用机构0时登录即可见)
|
||
try:
|
||
uorg = await get_userorgid()
|
||
except Exception:
|
||
uorg = ''
|
||
if str(uorg) != '0' and str(proj_org) != '0' and str(uorg) != str(proj_org):
|
||
return {"widgettype": "Message", "options": {"title": "无权限", "message": "无权访问其他机构的项目文件"}}
|
||
|
||
# 路径解析:绝对路径直接用;相对路径拼项目目录
|
||
cand = fpath if fpath.startswith('/') else os.path.join(project_dir, fpath)
|
||
|
||
# 路径穿越校验:真实路径必须落在项目目录内
|
||
real_proj = os.path.realpath(project_dir)
|
||
real_full = os.path.realpath(cand)
|
||
if not real_full.startswith(real_proj + os.sep):
|
||
return {"widgettype": "Message", "options": {"title": "错误", "message": "非法路径(不在项目空间内)"}}
|
||
if not os.path.isfile(real_full):
|
||
return {"widgettype": "Message", "options": {"title": "文件不存在",
|
||
"message": "文件未找到:" + os.path.basename(cand) + "(可能尚未生成或已删除)"}}
|
||
|
||
headers = {}
|
||
if download:
|
||
filename = os.path.basename(real_full)
|
||
safe_name = quote(filename)
|
||
headers['Content-Disposition'] = 'attachment; filename="%s"; filename*=UTF-8\'\'%s' % (filename, safe_name)
|
||
|
||
return FileResponse(real_full, headers=headers)
|