Go+Gin實現(xiàn)安全多文件上傳功能
更新時間:2025年04月02日 15:18:53 作者:赴前塵
這篇文章主要為大家詳細(xì)介紹了Go如何利用Gin框架實現(xiàn)安全多文件上傳功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
Go+Gin實現(xiàn)安全多文件上傳:帶MD5校驗的完整解決方案
完整代碼如下
后端
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// 前端傳來的文件元數(shù)據(jù)
type FileMetaRequest struct {
FileName string `json:"fileName" binding:"required"`
FileSize int64 `json:"fileSize" binding:"required"`
FileType string `json:"fileType" binding:"required"`
FileMD5 string `json:"fileMD5" binding:"required"`
}
// 返回給前端的響應(yīng)結(jié)構(gòu)
type UploadResponse struct {
OriginalName string `json:"originalName"`
SavedPath string `json:"savedPath"`
ReceivedMD5 string `json:"receivedMD5"`
IsVerified bool `json:"isVerified"` // 是否通過驗證
}
func main() {
r := gin.Default()
// 配置CORS
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"POST"},
}))
// 上傳目錄
uploadDir := "uploads"
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
os.Mkdir(uploadDir, 0755)
}
r.POST("/upload", func(c *gin.Context) {
// 1. 獲取元數(shù)據(jù)JSON
metaJson := c.PostForm("metadata")
var fileMetas []FileMetaRequest
if err := json.Unmarshal([]byte(metaJson), &fileMetas); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "元數(shù)據(jù)解析失敗"})
return
}
// 2. 獲取文件
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件獲取失敗"})
return
}
files := form.File["files"]
// 3. 驗證文件數(shù)量匹配
if len(files) != len(fileMetas) {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("元數(shù)據(jù)與文件數(shù)量不匹配(元數(shù)據(jù):%d 文件:%d)",
len(fileMetas), len(files)),
})
return
}
var results []UploadResponse
for i, file := range files {
meta := fileMetas[i]
// 4. 驗證基本元數(shù)據(jù)
if file.Filename != meta.FileName ||
file.Size != meta.FileSize {
results = append(results, UploadResponse{
OriginalName: file.Filename,
IsVerified: false,
})
continue
}
// 5. 保存文件
savedName := fmt.Sprintf("%s%s", meta.FileMD5, filepath.Ext(file.Filename))
savePath := filepath.Join(uploadDir, savedName)
if err := c.SaveUploadedFile(file, savePath); err != nil {
results = append(results, UploadResponse{
OriginalName: file.Filename,
IsVerified: false,
})
continue
}
// 6. 記錄結(jié)果(實際項目中這里應(yīng)該做MD5校驗)
results = append(results, UploadResponse{
OriginalName: file.Filename,
SavedPath: savePath,
ReceivedMD5: meta.FileMD5,
IsVerified: true,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"results": results,
})
})
log.Println("服務(wù)啟動在 :8080")
r.Run(":8080")
}
前端
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上傳系統(tǒng)</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
h1 {
color: #2c3e50;
text-align: center;
margin-bottom: 30px;
}
.upload-container {
background-color: white;
padding: 25px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.file-drop-area {
border: 2px dashed #3498db;
border-radius: 5px;
padding: 30px;
text-align: center;
margin-bottom: 20px;
transition: all 0.3s;
}
.file-drop-area.highlight {
background-color: #f0f8ff;
border-color: #2980b9;
}
#fileInput {
display: none;
}
.file-label {
display: inline-block;
padding: 10px 20px;
background-color: #3498db;
color: white;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.file-label:hover {
background-color: #2980b9;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: bold;
}
.file-meta {
font-size: 0.8em;
color: #7f8c8d;
}
.file-type {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.8em;
margin-left: 10px;
}
.type-body {
background-color: #2ecc71;
color: white;
}
.type-attachment {
background-color: #e74c3c;
color: white;
}
.progress-container {
margin-top: 20px;
}
.progress-bar {
height: 20px;
background-color: #ecf0f1;
border-radius: 4px;
margin-bottom: 10px;
overflow: hidden;
}
.progress {
height: 100%;
background-color: #3498db;
width: 0%;
transition: width 0.3s;
}
.results {
margin-top: 30px;
}
.result-item {
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
background-color: #f8f9fa;
}
.success {
border-left: 4px solid #2ecc71;
}
.error {
border-left: 4px solid #e74c3c;
}
button {
padding: 10px 20px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
</style>
</head>
<body>
<h1>郵件文件上傳系統(tǒng)</h1>
<div class="upload-container">
<div class="file-drop-area" id="dropArea">
<input type="file" id="fileInput" multiple>
<label for="fileInput" class="file-label">選擇文件或拖放到此處</label>
<p>支持多文件上傳,自動計算MD5校驗值</p>
</div>
<div class="file-list" id="fileList"></div>
<div class="progress-container" id="progressContainer" style="display: none;">
<h3>上傳進(jìn)度</h3>
<div class="progress-bar">
<div class="progress" id="progressBar"></div>
</div>
<div id="progressText">準(zhǔn)備上傳...</div>
</div>
<button id="uploadBtn" disabled>開始上傳</button>
<button id="clearBtn">清空列表</button>
</div>
<div class="results" id="results"></div>
<script>
// 全局變量
let files = [];
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const fileList = document.getElementById('fileList');
const uploadBtn = document.getElementById('uploadBtn');
const clearBtn = document.getElementById('clearBtn');
const progressContainer = document.getElementById('progressContainer');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultsContainer = document.getElementById('results');
// 拖放功能
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('highlight');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('highlight');
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('highlight');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFiles();
}
});
// 文件選擇處理
fileInput.addEventListener('change', handleFiles);
async function handleFiles() {
const newFiles = Array.from(fileInput.files);
if (newFiles.length === 0) return;
// 為每個文件計算MD5并創(chuàng)建元數(shù)據(jù)
for (const file of newFiles) {
const fileMeta = {
file: file,
name: file.name,
size: file.size,
type: file.type,
md5: await calculateMD5(file),
};
files.push(fileMeta);
}
renderFileList();
uploadBtn.disabled = false;
}
// 計算MD5
async function calculateMD5(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
const hash = md5(e.target.result);
resolve(hash);
};
reader.readAsBinaryString(file); // 注意這里使用 readAsBinaryString
});
}
// 渲染文件列表
function renderFileList() {
fileList.innerHTML = '';
if (files.length === 0) {
fileList.innerHTML = '<p>沒有選擇文件</p>';
uploadBtn.disabled = true;
return;
}
files.forEach((fileMeta, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<div class="file-info">
<div class="file-name">${fileMeta.name}</div>
<div class="file-meta">
大小: ${formatFileSize(fileMeta.size)} |
MD5: ${fileMeta.md5.substring(0, 8)}... |
類型: ${fileMeta.type || '未知'}
</div>
</div>
<div>
<button onclick="toggleFileType(${index})" class="file-type ${fileMeta.isAttachment ? 'type-attachment' : 'type-body'}">
${fileMeta.isAttachment ? '附件' : '正文'}
</button>
</div>
`;
fileList.appendChild(fileItem);
});
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 上傳文件
uploadBtn.addEventListener('click', async () => {
if (files.length === 0) return;
uploadBtn.disabled = true;
progressContainer.style.display = 'block';
resultsContainer.innerHTML = '<h3>上傳結(jié)果</h3>';
try {
const formData = new FormData();
// 添加元數(shù)據(jù)
const metadata = files.map(f => ({
fileName: f.name,
fileSize: f.size,
fileType: f.type,
fileMD5: f.md5,
}));
formData.append('metadata', JSON.stringify(metadata));
// 添加文件
files.forEach(f => formData.append('files', f.file));
// 使用Fetch API上傳
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:8080/upload', true);
// 進(jìn)度監(jiān)聽
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressBar.style.width = percent + '%';
progressText.textContent = `上傳中: ${percent}% (${formatFileSize(e.loaded)}/${formatFileSize(e.total)})`;
}
};
xhr.onload = () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
showResults(response);
} else {
showError('上傳失敗: ' + xhr.statusText);
}
};
xhr.onerror = () => {
showError('網(wǎng)絡(luò)錯誤,上傳失敗');
};
xhr.send(formData);
} catch (error) {
showError('上傳出錯: ' + error.message);
}
});
// 顯示上傳結(jié)果
function showResults(response) {
progressText.textContent = '上傳完成!';
if (response.success) {
response.results.forEach(result => {
const resultItem = document.createElement('div');
resultItem.className = `result-item ${result.isVerified ? 'success' : 'error'}`;
resultItem.innerHTML = `
<div><strong>${result.originalName}</strong></div>
<div>保存路徑: ${result.savedPath || '無'}</div>
<div>MD5校驗: ${result.receivedMD5 || '無'} -
<span style="color: ${result.isVerified ? '#2ecc71' : '#e74c3c'}">
${result.isVerified ? '? 驗證通過' : '× 驗證失敗'}
</span>
</div>
`;
resultsContainer.appendChild(resultItem);
});
} else {
showError(response.error || '上傳失敗');
}
}
// 顯示錯誤
function showError(message) {
const errorItem = document.createElement('div');
errorItem.className = 'result-item error';
errorItem.textContent = message;
resultsContainer.appendChild(errorItem);
}
// 清空列表
clearBtn.addEventListener('click', () => {
files = [];
fileInput.value = '';
renderFileList();
progressContainer.style.display = 'none';
resultsContainer.innerHTML = '';
uploadBtn.disabled = true;
});
</script>
</body>
</html>
上傳截圖

到此這篇關(guān)于Go+Gin實現(xiàn)安全多文件上傳功能的文章就介紹到這了,更多相關(guān)Go Gin多文件上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
go語言中基本數(shù)據(jù)類型及應(yīng)用快速了解
這篇文章主要為大家介紹了go語言中基本數(shù)據(jù)類型應(yīng)用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07
Go channel發(fā)送方和接收方如何相互阻塞等待源碼解讀
這篇文章主要為大家介紹了Go channel發(fā)送方和接收方如何相互阻塞等待源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
使用自定義錯誤碼攔截grpc內(nèi)部狀態(tài)碼問題
這篇文章主要介紹了使用自定義錯誤碼攔截grpc內(nèi)部狀態(tài)碼問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-09-09

