516 lines
12 KiB
Markdown
516 lines
12 KiB
Markdown
# 存储工具文档
|
||
|
||
## 概述
|
||
|
||
存储工具提供了文件上传和查看功能,支持OSS和MinIO两种存储方式,并提供HTTP处理器用于文件上传和代理查看。
|
||
|
||
## 功能特性
|
||
|
||
- 支持OSS对象存储(阿里云、腾讯云、AWS、七牛云等)
|
||
- 支持MinIO对象存储
|
||
- 提供统一的存储接口
|
||
- 支持文件上传HTTP处理器
|
||
- 支持文件代理查看HTTP处理器
|
||
- 支持文件大小和扩展名限制
|
||
- 自动生成唯一文件名
|
||
- 支持自定义对象键前缀
|
||
|
||
## 使用方法
|
||
|
||
### 1. 创建存储实例
|
||
|
||
```go
|
||
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)
|
||
}
|
||
```
|
||
|
||
### 2. 上传文件
|
||
|
||
```go
|
||
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)
|
||
```
|
||
|
||
### 3. 使用HTTP处理器上传文件
|
||
|
||
```go
|
||
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)
|
||
```
|
||
|
||
**上传请求示例:**
|
||
```bash
|
||
curl -X POST http://localhost:8080/upload \
|
||
-F "file=@test.jpg" \
|
||
-F "prefix=images/"
|
||
```
|
||
|
||
**响应示例:**
|
||
```json
|
||
{
|
||
"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处理器查看文件
|
||
|
||
```go
|
||
import (
|
||
"net/http"
|
||
"git.toowon.com/jimmy/go-common/storage"
|
||
)
|
||
|
||
// 创建代理查看处理器
|
||
proxyHandler := storage.NewProxyHandler(ossStorage)
|
||
|
||
// 注册路由
|
||
http.Handle("/file", proxyHandler)
|
||
http.ListenAndServe(":8080", nil)
|
||
```
|
||
|
||
**查看请求示例:**
|
||
```
|
||
GET /file?key=images/test.jpg
|
||
```
|
||
|
||
### 5. 生成对象键
|
||
|
||
```go
|
||
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. 删除文件
|
||
|
||
```go
|
||
ctx := context.Background()
|
||
err := ossStorage.Delete(ctx, "images/test.jpg")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
```
|
||
|
||
### 7. 检查文件是否存在
|
||
|
||
```go
|
||
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 接口
|
||
|
||
```go
|
||
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.StorageTypeOSS` 或 `storage.StorageTypeMinIO`)
|
||
- `cfg`: 配置对象
|
||
|
||
**返回:** 存储实例和错误信息
|
||
|
||
### UploadHandler
|
||
|
||
文件上传HTTP处理器。
|
||
|
||
#### NewUploadHandler(cfg UploadHandlerConfig) *UploadHandler
|
||
|
||
创建上传处理器。
|
||
|
||
**配置参数:**
|
||
- `Storage`: 存储实例
|
||
- `MaxFileSize`: 最大文件大小(字节),0表示不限制
|
||
- `AllowedExts`: 允许的文件扩展名,空表示不限制
|
||
- `ObjectPrefix`: 对象键前缀
|
||
|
||
#### 请求格式
|
||
|
||
- **方法**: POST
|
||
- **表单字段**:
|
||
- `file`: 文件(必需)
|
||
- `prefix`: 对象键前缀(可选,会覆盖配置中的前缀)
|
||
|
||
#### 响应格式
|
||
|
||
```json
|
||
{
|
||
"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:文件上传和查看
|
||
|
||
```go
|
||
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:直接使用存储接口
|
||
|
||
```go
|
||
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.go`和`minio.go`中实现具体的SDK调用
|
||
|
||
2. **文件大小限制**:
|
||
- 建议设置合理的文件大小限制
|
||
- 大文件上传可能需要分片上传
|
||
|
||
3. **文件扩展名验证**:
|
||
- 建议限制允许的文件类型,防止上传恶意文件
|
||
- 仅验证扩展名不够安全,建议结合文件内容验证
|
||
|
||
4. **安全性**:
|
||
- 上传接口应该添加身份验证
|
||
- 代理查看接口可以添加访问控制
|
||
|
||
5. **性能优化**:
|
||
- 对于大文件,考虑使用分片上传
|
||
- 代理查看可以添加缓存机制
|
||
|
||
6. **错误处理**:
|
||
- 所有操作都应该进行错误处理
|
||
- 建议记录详细的错误日志
|
||
|
||
## 实现OSS和MinIO SDK集成
|
||
|
||
由于不同的OSS提供商和MinIO有不同的SDK,当前实现提供了框架,需要根据实际情况集成:
|
||
|
||
### OSS SDK集成示例(阿里云OSS)
|
||
|
||
```go
|
||
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集成示例
|
||
|
||
```go
|
||
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`
|
||
|