1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-12-12 13:30:26 +00:00

Merge code from two branches, make it show something

This commit is contained in:
Timur Ismagilov 2020-06-16 23:35:52 +05:00
parent c69002de7b
commit 2a3f346034
5 changed files with 243 additions and 244 deletions

View File

@ -9,8 +9,8 @@ type Genealogy struct {
func setRelations(hyphae map[string]*Hypha) { func setRelations(hyphae map[string]*Hypha) {
for name, h := range hyphae { for name, h := range hyphae {
if _, ok := hyphae[h.ParentName]; ok && h.ParentName != "." { if _, ok := hyphae[h.ParentName()]; ok && h.ParentName() != "." {
hyphae[h.ParentName].ChildrenNames = append(hyphae[h.ParentName].ChildrenNames, name) hyphae[h.ParentName()].ChildrenNames = append(hyphae[h.ParentName()].ChildrenNames, name)
} }
} }
} }

View File

@ -1,70 +1,52 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"log"
"net/http"
"strconv" "strconv"
) )
type Hypha struct { type Hypha struct {
// Hypha is physically located here. Most fields below are stored in <path>/mm.ini (mm for metametadata). Its revisions are physically located in <path>/<n>/ subfolders. <n> ∈ [0;∞] with 0 being latest revision, 1 the first. FullName string
Path string Path string
// Every hypha was created at some point ViewCount int `json:"views"`
CreationTime int `json:"creationTime"` Deleted bool `json:"deleted"`
// Hypha has name but it can be changed Revisions map[string]*Revision `json:"revisions"`
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 <path>/..
ParentName string
// Hypha can have any number of children which are stored as subfolders in <path>.
ChildrenNames []string ChildrenNames []string
Revisions []Revision parentName string
} }
func (h Hypha) String() string { func (h *Hypha) AddChild(childName string) {
var revbuf string h.ChildrenNames = append(h.ChildrenNames, childName)
for _, r := range h.Revisions { }
revbuf += r.String() + "\n"
// 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"+ r, ok := h.Revisions[rev]
"path %v\n\t"+ if !ok {
"created at %v\n\t"+ return "", fmt.Errorf("Hypha %v has no such revision: %v", h.FullName, rev)
"child of %v\n\t"+ }
"parent of %v\n\t"+ html, err := r.AsHtml(hyphae)
"Having these revisions:\n%v"+ return html, err
"}\n", h.Name, h.Path, h.CreationTime, h.ParentName, h.ChildrenNames,
revbuf)
} }
func GetRevision(hyphae map[string]*Hypha, hyphaName string, rev string, w http.ResponseWriter) (Revision, bool) { func (h *Hypha) Name() string {
for name, _ := range hyphae { return h.FullName
if name == hyphaName { }
for _, r := range hyphae[name].Revisions {
id, err := strconv.Atoi(rev) func (h *Hypha) NewestRevision() string {
if err != nil { var largest int
log.Println("No such revision", rev, "at hypha", hyphaName) for k, _ := range h.Revisions {
w.WriteHeader(http.StatusNotFound) rev, _ := strconv.Atoi(k)
return Revision{}, false if rev > largest {
} largest = rev
if r.Id == id {
return r, true
}
}
} }
} }
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) ParentName() string {
func (h Hypha) Render(hyphae map[string]*Hypha, rev int) (ret string, err error) { return h.parentName
for _, r := range h.Revisions {
if r.Id == rev {
return r.Render(hyphae)
}
}
return "", errors.New("Revision was not found")
} }

29
main.go
View File

@ -12,6 +12,19 @@ import (
"time" "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 { func RevInMap(m map[string]string) string {
if val, ok := m["rev"]; ok { if val, ok := m["rev"]; ok {
return val return val
@ -33,7 +46,7 @@ func HandlerGetBinary(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
return return
} }
w.Header().Set("Content-Type", rev.MimeType) w.Header().Set("Content-Type", rev.BinaryMime)
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write(fileContents) w.Write(fileContents)
log.Println("Showing image of", rev.FullName, rev.Id) log.Println("Showing image of", rev.FullName, rev.Id)
@ -65,7 +78,7 @@ func HandlerZen(w http.ResponseWriter, r *http.Request) {
if !ok { if !ok {
return return
} }
html, err := rev.Render(hyphae) html, err := rev.AsHtml(hyphae)
if err != nil { if err != nil {
log.Println("Failed to render", rev.FullName) log.Println("Failed to render", rev.FullName)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -83,7 +96,7 @@ func HandlerView(w http.ResponseWriter, r *http.Request) {
if !ok { if !ok {
return return
} }
html, err := rev.Render(hyphae) html, err := rev.AsHtml(hyphae)
if err != nil { if err != nil {
log.Println("Failed to render", rev.FullName) log.Println("Failed to render", rev.FullName)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -131,7 +144,7 @@ var hyphae map[string]*Hypha
func hyphaeAsMap(hyphae []*Hypha) map[string]*Hypha { func hyphaeAsMap(hyphae []*Hypha) map[string]*Hypha {
mh := make(map[string]*Hypha) mh := make(map[string]*Hypha)
for _, h := range hyphae { for _, h := range hyphae {
mh[h.Name] = h mh[h.Name()] = h
} }
return mh return mh
} }
@ -147,8 +160,8 @@ func main() {
panic(err) panic(err)
} }
hyphae = hyphaeAsMap(recurFindHyphae(rootWikiDir)) hyphae = recurFindHyphae(rootWikiDir)
setRelations(hyphae) // setRelations(hyphae)
// Start server code // Start server code
r := mux.NewRouter() r := mux.NewRouter()
@ -197,8 +210,8 @@ func main() {
w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
for _, v := range hyphae { for _, v := range hyphae {
log.Println("Rendering latest revision of hypha", v.Name) log.Println("Rendering latest revision of hypha", v.Name())
html, err := v.Render(hyphae, 0) html, err := v.AsHtml(hyphae, "0")
if err != nil { if err != nil {
fmt.Fprintln(w, err) fmt.Fprintln(w, err)
} }

View File

@ -5,55 +5,39 @@ import (
"fmt" "fmt"
"github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown"
"io/ioutil" "io/ioutil"
"log"
"net/http"
) )
type Revision struct { 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
Id int Tags []string `json:"tags"`
// Name used at this revision FullName string `json:"name"`
Name string `json:"name"` Comment string `json:"comment"`
// Name of hypha Author string `json:"author"`
FullName string Time int `json:"time"`
// Present in every hypha. Stored in t.txt. TextMime string `json:"text_mime"`
TextPath string BinaryMime string `json:"binary_mime"`
// In at least one markup. Supported ones are "myco", "html", "md", "plain" TextPath string
Markup string `json:"markup"` BinaryPath string
// 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"`
} }
func (h Revision) String() string { // During initialisation, it is guaranteed that r.BinaryMime is set to "" if the revision has no binary data.
return fmt.Sprintf(`Revision %v created at %v { func (r *Revision) hasBinaryData() bool {
name: %v return r.BinaryMime != ""
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)
} }
// This method is meant to be called only by Hypha#Render. func (r *Revision) urlOfBinary() string {
func (r Revision) Render(hyphae map[string]*Hypha) (ret string, err error) { 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 += `<article class="page"> ret += `<article class="page">
` `
// If it is a binary hypha (we support only images for now): // TODO: support things other than images
// TODO: support things other than images. if r.hasBinaryData() {
if r.MimeType != "application/x-hypha" { ret += fmt.Sprintf(`<img src="/%s" class="page__image"/>`, r.urlOfBinary())
ret += fmt.Sprintf(`<img src="/%s" class="page__image"/>`, r.BinaryRequest)
} }
contents, err := ioutil.ReadFile(r.TextPath) 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 more markups.
// TODO: support mycorrhiza extensions like transclusion. // TODO: support mycorrhiza extensions like transclusion.
switch r.Markup { switch r.TextMime {
case "plain": case "text/plain":
ret += fmt.Sprintf(`<pre>%s</pre>`, contents) ret += fmt.Sprintf(`<pre>%s</pre>`, contents)
case "md": case "text/markdown":
html := markdown.ToHTML(contents, nil, nil) html := markdown.ToHTML(contents, nil, nil)
ret += string(html) ret += string(html)
default: default:
return "", errors.New("Unsupported markup: " + r.Markup) return "", errors.New("Unsupported mime-type: " + r.TextMime)
} }
ret += ` ret += `
</article>` </article>`
return ret, nil 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
}

244
walk.go
View File

@ -2,190 +2,156 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"io/ioutil" "io/ioutil"
"log"
"path/filepath" "path/filepath"
"regexp" "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) nodes, err := ioutil.ReadDir(fullPath)
if err != nil { if err != nil {
reterr = err
return // implicit return values return // implicit return values
} }
var (
mmJsonPresent bool
zeroDirPresent bool
)
for _, node := range nodes { for _, node := range nodes {
matchedHypha, _ := regexp.MatchString(hyphaPattern, node.Name()) hyphaM, revTxtM, revBinM, metaJsonM := matchNameToEverything(node.Name())
matchedRev, _ := regexp.MatchString(revisionPattern, node.Name())
switch { switch {
case matchedRev && node.IsDir(): case hyphaM && node.IsDir():
if node.Name() == "0" { possibleSubhyphae = append(possibleSubhyphae, filepath.Join(fullPath, node.Name()))
zeroDirPresent = true case revTxtM && !node.IsDir():
revId := stripLeadingInt(node.Name())
if _, ok := revs[revId]; !ok {
revs[revId] = make(map[string]string)
} }
possibleRevisionPaths = append( revs[revId]["txt"] = filepath.Join(fullPath, node.Name())
possibleRevisionPaths, case revBinM && !node.IsDir():
filepath.Join(fullPath, node.Name()), revId := stripLeadingInt(node.Name())
) if _, ok := revs[revId]; !ok {
case (node.Name() == "mm.json") && !node.IsDir(): revs[revId] = make(map[string]string)
mmJsonPresent = true }
case matchedHypha && node.IsDir(): revs[revId]["bin"] = filepath.Join(fullPath, node.Name())
possibleHyphaPaths = append( case metaJsonM && !node.IsDir():
possibleHyphaPaths, metaJsonPath = filepath.Join(fullPath, "meta.json")
filepath.Join(fullPath, node.Name()),
)
// Other nodes are ignored. It is not promised they will be ignored in future versions // Other nodes are ignored. It is not promised they will be ignored in future versions
} }
} }
if mmJsonPresent && zeroDirPresent { valid = hyphaDirRevsValidate(revs)
structureMet = true
}
return // implicit return values return // implicit return values
} }
func check(e error) {
if e != nil {
panic(e)
}
}
// Hypha name is rootWikiDir/{here} // Hypha name is rootWikiDir/{here}
func hyphaName(fullPath string) string { func hyphaName(fullPath string) string {
return fullPath[len(rootWikiDir)+1:] return fullPath[len(rootWikiDir)+1:]
} }
const ( func recurFindHyphae(fullPath string) map[string]*Hypha {
hyphaPattern = `[^\s\d:/?&\\][^:?&\\]*` hyphae := make(map[string]*Hypha)
revisionPattern = `[\d]+` valid, revs, possibleSubhyphae, metaJsonPath, err := scanHyphaDir(fullPath)
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)
if err != nil { if err != nil {
return hyphae return hyphae
} }
// First, let's process inner hyphae // First, let's process subhyphae
for _, possibleHyphaPath := range possibleHyphaPaths { for _, possibleSubhypha := range possibleSubhyphae {
hyphae = append(hyphae, recurFindHyphae(possibleHyphaPath)...) for k, v := range recurFindHyphae(possibleSubhypha) {
hyphae[k] = v
}
} }
// This folder is not a hypha itself, nothing to do here // This folder is not a hypha itself, nothing to do here
if !structureMet { if !valid {
return hyphae return hyphae
} }
// Template hypha struct. Other fields are default jsont values. // Template hypha struct. Other fields are default json values.
h := Hypha{ h := Hypha{
FullName: hyphaName(fullPath),
Path: fullPath, Path: fullPath,
Name: hyphaName(fullPath), parentName: filepath.Dir(hyphaName(fullPath)),
ParentName: filepath.Dir(hyphaName(fullPath)),
// Children names are unknown now // Children names are unknown now
} }
// Fill in every revision metaJsonContents, err := ioutil.ReadFile(metaJsonPath)
for _, possibleRevisionPath := range possibleRevisionPaths { if err != nil {
rev, err := makeRevision(possibleRevisionPath, h.Name) log.Printf("Error when reading `%s`; skipping", metaJsonPath)
if err == nil { return hyphae
h.Revisions = append(h.Revisions, rev) }
} err = json.Unmarshal(metaJsonContents, &h)
if err != nil {
log.Printf("Error when unmarshaling `%s`; skipping", metaJsonPath)
return hyphae
} }
mmJsonPath := filepath.Join(fullPath, "mm.json") // Fill in every revision paths
mmJsonContents, err := ioutil.ReadFile(mmJsonPath) for id, paths := range revs {
if err != nil { if r, ok := h.Revisions[id]; ok {
fmt.Println(fullPath, ">\tError:", err) for fType, fPath := range paths {
return hyphae switch fType {
} case "bin":
err = json.Unmarshal(mmJsonContents, &h) r.BinaryPath = fPath
if err != nil { case "txt":
fmt.Println(fullPath, ">\tError:", err) r.TextPath = fPath
return hyphae }
}
} 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 // Now the hypha should be ok, gotta send structs
hyphae = append(hyphae, &h) hyphae[h.FullName] = &h
return hyphae 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
} }