@@ -4,11 +4,15 @@ import (
44 "context"
55 "crypto/tls"
66 "crypto/x509"
7+ "encoding/csv"
8+ "encoding/json"
79 "fmt"
810 "io"
911 "log"
1012 "net"
1113 "os"
14+ "path/filepath"
15+ "strings"
1216 "sync"
1317 "time"
1418
@@ -1032,3 +1036,222 @@ func (a *App) ExportConfig() (string, error) {
10321036func (a * App ) ImportConfig (jsonData string , merge bool ) error {
10331037 return importConfigToStorage (jsonData , merge )
10341038}
1039+
1040+ // ================================================================================
1041+ // BATCH IMPORT API (Enterprise Features)
1042+ // ================================================================================
1043+
1044+ // BatchImportResult represents the result of a batch import operation
1045+ type BatchImportResult struct {
1046+ Messages []string `json:"messages"` // Successfully parsed messages
1047+ TotalLines int `json:"totalLines"` // Total lines processed
1048+ Errors []string `json:"errors"` // Any parsing errors
1049+ }
1050+
1051+ // SelectLogFile opens a file dialog to select a log file for batch import
1052+ // Supports CSV, JSON, and plain text files
1053+ func (a * App ) SelectLogFile () (string , error ) {
1054+ filePath , err := runtime .OpenFileDialog (a .ctx , runtime.OpenDialogOptions {
1055+ Title : "Select Log File for Import" ,
1056+ Filters : []runtime.FileFilter {
1057+ {
1058+ DisplayName : "Log Files (*.csv, *.json, *.txt, *.log)" ,
1059+ Pattern : "*.csv;*.json;*.txt;*.log" ,
1060+ },
1061+ {
1062+ DisplayName : "CSV Files (*.csv)" ,
1063+ Pattern : "*.csv" ,
1064+ },
1065+ {
1066+ DisplayName : "JSON Files (*.json)" ,
1067+ Pattern : "*.json" ,
1068+ },
1069+ {
1070+ DisplayName : "Text Files (*.txt, *.log)" ,
1071+ Pattern : "*.txt;*.log" ,
1072+ },
1073+ {
1074+ DisplayName : "All Files (*.*)" ,
1075+ Pattern : "*.*" ,
1076+ },
1077+ },
1078+ })
1079+ if err != nil {
1080+ return "" , fmt .Errorf ("failed to open file dialog: %w" , err )
1081+ }
1082+ return filePath , nil
1083+ }
1084+
1085+ // ImportLogFile reads and parses a log file, returning the messages
1086+ // Automatically detects format based on file extension and content
1087+ func (a * App ) ImportLogFile (filePath string ) (BatchImportResult , error ) {
1088+ result := BatchImportResult {
1089+ Messages : []string {},
1090+ Errors : []string {},
1091+ }
1092+
1093+ // Read file content
1094+ content , err := os .ReadFile (filePath )
1095+ if err != nil {
1096+ return result , fmt .Errorf ("failed to read file: %w" , err )
1097+ }
1098+
1099+ // Detect format based on extension
1100+ ext := strings .ToLower (filepath .Ext (filePath ))
1101+
1102+ switch ext {
1103+ case ".json" :
1104+ return a .parseJSONLogs (content )
1105+ case ".csv" :
1106+ return a .parseCSVLogs (content )
1107+ default :
1108+ // Try JSON first, then fallback to plain text
1109+ if len (content ) > 0 && (content [0 ] == '[' || content [0 ] == '{' ) {
1110+ jsonResult , err := a .parseJSONLogs (content )
1111+ if err == nil && len (jsonResult .Messages ) > 0 {
1112+ return jsonResult , nil
1113+ }
1114+ }
1115+ return a .parseTextLogs (content )
1116+ }
1117+ }
1118+
1119+ // parseJSONLogs parses JSON format logs
1120+ // Supports: array of strings, array of objects with "message" field, or newline-delimited JSON
1121+ func (a * App ) parseJSONLogs (content []byte ) (BatchImportResult , error ) {
1122+ result := BatchImportResult {
1123+ Messages : []string {},
1124+ Errors : []string {},
1125+ }
1126+
1127+ // Try parsing as array of strings
1128+ var stringArray []string
1129+ if err := json .Unmarshal (content , & stringArray ); err == nil {
1130+ for _ , msg := range stringArray {
1131+ if trimmed := strings .TrimSpace (msg ); trimmed != "" {
1132+ result .Messages = append (result .Messages , trimmed )
1133+ }
1134+ }
1135+ result .TotalLines = len (stringArray )
1136+ return result , nil
1137+ }
1138+
1139+ // Try parsing as array of objects with "message" field
1140+ var objArray []map [string ]interface {}
1141+ if err := json .Unmarshal (content , & objArray ); err == nil {
1142+ result .TotalLines = len (objArray )
1143+ for i , obj := range objArray {
1144+ if msg , ok := obj ["message" ].(string ); ok && strings .TrimSpace (msg ) != "" {
1145+ result .Messages = append (result .Messages , strings .TrimSpace (msg ))
1146+ } else if msg , ok := obj ["msg" ].(string ); ok && strings .TrimSpace (msg ) != "" {
1147+ result .Messages = append (result .Messages , strings .TrimSpace (msg ))
1148+ } else if msg , ok := obj ["log" ].(string ); ok && strings .TrimSpace (msg ) != "" {
1149+ result .Messages = append (result .Messages , strings .TrimSpace (msg ))
1150+ } else {
1151+ result .Errors = append (result .Errors , fmt .Sprintf ("Line %d: no 'message', 'msg', or 'log' field found" , i + 1 ))
1152+ }
1153+ }
1154+ return result , nil
1155+ }
1156+
1157+ // Try parsing as newline-delimited JSON (NDJSON)
1158+ lines := strings .Split (string (content ), "\n " )
1159+ result .TotalLines = len (lines )
1160+ for i , line := range lines {
1161+ line = strings .TrimSpace (line )
1162+ if line == "" {
1163+ continue
1164+ }
1165+ var obj map [string ]interface {}
1166+ if err := json .Unmarshal ([]byte (line ), & obj ); err == nil {
1167+ if msg , ok := obj ["message" ].(string ); ok && strings .TrimSpace (msg ) != "" {
1168+ result .Messages = append (result .Messages , strings .TrimSpace (msg ))
1169+ } else if msg , ok := obj ["msg" ].(string ); ok && strings .TrimSpace (msg ) != "" {
1170+ result .Messages = append (result .Messages , strings .TrimSpace (msg ))
1171+ } else {
1172+ result .Errors = append (result .Errors , fmt .Sprintf ("Line %d: no message field found" , i + 1 ))
1173+ }
1174+ } else {
1175+ result .Errors = append (result .Errors , fmt .Sprintf ("Line %d: invalid JSON" , i + 1 ))
1176+ }
1177+ }
1178+
1179+ return result , nil
1180+ }
1181+
1182+ // parseCSVLogs parses CSV format logs
1183+ // First column is treated as the message, or looks for "message" header
1184+ func (a * App ) parseCSVLogs (content []byte ) (BatchImportResult , error ) {
1185+ result := BatchImportResult {
1186+ Messages : []string {},
1187+ Errors : []string {},
1188+ }
1189+
1190+ reader := csv .NewReader (strings .NewReader (string (content )))
1191+ reader .TrimLeadingSpace = true
1192+ reader .LazyQuotes = true
1193+
1194+ records , err := reader .ReadAll ()
1195+ if err != nil {
1196+ return result , fmt .Errorf ("failed to parse CSV: %w" , err )
1197+ }
1198+
1199+ if len (records ) == 0 {
1200+ return result , nil
1201+ }
1202+
1203+ result .TotalLines = len (records )
1204+
1205+ // Check for header row
1206+ messageColIdx := 0
1207+ hasHeader := false
1208+ header := records [0 ]
1209+
1210+ for i , col := range header {
1211+ colLower := strings .ToLower (strings .TrimSpace (col ))
1212+ if colLower == "message" || colLower == "msg" || colLower == "log" {
1213+ messageColIdx = i
1214+ hasHeader = true
1215+ break
1216+ }
1217+ }
1218+
1219+ startRow := 0
1220+ if hasHeader {
1221+ startRow = 1
1222+ }
1223+
1224+ for i := startRow ; i < len (records ); i ++ {
1225+ record := records [i ]
1226+ if len (record ) > messageColIdx {
1227+ msg := strings .TrimSpace (record [messageColIdx ])
1228+ if msg != "" {
1229+ result .Messages = append (result .Messages , msg )
1230+ }
1231+ } else {
1232+ result .Errors = append (result .Errors , fmt .Sprintf ("Line %d: insufficient columns" , i + 1 ))
1233+ }
1234+ }
1235+
1236+ return result , nil
1237+ }
1238+
1239+ // parseTextLogs parses plain text logs (one message per line)
1240+ func (a * App ) parseTextLogs (content []byte ) (BatchImportResult , error ) {
1241+ result := BatchImportResult {
1242+ Messages : []string {},
1243+ Errors : []string {},
1244+ }
1245+
1246+ lines := strings .Split (string (content ), "\n " )
1247+ result .TotalLines = len (lines )
1248+
1249+ for _ , line := range lines {
1250+ trimmed := strings .TrimSpace (line )
1251+ if trimmed != "" {
1252+ result .Messages = append (result .Messages , trimmed )
1253+ }
1254+ }
1255+
1256+ return result , nil
1257+ }
0 commit comments