- find/listdir/r/sound/SendWX 及 WARN.mp3 移入 tools/(sound.py 相对路径同目录保持可用) - Version 死代码(import 自增 VER)归档 tools/Version - Main.py(生产入口候选)与 TODO 保留根目录待确认
287 lines
11 KiB
Python
287 lines
11 KiB
Python
import os
|
||
import mimetypes
|
||
from pathlib import Path
|
||
from django.http import HttpResponse, Http404, JsonResponse, HttpResponseRedirect
|
||
from django.views import View
|
||
from django.conf import settings
|
||
from gvsds.template_engine import render_template
|
||
|
||
TEMPLATE_DIR = Path(settings.BASE_DIR) / "TempLate"
|
||
|
||
MIME_TYPES = {
|
||
'.js': 'application/javascript',
|
||
'.mjs': 'application/javascript',
|
||
'.css': 'text/css',
|
||
'.png': 'image/png',
|
||
'.jpg': 'image/jpeg',
|
||
'.jpeg': 'image/jpeg',
|
||
'.gif': 'image/gif',
|
||
'.svg': 'image/svg+xml',
|
||
'.ico': 'image/x-icon',
|
||
'.woff': 'font/woff',
|
||
'.woff2': 'font/woff2',
|
||
'.ttf': 'font/ttf',
|
||
'.eot': 'application/vnd.ms-fontobject',
|
||
'.json': 'application/json',
|
||
'.xml': 'application/xml',
|
||
'.html': 'text/html',
|
||
'.htm': 'text/html',
|
||
'.txt': 'text/plain',
|
||
'.pdf': 'application/pdf',
|
||
'.zip': 'application/zip',
|
||
'.mp4': 'video/mp4',
|
||
'.webp': 'image/webp',
|
||
'.map': 'application/json',
|
||
}
|
||
|
||
SUBDOMAIN_CONFIG = {
|
||
'www': {'dir': '', 'root_is_index': True},
|
||
'gerp': {'dir': 'GERP', 'root_is_index': True},
|
||
'ugm': {'dir': '', 'root_is_index': True},
|
||
'api': {'dir': None, 'root_is_index': False},
|
||
'mc': {'dir': 'MC', 'root_is_index': True},
|
||
'3crb': {'dir': 'CCCRB', 'root_is_index': True},
|
||
'ccgo': {'dir': 'CCGO', 'root_is_index': True},
|
||
'invite': {'dir': '', 'root_is_index': True},
|
||
'doc': {'dir': '', 'root_is_index': True},
|
||
'discuss': {'dir': '', 'root_is_index': True},
|
||
'community': {'dir': '', 'root_is_index': True},
|
||
'comm': {'dir': '', 'root_is_index': True},
|
||
't4': {'dir': None, 'root_is_index': False},
|
||
'verify': {'dir': 'Verify', 'root_is_index': True},
|
||
'verifox': {'dir': 'Verifox', 'root_is_index': True},
|
||
'nav': {'dir': 'nav', 'root_is_index': True},
|
||
'history': {'dir': 'history', 'root_is_index': True},
|
||
'cdn': {'dir': 'cdn', 'root_is_index': False, 'root_text': 'GVSDS CDN'},
|
||
'yc8z': {'dir': 'yc8z', 'root_is_index': True},
|
||
'omc': {'dir': 'OMC', 'root_is_index': True},
|
||
'sve': {'dir': 'SVE', 'root_is_index': True},
|
||
'tailwind': {'dir': 'tailwindcss', 'root_is_index': False, 'root_redirect': '/3.4.17'},
|
||
'nanjingmassacre': {'dir': 'NanjingMassacre', 'root_is_index': True},
|
||
'3c': {'dir': 'CreateECentralCourt', 'root_is_index': True},
|
||
'vu': {'dir': None, 'root_is_index': False},
|
||
'links': {'dir': None, 'root_is_index': False},
|
||
'unclear': {'dir': 'Unclear', 'root_is_index': True},
|
||
}
|
||
|
||
|
||
def _get_subdomain(request):
|
||
return getattr(request, 'subdomain', 'www') or 'www'
|
||
|
||
|
||
def _get_subdomain_dir(subdomain):
|
||
config = SUBDOMAIN_CONFIG.get(subdomain, {})
|
||
return config.get('dir', '')
|
||
|
||
|
||
def _resolve_file(subdomain, file_path):
|
||
if '..' in file_path.split('/') or '..' in file_path.split(os.sep):
|
||
return None
|
||
subdomain_dir = _get_subdomain_dir(subdomain)
|
||
if subdomain_dir is None:
|
||
return None
|
||
if subdomain_dir:
|
||
candidate = TEMPLATE_DIR / subdomain_dir / file_path
|
||
try:
|
||
candidate.resolve().relative_to(TEMPLATE_DIR.resolve())
|
||
except ValueError:
|
||
return None
|
||
if candidate.is_file():
|
||
return candidate
|
||
candidate = TEMPLATE_DIR / file_path
|
||
try:
|
||
candidate.resolve().relative_to(TEMPLATE_DIR.resolve())
|
||
except ValueError:
|
||
return None
|
||
if candidate.is_file():
|
||
return candidate
|
||
return None
|
||
|
||
|
||
def _serve_file(full_path, subdomain=None):
|
||
ext = full_path.suffix.lower()
|
||
content_type = MIME_TYPES.get(ext, mimetypes.guess_type(str(full_path))[0] or 'application/octet-stream')
|
||
|
||
if ext in ('.html', '.htm'):
|
||
rel_path = full_path.relative_to(TEMPLATE_DIR)
|
||
template_name = str(rel_path).replace('\\', '/')
|
||
base = None
|
||
use_base = False
|
||
if subdomain:
|
||
config = SUBDOMAIN_CONFIG.get(subdomain, {})
|
||
base = config.get('base')
|
||
use_base = config.get('use_base', False)
|
||
if base:
|
||
response = render_template(request=None, template_name=template_name, UseBase='false', SubBase=base)
|
||
elif not use_base:
|
||
response = render_template(request=None, template_name=template_name, UseBase='false')
|
||
else:
|
||
response = render_template(request=None, template_name=template_name, UseBase='true')
|
||
# HTML 永不缓存:Vue SPA 的 index.html 引用了带 hash 的资源名,
|
||
# 若被浏览器/代理缓存,会引用已删除的旧资源导致 404、页面加载不全
|
||
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
|
||
return response
|
||
|
||
with open(full_path, 'rb') as f:
|
||
response = HttpResponse(f.read(), content_type=content_type)
|
||
|
||
if ext in ('.css', '.js', '.mjs', '.json', '.xml', '.txt', '.svg'):
|
||
response['Cache-Control'] = 'public, max-age=3600'
|
||
elif ext in ('.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.mp4'):
|
||
response['Cache-Control'] = 'public, max-age=86400'
|
||
|
||
return response
|
||
|
||
|
||
# ============================================================================
|
||
# doc 子域 JSON API(页面由 GVSite SPA 渲染)
|
||
# /API/GetDoc/<uuid> → 文章 JSON
|
||
# /API/GetSerieDirectory/<uuid> → 系列目录 / 单文档标记 JSON
|
||
# ============================================================================
|
||
|
||
|
||
def _doc_api(file_path):
|
||
"""doc 子域 JSON API:API/GetDoc/<uuid> 与 API/GetSerieDirectory/<uuid>。"""
|
||
from gvsds.apps.doc.manager import DocManager
|
||
from gvsdsdk.model_utils import UUIDObj
|
||
|
||
parts = file_path.split('/')
|
||
if len(parts) < 3 or parts[0] != 'API':
|
||
return None
|
||
action = parts[1]
|
||
uuid_text = parts[2].strip('/')
|
||
try:
|
||
uuid_bytes = UUIDObj(uuid_text).bytes
|
||
except (ValueError, TypeError, AttributeError):
|
||
return JsonResponse({'error': 'invalid_uuid'}, status=404)
|
||
|
||
if action == 'GetDoc':
|
||
doc = DocManager.GetDocument(uuid_bytes)
|
||
if doc is None:
|
||
return JsonResponse({'error': 'not_found'}, status=404)
|
||
return JsonResponse({
|
||
'DocTitle': doc.DocTitle or '未命名文档',
|
||
'DocContent': doc.DocContent or '',
|
||
'DocFormat': doc.DocFormat,
|
||
'DocPermission': 1,
|
||
'DocDecrypt': True,
|
||
})
|
||
|
||
if action == 'GetSerieDirectory':
|
||
from gvsds.apps.doc.models import CommunityDocument, CommunitySeries
|
||
try:
|
||
serie = CommunitySeries.objects.get(DocSerieUUID=uuid_bytes)
|
||
except CommunitySeries.DoesNotExist:
|
||
serie = None
|
||
if serie is not None:
|
||
docs = CommunityDocument.objects.filter(DocSerieUUID=uuid_bytes)
|
||
chapters = [
|
||
{
|
||
'id': UUIDObj(d.DocUUID).str.replace('-', ''),
|
||
'title': d.DocTitle or '未命名文档',
|
||
'sections': [],
|
||
}
|
||
for d in docs
|
||
]
|
||
return JsonResponse({
|
||
'title': serie.DocSerieName or '未命名系列',
|
||
'document': False,
|
||
'chapters': chapters,
|
||
})
|
||
# 非系列 UUID:单文档模式,返回所属系列的全部文章供侧边栏展示
|
||
doc = DocManager.GetDocument(uuid_bytes)
|
||
if doc is None:
|
||
return JsonResponse({'error': 'not_found'}, status=404)
|
||
chapters = []
|
||
if doc.DocSerieUUID:
|
||
serie_docs = CommunityDocument.objects.filter(DocSerieUUID=doc.DocSerieUUID)
|
||
chapters = [
|
||
{
|
||
'id': UUIDObj(d.DocUUID).str.replace('-', ''),
|
||
'title': d.DocTitle or '未命名文档',
|
||
'sections': [],
|
||
}
|
||
for d in serie_docs
|
||
]
|
||
return JsonResponse({
|
||
'title': doc.DocTitle or '未命名文档',
|
||
'document': True,
|
||
'id': UUIDObj(doc.DocUUID).str.replace('-', ''),
|
||
'chapters': chapters,
|
||
})
|
||
|
||
return None
|
||
|
||
|
||
class SubdomainView(View):
|
||
def get(self, request, file_path=''):
|
||
subdomain = _get_subdomain(request)
|
||
config = SUBDOMAIN_CONFIG.get(subdomain, {})
|
||
|
||
if config.get('dir') is None:
|
||
raise Http404
|
||
|
||
# doc 子域:动态阅读 JSON API(页面由 GVSite SPA 渲染,走下方通用静态/SPA fallback)
|
||
# doc.gvsds.com/API/GetDoc/<uuid> → 文章 JSON
|
||
# doc.gvsds.com/API/GetSerieDirectory/<uuid> → 目录 JSON
|
||
if subdomain == 'doc' and file_path and file_path.startswith('API/'):
|
||
api_resp = _doc_api(file_path)
|
||
if api_resp:
|
||
return api_resp
|
||
|
||
# ugm 子域:未登录则跳转登录页,已登录则跳转 gerp
|
||
if subdomain == 'ugm':
|
||
# JWT Cookie 认证:检查 AccessToken 或 RefreshToken
|
||
has_jwt = request.COOKIES.get('AccessToken') or request.COOKIES.get('RefreshToken')
|
||
if not has_jwt:
|
||
# 未登录 → 跳转 www 登录页
|
||
if file_path and file_path not in ('/', 'login.html', 'register.html'):
|
||
login_url = f"https://www.gvsds.com/login?redirect=https://gerp.gvsds.com/"
|
||
return HttpResponseRedirect(login_url)
|
||
if not file_path or file_path == '/':
|
||
return HttpResponseRedirect('https://www.gvsds.com/login')
|
||
else:
|
||
# 已登录 → 跳转 gerp
|
||
if not file_path or file_path == '/' or file_path == 'index.html':
|
||
return HttpResponseRedirect('https://gerp.gvsds.com/')
|
||
|
||
if not file_path or file_path == '/':
|
||
if config.get('root_redirect'):
|
||
return HttpResponseRedirect(config['root_redirect'])
|
||
if config.get('root_text') is not None:
|
||
return HttpResponse(config['root_text'])
|
||
if config.get('root_is_index'):
|
||
file_path = 'index.html'
|
||
else:
|
||
raise Http404
|
||
|
||
if file_path.endswith('/'):
|
||
file_path += 'index.html'
|
||
|
||
if subdomain == 'tailwind' and not file_path.endswith('.js') and not file_path.endswith('.json'):
|
||
js_path = _resolve_file(subdomain, file_path + '.js')
|
||
if js_path:
|
||
return _serve_file(js_path, subdomain)
|
||
|
||
resolved = _resolve_file(subdomain, file_path)
|
||
if resolved:
|
||
return _serve_file(resolved, subdomain)
|
||
|
||
# www/invite 子域:旧 .html 页面已被 Vue SPA 取代,去掉后缀重定向到 Vue 路由
|
||
# 兼容历史链接(如 /login.html、/gallery.html 等被外部页面/脚本引用的旧地址)
|
||
if subdomain in ('www', 'invite') and file_path.endswith('.html'):
|
||
new_path = file_path[:-5] or '/'
|
||
redirect_url = new_path
|
||
if request.META.get('QUERY_STRING'):
|
||
redirect_url += '?' + request.META['QUERY_STRING']
|
||
return HttpResponseRedirect(redirect_url)
|
||
|
||
# SPA fallback:前端单页应用(如 GERP Vue)的客户端路由路径(无扩展名)
|
||
# 找不到对应文件时,回退到 index.html,由前端路由处理
|
||
if config.get('root_is_index') and '.' not in os.path.basename(file_path):
|
||
index_resolved = _resolve_file(subdomain, 'index.html')
|
||
if index_resolved:
|
||
return _serve_file(index_resolved, subdomain)
|
||
|
||
raise Http404
|