| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- package audit
- import (
- "bytes"
- "context"
- "encoding/hex"
- "encoding/json"
- "fmt"
- "io"
- "mime"
- "mime/multipart"
- "os"
- "strings"
- "time"
- "github.com/tjfoc/gmsm/sm3"
- )
- // UploadAndDigest computes the SM3 digest of the payload and uploads the
- // raw bytes to MinIO under logs/{traceID}/{spanID}/. When the MinIO client
- // is not loaded (no/invalid file config) the upload is skipped and only the
- // digest is returned with an empty file key, so the audit chain keeps
- // working without MinIO.
- // Supported content types: application/json and multipart/form-data.
- func UploadAndDigest(data []byte, traceID, spanID, label string, minioClient *MinioClient, contentType string) (string, string, error) {
- // 1.计算摘要
- h := sm3.New()
- h.Write(data)
- digest := hex.EncodeToString(h.Sum(nil))
- // 2.构造标签、文件名、文件上传路径
- safeLabel := strings.Trim(label, "[]")
- baseName := fmt.Sprintf("%s_%d", safeLabel, time.Now().UnixNano())
- objectDir := fmt.Sprintf("%s/%s/%s", ObjectDirLogs, traceID, spanID)
- // MinIO 未就绪时只返回摘要
- if minioClient == nil {
- return digest, "", nil
- }
- // 空 body(典型如 GET 请求)无需上传原文
- if len(data) == 0 {
- return digest, "", nil
- }
- // 3.判断请求头类型
- switch {
- case strings.HasPrefix(contentType, "application/json"):
- // application/json
- fileName := baseName + ".json"
- objectKey := fmt.Sprintf("%s/%s", objectDir, fileName)
- fileKey, err := writeAndUploadTempFile(minioClient, data, fileName, objectKey)
- if err != nil {
- return "", "", fmt.Errorf("minio upload file failed: %w", err)
- }
- return digest, fileKey, nil
- case strings.HasPrefix(contentType, "multipart/form-data"):
- // multipart/form-data
- boundary := extractBoundary(contentType)
- if boundary == "" {
- return "", "", fmt.Errorf("invalid multipart content-type: %s", contentType)
- }
- reader := multipart.NewReader(bytes.NewReader(data), boundary)
- fields := make(map[string]string)
- fileKeys := make(map[string]string)
- for {
- part, err := reader.NextPart()
- if err == io.EOF {
- break
- }
- if err != nil {
- return "", "", fmt.Errorf("read multipart part failed: %w", err)
- }
- partData, err := io.ReadAll(part)
- if err != nil {
- return "", "", fmt.Errorf("read part data failed: %w", err)
- }
- if part.FileName() == "" {
- // 普通字段
- fields[part.FormName()] = string(partData)
- continue
- }
- // 文件字段
- objectKey := fmt.Sprintf("%s/%s_%s", objectDir, baseName, part.FileName())
- fileKey, err := writeAndUploadTempFile(minioClient, partData, part.FileName(), objectKey)
- if err != nil {
- return "", "", fmt.Errorf("upload file part failed: %w", err)
- }
- fileKeys[part.FormName()] = fileKey
- }
- // 保存字段 JSON
- fieldsFile := baseName + "_fields.json"
- fieldsKey := fmt.Sprintf("%s/%s", objectDir, fieldsFile)
- fieldBytes, err := json.MarshalIndent(fields, "", " ")
- if err != nil {
- return "", "", fmt.Errorf("marshal fields failed: %w", err)
- }
- if _, err := writeAndUploadTempFile(minioClient, fieldBytes, fieldsFile, fieldsKey); err != nil {
- return "", "", fmt.Errorf("upload fields failed: %w", err)
- }
- // 保存元数据 JSON
- metaJson := map[string]interface{}{
- "digest": digest,
- "trace_id": traceID,
- "span_id": spanID,
- "files": fileKeys,
- "fields": fieldsKey,
- }
- metaFile := baseName + "_meta.json"
- metaKey := fmt.Sprintf("%s/%s", objectDir, metaFile)
- metaBytes, err := json.MarshalIndent(metaJson, "", " ")
- if err != nil {
- return "", "", fmt.Errorf("marshal meta failed: %w", err)
- }
- if _, err := writeAndUploadTempFile(minioClient, metaBytes, metaFile, metaKey); err != nil {
- return "", "", fmt.Errorf("upload meta failed: %w", err)
- }
- return digest, metaKey, nil
- default:
- return "", "", fmt.Errorf("unsupported content type: %s", contentType)
- }
- }
- // extractBoundary extracts the boundary from a Content-Type header value.
- func extractBoundary(contentType string) string {
- _, params, err := mime.ParseMediaType(contentType)
- if err != nil {
- return ""
- }
- return params["boundary"]
- }
- // writeAndUploadTempFile writes data to a temp file, uploads it, and
- // removes the temp file afterwards.
- func writeAndUploadTempFile(minioClient *MinioClient, data []byte, tmpName, objectKey string) (string, error) {
- tmpFile, err := os.CreateTemp("", tmpName)
- if err != nil {
- return "", fmt.Errorf("create temp file failed: %w", err)
- }
- defer func() {
- _ = tmpFile.Close()
- _ = os.Remove(tmpFile.Name())
- }()
- if _, err := tmpFile.Write(data); err != nil {
- return "", fmt.Errorf("write temp file failed: %w", err)
- }
- if err := tmpFile.Close(); err != nil {
- return "", fmt.Errorf("close temp file failed: %w", err)
- }
- fileKey, err := minioClient.UploadFile(context.Background(), tmpFile.Name(), objectKey)
- if err != nil {
- return "", fmt.Errorf("minio upload failed: %w", err)
- }
- return fileKey, nil
- }
|