arns-lt-gemini/main.go
Arnas Udovic c7c23b9e75
Some checks failed
continuous-integration/drone/push Build is failing
redirect /r/:id action
2024-08-12 17:32:27 +03:00

549 lines
14 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"mime"
"path"
"regexp"
"strings"
"time"
"g.arns.lt/zordsdavini/zordfsdb"
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
"github.com/flosch/pongo2/v6"
"github.com/gorilla/feeds"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var (
hostname string
port string
fileSrvHost string
fileSrvPort string
certificatePath string
defaultLang string
supportedLang string
key = "WgiPGpt5wM6SVEWo5iqI"
)
func init() {
flag.StringVar(&hostname, "hostname", "", "capsule hostname")
flag.StringVar(&port, "port", "1965", "capsule port")
flag.StringVar(&certificatePath, "certificatePath", "", "capsule certificate path")
flag.StringVar(&fileSrvHost, "fileSrvHost", "", "file push server host")
flag.StringVar(&fileSrvPort, "fileSrvPort", "", "file push server port")
flag.StringVar(&defaultLang, "defaultLang", "", "default language, ex. sgs")
flag.StringVar(
&supportedLang,
"supportedLang",
"",
"other supported languages separated by |, ex., prg|lt",
)
flag.StringVar(&key, "key", "", "secret key to communicate admin")
flag.Parse()
}
func main() {
certificates := &certificate.Store{}
certificates.Register(hostname)
if err := certificates.Load(certificatePath); err != nil {
panic(err)
}
mux := &gemini.Mux{}
mux.HandleFunc("/favicon.txt", processFavicon)
mux.HandleFunc("/rebuild", processRebuild)
// security
// feed
mux.HandleFunc("/", process)
server := &gemini.Server{
Addr: ":" + port,
Handler: mux,
ReadTimeout: 30 * time.Second,
WriteTimeout: 1 * time.Minute,
GetCertificate: certificates.Get,
}
ctx := context.Background()
if err := server.ListenAndServe(ctx); err != nil {
log.Fatal(err)
}
}
func processRebuild(ctx context.Context, w gemini.ResponseWriter, r *gemini.Request) {
db, err := zordfsdb.InitDB("./db")
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
db.Now("last_rebuild")
log.Println("REBUILD")
q, err := gemini.QueryUnescape(r.URL.RawQuery)
if err != nil || q != key {
w.WriteHeader(gemini.StatusServerUnavailable, "Don't wake up the dragons :-*")
}
conn, err := grpc.Dial(
fileSrvHost+":"+fileSrvPort,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
defer conn.Close()
client := NewTreeManagerClient(conn)
ts := TreeSecret{Key: key}
rsp, err := client.RebuildTree(context.Background(), &ts)
if rsp.Success {
_, err = w.Write([]byte(":-)"))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
}
} else {
w.WriteHeader(gemini.StatusNotFound, "Out of space")
}
log.Println("REBUILD: ", rsp.Success)
}
func processFavicon(_ context.Context, w gemini.ResponseWriter, _ *gemini.Request) {
w.SetMediaType("text/plain")
_, err := w.Write([]byte("\U0001F31B"))
if err != nil {
w.WriteHeader(gemini.StatusNotFound, "Out of space")
}
}
func process(_ context.Context, w gemini.ResponseWriter, r *gemini.Request) {
log.Println("-> " + r.URL.Path)
db, err := zordfsdb.InitDB("./db")
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
db.Inc("page_counter._" + defaultLang)
conn, err := grpc.Dial(
fileSrvHost+":"+fileSrvPort,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
defer conn.Close()
client := NewTreeManagerClient(conn)
switch {
case "/" == r.URL.Path:
renderIndex(w, client)
case regexp.MustCompile(`^/atom.xml$`).MatchString(r.URL.Path):
renderFeed(w, r, client)
case regexp.MustCompile(`^/a/?$`).MatchString(r.URL.Path):
renderAbout(w)
case regexp.MustCompile(`^/s/?$`).MatchString(r.URL.Path):
renderSearch(w, r, client)
case regexp.MustCompile(`^/f/?$`).MatchString(r.URL.Path):
renderAllFiles(w, client)
case regexp.MustCompile(`^/f/([\p{L}\d_+.]+/)+[\d\w]+/index.gmi$`).MatchString(r.URL.Path):
urlParts := strings.Split(r.URL.Path, "/")
w.WriteHeader(
gemini.StatusPermanentRedirect,
strings.Join(urlParts[:(len(urlParts)-2)], "/"),
)
case regexp.MustCompile(`^/f/([\p{L}\d_+.]+/)+[\d\w]+/[\p{L}\d_+.]+.gmi$`).MatchString(r.URL.Path):
renderFile(w, r, client)
case regexp.MustCompile(`^/f/[\p{L}\d_+.]+(/[\p{L}\d_+.]+)*/?$`).MatchString(r.URL.Path):
renderCategory(w, r, client)
case regexp.MustCompile(`^/t/[\p{L}\d_+.]+/?$`).MatchString(r.URL.Path):
renderTag(w, r, client)
case regexp.MustCompile(`^/__a/`).MatchString(r.URL.Path):
downloadAttachment(w, r, client)
case regexp.MustCompile(`^/r/.+`).MatchString(r.URL.Path):
redirectAction(w, r, client)
default:
w.WriteHeader(gemini.StatusNotFound, "Out of space")
}
}
func downloadAttachment(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
filePath := urlParts[len(urlParts)-1]
ar := AttachmentRequest{Path: filePath}
response, err := client.DownloadAttachment(context.Background(), &ar)
if err != nil {
log.Fatalf("client.DownloadAttachment failed: %v", err)
w.WriteHeader(gemini.StatusNotFound, "We lost it!")
return
}
w.SetMediaType(mime.TypeByExtension(path.Ext(filePath)))
for {
chunkResponse, err := response.Recv()
if err == io.EOF {
log.Println("received all chunks")
break
}
if err != nil {
log.Println("err receiving chunk:", err)
break
}
_, err = w.Write(chunkResponse.Chunk)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
}
func getLangFilters() []*TreeRequest_Filter {
filters := []*TreeRequest_Filter{{Key: "lang", Value: defaultLang}}
for _, l := range strings.Split(supportedLang, "|") {
filters = append(filters, &TreeRequest_Filter{Key: "lang", Value: l})
}
return filters
}
func renderIndex(w gemini.ResponseWriter, client TreeManagerClient) {
db, err := zordfsdb.InitDB("./db")
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
path := ""
tr := TreeRequest{Path: &path, Filter: getLangFilters()}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
lastRebuild, found := db.Get("last_rebuild")
if !found {
}
pageCounter, found := db.Get("page_counter._" + defaultLang)
if !found {
pageCounter = "1"
db.Save("page_counter._"+defaultLang, "1")
}
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile(fmt.Sprintf("templates/%s/index.gmi", defaultLang)))
page, err := tpl.Execute(
pongo2.Context{
"tree": tree,
"lang": defaultLang,
"lastFiles": GetLastFiles(tree.Files),
"lastRebuild": lastRebuild,
"pageCounter": pageCounter,
},
)
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
func renderFeed(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
path := ""
tr := TreeRequest{Path: &path, Filter: getLangFilters()}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
w.SetMediaType("application/atom+xml")
now := time.Now()
feed := &feeds.Feed{
Title: "\U0001F31B Arna alkierios [" + defaultLang + "]",
Link: &feeds.Link{Href: "gemini://" + Dict().hostname},
Description: "personal gemini capsule by Arns Udovič [" + Dict().languageEn + " version]",
Author: &feeds.Author{Name: "Arns Udovič", Email: "zordsdavini@arns.lt"},
Created: now,
}
for _, file := range GetLastFiles(tree.Files) {
created, err := time.Parse("2006-01-02", file.Created)
if err != nil {
created = time.Now()
}
feed.Add(
&feeds.Item{
Title: file.Description,
Link: &feeds.Link{Href: fmt.Sprintf("gemini://%s/f%s/%s/%s",
r.URL.Host,
file.CategoryPath(),
file.Id,
file.GmiName(),
)},
Description: fmt.Sprintf("%s %s [%s]",
file.Description,
file.CategoryPath(),
strings.Join(file.Tags, ","),
),
Author: &feeds.Author{Name: "Arns Udovič", Email: "zordsdavini@arns.lt"},
Created: created,
},
)
}
atom, err := feed.ToAtom()
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
_, err = w.Write([]byte(atom))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
func renderAbout(w gemini.ResponseWriter) {
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile(fmt.Sprintf("templates/%s/about.gmi", defaultLang)))
page, err := tpl.Execute(pongo2.Context{})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
}
}
func renderSearch(
w gemini.ResponseWriter,
r *gemini.Request,
client TreeManagerClient,
) {
q, err := gemini.QueryUnescape(r.URL.RawQuery)
if err != nil || q == "" {
w.WriteHeader(gemini.StatusInput, Dict().searchStr)
return
}
descFilter := TreeRequest_Filter{Key: "description", Value: q}
createdFilter := TreeRequest_Filter{Key: "created", Value: q}
tagFilter := TreeRequest_Filter{Key: "tag", Value: q}
filters := []*TreeRequest_Filter{&descFilter, &createdFilter, &tagFilter}
path := ""
tr := TreeRequest{Path: &path, Filter: filters}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/search.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": defaultLang, "tree": tree, "q": q})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
func renderCategory(
w gemini.ResponseWriter,
r *gemini.Request,
client TreeManagerClient,
) {
urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
path := "/" + strings.Join(urlParts[1:], "/")
tr := TreeRequest{Path: &path, Filter: []*TreeRequest_Filter{}}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
indexFile, err := tree.GetIndexFile()
var file *FileContent
if err == nil {
fr := FileRequest{Id: indexFile.Id}
file, err = client.GetFile(context.Background(), &fr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/category.gmi"))
page, err := tpl.Execute(
pongo2.Context{"lang": defaultLang, "tree": tree, "path": path, "indexFile": file},
)
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
func renderFile(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
urlParts := strings.Split(r.URL.Path, "/")
id := urlParts[len(urlParts)-2]
fr := FileRequest{Id: id}
file, err := client.GetFile(context.Background(), &fr)
if err != nil {
log.Fatalf("client.GetSummery failed: %v", err)
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
db, err := zordfsdb.InitDB("./db")
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
log.Fatal(err)
return
}
db.Inc("page_counter." + id)
pageCounter, found := db.Get("page_counter." + id)
if !found {
pageCounter = "1"
db.Save("page_counter."+id, "1")
}
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/page.gmi"))
page, err := tpl.Execute(
pongo2.Context{"lang": defaultLang, "file": file, "pageCounter": pageCounter},
)
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
func renderAllFiles(w gemini.ResponseWriter, client TreeManagerClient) {
path := ""
tr := TreeRequest{Path: &path, Filter: getLangFilters()}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/all_texts.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": defaultLang, "tree": tree})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
func renderTag(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
urlParts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
tag := urlParts[len(urlParts)-1]
tagFilter := TreeRequest_Filter{Key: "tag", Value: tag}
filters := []*TreeRequest_Filter{&tagFilter}
path := ""
tr := TreeRequest{Path: &path, Filter: filters}
tree, err := client.GetSummery(context.Background(), &tr)
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
w.SetMediaType("text/gemini")
tpl := pongo2.Must(pongo2.FromFile("templates/tag.gmi"))
page, err := tpl.Execute(pongo2.Context{"lang": defaultLang, "tree": tree, "tag": tag})
if err != nil {
log.Fatalf("template failed: %v", err)
return
}
_, err = w.Write([]byte(page))
if err != nil {
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
}
func redirectAction(w gemini.ResponseWriter, r *gemini.Request, client TreeManagerClient) {
urlParts := strings.Split(r.URL.Path, "/")
id := urlParts[1]
fr := FileRequest{Id: id}
file, err := client.GetFile(context.Background(), &fr)
if err != nil {
log.Fatalf("client.GetFile failed: %v", err)
w.WriteHeader(gemini.StatusTemporaryFailure, "Internal server error")
return
}
w.WriteHeader(
gemini.StatusPermanentRedirect,
fmt.Sprintf(
"/f/%s/%s/%s",
strings.Join(file.File.Category, "/"),
file.File.Id,
file.File.GmiName(),
),
)
}