From 2e674b4f8a066ed523993235432175d4bfc59d57 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sat, 14 Mar 2020 13:22:03 +0300 Subject: [PATCH] Gopher protocol support --- cmd/sgblog/main.go | 212 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 196 insertions(+), 16 deletions(-) diff --git a/cmd/sgblog/main.go b/cmd/sgblog/main.go index 2db6dbe..a55411f 100644 --- a/cmd/sgblog/main.go +++ b/cmd/sgblog/main.go @@ -19,6 +19,7 @@ along with this program. If not, see . package main import ( + "bufio" "bytes" "compress/gzip" "encoding/hex" @@ -50,6 +51,7 @@ import ( const ( PageEntries = 50 AtomFeed = "feed.atom" + CRLF = "\r\n" ) var ( @@ -66,6 +68,7 @@ var ( "https": struct{}{}, "telnet": struct{}{}, } + DashLine = strings.Repeat("-", 72) ) type TableEntry struct { @@ -91,6 +94,8 @@ type Cfg struct { CommentsNotesRef string CommentsEmail string + + GopherDomain string } func makeA(href, text string) string { @@ -223,20 +228,48 @@ func checkETag(etag hash.Hash) { } } -func main() { - cfgPath := os.Getenv("SGBLOG_CFG") - if cfgPath == "" { - log.Fatalln("SGBLOG_CFG is not set") - } - pathInfo, exists := os.LookupEnv("PATH_INFO") - if !exists { - pathInfo = "/" +func initRepo(cfg *Cfg) (*plumbing.Hash, error) { + var err error + repo, err = git.PlainOpen(cfg.GitPath) + if err != nil { + return nil, err } - queryValues, err := url.ParseQuery(os.Getenv("QUERY_STRING")) + head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false) if err != nil { - makeErr(err) + return nil, err } + headHash := head.Hash() + if notes, err := repo.Notes(); err == nil { + var notesRef *plumbing.Reference + var commentsRef *plumbing.Reference + notes.ForEach(func(ref *plumbing.Reference) error { + switch string(ref.Name()) { + case "refs/notes/commits": + notesRef = ref + case cfg.CommentsNotesRef: + commentsRef = ref + } + return nil + }) + if notesRef != nil { + if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil { + notesTree, _ = commentsCommit.Tree() + } + } + if commentsRef != nil { + if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil { + commentsTree, _ = commentsCommit.Tree() + } + } + } + return &headHash, nil +} +func serveHTTP() { + cfgPath := os.Getenv("SGBLOG_CFG") + if cfgPath == "" { + log.Fatalln("SGBLOG_CFG is not set") + } cfgRaw, err := ioutil.ReadFile(cfgPath) if err != nil { makeErr(err) @@ -253,6 +286,14 @@ func main() { if err = json.Unmarshal(cfgRaw, &cfg); err != nil { makeErr(err) } + pathInfo, exists := os.LookupEnv("PATH_INFO") + if !exists { + pathInfo = "/" + } + queryValues, err := url.ParseQuery(os.Getenv("QUERY_STRING")) + if err != nil { + makeErr(err) + } etagHash, err := blake2b.New256(nil) if err != nil { @@ -295,11 +336,7 @@ func main() { atomURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomFeed defaultLinks = append(defaultLinks, ``) - repo, err = git.PlainOpen(cfg.GitPath) - if err != nil { - makeErr(err) - } - head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false) + headHash, err := initRepo(cfg) if err != nil { makeErr(err) } @@ -451,7 +488,7 @@ func main() { out.Write(refs.Bytes()) out.Write([]byte("\n")) } else if pathInfo == "/"+AtomFeed { - commit, err := repo.CommitObject(head.Hash()) + commit, err := repo.CommitObject(*headHash) if err != nil { makeErr(err) } @@ -605,3 +642,146 @@ func main() { } os.Stdout.Write(outBuf.Bytes()) } + +func serveGopher() { + cfgPath := os.Args[2] + cfgRaw, err := ioutil.ReadFile(cfgPath) + if err != nil { + log.Fatalln(err) + } + var cfgGeneral map[string]interface{} + if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil { + log.Fatalln(err) + } + cfgRaw, err = json.Marshal(cfgGeneral) + if err != nil { + log.Fatalln(err) + } + var cfg *Cfg + if err = json.Unmarshal(cfgRaw, &cfg); err != nil { + log.Fatalln(err) + } + if cfg.GopherDomain == "" { + log.Fatalln("GopherDomain is not configured") + } + + headHash, err := initRepo(cfg) + if err != nil { + log.Fatalln(err) + } + + scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 1<<8)) + if !scanner.Scan() { + log.Fatalln(errors.New("no CRLF found")) + } + selector := scanner.Text() + if selector == "" { + selector = "offset/0" + } + if strings.HasPrefix(selector, "offset/") { + offset, err := strconv.Atoi(selector[len("offset/"):]) + if err != nil { + log.Fatalln(err) + } + repoLog, err := repo.Log(&git.LogOptions{From: *headHash}) + if err != nil { + log.Fatalln(err) + } + commitN := 0 + for i := 0; i < offset; i++ { + if _, err = repoLog.Next(); err != nil { + break + } + commitN++ + } + + logEnded := false + var menu bytes.Buffer + for i := 0; i < PageEntries; i++ { + commit, err := repoLog.Next() + if err != nil { + logEnded = true + break + } + commitN++ + lines := msgSplit(commit.Message) + + var commentsValue string + if l := len(parseComments(getNote(commentsTree, commit.Hash))); l > 0 { + commentsValue = fmt.Sprintf(" (%dC)", l) + } + menu.WriteString(fmt.Sprintf( + "0[%s] %s (%dL)%s\t/%s\t%s\t%d%s", + commit.Author.When.Format(sgblog.WhenFmt), + lines[0], + len(lines)-2, + commentsValue, + commit.Hash.String(), + cfg.GopherDomain, 70, CRLF, + )) + } + + var links bytes.Buffer + if offset > 0 { + offsetPrev := offset - PageEntries + if offsetPrev < 0 { + offsetPrev = 0 + } + links.WriteString(fmt.Sprintf( + "1Prev\toffset/%d\t%s\t%d%s", + offsetPrev, + cfg.GopherDomain, 70, CRLF, + )) + } + if !logEnded { + links.WriteString(fmt.Sprintf( + "1Next\toffset/%d\t%s\t%d%s", + offset+PageEntries, + cfg.GopherDomain, 70, CRLF, + )) + } + + fmt.Printf( + "i%s (%d-%d)\t\tnull.host\t1%s", + cfg.Title, + offset, + offset+PageEntries, + CRLF, + ) + if cfg.AboutURL != "" { + fmt.Printf("iAbout: %s\t\tnull.host\t1%s", cfg.AboutURL, CRLF) + } + fmt.Print(links.String()) + fmt.Print(menu.String()) + fmt.Print("." + CRLF) + } else if sha1DigestRe.MatchString(selector) { + commit, err := repo.CommitObject(plumbing.NewHash(selector[1:])) + if err != nil { + log.Fatalln(err) + } + fmt.Printf( + "What: %s\nWhen: %s\n%s\n%s", + commit.Hash.String(), + commit.Author.When.Format(sgblog.WhenFmt), + DashLine, + commit.Message, + ) + notesRaw := getNote(notesTree, commit.Hash) + if len(notesRaw) > 0 { + fmt.Printf("%s\nNote:\n%s\n", DashLine, string(notesRaw)) + } + for i, comment := range parseComments(getNote(commentsTree, commit.Hash)) { + fmt.Printf("%s\ncomment %d:\n%s\n", DashLine, i, comment) + } + } else { + log.Fatalln(errors.New("unknown selector")) + } +} + +func main() { + if len(os.Args) == 3 && os.Args[1] == "-gopher" { + serveGopher() + } else { + serveHTTP() + } +} -- 2.44.0