Mercurial > qtd
changeset 0:250e333a372e
initial
| author | Atarwn Gard <a@qwa.su> |
|---|---|
| date | Mon, 20 Oct 2025 00:13:07 +0500 |
| parents | |
| children | 960887cc6641 |
| files | go.mod qtd.go |
| diffstat | 2 files changed, 273 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/go.mod Mon Oct 20 00:13:07 2025 +0500 @@ -0,0 +1,3 @@ +module qtd + +go 1.24 \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/qtd.go Mon Oct 20 00:13:07 2025 +0500 @@ -0,0 +1,270 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "html" + "log" + "net" + "net/http" + "os" + "path/filepath" + "sort" + "strings" +) + +var ( + dir = flag.String("d", ".", "root directory") + port = flag.Int("p", 8080, "port") +) + +func parseFile(path string) (title, content string, err error) { + file, err := os.Open(path) + if err != nil { + return "", "", err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + if !scanner.Scan() { + return "", "", fmt.Errorf("empty file") + } + + firstLine := scanner.Text() + if strings.HasPrefix(firstLine, "title ") { + title = strings.TrimPrefix(firstLine, "title ") + } else { + content = firstLine + "\n" + } + + var buf strings.Builder + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") { + continue + } + buf.WriteString(line) + buf.WriteString("\n") + } + if content == "" { + content = buf.String() + } else { + content += buf.String() + } + + return title, content, nil +} + +func resolvePath(root, inputPath string) (title, content string, isDir bool, err error) { + cleanPath := filepath.Clean("/" + inputPath) + target := filepath.Join(root, cleanPath) + + info, err := os.Stat(target) + if err != nil { + if os.IsNotExist(err) { + if !strings.HasSuffix(inputPath, "/") { + title, content, err2 := parseFile(target) + if err2 == nil { + return title, content, false, nil + } + } + indexFile := filepath.Join(target, "index") + if _, err3 := os.Stat(indexFile); err3 == nil { + title, content, _ := parseFile(indexFile) + return title, content, true, nil + } + if strings.HasSuffix(inputPath, "/") || inputPath == "" { + listing, err := dirListing(filepath.Dir(target)) + if err == nil { + return "", listing, true, nil + } + } + return "", "", false, fmt.Errorf("not found") + } + return "", "", false, err + } + + if info.IsDir() { + indexFile := filepath.Join(target, "index") + if _, err := os.Stat(indexFile); err == nil { + title, content, _ := parseFile(indexFile) + return title, content, true, nil + } + listing, err := dirListing(target) + if err != nil { + return "", "", true, err + } + return "", listing, true, nil + } + + title, content, err = parseFile(target) + return title, content, false, err +} + +func dirListing(dir string) (string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", err + } + + var items []string + for _, e := range entries { + if strings.HasPrefix(e.Name(), ".") { + continue + } + suffix := "" + if e.IsDir() { + suffix = "/" + } + items = append(items, " => "+e.Name()+suffix) + } + sort.Strings(items) + return strings.Join(items, "\n"), nil +} + +func linkify(text string) string { + lines := strings.Split(text, "\n") + var result []string + + for _, line := range lines { + if strings.HasPrefix(line, " => ") { + parts := strings.Fields(line) + if len(parts) >= 2 { + target := parts[1] + label := target + if len(parts) > 2 { + label = strings.Join(parts[2:], " ") + } + + href := target + if !strings.HasPrefix(target, "http://") && !strings.HasPrefix(target, "https://") { + href = "./" + strings.TrimPrefix(target, "/") + } + + linkLine := ` => <a href="` + html.EscapeString(href) + `">` + html.EscapeString(label) + `</a>` + result = append(result, linkLine) + continue + } + } + result = append(result, html.EscapeString(line)) + } + + return strings.Join(result, "\n") +} + +func handleHTTP(w http.ResponseWriter, r *http.Request) { + root := *dir + path := strings.TrimPrefix(r.URL.Path, "/") + title, content, isDir, err := resolvePath(root, path) + + if err != nil { + http.NotFound(w, r) + return + } + + if strings.HasSuffix(r.URL.Path, "/") || isDir || path == "" { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + content = linkify(content) + + htmlContent := `<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <style> + @import url(https://fonts.bunny.net/css?family=jetbrains-mono:400); + *{margin:0;box-sizing:border-box;font-family:'JetBrains Mono',monospace;} + body { + background: #1a1b26; + color: #a9b1d6; + line-height: 1.2em; + } + .qtd { + background: #11111b; + padding: 4px 12px; + } + .qtd, .qtd a, .qtd a:visited { + color: #c0caf5; + } + pre { + padding: 8px; + overflow-x: auto; + } + a, a:visited { + color: #7dcfff; + } + </style> + <title>` + html.EscapeString(title) + `</title> +</head> +<body> + <div class="qtd"> + echo ` + html.EscapeString(path) + ` | nc <a href="//` + html.EscapeString(r.Host) + `">` + html.EscapeString(r.Host) + `</a> 1130 | less + </div> + <pre>` + content + `</pre> +</body> +</html>` + + w.Write([]byte(htmlContent)) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Write([]byte(content)) +} + +func handleTCP(conn net.Conn, root string) { + defer conn.Close() + reader := bufio.NewReader(conn) + line, err := reader.ReadString('\n') + if err != nil { + return + } + path := strings.TrimSpace(line) + + _, content, _, err := resolvePath(root, path) + if err != nil { + content = "Not found\n" + } + + conn.Write([]byte(content)) +} + +func startTCPServer(root string) { + listener, err := net.Listen("tcp", ":1130") + if err != nil { + log.Printf("TCP server error: %v", err) + return + } + defer listener.Close() + + log.Printf("TCP server on :1130, root=%s", root) + + for { + conn, err := listener.Accept() + if err != nil { + continue + } + go handleTCP(conn, root) + } +} + +func main() { + flag.Parse() + if flag.NArg() > 0 { + log.Fatal("unexpected arguments; use flags only") + } + + root, err := filepath.Abs(*dir) + if err != nil { + log.Fatal(err) + } + + go startTCPServer(root) + + addr := fmt.Sprintf(":%d", *port) + http.HandleFunc("/", handleHTTP) + + log.Printf("HTTP server on %s, root=%s", addr, root) + log.Fatal(http.ListenAndServe(addr, nil)) +} \ No newline at end of file