| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | package testdatasource | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"encoding/csv" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 	"path/filepath" | 
					
						
							|  |  |  | 	"regexp" | 
					
						
							|  |  |  | 	"strconv" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/backend" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana-plugin-sdk-go/data" | 
					
						
							|  |  |  | 	"github.com/grafana/grafana/pkg/components/simplejson" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | func (s *Service) handleCsvContentScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 	resp := backend.NewQueryDataResponse() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, q := range req.Queries { | 
					
						
							|  |  |  | 		model, err := simplejson.NewJson(q.JSON) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, fmt.Errorf("failed to parse query json: %v", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		csvContent := model.Get("csvContent").MustString() | 
					
						
							| 
									
										
										
										
											2021-11-29 22:21:18 +08:00
										 |  |  | 		if len(csvContent) == 0 { | 
					
						
							|  |  |  | 			return backend.NewQueryDataResponse(), nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-13 16:51:34 +08:00
										 |  |  | 		alias := model.Get("alias").MustString("") | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-10 22:44:47 +08:00
										 |  |  | 		frame, err := LoadCsvContent(strings.NewReader(csvContent), alias) | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		respD := resp.Responses[q.RefID] | 
					
						
							|  |  |  | 		respD.Frames = append(respD.Frames, frame) | 
					
						
							|  |  |  | 		resp.Responses[q.RefID] = respD | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return resp, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | func (s *Service) handleCsvFileScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 	resp := backend.NewQueryDataResponse() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, q := range req.Queries { | 
					
						
							|  |  |  | 		model, err := simplejson.NewJson(q.JSON) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, fmt.Errorf("failed to parse query json %v", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		fileName := model.Get("csvFileName").MustString() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if len(fileName) == 0 { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | 		frame, err := s.loadCsvFile(fileName) | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		respD := resp.Responses[q.RefID] | 
					
						
							|  |  |  | 		respD.Frames = append(respD.Frames, frame) | 
					
						
							|  |  |  | 		resp.Responses[q.RefID] = respD | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return resp, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | func (s *Service) loadCsvFile(fileName string) (*data.Frame, error) { | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 	validFileName := regexp.MustCompile(`([\w_]+)\.csv`) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if !validFileName.MatchString(fileName) { | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("invalid csv file name: %q", fileName) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | 	filePath := filepath.Join(s.cfg.StaticRootPath, "testdata", fileName) | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Can ignore gosec G304 here, because we check the file pattern above
 | 
					
						
							|  |  |  | 	// nolint:gosec
 | 
					
						
							|  |  |  | 	fileReader, err := os.Open(filePath) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("failed open file: %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	defer func() { | 
					
						
							|  |  |  | 		if err := fileReader.Close(); err != nil { | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | 			s.logger.Warn("Failed to close file", "err", err, "path", fileName) | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	}() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-10 22:44:47 +08:00
										 |  |  | 	return LoadCsvContent(fileReader, fileName) | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-09-10 22:44:47 +08:00
										 |  |  | // LoadCsvContent should be moved to the SDK
 | 
					
						
							|  |  |  | func LoadCsvContent(ioReader io.Reader, name string) (*data.Frame, error) { | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 	reader := csv.NewReader(ioReader) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Read the header records
 | 
					
						
							|  |  |  | 	headerFields, err := reader.Read() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("failed to read header line: %v", err) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	fields := []*data.Field{} | 
					
						
							|  |  |  | 	fieldNames := []string{} | 
					
						
							|  |  |  | 	fieldRawValues := [][]string{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for _, fieldName := range headerFields { | 
					
						
							|  |  |  | 		fieldNames = append(fieldNames, strings.Trim(fieldName, " ")) | 
					
						
							|  |  |  | 		fieldRawValues = append(fieldRawValues, []string{}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	for { | 
					
						
							|  |  |  | 		lineValues, err := reader.Read() | 
					
						
							|  |  |  | 		if errors.Is(err, io.EOF) { | 
					
						
							|  |  |  | 			break // reached end of the file
 | 
					
						
							|  |  |  | 		} else if err != nil { | 
					
						
							|  |  |  | 			return nil, fmt.Errorf("failed to read line: %v", err) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for fieldIndex, value := range lineValues { | 
					
						
							|  |  |  | 			fieldRawValues[fieldIndex] = append(fieldRawValues[fieldIndex], strings.Trim(value, " ")) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	longest := 0 | 
					
						
							|  |  |  | 	for fieldIndex, rawValues := range fieldRawValues { | 
					
						
							|  |  |  | 		fieldName := fieldNames[fieldIndex] | 
					
						
							|  |  |  | 		field, err := csvValuesToField(rawValues) | 
					
						
							|  |  |  | 		if err == nil { | 
					
						
							|  |  |  | 			// Check if the values are actually a time field
 | 
					
						
							|  |  |  | 			if strings.Contains(strings.ToLower(fieldName), "time") { | 
					
						
							|  |  |  | 				timeField := toTimeField(field) | 
					
						
							|  |  |  | 				if timeField != nil { | 
					
						
							|  |  |  | 					field = timeField | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			field.Name = fieldName | 
					
						
							|  |  |  | 			fields = append(fields, field) | 
					
						
							|  |  |  | 			if field.Len() > longest { | 
					
						
							|  |  |  | 				longest = field.Len() | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Make all fields the same length
 | 
					
						
							|  |  |  | 	for _, field := range fields { | 
					
						
							|  |  |  | 		delta := field.Len() - longest | 
					
						
							|  |  |  | 		if delta > 0 { | 
					
						
							|  |  |  | 			field.Extend(delta) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	frame := data.NewFrame(name, fields...) | 
					
						
							|  |  |  | 	return frame, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func csvLineToField(stringInput string) (*data.Field, error) { | 
					
						
							|  |  |  | 	return csvValuesToField(strings.Split(strings.ReplaceAll(stringInput, " ", ""), ",")) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func csvValuesToField(parts []string) (*data.Field, error) { | 
					
						
							|  |  |  | 	if len(parts) < 1 { | 
					
						
							|  |  |  | 		return nil, fmt.Errorf("csv must have at least one value") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	first := strings.ToUpper(parts[0]) | 
					
						
							|  |  |  | 	if first == "T" || first == "F" || first == "TRUE" || first == "FALSE" { | 
					
						
							|  |  |  | 		field := data.NewFieldFromFieldType(data.FieldTypeNullableBool, len(parts)) | 
					
						
							|  |  |  | 		for idx, strVal := range parts { | 
					
						
							|  |  |  | 			strVal = strings.ToUpper(strVal) | 
					
						
							|  |  |  | 			if strVal == "NULL" || strVal == "" { | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			field.SetConcrete(idx, strVal == "T" || strVal == "TRUE") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return field, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-11 19:12:11 +08:00
										 |  |  | 	// Try parsing values as numbers
 | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 	ok := false | 
					
						
							| 
									
										
										
										
											2021-06-11 19:12:11 +08:00
										 |  |  | 	field := data.NewFieldFromFieldType(data.FieldTypeNullableInt64, len(parts)) | 
					
						
							|  |  |  | 	for idx, strVal := range parts { | 
					
						
							|  |  |  | 		if strVal == "null" || strVal == "" { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		val, err := strconv.ParseInt(strVal, 10, 64) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			ok = false | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		field.SetConcrete(idx, val) | 
					
						
							|  |  |  | 		ok = true | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if ok { | 
					
						
							|  |  |  | 		return field, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Maybe floats
 | 
					
						
							|  |  |  | 	field = data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, len(parts)) | 
					
						
							| 
									
										
										
										
											2021-05-26 16:42:42 +08:00
										 |  |  | 	for idx, strVal := range parts { | 
					
						
							|  |  |  | 		if strVal == "null" || strVal == "" { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		val, err := strconv.ParseFloat(strVal, 64) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			ok = false | 
					
						
							|  |  |  | 			break | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		field.SetConcrete(idx, val) | 
					
						
							|  |  |  | 		ok = true | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if ok { | 
					
						
							|  |  |  | 		return field, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Replace empty strings with null
 | 
					
						
							|  |  |  | 	field = data.NewFieldFromFieldType(data.FieldTypeNullableString, len(parts)) | 
					
						
							|  |  |  | 	for idx, strVal := range parts { | 
					
						
							|  |  |  | 		if strVal == "null" || strVal == "" { | 
					
						
							|  |  |  | 			continue | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		field.SetConcrete(idx, strVal) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return field, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // This will try to convert the values to a timestamp
 | 
					
						
							|  |  |  | func toTimeField(field *data.Field) *data.Field { | 
					
						
							|  |  |  | 	found := false | 
					
						
							|  |  |  | 	count := field.Len() | 
					
						
							|  |  |  | 	timeField := data.NewFieldFromFieldType(data.FieldTypeNullableTime, count) | 
					
						
							|  |  |  | 	timeField.Config = field.Config | 
					
						
							|  |  |  | 	timeField.Name = field.Name | 
					
						
							|  |  |  | 	timeField.Labels = field.Labels | 
					
						
							|  |  |  | 	ft := field.Type() | 
					
						
							|  |  |  | 	if ft.Numeric() { | 
					
						
							|  |  |  | 		for i := 0; i < count; i++ { | 
					
						
							|  |  |  | 			v, err := field.FloatAt(i) | 
					
						
							|  |  |  | 			if err == nil { | 
					
						
							|  |  |  | 				t := time.Unix(0, int64(v)*int64(time.Millisecond)) | 
					
						
							|  |  |  | 				timeField.SetConcrete(i, t.UTC()) | 
					
						
							|  |  |  | 				found = true | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !found { | 
					
						
							|  |  |  | 			return nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return timeField | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if ft == data.FieldTypeNullableString || ft == data.FieldTypeString { | 
					
						
							|  |  |  | 		for i := 0; i < count; i++ { | 
					
						
							|  |  |  | 			v, ok := field.ConcreteAt(i) | 
					
						
							|  |  |  | 			if ok && v != nil { | 
					
						
							|  |  |  | 				t, err := time.Parse(time.RFC3339, v.(string)) | 
					
						
							|  |  |  | 				if err == nil { | 
					
						
							|  |  |  | 					timeField.SetConcrete(i, t.UTC()) | 
					
						
							|  |  |  | 					found = true | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if !found { | 
					
						
							|  |  |  | 			return nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return timeField | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return nil | 
					
						
							|  |  |  | } |