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 }