From 8f2515b37a985b24bc70278409dcb80da1e8e052 Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Wed, 24 Jun 2020 19:01:51 +0500 Subject: [PATCH] Reimplement action=raw and action=getBinary --- cfg/config.go | 7 ++ fs/fs.go | 60 +++++++++++++++ fs/hypha.go | 137 ++++++++++++++++++++++++++++++++++ fs/revision.go | 17 +++++ genealogy.go => genealogy.gog | 0 handlers.go | 71 +++++++++++------- hypha.go => hypha.gog | 39 ---------- main.go | 88 +++++++++------------- render.go => render.gog | 0 revision.go => revision.gog | 13 ---- w/m/Fruit/Pineapple/1.html | 1 + w/m/Fruit/Pineapple/meta.json | 19 +++++ walk.go => walk.gog | 0 13 files changed, 317 insertions(+), 135 deletions(-) create mode 100644 fs/fs.go create mode 100644 fs/hypha.go create mode 100644 fs/revision.go rename genealogy.go => genealogy.gog (100%) rename hypha.go => hypha.gog (62%) rename render.go => render.gog (100%) rename revision.go => revision.gog (90%) create mode 100644 w/m/Fruit/Pineapple/1.html create mode 100644 w/m/Fruit/Pineapple/meta.json rename walk.go => walk.gog (100%) diff --git a/cfg/config.go b/cfg/config.go index 89cdd6c..4597353 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -8,6 +8,13 @@ import ( "path/filepath" ) +const ( + HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*` + HyphaUrl = `/{hypha:` + HyphaPattern + `}` + RevisionPattern = `[\d]+` + RevQuery = `{rev:` + RevisionPattern + `}` +) + var ( WikiDir string TemplatesDir string diff --git a/fs/fs.go b/fs/fs.go new file mode 100644 index 0000000..4323340 --- /dev/null +++ b/fs/fs.go @@ -0,0 +1,60 @@ +package fs + +import ( + "github.com/bouncepaw/mycorrhiza/cfg" + "io/ioutil" + "log" + "path/filepath" + "regexp" +) + +type Storage struct { + // hypha name => path + paths map[string]string + root string +} + +// InitStorage initiates filesystem-based hypha storage. It has to be called after configuration was inited. +func InitStorage() *Storage { + s := &Storage{ + paths: make(map[string]string), + root: cfg.WikiDir, + } + s.indexHyphae(s.root) + log.Println(s.paths) + log.Printf("Indexed %v hyphae\n", len(s.paths)) + return s +} + +// hyphaName gets name of a hypha by stripping path to the hypha in `fullPath` +func hyphaName(fullPath string) string { + // {cfg.WikiDir}/{the name} + return fullPath[len(cfg.WikiDir)+1:] +} + +// indexHyphae searches for all hyphae that seem valid in `path` and saves their absolute paths to `s.paths`. This function is recursive. +func (s *Storage) indexHyphae(path string) { + nodes, err := ioutil.ReadDir(path) + if err != nil { + log.Fatal("Error when checking", path, ":", err, "; skipping") + return + } + + for _, node := range nodes { + matchesHypha, err := regexp.MatchString(cfg.HyphaPattern, node.Name()) + if err != nil { + log.Fatal("Error when matching", node.Name(), err, "\n") + return + } + switch name := filepath.Join(path, node.Name()); { + case matchesHypha && node.IsDir(): + s.indexHyphae(name) + case node.Name() == "meta.json" && !node.IsDir(): + log.Printf("%v seems to be a hypha, adding it to the list\n", path) + s.paths[hyphaName(path)] = path + } + } +} + +func (h *Hypha) Close() { +} diff --git a/fs/hypha.go b/fs/hypha.go new file mode 100644 index 0000000..559457a --- /dev/null +++ b/fs/hypha.go @@ -0,0 +1,137 @@ +package fs + +import ( + "encoding/json" + "errors" + "io/ioutil" + "log" + "net/http" + "path/filepath" + "strconv" + + "github.com/bouncepaw/mycorrhiza/cfg" +) + +type Hypha struct { + Exists bool `json:"-"` + FullName string `json:"-"` + ViewCount int `json:"views"` + Deleted bool `json:"deleted"` + Revisions map[string]*Revision `json:"revisions"` + actual *Revision `json:"-"` +} + +func (s *Storage) Open(name string) (*Hypha, error) { + h := &Hypha{ + Exists: true, + FullName: name, + } + path, ok := s.paths[name] + // This hypha does not exist yet + if !ok { + log.Println("Hypha", name, "does not exist") + h.Exists = false + h.Revisions = make(map[string]*Revision) + } else { + metaJsonText, err := ioutil.ReadFile(filepath.Join(path, "meta.json")) + if err != nil { + log.Fatal(err) + return nil, err + } + + err = json.Unmarshal(metaJsonText, &h) + if err != nil { + log.Fatal(err) + return nil, err + } + // fill in rooted paths to content files and full names + for idStr, rev := range h.Revisions { + rev.FullName = filepath.Join(h.parentName(), rev.ShortName) + rev.Id, _ = strconv.Atoi(idStr) + if rev.BinaryName != "" { + rev.BinaryPath = filepath.Join(path, rev.BinaryName) + } + rev.TextPath = filepath.Join(path, rev.TextName) + } + + err = h.OnRevision("0") + return h, err + } + log.Println(h) + return h, nil +} + +func (h *Hypha) parentName() string { + return filepath.Dir(h.FullName) +} + +func (h *Hypha) metaJsonPath() string { + return filepath.Join(cfg.WikiDir, h.FullName, "meta.json") +} + +// OnRevision tries to change to a revision specified by `id`. +func (h *Hypha) OnRevision(id string) error { + if len(h.Revisions) == 0 { + return errors.New("This hypha has no revisions") + } + if id == "0" { + id = h.NewestId() + } + // Revision must be there, so no error checking + if rev, _ := h.Revisions[id]; true { + h.actual = rev + } + return nil +} + +// NewestId finds the largest id among all revisions. +func (h *Hypha) NewestId() string { + var largest int + for k, _ := range h.Revisions { + id, _ := strconv.Atoi(k) + if id > largest { + largest = id + } + } + return strconv.Itoa(largest) +} + +func (h *Hypha) PlainLog(s string) { + log.Println(h.FullName, h.actual.Id, s) +} + +func (h *Hypha) mimeTypeForActionRaw() string { + // If text mime type is text/html, it is not good as it will be rendered. + if h.actual.TextMime == "text/html" { + return "text/plain" + } + return h.actual.TextMime +} + +// ActionRaw is used with `?action=raw`. +// It writes text content of the revision without any parsing or rendering. +func (h *Hypha) ActionRaw(w http.ResponseWriter) { + fileContents, err := ioutil.ReadFile(h.actual.TextPath) + if err != nil { + log.Fatal(err) + return + } + w.Header().Set("Content-Type", h.mimeTypeForActionRaw()) + w.WriteHeader(http.StatusOK) + w.Write(fileContents) + h.PlainLog("Serving raw text") +} + +// ActionGetBinary is used with `?action=getBinary`. +// It writes contents of binary content file. +func (h *Hypha) ActionGetBinary(w http.ResponseWriter) { + fileContents, err := ioutil.ReadFile(h.actual.BinaryPath) + if err != nil { + log.Fatal(err) + return + } + w.Header().Set("Content-Type", h.actual.BinaryMime) + w.WriteHeader(http.StatusOK) + w.Write(fileContents) + h.PlainLog("Serving raw text") +} diff --git a/fs/revision.go b/fs/revision.go new file mode 100644 index 0000000..67f0346 --- /dev/null +++ b/fs/revision.go @@ -0,0 +1,17 @@ +package fs + +type Revision struct { + Id int `json:"-"` + FullName string `json:"-"` + Tags []string `json:"tags"` + ShortName string `json:"name"` + Comment string `json:"comment"` + Author string `json:"author"` + Time int `json:"time"` + TextMime string `json:"text_mime"` + BinaryMime string `json:"binary_mime"` + TextPath string `json:"-"` + BinaryPath string `json:"-"` + TextName string `json:"text_name"` + BinaryName string `json:"binary_name"` +} diff --git a/genealogy.go b/genealogy.gog similarity index 100% rename from genealogy.go rename to genealogy.gog diff --git a/handlers.go b/handlers.go index fab7b0f..4479ea1 100644 --- a/handlers.go +++ b/handlers.go @@ -1,65 +1,75 @@ package main import ( - "io/ioutil" "log" + // "io/ioutil" + // "log" "net/http" - "path/filepath" - "strconv" - "strings" - "time" + // "path/filepath" + // "strconv" + // "strings" + // "time" - "github.com/bouncepaw/mycorrhiza/cfg" + "github.com/bouncepaw/mycorrhiza/fs" "github.com/gorilla/mux" ) // There are handlers below. See main() for their usage. // Boilerplate code present in many handlers. Good to have it. -func HandlerBase(w http.ResponseWriter, rq *http.Request) (Revision, bool) { +func HandlerBase(w http.ResponseWriter, rq *http.Request) (*fs.Hypha, bool) { vars := mux.Vars(rq) - revno := RevInMap(vars) - return GetRevision(vars["hypha"], revno) -} - -func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionGetBinary(w) + h, err := hs.Open(vars["hypha"]) + if err != nil { + log.Println(err) + return nil, false } + err = h.OnRevision(RevInMap(vars)) + if err != nil { + log.Println(err) + } + + log.Println(*h) + return h, true } func HandlerRaw(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionRaw(w) + log.Println("?action=raw") + if h, ok := HandlerBase(w, rq); ok { + h.ActionRaw(w) } } +func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) { + log.Println("?action=getBinary") + if h, ok := HandlerBase(w, rq); ok { + h.ActionGetBinary(w) + } +} + +/* func HandlerZen(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionZen(w) + if h, ok := HandlerBase(w, rq); ok { + h.ActionZen(w) } } func HandlerView(w http.ResponseWriter, rq *http.Request) { - if rev, ok := HandlerBase(w, rq); ok { - rev.ActionView(w, HyphaPage) - } else { // Hypha does not exist - log.Println("Hypha does not exist, showing 404") - w.WriteHeader(http.StatusNotFound) - w.Write([]byte(Hypha404(mux.Vars(rq)["hypha"]))) + if h, ok := HandlerBase(w, rq); ok { + h.ActionView(w, HyphaPage) } } -func HandlerHistory(w http.ResponseWriter, rq *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - log.Println("Attempt to access an unimplemented thing") -} - func HandlerEdit(w http.ResponseWriter, rq *http.Request) { vars := mux.Vars(rq) ActionEdit(vars["hypha"], w) } +func HandlerHistory(w http.ResponseWriter, rq *http.Request) { + w.WriteHeader(http.StatusNotImplemented) + log.Println("Attempt to access an unimplemented thing") +} + func HandlerRewind(w http.ResponseWriter, rq *http.Request) { w.WriteHeader(http.StatusNotImplemented) log.Println("Attempt to access an unimplemented thing") @@ -74,7 +84,9 @@ func HandlerRename(w http.ResponseWriter, rq *http.Request) { w.WriteHeader(http.StatusNotImplemented) log.Println("Attempt to access an unimplemented thing") } +*/ +/* // makeTagsSlice turns strings like `"foo,, bar,kek"` to slice of strings that represent tag names. Whitespace around commas is insignificant. // Expected output for string above: []string{"foo", "bar", "kek"} func makeTagsSlice(responseTagsString string) (ret []string) { @@ -211,3 +223,4 @@ func HandlerUpdate(w http.ResponseWriter, rq *http.Request) { d := map[string]string{"Name": h.FullName} w.Write([]byte(renderFromMap(d, "updateOk.html"))) } +*/ diff --git a/hypha.go b/hypha.gog similarity index 62% rename from hypha.go rename to hypha.gog index 540d31b..bbdc6ee 100644 --- a/hypha.go +++ b/hypha.gog @@ -14,18 +14,6 @@ import ( "github.com/bouncepaw/mycorrhiza/cfg" ) -// `Hypha` represents a hypha. It is the thing MycorrhizaWiki generally serves. -// Each hypha has 1 or more revisions. -type Hypha struct { - FullName string `json:"-"` - Path string `json:"-"` - ViewCount int `json:"views"` - Deleted bool `json:"deleted"` - Revisions map[string]*Revision `json:"revisions"` - ChildrenNames []string `json:"-"` - parentName string -} - // AsHtml returns HTML representation of the hypha. // No layout or navigation are present here. Just the hypha. func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) { @@ -38,33 +26,6 @@ func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) { return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, id) } -// GetNewestRevision returns the most recent Revision. -func (h *Hypha) GetNewestRevision() Revision { - return *h.Revisions[h.NewestRevision()] -} - -// NewestRevision returns the most recent revision's id as a string. -func (h *Hypha) NewestRevision() string { - return strconv.Itoa(h.NewestRevisionInt()) -} - -// NewestRevision returns the most recent revision's id as an integer. -func (h *Hypha) NewestRevisionInt() (ret int) { - for k, _ := range h.Revisions { - id, _ := strconv.Atoi(k) - if id > ret { - ret = id - } - } - return ret -} - -// MetaJsonPath returns rooted path to the hypha's `meta.json` file. -// It is not promised that the file exists. -func (h *Hypha) MetaJsonPath() string { - return filepath.Join(h.Path, "meta.json") -} - // CreateDir creates directory where the hypha must reside. // It is meant to be used with new hyphae. func (h *Hypha) CreateDir() error { diff --git a/main.go b/main.go index acbc044..b4301de 100644 --- a/main.go +++ b/main.go @@ -9,25 +9,10 @@ import ( "time" "github.com/bouncepaw/mycorrhiza/cfg" + "github.com/bouncepaw/mycorrhiza/fs" "github.com/gorilla/mux" ) -// GetRevision finds revision with id `id` of `hyphaName` in `hyphae`. -// If `id` is `"0"`, it means the last revision. -// If no such revision is found, last return value is false. -func GetRevision(hyphaName string, id string) (Revision, bool) { - log.Println("Getting hypha", hyphaName, id) - if hypha, ok := hyphae[hyphaName]; ok { - if id == "0" { - id = hypha.NewestRevision() - } - if rev, ok := hypha.Revisions[id]; ok { - return *rev, true - } - } - return Revision{}, false -} - // RevInMap finds value of `rev` (the one from URL queries like) in the passed map that is usually got from `mux.Vars(*http.Request)`. // If there is no `rev`, return "0". func RevInMap(m map[string]string) string { @@ -37,8 +22,7 @@ func RevInMap(m map[string]string) string { return "0" } -// `hyphae` is a map with all hyphae. Many functions use it. -var hyphae map[string]*Hypha +var hs *fs.Storage func main() { if len(os.Args) == 1 { @@ -52,51 +36,51 @@ func main() { log.Println("Welcome to MycorrhizaWiki α") cfg.InitConfig(wikiDir) log.Println("Indexing hyphae...") - hyphae = recurFindHyphae(wikiDir) - log.Println("Indexed", len(hyphae), "hyphae. Ready to accept requests.") + hs = fs.InitStorage() // Start server code. See handlers.go for handlers' implementations. r := mux.NewRouter() - r.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl). + r.Queries("action", "getBinary", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). HandlerFunc(HandlerGetBinary) - r.Queries("action", "getBinary").Path(hyphaUrl). + r.Queries("action", "getBinary").Path(cfg.HyphaUrl). HandlerFunc(HandlerGetBinary) - r.Queries("action", "raw", "rev", revQuery).Path(hyphaUrl). + r.Queries("action", "raw", "rev", cfg.RevQuery).Path(cfg.HyphaUrl). HandlerFunc(HandlerRaw) - r.Queries("action", "raw").Path(hyphaUrl). + r.Queries("action", "raw").Path(cfg.HyphaUrl). HandlerFunc(HandlerRaw) + /* + r.Queries("action", "zen", "rev", revQuery).Path(hyphaUrl). + HandlerFunc(HandlerZen) + r.Queries("action", "zen").Path(hyphaUrl). + HandlerFunc(HandlerZen) - r.Queries("action", "zen", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerZen) - r.Queries("action", "zen").Path(hyphaUrl). - HandlerFunc(HandlerZen) + r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl). + HandlerFunc(HandlerView) + r.Queries("action", "view").Path(hyphaUrl). + HandlerFunc(HandlerView) - r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerView) - r.Queries("action", "view").Path(hyphaUrl). - HandlerFunc(HandlerView) + r.Queries("action", "history").Path(hyphaUrl). + HandlerFunc(HandlerHistory) - r.Queries("action", "history").Path(hyphaUrl). - HandlerFunc(HandlerHistory) + r.Queries("action", "edit").Path(hyphaUrl). + HandlerFunc(HandlerEdit) - r.Queries("action", "edit").Path(hyphaUrl). - HandlerFunc(HandlerEdit) + r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl). + HandlerFunc(HandlerRewind) - r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl). - HandlerFunc(HandlerRewind) + r.Queries("action", "delete").Path(hyphaUrl). + HandlerFunc(HandlerDelete) - r.Queries("action", "delete").Path(hyphaUrl). - HandlerFunc(HandlerDelete) + r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). + HandlerFunc(HandlerRename) - r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). - HandlerFunc(HandlerRename) + r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). + HandlerFunc(HandlerUpdate) + */ - r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). - HandlerFunc(HandlerUpdate) - - r.HandleFunc(hyphaUrl, HandlerView) + // r.HandleFunc(hyphaUrl, HandlerView) // Debug page that renders all hyphae. // TODO: make it redirect to home page. @@ -104,14 +88,10 @@ func main() { r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) - for _, h := range hyphae { - log.Println("Rendering latest revision of hypha", h.FullName) - html, err := h.AsHtml("0", w) - if err != nil { - fmt.Fprintln(w, err) - } - fmt.Fprintln(w, html) - } + fmt.Fprintln(w, ` +

Check out Fruit maybe.

+

TODO: make this page usable

+ `) }) http.Handle("/", r) diff --git a/render.go b/render.gog similarity index 100% rename from render.go rename to render.gog diff --git a/revision.go b/revision.gog similarity index 90% rename from revision.go rename to revision.gog index 35b653b..3117b2a 100644 --- a/revision.go +++ b/revision.gog @@ -114,19 +114,6 @@ func (rev *Revision) ActionGetBinary(w http.ResponseWriter) { log.Println("Serving binary data of", rev.FullName, rev.Id) } -// ActionRaw is used with `?action=raw`. -// It writes text content of the revision without any parsing or rendering. -func (rev *Revision) ActionRaw(w http.ResponseWriter) { - fileContents, err := ioutil.ReadFile(rev.TextPath) - if err != nil { - return - } - w.Header().Set("Content-Type", rev.TextMime) - w.WriteHeader(http.StatusOK) - w.Write(fileContents) - log.Println("Serving text data of", rev.FullName, rev.Id) -} - // ActionZen is used with `?action=zen`. // It renders the revision without any layout or navigation. func (rev *Revision) ActionZen(w http.ResponseWriter) { diff --git a/w/m/Fruit/Pineapple/1.html b/w/m/Fruit/Pineapple/1.html new file mode 100644 index 0000000..61b3b19 --- /dev/null +++ b/w/m/Fruit/Pineapple/1.html @@ -0,0 +1 @@ +Pineapple is apple from a pine \ No newline at end of file diff --git a/w/m/Fruit/Pineapple/meta.json b/w/m/Fruit/Pineapple/meta.json new file mode 100644 index 0000000..877bb4a --- /dev/null +++ b/w/m/Fruit/Pineapple/meta.json @@ -0,0 +1,19 @@ +{ + "views": 0, + "deleted": false, + "revisions": { + "1": { + "tags": [ + "" + ], + "name": "Pineapple", + "comment": "Update Fruit/Pineapple", + "author": "", + "time": 1592997100, + "text_mime": "text/html", + "binary_mime": "", + "text_name": "1.html", + "binary_name": "" + } + } +} \ No newline at end of file diff --git a/walk.go b/walk.gog similarity index 100% rename from walk.go rename to walk.gog