1package testdatasource 2 3import ( 4 "context" 5 "encoding/csv" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/grafana/grafana-plugin-sdk-go/backend" 17 "github.com/grafana/grafana-plugin-sdk-go/data" 18 "github.com/grafana/grafana/pkg/components/simplejson" 19) 20 21func (s *Service) handleCsvContentScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 22 resp := backend.NewQueryDataResponse() 23 24 for _, q := range req.Queries { 25 model, err := simplejson.NewJson(q.JSON) 26 if err != nil { 27 return nil, fmt.Errorf("failed to parse query json: %v", err) 28 } 29 30 csvContent := model.Get("csvContent").MustString() 31 alias := model.Get("alias").MustString("") 32 33 frame, err := LoadCsvContent(strings.NewReader(csvContent), alias) 34 if err != nil { 35 return nil, err 36 } 37 38 respD := resp.Responses[q.RefID] 39 respD.Frames = append(respD.Frames, frame) 40 resp.Responses[q.RefID] = respD 41 } 42 43 return resp, nil 44} 45 46func (s *Service) handleCsvFileScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { 47 resp := backend.NewQueryDataResponse() 48 49 for _, q := range req.Queries { 50 model, err := simplejson.NewJson(q.JSON) 51 if err != nil { 52 return nil, fmt.Errorf("failed to parse query json %v", err) 53 } 54 55 fileName := model.Get("csvFileName").MustString() 56 57 if len(fileName) == 0 { 58 continue 59 } 60 61 frame, err := s.loadCsvFile(fileName) 62 63 if err != nil { 64 return nil, err 65 } 66 67 respD := resp.Responses[q.RefID] 68 respD.Frames = append(respD.Frames, frame) 69 resp.Responses[q.RefID] = respD 70 } 71 72 return resp, nil 73} 74 75func (s *Service) loadCsvFile(fileName string) (*data.Frame, error) { 76 validFileName := regexp.MustCompile(`^\w+\.csv$`) 77 78 if !validFileName.MatchString(fileName) { 79 return nil, fmt.Errorf("invalid csv file name: %q", fileName) 80 } 81 82 csvFilepath := filepath.Clean(filepath.Join("/", fileName)) 83 filePath := filepath.Join(s.cfg.StaticRootPath, "testdata", csvFilepath) 84 85 // Can ignore gosec G304 here, because we check the file pattern above 86 // nolint:gosec 87 fileReader, err := os.Open(filePath) 88 if err != nil { 89 return nil, fmt.Errorf("failed open file: %v", err) 90 } 91 92 defer func() { 93 if err := fileReader.Close(); err != nil { 94 s.logger.Warn("Failed to close file", "err", err, "path", fileName) 95 } 96 }() 97 98 return LoadCsvContent(fileReader, fileName) 99} 100 101// LoadCsvContent should be moved to the SDK 102func LoadCsvContent(ioReader io.Reader, name string) (*data.Frame, error) { 103 reader := csv.NewReader(ioReader) 104 105 // Read the header records 106 headerFields, err := reader.Read() 107 if err != nil { 108 return nil, fmt.Errorf("failed to read header line: %v", err) 109 } 110 111 fields := []*data.Field{} 112 fieldNames := []string{} 113 fieldRawValues := [][]string{} 114 115 for _, fieldName := range headerFields { 116 fieldNames = append(fieldNames, strings.Trim(fieldName, " ")) 117 fieldRawValues = append(fieldRawValues, []string{}) 118 } 119 120 for { 121 lineValues, err := reader.Read() 122 if errors.Is(err, io.EOF) { 123 break // reached end of the file 124 } else if err != nil { 125 return nil, fmt.Errorf("failed to read line: %v", err) 126 } 127 128 for fieldIndex, value := range lineValues { 129 fieldRawValues[fieldIndex] = append(fieldRawValues[fieldIndex], strings.Trim(value, " ")) 130 } 131 } 132 133 longest := 0 134 for fieldIndex, rawValues := range fieldRawValues { 135 fieldName := fieldNames[fieldIndex] 136 field, err := csvValuesToField(rawValues) 137 if err == nil { 138 // Check if the values are actually a time field 139 if strings.Contains(strings.ToLower(fieldName), "time") { 140 timeField := toTimeField(field) 141 if timeField != nil { 142 field = timeField 143 } 144 } 145 146 field.Name = fieldName 147 fields = append(fields, field) 148 if field.Len() > longest { 149 longest = field.Len() 150 } 151 } 152 } 153 154 // Make all fields the same length 155 for _, field := range fields { 156 delta := field.Len() - longest 157 if delta > 0 { 158 field.Extend(delta) 159 } 160 } 161 162 frame := data.NewFrame(name, fields...) 163 return frame, nil 164} 165 166func csvLineToField(stringInput string) (*data.Field, error) { 167 return csvValuesToField(strings.Split(strings.ReplaceAll(stringInput, " ", ""), ",")) 168} 169 170func csvValuesToField(parts []string) (*data.Field, error) { 171 if len(parts) < 1 { 172 return nil, fmt.Errorf("csv must have at least one value") 173 } 174 175 first := strings.ToUpper(parts[0]) 176 if first == "T" || first == "F" || first == "TRUE" || first == "FALSE" { 177 field := data.NewFieldFromFieldType(data.FieldTypeNullableBool, len(parts)) 178 for idx, strVal := range parts { 179 strVal = strings.ToUpper(strVal) 180 if strVal == "NULL" || strVal == "" { 181 continue 182 } 183 field.SetConcrete(idx, strVal == "T" || strVal == "TRUE") 184 } 185 return field, nil 186 } 187 188 // Try parsing values as numbers 189 ok := false 190 field := data.NewFieldFromFieldType(data.FieldTypeNullableInt64, len(parts)) 191 for idx, strVal := range parts { 192 if strVal == "null" || strVal == "" { 193 continue 194 } 195 196 val, err := strconv.ParseInt(strVal, 10, 64) 197 if err != nil { 198 ok = false 199 break 200 } 201 field.SetConcrete(idx, val) 202 ok = true 203 } 204 if ok { 205 return field, nil 206 } 207 208 // Maybe floats 209 field = data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(parts)) 210 for idx, strVal := range parts { 211 if strVal == "null" || strVal == "" { 212 continue 213 } 214 215 val, err := strconv.ParseFloat(strVal, 64) 216 if err != nil { 217 ok = false 218 break 219 } 220 field.SetConcrete(idx, val) 221 ok = true 222 } 223 if ok { 224 return field, nil 225 } 226 227 // Replace empty strings with null 228 field = data.NewFieldFromFieldType(data.FieldTypeNullableString, len(parts)) 229 for idx, strVal := range parts { 230 if strVal == "null" || strVal == "" { 231 continue 232 } 233 field.SetConcrete(idx, strVal) 234 } 235 return field, nil 236} 237 238// This will try to convert the values to a timestamp 239func toTimeField(field *data.Field) *data.Field { 240 found := false 241 count := field.Len() 242 timeField := data.NewFieldFromFieldType(data.FieldTypeNullableTime, count) 243 timeField.Config = field.Config 244 timeField.Name = field.Name 245 timeField.Labels = field.Labels 246 ft := field.Type() 247 if ft.Numeric() { 248 for i := 0; i < count; i++ { 249 v, err := field.FloatAt(i) 250 if err == nil { 251 t := time.Unix(0, int64(v)*int64(time.Millisecond)) 252 timeField.SetConcrete(i, t.UTC()) 253 found = true 254 } 255 } 256 if !found { 257 return nil 258 } 259 return timeField 260 } 261 if ft == data.FieldTypeNullableString || ft == data.FieldTypeString { 262 for i := 0; i < count; i++ { 263 v, ok := field.ConcreteAt(i) 264 if ok && v != nil { 265 t, err := time.Parse(time.RFC3339, v.(string)) 266 if err == nil { 267 timeField.SetConcrete(i, t.UTC()) 268 found = true 269 } 270 } 271 } 272 if !found { 273 return nil 274 } 275 return timeField 276 } 277 return nil 278} 279