This commit is contained in:
commit
0ef15167d5
28 changed files with 2789 additions and 0 deletions
192
pkg/reporter/reporter.go
Normal file
192
pkg/reporter/reporter.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
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] + "..."
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue