package main import ( "bufio" "database/sql" "embed" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" _ "github.com/go-sql-driver/mysql" ) //go:embed index.html static/* var embeddedFiles embed.FS // Config struct for database connection type Config struct { Username string `json:"username"` Password string `json:"password"` Hostname string `json:"hostname"` Port int `json:"port"` Database string `json:"database"` } // QueryRequest from the frontend type QueryRequest struct { ID string `json:"id"` Query string `json:"query"` } // QueryResult represents a single historical query execution type QueryResult struct { ID string `json:"id"` Query string `json:"query"` Timestamp int64 `json:"timestamp"` Duration int64 `json:"duration,omitempty"` // Duration in milliseconds Error string `json:"error,omitempty"` Columns []string `json:"columns,omitempty"` Rows [][]interface{} `json:"rows,omitempty"` RowsAffected int64 `json:"rowsAffected,omitempty"` } var db *sql.DB var historyDir = "./history" func main() { config, err := loadOrCreateConfig("config.json") if err != nil { log.Fatalf("Failed to load configuration: %v", err) } // Connect to the database dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true", config.Username, config.Password, config.Hostname, config.Port, config.Database) db, err = sql.Open("mysql", dsn) if err != nil { log.Fatalf("Failed to open database connection: %v", err) } defer db.Close() if err := db.Ping(); err != nil { log.Fatalf("Failed to connect to the database: %v", err) } log.Println("Successfully connected to the database.") // Ensure history directory exists if _, err := os.Stat(historyDir); os.IsNotExist(err) { os.Mkdir(historyDir, 0755) } // Serve static files fs := http.FS(embeddedFiles) // HTTP Handlers http.Handle("/", http.FileServer(fs)) http.HandleFunc("/query", handleQuery) http.HandleFunc("/history/", handleHistory) log.Println("Starting server on :8080...") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatalf("Server failed: %v", err) } } func loadOrCreateConfig(path string) (Config, error) { if _, err := os.Stat(path); os.IsNotExist(err) { fmt.Printf("Configuration file %q was not found.\n", path) fmt.Println("Please provide MySQL connection details to create it.") cfg, promptErr := promptConfigFromConsole() if promptErr != nil { return Config{}, promptErr } data, marshalErr := json.MarshalIndent(cfg, "", " ") if marshalErr != nil { return Config{}, fmt.Errorf("failed to serialize config: %w", marshalErr) } if writeErr := os.WriteFile(path, append(data, '\n'), 0600); writeErr != nil { return Config{}, fmt.Errorf("failed to create %s: %w", path, writeErr) } fmt.Printf("Created %s successfully.\n", path) return cfg, nil } else if err != nil { return Config{}, fmt.Errorf("failed to check config file: %w", err) } file, err := os.Open(path) if err != nil { return Config{}, fmt.Errorf("failed to open config file: %w", err) } defer file.Close() var cfg Config decoder := json.NewDecoder(file) if err := decoder.Decode(&cfg); err != nil { return Config{}, fmt.Errorf("failed to decode config: %w", err) } return cfg, nil } func promptConfigFromConsole() (Config, error) { reader := bufio.NewReader(os.Stdin) hostname, err := promptRequired(reader, "hostname") if err != nil { return Config{}, err } port, err := promptPort(reader, 3306) if err != nil { return Config{}, err } username, err := promptRequired(reader, "username") if err != nil { return Config{}, err } password, err := promptRequired(reader, "password") if err != nil { return Config{}, err } database, err := promptRequired(reader, "database") if err != nil { return Config{}, err } return Config{ Username: username, Password: password, Hostname: hostname, Port: port, Database: database, }, nil } func promptRequired(reader *bufio.Reader, field string) (string, error) { for { fmt.Printf("%s: ", field) value, err := reader.ReadString('\n') if err != nil { return "", fmt.Errorf("failed to read %s: %w", field, err) } value = strings.TrimSpace(value) if value == "" { fmt.Printf("%s is required.\n", field) continue } return value, nil } } func promptPort(reader *bufio.Reader, defaultPort int) (int, error) { for { fmt.Printf("port [%d]: ", defaultPort) value, err := reader.ReadString('\n') if err != nil { return 0, fmt.Errorf("failed to read port: %w", err) } value = strings.TrimSpace(value) if value == "" { return defaultPort, nil } port, atoiErr := strconv.Atoi(value) if atoiErr != nil || port <= 0 || port > 65535 { fmt.Println("Port must be a number between 1 and 65535.") continue } return port, nil } } func serveIndex(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "index.html") } func handleHistory(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: files, err := ioutil.ReadDir(historyDir) if err != nil { http.Error(w, "Could not read history", http.StatusInternalServerError) return } var history []QueryResult for _, file := range files { if filepath.Ext(file.Name()) == ".json" { data, err := ioutil.ReadFile(filepath.Join(historyDir, file.Name())) if err != nil { continue } var qr QueryResult if err := json.Unmarshal(data, &qr); err == nil { history = append(history, qr) } } } // Sort by timestamp, oldest first sort.Slice(history, func(i, j int) bool { return history[i].Timestamp < history[j].Timestamp }) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(history) case http.MethodDelete: id := strings.TrimPrefix(r.URL.Path, "/history/") if id == "" { http.Error(w, "History ID is required", http.StatusBadRequest) return } // Basic sanitization to prevent directory traversal cleanID := filepath.Base(id) if cleanID != id || strings.Contains(cleanID, "..") { http.Error(w, "Invalid history ID", http.StatusBadRequest) return } filePath := filepath.Join(historyDir, cleanID+".json") if _, err := os.Stat(filePath); os.IsNotExist(err) { http.Error(w, "History item not found", http.StatusNotFound) return } if err := os.Remove(filePath); err != nil { log.Printf("Failed to delete history file %s: %v", filePath, err) http.Error(w, "Failed to delete history item", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func handleQuery(w http.ResponseWriter, r *http.Request) { var req QueryRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } query := strings.TrimSpace(req.Query) query_lower := strings.ToLower(query) isSelect := strings.HasPrefix(query_lower, "select") || strings.HasPrefix(query_lower, "show") || strings.HasPrefix(query_lower, "describe") || strings.HasPrefix(query_lower, "desc") result := QueryResult{ ID: req.ID, Query: req.Query, Timestamp: time.Now().UnixNano(), } // Generate new ID if it's a new query if result.ID == "" { result.ID = strconv.FormatInt(result.Timestamp, 10) } startTime := time.Now() if isSelect { // It's a query that returns rows rows, err := db.Query(query) if err != nil { result.Error = err.Error() } else { defer rows.Close() cols, _ := rows.Columns() result.Columns = cols for rows.Next() { // Create a slice of interface{}'s to represent a row columns := make([]interface{}, len(cols)) columnPointers := make([]interface{}, len(cols)) for i := range columns { columnPointers[i] = &columns[i] } // Scan the result into the column pointers... if err := rows.Scan(columnPointers...); err != nil { result.Error = err.Error() break } // Convert byte slices to strings for better JSON representation for i, col := range columns { if b, ok := col.([]byte); ok { columns[i] = string(b) } } result.Rows = append(result.Rows, columns) } if err := rows.Err(); err != nil { result.Error = err.Error() } } } else { // It's a statement like INSERT, UPDATE, DELETE res, err := db.Exec(query) if err != nil { result.Error = err.Error() } else { affected, err := res.RowsAffected() if err != nil { result.Error = err.Error() } else { result.RowsAffected = affected } } } result.Duration = time.Since(startTime).Milliseconds() // Save to history filePath := filepath.Join(historyDir, result.ID+".json") fileData, _ := json.MarshalIndent(result, "", " ") if err := ioutil.WriteFile(filePath, fileData, 0644); err != nil { log.Printf("Failed to write history file %s: %v", filePath, err) // Don't send a server error back to client, they can still see the result } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(result) }