Skip to content

Commit 2befcf8

Browse files
committed
reintroduce memexd web interface with core changes that happened to core memex. minimal work, it just shows a graph in js, nothing fancy or worth it.
1 parent 7dfdd91 commit 2befcf8

File tree

6 files changed

+454
-414
lines changed

6 files changed

+454
-414
lines changed

cmd/memexd/main.go

Lines changed: 147 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -7,50 +7,62 @@ import (
77
"html/template"
88
"log"
99
"net/http"
10+
"os"
1011
"path/filepath"
1112

12-
"github.com/systemshift/memex/internal/memex/storage"
13+
"github.com/go-chi/chi/v5"
14+
"github.com/go-chi/chi/v5/middleware"
15+
"github.com/systemshift/memex/internal/memex/core"
16+
"github.com/systemshift/memex/internal/memex/repository"
1317
)
1418

19+
// Server handles HTTP requests and manages the repository
1520
type Server struct {
16-
memex *storage.MXStore
21+
repo core.Repository
1722
template *template.Template
1823
}
1924

20-
type GraphData struct {
21-
Nodes []NodeData `json:"nodes"`
22-
Edges []EdgeData `json:"edges"`
25+
// GraphResponse represents the graph visualization data
26+
type GraphResponse struct {
27+
Nodes []NodeResponse `json:"nodes"`
28+
Links []LinkResponse `json:"links"`
2329
}
2430

25-
type NodeData struct {
26-
ID string `json:"id"`
27-
Type string `json:"type"`
28-
Meta map[string]any `json:"meta"`
29-
Created string `json:"created"`
31+
// NodeResponse represents a node in the graph visualization
32+
type NodeResponse struct {
33+
ID string `json:"id"`
34+
Type string `json:"type"`
35+
Meta map[string]interface{} `json:"meta"`
36+
Created string `json:"created"`
37+
Modified string `json:"modified"`
3038
}
3139

32-
type EdgeData struct {
33-
Source string `json:"source"`
34-
Target string `json:"target"`
35-
Type string `json:"type"`
36-
Meta map[string]any `json:"meta"`
40+
// LinkResponse represents a link in the graph visualization
41+
type LinkResponse struct {
42+
Source string `json:"source"`
43+
Target string `json:"target"`
44+
Type string `json:"type"`
45+
Meta map[string]interface{} `json:"meta"`
46+
Created string `json:"created"`
47+
Modified string `json:"modified"`
3748
}
3849

3950
func main() {
51+
// Parse command line flags
4052
addr := flag.String("addr", ":3000", "HTTP service address")
41-
repo := flag.String("repo", "", "Repository path")
53+
repoPath := flag.String("repo", "", "Repository path")
4254
flag.Parse()
4355

44-
if *repo == "" {
56+
if *repoPath == "" {
4557
log.Fatal("Repository path required")
4658
}
4759

48-
// Open repository
49-
store, err := storage.OpenMX(*repo)
60+
// Initialize repository
61+
repo, err := repository.Open(*repoPath)
5062
if err != nil {
5163
log.Fatalf("Error opening repository: %v", err)
5264
}
53-
defer store.Close()
65+
defer repo.Close()
5466

5567
// Parse templates
5668
tmpl, err := template.ParseGlob("cmd/memexd/templates/*.html")
@@ -60,121 +72,173 @@ func main() {
6072

6173
// Create server
6274
server := &Server{
63-
memex: store,
75+
repo: repo,
6476
template: tmpl,
6577
}
6678

67-
// Setup routes
68-
http.HandleFunc("/", server.handleIndex)
69-
http.HandleFunc("/api/graph", server.handleGraph)
70-
http.HandleFunc("/api/content/", server.handleContent)
71-
http.HandleFunc("/node/", server.handleNode)
79+
// Create router
80+
r := chi.NewRouter()
7281

73-
// Serve static files
74-
fs := http.FileServer(http.Dir("cmd/memexd/static"))
75-
http.Handle("/static/", http.StripPrefix("/static/", fs))
82+
// Middleware
83+
r.Use(middleware.Logger)
84+
r.Use(middleware.Recoverer)
85+
r.Use(middleware.Compress(5))
86+
87+
// Routes
88+
r.Get("/", server.handleIndex)
89+
r.Route("/api", func(r chi.Router) {
90+
r.Get("/graph", server.handleGraph)
91+
r.Get("/nodes/{id}", server.handleGetNode)
92+
r.Get("/nodes/{id}/content", server.handleGetContent)
93+
})
94+
95+
// Static files
96+
workDir, _ := os.Getwd()
97+
filesDir := http.Dir(filepath.Join(workDir, "cmd/memexd/static"))
98+
r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(filesDir)))
7699

77100
// Start server
78101
log.Printf("Starting server on %s", *addr)
79-
log.Fatal(http.ListenAndServe(*addr, nil))
102+
log.Fatal(http.ListenAndServe(*addr, r))
80103
}
81104

105+
// handleIndex serves the main page
82106
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
83107
if err := s.template.ExecuteTemplate(w, "index.html", nil); err != nil {
84108
http.Error(w, err.Error(), http.StatusInternalServerError)
85109
return
86110
}
87111
}
88112

113+
// handleGraph returns the graph data for visualization
89114
func (s *Server) handleGraph(w http.ResponseWriter, r *http.Request) {
90115
// Get all nodes
91-
var graph GraphData
92-
for _, entry := range s.memex.Nodes() {
93-
node, err := s.memex.GetNode(fmt.Sprintf("%x", entry.ID[:]))
116+
nodeIDs, err := s.repo.ListNodes()
117+
if err != nil {
118+
http.Error(w, fmt.Sprintf("Error listing nodes: %v", err), http.StatusInternalServerError)
119+
return
120+
}
121+
122+
response := GraphResponse{
123+
Nodes: make([]NodeResponse, 0, len(nodeIDs)),
124+
Links: make([]LinkResponse, 0),
125+
}
126+
127+
// Process each node
128+
for _, id := range nodeIDs {
129+
node, err := s.repo.GetNode(id)
94130
if err != nil {
131+
log.Printf("Error getting node %s: %v", id, err)
95132
continue
96133
}
97134

98-
// Add node
99-
graph.Nodes = append(graph.Nodes, NodeData{
100-
ID: node.ID,
101-
Type: node.Type,
102-
Meta: node.Meta,
103-
Created: node.Created.Format("2006-01-02 15:04:05"),
135+
// Add node to response
136+
response.Nodes = append(response.Nodes, NodeResponse{
137+
ID: node.ID,
138+
Type: node.Type,
139+
Meta: node.Meta,
140+
Created: node.Created.Format("2006-01-02 15:04:05"),
141+
Modified: node.Modified.Format("2006-01-02 15:04:05"),
104142
})
105143

106-
// Get links
107-
links, err := s.memex.GetLinks(node.ID)
144+
// Get and process links
145+
links, err := s.repo.GetLinks(node.ID)
108146
if err != nil {
147+
log.Printf("Error getting links for node %s: %v", id, err)
109148
continue
110149
}
111150

112-
// Add edges
113151
for _, link := range links {
114-
graph.Edges = append(graph.Edges, EdgeData{
115-
Source: node.ID,
116-
Target: link.Target,
117-
Type: link.Type,
118-
Meta: link.Meta,
152+
response.Links = append(response.Links, LinkResponse{
153+
Source: link.Source,
154+
Target: link.Target,
155+
Type: link.Type,
156+
Meta: link.Meta,
157+
Created: link.Created.Format("2006-01-02 15:04:05"),
158+
Modified: link.Modified.Format("2006-01-02 15:04:05"),
119159
})
120160
}
121161
}
122162

123-
// Write response
163+
// Send response
124164
w.Header().Set("Content-Type", "application/json")
125-
if err := json.NewEncoder(w).Encode(graph); err != nil {
126-
http.Error(w, err.Error(), http.StatusInternalServerError)
165+
if err := json.NewEncoder(w).Encode(response); err != nil {
166+
http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
127167
return
128168
}
129169
}
130170

131-
func (s *Server) handleContent(w http.ResponseWriter, r *http.Request) {
132-
// Get content hash from URL
133-
hash := filepath.Base(r.URL.Path)
171+
// handleGetNode returns information about a specific node
172+
func (s *Server) handleGetNode(w http.ResponseWriter, r *http.Request) {
173+
id := chi.URLParam(r, "id")
174+
if id == "" {
175+
http.Error(w, "Node ID required", http.StatusBadRequest)
176+
return
177+
}
134178

135-
// Reconstruct content from chunks
136-
content, err := s.memex.ReconstructContent(hash)
179+
node, err := s.repo.GetNode(id)
137180
if err != nil {
138-
http.Error(w, "Content not found", http.StatusNotFound)
181+
http.Error(w, fmt.Sprintf("Error getting node: %v", err), http.StatusNotFound)
139182
return
140183
}
141184

142-
// Write response
143-
w.Header().Set("Content-Type", "text/plain")
144-
w.Write(content)
185+
// Create response with formatted timestamps
186+
response := struct {
187+
ID string `json:"id"`
188+
Type string `json:"type"`
189+
Content string `json:"content"`
190+
Meta map[string]interface{} `json:"meta"`
191+
Created string `json:"created"`
192+
Modified string `json:"modified"`
193+
}{
194+
ID: node.ID,
195+
Type: node.Type,
196+
Content: string(node.Content),
197+
Meta: node.Meta,
198+
Created: node.Created.Format("2006-01-02 15:04:05"),
199+
Modified: node.Modified.Format("2006-01-02 15:04:05"),
200+
}
201+
202+
w.Header().Set("Content-Type", "application/json")
203+
if err := json.NewEncoder(w).Encode(response); err != nil {
204+
http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError)
205+
return
206+
}
145207
}
146208

147-
func (s *Server) handleNode(w http.ResponseWriter, r *http.Request) {
148-
// Get node ID from URL
149-
id := filepath.Base(r.URL.Path)
209+
// handleGetContent returns the content of a node
210+
func (s *Server) handleGetContent(w http.ResponseWriter, r *http.Request) {
211+
id := chi.URLParam(r, "id")
212+
if id == "" {
213+
http.Error(w, "Node ID required", http.StatusBadRequest)
214+
return
215+
}
150216

151-
// Get node
152-
node, err := s.memex.GetNode(id)
217+
// Get node first to check type and get metadata
218+
node, err := s.repo.GetNode(id)
153219
if err != nil {
154-
http.Error(w, "Node not found", http.StatusNotFound)
220+
http.Error(w, fmt.Sprintf("Error getting node: %v", err), http.StatusNotFound)
155221
return
156222
}
157223

158-
// If file, serve content
159-
if node.Type == "file" {
160-
if contentHash, ok := node.Meta["content"].(string); ok {
161-
content, err := s.memex.ReconstructContent(contentHash)
162-
if err != nil {
163-
http.Error(w, "Content not found", http.StatusNotFound)
164-
return
165-
}
224+
// Get content
225+
content, err := s.repo.GetContent(id)
226+
if err != nil {
227+
http.Error(w, fmt.Sprintf("Error getting content: %v", err), http.StatusNotFound)
228+
return
229+
}
166230

167-
// Set filename for download
168-
if filename, ok := node.Meta["filename"].(string); ok {
169-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
170-
}
231+
// Set content type if available in metadata
232+
if contentType, ok := node.Meta["content-type"].(string); ok {
233+
w.Header().Set("Content-Type", contentType)
234+
} else {
235+
w.Header().Set("Content-Type", "application/octet-stream")
236+
}
171237

172-
w.Write(content)
173-
return
174-
}
238+
// Set filename for download if available
239+
if filename, ok := node.Meta["filename"].(string); ok {
240+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
175241
}
176242

177-
// Otherwise show node info
178-
w.Header().Set("Content-Type", "application/json")
179-
json.NewEncoder(w).Encode(node)
243+
w.Write(content)
180244
}

0 commit comments

Comments
 (0)