This commit is contained in:
@@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
||||
'accounts',
|
||||
'main',
|
||||
'elastic',
|
||||
'minio_storage',
|
||||
'django_elasticsearch_dsl',
|
||||
]
|
||||
|
||||
|
||||
@@ -328,7 +328,7 @@ function renderTable(data) {
|
||||
row.innerHTML = `
|
||||
<td style="max-width:140px; word-break:break-all; font-size: 12px;">${item._id || item.id || ''}</td>
|
||||
<td>
|
||||
${item.image ? `<img src="/media/${item.image}" onerror="this.src=''; this.alt='图片加载失败'" class="clickable-image" data-image="/media/${item.image}" />` : '无图片'}
|
||||
${item.image_url ? `<img src="${item.image_url}" onerror="this.src=''; this.alt='图片加载失败'" class="clickable-image" data-image="${item.image_url}" />` : '无图片'}
|
||||
</td>
|
||||
<td>
|
||||
<pre style="white-space:pre-wrap; word-wrap:break-word; max-height: 100px; overflow-y: auto; font-size: 12px; margin: 0;">${escapeHtml(displayData)}</pre>
|
||||
|
||||
@@ -23,6 +23,37 @@ from .es_connect import (
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def _image_ref_to_url(request, image_ref: str) -> str:
|
||||
s = str(image_ref or '').strip()
|
||||
if not s:
|
||||
return ''
|
||||
|
||||
if not s.startswith('minio:'):
|
||||
return ''
|
||||
|
||||
object_name = s[len('minio:'):].lstrip('/')
|
||||
if not object_name:
|
||||
return ''
|
||||
|
||||
try:
|
||||
from minio_storage.minio_connect import presigned_get_url
|
||||
return presigned_get_url(object_name, expires_seconds=8 * 60 * 60)
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
|
||||
def _attach_image_urls(request, items):
|
||||
out = []
|
||||
for it in list(items or []):
|
||||
try:
|
||||
d = dict(it or {})
|
||||
except Exception:
|
||||
continue
|
||||
d['image_url'] = _image_ref_to_url(request, d.get('image', ''))
|
||||
out.append(d)
|
||||
return out
|
||||
|
||||
|
||||
@require_http_methods(["GET", "POST"])
|
||||
@csrf_exempt
|
||||
def init_index(request):
|
||||
@@ -59,7 +90,7 @@ def search(request):
|
||||
return JsonResponse({"status": "error", "message": "搜索关键词不能为空"}, status=400)
|
||||
|
||||
results = search_data(query)
|
||||
return JsonResponse({"status": "success", "data": results})
|
||||
return JsonResponse({"status": "success", "data": _attach_image_urls(request, results)})
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@@ -73,7 +104,7 @@ def fuzzy_search(request):
|
||||
return JsonResponse({"status": "error", "message": "搜索关键词不能为空"}, status=400)
|
||||
|
||||
results = search_by_any_field(keyword)
|
||||
return JsonResponse({"status": "success", "data": results})
|
||||
return JsonResponse({"status": "success", "data": _attach_image_urls(request, results)})
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@@ -82,7 +113,7 @@ def get_all_data(request):
|
||||
"""获取所有数据"""
|
||||
try:
|
||||
results = search_all()
|
||||
return JsonResponse({"status": "success", "data": results})
|
||||
return JsonResponse({"status": "success", "data": _attach_image_urls(request, results)})
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": str(e)}, status=500)
|
||||
|
||||
@@ -176,7 +207,9 @@ def get_data(request, doc_id):
|
||||
try:
|
||||
result = get_by_id(doc_id)
|
||||
if result:
|
||||
return JsonResponse({"status": "success", "data": result})
|
||||
wrapped = dict(result)
|
||||
wrapped['image_url'] = _image_ref_to_url(request, wrapped.get('image', ''))
|
||||
return JsonResponse({"status": "success", "data": wrapped})
|
||||
else:
|
||||
return JsonResponse({"status": "error", "message": "数据不存在"}, status=404)
|
||||
except Exception as e:
|
||||
@@ -473,29 +506,45 @@ def confirm(request):
|
||||
return JsonResponse({"status": "error", "message": "数据不能为空"}, status=400)
|
||||
|
||||
ensure_type_in_list(edited.get("数据类型"))
|
||||
final_image_rel = image_rel
|
||||
try:
|
||||
image_ref_to_store = ""
|
||||
temp_files_to_delete = []
|
||||
if image_rel:
|
||||
images_dir = os.path.join(settings.MEDIA_ROOT, "images")
|
||||
os.makedirs(images_dir, exist_ok=True)
|
||||
src_abs = os.path.join(settings.MEDIA_ROOT, image_rel)
|
||||
base = os.path.splitext(os.path.basename(image_rel))[0]
|
||||
webp_name = base + ".webp"
|
||||
if not os.path.isfile(src_abs):
|
||||
return JsonResponse({"status": "error", "message": "图片文件不存在"}, status=400)
|
||||
|
||||
webp_name = f"{uuid.uuid4().hex}.webp"
|
||||
webp_abs = os.path.join(images_dir, webp_name)
|
||||
try:
|
||||
with Image.open(src_abs) as im:
|
||||
if im.mode in ("RGBA", "LA", "P"):
|
||||
im = im.convert("RGBA")
|
||||
else:
|
||||
im = im.convert("RGB")
|
||||
im.save(webp_abs, format="WEBP", quality=80)
|
||||
final_image_rel = f"images/{webp_name}"
|
||||
except Exception:
|
||||
final_image_rel = image_rel
|
||||
try:
|
||||
if os.path.isfile(webp_abs):
|
||||
os.remove(webp_abs)
|
||||
except Exception:
|
||||
pass
|
||||
return JsonResponse({"status": "error", "message": "图片转换WEBP失败"}, status=500)
|
||||
|
||||
try:
|
||||
object_name = f"images/{webp_name}"
|
||||
from minio_storage.minio_connect import upload_file
|
||||
upload_file(webp_abs, object_name, content_type="image/webp")
|
||||
image_ref_to_store = f"minio:{object_name}"
|
||||
temp_files_to_delete.extend([src_abs, webp_abs])
|
||||
except Exception as e:
|
||||
return JsonResponse({"status": "error", "message": f"上传到MinIO失败: {e}"}, status=500)
|
||||
|
||||
to_store = {
|
||||
"writer_id": str(request.session.get("user_id")),
|
||||
"data": json_to_string(edited),
|
||||
"image": final_image_rel,
|
||||
"image": image_ref_to_store,
|
||||
}
|
||||
|
||||
ok = insert_data(to_store)
|
||||
@@ -503,10 +552,9 @@ def confirm(request):
|
||||
return JsonResponse({"status": "error", "message": "写入ES失败"}, status=500)
|
||||
|
||||
try:
|
||||
if image_rel and final_image_rel != image_rel:
|
||||
orig_abs = os.path.join(settings.MEDIA_ROOT, image_rel)
|
||||
if os.path.isfile(orig_abs):
|
||||
os.remove(orig_abs)
|
||||
for p in temp_files_to_delete:
|
||||
if p and os.path.isfile(p):
|
||||
os.remove(p)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
1
minio_storage/__init__.py
Normal file
1
minio_storage/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
22
minio_storage/apps.py
Normal file
22
minio_storage/apps.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.apps import AppConfig
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
class MinioStorageConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'minio_storage'
|
||||
|
||||
def ready(self):
|
||||
if os.path.basename(sys.argv[0]) == 'manage.py':
|
||||
if os.environ.get('RUN_MAIN') != 'true':
|
||||
return
|
||||
if 'runserver' not in sys.argv:
|
||||
return
|
||||
|
||||
from .minio_connect import ensure_bucket_exists
|
||||
try:
|
||||
ensure_bucket_exists()
|
||||
except Exception as e:
|
||||
print(f"❌ MinIO 初始化失败: {e}")
|
||||
|
||||
129
minio_storage/minio_connect.py
Normal file
129
minio_storage/minio_connect.py
Normal file
@@ -0,0 +1,129 @@
|
||||
import os
|
||||
from datetime import timedelta
|
||||
import mimetypes
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from minio import Minio
|
||||
from minio.error import S3Error
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool = False) -> bool:
|
||||
v = os.environ.get(name)
|
||||
if v is None:
|
||||
return default
|
||||
return str(v).strip().lower() in {'1', 'true', 'yes', 'y', 'on'}
|
||||
|
||||
|
||||
def _normalize_endpoint(minio_url: str):
|
||||
if not minio_url:
|
||||
return None, None
|
||||
|
||||
u = str(minio_url).strip()
|
||||
parsed = urlparse(u)
|
||||
if parsed.scheme in {'http', 'https'}:
|
||||
endpoint = parsed.netloc
|
||||
secure = parsed.scheme == 'https'
|
||||
else:
|
||||
endpoint = u
|
||||
secure = None
|
||||
|
||||
endpoint = endpoint.strip().rstrip('/')
|
||||
return endpoint, secure
|
||||
|
||||
|
||||
def _get_env(*names: str, default: str | None = None) -> str | None:
|
||||
for n in names:
|
||||
v = os.environ.get(n)
|
||||
if v is not None and str(v).strip() != '':
|
||||
return str(v).strip()
|
||||
return default
|
||||
|
||||
|
||||
def get_minio_client() -> Minio | None:
|
||||
minio_url = _get_env('MINIO_URL', 'MINIO_ENDPOINT')
|
||||
access_key = _get_env('MINIO_ACCESS_KEY')
|
||||
secret_key = _get_env('MINIO_SECRET_KEY')
|
||||
|
||||
if not minio_url or not access_key or not secret_key:
|
||||
return None
|
||||
|
||||
endpoint, secure_from_url = _normalize_endpoint(minio_url)
|
||||
if not endpoint:
|
||||
return None
|
||||
|
||||
secure = _env_bool('MINIO_SECURE', default=secure_from_url if secure_from_url is not None else False)
|
||||
region = _get_env('MINIO_REGION', default=None)
|
||||
|
||||
return Minio(
|
||||
endpoint=endpoint,
|
||||
access_key=access_key,
|
||||
secret_key=secret_key,
|
||||
secure=secure,
|
||||
region=region,
|
||||
)
|
||||
|
||||
|
||||
def get_bucket_name() -> str:
|
||||
return _get_env('MINIO_BUCKET', default='achievement') or 'achievement'
|
||||
|
||||
|
||||
def ensure_bucket_exists() -> bool:
|
||||
client = get_minio_client()
|
||||
bucket = get_bucket_name()
|
||||
if client is None:
|
||||
print('ℹ️ MinIO 环境变量未配置,跳过桶检查')
|
||||
return False
|
||||
|
||||
if not bucket:
|
||||
print('ℹ️ MINIO_BUCKET 为空,跳过桶检查')
|
||||
return False
|
||||
|
||||
try:
|
||||
exists = client.bucket_exists(bucket)
|
||||
except S3Error as e:
|
||||
print(f'❌ MinIO 连接失败: {e}')
|
||||
return False
|
||||
|
||||
if exists:
|
||||
print(f'ℹ️ MinIO 桶已存在: {bucket}')
|
||||
return True
|
||||
|
||||
try:
|
||||
region = _get_env('MINIO_REGION', default=None)
|
||||
if region:
|
||||
client.make_bucket(bucket, location=region)
|
||||
else:
|
||||
client.make_bucket(bucket)
|
||||
print(f'✅ MinIO 桶已创建: {bucket}')
|
||||
return True
|
||||
except S3Error as e:
|
||||
print(f'❌ MinIO 创建桶失败: {e}')
|
||||
return False
|
||||
|
||||
|
||||
def upload_file(file_path: str, object_name: str, content_type: str | None = None) -> str:
|
||||
client = get_minio_client()
|
||||
if client is None:
|
||||
raise RuntimeError('MinIO 未配置')
|
||||
|
||||
bucket = get_bucket_name()
|
||||
ensure_bucket_exists()
|
||||
|
||||
ct = content_type
|
||||
if not ct:
|
||||
guessed, _ = mimetypes.guess_type(object_name)
|
||||
ct = guessed or 'application/octet-stream'
|
||||
|
||||
client.fput_object(bucket, object_name, file_path, content_type=ct)
|
||||
return object_name
|
||||
|
||||
|
||||
def presigned_get_url(object_name: str, expires_seconds: int = 8 * 60 * 60) -> str:
|
||||
client = get_minio_client()
|
||||
if client is None:
|
||||
raise RuntimeError('MinIO 未配置')
|
||||
|
||||
bucket = get_bucket_name()
|
||||
ensure_bucket_exists()
|
||||
exp = max(1, int(expires_seconds or 0))
|
||||
return client.presigned_get_object(bucket, object_name, expires=timedelta(seconds=exp))
|
||||
@@ -7,6 +7,7 @@ requests==2.32.3
|
||||
openai==1.52.2
|
||||
httpx==0.27.2
|
||||
Pillow==10.4.0
|
||||
minio>=7.2.0,<8
|
||||
gunicorn==21.2.0
|
||||
whitenoise==6.6.0
|
||||
django-browser-reload==1.21.0
|
||||
|
||||
Reference in New Issue
Block a user