2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
3 Copyright (C) 2020-2023 Sergey Matveev <stargrave@stargrave.org>
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU Affero General Public License as
7 published by the Free Software Foundation, version 3 of the License.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU Affero General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
34 "github.com/go-git/go-git/v5"
35 "github.com/go-git/go-git/v5/plumbing"
36 "github.com/go-git/go-git/v5/plumbing/object"
37 "github.com/vorlif/spreak"
38 "go.stargrave.org/sgblog"
42 //go:embed gemini-menu.tmpl
44 //go:embed gemini-entry.tmpl
45 TmplGemEntryRaw string
47 TmplGemMenu = template.Must(template.New("menu").Parse(TmplGemMenuRaw))
48 TmplGemEntry = template.Must(template.New("entry").Parse(TmplGemEntryRaw))
51 func makeGemErr(err error) {
52 fmt.Print("59 " + err.Error() + "\r\n")
56 func serveGemini(cfgPath string) {
57 cfg, err := readCfg(cfgPath)
61 initLocalizer(cfg.Lang)
63 headHash, err := initRepo(cfg)
68 scanner := bufio.NewScanner(io.LimitReader(os.Stdin, 2+1<<10))
70 makeGemErr(errors.New("no CRLF found"))
72 urlRaw := scanner.Text()
73 u, err := url.Parse(urlRaw)
77 if u.Scheme != "gemini" {
78 makeGemErr(errors.New("only gemini:// is supported" + u.String()))
83 if offsetRaw, exists := u.Query()["offset"]; exists {
84 offset, err = strconv.Atoi(offsetRaw[0])
89 repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
93 topicsCache, err := getTopicsCache(cfg, repoLog)
97 repoLog, err = repo.Log(&git.LogOptions{From: *headHash})
103 var commits CommitIterNext
105 if t, exists := u.Query()["topic"]; exists {
107 hashes := topicsCache[topic]
109 makeGemErr(errors.New("no posts with that topic"))
111 if len(hashes) > offset {
112 hashes = hashes[offset:]
115 commits = &HashesIter{hashes}
117 for i := 0; i < offset; i++ {
118 if _, err = repoLog.Next(); err != nil {
127 entries := make([]TableMenuEntry, 0, PageEntries)
128 for i := 0; i < PageEntries; i++ {
129 commit, err := commits.Next()
134 lines := msgSplit(commit.Message)
135 entries = append(entries, TableMenuEntry{
138 LinesNum: len(lines) - 2,
139 ImagesNum: len(listImgs(cfg, commit.Hash)),
140 CommentsNum: len(sgblog.ParseComments(sgblog.GetNote(
141 repo, commentsTree, commit.Hash,
143 Topics: sgblog.ParseTopics(sgblog.GetNote(
144 repo, topicsTree, commit.Hash,
148 offsetPrev := offset - PageEntries
152 err = TmplGemMenu.Execute(os.Stdout, struct {
160 Entries []TableMenuEntry
168 OffsetPrev: offsetPrev,
169 OffsetNext: offset + PageEntries,
172 Topics: topicsCache.Topics(),
173 Version: sgblog.Version,
178 } else if sha1DigestRe.MatchString(u.Path[1:]) {
179 commit, err := repo.CommitObject(plumbing.NewHash(u.Path[1:]))
183 title := msgSplit(commit.Message)[0]
184 err = TmplGemEntry.Execute(os.Stdout, struct {
187 Commit *object.Commit
200 When: commit.Author.When.Format(sgblog.WhenFmt),
202 Note: string(sgblog.GetNote(repo, notesTree, commit.Hash)),
203 Images: listImgs(cfg, commit.Hash),
204 Comments: sgblog.ParseComments(sgblog.GetNote(repo, commentsTree, commit.Hash)),
205 Topics: sgblog.ParseTopics(sgblog.GetNote(repo, topicsTree, commit.Hash)),
206 Version: sgblog.Version,
207 TitleEscaped: url.PathEscape(fmt.Sprintf(
208 "Re: %s (%s)", title, commit.Hash,
214 } else if strings.HasPrefix(u.Path, "/img/") {
215 pth := strings.TrimPrefix(u.Path, "/img/")
216 if strings.Contains(pth, "..") {
217 log.Fatalln("unacceptable double dots")
219 typ := ImgTypes[path.Ext(pth)]
221 typ = "application/octet-stream"
223 fd, err := os.Open(path.Join(cfg.ImgPath, pth))
227 bw := bufio.NewWriter(os.Stdout)
228 bw.Write([]byte("20 " + typ + "\r\n"))
229 io.Copy(bw, bufio.NewReader(fd))
233 makeGemErr(errors.New("unknown URL action"))