Skip to content

文件存储

VEF Framework 提供了统一的文件存储接口,支持本地文件系统和 MinIO 对象存储。

存储配置

yaml
# config.yaml
storage:
  driver: "minio"  # local or minio
  
  local:
    basePath: "./uploads"
    baseUrl: "/uploads"
  
  minio:
    endpoint: "localhost:9000"
    accessKey: "minioadmin"
    secretKey: "minioadmin"
    bucket: "myapp"
    useSSL: false
    region: "us-east-1"

基本用法

获取存储实例

go
import "github.com/ilxqx/vef-framework-go/storage"

// Get default storage
s := storage.Default()

// Get named storage
s := storage.Get("images")

上传文件

go
// Upload from file path
url, err := s.Put("avatars/user-001.jpg", "/tmp/avatar.jpg")

// Upload from bytes
url, err := s.PutBytes("documents/report.pdf", pdfBytes)

// Upload from reader
url, err := s.PutReader("files/data.csv", reader, size)

下载文件

go
// Download to file
err := s.Get("avatars/user-001.jpg", "/tmp/downloaded.jpg")

// Get as bytes
data, err := s.GetBytes("documents/report.pdf")

// Get as reader
reader, err := s.GetReader("files/data.csv")

删除文件

go
// Delete single file
err := s.Delete("avatars/user-001.jpg")

// Delete multiple files
err := s.DeleteMany("avatars/user-001.jpg", "avatars/user-002.jpg")

文件上传 API

处理文件上传

go
func (r *FileResource) Upload(ctx fiber.Ctx) error {
    // Get uploaded file
    file, err := ctx.FormFile("file")
    if err != nil {
        return api.Error(ctx, "No file uploaded")
    }
    
    // Validate file
    if file.Size > 10*1024*1024 { // 10MB limit
        return api.Error(ctx, "File too large")
    }
    
    // Generate unique filename
    ext := filepath.Ext(file.Filename)
    filename := fmt.Sprintf("%s%s", uuid.New().String(), ext)
    path := fmt.Sprintf("uploads/%s/%s", time.Now().Format("2006/01/02"), filename)
    
    // Open file
    src, err := file.Open()
    if err != nil {
        return api.Error(ctx, "Failed to read file")
    }
    defer src.Close()
    
    // Upload to storage
    url, err := storage.Default().PutReader(path, src, file.Size)
    if err != nil {
        return api.Error(ctx, "Failed to upload file")
    }
    
    return api.Success(ctx, map[string]string{
        "url":      url,
        "filename": file.Filename,
        "size":     fmt.Sprintf("%d", file.Size),
    })
}

文件命名约定

生成唯一文件名

go
func generateFilePath(originalName string) string {
    ext := filepath.Ext(originalName)
    date := time.Now().Format("2006/01/02")
    uuid := uuid.New().String()
    return fmt.Sprintf("uploads/%s/%s%s", date, uuid, ext)
}

按类型组织

go
func getStoragePath(fileType, filename string) string {
    date := time.Now().Format("2006/01/02")
    
    switch fileType {
    case "avatar":
        return fmt.Sprintf("avatars/%s/%s", date, filename)
    case "document":
        return fmt.Sprintf("documents/%s/%s", date, filename)
    case "image":
        return fmt.Sprintf("images/%s/%s", date, filename)
    default:
        return fmt.Sprintf("files/%s/%s", date, filename)
    }
}

文件验证

go
// Allowed file types
var allowedTypes = map[string][]string{
    "image":    {".jpg", ".jpeg", ".png", ".gif", ".webp"},
    "document": {".pdf", ".doc", ".docx", ".xls", ".xlsx"},
}

func validateFile(file *multipart.FileHeader, fileType string) error {
    // Check file size
    maxSize := int64(10 * 1024 * 1024) // 10MB
    if file.Size > maxSize {
        return errors.New("file too large")
    }
    
    // Check file extension
    ext := strings.ToLower(filepath.Ext(file.Filename))
    allowed := allowedTypes[fileType]
    
    for _, a := range allowed {
        if ext == a {
            return nil
        }
    }
    
    return errors.New("file type not allowed")
}

图片处理

go
import "github.com/disintegration/imaging"

func processImage(src io.Reader, maxWidth, maxHeight int) ([]byte, error) {
    // Decode image
    img, err := imaging.Decode(src)
    if err != nil {
        return nil, err
    }
    
    // Resize if needed
    bounds := img.Bounds()
    if bounds.Dx() > maxWidth || bounds.Dy() > maxHeight {
        img = imaging.Fit(img, maxWidth, maxHeight, imaging.Lanczos)
    }
    
    // Encode to JPEG
    var buf bytes.Buffer
    if err := imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(85)); err != nil {
        return nil, err
    }
    
    return buf.Bytes(), nil
}

完整文件上传示例

go
type FileResource struct {
    api.Resource
    storage storage.Storage
}

func NewFileResource(s storage.Storage) api.Resource {
    resource := &FileResource{
        Resource: api.NewResource("file"),
        storage:  s,
    }
    
    resource.RegisterApi("upload", resource.Upload)
    resource.RegisterApi("delete", resource.Delete)
    
    return resource
}

func (r *FileResource) Upload(ctx fiber.Ctx) error {
    file, err := ctx.FormFile("file")
    if err != nil {
        return api.Error(ctx, "No file uploaded")
    }
    
    fileType := ctx.FormValue("type", "file")
    
    // Validate file
    if err := validateFile(file, fileType); err != nil {
        return api.Error(ctx, err.Error())
    }
    
    // Generate path
    path := getStoragePath(fileType, generateFilename(file.Filename))
    
    // Open file
    src, err := file.Open()
    if err != nil {
        return api.Error(ctx, "Failed to read file")
    }
    defer src.Close()
    
    // Upload
    url, err := r.storage.PutReader(path, src, file.Size)
    if err != nil {
        return api.Error(ctx, "Failed to upload file")
    }
    
    // Save file record
    record := &models.FileRecord{
        Path:         path,
        Url:          url,
        OriginalName: file.Filename,
        Size:         file.Size,
        Type:         fileType,
        UploadedBy:   contextx.GetCurrentUser(ctx).Id,
    }
    orm.DB().Create(record)
    
    return api.Success(ctx, record)
}

下一步

基于 Apache License 2.0 许可发布