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/_<原始文件名>) 2. ./resource//// —— 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//// 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'})