Skip to content

Commit 3010fa8

Browse files
committed
feat: implement File API for file upload and retrieval with new endpoints
1 parent 06d5c10 commit 3010fa8

File tree

7 files changed

+230
-20
lines changed

7 files changed

+230
-20
lines changed

apps/knowledge/api/document.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,7 @@ def get_parameters():
1919
location='path',
2020
required=True,
2121
),
22-
OpenApiParameter(
23-
name="file",
24-
description="文件",
25-
type=OpenApiTypes.BINARY,
26-
location='query',
27-
required=False,
28-
),
22+
2923
OpenApiParameter(
3024
name="limit",
3125
description="分段长度",
@@ -49,6 +43,20 @@ def get_parameters():
4943
),
5044
]
5145

46+
@staticmethod
47+
def get_request():
48+
return {
49+
'multipart/form-data': {
50+
'type': 'object',
51+
'properties': {
52+
'file': {
53+
'type': 'string',
54+
'format': 'binary' # Tells Swagger it's a file
55+
}
56+
}
57+
}
58+
}
59+
5260

5361
class DocumentBatchAPI(APIMixin):
5462
@staticmethod
@@ -197,15 +205,23 @@ def get_parameters():
197205
location='path',
198206
required=True,
199207
),
200-
OpenApiParameter(
201-
name="file",
202-
description="文件",
203-
type=OpenApiTypes.BINARY,
204-
location='query',
205-
required=False,
206-
),
208+
207209
]
208210

211+
@staticmethod
212+
def get_request():
213+
return {
214+
'multipart/form-data': {
215+
'type': 'object',
216+
'properties': {
217+
'file': {
218+
'type': 'string',
219+
'format': 'binary' # Tells Swagger it's a file
220+
}
221+
}
222+
}
223+
}
224+
209225
@staticmethod
210226
def get_response():
211227
return DefaultResultSerializer

apps/knowledge/api/file.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from drf_spectacular.types import OpenApiTypes
2+
from drf_spectacular.utils import OpenApiParameter
3+
4+
from common.mixins.api_mixin import APIMixin
5+
from common.result import DefaultResultSerializer
6+
7+
8+
class FileUploadAPI(APIMixin):
9+
10+
@staticmethod
11+
def get_request():
12+
return {
13+
'multipart/form-data': {
14+
'type': 'object',
15+
'properties': {
16+
'file': {
17+
'type': 'string',
18+
'format': 'binary' # Tells Swagger it's a file
19+
}
20+
}
21+
}
22+
}
23+
24+
@staticmethod
25+
def get_response():
26+
return DefaultResultSerializer
27+
28+
29+
class FileGetAPI(APIMixin):
30+
@staticmethod
31+
def get_parameters():
32+
return [
33+
OpenApiParameter(
34+
name="file_id",
35+
description="文件id",
36+
type=OpenApiTypes.STR,
37+
location='path',
38+
required=True,
39+
),
40+
]
41+
42+
@staticmethod
43+
def get_response():
44+
return DefaultResultSerializer

