文件存储
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)
}