Go+Gin實(shí)現(xiàn)安全多文件上傳功能
更新時(shí)間:2025年04月02日 15:18:53 作者:赴前塵
這篇文章主要為大家詳細(xì)介紹了Go如何利用Gin框架實(shí)現(xiàn)安全多文件上傳功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
Go+Gin實(shí)現(xiàn)安全多文件上傳:帶MD5校驗(yàn)的完整解決方案
完整代碼如下
后端
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"` // 是否通過驗(yàn)證 } 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. 驗(yàn)證文件數(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. 驗(yàn)證基本元數(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é)果(實(shí)際項(xiàng)目中這里應(yīng)該做MD5校驗(yàn)) 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>支持多文件上傳,自動計(jì)算MD5校驗(yàn)值</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; // 為每個(gè)文件計(jì)算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; } // 計(jì)算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ò)錯(cuò)誤,上傳失敗'); }; xhr.send(formData); } catch (error) { showError('上傳出錯(cuò): ' + 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校驗(yàn): ${result.receivedMD5 || '無'} - <span style="color: ${result.isVerified ? '#2ecc71' : '#e74c3c'}"> ${result.isVerified ? '? 驗(yàn)證通過' : '× 驗(yàn)證失敗'} </span> </div> `; resultsContainer.appendChild(resultItem); }); } else { showError(response.error || '上傳失敗'); } } // 顯示錯(cuò)誤 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實(shí)現(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-07Go channel發(fā)送方和接收方如何相互阻塞等待源碼解讀
這篇文章主要為大家介紹了Go channel發(fā)送方和接收方如何相互阻塞等待源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12使用自定義錯(cuò)誤碼攔截grpc內(nèi)部狀態(tài)碼問題
這篇文章主要介紹了使用自定義錯(cuò)誤碼攔截grpc內(nèi)部狀態(tài)碼問題,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09