]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/main.go
2e4a483e27d1683c0e064481578c61bf57b4558f
[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/netstring/v2"
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         nsr := netstring.NewReader(bytes.NewReader(data))
123         for {
124                 if _, err := nsr.Next(); err != nil {
125                         break
126                 }
127                 if comment, err := ioutil.ReadAll(nsr); err == nil {
128                         comments = append(comments, string(comment))
129                 }
130         }
131         return comments
132 }
133
134 func parseTopics(data []byte) []string {
135         var s scanner.Scanner
136         s.Init(bytes.NewBuffer(data))
137         topics := []string{}
138         for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
139                 topics = append(topics, s.TokenText())
140         }
141         sort.Strings(topics)
142         return topics
143 }
144
145 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
146         var err error
147         repo, err = git.PlainOpen(cfg.GitPath)
148         if err != nil {
149                 return nil, err
150         }
151         head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
152         if err != nil {
153                 return nil, err
154         }
155         headHash := head.Hash()
156         if notes, err := repo.Notes(); err == nil {
157                 var notesRef *plumbing.Reference
158                 notes.ForEach(func(ref *plumbing.Reference) error {
159                         switch string(ref.Name()) {
160                         case "refs/notes/commits":
161                                 notesRef = ref
162                         case cfg.CommentsNotesRef:
163                                 commentsRef = ref
164                         case cfg.TopicsNotesRef:
165                                 topicsRef = ref
166                         }
167                         return nil
168                 })
169                 if notesRef != nil {
170                         if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
171                                 notesTree, _ = commentsCommit.Tree()
172                         }
173                 }
174                 if commentsRef != nil {
175                         if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
176                                 commentsTree, _ = commentsCommit.Tree()
177                         }
178                 }
179                 if topicsRef != nil {
180                         if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
181                                 topicsTree, _ = topicsCommit.Tree()
182                         }
183                 }
184         }
185         return &headHash, nil
186 }
187
188 func main() {
189         if len(os.Args) == 3 && os.Args[1] == "-gopher" {
190                 serveGopher()
191         } else {
192                 serveHTTP()
193         }
194 }