/*
-SGBlog -- Git-backed CGI/inetd blogging/phlogging engine
-Copyright (C) 2020 Sergey Matveev <stargrave@stargrave.org>
+SGBlog -- Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
+Copyright (C) 2020-2022 Sergey Matveev <stargrave@stargrave.org>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-// Git-backed CGI/inetd blogging/phlogging engine
+// Git-backed CGI/UCSPI blogging/phlogging/gemlogging engine
package main
import (
- "bytes"
- "io/ioutil"
+ "crypto/sha1"
+ "embed"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/fs"
+ "log"
"os"
"regexp"
"strings"
- "go.cypherpunks.ru/netstring/v2"
- "gopkg.in/src-d/go-git.v4"
- "gopkg.in/src-d/go-git.v4/plumbing"
- "gopkg.in/src-d/go-git.v4/plumbing/object"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/hjson/hjson-go"
+ "github.com/vorlif/spreak"
+ "golang.org/x/text/language"
)
const (
)
var (
- sha1DigestRe = regexp.MustCompilePOSIX("([0-9a-f]{40,40})")
+ sha1DigestRe = regexp.MustCompilePOSIX(fmt.Sprintf("([0-9a-f]{%d,%d})", sha1.Size*2, sha1.Size*2))
repo *git.Repository
notesTree *object.Tree
+ commentsRef *plumbing.Reference
commentsTree *object.Tree
+ topicsRef *plumbing.Reference
+ topicsTree *object.Tree
+
+ localizer *spreak.Localizer
+
+ //go:embed locale/*
+ locales embed.FS
)
type Cfg struct {
GitPath string
Branch string
Title string
+ Lang string
URLPrefix string
CommentsNotesRef string
CommentsEmail string
+ TopicsNotesRef string
+ TopicsCachePath string
+
GopherDomain string
}
return lines
}
-func getNote(tree *object.Tree, what plumbing.Hash) []byte {
- if tree == nil {
- return nil
- }
- var entry *object.TreeEntry
- var err error
- paths := make([]string, 3)
- paths[0] = what.String()
- paths[1] = paths[0][:2] + "/" + paths[0][2:]
- paths[2] = paths[1][:4+1] + "/" + paths[1][4+1:]
- for _, p := range paths {
- entry, err = tree.FindEntry(p)
- if err == nil {
- break
- }
- }
- if entry == nil {
- return nil
- }
- blob, err := repo.BlobObject(entry.Hash)
- if err != nil {
- return nil
- }
- r, err := blob.Reader()
- if err != nil {
- return nil
- }
- data, err := ioutil.ReadAll(r)
- if err != nil {
- return nil
- }
- return bytes.TrimSuffix(data, []byte{'\n'})
-}
-
-func parseComments(data []byte) []string {
- comments := []string{}
- nsr := netstring.NewReader(bytes.NewReader(data))
- for {
- if _, err := nsr.Next(); err != nil {
- break
- }
- if comment, err := ioutil.ReadAll(nsr); err == nil {
- comments = append(comments, string(comment))
- }
- }
- return comments
-}
-
func initRepo(cfg *Cfg) (*plumbing.Hash, error) {
var err error
repo, err = git.PlainOpen(cfg.GitPath)
headHash := head.Hash()
if notes, err := repo.Notes(); err == nil {
var notesRef *plumbing.Reference
- var commentsRef *plumbing.Reference
notes.ForEach(func(ref *plumbing.Reference) error {
switch string(ref.Name()) {
case "refs/notes/commits":
notesRef = ref
case cfg.CommentsNotesRef:
commentsRef = ref
+ case cfg.TopicsNotesRef:
+ topicsRef = ref
}
return nil
})
commentsTree, _ = commentsCommit.Tree()
}
}
+ if topicsRef != nil {
+ if topicsCommit, err := repo.CommitObject(topicsRef.Hash()); err == nil {
+ topicsTree, _ = topicsCommit.Tree()
+ }
+ }
}
return &headHash, nil
}
+func readCfg(cfgPath string) (*Cfg, error) {
+ cfgRaw, err := os.ReadFile(cfgPath)
+ if err != nil {
+ return nil, err
+ }
+ var cfgGeneral map[string]interface{}
+ if err = hjson.Unmarshal(cfgRaw, &cfgGeneral); err != nil {
+ return nil, err
+ }
+ cfgRaw, err = json.Marshal(cfgGeneral)
+ if err != nil {
+ return nil, err
+ }
+ var cfg *Cfg
+ if err = json.Unmarshal(cfgRaw, &cfg); err != nil {
+ return nil, err
+ }
+ return cfg, nil
+}
+
+func initLocalizer(lang string) {
+ fsys, _ := fs.Sub(locales, "locale")
+ bundle, err := spreak.NewBundle(
+ spreak.WithSourceLanguage(language.English),
+ spreak.WithDomainFs(spreak.NoDomain, fsys),
+ spreak.WithLanguage(language.Russian),
+ )
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if lang == "" {
+ lang = language.English.String()
+ }
+ localizer = spreak.NewLocalizer(bundle, language.MustParse(lang))
+}
+
func main() {
- if len(os.Args) == 3 && os.Args[1] == "-gopher" {
- serveGopher()
+ gopherCfgPath := flag.String("gopher", "", "Path to gopher-related configuration file")
+ geminiCfgPath := flag.String("gemini", "", "Path to gemini-related configuration file")
+ flag.Usage = func() {
+ fmt.Fprintf(flag.CommandLine.Output(), `Usage of sgblog:
+ sgblog -- run CGI HTTP backend
+ sgblog -gopher /path/to/cfg.hjson -- run UCSPI/inetd Gopher backend
+ sgblog -gemini /path/to/cfg.hjson -- run UCSPI+tlss Gemini backend
+`)
+ }
+ flag.Parse()
+ log.SetFlags(log.Lshortfile)
+ if *gopherCfgPath != "" {
+ serveGopher(*gopherCfgPath)
+ } else if *geminiCfgPath != "" {
+ serveGemini(*geminiCfgPath)
} else {
serveHTTP()
}