]> Sergey Matveev's repositories - sgblog.git/blob - cmd/sgblog/main.go
Images support
[sgblog.git] / cmd / sgblog / main.go
1 /*
2 SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
3 Copyright (C) 2020-2023 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/UCSPI blogging/phlogging/gemlogging engine
19 package main
20
21 import (
22         "crypto/sha1"
23         "embed"
24         "encoding/json"
25         "flag"
26         "fmt"
27         "io/fs"
28         "log"
29         "os"
30         "regexp"
31         "strings"
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         "github.com/vorlif/spreak"
38         "golang.org/x/text/language"
39 )
40
41 const (
42         PageEntries = 50
43 )
44
45 var (
46         sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
47         repo         *git.Repository
48         notesTree    *object.Tree
49         commentsRef  *plumbing.Reference
50         commentsTree *object.Tree
51         topicsRef    *plumbing.Reference
52         topicsTree   *object.Tree
53
54         localizer *spreak.Localizer
55
56         //go:embed locale/*
57         locales embed.FS
58 )
59
60 type Cfg struct {
61         GitPath string
62         Branch  string
63         Title   string
64         Lang    string
65
66         URLPrefix string
67
68         AtomBaseURL string
69         AtomId      string
70         AtomAuthor  string
71
72         CSS       string
73         Webmaster string
74         AboutURL  string
75         GitURLs   []string
76
77         CommentsNotesRef string
78         CommentsEmail    string
79
80         TopicsNotesRef  string
81         TopicsCachePath string
82
83         GopherDomain string
84
85         ImgPath   string
86         ImgDomain string
87 }
88
89 func msgSplit(msg string) []string {
90         lines := strings.Split(msg, "\n")
91         lines = lines[:len(lines)-1]
92         if len(lines) < 3 {
93                 lines = []string{lines[0], "", ""}
94         }
95         return lines
96 }
97
98 func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
99         var err error
100         repo, err = git.PlainOpen(cfg.GitPath)
101         if err != nil {
102                 return nil, err
103         }
104         head, err := repo.Reference(plumbing.ReferenceName(cfg.Branch), false)
105         if err != nil {
106                 return nil, err
107         }
108         headHash := head.Hash()
109         if notes, err := repo.Notes(); err == nil {
110                 var notesRef *plumbing.Reference
111                 notes.ForEach(func(ref *plumbing.Reference) error {
112                         switch string(ref.Name()) {
113                         case "refs/notes/commits":
114                                 notesRef = ref
115                         case cfg.CommentsNotesRef:
116                                 commentsRef = ref
117                         case cfg.TopicsNotesRef:
118                                 topicsRef = ref
119                         }
120                         return nil
121                 })
122                 if notesRef != nil {
123                         if commentsCommit, err := repo.CommitObject(notesRef.Hash()); err == nil {
124                                 notesTree, _ = commentsCommit.Tree()
125                         }
126                 }
127                 if commentsRef != nil {
128                         if commentsCommit, err := repo.CommitObject(commentsRef.Hash()); err == nil {
129                                 commentsTree, _ = commentsCommit.Tree()
130                         }
131                 }
132                 if topicsRef != nil {
133                         if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
134                                 topicsTree, _ = topicsCommit.Tree()
135                         }
136                 }
137         }
138         return &headHash, nil
139 }
140
141 func readCfg(cfgPath string) (*Cfg, error) {
142         cfgRaw, err := os.ReadFile(cfgPath)
143         if err != nil {
144                 return nil, err
145         }
146         var cfgGeneral map[string]interface{}
147         if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
148                 return nil, err
149         }
150         cfgRaw, err = json.Marshal(cfgGeneral)
151         if err != nil {
152                 return nil, err
153         }
154         var cfg *Cfg
155         if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
156                 return nil, err
157         }
158         return cfg, nil
159 }
160
161 func initLocalizer(lang string) {
162         fsys, _ := fs.Sub(locales, "locale")
163         bundle, err := spreak.NewBundle(
164                 spreak.WithSourceLanguage(language.English),
165                 spreak.WithDomainFs(spreak.NoDomain, fsys),
166                 spreak.WithLanguage(language.Russian),
167         )
168         if err != nil {
169                 log.Fatalln(err)
170         }
171         if lang == "" {
172                 lang = language.English.String()
173         }
174         localizer = spreak.NewLocalizer(bundle, language.MustParse(lang))
175 }
176
177 func main() {
178         gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file")
179         geminiCfgPath := flag.String("gemini", "", "Path to gemini-related configuration file")
180         flag.Usage = func() {
181                 fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog:
182         sgblog -- run CGI HTTP backend
183         sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend
184         sgblog -gemini /path/to/cfg.hjson -- run UCSPI+tlss Gemini backend
185 `)
186         }
187         flag.Parse()
188         log.SetFlags(log.Lshortfile)
189         if *gopherCfgPath != "" {
190                 serveGopher(*gopherCfgPath)
191         } else if *geminiCfgPath != "" {
192                 serveGemini(*geminiCfgPath)
193         } else {
194                 serveHTTP()
195         }
196 }