2023-12-24 17:02:13 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"flag"
|
2023-12-24 17:37:07 +00:00
|
|
|
"fmt"
|
2023-12-24 17:02:13 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
2023-12-29 20:05:02 +00:00
|
|
|
"sync"
|
2023-12-24 17:37:07 +00:00
|
|
|
"time"
|
2023-12-24 17:02:13 +00:00
|
|
|
|
|
|
|
"github.com/fatih/color"
|
|
|
|
)
|
|
|
|
|
|
|
|
type LinkCheckResult struct {
|
|
|
|
Link string
|
|
|
|
IsValid bool
|
|
|
|
StatusCode int
|
|
|
|
}
|
|
|
|
|
2023-12-29 20:05:02 +00:00
|
|
|
var wg sync.WaitGroup
|
|
|
|
var verboseMode bool
|
|
|
|
|
2023-12-24 17:37:07 +00:00
|
|
|
func logWithColor(level string, msg string, args ...interface{}) {
|
2023-12-29 20:05:02 +00:00
|
|
|
if verboseMode {
|
|
|
|
timestamp := time.Now().Format("2006/01/02 15:04:05")
|
|
|
|
colorFunc := color.New(color.FgWhite).SprintFunc()
|
|
|
|
switch level {
|
|
|
|
case "ERROR":
|
|
|
|
colorFunc = color.New(color.FgRed).SprintFunc()
|
|
|
|
case "WARN":
|
|
|
|
colorFunc = color.New(color.FgYellow).SprintFunc()
|
|
|
|
case "INFO":
|
|
|
|
colorFunc = color.New(color.FgCyan).SprintFunc()
|
|
|
|
}
|
|
|
|
fmt.Printf("%s %s %s\n", timestamp, colorFunc(level), fmt.Sprintf(msg, args...))
|
2023-12-24 17:37:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-29 21:02:30 +00:00
|
|
|
func worker(jobs <-chan string, results chan<- LinkCheckResult, verboseFlag *bool, failedOnly *bool) {
|
|
|
|
for link := range jobs {
|
|
|
|
resp, err := http.Head(link)
|
|
|
|
if err != nil {
|
|
|
|
if *verboseFlag {
|
|
|
|
logWithColor("ERROR", "Invalid link: %s", link)
|
|
|
|
}
|
|
|
|
results <- LinkCheckResult{
|
|
|
|
Link: link,
|
|
|
|
IsValid: false,
|
|
|
|
StatusCode: http.StatusBadRequest,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
isValid := resp.StatusCode == http.StatusOK
|
|
|
|
result := LinkCheckResult{
|
|
|
|
Link: link,
|
|
|
|
IsValid: isValid,
|
|
|
|
StatusCode: resp.StatusCode,
|
|
|
|
}
|
|
|
|
results <- result
|
|
|
|
if *verboseFlag || (!*failedOnly && !isValid) {
|
|
|
|
statusColor := color.GreenString
|
|
|
|
if !isValid {
|
|
|
|
statusColor = color.RedString
|
|
|
|
}
|
|
|
|
logWithColor("INFO", "%s: %s", link, statusColor("%d", resp.StatusCode))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
wg.Done()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-24 17:02:13 +00:00
|
|
|
func main() {
|
|
|
|
filename := flag.String("file", "", "Markdown file to test")
|
2023-12-29 20:05:02 +00:00
|
|
|
verboseFlag := flag.Bool("verbose", false, "Enable verbose mode")
|
2023-12-24 17:02:13 +00:00
|
|
|
failedOnly := flag.Bool("failed-only", false, "Return only failed links")
|
|
|
|
flag.Parse()
|
|
|
|
|
2023-12-29 20:05:02 +00:00
|
|
|
verboseMode = *verboseFlag
|
|
|
|
|
2023-12-24 17:02:13 +00:00
|
|
|
if *filename == "" {
|
2023-12-24 17:37:07 +00:00
|
|
|
logWithColor("INFO", "Please provide a markdown file.")
|
2023-12-24 17:02:13 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
file, err := os.Open(*filename)
|
|
|
|
if err != nil {
|
2023-12-24 17:37:07 +00:00
|
|
|
logWithColor("ERROR", "Failed to open file: %v", err)
|
2023-12-24 17:02:13 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2023-12-24 18:58:26 +00:00
|
|
|
if file == nil {
|
|
|
|
logWithColor("ERROR", "File is nil")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2023-12-24 17:02:13 +00:00
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(file)
|
2023-12-24 18:58:26 +00:00
|
|
|
if scanner == nil {
|
|
|
|
logWithColor("ERROR", "Scanner is nil")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2023-12-24 17:02:13 +00:00
|
|
|
re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
|
|
|
|
|
2023-12-29 21:02:30 +00:00
|
|
|
jobs := make(chan string, 10000)
|
|
|
|
results := make(chan LinkCheckResult, 10000)
|
|
|
|
|
|
|
|
// Start workers
|
|
|
|
for i := 1; i <= 10; i++ {
|
|
|
|
go worker(jobs, results, verboseFlag, failedOnly)
|
|
|
|
}
|
2023-12-24 17:02:13 +00:00
|
|
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
line := scanner.Text()
|
|
|
|
matches := re.FindAllStringSubmatch(line, -1)
|
|
|
|
for _, match := range matches {
|
2023-12-29 20:05:02 +00:00
|
|
|
wg.Add(1)
|
2023-12-29 21:02:30 +00:00
|
|
|
jobs <- strings.TrimSpace(match[2])
|
2023-12-24 17:02:13 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-29 21:02:30 +00:00
|
|
|
close(jobs)
|
2023-12-24 17:02:13 +00:00
|
|
|
|
2023-12-29 21:02:30 +00:00
|
|
|
go func() {
|
|
|
|
wg.Wait()
|
|
|
|
close(results)
|
|
|
|
}()
|
2023-12-29 20:05:02 +00:00
|
|
|
|
2023-12-29 21:02:30 +00:00
|
|
|
var invalidCount int
|
2023-12-24 18:58:26 +00:00
|
|
|
|
2023-12-29 21:02:30 +00:00
|
|
|
for res := range results {
|
|
|
|
if *failedOnly && res.IsValid {
|
|
|
|
continue
|
2023-12-24 17:02:13 +00:00
|
|
|
}
|
2023-12-29 21:02:30 +00:00
|
|
|
if *verboseFlag || (!*failedOnly && !res.IsValid) {
|
|
|
|
if res.IsValid {
|
|
|
|
logWithColor("INFO", "Link %s is valid with status code %d", res.Link, res.StatusCode)
|
|
|
|
} else {
|
|
|
|
logWithColor("ERROR", "Link %s is invalid with status code %d", res.Link, res.StatusCode)
|
|
|
|
invalidCount++
|
2023-12-29 20:05:02 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-29 21:02:30 +00:00
|
|
|
wg.Done()
|
2023-12-24 17:02:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if invalidCount > 0 {
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2023-12-29 21:02:30 +00:00
|
|
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
|
|
|
logWithColor("ERROR", "Error scanning file: %v", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
2023-12-24 17:02:13 +00:00
|
|
|
}
|