package codeview import ( "encoding/json" "fmt" "io" "log/slog" "net/http" "net/url" "strings" "github.com/kulak/gemini" ) /* * Codeview is a feature that proxies my personal git server (forgejo) * Allowing me to render code natively in gemini * * Features: * - [x] Individual file view with line numbers? * - [x] Dir view with links to files * - [x] Project root view with markdown to gemtxt conversion and file tree */ func HandleRequest(w gemini.ResponseWriter, req *gemini.Request) { path := req.URL.Path switch { case strings.HasPrefix(path, "/codeview/dir"): serveDir(w, req, path) case strings.HasPrefix(path, "/codeview/raw/"): serveFile(w, req, path) default: w.WriteStatusMsg(gemini.StatusNotFound, "Page not found") } } func serveDir(w gemini.ResponseWriter, req *gemini.Request, path string) { repo := req.URL.Query().Get("repo") filepath := url.QueryEscape(req.URL.Query().Get("filepath")) branch := req.URL.Query().Get("branch") commit := req.URL.Query().Get("commit") slog.Info("Serving directory", "repo", repo, "filepath", filepath, "branch", branch, "commit", commit) var gitURL string ref := "" if branch != "" { ref = branch } else if commit != "" { ref = commit } if filepath == "." { gitURL = "https://git.travisshears.com/api/v1/repos/travisshears/" + repo + "/contents?ref=" + ref } else { gitURL = "https://git.travisshears.com/api/v1/repos/travisshears/" + repo + "/contents/" + filepath + "?ref=" + ref } slog.Info("Fetching directory contents", "url", gitURL) httpReq, err := http.NewRequest("GET", gitURL, nil) if err != nil { slog.Error("Failed to create request to git server", "error", err) w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server") return } res, err := http.DefaultClient.Do(httpReq) if err != nil { slog.Error("Failed to send request to git server", "error", err) w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server") return } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { slog.Error("Failed to read res body", "error", err) w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server") return } if res.StatusCode < 200 || res.StatusCode >= 300 { slog.Error("unexpected status code", "statusCode", res.StatusCode, "body", string(body)) w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server") return } var dirRes []struct { Name string `json:"name"` Path string `json:"path"` URL string `json:"html_url"` Type string `json:"type"` RawURL string `json:"download_url"` } if err := json.Unmarshal(body, &dirRes); err != nil { slog.Error("failed to parse response", "error", err) w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem parsing response from git server") return } slog.Info("Successfully parsed response from git server", "res", dirRes) var content strings.Builder w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") content.WriteString("# Directory CodeView\n") httpURL := fmt.Sprintf("https://git.travisshears.com/travisshears/personal-gemini-capsule/src/branch/main/%s", filepath) if branch != "" { httpURL = fmt.Sprintf("https://git.travisshears.com/travisshears/personal-gemini-capsule/src/branch/%s/%s", branch, filepath) } else if commit != "" { httpURL = fmt.Sprintf("https://git.travisshears.com/travisshears/personal-gemini-capsule/src/commit/%s/%s", commit, filepath) } content.WriteString(fmt.Sprintf("This is a code view which proxies my personal git server. On the clearnet the following dir is available:\n=> %s here\n\n", httpURL)) // print readme page // var readmeURL string for _, file := range dirRes { if file.Type == "file" && file.Name == "README.md" { slog.Info("Found README.md", "file", file) body, err := getRemoteFile(file.RawURL) if err != nil { slog.Error("Failed to fetch README.md", "url", file.RawURL, "error", err) continue } content.WriteString("README.md found, directory listing below.\n") content.WriteString(renderCodeFile(body)) content.WriteString("\n\n-----------------------------\n\n") } } for _, file := range dirRes { switch file.Type { case "dir": url := fmt.Sprintf("/codeview/dir?repo=%s&filepath=%s&commit=%s&branch=%s", repo, file.Path, commit, branch) content.WriteString(fmt.Sprintf("=> %s %s/\n", url, file.Path)) case "file": p := strings.TrimPrefix(file.URL, "https://git.travisshears.com/travisshears/") url := fmt.Sprintf("/codeview/raw/%s", p) content.WriteString(fmt.Sprintf("=> %s %s\n", url, file.Path)) } } content.WriteString("\n\n-----------------------------\n\n=> / Back to home\n") w.WriteBody([]byte(content.String())) } func renderCodeFile(code string) string { // Escape any triple backticks in the code to avoid prematurely ending Gemini preformatted blocks code = strings.ReplaceAll(code, "```", "\\`\\`\\`") return fmt.Sprintf("```code\n%s\n```", code) } func getRemoteFile(url string) (string, error) { resp, err := http.Get(url) if err != nil { slog.Error("Failed to fetch remote file", "url", url, "error", err) return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { slog.Error("Failed to fetch remote file", "url", url, "status", resp.Status) return "", fmt.Errorf("failed to fetch remote file") } body, err := io.ReadAll(resp.Body) if err != nil { slog.Error("Failed to read remote file", "url", url, "error", err) return "", err } return string(body), nil } func serveFile(w gemini.ResponseWriter, req *gemini.Request, path string) { path = strings.TrimPrefix(path, "/codeview/raw/") rawURL := strings.Replace(path, "src", "raw", 1) rawURL = "https://git.travisshears.com/travisshears/" + rawURL remoteURL := "https://git.travisshears.com/travisshears/" + path body, err := getRemoteFile(rawURL) if err != nil { slog.Error("Failed to create request to git server", "error", err) w.WriteStatusMsg(gemini.StatusGeneralPermFail, "Problem connecting to git server") return } var content strings.Builder w.WriteStatusMsg(gemini.StatusSuccess, "text/gemini") // filename := strings.TrimPrefix(remoteURL, "https://git.travisshears.com/travisshears/") // content.WriteString(fmt.Sprintf("# File: %s\n", filename)) content.WriteString("# File CodeView\n") content.WriteString(fmt.Sprintf("This is a code view which proxies my personal git server. On the clearnet the following file is available at:\n=> %s %s\n\n", remoteURL, path)) content.WriteString("----------------------------\n\n") content.WriteString(renderCodeFile(body)) content.WriteString("\n\n-----------------------------\n\n=> / Back to home\n") w.WriteBody([]byte(content.String())) }