192 lines
4.6 KiB
Go
192 lines
4.6 KiB
Go
package reporter
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.nakama.town/fmartingr/dharma/pkg/scraper"
|
|
"github.com/fatih/color"
|
|
)
|
|
|
|
// Reporter is an interface for report generators
|
|
type Reporter interface {
|
|
Generate(results *scraper.Results, writer io.Writer) error
|
|
}
|
|
|
|
// New creates a new reporter based on the format
|
|
func New(format string) (Reporter, error) {
|
|
switch strings.ToLower(format) {
|
|
case "pretty":
|
|
return &PrettyReporter{}, nil
|
|
case "json":
|
|
return &JSONReporter{}, nil
|
|
case "csv":
|
|
return &CSVReporter{}, nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported format: %s", format)
|
|
}
|
|
}
|
|
|
|
// PrettyReporter generates a human-readable report for terminal
|
|
type PrettyReporter struct{}
|
|
|
|
// Generate generates a pretty report
|
|
func (r *PrettyReporter) Generate(results *scraper.Results, writer io.Writer) error {
|
|
red := color.New(color.FgRed).SprintFunc()
|
|
green := color.New(color.FgGreen).SprintFunc()
|
|
yellow := color.New(color.FgYellow).SprintFunc()
|
|
blue := color.New(color.FgBlue).SprintFunc()
|
|
cyan := color.New(color.FgCyan).SprintFunc()
|
|
|
|
// Count internal vs external links
|
|
countInternalSuccess := 0
|
|
countInternalErrors := 0
|
|
countExternalSuccess := 0
|
|
countExternalErrors := 0
|
|
|
|
for _, result := range results.Successes {
|
|
if result.IsExternal {
|
|
countExternalSuccess++
|
|
} else {
|
|
countInternalSuccess++
|
|
}
|
|
}
|
|
|
|
for _, result := range results.Errors {
|
|
if result.IsExternal {
|
|
countExternalErrors++
|
|
} else {
|
|
countInternalErrors++
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(writer, "Website scan report for: %s\n", blue(results.BaseURL))
|
|
fmt.Fprintf(writer, "Scanned at: %s\n", time.Now().Format(time.RFC1123))
|
|
fmt.Fprintf(writer, "Total resources checked: %d\n", results.Total)
|
|
fmt.Fprintf(writer, "Success: %s, Errors: %s\n",
|
|
green(len(results.Successes)),
|
|
red(len(results.Errors)))
|
|
fmt.Fprintf(writer, "Internal links: %s success, %s errors\n",
|
|
green(countInternalSuccess),
|
|
red(countInternalErrors))
|
|
fmt.Fprintf(writer, "External links: %s success, %s errors\n\n",
|
|
green(countExternalSuccess),
|
|
red(countExternalErrors))
|
|
|
|
if len(results.Errors) == 0 {
|
|
fmt.Fprintf(writer, "%s No errors found!\n", green("✓"))
|
|
return nil
|
|
}
|
|
|
|
// Group errors by internal/external
|
|
internalErrors := []scraper.Result{}
|
|
externalErrors := []scraper.Result{}
|
|
|
|
for _, result := range results.Errors {
|
|
if result.IsExternal {
|
|
externalErrors = append(externalErrors, result)
|
|
} else {
|
|
internalErrors = append(internalErrors, result)
|
|
}
|
|
}
|
|
|
|
// Print internal errors first if we have any
|
|
if len(internalErrors) > 0 {
|
|
fmt.Fprintln(writer, "Errors found:")
|
|
|
|
for _, result := range internalErrors {
|
|
status := fmt.Sprintf("%d", result.Status)
|
|
if result.Status == 0 {
|
|
status = "ERR"
|
|
}
|
|
|
|
fmt.Fprintf(writer, "%-6s (%-10s) %s [from: %s]\n",
|
|
red(status),
|
|
yellow(result.Type),
|
|
result.URL,
|
|
result.SourceURL,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Print external errors if we have any
|
|
if len(externalErrors) > 0 {
|
|
if len(internalErrors) > 0 {
|
|
fmt.Fprintln(writer, "")
|
|
}
|
|
fmt.Fprintln(writer, "External Errors:")
|
|
fmt.Fprintln(writer, strings.Repeat("-", 80))
|
|
fmt.Fprintf(writer, "%-6s | %-10s | %s | %s\n", "Status", "Type", "URL", "Source")
|
|
fmt.Fprintln(writer, strings.Repeat("-", 80))
|
|
|
|
for _, result := range externalErrors {
|
|
status := fmt.Sprintf("%d", result.Status)
|
|
if result.Status == 0 {
|
|
status = "ERR"
|
|
}
|
|
|
|
fmt.Fprintf(writer, "%-6s | %-10s | %s | %s\n",
|
|
red(status),
|
|
cyan(result.Type),
|
|
result.URL,
|
|
result.SourceURL,
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// JSONReporter generates a JSON report
|
|
type JSONReporter struct{}
|
|
|
|
// Generate generates a JSON report
|
|
func (r *JSONReporter) Generate(results *scraper.Results, writer io.Writer) error {
|
|
return json.NewEncoder(writer).Encode(results)
|
|
}
|
|
|
|
// CSVReporter generates a CSV report
|
|
type CSVReporter struct{}
|
|
|
|
// Generate generates a CSV report
|
|
func (r *CSVReporter) Generate(results *scraper.Results, writer io.Writer) error {
|
|
csvWriter := csv.NewWriter(writer)
|
|
defer csvWriter.Flush()
|
|
|
|
// Write header
|
|
if err := csvWriter.Write([]string{"Status", "Type", "URL", "Source URL", "Error"}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write errors
|
|
for _, result := range results.Errors {
|
|
status := fmt.Sprintf("%d", result.Status)
|
|
if result.Status == 0 {
|
|
status = "ERROR"
|
|
}
|
|
|
|
if err := csvWriter.Write([]string{
|
|
status,
|
|
result.Type,
|
|
result.URL,
|
|
result.SourceURL,
|
|
result.Error,
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Helper function to truncate strings
|
|
func truncate(s string, maxLen int) string {
|
|
if len(s) <= maxLen {
|
|
return s
|
|
}
|
|
return s[:maxLen-3] + "..."
|
|
}
|