Files
go-common/docs/storage.md
2026-01-30 21:40:21 +08:00

13 KiB
Raw Blame History

存储工具文档

概述

存储工具提供了文件上传和查看功能,支持 本地文件夹(Local)、OSS 和 MinIO 三种存储方式并提供HTTP处理器用于文件上传和代理查看。

功能特性

  • 支持本地文件夹存储Local
  • 支持OSS对象存储阿里云、腾讯云、AWS、七牛云等
  • 支持MinIO对象存储
  • 提供统一的存储接口
  • 支持文件上传HTTP处理器
  • 支持文件代理查看HTTP处理器
  • 支持文件大小和扩展名限制
  • 自动生成唯一文件名
  • 支持自定义对象键前缀

使用方法

0. 工厂调用方式(推荐)

当你使用 factory 黑盒模式时,外部项目无需关心底层是 Local/MinIO/OSS

import (
    "context"
    "os"

    "git.toowon.com/jimmy/go-common/factory"
    "git.toowon.com/jimmy/go-common/storage"
)

fac, _ := factory.NewFactoryFromFile("./config.json")

f, _ := os.Open("test.jpg")
defer f.Close()

objectKey := storage.GenerateObjectKeyWithDate("uploads/images", "test.jpg")
url, err := fac.UploadFile(context.Background(), objectKey, f, "image/jpeg")
if err != nil {
    panic(err)
}
_ = url

1. 创建存储实例

import (
    "git.toowon.com/jimmy/go-common/config"
    "git.toowon.com/jimmy/go-common/storage"
)

// 加载配置
cfg, err := config.LoadFromFile("./config.json")
if err != nil {
    log.Fatal(err)
}

// 创建OSS存储实例
ossStorage, err := storage.NewStorage(storage.StorageTypeOSS, cfg)
if err != nil {
    log.Fatal(err)
}

// 创建MinIO存储实例
minioStorage, err := storage.NewStorage(storage.StorageTypeMinIO, cfg)
if err != nil {
    log.Fatal(err)
}

// 创建本地存储实例
localStorage, err := storage.NewStorage(storage.StorageTypeLocal, cfg)
if err != nil {
    log.Fatal(err)
}

2. 上传文件

import (
    "context"
    "os"
    "git.toowon.com/jimmy/go-common/storage"
)

// 打开文件
file, err := os.Open("test.jpg")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

// 上传文件
ctx := context.Background()
objectKey := "images/test.jpg"
err = ossStorage.Upload(ctx, objectKey, file, "image/jpeg")
if err != nil {
    log.Fatal(err)
}

// 获取文件URL
url, err := ossStorage.GetURL(objectKey, 0)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("File URL: %s\n", url)

2.1 本地存储配置示例

config.json 增加 localStorage 配置段:

{
  "localStorage": {
    "baseDir": "./uploads",
    "publicURL": "http://localhost:8080/file?key={objectKey}"
  }
}

说明:

  • baseDir:文件保存根目录
  • publicURL:用于 GetURL() 返回对外可访问的 URL
    • 推荐配合本文的 ProxyHandler,示例 http://localhost:8080/file?key={objectKey}
    • {objectKey} 会自动做 url.QueryEscape 处理

3. 使用HTTP处理器上传文件

import (
    "net/http"
    "git.toowon.com/jimmy/go-common/storage"
)

// 创建上传处理器
uploadHandler := storage.NewUploadHandler(storage.UploadHandlerConfig{
    Storage:      ossStorage,
    MaxFileSize:  10 * 1024 * 1024, // 10MB
    AllowedExts:  []string{".jpg", ".jpeg", ".png", ".gif"},
    ObjectPrefix: "images/",
})

// 注册路由
http.Handle("/upload", uploadHandler)
http.ListenAndServe(":8080", nil)

上传请求示例:

curl -X POST http://localhost:8080/upload \
  -F "file=@test.jpg" \
  -F "prefix=images/"

响应示例:

{
  "code": 0,
  "message": "Upload successful",
  "timestamp": 1704067200,
  "data": {
    "objectKey": "images/test_1704067200000000000.jpg",
    "url": "https://bucket.oss-cn-hangzhou.aliyuncs.com/images/test_1704067200000000000.jpg",
    "size": 102400,
    "contentType": "image/jpeg",
    "uploadTime": "2024-01-01T12:00:00Z"
  }
}

4. 使用HTTP处理器查看文件

import (
    "net/http"
    "git.toowon.com/jimmy/go-common/storage"
)

// 创建代理查看处理器
proxyHandler := storage.NewProxyHandler(ossStorage)

