@@ -0,0 +1,78 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user