apps/knowledge/serializers/file.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# coding=utf-8
2+
3+
import uuid_utils.compat as uuid
4+
from django.db.models import QuerySet
5+
from django.http import HttpResponse
6+
from django.utils.translation import gettext_lazy as _
7+
from rest_framework import serializers
8+
9+
from common.exception.app_exception import NotFound404
10+
from knowledge.models import File
11+
from tools.serializers.tool import UploadedFileField
12+
13+
mime_types = {
14+
"html": "text/html", "htm": "text/html", "shtml": "text/html", "css": "text/css", "xml": "text/xml",
15+
"gif": "image/gif", "jpeg": "image/jpeg", "jpg": "image/jpeg", "js": "application/javascript",
16+
"atom": "application/atom+xml", "rss": "application/rss+xml", "mml": "text/mathml", "txt": "text/plain",
17+
"jad": "text/vnd.sun.j2me.app-descriptor", "wml": "text/vnd.wap.wml", "htc": "text/x-component",
18+
"avif": "image/avif", "png": "image/png", "svg": "image/svg+xml", "svgz": "image/svg+xml",
19+
"tif": "image/tiff", "tiff": "image/tiff", "wbmp": "image/vnd.wap.wbmp", "webp": "image/webp",
20+
"ico": "image/x-icon", "jng": "image/x-jng", "bmp": "image/x-ms-bmp", "woff": "font/woff",
21+
"woff2": "font/woff2", "jar": "application/java-archive", "war": "application/java-archive",
22+
"ear": "application/java-archive", "json": "application/json", "hqx": "application/mac-binhex40",
23+
"doc": "application/msword", "pdf": "application/pdf", "ps": "application/postscript",
24+
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
25+
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
26+
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
27+
"eps": "application/postscript", "ai": "application/postscript", "rtf": "application/rtf",
28+
"m3u8": "application/vnd.apple.mpegurl", "kml": "application/vnd.google-earth.kml+xml",
29+
"kmz": "application/vnd.google-earth.kmz", "xls": "application/vnd.ms-excel",
30+
"eot": "application/vnd.ms-fontobject", "ppt": "application/vnd.ms-powerpoint",
31+
"odg": "application/vnd.oasis.opendocument.graphics",
32+
"odp": "application/vnd.oasis.opendocument.presentation",
33+
"ods": "application/vnd.oasis.opendocument.spreadsheet", "odt": "application/vnd.oasis.opendocument.text",
34+
"wmlc": "application/vnd.wap.wmlc", "wasm": "application/wasm", "7z": "application/x-7z-compressed",
35+
"cco": "application/x-cocoa", "jardiff": "application/x-java-archive-diff",
36+
"jnlp": "application/x-java-jnlp-file", "run": "application/x-makeself", "pl": "application/x-perl",
37+
"pm": "application/x-perl", "prc": "application/x-pilot", "pdb": "application/x-pilot",
38+
"rar": "application/x-rar-compressed", "rpm": "application/x-redhat-package-manager",
39+
"sea": "application/x-sea", "swf": "application/x-shockwave-flash", "sit": "application/x-stuffit",
40+
"tcl": "application/x-tcl", "tk": "application/x-tcl", "der": "application/x-x509-ca-cert",
41+
"pem": "application/x-x509-ca-cert", "crt": "application/x-x509-ca-cert",
42+
"xpi": "application/x-xpinstall", "xhtml": "application/xhtml+xml", "xspf": "application/xspf+xml",
43+
"zip": "application/zip", "bin": "application/octet-stream", "exe": "application/octet-stream",
44+
"dll": "application/octet-stream", "deb": "application/octet-stream", "dmg": "application/octet-stream",
45+
"iso": "application/octet-stream", "img": "application/octet-stream", "msi": "application/octet-stream",
46+
"msp": "application/octet-stream", "msm": "application/octet-stream", "mid": "audio/midi",
47+
"midi": "audio/midi", "kar": "audio/midi", "mp3": "audio/mpeg", "ogg": "audio/ogg", "m4a": "audio/x-m4a",
48+
"ra": "audio/x-realaudio", "3gpp": "video/3gpp", "3gp": "video/3gpp", "ts": "video/mp2t",
49+
"mp4": "video/mp4", "mpeg": "video/mpeg", "mpg": "video/mpeg", "mov": "video/quicktime",
50+
"webm": "video/webm", "flv": "video/x-flv", "m4v": "video/x-m4v", "mng": "video/x-mng",
51+
"asx": "video/x-ms-asf", "asf": "video/x-ms-asf", "wmv": "video/x-ms-wmv", "avi": "video/x-msvideo"
52+
}
53+
54+
55+
class FileSerializer(serializers.Serializer):
56+
file = UploadedFileField(required=True, label=_('file'))
57+
meta = serializers.JSONField(required=False, allow_null=True)
58+
59+
def upload(self, with_valid=True):
60+
if with_valid:
61+
self.is_valid(raise_exception=True)
62+
meta = self.data.get('meta', None)
63+
if not meta:
64+
meta = {'debug': True}
65+
file_id = meta.get('file_id', uuid.uuid7())
66+
file = File(id=file_id, file_name=self.data.get('file').name, meta=meta)
67+
file.save(self.data.get('file').read())
68+
return f'/api/file/{file_id}'
69+
70+
class Operate(serializers.Serializer):
71+
id = serializers.UUIDField(required=True)
72+
73+
def get(self, with_valid=True):
74+
if with_valid:
75+
self.is_valid(raise_exception=True)
76+
file_id = self.data.get('id')
77+
file = QuerySet(File).filter(id=file_id).first()
78+
if file is None:
79+
raise NotFound404(404, _('File not found'))
80+
# 如果是音频文件,直接返回文件流
81+
file_type = file.file_name.split(".")[-1]
82+
if file_type in ['mp3', 'wav', 'ogg', 'aac']:
83+
return HttpResponse(
84+
file.get_bytes(),
85+
status=200,
86+
headers={
87+
'Content-Type': f'audio/{file_type}',
88+
'Content-Disposition': 'attachment; filename="{}"'.format(file.file_name)
89+
}
90+
)
91+
return HttpResponse(
92+
file.get_bytes(),
93+
status=200,
94+
headers={'Content-Type': mime_types.get(file_type, 'text/plain')}
95+
)

