package audit import ( "context" "fmt" "io" "os" "path/filepath" "sync/atomic" "time" "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) var ( minioClient atomic.Value // clientSignature caches the config signature of the loaded client so a // hot reload that keeps the same endpoint/keys/bucket reuses the client // instead of rebuilding it on every request. clientSignature atomic.Value ) // MinioClient wraps a minio client together with the target bucket. type MinioClient struct { Client *minio.Client BucketName string } // GetMinioClient atomically reads the global MinioClient (nil if not set). func GetMinioClient() *MinioClient { v := minioClient.Load() if v == nil { return nil } return v.(*MinioClient) } // SetMinioClient atomically writes the global MinioClient. func SetMinioClient(c *MinioClient) { minioClient.Store(c) } // newMinioClient builds a MinioClient and makes sure the bucket exists. func newMinioClient(endPoint, accessKey, secretKey, bucketName string, useSSl bool) (*MinioClient, error) { client, err := minio.New(endPoint, &minio.Options{ Creds: credentials.NewStaticV4(accessKey, secretKey, ""), Secure: useSSl, }) if err != nil { return nil, err } ctx := context.Background() exists, err := client.BucketExists(ctx, bucketName) if err != nil { return nil, err } if !exists { if err := client.MakeBucket(ctx, bucketName, minio.MakeBucketOptions{}); err != nil { return nil, err } } return &MinioClient{ Client: client, BucketName: bucketName, }, nil } // ReloadMinioClient rebuilds the global client when the file configuration // changed. An empty configuration (before t_config has been populated) or a // failed connection keeps the previously loaded client untouched. func ReloadMinioClient(fileConfig FileConfigManager) { if fileConfig.endpoint == "" || fileConfig.bucket == "" || fileConfig.accessKey == "" || fileConfig.secretKey == "" { return } signature := fmt.Sprintf("%s|%s|%s|%s|%t", fileConfig.endpoint, fileConfig.accessKey, fileConfig.secretKey, fileConfig.bucket, fileConfig.useSSL) if stored, ok := clientSignature.Load().(string); ok && stored == signature { return // configuration unchanged, keep the loaded client } client, err := newMinioClient(fileConfig.endpoint, fileConfig.accessKey, fileConfig.secretKey, fileConfig.bucket, fileConfig.useSSL) if err != nil { fmt.Printf("Minio client reload failed: %v, keep previous client\n", err) return } clientSignature.Store(signature) SetMinioClient(client) } // UploadFile uploads a local file and returns its object key. func (m *MinioClient) UploadFile(ctx context.Context, localFilePath, objectName string) (string, error) { file, err := os.Open(localFilePath) if err != nil { return "", err } defer func() { _ = file.Close() }() fileStat, err := file.Stat() if err != nil { return "", err } uploadInfo, err := m.Client.PutObject(ctx, m.BucketName, objectName, file, fileStat.Size(), minio.PutObjectOptions{}) if err != nil { return "", err } return uploadInfo.Key, nil } // DownloadFile downloads an object into a local temp file and returns the // temp file path. func (m *MinioClient) DownloadFile(ctx context.Context, fileKey string) (string, error) { fileName := filepath.Base(fileKey) if fileName == "" || fileName == "." || fileName == "/" { return "", fmt.Errorf("无效的FileKey:%s", fileKey) } tmpFile, err := os.CreateTemp("", fmt.Sprintf("log_restore_*_%s", fileName)) if err != nil { return "", fmt.Errorf("创建临时文件失败: %w", err) } defer func() { _ = tmpFile.Close() }() object, err := m.Client.GetObject(ctx, m.BucketName, fileKey, minio.GetObjectOptions{}) if err != nil { return "", fmt.Errorf("从 MinIO 获取对象失败: %w", err) } defer object.Close() if _, err = io.Copy(tmpFile, object); err != nil { return "", fmt.Errorf("写入临时文件失败: %w", err) } return tmpFile.Name(), nil } // GetTempDownloadURL returns a presigned private-bucket download URL. func (m *MinioClient) GetTempDownloadURL(ctx context.Context, fileKey string, expire int32) (string, error) { url, err := m.Client.PresignedGetObject(ctx, m.BucketName, fileKey, time.Duration(expire)*time.Second, nil) if err != nil { return "", err } return url.String(), nil } // RemoveFile deletes an object from the bucket. func (m *MinioClient) RemoveFile(ctx context.Context, fileKey string) error { if fileKey == "" { return fmt.Errorf("无效的FileKey:%s", fileKey) } if err := m.Client.RemoveObject(ctx, m.BucketName, fileKey, minio.RemoveObjectOptions{}); err != nil { return fmt.Errorf("remove object failed: %w", err) } return nil }