Files
alexey.bagno 47e4066de5
Deploy / deploy (push) Successful in 8s
pretty search
2026-08-31 21:15:26 +05:00

79 lines
2.5 KiB
Go

package main
import (
"connectrpc.com/connect"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
homelabv1 "pwa-homelab-mon/gen/homelab/v1"
"strings"
)
// --- docs ---
func docHash(b []byte) string {
h := sha256.Sum256(b)
return hex.EncodeToString(h[:])
}
// docPath resolves a doc name safely: plain file name only, no separators, always inside docsDir.
func (s *server) docPath(name string) (string, error) {
if filepath.Base(name) != name || filepath.Ext(name) != ".md" {
return "", fmt.Errorf("bad doc name %q", name)
}
p := filepath.Join(s.docsDir, name)
if filepath.Dir(p) != s.docsDir {
return "", fmt.Errorf("bad doc name %q", name)
}
return p, nil
}
func (s *server) ListDocs(context.Context, *connect.Request[homelabv1.ListDocsRequest]) (*connect.Response[homelabv1.ListDocsResponse], error) {
resp := &homelabv1.ListDocsResponse{}
entries, err := os.ReadDir(s.docsDir)
if err != nil {
return connect.NewResponse(resp), nil // no docs dir = no docs
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") {
continue
}
b, err := os.ReadFile(filepath.Join(s.docsDir, e.Name()))
if err != nil {
continue
}
resp.Docs = append(resp.Docs, &homelabv1.DocMeta{Name: e.Name(), Hash: docHash(b)})
}
return connect.NewResponse(resp), nil
}
func (s *server) GetDoc(_ context.Context, req *connect.Request[homelabv1.GetDocRequest]) (*connect.Response[homelabv1.GetDocResponse], error) {
p, err := s.docPath(req.Msg.GetName())
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
b, err := os.ReadFile(p)
if err != nil {
return nil, connect.NewError(connect.CodeNotFound, err)
}
return connect.NewResponse(&homelabv1.GetDocResponse{Content: string(b)}), nil
}
func (s *server) SaveDoc(_ context.Context, req *connect.Request[homelabv1.SaveDocRequest]) (*connect.Response[homelabv1.SaveDocResponse], error) {
p, err := s.docPath(req.Msg.GetName())
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}
content := []byte(req.Msg.GetContent())
// ponytail: client authority — last write wins, no base-hash check; add optimistic locking if concurrent edits appear
if err := os.WriteFile(p, content, 0o644); err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
log.Printf("doc saved: %s (%d bytes)", filepath.Base(p), len(content))
return connect.NewResponse(&homelabv1.SaveDocResponse{Hash: docHash(content)}), nil
}