]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/main.go
Simplify config read and arguments parsing
[sgblog.git] / cmd / sgblog / main.go
1 /*
2 SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
3 Copyright (C) 2020-2021 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         "encoding/json"
25         "flag"
26         "fmt"
27         "io/ioutil"
28         "regexp"
29         "sort"
30         "strings"
31         "text/scanner"
32
33         "github.com/go-git/go-git/v5"
34         "github.com/go-git/go-git/v5/plumbing"
35         "github.com/go-git/go-git/v5/plumbing/object"
36         "github.com/hjson/hjson-go"
37         "go.cypherpunks.ru/recfile"
38 )
39
40 const (
41         PageEntries = 50
42 )
43
44 var (
45         sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
46         repo         *git.Repository
47         notesTree    *object.Tree
48         commentsRef  *plumbing.Reference
49         commentsTree *object.Tree
50         topicsRef    *plumbing.Reference
51         topicsTree   *object.Tree
52 )
53
54 type Cfg struct {
55         GitPath string
56         Branch  string
57         Title   string
58
59         URLPrefix string
60
61         AtomBaseURL string
62         AtomId      string
63         AtomAuthor  string
64
65         CSS       string
66         Webmaster string
67         AboutURL  string
68         GitURLs   []string
69
70         CommentsNotesRef string
71         CommentsEmail    string
72
73         TopicsNotesRef  string
74         TopicsCachePath string
75
76         GopherDomain string
77 }
78
79 func msgSplit(msg string) []string {
80         lines := strings.Split(msg, "\n")
81         lines = lines[:len(lines)-1]
82         if len(lines) < 3 {
83                 lines = []string{lines[0], "", ""}
84         }
85         return lines
86 }
87
88 func getNote(tree *object.Tree, what plumbing.Hash) []byte {
89         if tree == nil {
90                 return nil
91         }
92         var entry *object.TreeEntry
93         var err error
94         paths := make([]string, 3)
95         paths[0] = what.String()
96         paths[1] = paths[0][:2] + "/" + paths[0][2:]
97         paths[2] = paths[1][:4+1] + "/" + paths[1][4+1:]
98         for _, p := range paths {
99                 entry, err = tree.FindEntry(p)
100                 if err == nil {
101                         break
102                 }
103         }
104         if entry == nil {
105                 return nil
106         }
107         blob, err := repo.BlobObject(entry.Hash)
108         if err != nil {
109                 return nil
110         }
111         r, err := blob.Reader()
112         if err != nil {
113                 return nil
114         }
115         data, err := ioutil.ReadAll(r)
116         if err != nil {
117                 return nil
118         }
119         return bytes.TrimSuffix(data, []byte{'\n'})
120 }
121
122 func parseComments(data []byte) []string {
123         comments := []string{}
124         r := recfile.NewReader(bytes.NewReader(data))
125         for {
126                 fields, err := r.Next()
127                 if err != nil {
128                         break
129                 }
130                 if len(fields) != 3 ||
131                         fields[0].Name != "From" ||
132                         fields[1].Name != "Date" ||
133                         fields[2].Name != "Body" {
134                         continue
135                 }
136                 comments = append(comments, fmt.Sprintf(
137                         "%s: %s\n%s: %s\n%s",
138                         fields[0].Name, fields[0].Value,
139                         fields[1].Name, fields[1].Value,
140                         fields[2].Value,
141                 ))
142         }
143         return comments
144 }
145
146 func parseTopics(data []byte) []string {
147         var s scanner.Scanner
148         s.Init(bytes.NewBuffer(data))
149         topics := []string{}
150         for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
151                 topics = append(topics, s.TokenText())
152         }
153         sort.Strings(topics)
154         return topics
155 }
156
157 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
158         var err error
159         repo, err = git.PlainOpen(cfg.GitPath)
160         if err != nil {
161                 return nil, err
162         }
163         head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
164         if err != nil {
165                 return nil, err
166         }
167         headHash := head.Hash()
168         if notes, err := repo.Notes(); err == nil {
169                 var notesRef *plumbing.Reference
170                 notes.ForEach(func(ref *plumbing.Reference) error {
171                         switch string(ref.Name()) {
172                         case "refs/notes/commits":
173                                 notesRef = ref
174                         case cfg.CommentsNotesRef:
175                                 commentsRef = ref
176                         case cfg.TopicsNotesRef:
177                                 topicsRef = ref
178                         }
179                         return nil
180                 })
181                 if notesRef != nil {
182                         if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
183                                 notesTree, _ = commentsCommit.Tree()
184                         }
185                 }
186                 if commentsRef != nil {
187                         if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
188                                 commentsTree, _ = commentsCommit.Tree()
189                         }
190                 }
191                 if topicsRef != nil {
192                         if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
193                                 topicsTree, _ = topicsCommit.Tree()
194                         }
195                 }
196         }
197         return &headHash, nil
198 }
199
200 func readCfg(cfgPath string) (*Cfg, error) {
201         cfgRaw, err := ioutil.ReadFile(cfgPath)
202         if err != nil {
203                 return nil, err
204         }
205         var cfgGeneral map[string]interface{}
206         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
207                 return nil, err
208         }
209         cfgRaw, err = json.Marshal(cfgGeneral)
210         if err != nil {
211                 return nil, err
212         }
213         var cfg *Cfg
214         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
215                 return nil, err
216         }
217         return cfg, nil
218 }
219
220 func main() {
221         gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file")
222         flag.Usage = func() {
223                 fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog:
224         sgblog -- run CGI HTTP backend
225         sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend
226 `)
227         }
228         flag.Parse()
229         if *gopherCfgPath != "" {
230                 serveGopher(*gopherCfgPath)
231         } else {
232                 serveHTTP()
233         }
234 }