文件上传
1. 功能概述
NexFrame框架提供了完整的文件上传支持,包括:
单文件和多文件上传
不同类型文件的分类上传
文件类型验证
文件大小限制
自定义存储路径
文件元数据处理
2. 后端实现
2.1 API 定义
// 基本的文件上传请求结构
type ContentUploadReq struct {
g.Meta `path:"/upload" method:"POST" summary:"上传文件" tags:"内容管理"`
OrgKey string `json:"orgKey" description:"组织标识" v:"required#组织标识不能为空"`
SpaceKey string `json:"spaceKey" description:"所属空间标识" v:"required#空间标识不能为空"`
File []meta.FileUploadMeta `json:"file" description:"上传文件" v:"required#上传文件不能为空"`
}
// 上传响应结构
type ContentUploadRes struct {
Files []FileInfo `json:"files"` // 返回上传后的文件信息
}
// 文件信息结构
type FileInfo struct {
FileName string `json:"fileName"` // 文件名
Path string `json:"path"` // 文件路径
Size int64 `json:"size"` // 文件大小
Type string `json:"type"` // 文件类型
}
2.2 控制器实现
// 控制器定义
type ContentController struct {
F *nf.APIFramework
Service *services.ContentService
}
// 文件上传处理方法
func (c *ContentController) Upload(ctx context.Context, req *ContentUploadReq) (res *ContentUploadRes, err error) {
res = &ContentUploadRes{
Files: make([]FileInfo, 0, len(req.File)),
}
for _, fileMeta := range req.File {
// 文件验证
if err := validateFile(fileMeta); err != nil {
return nil, err
}
// 生成存储路径
savePath := filepath.Join("uploads", req.OrgKey, req.SpaceKey, fileMeta.FileName)
// 保存文件
if err := fileMeta.SaveTo(savePath); err != nil {
return nil, fmt.Errorf("保存文件失败: %w", err)
}
// 添加文件信息到响应
res.Files = append(res.Files, FileInfo{
FileName: fileMeta.FileName,
Path: savePath,
Size: fileMeta.Size,
Type: fileMeta.ContentType,
})
}
return res, nil
}
// 文件验证函数
func validateFile(file meta.FileUploadMeta) error {
// 检查文件大小
if file.Size > maxFileSize {
return fmt.Errorf("文件 %s 超过大小限制", file.FileName)
}
// 检查文件类型
if !isAllowedType(file.ContentType) {
return fmt.Errorf("不支持的文件类型:%s", file.ContentType)
}
return nil
}
2.3 多类型文件上传
// 多类型文件上传请求
type MultiTypeUploadReq struct {
g.Meta `path:"/upload/multi" method:"POST" summary:"多类型文件上传" tags:"内容管理"`
OrgKey string `json:"orgKey" v:"required#组织标识不能为空"`
SpaceKey string `json:"spaceKey" v:"required#空间标识不能为空"`
Images []meta.FileUploadMeta `json:"images" description:"图片文件"`
Documents []meta.FileUploadMeta `json:"documents" description:"文档文件"`
Videos []meta.FileUploadMeta `json:"videos" description:"视频文件"`
}
// 处理方法
func (c *ContentController) MultiTypeUpload(ctx context.Context, req *MultiTypeUploadReq) (*ContentUploadRes, error) {
res := &ContentUploadRes{
Files: make([]FileInfo, 0),
}
// 处理图片
for _, img := range req.Images {
if !isImageFile(img.ContentType) {
return nil, fmt.Errorf("非图片文件:%s", img.FileName)
}
// 处理图片上传...
}
// 处理文档
for _, doc := range req.Documents {
if !isDocumentFile(doc.ContentType) {
return nil, fmt.Errorf("非文档文件:%s", doc.FileName)
}
// 处理文档上传...
}
// 处理视频
for _, video := range req.Videos {
if !isVideoFile(video.ContentType) {
return nil, fmt.Errorf("非视频文件:%s", video.FileName)
}
// 处理视频上传...
}
return res, nil
}
3. 前端实现
3.1 基础HTML表单
<!-- 基础上传表单 -->
<form id="uploadForm">
<input type="text" name="orgKey" required />
<input type="text" name="spaceKey" required />
<input type="file" name="file" multiple required />
<button type="submit">上传</button>
</form>
<script>
document.getElementById('uploadForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
};
</script>
3.2 多类型文件上传
<form id="multiUploadForm">
<input type="text" name="orgKey" required />
<input type="text" name="spaceKey" required />
<div>
<label>图片文件:</label>
<input type="file" name="images" multiple accept="image/*" />
</div>
<div>
<label>文档文件:</label>
<input type="file" name="documents" multiple accept=".pdf,.doc,.docx" />
</div>
<div>
<label>视频文件:</label>
<input type="file" name="videos" multiple accept="video/*" />
</div>
<button type="submit">上传</button>
</form>
<script>
document.getElementById('multiUploadForm').onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('/api/upload/multi', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
};
</script>
3.3 使用 Vue 实现
<template>
<div class="upload-component">
<div class="form-inputs">
<input v-model="form.orgKey" placeholder="组织标识" />
<input v-model="form.spaceKey" placeholder="空间标识" />
</div>
<div class="upload-area"
@drop.prevent="handleDrop"
@dragover.prevent="dragover = true"
@dragleave.prevent="dragover = false"
:class="{ 'dragover': dragover }">
<div class="upload-prompt">
<span v-if="files.length">已选择 {{ files.length }} 个文件</span>
<span v-else>拖拽文件到此处或点击选择文件</span>
</div>
<input ref="fileInput"
type="file"
multiple
@change="handleFileChange"
style="display: none" />
<button @click="$refs.fileInput.click()"
type="button">
选择文件
</button>
</div>
<div v-if="files.length" class="file-list">
<div v-for="(file, index) in files"
:key="index"
class="file-item">
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatSize(file.size) }}</span>
<button @click="removeFile(index)"
type="button"
class="remove-btn">
删除
</button>
</div>
</div>
<button @click="handleUpload"
:disabled="!canUpload || uploading"
class="upload-btn">
{{ uploading ? '上传中...' : '开始上传' }}
</button>
<div v-if="progress !== null" class="progress">
上传进度: {{ progress }}%
</div>
</div>
</template>
<script>
export default {
data() {
return {
form: {
orgKey: '',
spaceKey: ''
},
files: [],
dragover: false,
uploading: false,
progress: null
}
},
computed: {
canUpload() {
return this.files.length > 0 &&
this.form.orgKey &&
this.form.spaceKey &&
!this.uploading;
}
},
methods: {
handleDrop(e) {
this.dragover = false;
this.addFiles(e.dataTransfer.files);
},
handleFileChange(e) {
this.addFiles(e.target.files);
},
addFiles(fileList) {
this.files.push(...Array.from(fileList));
},
removeFile(index) {
this.files.splice(index, 1);
},
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
return `${size.toFixed(2)} ${units[unitIndex]}`;
},
async handleUpload() {
if (!this.canUpload) return;
const formData = new FormData();
formData.append('orgKey', this.form.orgKey);
formData.append('spaceKey', this.form.spaceKey);
this.files.forEach(file => {
formData.append('file', file);
});
this.uploading = true;
this.progress = 0;
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
onUploadProgress: (progressEvent) => {
this.progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
}
});
const result = await response.json();
if (result.code === 0) {
this.$emit('upload-success', result);
this.files = [];
this.$message.success('上传成功');
} else {
throw new Error(result.message);
}
} catch (error) {
this.$emit('upload-error', error);
this.$message.error('上传失败:' + error.message);
} finally {
this.uploading = false;
this.progress = null;
}
}
}
}
</script>
<style scoped>
.upload-area {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
cursor: pointer;
}
.upload-area.dragover {
border-color: #409eff;
background: #ecf5ff;
}
.file-list {
margin-top: 10px;
}
.file-item {
display: flex;
align-items: center;
padding: 5px;
margin: 5px 0;
background: #f5f5f5;
}
.file-name {
flex: 1;
}
.file-size {
margin: 0 10px;
color: #666;
}
.progress {
margin-top: 10px;
text-align: center;
}
.upload-btn {
margin-top: 10px;
width: 100%;
padding: 10px;
}
.upload-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
4. 注意事项
4.1 后端注意事项
文件大小限制
// 在配置文件中设置
const (
MaxFileSize = 10 << 20 // 单个文件最大10MB
MaxTotalSize = 50 << 20 // 总上传大小50MB
)
文件类型验证
// 允许的文件类型
var AllowedTypes = map[string][]string{
"images": {
"image/jpeg",
"image/png",
"image/gif",
},
"documents": {
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
},
"videos": {
"video/mp4",
"video/quicktime",
},
}
安全存储路径
// 安全的文件路径处理
safePath := filepath.Join(baseDir, filepath.Clean(filename))
if !strings.HasPrefix(safePath, baseDir) {
return errors.New("非法的文件路径")
}
4.2 前端注意事项
文件类型限制
<input type="file" accept="image/*,.pdf,.doc,.docx" />
文件大小检查
function validateFile(file) {
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
throw new Error(`文件大小不能超过${formatSize(maxSize)}`);
}
}
上传进度显示
fetch('/api/upload', {### 4.2 前端注意事项(续)
3. 上传进度显示
```javascript
// 使用 XMLHttpRequest 实现上传进度
function uploadWithProgress(formData, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
const progress = Math.round((event.loaded * 100) / event.total);
onProgress(progress);
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.response));
} else {
reject(new Error('上传失败'));
}
});
xhr.addEventListener('error', () => reject(new Error('网络错误')));
xhr.open('POST', '/api/upload');
xhr.send(formData);
});
}
// 使用示例
async function handleUpload() {
const formData = new FormData();
// ... 添加文件和其他数据
try {
const result = await uploadWithProgress(formData, (progress) => {
console.log(`上传进度: ${progress}%`);
});
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
}
文件预览
// 图片预览
function previewImage(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.readAsDataURL(file);
});
}
// 使用示例
async function handleFileSelect(files) {
for (const file of files) {
if (file.type.startsWith('image/')) {
const previewUrl = await previewImage(file);
// 显示图片预览
const img = document.createElement('img');
img.src = previewUrl;
previewContainer.appendChild(img);
}
}
}
断点续传实现
async function uploadChunks(file, chunkSize = 1024 * 1024) {
const chunks = Math.ceil(file.size / chunkSize);
const uploadedChunks = new Set();
for (let i = 0; i < chunks; i++) {
if (uploadedChunks.has(i)) continue;
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('chunkIndex', i);
formData.append('totalChunks', chunks);
formData.append('filename', file.name);
try {
await fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
uploadedChunks.add(i);
} catch (error) {
console.error(`Chunk ${i} upload failed:`, error);
// 可以稍后重试失败的块
}
}
}
5. 高级功能实现
5.1 图片处理
// 图片处理选项
type ImageProcessOptions struct {
MaxWidth int
MaxHeight int
Quality int
Format string
}
// 处理上传的图片
func processImage(file meta.FileUploadMeta, opts ImageProcessOptions) error {
// 打开图片
img, err := imaging.Open(file.FileHeader)
if err != nil {
return fmt.Errorf("打开图片失败: %w", err)
}
// 调整大小
if opts.MaxWidth > 0 || opts.MaxHeight > 0 {
img = imaging.Fit(img, opts.MaxWidth, opts.MaxHeight, imaging.Lanczos)
}
// 保存处理后的图片
return imaging.Save(img, file.Path, imaging.JPEGQuality(opts.Quality))
}
// 在控制器中使用
func (c *ContentController) UploadImage(ctx context.Context, req *ImageUploadReq) (*ImageUploadRes, error) {
for _, file := range req.Images {
if err := processImage(file, ImageProcessOptions{
MaxWidth: 1920,
MaxHeight: 1080,
Quality: 85,
Format: "jpeg",
}); err != nil {
return nil, err
}
}
return &ImageUploadRes{}, nil
}
5.2 大文件分片上传
// 分片上传请求
type ChunkUploadReq struct {
g.Meta `path:"/upload/chunk" method:"POST"`
File meta.FileUploadMeta `json:"chunk"`
ChunkIndex int `json:"chunkIndex"`
TotalChunks int `json:"totalChunks"`
FileHash string `json:"fileHash"`
FileName string `json:"fileName"`
}
// 分片上传处理
func (c *ContentController) UploadChunk(ctx context.Context, req *ChunkUploadReq) (*ChunkUploadRes, error) {
// 创建临时目录
tempDir := filepath.Join("uploads", "temp", req.FileHash)
if err := os.MkdirAll(tempDir, 0755); err != nil {
return nil, err
}
// 保存分片
chunkPath := filepath.Join(tempDir, fmt.Sprintf("%d", req.ChunkIndex))
if err := req.File.SaveTo(chunkPath); err != nil {
return nil, err
}
// 检查是否所有分片都已上传
if isAllChunksUploaded(tempDir, req.TotalChunks) {
// 合并文件
finalPath := filepath.Join("uploads", req.FileName)
if err := mergeChunks(tempDir, finalPath, req.TotalChunks); err != nil {
return nil, err
}
// 清理临时文件
os.RemoveAll(tempDir)
}
return &ChunkUploadRes{}, nil
}
// 检查分片是否完整
func isAllChunksUploaded(dir string, total int) bool {
for i := 0; i < total; i++ {
chunkPath := filepath.Join(dir, fmt.Sprintf("%d", i))
if _, err := os.Stat(chunkPath); os.IsNotExist(err) {
return false
}
}
return true
}
// 合并分片
func mergeChunks(tempDir, finalPath string, totalChunks int) error {
outFile, err := os.Create(finalPath)
if err != nil {
return err
}
defer outFile.Close()
for i := 0; i < totalChunks; i++ {
chunkPath := filepath.Join(tempDir, fmt.Sprintf("%d", i))
chunkData, err := os.ReadFile(chunkPath)
if err != nil {
return err
}
if _, err := outFile.Write(chunkData); err != nil {
return err
}
}
return nil
}
5.3 文件秒传实现
// 检查文件是否存在的请求
type CheckFileReq struct {
g.Meta `path:"/check/file" method:"POST"`
FileHash string `json:"fileHash"`
FileName string `json:"fileName"`
}
// 检查文件是否已存在
func (c *ContentController) CheckFile(ctx context.Context, req *CheckFileReq) (*CheckFileRes, error) {
// 查找文件记录
file, err := c.Service.FindFileByHash(ctx, req.FileHash)
if err != nil {
return nil, err
}
if file != nil {
// 文件已存在,直接返回文件信息
return &CheckFileRes{
Exists: true,
Path: file.Path,
}, nil
}
return &CheckFileRes{Exists: false}, nil
}
// 前端实现秒传
async function uploadFile(file) {
// 计算文件哈希
const hash = await calculateFileHash(file);
// 检查文件是否存在
const checkResult = await fetch('/api/check/file', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
fileHash: hash,
fileName: file.name
})
}).then(r => r.json());
if (checkResult.exists) {
// 文件已存在,直接使用
console.log('文件秒传成功');
return checkResult;
}
// 文件不存在,执行常规上传
return normalUpload(file, hash);
}
// 计算文件哈希
async function calculateFileHash(file) {
const chunkSize = 2 * 1024 * 1024; // 2MB chunks
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
for (let i = 0; i < chunks; i++) {
const chunk = await readChunk(file, i * chunkSize, chunkSize);
spark.append(chunk);
}
return spark.end();
}
function readChunk(file, start, size) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
const chunk = file.slice(start, start + size);
reader.readAsArrayBuffer(chunk);
});
}
6. 问题排查和最佳实践
6.1 常见问题及解决方案
上传失败
- 检查文件大小是否超限
- 验证文件类型是否允许
- 确认存储路径权限
- 查看服务器错误日志
上传超时
- 调整服务器超时设置
- 考虑使用分片上传
- 检查网络状况
内存使用过高
- 调整文件缓冲区大小
- 使用流式处理
- 启用分片上传
6.2 性能优化建议
服务器端优化
// 1. 使用缓冲区处理大文件
func copyWithBuffer(dst io.Writer, src io.Reader) error {
buf := make([]byte, 32*1024)
_, err := io.CopyBuffer(dst, src, buf)
return err
}
// 2. 并行处理多个文件
func processFilesParallel(files []meta.FileUploadMeta) error {
var wg sync.WaitGroup
errors := make(chan error, len(files))
for _, file := range files {
wg.Add(1)
go func(f meta.FileUploadMeta) {
defer wg.Done()
if err := processFile(f); err != nil {
errors <- err
}
}(file)
}
wg.Wait()
close(errors)
if len(errors) > 0 {
return <-errors
}
return nil
}
// 3. 使用临时文件
func handleLargeFile(file meta.FileUploadMeta) error {
tempFile, err := os.CreateTemp("", "upload-*")
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
// 处理文件...
return nil
}
前端优化
// 1. 压缩图片
async function compressImage(file) {
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true
};
try {
return await imageCompression(file, options);
} catch (error) {
console.error('压缩失败:', error);
return file;
}
}
// 2. 并行上传
async function uploadFiles(files) {
const promises = Array.from(files).map(file =>
uploadSingle(file)
);
return Promise.all(promises);
}
// 3. 失败重试
async function uploadWithRetry(file, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await uploadFile(file);
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
6.3 安全性建议
文件类型验证
func validateFileType(file meta.FileUploadMeta) error {
// 读取文件头
buffer := make([]byte, 512)
source, err := file.GetFile()
if err != nil {
return err
}
defer source.Close()
n, err := source.Read(buffer)
if err != nil && err != io.EOF {
return err
}
// 检查实际文件类型
contentType := http.DetectContentType(buffer[:n])
if !isAllowedType(contentType) {
return errors.New("不允许的文件类型")
}
return nil
}
路径安全检查
func validatePath(path string) error {
// 规范化路径
cleanPath := filepath.Clean(path)
// 检查是否包含父目录引用
if strings.Contains(cleanPath, "..") {
return errors.New("非法的文件路径")
}
// 检查是否在允许的目录内
absPath, err := filepath.Abs(cleanPath)
if err != nil {
return err
}
if !strings.HasPrefix(absPath, config.AllowedBasePath) {
return errors.New("路径超出允许范围")
}
return nil
}
文件扫描
func scanFile(file meta.FileUploadMeta) error {
// 检查文件大小
if file.Size > maxFileSize {
return errors.New("文件过大")
}
// 检查文件类型
if err := validateFileType(file); err != nil {
return err
}
// 检查文件内容(可### 6.3 安全性建议(续)
3. 文件扫描(续)
```go
// 文件安全扫描配置
type FileScanConfig struct {
// 文件类型白名单
AllowedTypes []string
// 文件大小限制
MaxFileSize int64
// 病毒扫描选项
VirusScan bool
// 内容检查选项
ContentCheck bool
}
// 文件安全扫描
func scanFile(file meta.FileUploadMeta, config FileScanConfig) error {
// 检查文件名安全性
if !isSecureFilename(file.FileName) {
return errors.New("不安全的文件名")
}
// 检查 MIME 类型
if !isAllowedMimeType(file.ContentType, config.AllowedTypes) {
return errors.New("不允许的文件类型")
}
// 文件大小检查
if file.Size > config.MaxFileSize {
return errors.New("文件超过大小限制")
}
// 病毒扫描
if config.VirusScan {
if err := scanVirus(file); err != nil {
return fmt.Errorf("病毒扫描失败: %w", err)
}
}
// 内容安全检查
if config.ContentCheck {
if err := checkContent(file); err != nil {
return fmt.Errorf("内容检查失败: %w", err)
}
}
return nil
}
// 文件名安全检查
func isSecureFilename(filename string) bool {
// 移除路径分隔符
filename = filepath.Base(filename)
// 检查文件名长度
if len(filename) > 255 {
return false
}
// 检查文件名字符
matched, err := regexp.MatchString(`^[a-zA-Z0-9\-_.]+$`, filename)
if err != nil || !matched {
return false
}
// 检查常见的危险扩展名
dangerousExts := []string{".exe", ".dll", ".so", ".sh", ".bat"}
ext := strings.ToLower(filepath.Ext(filename))
for _, dangerous := range dangerousExts {
if ext == dangerous {
return false
}
}
return true
}
7. 扩展功能
7.1 云存储支持
// 云存储接口
type StorageProvider interface {
Upload(ctx context.Context, file meta.FileUploadMeta) (string, error)
Download(ctx context.Context, path string) (io.ReadCloser, error)
Delete(ctx context.Context, path string) error
}
// OSS存储实现
type OSSStorage struct {
client *oss.Client
bucket *oss.Bucket
}
func (s *OSSStorage) Upload(ctx context.Context, file meta.FileUploadMeta) (string, error) {
objectKey := generateObjectKey(file)
// 使用临时文件避免内存占用
tempFile, err := os.CreateTemp("", "oss-upload-*")
if err != nil {
return "", err
}
defer os.Remove(tempFile.Name())
// 复制文件内容到临时文件
src, err := file.GetFile()
if err != nil {
return "", err
}
defer src.Close()
if _, err := io.Copy(tempFile, src); err != nil {
return "", err
}
// 上传到OSS
if err := s.bucket.PutObjectFromFile(objectKey, tempFile.Name()); err != nil {
return "", err
}
return objectKey, nil
}
7.2 自定义存储策略
// 存储策略接口
type StorageStrategy interface {
DetermineStorage(file meta.FileUploadMeta) StorageProvider
}
// 基于文件类型的存储策略
type TypeBasedStrategy struct {
providers map[string]StorageProvider
}
func (s *TypeBasedStrategy) DetermineStorage(file meta.FileUploadMeta) StorageProvider {
// 根据文件类型选择存储提供者
if strings.HasPrefix(file.ContentType, "image/") {
return s.providers["image"]
}
if strings.HasPrefix(file.ContentType, "video/") {
return s.providers["video"]
}
return s.providers["default"]
}
// 基于文件大小的存储策略
type SizeBasedStrategy struct {
smallFiles StorageProvider // < 10MB
mediumFiles StorageProvider // 10MB - 100MB
largeFiles StorageProvider // > 100MB
}
func (s *SizeBasedStrategy) DetermineStorage(file meta.FileUploadMeta) StorageProvider {
switch {
case file.Size < 10<<20:
return s.smallFiles
case file.Size < 100<<20:
return s.mediumFiles
default:
return s.largeFiles
}
}
7.3 文件处理管道
// 文件处理管道
type ProcessingPipeline struct {
processors []FileProcessor
}
// 文件处理器接口
type FileProcessor interface {
Process(file meta.FileUploadMeta) error
}
// 图片处理器
type ImageProcessor struct {
maxWidth int
maxHeight int
quality int
}
func (p *ImageProcessor) Process(file meta.FileUploadMeta) error {
if !strings.HasPrefix(file.ContentType, "image/") {
return nil
}
// 处理图片...
return nil
}
// 文档处理器
type DocumentProcessor struct {
convertToPDF bool
addWatermark bool
}
func (p *DocumentProcessor) Process(file meta.FileUploadMeta) error {
if !isDocument(file.ContentType) {
return nil
}
// 处理文档...
return nil
}
// 使用处理管道
func processFile(file meta.FileUploadMeta) error {
pipeline := &ProcessingPipeline{
processors: []FileProcessor{
&ImageProcessor{
maxWidth: 1920,
maxHeight: 1080,
quality: 85,
},
&DocumentProcessor{
convertToPDF: true,
addWatermark: true,
},
},
}
for _, processor := range pipeline.processors {
if err := processor.Process(file); err != nil {
return err
}
}
return nil
}
7.4 异步处理支持
// 异步任务接口
type UploadTask interface {
Process() error
GetStatus() TaskStatus
GetProgress() int
Cancel() error
}
// 任务状态
type TaskStatus string
const (
TaskPending TaskStatus = "pending"
TaskRunning TaskStatus = "running"
TaskComplete TaskStatus = "complete"
TaskFailed TaskStatus = "failed"
TaskCancelled TaskStatus = "cancelled"
)
// 异步上传任务
type AsyncUploadTask struct {
ID string
File meta.FileUploadMeta
Status TaskStatus
Progress int
Result string
Error error
cancel chan struct{}
}
func (t *AsyncUploadTask) Process() error {
t.Status = TaskRunning
// 启动处理协程
go func() {
defer func() {
if r := recover(); r != nil {
t.Error = fmt.Errorf("task panic: %v", r)
t.Status = TaskFailed
}
}()
// 处理文件...
for i := 0; i <= 100; i += 10 {
select {
case <-t.cancel:
t.Status = TaskCancelled
return
default:
t.Progress = i
time.Sleep(time.Second)
}
}
t.Status = TaskComplete
}()
return nil
}
// 任务管理器
type TaskManager struct {
tasks map[string]*AsyncUploadTask
mu sync.RWMutex
}
func (m *TaskManager) AddTask(file meta.FileUploadMeta) string {
m.mu.Lock()
defer m.mu.Unlock()
taskID := generateTaskID()
task := &AsyncUploadTask{
ID: taskID,
File: file,
Status: TaskPending,
cancel: make(chan struct{}),
}
m.tasks[taskID] = task
task.Process()
return taskID
}
func (m *TaskManager) GetTask(taskID string) (*AsyncUploadTask, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
task, exists := m.tasks[taskID]
return task, exists
}
8. 监控和日志
8.1 性能监控
// 性能指标收集
type UploadMetrics struct {
TotalUploads int64
TotalSize int64
TotalErrors int64
ProcessingTime time.Duration
ActiveUploads int32
}
var metrics = &UploadMetrics{}
// 监控中间件
func uploadMetricsMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
atomic.AddInt32(&metrics.ActiveUploads, 1)
defer atomic.AddInt32(&metrics.ActiveUploads, -1)
next(w, r)
atomic.AddInt64(&metrics.TotalUploads, 1)
atomic.AddInt64(&metrics.ProcessingTime, time.Since(start).Nanoseconds())
}
}
// 指标导出
func exportMetrics() {
prometheus.MustRegister(
prometheus.NewCounterFunc(
prometheus.CounterOpts{
Name: "upload_total",
Help: "Total number of uploads",
},
func() float64 {
return float64(atomic.LoadInt64(&metrics.TotalUploads))
},
),
)
}
8.2 日志记录
// 日志结构
type UploadLog struct {
Time time.Time
FileName string
FileSize int64
FileType string
Duration time.Duration
Status string
Error string
ClientIP string
}
// 日志记录器
type UploadLogger struct {
logger *zap.Logger
}
func (l *UploadLogger) LogUpload(ctx context.Context, file meta.FileUploadMeta, duration time.Duration, err error) {
logger := l.logger.With(
zap.String("filename", file.FileName),
zap.Int64("size", file.Size),
zap.String("type", file.ContentType),
zap.Duration("duration", duration),
)
if err != nil {
logger.Error("Upload failed",
zap.Error(err))
} else {
logger.Info("Upload successful")
}
}
9. 示例代码和最佳实践
9.1 完整的上传控制器示例
// 完整的文件上传控制器
type UploadController struct {
F *nf.APIFramework
storage StorageProvider
logger *UploadLogger
metrics *UploadMetrics
}
// 上传处理
func (c *UploadController) Upload(ctx context.Context, req *UploadRequest) (*UploadResponse, error) {
start := time.Now()
// 记录上传开始
c.logger.LogStart(ctx, req)
defer func() {
c.logger.LogEnd(ctx, time.Since(start))
}()
// 验证请求
if err := c.validateRequest(req); err != nil {
return nil, err
}
// 处理文件
results := make([]UploadResult, 0, len(req.Files))
for _, file := range req.Files {
result, err := c.processFile(ctx, file)
if err != nil {
return nil, err
}
results = append(results, result)
}
// 返回结果
return &UploadResponse{
Results: results,
}, nil
}
// 文件处理
func (c *UploadController) processFile(ctx context.Context, file meta.FileUploadMeta) (UploadResult, error) {
// 文件验证
if err := c.validateFile(file); err != nil {
return UploadResult{}, err
}
// 文件处理
processed, err := c.processingPipeline.Process(file)
if err != nil {
return UploadResult{}, err
}
// 存储文件
path, err := c.storage.Store(ctx, processed)
if err != nil {
return UploadResult{}, err
}
// 返回结果
return UploadResult{
FileName: file.FileName,
Path: path,
Size: file.Size,
Type: file.ContentType,
}, nil
}
9.2 完整的前端组件示例
<template>
<div class="upload-component">
<div class="upload-header">
<h3>文件上传</h3>
<div class="upload-stats" v-if="stats.total > 0">
已上传: {{ stats.successful }}/{{ stats.total }}
失败: {{ stats.failed }}
</div>
</div>
<div
class="upload-dropzone"
:class="{ 'is-dragover': isDragging }"
@dragenter.prevent="isDragging = true"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="handleDrop"
@click="$refs.fileInput.click()"
>
<input
type="file"
ref="fileInput"
multiple
:accept="acceptedTypes"
@change="handleFileSelect"
class="hidden"
/>
<div class="upload-prompt">
<i class="upload-icon"></i>
<p>拖拽文件到此处或点击选择文件</p>
<p class="upload-hint">
支持的文件类型: {{ supportedTypes.join(', ') }}
<br>
单文件最大: {{ formatSize(maxFileSize) }}
</p>
</div>
</div>
<div v-if="### 9.2 完整的前端组件示例(续)
```vue
<div v-if="files.length" class="file-list">
<div
v-for="(file, index) in files"
:key="file.id"
class="file-item"
:class="{ 'is-error': file.error }"
>
<div class="file-info">
<span class="file-name">{{ file.name }}</span>
<span class="file-size">{{ formatSize(file.size) }}</span>
<span class="file-type">{{ file.type }}</span>
</div>
<div class="file-status">
<template v-if="file.status === 'pending'">
<button
@click="uploadFile(file)"
class="upload-btn"
>
上传
</button>
<button
@click="removeFile(index)"
class="remove-btn"
>
删除
</button>
</template>
<template v-else-if="file.status === 'uploading'">
<div class="progress-bar">
<div
class="progress"
:style="{ width: file.progress + '%' }"
></div>
<span class="progress-text">
{{ file.progress }}%
</span>
</div>
<button
@click="cancelUpload(file)"
class="cancel-btn"
>
取消
</button>
</template>
<template v-else-if="file.status === 'success'">
<span class="success-text">上传成功</span>
</template>
<template v-else-if="file.status === 'error'">
<span class="error-text">{{ file.error }}</span>
<button
@click="retryUpload(file)"
class="retry-btn"
>
重试
</button>
</template>
</div>
</div>
</div>
<div class="upload-actions" v-if="hasPendingFiles">
<button
@click="uploadAllFiles"
:disabled="uploading"
class="upload-all-btn"
>
{{ uploading ? '上传中...' : '上传全部' }}
</button>
</div>
<div class="upload-tips">
<h4>上传提示:</h4>
<ul>
<li>支持拖拽上传或点击选择文件</li>
<li>单个文件大小不超过{{ formatSize(maxFileSize) }}</li>
<li>支持批量上传,单次最多{{ maxFiles }}个文件</li>
<li>上传失败的文件可以重试</li>
<li>上传过程中可以取消</li>
</ul>
</div>
</div>
</template>
<script>
export default {
name: 'FileUploader',
props: {
// 支持的文件类型
acceptedTypes: {
type: String,
default: '*/*'
},
// 最大文件大小(字节)
maxFileSize: {
type: Number,
default: 10 * 1024 * 1024 // 10MB
},
// 最大文件数
maxFiles: {
type: Number,
default: 10
},
// 自动上传
autoUpload: {
type: Boolean,
default: false
}
},
data() {
return {
files: [],
isDragging: false,
uploading: false,
stats: {
total: 0,
successful: 0,
failed: 0
}
}
},
computed: {
hasPendingFiles() {
return this.files.some(f => f.status === 'pending')
},
supportedTypes() {
return this.acceptedTypes.split(',').map(t => t.trim())
}
},
methods: {
// 处理文件选择
handleFileSelect(event) {
const selectedFiles = Array.from(event.target.files)
this.addFiles(selectedFiles)
event.target.value = null // 重置input,允许选择相同文件
},
// 处理拖放
handleDrop(event) {
this.isDragging = false
const droppedFiles = Array.from(event.dataTransfer.files)
this.addFiles(droppedFiles)
},
// 添加文件
addFiles(newFiles) {
// 验证文件
const validFiles = newFiles.filter(file => {
if (file.size > this.maxFileSize) {
this.showError(`文件 ${file.name} 超过大小限制`)
return false
}
if (!this.isTypeAllowed(file.type)) {
this.showError(`不支持的文件类型:${file.type}`)
return false
}
return true
})
// 检查文件数量限制
if (this.files.length + validFiles.length > this.maxFiles) {
this.showError(`最多只能上传 ${this.maxFiles} 个文件`)
return
}
// 添加文件到列表
validFiles.forEach(file => {
this.files.push({
id: Date.now() + Math.random(),
file,
name: file.name,
size: file.size,
type: file.type,
status: 'pending',
progress: 0,
error: null
})
})
// 自动上传
if (this.autoUpload) {
this.uploadAllFiles()
}
},
// 上传单个文件
async uploadFile(file) {
if (file.status === 'uploading') return
file.status = 'uploading'
file.error = null
this.stats.total++
try {
const formData = new FormData()
formData.append('file', file.file)
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
onUploadProgress: (e) => {
file.progress = Math.round((e.loaded * 100) / e.total)
}
})
if (!response.ok) {
throw new Error('上传失败')
}
const result = await response.json()
if (result.code !== 0) {
throw new Error(result.message || '上传失败')
}
file.status = 'success'
this.stats.successful++
this.$emit('upload-success', file, result.data)
} catch (error) {
file.status = 'error'
file.error = error.message
this.stats.failed++
this.$emit('upload-error', file, error)
}
},
// 上传所有文件
async uploadAllFiles() {
this.uploading = true
const pending = this.files.filter(f => f.status === 'pending')
try {
await Promise.all(pending.map(file => this.uploadFile(file)))
} finally {
this.uploading = false
}
},
// 重试上传
retryUpload(file) {
file.status = 'pending'
file.progress = 0
file.error = null
this.uploadFile(file)
},
// 取消上传
cancelUpload(file) {
// TODO: 实现取消上传逻辑
file.status = 'pending'
file.progress = 0
},
// 移除文件
removeFile(index) {
this.files.splice(index, 1)
},
// 工具方法
isTypeAllowed(type) {
return this.acceptedTypes === '*/*' ||
this.supportedTypes.some(t => type.match(new RegExp(t.replace('*', '.*'))))
},
formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
return `${size.toFixed(2)} ${units[unitIndex]}`
},
showError(message) {
this.$emit('error', message)
// 如果使用element-ui等UI库
// this.$message.error(message)
}
}
}
</script>
<style scoped>
.upload-component {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.upload-dropzone {
border: 2px dashed #ccc;
border-radius: 4px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.upload-dropzone.is-dragover {
background-color: #f8f9fa;
border-color: #409eff;
}
.file-list {
margin-top: 20px;
}
.file-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
margin: 5px 0;
background: #f8f9fa;
border-radius: 4px;
}
.file-item.is-error {
background: #fff2f0;
}
.progress-bar {
width: 200px;
height: 6px;
background: #eee;
border-radius: 3px;
overflow: hidden;
}
.progress {
height: 100%;
background: #409eff;
transition: width 0.3s ease;
}
/* 其他样式... */
</style>
10. 常见问题解答
10.1 如何处理大文件上传?
分片上传
前端将大文件切分成小块
后端合并文件块
支持断点续传
使用流式处理
避免一次性加载整个文件到内存
使用缓冲区处理
监控内存使用
设置适当的文件大小限制
使用临时文件存储
10.2 如何优化上传性能?
前端优化
压缩文件
并发上传
预览优化
后端优化
使用对象池
异步处理
合理的超时设置
存储优化
使用合适的存储介质
实现缓存机制
CDN 加速
10.3 如何保证上传安全?
文件验证
类型检查
大小限制
内容扫描
路径安全
规范化路径
防止目录遍历
权限控制
访问控制
用户认证
上传限制
日志记录
结语
本手册详细介绍了 NexFrame 框架的文件上传功能的实现和使用。通过合理使用这些功能,可以构建安全、高效的文件上传服务。如果遇到问题,请查看相关章节或联系技术支持。
最后更新于