apps/knowledge/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,7 @@
3838
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/problem/<int:current_page>/<int:page_size>', views.ProblemView.Page.as_view()),
3939
path('workspace/<str:workspace_id>/knowledge/<str:knowledge_id>/document/<int:current_page>/<int:page_size>', views.DocumentView.Page.as_view()),
4040
path('workspace/<str:workspace_id>/knowledge/<int:current_page>/<int:page_size>', views.KnowledgeView.Page.as_view()),
41+
path('file', views.FileView.as_view()),
42+
path('file/<str:file_id>', views.FileView.Operate.as_view()),
43+
4144
]

apps/knowledge/views/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
from .knowledge import *
33
from .paragraph import *
44
from .problem import *
5+
from .file import *

apps/knowledge/views/file.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# coding=utf-8
2+
from django.utils.translation import gettext_lazy as _
3+
from drf_spectacular.utils import extend_schema
4+
from rest_framework.parsers import MultiPartParser
5+
from rest_framework.views import APIView
6+
from rest_framework.views import Request
7+
8+
from common.auth import TokenAuth
9+
from common.result import result
10+
from knowledge.api.file import FileUploadAPI, FileGetAPI
11+
from knowledge.serializers.file import FileSerializer
12+
13+
14+
class FileView(APIView):
15+
authentication_classes = [TokenAuth]
16+
parser_classes = [MultiPartParser]
17+
18+
@extend_schema(
19+
methods=['POST'],
20+
summary=_('Upload file'),
21+
description=_('Upload file'),
22+
operation_id=_('Upload file'),
23+
parameters=FileUploadAPI.get_parameters(),
24+
request=FileUploadAPI.get_request(),
25+
responses=FileUploadAPI.get_response(),
26+
tags=[_('file')]
27+
)
28+
def post(self, request: Request):
29+
return result.success(FileSerializer(data={'file': request.FILES.get('file')}).upload())
30+
31+
class Operate(APIView):
32+
@extend_schema(
33+
methods=['GET'],
34+
summary=_('Get file'),
35+
description=_('Get file'),
36+
operation_id=_('Get file'),
37+
parameters=FileGetAPI.get_parameters(),
38+
responses=FileGetAPI.get_response(),
39+
tags=[_('file')]
40+
)
41+
def get(self, request: Request, file_id: str):
42+
return FileSerializer.Operate(data={'id': file_id}).get()

apps/tools/api/tool.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,14 +139,23 @@ def get_parameters():
139139
location='path',
140140
required=True,
141141
),
142-
OpenApiParameter(
143-
name='file',
144-
type=OpenApiTypes.BINARY,
145-
description='工具文件',
146-
required=True
147-
),
142+
148143
]
149144

145+
@staticmethod
146+
def get_request():
147+
return {
148+
'multipart/form-data': {
149+
'type': 'object',
150+
'properties': {
151+
'file': {
152+
'type': 'string',
153+
'format': 'binary' # Tells Swagger it's a file
154+
}
155+
}
156+
}
157+
}
158+
150159
@staticmethod
151160
def get_response():
152161
return DefaultResultSerializer

0 commit comments

Comments
 (0)