it编程 > 前端脚本 > Golang

Go+Gin实现安全多文件上传功能

4人参与 2025-04-24 Golang

go+gin实现安全多文件上传:带md5校验的完整解决方案

完整代码如下

后端

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"

	"github.com/gin-contrib/cors"
	"github.com/gin-gonic/gin"
)

// 前端传来的文件元数据
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"`
}

// 返回给前端的响应结构
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. 获取元数据json
		metajson := c.postform("metadata")
		var filemetas []filemetarequest
		if err := json.unmarshal([]byte(metajson), &filemetas); err != nil {
			c.json(http.statusbadrequest, gin.h{"error": "元数据解析失败"})
			return
		}

		// 2. 获取文件
		form, err := c.multipartform()
		if err != nil {
			c.json(http.statusbadrequest, gin.h{"error": "文件获取失败"})
			return
		}
		files := form.file["files"]

		// 3. 验证文件数量匹配
		if len(files) != len(filemetas) {
			c.json(http.statusbadrequest, gin.h{
				"error": fmt.sprintf("元数据与文件数量不匹配(元数据:%d 文件:%d)",
					len(filemetas), len(files)),
			})
			return
		}

		var results []uploadresponse
		for i, file := range files {
			meta := filemetas[i]

			// 4. 验证基本元数据
			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. 记录结果(实际项目中这里应该做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("服务启动在 :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>文件上传系统</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>邮件文件上传系统</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>上传进度</h3>
        <div class="progress-bar">
            <div class="progress" id="progressbar"></div>
        </div>
        <div id="progresstext">准备上传...</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并创建元数据
        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>上传结果</h3>';

        try {
            const formdata = new formdata();

            // 添加元数据
            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);

            // 进度监听
            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('网络错误,上传失败');
            };

            xhr.send(formdata);

        } catch (error) {
            showerror('上传出错: ' + error.message);
        }
    });

    // 显示上传结果
    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>

上传截图

到此这篇关于go+gin实现安全多文件上传功能的文章就介绍到这了,更多相关go gin多文件上传内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

Go语言轻松实现邮件发送通知功能的完全指南

04-24

使用pprof分析golang内存泄露问题及解决

04-24

Go标准库常见错误分析和解决办法

04-24

go中空接口的具体使用

04-24

Go如何实现Websocket服务以及代理

04-24

go 异常处理panic和recover的简单实践

04-24

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论