Files
TermiNexus fbf1bb7d6e refactor(gerp): 应用按子应用目录规约重组,清单外置并拆分路由
- 新增 gvsds/apps/gerp/apps/<appid>/:talk/mailbox/gitea/oa/ehrm/dingcalc_finance/
  product_order/shop/store_value/wallet/enterprise,各应用自带 models/manager/views/urls
  与 apps.gerp_manifests(JSON 清单,$schema 指向 apps/gerp_manifests.schema.json)
- 删除 app_manifests.py 内联 _LEGACY_MANIFESTS,改为扫描各应用清单文件(未知键/缺必填键即判非法并跳过)
- 路由按应用拆:/API/Mail/、/API/Gitea/、/API/Oa/ 由扁平直挂改为 include 子应用 urls.py
  (reverse() 断言 19 个 name 的路径逐字不变)
- AppConfig 移至 gerp_app.py 并改显式点分路径注册,规避 apps/ 目录与 Django <app>.apps 同名冲突
- gerp/models.py 裁剪已搬迁模型(保留 _NewUuidBytes 与平台模型),修复 views/__init__.py 与 oidc.py 导入
- AssetDownloadView:优先按 FileAsset.StoragePath 定位并回退 ./resource 旧布局,修上传成功却下载 404
- 同批落地 Talk 应用后端(app 已迁入 apps/talk,gerp.0046 状态认领迁移)
- 注意:app.ini 被 .gitignore 忽略,INSTALLED_APPS 需在部署环境手工同步 gvsds.apps.gerp.gerp_app.GerpConfig
2026-09-26 23:46:00 +08:00

102 lines
3.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import re
import json
import hashlib
from pathlib import Path
from django.http import HttpResponse, JsonResponse, FileResponse
from django.views import View
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny
class IPInfoView(APIView):
permission_classes = [AllowAny]
def get(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '')
ip = x_forwarded_for.split(',')[0].strip() if x_forwarded_for else request.META.get('REMOTE_ADDR', '')
return Response({'code': 0, 'data': {'ip': ip}})
class AssetDownloadView(View):
"""文件资产下载(按 SHA1)
取物理文件的顺序(先新后旧):
1. FileAsset.StoragePath —— 当前上传链路(/API/Asset/Upload*)的落地位置
(MEDIA_ROOT/assets/<uploadId>_<原始文件名>)
2. ./resource/<aa>/<bb>/<cc>/<sha1> —— Flask 期遗留布局,保持向后兼容
历史问题:上传写 MEDIA_ROOT/assets,而本视图原先只读 ./resource 布局,
导致上传成功却下载 404(资产管理页与 Talk 图片/文件消息均受影响)。
"""
def get(self, request, sha1):
# 验证 sha1 参数格式,防止路径遍历
if not re.match(r'^[0-9a-fA-F]{40}$', sha1):
return HttpResponse('Invalid hash', status=400)
resolved = _ResolveAssetFile(sha1)
if resolved is None:
return JsonResponse({'code': 404, 'msg': '文件不存在'}, status=404)
file_path, file_name = resolved
return FileResponse(open(file_path, 'rb'), as_attachment=True, filename=file_name or sha1)
#: 允许下载的目录白名单(解析后必须落在其中,防路径遍历)
def _ResolveAssetFile(sha1):
"""解析资产物理文件:返回 (Path, 下载文件名);找不到返回 None
先在 FileAsset 表按 SHA1 查 StoragePath(并校验路径落在允许目录内),
再回退到 ./resource 分段布局。
"""
from django.conf import settings as _settings
media_root = Path(getattr(_settings, 'MEDIA_ROOT', '.')).resolve()
resource_root = Path('./resource').resolve()
allowed_roots = (media_root, resource_root)
def _InAllowed(p):
try:
resolved = p.resolve()
except OSError:
return None
for root in allowed_roots:
try:
resolved.relative_to(root)
return resolved
except ValueError:
continue
return None
# 1) 当前上传链路:FileAsset.StoragePath
try:
from gvsds.apps.gerp.models import FileAsset
asset = FileAsset.objects.filter(FileSHA1=sha1).first()
if asset and asset.StoragePath:
stored = Path(asset.StoragePath)
for candidate in (stored, (media_root / str(asset.StoragePath).lstrip('/\\'))):
safe = _InAllowed(candidate)
if safe is not None and safe.is_file():
return safe, asset.FileName or sha1
except Exception:
# 查库失败不能影响旧布局回退(保持下载可用)
import logging
logging.getLogger(__name__).warning('资产按 StoragePath 定位失败:%s', sha1, exc_info=True)
# 2) Flask 期遗留布局:./resource/<aa>/<bb>/<cc>/<sha1>
legacy = _InAllowed(resource_root / sha1[0:2] / sha1[2:4] / sha1[4:6] / sha1)
if legacy is not None and legacy.is_file():
return legacy, sha1
return None
class HealthCheckView(APIView):
permission_classes = [AllowAny]
def get(self, request):
return Response({'code': 0, 'msg': 'ok', 'service': 'gvsds-django'})