后端
package main
import (
"errors"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const (
uploadDir = "uploads"
maxUploadSize = 10 << 20 // 10MB
)
type UploadRes struct {
OriginFileName string `json:"originFileName"`
SavedFileName string `json:"savedFileName"`
}
func main() {
r := gin.Default()
// 更完善的CORS配置
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"POST", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// 确保上传目录存在
if err := os.MkdirAll(uploadDir, 0755); err != nil {
log.Fatalf("创建上传目录失败: %v", err)
}
r.Static("./static", ".")
// 文件上传接口
r.POST("/upload", func(c *gin.Context) {
// 获取文件
form, err := c.MultipartForm()
if err != nil {
if errors.Is(err, http.ErrNotMultipart) {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求不是multipart格式"})
} else if errors.Is(http.ErrMissingBoundary, err) {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少multipart边界"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件获取失败: " + err.Error()})
}
return
}
files := form.File["files"]
if len(files) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "没有上传文件"})
return
}
var ress = make([]UploadRes, 0, len(files))
for _, file := range files {
if file.Size > maxUploadSize {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小超过限制"})
return
}
// 安全的文件名生成
savedName := fmt.Sprintf("%d_%s%s",
time.Now().UnixNano(),
uuid.New().String(), // 添加随机字符串防止冲突
filepath.Ext(file.Filename))
savePath := filepath.Join(uploadDir, savedName)
// 保存文件
if err := c.SaveUploadedFile(file, savePath); err != nil {
log.Printf("保存文件失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
ress = append(ress, UploadRes{OriginFileName: file.Filename, SavedFileName: savedName})
}
c.JSON(http.StatusOK, gin.H{
"message": "上传成功",
"results": ress,
})
})
log.Println("服务启动在 :8080")
if err := r.Run(":8080"); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}
前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.upload-container {
max-width: 600px;
margin: auto;
}
.file-input {
display: block;
margin-bottom: 20px;
}
.submit-btn {
padding: 10px 20px;
background-color:
color: white;
border: none;
cursor: pointer;
}
.submit-btn:hover {
background-color:
}
.response-message {
margin-top: 20px;
font-weight: bold;
}
.selected-files {
margin-top: 20px;
}
.uploaded-files {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="upload-container">
<h1>文件上传</h1>
<form id="upload-form" enctype="multipart/form-data">
<input type="file" name="files" multiple class="file-input" id="file-input">
<button type="submit" class="submit-btn">上传文件</button>
</form>
<div class="selected-files" id="selected-files"></div>
<div class="response-message" id="response-message"></div>
<div class="uploaded-files" id="uploaded-files"></div>
</div>
<script>
document.getElementById('file-input').addEventListener('change', function(event) {
const files = event.target.files;
const selectedFilesElement = document.getElementById('selected-files');
if (files.length > 0) {
selectedFilesElement.innerHTML = '<p><strong>已选择的文件:</strong></p>';
const ul = document.createElement('ul');
for (let i = 0; i < files.length; i++) {
const li = document.createElement('li');
li.textContent = files[i].name;
ul.appendChild(li);
}
selectedFilesElement.appendChild(ul);
} else {
selectedFilesElement.innerHTML = '';
}
});
document.getElementById('upload-form').addEventListener('submit', function(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
const messageElement = document.getElementById('response-message');
const uploadedFilesElement = document.getElementById('uploaded-files');
if (data.error) {
messageElement.style.color = 'red';
messageElement.textContent = data.error;
uploadedFilesElement.innerHTML = '';
} else {
messageElement.style.color = 'green';
messageElement.textContent = '上传成功!';
// 清空文件输入字段
document.getElementById('file-input').value = '';
document.getElementById('selected-files').innerHTML = '';
// 展示上传结果
uploadedFilesElement.innerHTML = '<p><strong>已保存的文件:</strong></p>';
const ul = document.createElement('ul');
data.results.forEach(result => {
const li = document.createElement('li');
li.textContent = `原始文件名: ${result.originFileName}, 保存文件名: ${result.savedFileName}`;
ul.appendChild(li);
});
uploadedFilesElement.appendChild(ul);
}
})
.catch(error => {
console.error('Error:', error);
document.getElementById('response-message').textContent = '发生错误,请重试。';
document.getElementById('uploaded-files').innerHTML = '';
});
});
</script>
</body>
</html>
运行截图
