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

Reimplement action=raw and action=getBinary

This commit is contained in:
Timur Ismagilov 2020-06-24 19:01:51 +05:00
parent 753d63c70a
commit 8f2515b37a
13 changed files with 317 additions and 135 deletions

View File

@ -8,6 +8,13 @@ import (
"path/filepath" "path/filepath"
) )
const (
HyphaPattern = `[^\s\d:/?&\\][^:?&\\]*`
HyphaUrl = `/{hypha:` + HyphaPattern + `}`
RevisionPattern = `[\d]+`
RevQuery = `{rev:` + RevisionPattern + `}`
)
var ( var (
WikiDir string WikiDir string
TemplatesDir string TemplatesDir string

60
fs/fs.go Normal file
View File

@ -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() {
}

137
fs/hypha.go Normal file
View File

@ -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")
}

17
fs/revision.go Normal file
View File

@ -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"`
}

View File

@ -1,65 +1,75 @@
package main package main
import ( import (
"io/ioutil"
"log" "log"
// "io/ioutil"
// "log"
"net/http" "net/http"
"path/filepath" // "path/filepath"
"strconv" // "strconv"
"strings" // "strings"
"time" // "time"
"github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/fs"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
// There are handlers below. See main() for their usage. // There are handlers below. See main() for their usage.
// Boilerplate code present in many handlers. Good to have it. // 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) vars := mux.Vars(rq)
revno := RevInMap(vars) h, err := hs.Open(vars["hypha"])
return GetRevision(vars["hypha"], revno) if err != nil {
} log.Println(err)
return nil, false
func HandlerGetBinary(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok {
rev.ActionGetBinary(w)
} }
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) { func HandlerRaw(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok { log.Println("?action=raw")
rev.ActionRaw(w) 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) { func HandlerZen(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok { if h, ok := HandlerBase(w, rq); ok {
rev.ActionZen(w) h.ActionZen(w)
} }
} }
func HandlerView(w http.ResponseWriter, rq *http.Request) { func HandlerView(w http.ResponseWriter, rq *http.Request) {
if rev, ok := HandlerBase(w, rq); ok { if h, ok := HandlerBase(w, rq); ok {
rev.ActionView(w, HyphaPage) h.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"])))
} }
} }
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) { func HandlerEdit(w http.ResponseWriter, rq *http.Request) {
vars := mux.Vars(rq) vars := mux.Vars(rq)
ActionEdit(vars["hypha"], w) 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) { func HandlerRewind(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing") log.Println("Attempt to access an unimplemented thing")
@ -74,7 +84,9 @@ func HandlerRename(w http.ResponseWriter, rq *http.Request) {
w.WriteHeader(http.StatusNotImplemented) w.WriteHeader(http.StatusNotImplemented)
log.Println("Attempt to access an unimplemented thing") 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. // 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"} // Expected output for string above: []string{"foo", "bar", "kek"}
func makeTagsSlice(responseTagsString string) (ret []string) { 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} d := map[string]string{"Name": h.FullName}
w.Write([]byte(renderFromMap(d, "updateOk.html"))) w.Write([]byte(renderFromMap(d, "updateOk.html")))
} }
*/

View File

@ -14,18 +14,6 @@ import (
"github.com/bouncepaw/mycorrhiza/cfg" "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. // AsHtml returns HTML representation of the hypha.
// No layout or navigation are present here. Just the hypha. // No layout or navigation are present here. Just the hypha.
func (h *Hypha) AsHtml(id string, w http.ResponseWriter) (string, error) { 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) 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. // CreateDir creates directory where the hypha must reside.
// It is meant to be used with new hyphae. // It is meant to be used with new hyphae.
func (h *Hypha) CreateDir() error { func (h *Hypha) CreateDir() error {

88
main.go
View File

@ -9,25 +9,10 @@ import (
"time" "time"
"github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/cfg"
"github.com/bouncepaw/mycorrhiza/fs"
"github.com/gorilla/mux" "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)`. // 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". // If there is no `rev`, return "0".
func RevInMap(m map[string]string) string { func RevInMap(m map[string]string) string {
@ -37,8 +22,7 @@ func RevInMap(m map[string]string) string {
return "0" return "0"
} }
// `hyphae` is a map with all hyphae. Many functions use it. var hs *fs.Storage
var hyphae map[string]*Hypha
func main() { func main() {
if len(os.Args) == 1 { if len(os.Args) == 1 {
@ -52,51 +36,51 @@ func main() {
log.Println("Welcome to MycorrhizaWiki α") log.Println("Welcome to MycorrhizaWiki α")
cfg.InitConfig(wikiDir) cfg.InitConfig(wikiDir)
log.Println("Indexing hyphae...") log.Println("Indexing hyphae...")
hyphae = recurFindHyphae(wikiDir) hs = fs.InitStorage()
log.Println("Indexed", len(hyphae), "hyphae. Ready to accept requests.")
// Start server code. See handlers.go for handlers' implementations. // Start server code. See handlers.go for handlers' implementations.
r := mux.NewRouter() r := mux.NewRouter()
r.Queries("action", "getBinary", "rev", revQuery).Path(hyphaUrl). r.Queries("action", "getBinary", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
HandlerFunc(HandlerGetBinary) HandlerFunc(HandlerGetBinary)
r.Queries("action", "getBinary").Path(hyphaUrl). r.Queries("action", "getBinary").Path(cfg.HyphaUrl).
HandlerFunc(HandlerGetBinary) HandlerFunc(HandlerGetBinary)
r.Queries("action", "raw", "rev", revQuery).Path(hyphaUrl). r.Queries("action", "raw", "rev", cfg.RevQuery).Path(cfg.HyphaUrl).
HandlerFunc(HandlerRaw) HandlerFunc(HandlerRaw)
r.Queries("action", "raw").Path(hyphaUrl). r.Queries("action", "raw").Path(cfg.HyphaUrl).
HandlerFunc(HandlerRaw) 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). r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl).
HandlerFunc(HandlerZen) HandlerFunc(HandlerView)
r.Queries("action", "zen").Path(hyphaUrl). r.Queries("action", "view").Path(hyphaUrl).
HandlerFunc(HandlerZen) HandlerFunc(HandlerView)
r.Queries("action", "view", "rev", revQuery).Path(hyphaUrl). r.Queries("action", "history").Path(hyphaUrl).
HandlerFunc(HandlerView) HandlerFunc(HandlerHistory)
r.Queries("action", "view").Path(hyphaUrl).
HandlerFunc(HandlerView)
r.Queries("action", "history").Path(hyphaUrl). r.Queries("action", "edit").Path(hyphaUrl).
HandlerFunc(HandlerHistory) HandlerFunc(HandlerEdit)
r.Queries("action", "edit").Path(hyphaUrl). r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl).
HandlerFunc(HandlerEdit) HandlerFunc(HandlerRewind)
r.Queries("action", "rewind", "rev", revQuery).Path(hyphaUrl). r.Queries("action", "delete").Path(hyphaUrl).
HandlerFunc(HandlerRewind) HandlerFunc(HandlerDelete)
r.Queries("action", "delete").Path(hyphaUrl). r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl).
HandlerFunc(HandlerDelete) HandlerFunc(HandlerRename)
r.Queries("action", "rename", "to", hyphaPattern).Path(hyphaUrl). r.Queries("action", "update").Path(hyphaUrl).Methods("POST").
HandlerFunc(HandlerRename) HandlerFunc(HandlerUpdate)
*/
r.Queries("action", "update").Path(hyphaUrl).Methods("POST"). // r.HandleFunc(hyphaUrl, HandlerView)
HandlerFunc(HandlerUpdate)
r.HandleFunc(hyphaUrl, HandlerView)
// Debug page that renders all hyphae. // Debug page that renders all hyphae.
// TODO: make it redirect to home page. // TODO: make it redirect to home page.
@ -104,14 +88,10 @@ func main() {
r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { r.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
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 _, h := range hyphae { fmt.Fprintln(w, `
log.Println("Rendering latest revision of hypha", h.FullName) <p>Check out <a href="/Fruit">Fruit</a> maybe.</p>
html, err := h.AsHtml("0", w) <p><strong>TODO:</strong> make this page usable</p>
if err != nil { `)
fmt.Fprintln(w, err)
}
fmt.Fprintln(w, html)
}
}) })
http.Handle("/", r) http.Handle("/", r)

View File

@ -114,19 +114,6 @@ func (rev *Revision) ActionGetBinary(w http.ResponseWriter) {
log.Println("Serving binary data of", rev.FullName, rev.Id) 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`. // ActionZen is used with `?action=zen`.
// It renders the revision without any layout or navigation. // It renders the revision without any layout or navigation.
func (rev *Revision) ActionZen(w http.ResponseWriter) { func (rev *Revision) ActionZen(w http.ResponseWriter) {

View File

@ -0,0 +1 @@
<b>Pineapple is apple from a pine</b>

View File

@ -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": ""
}
}
}

View File