package file import ( "context" "crypto/rand" "encoding/hex" "fmt" "io" "mime/multipart" "net/url" "path" "strings" "time" "hfb_sys/backend/internal/config" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) type Storage struct { client *minio.Client bucket string } type Object struct { Reader io.ReadCloser ContentType string Size int64 } func NewStorage(cfg config.StorageConfig) (*Storage, error) { endpoint, secure, err := normalizeEndpoint(cfg.Endpoint) if err != nil { return nil, err } client, err := minio.New(endpoint, &minio.Options{ Creds: credentials.NewStaticV4(cfg.AccessKeyID, cfg.SecretAccessKey, ""), Secure: secure, }) if err != nil { return nil, err } storage := &Storage{client: client, bucket: cfg.Bucket} if err := storage.ensureBucket(context.Background()); err != nil { return nil, err } return storage, nil } func (s *Storage) Put(ctx context.Context, scene string, header *multipart.FileHeader, reader io.Reader, contentType string) (string, error) { key, err := newObjectKey(scene, header.Filename) if err != nil { return "", err } if err := s.PutObject(ctx, key, reader, header.Size, contentType, map[string]string{ "original-filename": header.Filename, }); err != nil { return "", err } return key, nil } func (s *Storage) PutObject(ctx context.Context, key string, reader io.Reader, size int64, contentType string, metadata map[string]string) error { _, err := s.client.PutObject(ctx, s.bucket, key, reader, size, minio.PutObjectOptions{ ContentType: contentType, UserMetadata: metadata, }) return err } func (s *Storage) Get(ctx context.Context, key string) (*Object, error) { object, err := s.client.GetObject(ctx, s.bucket, key, minio.GetObjectOptions{}) if err != nil { return nil, err } info, err := object.Stat() if err != nil { _ = object.Close() return nil, err } return &Object{ Reader: object, ContentType: info.ContentType, Size: info.Size, }, nil } func (s *Storage) ensureBucket(ctx context.Context) error { exists, err := s.client.BucketExists(ctx, s.bucket) if err != nil { errResp := minio.ToErrorResponse(err) if errResp.Code != "NoSuchBucket" && !strings.Contains(strings.ToLower(err.Error()), "bucket does not exist") { return err } } if exists { return nil } return s.client.MakeBucket(ctx, s.bucket, minio.MakeBucketOptions{}) } func normalizeEndpoint(raw string) (string, bool, error) { parsed, err := url.Parse(raw) if err != nil { return "", false, err } if parsed.Scheme == "" { return raw, false, nil } return parsed.Host, parsed.Scheme == "https", nil } func newObjectKey(scene string, filename string) (string, error) { if scene == "" { scene = "misc" } scene = strings.ToLower(scene) now := time.Now() token := make([]byte, 12) if _, err := rand.Read(token); err != nil { return "", err } ext := strings.ToLower(path.Ext(filename)) return fmt.Sprintf("%s/%04d/%02d/%02d/%s%s", scene, now.Year(), now.Month(), now.Day(), hex.EncodeToString(token), ext), nil }