diff --git a/src/comments.go b/src/comments.go new file mode 100644 index 0000000..ab1c1dc --- /dev/null +++ b/src/comments.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "time" + + "github.com/prologic/bitcask" +) + +type Comment struct { + Author string + Content string + Date time.Time +} + +func getComments(db *bitcask.Bitcask, key string) ([]Comment, error) { + data, err := db.Get([]byte(key)) + if err != nil { + return nil, err + } + + var comments []Comment + err = json.Unmarshal(data, &comments) + if err != nil { + return nil, err + } + + return comments, nil +} + +func saveComment(db *bitcask.Bitcask, key string, comment Comment) error { + comments, err := getComments(db, key) + if err != nil && err != bitcask.ErrKeyNotFound { + return err + } + + comment.Date = time.Now() + comments = append(comments, comment) + + data, err := json.Marshal(comments) + if err != nil { + return err + } + + return db.Put([]byte(key), data) +} + +func submitCommentHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + err := r.ParseForm() + if err != nil { + http.Error(w, "Failed to parse form", http.StatusBadRequest) + return + } + + pagePath := r.FormValue("path") + if pagePath == "" { + http.Error(w, "Path is required", http.StatusBadRequest) + return + } + + comment := Comment{ + Author: r.FormValue("author"), + Content: r.FormValue("content"), + } + + err = saveComment(commentsDB, pagePath, comment) + if err != nil { + http.Error(w, "Failed to save comment", http.StatusInternalServerError) + return + } + + log.Printf("Saved comment: %+v", comment) + http.Redirect(w, r, pagePath, http.StatusSeeOther) +} diff --git a/src/git.go b/src/git.go new file mode 100644 index 0000000..1fa0326 --- /dev/null +++ b/src/git.go @@ -0,0 +1,73 @@ +package main + +import ( + //"fmt" + + "os" + + git "github.com/go-git/go-git/v5" +) + +func cloneRepository(repoURL, localPath string) error { + _, err := git.PlainClone(localPath, false, &git.CloneOptions{ + URL: repoURL, + Progress: os.Stdout, + }) + return err +} + +func pullRepository(localPath string) error { + repo, err := git.PlainOpen(localPath) + if err != nil { + return err + } + + worktree, err := repo.Worktree() + if err != nil { + return err + } + + err = worktree.Pull(&git.PullOptions{RemoteName: "origin"}) + if err != nil && err != git.NoErrAlreadyUpToDate { + return err + } + return nil +} + +func readFileFromRepo(localPath string, filePath string) ([]byte, error) { + // Open the local repository + repo, err := git.PlainOpen(localPath) + if err != nil { + return nil, err + } + + // Get the head reference + ref, err := repo.Head() + if err != nil { + return nil, err + } + + // Get the commit object + commit, err := repo.CommitObject(ref.Hash()) + if err != nil { + return nil, err + } + + // Get the file contents from the commit tree + tree, err := commit.Tree() + if err != nil { + return nil, err + } + + file, err := tree.File(filePath) + if err != nil { + return nil, err + } + + content, err := file.Contents() + if err != nil { + return nil, err + } + + return []byte(content), nil +} diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..91a71f1 --- /dev/null +++ b/src/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "log" + "net/http" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/prologic/bitcask" +) + +const repoURL = "https://git.tcp.direct/S4D/tcp-wiki.git" +const localPath = "../data" + +var commentsDB *bitcask.Bitcask + +func main() { + err := cloneRepository(repoURL, localPath) + if err != nil && err != git.ErrRepositoryAlreadyExists { + log.Fatalf("Failed to clone repository: %v", err) + } + + commentsDB, err = bitcask.Open("../comments.db") + if err != nil { + log.Fatalf("Failed to open comments database: %v", err) + } + defer commentsDB.Close() + + http.HandleFunc("/", handler) + http.HandleFunc("/submit_comment", submitCommentHandler) + log.Fatal(http.ListenAndServe(":8080", nil)) +} + +func handler(w http.ResponseWriter, r *http.Request) { + //for debugging + log.Printf("LOCAL PATH: %q", localPath) + + //... + + if r.URL.Path == "../assets/favicon.ico" { + return + } + + err := pullRepository(localPath) + if err != nil { + log.Printf("Failed to pull repository: %v", err) + } + + filePath := strings.TrimPrefix(r.URL.Path, "/") + if filePath == "" { + filePath = "README.md" + } + log.Printf("Rendering file %q from path %q", filePath, r.URL.Path) + + err = renderPage(w, r, localPath, filePath, commentsDB) + if err != nil { + log.Printf("Comment loading? %q", commentsDB.Path()) + + http.Error(w, "File not found", http.StatusNotFound) + } +} diff --git a/src/render.go b/src/render.go new file mode 100644 index 0000000..cf6977e --- /dev/null +++ b/src/render.go @@ -0,0 +1,102 @@ +package main + +import ( + "bytes" + "fmt" + "html/template" + "io/ioutil" + "log" + "net/http" + "path/filepath" + + "github.com/prologic/bitcask" + "github.com/yuin/goldmark" + highlighting "github.com/yuin/goldmark-highlighting" + "github.com/yuin/goldmark/extension" +) + +type Page struct { + Content template.HTML + Comments []Comment + Path string +} + +func renderPage(w http.ResponseWriter, r *http.Request, localPath, filePath string, commentsDB *bitcask.Bitcask) error { + content, err := readFileFromRepo(localPath, filePath) + if err != nil { + return err + } + + //log.Printf("Read file content: %s", content) + + ext := filepath.Ext(filePath) + switch ext { + case ".md": + renderMarkdown(w, r, content, commentsDB) + case ".html", ".css": + renderStatic(w, content, ext) + default: + return fmt.Errorf("unsupported file format") + } + return nil +} + +func renderMarkdown(w http.ResponseWriter, r *http.Request, content []byte, commentsDB *bitcask.Bitcask) { + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, // GitHub Flavored Markdown + highlighting.NewHighlighting( + highlighting.WithStyle("monokai"), + ), + ), + ) + + var mdBuf bytes.Buffer + err := md.Convert(content, &mdBuf) + if err != nil { + http.Error(w, "Error converting Markdown", http.StatusInternalServerError) + return + } + + layout, err := ioutil.ReadFile(filepath.Join(localPath, "assets/_layout.html")) + if err != nil { + http.Error(w, "Layout not found", http.StatusInternalServerError) + return + } + + comments, err := getComments(commentsDB, r.URL.Path) + if err != nil && err != bitcask.ErrKeyNotFound { + http.Error(w, "Error fetching comments", http.StatusInternalServerError) + return + } + + page := &Page{Content: template.HTML(mdBuf.String()), Comments: comments, Path: r.URL.Path} + t, err := template.New("layout").Parse(string(layout)) + if err != nil { + http.Error(w, "Error parsing layout", http.StatusInternalServerError) + return + } + + var buf bytes.Buffer + err = t.Execute(&buf, page) + if err != nil { + log.Printf("Error executing template: %v", err) // Add this line + http.Error(w, "Error rendering layout", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write(buf.Bytes()) +} + +func renderStatic(w http.ResponseWriter, content []byte, ext string) { + contentType := "" + switch ext { + case ".html": + contentType = "text/html; charset=utf-8" + case ".css": + contentType = "text/css; charset=utf-8" + } + w.Header().Set("Content-Type", contentType) + w.Write(content) +}