// 注册路由
http.Handle("/file", proxyHandler)
http.ListenAndServe(":8080", nil)

本地存储建议搭配:

  • POST /upload 上传文件(返回 url
  • GET /file?key=... 通过代理读取本地文件并返回二进制内容

查看请求示例:

GET /file?key=images/test.jpg

5. 生成对象键

import "git.toowon.com/jimmy/go-common/storage"

// 生成简单对象键
objectKey := storage.GenerateObjectKey("images/", "test.jpg")
// 输出: "images/test.jpg"

// 生成带日期的对象键
objectKey := storage.GenerateObjectKeyWithDate("images", "test.jpg")
// 输出: "images/2024/01/01/test.jpg"

6. 删除文件

ctx := context.Background()
err := ossStorage.Delete(ctx, "images/test.jpg")
if err != nil {
    log.Fatal(err)
}

7. 检查文件是否存在

ctx := context.Background()
exists, err := ossStorage.Exists(ctx, "images/test.jpg")
if err != nil {
    log.Fatal(err)
}
if exists {
    fmt.Println("File exists")
}

API 参考

Storage 接口

type Storage interface {
    // Upload 上传文件
    Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error

    // GetURL 获取文件访问URL
    GetURL(objectKey string, expires int64) (string, error)

    // Delete 删除文件
    Delete(ctx context.Context, objectKey string) error

    // Exists 检查文件是否存在
    Exists(ctx context.Context, objectKey string) (bool, error)

    // GetObject 获取文件内容
    GetObject(ctx context.Context, objectKey string) (io.ReadCloser, error)
}

NewStorage(storageType StorageType, cfg *config.Config) (Storage, error)

创建存储实例。

参数:

  • storageType: 存储类型(storage.StorageTypeOSSstorage.StorageTypeMinIO
  • cfg: 配置对象

返回: 存储实例和错误信息

UploadHandler

文件上传HTTP处理器。

NewUploadHandler(cfg UploadHandlerConfig) *UploadHandler

创建上传处理器。

配置参数:

  • Storage: 存储实例
  • MaxFileSize: 最大文件大小字节0表示不限制
  • AllowedExts: 允许的文件扩展名,空表示不限制
  • ObjectPrefix: 对象键前缀

请求格式

  • 方法: POST
  • 表单字段:
    • file: 文件(必需)
    • prefix: 对象键前缀(可选,会覆盖配置中的前缀)

响应格式

{
  "code": 0,
  "message": "Upload successful",
  "timestamp": 1704067200,
  "data": {
    "objectKey": "images/test.jpg",
    "url": "https://...",
    "size": 102400,
    "contentType": "image/jpeg",
    "uploadTime": "2024-01-01T12:00:00Z"
  }
}

ProxyHandler

文件代理查看HTTP处理器。

NewProxyHandler(storage Storage) *ProxyHandler

创建代理查看处理器。

请求格式

  • 方法: GET
  • URL参数:
    • key: 对象键(必需)

响应

  • 成功直接返回文件内容二进制设置适当的Content-Type
  • 错误返回标准HTTP错误状态码和错误消息文本格式
    • 400 Bad Request: 缺少必需参数
    • 404 Not Found: 文件不存在
    • 405 Method Not Allowed: 请求方法不正确
    • 500 Internal Server Error: 系统错误

注意ProxyHandler 返回的是文件内容二进制而不是JSON响应。错误时使用标准HTTP状态码保持与文件响应的一致性。

辅助函数

GenerateObjectKey(prefix, filename string) string

生成对象键。

GenerateObjectKeyWithDate(prefix, filename string) string

生成带日期的对象键(格式: prefix/YYYY/MM/DD/filename

完整示例

示例1文件上传和查看

package main

import (
    "log"
    "net/http"

    "git.toowon.com/jimmy/go-common/config"
    "git.toowon.com/jimmy/go-common/middleware"
    "git.toowon.com/jimmy/go-common/storage"
)

func main() {
    // 加载配置
    cfg, err := config.LoadFromFile("./config.json")
    if err != nil {
        log.Fatal(err)
    }

    // 创建存储实例使用OSS
    ossStorage, err := storage.NewStorage(storage.StorageTypeOSS, cfg)
    if err != nil {
        log.Fatal(err)
    }

    // 创建上传处理器
    uploadHandler := storage.NewUploadHandler(storage.UploadHandlerConfig{
        Storage:      ossStorage,
        MaxFileSize:  10 * 1024 * 1024, // 10MB
        AllowedExts: []string{".jpg", ".jpeg", ".png", ".gif", ".pdf"},
        ObjectPrefix: "uploads/",
    })

    // 创建代理查看处理器
    proxyHandler := storage.NewProxyHandler(ossStorage)

    // 创建中间件链
    var corsConfig *middleware.CORSConfig
    if cfg.GetCORS() != nil {
        c := cfg.GetCORS()
        corsConfig = middleware.NewCORSConfig(
            c.AllowedOrigins,
            c.AllowedMethods,
            c.AllowedHeaders,
            c.ExposedHeaders,
            c.AllowCredentials,
            c.MaxAge,
        )
    }
    chain := middleware.NewChain(
        middleware.CORS(corsConfig),
        middleware.Timezone,
    )

    // 注册路由
    mux := http.NewServeMux()
    mux.Handle("/upload", chain.Then(uploadHandler))
    mux.Handle("/file", chain.Then(proxyHandler))

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

示例2直接使用存储接口

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "git.toowon.com/jimmy/go-common/config"
    "git.toowon.com/jimmy/go-common/storage"
)

func main() {
    // 加载配置
    cfg, err := config.LoadFromFile("./config.json")
    if err != nil {
        log.Fatal(err)
    }

    // 创建存储实例
    s, err := storage.NewStorage(storage.StorageTypeMinIO, cfg)
    if err != nil {
        log.Fatal(err)
    }

    // 打开文件
    file, err := os.Open("test.jpg")
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // 生成对象键
    objectKey := storage.GenerateObjectKeyWithDate("images", "test.jpg")

    // 上传文件
    ctx := context.Background()
    err = s.Upload(ctx, objectKey, file, "image/jpeg")
    if err != nil {
        log.Fatal(err)
    }

    // 获取文件URL
    url, err := s.GetURL(objectKey, 0)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("File uploaded: %s\n", url)
}

注意事项

  1. OSS和MinIO SDK实现

    • 当前实现提供了接口和框架但具体的OSS和MinIO SDK集成需要根据实际使用的SDK实现
    • 需要在oss.gominio.go中实现具体的SDK调用
  2. 文件大小限制

    • 建议设置合理的文件大小限制
    • 大文件上传可能需要分片上传
  3. 文件扩展名验证

    • 建议限制允许的文件类型,防止上传恶意文件
    • 仅验证扩展名不够安全,建议结合文件内容验证
  4. 安全性

    • 上传接口应该添加身份验证
    • 代理查看接口可以添加访问控制
  5. 性能优化

    • 对于大文件,考虑使用分片上传
    • 代理查看可以添加缓存机制
  6. 错误处理

    • 所有操作都应该进行错误处理
    • 建议记录详细的错误日志

实现OSS和MinIO SDK集成

由于不同的OSS提供商和MinIO有不同的SDK当前实现提供了框架需要根据实际情况集成

OSS SDK集成示例阿里云OSS

import (
    "github.com/aliyun/aliyun-oss-go-sdk/oss"
)

func NewOSSStorage(cfg *config.OSSConfig) (*OSSStorage, error) {
    client, err := oss.New(cfg.Endpoint, cfg.AccessKeyID, cfg.AccessKeySecret)
    if err != nil {
        return nil, err
    }
    
    storage := &OSSStorage{
        config: cfg,
        client: client,
    }
    return storage, nil
}

func (s *OSSStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error {
    bucket, err := s.client.Bucket(s.config.Bucket)
    if err != nil {
        return err
    }
    
    options := []oss.Option{}
    if len(contentType) > 0 && contentType[0] != "" {
        options = append(options, oss.ContentType(contentType[0]))
    }
    
    return bucket.PutObject(objectKey, reader, options...)
}

MinIO SDK集成示例

import (
    "github.com/minio/minio-go/v7"
    "github.com/minio/minio-go/v7/pkg/credentials"
)

func NewMinIOStorage(cfg *config.MinIOConfig) (*MinIOStorage, error) {
    client, err := minio.New(cfg.Endpoint, &minio.Options{
        Creds:  credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""),
        Secure: cfg.UseSSL,
    })
    if err != nil {
        return nil, err
    }
    
    storage := &MinIOStorage{
        config: cfg,
        client: client,
    }
    return storage, nil
}

func (s *MinIOStorage) Upload(ctx context.Context, objectKey string, reader io.Reader, contentType ...string) error {
    ct := "application/octet-stream"
    if len(contentType) > 0 && contentType[0] != "" {
        ct = contentType[0]
    }
    
    _, err := s.client.PutObject(ctx, s.config.Bucket, objectKey, reader, -1, minio.PutObjectOptions{
        ContentType: ct,
    })
    return err
}

示例

完整示例请参考 examples/storage_example.go