]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/main.go
Use external recfiles parser library
[sgblog.git] / cmd / sgblog / main.go
1 /*
2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
4
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.
8
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.
13
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/>.
16 */
17
18 // Git-backed CGI/inetd blogging/phlogging engine
19 package main
20
21 import (
22         "bytes"
23         "crypto/sha1"
24         "fmt"
25         "io/ioutil"
26         "os"
27         "regexp"
28         "sort"
29         "strings"
30         "text/scanner"
31
32         "github.com/go-git/go-git/v5"
33         "github.com/go-git/go-git/v5/plumbing"
34         "github.com/go-git/go-git/v5/plumbing/object"
35         "go.cypherpunks.ru/recfile"
36 )
37
38 const (
39         PageEntries = 50
40 )
41
42 var (
43         sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
44         repo         *git.Repository
45         notesTree    *object.Tree
46         commentsRef  *plumbing.Reference
47         commentsTree *object.Tree
48         topicsRef    *plumbing.Reference
49         topicsTree   *object.Tree
50 )
51
52 type Cfg struct {
53         GitPath string
54         Branch  string
55         Title   string
56
57         URLPrefix string
58
59         AtomBaseURL string
60         AtomId      string
61         AtomAuthor  string
62
63         CSS       string
64         Webmaster string
65         AboutURL  string
66         GitURLs   []string
67
68         CommentsNotesRef string
69         CommentsEmail    string
70
71         TopicsNotesRef  string
72         TopicsCachePath string
73
74         GopherDomain string
75 }
76
77 func msgSplit(msg string) []string {
78         lines := strings.Split(msg, "\n")
79         lines = lines[:len(lines)-1]
80         if len(lines) < 3 {
81                 lines = []string{lines[0], "", ""}
82         }
83         return lines
84 }
85
86 func getNote(tree *object.Tree, what plumbing.Hash) []byte {
87         if tree == nil {
88                 return nil
89         }
90         var entry *object.TreeEntry
91         var err error
92         paths := make([]string, 3)
93         paths[0] = what.String()
94         paths[1] = paths[0][:2] + "/" + paths[0][2:]
95         paths[2] = paths[1][:4+1] + "/" + paths[1][4+1:]
96         for _, p := range paths {
97                 entry, err = tree.FindEntry(p)
98                 if err == nil {
99                         break
100                 }
101         }
102         if entry == nil {
103                 return nil
104         }
105         blob, err := repo.BlobObject(entry.Hash)
106         if err != nil {
107                 return nil
108         }
109         r, err := blob.Reader()
110         if err != nil {
111                 return nil
112         }
113         data, err := ioutil.ReadAll(r)
114         if err != nil {
115                 return nil
116         }
117         return bytes.TrimSuffix(data, []byte{'\n'})
118 }
119
120 func parseComments(data []byte) []string {
121         comments := []string{}
122         r := recfile.NewReader(bytes.NewReader(data))
123         for {
124                 fields, err := r.Next()
125                 if err != nil {
126                         break
127                 }
128                 if len(fields) != 3 ||
129                         fields[0].Name != "From" ||
130                         fields[1].Name != "Date" ||
131                         fields[2].Name != "Body" {
132                         continue
133                 }
134                 comments = append(comments, fmt.Sprintf(
135                         "%s: %s\n%s: %s\n%s",
136                         fields[0].Name, fields[0].Value,
137                         fields[1].Name, fields[1].Value,
138                         fields[2].Value,
139                 ))
140         }
141         return comments
142 }
143
144 func parseTopics(data []byte) []string {
145         var s scanner.Scanner
146         s.Init(bytes.NewBuffer(data))
147         topics := []string{}
148         for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
149                 topics = append(topics, s.TokenText())
150         }
151         sort.Strings(topics)
152         return topics
153 }
154
155 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
156         var err error
157         repo, err = git.PlainOpen(cfg.GitPath)
158         if err != nil {
159                 return nil, err
160         }
161         head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
162         if err != nil {
163                 return nil, err
164         }
165         headHash := head.Hash()
166         if notes, err := repo.Notes(); err == nil {
167                 var notesRef *plumbing.Reference
168                 notes.ForEach(func(ref *plumbing.Reference) error {
169                         switch string(ref.Name()) {
170                         case "refs/notes/commits":
171                                 notesRef = ref
172                         case cfg.CommentsNotesRef:
173                                 commentsRef = ref
174                         case cfg.TopicsNotesRef:
175                                 topicsRef = ref
176                         }
177                         return nil
178                 })
179                 if notesRef != nil {
180                         if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
181                                 notesTree, _ = commentsCommit.Tree()
182                         }
183                 }
184                 if commentsRef != nil {
185                         if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
186                                 commentsTree, _ = commentsCommit.Tree()
187                         }
188                 }
189                 if topicsRef != nil {
190                         if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
191                                 topicsTree, _ = topicsCommit.Tree()
192                         }
193                 }
194         }
195         return &headHash, nil
196 }
197
198 func main() {
199         if len(os.Args) == 3 && os.Args[1] == "-gopher" {
200                 serveGopher()
201         } else {
202                 serveHTTP()
203         }
204 }