Files
kefu_cloud/server/internal/storage/s3.go
T
yml2213 32d88f42e7 修复 MinIO 端口冲突并优化顶栏 IP 展示
- MinIO 宿主机端口改为 9100/9101,避免与 hfb-minio 冲突
- 测试与配置默认 PublicBase 同步为 9100
- 工作台顶栏固定展示 IP|地区,详情回填会话元数据
- 种子数据补充访客 IP/地区示例
2026-07-15 12:39:39 +08:00

105 lines
2.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package storage
import (
"context"
"fmt"
"io"
"net/url"
"strings"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"kefu-sys/server/internal/config"
)
// S3Storage 基于 minio-go 的 S3 兼容实现(MinIO / OSS / COS / AWS S3
type S3Storage struct {
client *minio.Client
bucket string
base string
}
func NewS3(cfg config.StorageConfig) (*S3Storage, error) {
endpoint := strings.TrimPrefix(strings.TrimPrefix(cfg.Endpoint, "https://"), "http://")
client, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.UseSSL,
Region: cfg.Region,
})
if err != nil {
return nil, fmt.Errorf("init s3 client: %w", err)
}
base := NormalizePublicBase(cfg.PublicBase)
if base == "" {
scheme := "http"
if cfg.UseSSL {
scheme = "https"
}
base = fmt.Sprintf("%s://%s/%s", scheme, endpoint, cfg.Bucket)
}
return &S3Storage{client: client, bucket: cfg.Bucket, base: base}, nil
}
func (s *S3Storage) EnsureBucket(ctx context.Context) error {
exists, err := s.client.BucketExists(ctx, s.bucket)
if err != nil {
return err
}
if !exists {
if err := s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}); err != nil {
return err
}
}
// 开发环境尽量开放公共读;生产用 CDN/桶策略配置
policy := fmt.Sprintf(`{
"Version":"2012-10-17",
"Statement":[{
"Effect":"Allow",
"Principal":{"AWS":["*"]},
"Action":["s3:GetObject"],
"Resource":["arn:aws:s3:::%s/*"]
}]
}`, s.bucket)
_ = s.client.SetBucketPolicy(ctx, s.bucket, policy)
return nil
}
func (s *S3Storage) Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) (string, error) {
if contentType == "" {
contentType = "application/octet-stream"
}
_, err := s.client.PutObject(ctx, s.bucket, key, r, size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
return "", err
}
return s.objectURL(key), nil
}
func (s *S3Storage) Delete(ctx context.Context, key string) error {
return s.client.RemoveObject(ctx, s.bucket, key, minio.RemoveObjectOptions{})
}
func (s *S3Storage) PublicBase() string { return s.base }
func (s *S3Storage) objectURL(key string) string {
// PublicBase 形如 http://localhost:9100/kefu(宿主机映射端口)
return s.base + "/" + strings.TrimPrefix(key, "/")
}
// ParseKeyFromURL 从公网 URL 还原 object key(删除时用)
func (s *S3Storage) ParseKeyFromURL(publicURL string) (string, error) {
if !strings.HasPrefix(publicURL, s.base+"/") {
return "", fmt.Errorf("url not in bucket")
}
key := strings.TrimPrefix(publicURL, s.base+"/")
if key == "" {
return "", fmt.Errorf("empty key")
}
if u, err := url.PathUnescape(key); err == nil {
return u, nil
}
return key, nil
}