From 3010fa835f5b09de9d729fe4e4a22f632f855b78 Mon Sep 17 00:00:00 2001 From: CaptainB Date: Thu, 8 May 2025 10:42:00 +0800 Subject: [PATCH] feat: implement File API for file upload and retrieval with new endpoints --- apps/knowledge/api/document.py | 44 +++++++++----- apps/knowledge/api/file.py | 44 ++++++++++++++ apps/knowledge/serializers/file.py | 95 ++++++++++++++++++++++++++++++ apps/knowledge/urls.py | 3 + apps/knowledge/views/__init__.py | 1 + apps/knowledge/views/file.py | 42 +++++++++++++ apps/tools/api/tool.py | 21 +++++-- 7 files changed, 230 insertions(+), 20 deletions(-) create mode 100644 apps/knowledge/api/file.py create mode 100644 apps/knowledge/serializers/file.py create mode 100644 apps/knowledge/views/file.py diff --git a/apps/knowledge/api/document.py b/apps/knowledge/api/document.py index 7050a854d..14d53142b 100644 --- a/apps/knowledge/api/document.py +++ b/apps/knowledge/api/document.py @@ -19,13 +19,7 @@ class DocumentSplitAPI(APIMixin): location='path', required=True, ), - OpenApiParameter( - name="file", - description="文件", - type=OpenApiTypes.BINARY, - location='query', - required=False, - ), + OpenApiParameter( name="limit", description="分段长度", @@ -49,6 +43,20 @@ class DocumentSplitAPI(APIMixin): ), ] + @staticmethod + def get_request(): + return { + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'file': { + 'type': 'string', + 'format': 'binary' # Tells Swagger it's a file + } + } + } + } + class DocumentBatchAPI(APIMixin): @staticmethod @@ -197,15 +205,23 @@ class TableDocumentCreateAPI(APIMixin): location='path', required=True, ), - OpenApiParameter( - name="file", - description="文件", - type=OpenApiTypes.BINARY, - location='query', - required=False, - ), + ] + @staticmethod + def get_request(): + return { + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'file': { + 'type': 'string', + 'format': 'binary' # Tells Swagger it's a file + } + } + } + } + @staticmethod def get_response(): return DefaultResultSerializer diff --git a/apps/knowledge/api/file.py b/apps/knowledge/api/file.py new file mode 100644 index 000000000..081f18e02 --- /dev/null +++ b/apps/knowledge/api/file.py @@ -0,0 +1,44 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import OpenApiParameter + +from common.mixins.api_mixin import APIMixin +from common.result import DefaultResultSerializer + + +class FileUploadAPI(APIMixin): + + @staticmethod + def get_request(): + return { + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'file': { + 'type': 'string', + 'format': 'binary' # Tells Swagger it's a file + } + } + } + } + + @staticmethod + def get_response(): + return DefaultResultSerializer + + +class FileGetAPI(APIMixin): + @staticmethod + def get_parameters(): + return [ + OpenApiParameter( + name="file_id", + description="文件id", + type=OpenApiTypes.STR, + location='path', + required=True, + ), + ] + + @staticmethod + def get_response(): + return DefaultResultSerializer diff --git a/apps/knowledge/serializers/file.py b/apps/knowledge/serializers/file.py new file mode 100644 index 000000000..794670d0f --- /dev/null +++ b/apps/knowledge/serializers/file.py @@ -0,0 +1,95 @@ +# coding=utf-8 + +import uuid_utils.compat as uuid +from django.db.models import QuerySet +from django.http import HttpResponse +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from common.exception.app_exception import NotFound404 +from knowledge.models import File +from tools.serializers.tool import UploadedFileField + +mime_types = { + "html": "text/html", "htm": "text/html", "shtml": "text/html", "css": "text/css", "xml": "text/xml", + "gif": "image/gif", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "application/javascript", + "atom": "application/atom+xml", "rss": "application/rss+xml", "mml": "text/mathml", "txt": "text/plain", + "jad": "text/vnd.sun.j2me.app-descriptor", "wml": "text/vnd.wap.wml", "htc": "text/x-component", + "avif": "image/avif", "png": "image/png", "svg": "image/svg+xml", "svgz": "image/svg+xml", + "tif": "image/tiff", "tiff": "image/tiff", "wbmp": "image/vnd.wap.wbmp", "webp": "image/webp", + "ico": "image/x-icon", "jng": "image/x-jng", "bmp": "image/x-ms-bmp", "woff": "font/woff", + "woff2": "font/woff2", "jar": "application/java-archive", "war": "application/java-archive", + "ear": "application/java-archive", "json": "application/json", "hqx": "application/mac-binhex40", + "doc": "application/msword", "pdf": "application/pdf", "ps": "application/postscript", + "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "eps": "application/postscript", "ai": "application/postscript", "rtf": "application/rtf", + "m3u8": "application/vnd.apple.mpegurl", "kml": "application/vnd.google-earth.kml+xml", + "kmz": "application/vnd.google-earth.kmz", "xls": "application/vnd.ms-excel", + "eot": "application/vnd.ms-fontobject", "ppt": "application/vnd.ms-powerpoint", + "odg": "application/vnd.oasis.opendocument.graphics", + "odp": "application/vnd.oasis.opendocument.presentation", + "ods": "application/vnd.oasis.opendocument.spreadsheet", "odt": "application/vnd.oasis.opendocument.text", + "wmlc": "application/vnd.wap.wmlc", "wasm": "application/wasm", "7z": "application/x-7z-compressed", + "cco": "application/x-cocoa", "jardiff": "application/x-java-archive-diff", + "jnlp": "application/x-java-jnlp-file", "run": "application/x-makeself", "pl": "application/x-perl", + "pm": "application/x-perl", "prc": "application/x-pilot", "pdb": "application/x-pilot", + "rar": "application/x-rar-compressed", "rpm": "application/x-redhat-package-manager", + "sea": "application/x-sea", "swf": "application/x-shockwave-flash", "sit": "application/x-stuffit", + "tcl": "application/x-tcl", "tk": "application/x-tcl", "der": "application/x-x509-ca-cert", + "pem": "application/x-x509-ca-cert", "crt": "application/x-x509-ca-cert", + "xpi": "application/x-xpinstall", "xhtml": "application/xhtml+xml", "xspf": "application/xspf+xml", + "zip": "application/zip", "bin": "application/octet-stream", "exe": "application/octet-stream", + "dll": "application/octet-stream", "deb": "application/octet-stream", "dmg": "application/octet-stream", + "iso": "application/octet-stream", "img": "application/octet-stream", "msi": "application/octet-stream", + "msp": "application/octet-stream", "msm": "application/octet-stream", "mid": "audio/midi", + "midi": "audio/midi", "kar": "audio/midi", "mp3": "audio/mpeg", "ogg": "audio/ogg", "m4a": "audio/x-m4a", + "ra": "audio/x-realaudio", "3gpp": "video/3gpp", "3gp": "video/3gpp", "ts": "video/mp2t", + "mp4": "video/mp4", "mpeg": "video/mpeg", "mpg": "video/mpeg", "mov": "video/quicktime", + "webm": "video/webm", "flv": "video/x-flv", "m4v": "video/x-m4v", "mng": "video/x-mng", + "asx": "video/x-ms-asf", "asf": "video/x-ms-asf", "wmv": "video/x-ms-wmv", "avi": "video/x-msvideo" +} + + +class FileSerializer(serializers.Serializer): + file = UploadedFileField(required=True, label=_('file')) + meta = serializers.JSONField(required=False, allow_null=True) + + def upload(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + meta = self.data.get('meta', None) + if not meta: + meta = {'debug': True} + file_id = meta.get('file_id', uuid.uuid7()) + file = File(id=file_id, file_name=self.data.get('file').name, meta=meta) + file.save(self.data.get('file').read()) + return f'/api/file/{file_id}' + + class Operate(serializers.Serializer): + id = serializers.UUIDField(required=True) + + def get(self, with_valid=True): + if with_valid: + self.is_valid(raise_exception=True) + file_id = self.data.get('id') + file = QuerySet(File).filter(id=file_id).first() + if file is None: + raise NotFound404(404, _('File not found')) + # 如果是音频文件,直接返回文件流 + file_type = file.file_name.split(".")[-1] + if file_type in ['mp3', 'wav', 'ogg', 'aac']: + return HttpResponse( + file.get_bytes(), + status=200, + headers={ + 'Content-Type': f'audio/{file_type}', + 'Content-Disposition': 'attachment; filename="{}"'.format(file.file_name) + } + ) + return HttpResponse( + file.get_bytes(), + status=200, + headers={'Content-Type': mime_types.get(file_type, 'text/plain')} + ) diff --git a/apps/knowledge/urls.py b/apps/knowledge/urls.py index 0560bb34d..04d660675 100644 --- a/apps/knowledge/urls.py +++ b/apps/knowledge/urls.py @@ -38,4 +38,7 @@ urlpatterns = [ path('workspace//knowledge//problem//', views.ProblemView.Page.as_view()), path('workspace//knowledge//document//', views.DocumentView.Page.as_view()), path('workspace//knowledge//', views.KnowledgeView.Page.as_view()), + path('file', views.FileView.as_view()), + path('file/', views.FileView.Operate.as_view()), + ] diff --git a/apps/knowledge/views/__init__.py b/apps/knowledge/views/__init__.py index 586b2d335..0ba89c800 100644 --- a/apps/knowledge/views/__init__.py +++ b/apps/knowledge/views/__init__.py @@ -2,3 +2,4 @@ from .document import * from .knowledge import * from .paragraph import * from .problem import * +from .file import * diff --git a/apps/knowledge/views/file.py b/apps/knowledge/views/file.py new file mode 100644 index 000000000..8a7ecfe2e --- /dev/null +++ b/apps/knowledge/views/file.py @@ -0,0 +1,42 @@ +# coding=utf-8 +from django.utils.translation import gettext_lazy as _ +from drf_spectacular.utils import extend_schema +from rest_framework.parsers import MultiPartParser +from rest_framework.views import APIView +from rest_framework.views import Request + +from common.auth import TokenAuth +from common.result import result +from knowledge.api.file import FileUploadAPI, FileGetAPI +from knowledge.serializers.file import FileSerializer + + +class FileView(APIView): + authentication_classes = [TokenAuth] + parser_classes = [MultiPartParser] + + @extend_schema( + methods=['POST'], + summary=_('Upload file'), + description=_('Upload file'), + operation_id=_('Upload file'), + parameters=FileUploadAPI.get_parameters(), + request=FileUploadAPI.get_request(), + responses=FileUploadAPI.get_response(), + tags=[_('file')] + ) + def post(self, request: Request): + return result.success(FileSerializer(data={'file': request.FILES.get('file')}).upload()) + + class Operate(APIView): + @extend_schema( + methods=['GET'], + summary=_('Get file'), + description=_('Get file'), + operation_id=_('Get file'), + parameters=FileGetAPI.get_parameters(), + responses=FileGetAPI.get_response(), + tags=[_('file')] + ) + def get(self, request: Request, file_id: str): + return FileSerializer.Operate(data={'id': file_id}).get() diff --git a/apps/tools/api/tool.py b/apps/tools/api/tool.py index 50e21eb66..049090ad1 100644 --- a/apps/tools/api/tool.py +++ b/apps/tools/api/tool.py @@ -139,14 +139,23 @@ class ToolImportAPI(APIMixin): location='path', required=True, ), - OpenApiParameter( - name='file', - type=OpenApiTypes.BINARY, - description='工具文件', - required=True - ), + ] + @staticmethod + def get_request(): + return { + 'multipart/form-data': { + 'type': 'object', + 'properties': { + 'file': { + 'type': 'string', + 'format': 'binary' # Tells Swagger it's a file + } + } + } + } + @staticmethod def get_response(): return DefaultResultSerializer