From 2a3f3460341196cf713ee22edf5b6ee084c70810 Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Tue, 16 Jun 2020 23:35:52 +0500 Subject: [PATCH] Merge code from two branches, make it show something --- genealogy.go | 4 +- hypha.go | 84 +++++++----------- main.go | 29 ++++-- revision.go | 126 ++++++++++++++++---------- walk.go | 244 ++++++++++++++++++++++----------------------------- 5 files changed, 243 insertions(+), 244 deletions(-) diff --git a/genealogy.go b/genealogy.go index bc8354e..657cd1d 100644 --- a/genealogy.go +++ b/genealogy.go @@ -9,8 +9,8 @@ type Genealogy struct { func setRelations(hyphae map[string]*Hypha) { for name, h := range hyphae { - if _, ok := hyphae[h.ParentName]; ok && h.ParentName != "." { - hyphae[h.ParentName].ChildrenNames = append(hyphae[h.ParentName].ChildrenNames, name) + if _, ok := hyphae[h.ParentName()]; ok && h.ParentName() != "." { + hyphae[h.ParentName()].ChildrenNames = append(hyphae[h.ParentName()].ChildrenNames, name) } } } diff --git a/hypha.go b/hypha.go index 192aaf4..4655092 100644 --- a/hypha.go +++ b/hypha.go @@ -1,70 +1,52 @@ package main import ( - "errors" "fmt" - "log" - "net/http" "strconv" ) type Hypha struct { - // Hypha is physically located here. Most fields below are stored in /mm.ini (mm for metametadata). Its revisions are physically located in // subfolders. ∈ [0;∞] with 0 being latest revision, 1 the first. - Path string - // Every hypha was created at some point - CreationTime int `json:"creationTime"` - // Hypha has name but it can be changed - Name string `json:"name"` - // Hypha can be deleted. If it is deleted, it is not indexed by most of the software but still can be recovered at some point. - Deleted bool `json:"deleted"` - // Fields below are not part of m.ini and are created when traversing the file tree. - // Hypha can be a child of any other hypha except its children. The parent hypha is stored in /.. - ParentName string - // Hypha can have any number of children which are stored as subfolders in . + FullName string + Path string + ViewCount int `json:"views"` + Deleted bool `json:"deleted"` + Revisions map[string]*Revision `json:"revisions"` ChildrenNames []string - Revisions []Revision + parentName string } -func (h Hypha) String() string { - var revbuf string - for _, r := range h.Revisions { - revbuf += r.String() + "\n" +func (h *Hypha) AddChild(childName string) { + h.ChildrenNames = append(h.ChildrenNames, childName) +} + +// Used with action=zen|view +func (h *Hypha) AsHtml(hyphae map[string]*Hypha, rev string) (string, error) { + if "0" == rev { + rev = h.NewestRevision() } - return fmt.Sprintf("Hypha %v {\n\t"+ - "path %v\n\t"+ - "created at %v\n\t"+ - "child of %v\n\t"+ - "parent of %v\n\t"+ - "Having these revisions:\n%v"+ - "}\n", h.Name, h.Path, h.CreationTime, h.ParentName, h.ChildrenNames, - revbuf) + r, ok := h.Revisions[rev] + if !ok { + return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, rev) + } + html, err := r.AsHtml(hyphae) + return html, err } -func GetRevision(hyphae map[string]*Hypha, hyphaName string, rev string, w http.ResponseWriter) (Revision, bool) { - for name, _ := range hyphae { - if name == hyphaName { - for _, r := range hyphae[name].Revisions { - id, err := strconv.Atoi(rev) - if err != nil { - log.Println("No such revision", rev, "at hypha", hyphaName) - w.WriteHeader(http.StatusNotFound) - return Revision{}, false - } - if r.Id == id { - return r, true - } - } +func (h *Hypha) Name() string { + return h.FullName +} + +func (h *Hypha) NewestRevision() string { + var largest int + for k, _ := range h.Revisions { + rev, _ := strconv.Atoi(k) + if rev > largest { + largest = rev } } - return Revision{}, false + return strconv.Itoa(largest) } -// `rev` is the id of revision to render. If it = 0, the last one is rendered. If the revision is not found, an error is returned. -func (h Hypha) Render(hyphae map[string]*Hypha, rev int) (ret string, err error) { - for _, r := range h.Revisions { - if r.Id == rev { - return r.Render(hyphae) - } - } - return "", errors.New("Revision was not found") +func (h *Hypha) ParentName() string { + return h.parentName } diff --git a/main.go b/main.go index 84ca3b6..b550a5d 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,19 @@ import ( "time" ) +func GetRevision(hyphae map[string]*Hypha, hyphaName string, rev string, w http.ResponseWriter) (Revision, bool) { + for name, _ := range hyphae { + if name == hyphaName { + for id, r := range hyphae[name].Revisions { + if rev == id { + return *r, true + } + } + } + } + return Revision{}, false +} + func RevInMap(m map[string]string) string { if val, ok := m["rev"]; ok { return val @@ -33,7 +46,7 @@ func HandlerGetBinary(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) return } - w.Header().Set("Content-Type", rev.MimeType) + w.Header().Set("Content-Type", rev.BinaryMime) w.WriteHeader(http.StatusOK) w.Write(fileContents) log.Println("Showing image of", rev.FullName, rev.Id) @@ -65,7 +78,7 @@ func HandlerZen(w http.ResponseWriter, r *http.Request) { if !ok { return } - html, err := rev.Render(hyphae) + html, err := rev.AsHtml(hyphae) if err != nil { log.Println("Failed to render", rev.FullName) w.WriteHeader(http.StatusInternalServerError) @@ -83,7 +96,7 @@ func HandlerView(w http.ResponseWriter, r *http.Request) { if !ok { return } - html, err := rev.Render(hyphae) + html, err := rev.AsHtml(hyphae) if err != nil { log.Println("Failed to render", rev.FullName) w.WriteHeader(http.StatusInternalServerError) @@ -131,7 +144,7 @@ var hyphae map[string]*Hypha func hyphaeAsMap(hyphae []*Hypha) map[string]*Hypha { mh := make(map[string]*Hypha) for _, h := range hyphae { - mh[h.Name] = h + mh[h.Name()] = h } return mh } @@ -147,8 +160,8 @@ func main() { panic(err) } - hyphae = hyphaeAsMap(recurFindHyphae(rootWikiDir)) - setRelations(hyphae) + hyphae = recurFindHyphae(rootWikiDir) + // setRelations(hyphae) // Start server code r := mux.NewRouter() @@ -197,8 +210,8 @@ func main() { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) for _, v := range hyphae { - log.Println("Rendering latest revision of hypha", v.Name) - html, err := v.Render(hyphae, 0) + log.Println("Rendering latest revision of hypha", v.Name()) + html, err := v.AsHtml(hyphae, "0") if err != nil { fmt.Fprintln(w, err) } diff --git a/revision.go b/revision.go index 6ae734f..6839991 100644 --- a/revision.go +++ b/revision.go @@ -5,55 +5,39 @@ import ( "fmt" "github.com/gomarkdown/markdown" "io/ioutil" + "log" + "net/http" ) type Revision struct { - // Revision is hypha's state at some point in time. Future revisions are not really supported. Most data here is stored in m.ini. - Id int - // Name used at this revision - Name string `json:"name"` - // Name of hypha - FullName string - // Present in every hypha. Stored in t.txt. - TextPath string - // In at least one markup. Supported ones are "myco", "html", "md", "plain" - Markup string `json:"markup"` - // Some hyphæ have binary contents such as images. Their presence change hypha's behavior in a lot of ways (see methods' implementations). If stored, it is stored in b (filename "b") - BinaryPath string - BinaryRequest string - // To tell what is meaning of binary content, mimeType for them is stored. If the hypha has no binary content, this field must be "application/x-hypha" - MimeType string `json:"mimeType"` - // Every revision was created at some point. This field stores the creation time of the latest revision - RevisionTime int `json:"createdAt"` - // Every hypha has any number of tags - Tags []string `json:"tags"` - // Current revision is authored by someone - RevisionAuthor string `json:"author"` - // and has a comment in plain text - RevisionComment string `json:"comment"` + Id int + Tags []string `json:"tags"` + FullName 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 + BinaryPath string } -func (h Revision) String() string { - return fmt.Sprintf(`Revision %v created at %v { - name: %v - textPath: %v - markup: %v - binaryPath: %v - mimeType: %v - tags: %v - revisionAuthor: %v - revisionComment: %v -}`, h.Id, h.RevisionTime, h.Name, h.TextPath, h.Markup, h.BinaryPath, h.MimeType, h.Tags, h.RevisionAuthor, h.RevisionComment) +// During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data. +func (r *Revision) hasBinaryData() bool { + return r.BinaryMime != "" } -// This method is meant to be called only by Hypha#Render. -func (r Revision) Render(hyphae map[string]*Hypha) (ret string, err error) { +func (r *Revision) urlOfBinary() string { + return fmt.Sprintf("/%s?action=getBinary&rev=%d", r.FullName, r.Id) +} + +// TODO: use templates https://github.com/bouncepaw/mycorrhiza/issues/2 +func (r *Revision) AsHtml(hyphae map[string]*Hypha) (ret string, err error) { ret += `
` - // If it is a binary hypha (we support only images for now): - // TODO: support things other than images. - if r.MimeType != "application/x-hypha" { - ret += fmt.Sprintf(``, r.BinaryRequest) + // TODO: support things other than images + if r.hasBinaryData() { + ret += fmt.Sprintf(``, r.urlOfBinary()) } contents, err := ioutil.ReadFile(r.TextPath) @@ -63,17 +47,71 @@ func (r Revision) Render(hyphae map[string]*Hypha) (ret string, err error) { // TODO: support more markups. // TODO: support mycorrhiza extensions like transclusion. - switch r.Markup { - case "plain": + switch r.TextMime { + case "text/plain": ret += fmt.Sprintf(`
%s
`, contents) - case "md": + case "text/markdown": html := markdown.ToHTML(contents, nil, nil) ret += string(html) default: - return "", errors.New("Unsupported markup: " + r.Markup) + return "", errors.New("Unsupported mime-type: " + r.TextMime) } ret += `
` return ret, nil } + +func (r *Revision) ActionGetBinary(w http.ResponseWriter) { + fileContents, err := ioutil.ReadFile(r.urlOfBinary()) + if err != nil { + log.Println("Failed to load binary data of", r.FullName, r.Id) + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", r.BinaryMime) + w.WriteHeader(http.StatusOK) + w.Write(fileContents) + log.Println("Serving binary data of", r.FullName, r.Id) +} + +func (r *Revision) ActionRaw(w http.ResponseWriter) { + fileContents, err := ioutil.ReadFile(r.TextPath) + if err != nil { + log.Println("Failed to load text data of", r.FullName, r.Id) + w.WriteHeader(http.StatusNotFound) + return + } + w.Header().Set("Content-Type", r.TextMime) + w.WriteHeader(http.StatusOK) + w.Write(fileContents) + log.Println("Serving text data of", r.FullName, r.Id) +} + +func (r *Revision) ActionZen(w http.ResponseWriter, hyphae map[string]*Hypha) { + html, err := r.AsHtml(hyphae) + if err != nil { + log.Println("Failed to render", r.FullName) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, html) +} + +func (r *Revision) ActionView(w http.ResponseWriter, hyphae map[string]*Hypha, layoutFun func(map[string]*Hypha, Revision, string) string) { + html, err := r.AsHtml(hyphae) + if err != nil { + log.Println("Failed to render", r.FullName) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, layoutFun(hyphae, *r, html)) + log.Println("Rendering", r.FullName) +} +func (r *Revision) Name() string { + return r.FullName +} diff --git a/walk.go b/walk.go index 6ec37cd..d24257f 100644 --- a/walk.go +++ b/walk.go @@ -2,190 +2,156 @@ package main import ( "encoding/json" - "errors" - "fmt" "io/ioutil" + "log" "path/filepath" "regexp" - "strconv" ) -func scanHyphaDir(fullPath string) (structureMet bool, possibleRevisionPaths []string, possibleHyphaPaths []string, reterr error) { +const ( + hyphaPattern = `[^\s\d:/?&\\][^:?&\\]*` + hyphaUrl = `/{hypha:` + hyphaPattern + `}` + revisionPattern = `[\d]+` + revQuery = `{rev:` + revisionPattern + `}` + revTxtPattern = revisionPattern + `\.txt` + revBinPattern = revisionPattern + `\.bin` + metaJsonPattern = `meta\.json` +) + +var ( + leadingInt = regexp.MustCompile(`^[-+]?\d+`) +) + +func matchNameToEverything(name string) (hyphaM bool, revTxtM bool, revBinM bool, metaJsonM bool) { + simpleMatch := func(s string, p string) bool { + m, _ := regexp.MatchString(p, s) + return m + } + switch { + case simpleMatch(name, revTxtPattern): + revTxtM = true + case simpleMatch(name, revBinPattern): + revBinM = true + case simpleMatch(name, metaJsonPattern): + metaJsonM = true + case simpleMatch(name, hyphaPattern): + hyphaM = true + } + return +} + +func stripLeadingInt(s string) string { + return leadingInt.FindString(s) +} + +func hyphaDirRevsValidate(dto map[string]map[string]string) (res bool) { + for k, _ := range dto { + switch k { + case "0": + delete(dto, "0") + default: + res = true + } + } + return res +} + +func scanHyphaDir(fullPath string) (valid bool, revs map[string]map[string]string, possibleSubhyphae []string, metaJsonPath string, err error) { + revs = make(map[string]map[string]string) nodes, err := ioutil.ReadDir(fullPath) if err != nil { - reterr = err return // implicit return values } - var ( - mmJsonPresent bool - zeroDirPresent bool - ) - for _, node := range nodes { - matchedHypha, _ := regexp.MatchString(hyphaPattern, node.Name()) - matchedRev, _ := regexp.MatchString(revisionPattern, node.Name()) + hyphaM, revTxtM, revBinM, metaJsonM := matchNameToEverything(node.Name()) switch { - case matchedRev && node.IsDir(): - if node.Name() == "0" { - zeroDirPresent = true + case hyphaM && node.IsDir(): + possibleSubhyphae = append(possibleSubhyphae, filepath.Join(fullPath, node.Name())) + case revTxtM && !node.IsDir(): + revId := stripLeadingInt(node.Name()) + if _, ok := revs[revId]; !ok { + revs[revId] = make(map[string]string) } - possibleRevisionPaths = append( - possibleRevisionPaths, - filepath.Join(fullPath, node.Name()), - ) - case (node.Name() == "mm.json") && !node.IsDir(): - mmJsonPresent = true - case matchedHypha && node.IsDir(): - possibleHyphaPaths = append( - possibleHyphaPaths, - filepath.Join(fullPath, node.Name()), - ) + revs[revId]["txt"] = filepath.Join(fullPath, node.Name()) + case revBinM && !node.IsDir(): + revId := stripLeadingInt(node.Name()) + if _, ok := revs[revId]; !ok { + revs[revId] = make(map[string]string) + } + revs[revId]["bin"] = filepath.Join(fullPath, node.Name()) + case metaJsonM && !node.IsDir(): + metaJsonPath = filepath.Join(fullPath, "meta.json") // Other nodes are ignored. It is not promised they will be ignored in future versions } } - if mmJsonPresent && zeroDirPresent { - structureMet = true - } + valid = hyphaDirRevsValidate(revs) return // implicit return values } -func check(e error) { - if e != nil { - panic(e) - } -} - // Hypha name is rootWikiDir/{here} func hyphaName(fullPath string) string { return fullPath[len(rootWikiDir)+1:] } -const ( - hyphaPattern = `[^\s\d:/?&\\][^:?&\\]*` - revisionPattern = `[\d]+` - hyphaUrl = "/{hypha:" + hyphaPattern + "}" - revQuery = `{rev:[\d]+}` -) - -// Sends found hyphae to the `ch`. `fullPath` is tested for hyphaness, then its subdirs with hyphaesque names are tested too using goroutines for each subdir. The function is recursive. -func recurFindHyphae(fullPath string) (hyphae []*Hypha) { - - structureMet, possibleRevisionPaths, possibleHyphaPaths, err := scanHyphaDir(fullPath) +func recurFindHyphae(fullPath string) map[string]*Hypha { + hyphae := make(map[string]*Hypha) + valid, revs, possibleSubhyphae, metaJsonPath, err := scanHyphaDir(fullPath) if err != nil { return hyphae } - // First, let's process inner hyphae - for _, possibleHyphaPath := range possibleHyphaPaths { - hyphae = append(hyphae, recurFindHyphae(possibleHyphaPath)...) + // First, let's process subhyphae + for _, possibleSubhypha := range possibleSubhyphae { + for k, v := range recurFindHyphae(possibleSubhypha) { + hyphae[k] = v + } } // This folder is not a hypha itself, nothing to do here - if !structureMet { + if !valid { return hyphae } - // Template hypha struct. Other fields are default jsont values. + // Template hypha struct. Other fields are default json values. h := Hypha{ + FullName: hyphaName(fullPath), Path: fullPath, - Name: hyphaName(fullPath), - ParentName: filepath.Dir(hyphaName(fullPath)), + parentName: filepath.Dir(hyphaName(fullPath)), // Children names are unknown now } - // Fill in every revision - for _, possibleRevisionPath := range possibleRevisionPaths { - rev, err := makeRevision(possibleRevisionPath, h.Name) - if err == nil { - h.Revisions = append(h.Revisions, rev) - } + metaJsonContents, err := ioutil.ReadFile(metaJsonPath) + if err != nil { + log.Printf("Error when reading `%s`; skipping", metaJsonPath) + return hyphae + } + err = json.Unmarshal(metaJsonContents, &h) + if err != nil { + log.Printf("Error when unmarshaling `%s`; skipping", metaJsonPath) + return hyphae } - mmJsonPath := filepath.Join(fullPath, "mm.json") - mmJsonContents, err := ioutil.ReadFile(mmJsonPath) - if err != nil { - fmt.Println(fullPath, ">\tError:", err) - return hyphae - } - err = json.Unmarshal(mmJsonContents, &h) - if err != nil { - fmt.Println(fullPath, ">\tError:", err) - return hyphae + // Fill in every revision paths + for id, paths := range revs { + if r, ok := h.Revisions[id]; ok { + for fType, fPath := range paths { + switch fType { + case "bin": + r.BinaryPath = fPath + case "txt": + r.TextPath = fPath + } + } + } else { + log.Printf("Error when reading hyphae from disk: hypha `%s`'s meta.json provided no information about revision `%s`, but files %s are provided; skipping\n", h.FullName, id, paths) + } } // Now the hypha should be ok, gotta send structs - hyphae = append(hyphae, &h) + hyphae[h.FullName] = &h return hyphae -} - -func makeRevision(fullPath string, fullName string) (r Revision, err error) { - // fullPath is expected to be a path to a dir. - // Revision directory must have at least `m.json` and `t.txt` files. - var ( - mJsonPresent bool - tTxtPresent bool - bPresent bool - ) - - nodes, err := ioutil.ReadDir(fullPath) - if err != nil { - return r, err - } - - for _, node := range nodes { - if node.IsDir() { - continue - } - switch node.Name() { - case "m.json": - mJsonPresent = true - case "t.txt": - tTxtPresent = true - case "b": - bPresent = true - } - } - - if !(mJsonPresent && tTxtPresent) { - return r, errors.New("makeRevision: m.json and t.txt files are not found") - } - - // If all the flags are true, this directory is assumed to be a revision. Gotta check further. This is template Revision struct. Other fields fall back to default init values. - mJsonPath := filepath.Join(fullPath, "m.json") - mJsonContents, err := ioutil.ReadFile(mJsonPath) - if err != nil { - fmt.Println(fullPath, ">\tError:", err) - return r, err - } - - r = Revision{FullName: fullName} - err = json.Unmarshal(mJsonContents, &r) - if err != nil { - fmt.Println(fullPath, ">\tError:", err) - return r, err - } - - // Now, let's fill in t.txt path - r.TextPath = filepath.Join(fullPath, "t.txt") - - // There's sense in reading binary file only if the hypha is marked as such - if r.MimeType != "application/x-hypha" { - // Do not check for binary file presence, attempt to read it will fail anyway - if bPresent { - r.BinaryPath = filepath.Join(fullPath, "b") - r.BinaryRequest = fmt.Sprintf("%s?rev=%d&action=getBinary", r.FullName, r.Id) - } else { - return r, errors.New("makeRevision: b file not present") - } - } - - // So far, so good. Let's fill in id. It is guaranteed to be correct, so no error checking - id, _ := strconv.Atoi(filepath.Base(fullPath)) - r.Id = id - - // It is safe now to return, I guess - return r, nil + }