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