django連接minio實現文件上傳下載(提供接口示例)
- 項目環境前提
- 1.模型創建
- 2. 在 settings.py 中添加 MINIO 配置
- 3.創建 MINIO 工具類
- 4.創建序列化器
- 5. 創建視圖
- 6. 配置 URL 路由
- 7.接口測試
項目環境前提
- 已安裝python3.8+以上環境
- 已安裝djangorestframework環境
- 已部署mysql數據庫
- 已部署minio
- 所需python依賴:django-storages、minio
1.模型創建
1.這里模型創建的前提是DRF的項目框架已搭建好。創建文件上傳模型字段如下所示
注:這里只展示文件上傳等字段,去除了其他字段,實際開發根據項目需求添加
# 這里僅介紹文件上傳,所以只展示文件上傳所需字段
class ApprovalProcess(models.Model): minio_url172_1 = models.TextField(null=True, blank=True, verbose_name='url172_1')minio_url10_1 = models.TextField(null=True, blank=True, verbose_name='url10_1')minio_source_name_1 = models.CharField(null=True, blank=True, max_length=200, verbose_name='源文件名1')minio_file_name_1 = models.TextField(null=True, blank=True, verbose_name='minio文件名1')create_time = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name='創建時間')update_time = models.DateTimeField(auto_now=True, null=True, blank=True, verbose_name='更新時間')is_delete = models.BooleanField(default=False, verbose_name='邏輯刪除')def delete(self, using=None, keep_parents=False):# 邏輯刪除# 把當前模型對象的is_delete字段改為True即可self.is_delete = Trueself.save()# 配置后臺管理系統每個模型的名字顯示class Meta:db_table = 'approval_process'verbose_name = '審批流程表' verbose_name_plural = verbose_nameindexes = [models.Index(fields=['minio_url172_1','minio_url10_1']),]
2. 在 settings.py 中添加 MINIO 配置
# MinIO 配置信息
# 這里有兩個網段IP地址,所以配置了兩個,只有一個IP的看情況配置
MINIO_STORAGE_ENDPOINT_172 = '172.xx.xx.xxx:5096' # MinIO 服務器地址1
MINIO_STORAGE_ENDPOINT_10 = '10.xx.xx.xxx:5096' # MinIO 服務器地址2
MINIO_STORAGE_ACCESS_KEY = 'minio賬號' # 你的minio賬號
MINIO_STORAGE_SECRET_KEY = 'minio賬號密碼' # 你的minio賬號密碼
MINIO_STORAGE_USE_HTTPS = False # 如果未啟用 HTTPS,則為 False
MINIO_STORAGE_MEDIA_BUCKET_NAME = 'backstickerv3' # 用于存儲文件的桶名稱,前提是已在minio創建好這個文件桶
3.創建 MINIO 工具類
- 在ApprovalProcess模型下創建utils文件,在該文件下創建monio_utils.py的文件,用于處理minio的文件上傳和下載
- 代碼如下。共三個主要函數:
1)upload_file:處理文件上傳
2)download_file:處理文件直接下載
3)get_presigned_url:生成預簽名URL,處理文件下載
下載文件時可根據需求選擇使用download_file或者get_presigned_url
# apps/ApprovalProcess/utils/minio_utils.py
from minio import Minio
from minio.error import S3Error
from django.conf import settings
import uuid
import os
import logging
from django.http import HttpResponse
from django.http import StreamingHttpResponse
import mimetypes # 用于根據文件名猜測 MIME 類型
import urllib.parse
from datetime import timedelta
# 配置日志
logger = logging.getLogger(__name__)class MinioClient:"""MinIO 操作工具類"""def __init__(self):# 從配置中獲取端點,移除協議頭#要確保這些值是字符串,可以打印查看endpoint_172 = settings.MINIO_STORAGE_ENDPOINT_172.replace('http://', '').replace('https://', '')endpoint_10 = settings.MINIO_STORAGE_ENDPOINT_10.replace('http://', '').replace('https://', '')access_key=settings.MINIO_STORAGE_ACCESS_KEYsecret_key=settings.MINIO_STORAGE_SECRET_KEYsecure=settings.MINIO_STORAGE_USE_HTTPS# 初始化 172 網段客戶端self.client_172 = Minio(endpoint=endpoint_172, # 使用一個端點access_key=access_key,secret_key=secret_key,secure=secure)# 初始化 10 網段客戶端self.client_10 = Minio(endpoint=endpoint_10,access_key=access_key,secret_key=secret_key,secure=secure)logger.info(f"MinIO clients initialized for both networks: 172 - {endpoint_172}, 10 - {endpoint_10}")# def upload_file(self, file_obj, file_name, bucket_name, content_type='application/octet-stream'):def upload_file(self, file_obj, file_name, bucket_name, content_type='message/rfc822'):"""上傳文件到 MinIOArgs:file_obj: 文件對象(如 Django 的 UploadedFile)file_name: 希望在 MinIO 中存儲的文件名bucket_name: 存儲桶名稱content_type: 文件類型Returns:dict: 包含文件訪問 URL 等信息的字典"""# 生成唯一的對象名稱,避免覆蓋file_extension = os.path.splitext(file_name)[1]unique_filename = f"{uuid.uuid4().hex}{file_extension}"object_name = f"approval_uploads/{unique_filename}" # 可以添加前綴分類try:# 確保存儲桶存在if not self.client_172.bucket_exists(bucket_name):self.client_172.make_bucket(bucket_name)logger.info(f"Bucket '{bucket_name}' created.")# 獲取文件大小# 對于 Django 的 UploadedFile,可以使用 file_obj.sizefile_size = file_obj.size# 上傳文件self.client_172.put_object(bucket_name,object_name,file_obj,file_size,content_type=content_type)# 構建文件的訪問 URL(路徑風格)# 添加協議頭(http 或 https)protocol = "https" if settings.MINIO_STORAGE_USE_HTTPS else "http"url_172 = f"{protocol}://172.xx.xx.xxx:5096/{bucket_name}/{object_name}"url_10 = f"{protocol}://10.xx.xx.xxx:5096/{bucket_name}/{object_name}"logger.info(f"File uploaded successfully: {object_name}")return {"url_172_1": url_172,"url_10_1": url_10,"file_name": unique_filename,"original_name": file_name,"object_name": object_name}except S3Error as e:logger.error(f"MinIO S3Error occurred: {e}")raise eexcept Exception as e:logger.error(f"Unexpected error during MinIO upload: {e}")raise edef download_file(self, bucket_name, object_name, file_name):"""從 MinIO 下載文件Args:bucket_name: 存儲桶名稱object_name: 對象名稱(在 MinIO 中的路徑)file_name: 下載時顯示的文件名Returns:HttpResponse: 包含文件數據的 HTTP 響應"""try:# print('file_name=', file_name)# 從 MinIO 獲取文件數據response = self.client_172.get_object(bucket_name, object_name)file_data = response.read()response.close()response.release_conn()# # 確保文件名有正確的擴展名# if not file_name.lower().endswith('.eml'):# # 如果文件名沒有 .eml 擴展名,添加它# file_name = f"{file_name}.eml"# 對文件名進行 URL 編碼,確保特殊字符正確處理encoded_filename = urllib.parse.quote(file_name)# 創建 HTTP 響應 - 使用正確的 Content-Typecontent_type = 'message/rfc822' # .eml 文件的正確 MIME 類型http_response = HttpResponse(file_data, content_type=content_type)# 設置 Content-Disposition 頭,確保瀏覽器正確下載文件# 使用 filename* 參數并指定 UTF-8 編碼來處理可能包含非 ASCII 字符的文件名http_response['Content-Disposition'] = f'attachment; filename="{encoded_filename}"; filename*=UTF-8\'\'{encoded_filename}'# 設置 Content-Lengthhttp_response['Content-Length'] = len(file_data)logger.info(f"File downloaded successfully: {object_name}")return http_responseexcept S3Error as e:logger.error(f"MinIO S3Error occurred during download: {e}")raise eexcept Exception as e:logger.error(f"Unexpected error during MinIO download: {e}")raise edef get_presigned_url(self, bucket_name, object_name, filename=None, expiry=3600, network='both'):"""生成預簽名 URL(支持雙網段)Args:bucket_name: 存儲桶名稱object_name: 對象名稱expiry: URL 有效期(秒),默認 1 小時network: 網絡類型,'172'、'10' 或 'both'Returns:str 或 dict: 預簽名 URL 或包含兩個 URL 的字典"""try:# 將秒數轉換為 timedelta 對象expires_td = timedelta(seconds=expiry)# 構建響應頭參數(如果提供了自定義文件名)extra_query_params = {}# print('filename=',filename)if filename:# 對文件名進行 URL 編碼encoded_filename = urllib.parse.quote(filename)# 添加響應內容處置參數,指定下載文件名extra_query_params['response-content-disposition'] = f'attachment; filename="{encoded_filename}"'# print('network=',network)if network == 'both':# 生成兩個網段的 URLurl_172 = self.client_172.presigned_get_object(bucket_name, object_name, expires=expires_td,extra_query_params=extra_query_params)url_10 = self.client_10.presigned_get_object(bucket_name, object_name, expires=expires_td,extra_query_params=extra_query_params)result = {'url_172': url_172,'url_10': url_10}return resultelif network == '10':# 只生成 10 網段的 URLurl_10 = self.client_10.presigned_get_object(bucket_name, object_name, expires=expires_td,extra_query_params=extra_query_params)result = {'url_172': '','url_10': url_10}return resultelse:# 默認生成 172 網段的 URLurl_172 = self.client_172.presigned_get_object(bucket_name, object_name, expires=expires_td,extra_query_params=extra_query_params)result = {'url_172': url_172,'url_10': ''}return resultexcept S3Error as e:logger.error(f"MinIO S3Error occurred generating presigned URL: {e}")raise eexcept Exception as e:logger.error(f"Unexpected error generating presigned URL: {e}")raise e # 創建全局 MinIO 客戶端實例
minio_client = MinioClient()
4.創建序列化器
- 創建處理文件上傳的序列化器:ApprovalProcessCreateSerializer,重寫 create 方法
from rest_framework import serializers
from rest_framework.serializers import ModelSerializer
from .models import *
from django.core.validators import FileExtensionValidatorclass ApprovalProcessCreateSerializer(serializers.ModelSerializer):# 注意:這個字段僅用于接收上傳的文件,不會保存在模型中(write_only=True)# upload_file = serializers.FileField(write_only=True, required=False, label="上傳文件")upload_file = serializers.FileField(write_only=True,required=False,validators=[FileExtensionValidator(allowed_extensions=['eml', 'doc', 'docx','xlsx']), # 允許的文件后綴# 還可以自定義驗證函數限制文件大小],label="上傳文件")class Meta:model = ApprovalProcess# 排除一些字段,這些字段將通過邏輯自動填充,而不是由用戶輸入exclude = ['is_delete', 'create_time', 'update_time', 'minio_url172_1', 'minio_url10_1', 'minio_source_name_1', 'minio_file_name_1']def create(self, validated_data):"""重寫 create 方法,處理文件上傳和模型創建"""# 1. 從驗證后的數據中彈出文件數據(如果存在)uploaded_file = validated_data.pop('upload_file', None)# 2. 創建 ApprovalProcess 模型實例(先不保存文件相關信息)instance = ApprovalProcess.objects.create(**validated_data)# 3. 如果上傳了文件,則處理 MinIO 上傳if uploaded_file:try:from .utils.minio_utils import minio_client # 在函數內部導入,避免循環導入# 調用 MinIO 工具類上傳文件upload_result = minio_client.upload_file(file_obj=uploaded_file,file_name=uploaded_file.name,bucket_name='backstickerv3' # 確保與 settings 中的桶名一致,或從設置中讀取)# 4. 更新實例的 MinIO 相關字段instance.minio_url172_1 = upload_result['url_172_1']instance.minio_url10_1 = upload_result['url_10_1']instance.minio_source_name_1 = upload_result['original_name']instance.minio_file_name_1 = upload_result['file_name']instance.save() # 保存文件信息到數據庫except Exception as e:# 處理文件上傳失敗的情況# 這里可以選擇記錄日志、刪除剛創建的實例,或者保留實例但標記文件上傳失敗# 例如:instance.file_upload_error = str(e); instance.save()# 暫時打印錯誤,生產環境應使用日志系統print(f"File upload failed for instance {instance.id}: {str(e)}")# 即使文件上傳失敗,也返回實例,但可能缺少文件信息return instance
5. 創建視圖
- 實現創建數據的接口:create_data
注:這里的create_data包含了其他字段的校驗并創建了操作記錄,根據實際情況來的,不只是處理文件上傳,如果想驗證文件上傳的,把其他數據的校驗去除即可。 - 實現文件下載的接口:download_file
- 實現獲取文件下載鏈接(預簽名 URL)的接口:get_download_url
from django.shortcuts import render
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status
import re,os,random
import datetime,time
from .serializers import *
from .models import *
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.http import StreamingHttpResponse
# from django.utils.http import urlquote
from urllib.parse import quote
from django.db.models import Q
from datetime import timedelta,date
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from datetime import timedelta,date
from django.db import transaction
import requests
from django.utils import timezone
from rest_framework.pagination import PageNumberPagination
from django.db import transaction
import logging
import MySQLdb
from django.db.models import Subquery, OuterRef, Exists
# 配置日志
logger = logging.getLogger(__name__)class CustomPagination(PageNumberPagination):page_size = 20 # 設置每頁數據量page_size_query_param = 'page_size' # 允許客戶端傳遞頁面大小參數max_page_size = 100 # 最大頁面大小限制class ApprovalProcessViewSet(viewsets.ModelViewSet): # ctrl+點擊ModelViewSet可以查看源代碼queryset = ApprovalProcess.objects.filter(is_delete=False) # 定義視圖集使用的查詢集serializer_class = ApprovalProcessSerializer # 定義視圖集使用的序列化器@action(methods=['post'], detail=False)def create_data(self, request):"""處理 POST 請求,創建審批流程數據(帶字段校驗和事務回滾)。Request Body (multipart/form-data 或 application/json):- 包含 ApprovalProcess 模型的字段(如 project, line, responser 等)- upload_file (可選): 要上傳的文件Returns:- 201 Created: 成功創建,返回創建的數據(包括文件URL,如果上傳了文件)- 400 Bad Request: 數據驗證失敗,返回錯誤信息- 500 Internal Server Error: 服務器內部錯誤(如MinIO連接失敗)"""# 1. 使用序列化器驗證和解析請求數據serializer = ApprovalProcessCreateSerializer(data=request.data)if not serializer.is_valid():return Response({"success": False,"message": "數據驗證失敗","errors": serializer.errors}, status=status.HTTP_400_BAD_REQUEST)# 2. 手動驗證必填字段required_fields = ['project', 'line', 'by_class', 'task', 'pro_code', 'work_order', 'responser', 'lose_reason', 'improve_method', 'status', 'custom_name']missing_fields = []for field in required_fields:if field not in serializer.validated_data or not serializer.validated_data[field]:missing_fields.append(field)if missing_fields:return Response({"success": False,"message": "以下字段為必填項且不能為空","missing_fields": missing_fields}, status=status.HTTP_400_BAD_REQUEST)# 3. 開始事務with transaction.atomic():# 創建保存點sid = transaction.savepoint()try:# 4. 從驗證數據中提取文件(如果存在)validated_data = serializer.validated_data.copy()uploaded_file = validated_data.pop('upload_file', None)# 5. 創建 ApprovalProcess 模型實例(先不包含文件信息)instance = ApprovalProcess.objects.create(**validated_data)# 6. 如果上傳了文件,則處理 MinIO 上傳if uploaded_file:try:from .utils.minio_utils import minio_client# 調用 MinIO 工具類上傳文件upload_result = minio_client.upload_file(file_obj=uploaded_file,file_name=uploaded_file.name,bucket_name='backstickerv3')# 7. 更新實例的 MinIO 相關字段instance.minio_url172_1 = upload_result['url_172_1']instance.minio_url10_1 = upload_result['url_10_1']instance.minio_source_name_1 = upload_result['original_name']instance.minio_file_name_1 = upload_result['file_name']instance.save()except Exception as e:# 文件上傳失敗,回滾事務transaction.savepoint_rollback(sid)logger.error(f"文件上傳失敗: {str(e)}")return Response({"success": False,"message": f"文件上傳失敗: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)else:# 如果沒有上傳文件,回滾事務transaction.savepoint_rollback(sid)logger.error("文件未上傳,數據創建失敗")return Response({"success": False,"message": "必須上傳文件才能創建數據"}, status=status.HTTP_400_BAD_REQUEST)# 創建 FlowData 操作記錄try:flow_data = FlowData.objects.create(P_id=instance.id,point=1, # 默認節點,"生產創建"result=1, # 默認處理結果,"提交"user=request.data['user'], worknumber=request.data['worknumber'], remark=request.data['remark'] # 可以根據實際情況調整備注)flow_data.save()except Exception as e:# 文件上傳失敗,回滾事務transaction.savepoint_rollback(sid)logger.error(f"操作記錄失敗: {str(e)}")return Response({"success": False,"message": f"操作記錄失敗: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)# 8. 提交事務transaction.savepoint_commit(sid)# 9. 構建成功的響應數據response_data = {"success": True,"message": "審批流程創建成功","data": {"id": instance.id,"project": instance.project,"task": instance.task,"status": instance.status,"create_time": instance.create_time,}}# 如果上傳了文件,在響應中包括文件信息if instance.minio_url172_1:response_data["data"]["file_info"] = {"original_name": instance.minio_source_name_1,"url_172": instance.minio_url172_1,"url_10": instance.minio_url10_1}return Response(response_data, status=status.HTTP_201_CREATED)except Exception as e:# 回滾事務transaction.savepoint_rollback(sid)logger.error(f"創建審批流程失敗: {str(e)}")return Response({"success": False,"message": f"服務器內部錯誤: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)@action(methods=['get'], detail=True)def download_file(self, request, pk=None):"""下載文件接口參數:pk: 審批流程記錄的主鍵 ID返回:- 200 OK: 文件下載- 404 Not Found: 記錄或文件不存在- 500 Internal Server Error: 服務器內部錯誤"""try:# 獲取審批流程記錄approval_process = self.get_object()# print('approval_process.minio_source_name_1=',approval_process.minio_source_name_1)# 檢查文件是否存在if not approval_process.minio_file_name_1:return Response({"success": False,"message": "文件不存在"}, status=status.HTTP_404_NOT_FOUND)# 從 MinIO 下載文件from .utils.minio_utils import minio_client# 構建對象名稱(與上傳時一致)object_name = f"approval_uploads/{approval_process.minio_file_name_1}"# 下載文件response = minio_client.download_file(bucket_name='backstickerv3',object_name=object_name,# file_name=approval_process.minio_source_name_1 or f"file_{approval_process.id}"file_name=approval_process.minio_source_name_1)return responseexcept ApprovalProcess.DoesNotExist:return Response({"success": False,"message": "審批流程記錄不存在"}, status=status.HTTP_404_NOT_FOUND)except Exception as e:logger.error(f"文件下載失敗: {str(e)}")return Response({"success": False,"message": f"文件下載失敗: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)@action(methods=['get'], detail=True)def get_download_url(self, request, pk=None):"""獲取文件下載鏈接(預簽名 URL)參數:pk: 審批流程記錄的主鍵 ID返回:- 200 OK: 包含下載鏈接的響應- 404 Not Found: 記錄或文件不存在- 500 Internal Server Error: 服務器內部錯誤"""try:# 獲取審批流程記錄approval_process = self.get_object()# print('approval_process=',approval_process.id)# 檢查文件是否存在if not approval_process.minio_file_name_1:return Response({"success": False,"message": "文件不存在"}, status=status.HTTP_404_NOT_FOUND)# 從 MinIO 獲取預簽名 URLfrom .utils.minio_utils import minio_client# 構建對象名稱(與上傳時一致)object_name = f"approval_uploads/{approval_process.minio_file_name_1}"filename = approval_process.minio_source_name_1network = 'both' # 三個值:10 、172 、both:生成兩個網段的 URL# 生成預簽名 URL(有效期 1 小時)# print('filename=',filename)presigned_url = minio_client.get_presigned_url(bucket_name='backstickerv3',object_name=object_name,expiry=3600, # 1 小時network=network,filename=filename)return Response({"success": True,"message": "獲取下載鏈接成功","data": {"download_url_172": presigned_url['url_172'],"download_url_10": presigned_url['url_10'],"expires_in": 3600, # 有效期(秒)"file_name": approval_process.minio_source_name_1}}, status=status.HTTP_200_OK)except ApprovalProcess.DoesNotExist:return Response({"success": False,"message": "審批流程記錄不存在"}, status=status.HTTP_404_NOT_FOUND)except Exception as e:logger.error(f"獲取下載鏈接失敗: {str(e)}")return Response({"success": False,"message": f"獲取下載鏈接失敗: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
6. 配置 URL 路由
- ApprovalProcess的APP下配置路由
from .views import *
from rest_framework.routers import DefaultRouter #導入默認路由器
from django.urls import path,includeurlpatterns = [
]
# 1.創建路由器
router = DefaultRouter() #有根路由
# 2.注冊路由,有其他路由時,只需要注冊進來即可
router.register('ApprovalProcess',ApprovalProcessViewSet)
# 3.得到生成的路由,只會自動生成標準的restful風格的增刪改查功能接口路由
#查詢單一:標準只會根據id來查詢 寫了id最后面要加/
urlpatterns += router.urls #添加到urlpatterns中即可
- 配置主路由
from django.contrib import admin
from django.urls import path,include
from rest_framework.documentation import include_docs_urlsurlpatterns = [path('admin/', admin.site.urls),path('docs/', include_docs_urls('接口文檔')), #配置接口文檔路由,文檔標題path('api/', include('ApprovalProcess.urls')),]
7.接口測試
使用apifox進行接口測試
- 測試文件上傳功能:調用create_data接口
- 測試文件下載功能:調用download_file接口
- 測試獲取文件下載鏈接功能:調用get_download_url接口。瀏覽器訪問鏈接可下載文件。