1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2025-01-05 17:40:26 +00:00

Start the Great Refactoring

This commit is contained in:
bouncepaw 2021-02-17 23:41:35 +05:00
parent 5cafaaa3d8
commit ee02211b3e
18 changed files with 836 additions and 625 deletions

View File

@ -42,9 +42,9 @@ func parseCliArgs() {
util.URL = "http://0.0.0.0:" + util.ServerPort
}
util.HomePage = CanonicalName(util.HomePage)
util.UserHypha = CanonicalName(util.UserHypha)
util.HeaderLinksHypha = CanonicalName(util.HeaderLinksHypha)
util.HomePage = util.CanonicalName(util.HomePage)
util.UserHypha = util.CanonicalName(util.UserHypha)
util.HeaderLinksHypha = util.CanonicalName(util.HeaderLinksHypha)
switch util.AuthMethod {
case "none":

View File

@ -11,6 +11,7 @@ import (
"git.sr.ht/~adnano/go-gemini"
"git.sr.ht/~adnano/go-gemini/certificate"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/util"
)
@ -28,13 +29,13 @@ Visit home hypha:
func geminiHypha(w *gemini.ResponseWriter, rq *gemini.Request) {
log.Println(rq.URL)
var (
hyphaName = geminiHyphaNameFromRq(rq, "page", "hypha")
data, hyphaExists = HyphaStorage[hyphaName]
hasAmnt = hyphaExists && data.BinaryPath != ""
contents string
hyphaName = geminiHyphaNameFromRq(rq, "page", "hypha")
h = hyphae.ByName(hyphaName)
hasAmnt = h.Exists && h.BinaryPath != ""
contents string
)
if hyphaExists {
fileContentsT, errT := ioutil.ReadFile(data.TextPath)
if h.Exists {
fileContentsT, errT := ioutil.ReadFile(h.TextPath)
if errT == nil {
md := markup.Doc(hyphaName, string(fileContentsT))
contents = md.AsGemtext()

View File

@ -134,3 +134,11 @@ func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp {
}
return hop
}
func (hop *HistoryOp) HasErrors() bool {
return len(hop.Errs) > 0
}
func (hop *HistoryOp) FirstErrorText() string {
return hop.Errs[0].Error()
}

View File

@ -6,6 +6,7 @@ import (
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
func init() {
@ -39,7 +40,7 @@ func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) {
func handlerLoginData(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
username = CanonicalName(rq.PostFormValue("username"))
username = util.CanonicalName(rq.PostFormValue("username"))
password = rq.PostFormValue("password")
err = user.LoginDataHTTP(w, rq, username, password)
)

View File

@ -5,6 +5,8 @@ import (
"log"
"net/http"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/templates"
"github.com/bouncepaw/mycorrhiza/user"
@ -25,182 +27,146 @@ func init() {
http.HandleFunc("/unattach-confirm/", handlerUnattachConfirm)
}
func handlerUnattachAsk(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "unattach-ask")
hd, isOld = HyphaStorage[hyphaName]
hasAmnt = hd != nil && hd.BinaryPath != ""
)
if !hasAmnt {
HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach")
log.Println("Rejected (no amnt):", rq.URL)
return
} else if ok := user.CanProceed(rq, "unattach-confirm"); !ok {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments")
log.Println("Rejected (no rights):", rq.URL)
return
}
util.HTTP200Page(w, base("Unattach "+hyphaName+"?", templates.UnattachAskHTML(rq, hyphaName, isOld), user.FromRequest(rq)))
}
func handlerUnattachConfirm(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "unattach-confirm")
hyphaData, isOld = HyphaStorage[hyphaName]
hasAmnt = hyphaData != nil && hyphaData.BinaryPath != ""
u = user.FromRequest(rq)
)
if !u.CanProceed("unattach-confirm") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments")
log.Println("Rejected (no rights):", rq.URL)
return
}
if !hasAmnt {
HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach")
log.Println("Rejected (no amnt):", rq.URL)
return
} else if !isOld {
// The precondition is to have the hypha in the first place.
HttpErr(w, http.StatusPreconditionFailed, hyphaName,
"Error: no such hypha",
"Could not unattach this hypha because it does not exist")
return
}
if hop := hyphaData.UnattachHypha(hyphaName, u); len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName,
"Error: could not unattach hypha",
fmt.Sprintf("Could not unattach this hypha due to internal errors. Server errors: <code>%v</code>", hop.Errs))
return
}
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
}
func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "rename-ask")
_, isOld = HyphaStorage[hyphaName]
u = user.FromRequest(rq)
)
if !u.CanProceed("rename-confirm") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
log.Println("Rejected", rq.URL)
return
}
util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(rq, hyphaName, isOld), u))
}
func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "rename-confirm")
_, isOld = HyphaStorage[hyphaName]
newName = CanonicalName(rq.PostFormValue("new-name"))
_, newNameIsUsed = HyphaStorage[newName]
recursive = rq.PostFormValue("recursive") == "true"
u = user.FromRequest(rq)
)
switch {
case !u.CanProceed("rename-confirm"):
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.")
log.Println("Rejected", rq.URL)
case newNameIsUsed:
HttpErr(w, http.StatusBadRequest, hyphaName, "Error: hypha exists",
fmt.Sprintf("Hypha named <a href='/page/%s'>%s</a> already exists.", hyphaName, hyphaName))
case newName == "":
HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no name",
"No new name is given.")
case !isOld:
HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no such hypha",
"Cannot rename a hypha that does not exist yet.")
case !HyphaPattern.MatchString(newName):
HttpErr(w, http.StatusBadRequest, hyphaName, "Error: invalid name",
"Invalid new name. Names cannot contain characters <code>^?!:#@&gt;&lt;*|\"\\'&amp;%</code>")
default:
if hop := RenameHypha(hyphaName, newName, recursive, u); len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName,
"Error: could not rename hypha",
fmt.Sprintf("Could not rename this hypha due to an internal error. Server errors: <code>%v</code>", hop.Errs))
} else {
http.Redirect(w, rq, "/page/"+newName, http.StatusSeeOther)
func factoryHandlerAsker(
actionPath string,
asker func(*hyphae.Hypha, *user.User) (error, string),
succTitleTemplate string,
succPageTemplate func(*http.Request, string, bool) string,
) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, actionPath)
h = hyphae.ByName(hyphaName)
u = user.FromRequest(rq)
)
if err, errtitle := asker(h, u); err != nil {
HttpErr(
w,
http.StatusInternalServerError,
hyphaName,
errtitle,
err.Error())
return
}
util.HTTP200Page(
w,
base(
fmt.Sprintf(succTitleTemplate, hyphaName),
succPageTemplate(rq, hyphaName, h.Exists),
u))
}
}
// handlerDeleteAsk shows a delete dialog.
func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "delete-ask")
_, isOld = HyphaStorage[hyphaName]
u = user.FromRequest(rq)
)
if !u.CanProceed("delete-ask") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
log.Println("Rejected", rq.URL)
return
var handlerUnattachAsk = factoryHandlerAsker(
"unattach-ask",
func(h *hyphae.Hypha, u *user.User) (error, string) {
return h.CanUnattach(u)
},
"Unattach %s?",
templates.UnattachAskHTML,
)
var handlerDeleteAsk = factoryHandlerAsker(
"delete-ask",
func(h *hyphae.Hypha, u *user.User) (error, string) {
return h.CanDelete(u)
},
"Delete %s?",
templates.DeleteAskHTML,
)
var handlerRenameAsk = factoryHandlerAsker(
"rename-ask",
func(h *hyphae.Hypha, u *user.User) (error, string) {
return h.CanRename(u)
},
"Rename %s?",
templates.RenameAskHTML,
)
func factoryHandlerConfirmer(
actionPath string,
confirmer func(*hyphae.Hypha, *user.User, *http.Request) (*history.HistoryOp, string),
) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "unattach-confirm")
h = hyphae.ByName(hyphaName)
u = user.FromRequest(rq)
)
if hop, errtitle := confirmer(h, u, rq); hop.HasErrors() {
HttpErr(w, http.StatusInternalServerError, hyphaName,
errtitle,
hop.FirstErrorText())
return
}
http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther)
}
util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(rq, hyphaName, isOld), u))
}
// handlerDeleteConfirm deletes a hypha for sure
func handlerDeleteConfirm(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "delete-confirm")
hyphaData, isOld = HyphaStorage[hyphaName]
u = user.FromRequest(rq)
)
if !u.CanProceed("delete-confirm") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.")
log.Println("Rejected", rq.URL)
return
}
if !isOld {
// The precondition is to have the hypha in the first place.
HttpErr(w, http.StatusPreconditionFailed, hyphaName,
"Error: no such hypha",
"Could not delete this hypha because it does not exist.")
return
}
if hop := hyphaData.DeleteHypha(hyphaName, u); len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName,
"Error: could not delete hypha",
fmt.Sprintf("Could not delete this hypha due to internal errors. Server errors: <code>%v</code>", hop.Errs))
return
}
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
}
var handlerUnattachConfirm = factoryHandlerConfirmer(
"unattach-confirm",
func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) {
return h.UnattachHypha(u)
},
)
var handlerDeleteConfirm = factoryHandlerConfirmer(
"delete-confirm",
func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) {
return h.DeleteHypha(u)
},
)
var handlerRenameConfirm = factoryHandlerConfirmer(
"rename-confirm",
func(oldHypha *hyphae.Hypha, u *user.User, rq *http.Request) (*history.HistoryOp, string) {
var (
newName = util.CanonicalName(rq.PostFormValue("new-name"))
recursive = rq.PostFormValue("recursive") == "true"
newHypha = hyphae.ByName(newName)
)
return oldHypha.RenameHypha(newHypha, recursive, u)
},
)
// handlerEdit shows the edit form. It doesn't edit anything actually.
func handlerEdit(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "edit")
hyphaData, isOld = HyphaStorage[hyphaName]
warning string
textAreaFill string
err error
u = user.FromRequest(rq)
hyphaName = HyphaNameFromRq(rq, "edit")
h = hyphae.ByName(hyphaName)
warning string
textAreaFill string
err error
u = user.FromRequest(rq)
)
if !u.CanProceed("edit") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
log.Println("Rejected", rq.URL)
if err, errtitle := h.CanEdit(u); err != nil {
HttpErr(w, http.StatusInternalServerError, hyphaName,
errtitle,
err.Error())
return
}
if isOld {
textAreaFill, err = FetchTextPart(hyphaData)
if h.Exists {
textAreaFill, err = h.FetchTextPart()
if err != nil {
log.Println(err)
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", "Could not fetch text data")
HttpErr(w, http.StatusInternalServerError, hyphaName,
"Error",
"Could not fetch text data")
return
}
} else {
warning = `<p>You are creating a new hypha.</p>`
warning = `<p class="warning warning_new-hypha">You are creating a new hypha.</p>`
}
util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(rq, hyphaName, textAreaFill, warning), u))
util.HTTP200Page(
w,
base(
"Edit "+hyphaName,
templates.EditHTML(rq, hyphaName, textAreaFill, warning),
u))
}
// handlerUploadText uploads a new text part for the hypha.
@ -208,62 +174,75 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "upload-text")
h = hyphae.ByName(hyphaName)
textData = rq.PostFormValue("text")
action = rq.PostFormValue("action")
u = user.FromRequest(rq)
hop *history.HistoryOp
errtitle string
)
if !u.CanProceed("upload-text") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
log.Println("Rejected", rq.URL)
return
}
if textData == "" {
HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed")
if action != "Preview" {
hop, errtitle = h.UploadText([]byte(textData), u)
}
if hop.HasErrors() {
HttpErr(w, http.StatusForbidden, hyphaName,
errtitle,
hop.FirstErrorText())
return
}
if action == "Preview" {
util.HTTP200Page(w, base("Preview "+hyphaName, templates.PreviewHTML(rq, hyphaName, textData, "", markup.Doc(hyphaName, textData).AsHTML()), u))
} else if hop := UploadText(hyphaName, textData, u); len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
util.HTTP200Page(
w,
base(
"Preview "+hyphaName,
templates.PreviewHTML(
rq,
hyphaName,
textData,
"",
markup.Doc(hyphaName, textData).AsHTML()),
u))
} else {
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther)
}
}
// handlerUploadBinary uploads a new binary part for the hypha.
func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "upload-binary")
u = user.FromRequest(rq)
)
if !u.CanProceed("upload-binary") {
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to upload attachments.")
log.Println("Rejected", rq.URL)
return
}
rq.ParseMultipartForm(10 << 20) // Set upload limit
file, handler, err := rq.FormFile("binary")
if file != nil {
defer file.Close()
var (
hyphaName = HyphaNameFromRq(rq, "upload-binary")
h = hyphae.ByName(hyphaName)
u = user.FromRequest(rq)
file, handler, err = rq.FormFile("binary")
)
if err, errtitle := h.CanAttach(err, u); err != nil {
HttpErr(w, http.StatusInternalServerError, hyphaName,
errtitle,
err.Error())
}
// If file is not passed:
if err != nil {
HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No binary data passed")
return
}
// If file is passed:
if file != nil {
defer file.Close()
}
var (
mime = handler.Header.Get("Content-Type")
hop = UploadBinary(hyphaName, mime, file, u)
mime = handler.Header.Get("Content-Type")
hop, errtitle = h.UploadBinary(mime, file, u)
)
if len(hop.Errs) != 0 {
HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error())
if hop.HasErrors() {
HttpErr(w, http.StatusInternalServerError, hyphaName, errtitle, hop.FirstErrorText())
return
}
http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther)
http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther)
}

View File

@ -10,6 +10,7 @@ import (
"strings"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/mimetype"
"github.com/bouncepaw/mycorrhiza/templates"
@ -33,7 +34,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
shorterUrl = strings.TrimPrefix(rq.URL.Path, "/rev/")
firstSlashIndex = strings.IndexRune(shorterUrl, '/')
revHash = shorterUrl[:firstSlashIndex]
hyphaName = CanonicalName(shorterUrl[firstSlashIndex+1:])
hyphaName = util.CanonicalName(shorterUrl[firstSlashIndex+1:])
contents = fmt.Sprintf(`<p>This hypha had no text at this revision.</p>`)
TextPath = hyphaName + ".myco"
textContents, err = history.FileAtRevision(TextPath, revHash)
@ -42,7 +43,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
if err == nil {
contents = markup.Doc(hyphaName, textContents).AsHTML()
}
treeHTML, _, _ := tree.Tree(hyphaName, IterateHyphaNamesWith)
treeHTML, _, _ := tree.Tree(hyphaName)
page := templates.RevisionHTML(
rq,
hyphaName,
@ -60,10 +61,10 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) {
func handlerText(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
hyphaName := HyphaNameFromRq(rq, "text")
if data, ok := HyphaStorage[hyphaName]; ok {
log.Println("Serving", data.TextPath)
if h := hyphae.ByName(hyphaName); h.Exists {
log.Println("Serving", h.TextPath)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
http.ServeFile(w, rq, data.TextPath)
http.ServeFile(w, rq, h.TextPath)
}
}
@ -71,10 +72,10 @@ func handlerText(w http.ResponseWriter, rq *http.Request) {
func handlerBinary(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
hyphaName := HyphaNameFromRq(rq, "binary")
if data, ok := HyphaStorage[hyphaName]; ok {
log.Println("Serving", data.BinaryPath)
w.Header().Set("Content-Type", mimetype.FromExtension(filepath.Ext(data.BinaryPath)))
http.ServeFile(w, rq, data.BinaryPath)
if h := hyphae.ByName(hyphaName); h.Exists {
log.Println("Serving", h.BinaryPath)
w.Header().Set("Content-Type", mimetype.FromExtension(filepath.Ext(h.BinaryPath)))
http.ServeFile(w, rq, h.BinaryPath)
}
}
@ -82,26 +83,26 @@ func handlerBinary(w http.ResponseWriter, rq *http.Request) {
func handlerHypha(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var (
hyphaName = HyphaNameFromRq(rq, "page", "hypha")
data, hyphaExists = HyphaStorage[hyphaName]
hasAmnt = hyphaExists && data.BinaryPath != ""
contents string
openGraph string
u = user.FromRequest(rq)
hyphaName = HyphaNameFromRq(rq, "page", "hypha")
h = hyphae.ByName(hyphaName)
hasAmnt = h.Exists && h.BinaryPath != ""
contents string
openGraph string
u = user.FromRequest(rq)
)
if hyphaExists {
fileContentsT, errT := ioutil.ReadFile(data.TextPath)
_, errB := os.Stat(data.BinaryPath)
if h.Exists {
fileContentsT, errT := ioutil.ReadFile(h.TextPath)
_, errB := os.Stat(h.BinaryPath)
if errT == nil {
md := markup.Doc(hyphaName, string(fileContentsT))
contents = md.AsHTML()
openGraph = md.OpenGraphHTML()
}
if !os.IsNotExist(errB) {
contents = binaryHtmlBlock(hyphaName, data) + contents
contents = h.BinaryHtmlBlock() + contents
}
}
treeHTML, prevHypha, nextHypha := tree.Tree(hyphaName, IterateHyphaNamesWith)
treeHTML, prevHypha, nextHypha := tree.Tree(hyphaName)
util.HTTP200Page(w,
templates.BaseHTML(
util.BeautifulName(hyphaName),

326
hypha.go
View File

@ -1,326 +0,0 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"log"
"mime/multipart"
"os"
"path/filepath"
"regexp"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/mimetype"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
func init() {
markup.HyphaExists = func(hyphaName string) bool {
_, hyphaExists := HyphaStorage[hyphaName]
return hyphaExists
}
markup.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) {
if hyphaData, ok := HyphaStorage[hyphaName]; ok {
rawText, err = FetchTextPart(hyphaData)
if hyphaData.BinaryPath != "" {
binaryBlock = binaryHtmlBlock(hyphaName, hyphaData)
}
} else {
err = errors.New("Hypha " + hyphaName + " does not exist")
}
return
}
markup.HyphaIterate = IterateHyphaNamesWith
markup.HyphaImageForOG = func(hyphaName string) string {
if hd, isOld := GetHyphaData(hyphaName); isOld && hd.BinaryPath != "" {
return util.URL + "/binary/" + hyphaName
}
return util.URL + "/favicon.ico"
}
}
// GetHyphaData finds a hypha addressed by `hyphaName` and returns its `hyphaData`. `hyphaData` is set to a zero value if this hypha does not exist. `isOld` is false if this hypha does not exist.
func GetHyphaData(hyphaName string) (hyphaData *HyphaData, isOld bool) {
hyphaData, isOld = HyphaStorage[hyphaName]
if hyphaData == nil {
hyphaData = &HyphaData{}
}
return
}
// HyphaData represents a hypha's meta information: binary and text parts rooted paths and content types.
type HyphaData hyphae.Hypha
// uploadHelp is a helper function for UploadText and UploadBinary
func uploadHelp(hop *history.HistoryOp, hyphaName, ext string, data []byte, u *user.User) *history.HistoryOp {
var (
hyphaData, isOld = GetHyphaData(hyphaName)
fullPath = filepath.Join(WikiDir, hyphaName+ext)
originalFullPath = &hyphaData.TextPath
)
if hop.Type == history.TypeEditBinary {
originalFullPath = &hyphaData.BinaryPath
}
if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
return hop.WithError(err)
}
if err := ioutil.WriteFile(fullPath, data, 0644); err != nil {
return hop.WithError(err)
}
if isOld && *originalFullPath != fullPath && *originalFullPath != "" {
if err := history.Rename(*originalFullPath, fullPath); err != nil {
return hop.WithError(err)
}
log.Println("Move", *originalFullPath, "to", fullPath)
}
// New hyphae must be added to the hypha storage
if !isOld {
HyphaStorage[hyphaName] = hyphaData
hyphae.IncrementCount()
}
*originalFullPath = fullPath
if isOld && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) {
return hop.Abort()
}
return hop.WithFiles(fullPath).
WithUser(u).
Apply()
}
// UploadText loads a new text part from `textData` for hypha `hyphaName`.
func UploadText(hyphaName, textData string, u *user.User) *history.HistoryOp {
return uploadHelp(
history.
Operation(history.TypeEditText).
WithMsg(fmt.Sprintf("Edit %s", hyphaName)),
hyphaName, ".myco", []byte(textData), u)
}
// UploadBinary loads a new binary part from `file` for hypha `hyphaName` with `hd`. The contents have the specified `mime` type. It must be marked if the hypha `isOld`.
func UploadBinary(hyphaName, mime string, file multipart.File, u *user.User) *history.HistoryOp {
var (
hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload binary part for %s with type %s", hyphaName, mime))
data, err = ioutil.ReadAll(file)
)
if err != nil {
return hop.WithError(err).Apply()
}
return uploadHelp(hop, hyphaName, mimetype.ToExtension(mime), data, u)
}
// DeleteHypha deletes hypha and makes a history record about that.
func (hd *HyphaData) DeleteHypha(hyphaName string, u *user.User) *history.HistoryOp {
hop := history.Operation(history.TypeDeleteHypha).
WithFilesRemoved(hd.TextPath, hd.BinaryPath).
WithMsg(fmt.Sprintf("Delete %s", hyphaName)).
WithUser(u).
Apply()
if len(hop.Errs) == 0 {
delete(HyphaStorage, hyphaName)
hyphae.DecrementCount()
}
return hop
}
// UnattachHypha unattaches hypha and makes a history record about that.
func (hd *HyphaData) UnattachHypha(hyphaName string, u *user.User) *history.HistoryOp {
hop := history.Operation(history.TypeUnattachHypha).
WithFilesRemoved(hd.BinaryPath).
WithMsg(fmt.Sprintf("Unattach %s", hyphaName)).
WithUser(u).
Apply()
if len(hop.Errs) == 0 {
hd, ok := HyphaStorage[hyphaName]
if ok {
if hd.BinaryPath != "" {
hd.BinaryPath = ""
}
// If nothing is left of the hypha
if hd.TextPath == "" {
delete(HyphaStorage, hyphaName)
hyphae.DecrementCount()
}
}
}
return hop
}
func findHyphaeToRename(hyphaName string, recursive bool) []string {
hyphae := []string{hyphaName}
if recursive {
hyphae = append(hyphae, util.FindSubhyphae(hyphaName, IterateHyphaNamesWith)...)
}
return hyphae
}
func renamingPairs(hyphaNames []string, replaceName func(string) string) (map[string]string, error) {
renameMap := make(map[string]string)
for _, hn := range hyphaNames {
if hd, ok := HyphaStorage[hn]; ok {
if _, nameUsed := HyphaStorage[replaceName(hn)]; nameUsed {
return nil, errors.New("Hypha " + replaceName(hn) + " already exists")
}
if hd.TextPath != "" {
renameMap[hd.TextPath] = replaceName(hd.TextPath)
}
if hd.BinaryPath != "" {
renameMap[hd.BinaryPath] = replaceName(hd.BinaryPath)
}
}
}
return renameMap, nil
}
// word Data is plural here
func relocateHyphaData(hyphaNames []string, replaceName func(string) string) {
for _, hyphaName := range hyphaNames {
if hd, ok := HyphaStorage[hyphaName]; ok {
hd.TextPath = replaceName(hd.TextPath)
hd.BinaryPath = replaceName(hd.BinaryPath)
HyphaStorage[replaceName(hyphaName)] = hd
delete(HyphaStorage, hyphaName)
}
}
}
// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way.
func RenameHypha(hyphaName, newName string, recursive bool, u *user.User) *history.HistoryOp {
var (
re = regexp.MustCompile(`(?i)` + hyphaName)
replaceName = func(str string) string {
return re.ReplaceAllString(CanonicalName(str), newName)
}
hyphaNames = findHyphaeToRename(hyphaName, recursive)
renameMap, err = renamingPairs(hyphaNames, replaceName)
renameMsg = "Rename %s to %s"
hop = history.Operation(history.TypeRenameHypha)
)
if err != nil {
hop.Errs = append(hop.Errs, err)
return hop
}
if recursive {
renameMsg += " recursively"
}
hop.WithFilesRenamed(renameMap).
WithMsg(fmt.Sprintf(renameMsg, hyphaName, newName)).
WithUser(u).
Apply()
if len(hop.Errs) == 0 {
relocateHyphaData(hyphaNames, replaceName)
}
return hop
}
// binaryHtmlBlock creates an html block for binary part of the hypha.
func binaryHtmlBlock(hyphaName string, hd *HyphaData) string {
switch filepath.Ext(hd.BinaryPath) {
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
return fmt.Sprintf(`
<div class="binary-container binary-container_with-img">
<a href="/binary/%[1]s"><img src="/binary/%[1]s"/></a>
</div>`, hyphaName)
case ".ogg", ".webm", ".mp4":
return fmt.Sprintf(`
<div class="binary-container binary-container_with-video">
<video>
<source src="/binary/%[1]s"/>
<p>Your browser does not support video. See video's <a href="/binary/%[1]s">direct url</a></p>
</video>
`, hyphaName)
case ".mp3":
return fmt.Sprintf(`
<div class="binary-container binary-container_with-audio">
<audio>
<source src="/binary/%[1]s"/>
<p>Your browser does not support audio. See audio's <a href="/binary/%[1]s">direct url</a></p>
</audio>
`, hyphaName)
default:
return fmt.Sprintf(`
<div class="binary-container binary-container_with-nothing">
<p>This hypha's media cannot be rendered. <a href="/binary/%s">Download it</a></p>
</div>
`, hyphaName)
}
}
// FetchTextPart tries to read text file in the `d`. If there is no file, empty string is returned.
func FetchTextPart(d *HyphaData) (string, error) {
if d.TextPath == "" {
return "", nil
}
_, err := os.Stat(d.TextPath)
if os.IsNotExist(err) {
return "", nil
} else if err != nil {
return "", err
}
text, err := ioutil.ReadFile(d.TextPath)
if err != nil {
return "", err
}
return string(text), nil
}
func setHeaderLinks() {
if userLinksHypha, ok := GetHyphaData(util.HeaderLinksHypha); !ok {
util.SetDefaultHeaderLinks()
} else {
contents, err := ioutil.ReadFile(userLinksHypha.TextPath)
if err != nil || len(contents) == 0 {
util.SetDefaultHeaderLinks()
} else {
text := string(contents)
util.ParseHeaderLinks(text, markup.Rocketlink)
}
}
}
func HyphaToTemporaryWorkaround(h *hyphae.Hypha) *HyphaData {
return &HyphaData{
Name: h.Name,
TextPath: h.TextPath,
BinaryPath: h.BinaryPath,
}
}
// MergeIn merges in content file paths from a different hypha object. Prints warnings sometimes.
func (h *HyphaData) MergeIn(oh *hyphae.Hypha) {
if h.TextPath == "" && oh.TextPath != "" {
h.TextPath = oh.TextPath
}
if oh.BinaryPath != "" {
if h.BinaryPath != "" {
log.Println("There is a file collision for binary part of a hypha:", h.BinaryPath, "and", oh.BinaryPath, "-- going on with the latter")
}
h.BinaryPath = oh.BinaryPath
}
}
// Index finds all hypha files in the full `path` and saves them to HyphaStorage. This function is recursive.
func Index(path string) {
ch := make(chan *hyphae.Hypha, 5)
go func() {
hyphae.Index(path, 0, ch)
close(ch)
}()
for h := range ch {
if oldHypha, ok := HyphaStorage[h.Name]; ok {
oldHypha.MergeIn(h)
} else {
HyphaStorage[h.Name] = HyphaToTemporaryWorkaround(h)
hyphae.IncrementCount()
}
}
}

52
hyphae/delete.go Normal file
View File

@ -0,0 +1,52 @@
package hyphae
import (
"errors"
"fmt"
"log"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/user"
)
func rejectDeleteLog(h *Hypha, u *user.User, errmsg string) {
log.Printf("Reject delete %s by @%s: %s\n", h.Name, u.Name, errmsg)
}
// CanDelete checks if given user can delete given hypha.
func (h *Hypha) CanDelete(u *user.User) (err error, errtitle string) {
// First, check if can unattach at all
if !u.CanProceed("delete-confirm") {
rejectDeleteLog(h, u, "no rights")
return errors.New("Not enough rights to delete, you must be a moderator"), "Not enough rights"
}
if !h.Exists {
rejectDeleteLog(h, u, "does not exist")
return errors.New("Cannot delete this hypha because it does not exist"), "Does not exist"
}
return nil, ""
}
// DeleteHypha deletes hypha and makes a history record about that.
func (h *Hypha) DeleteHypha(u *user.User) (hop *history.HistoryOp, errtitle string) {
h.Lock()
defer h.Unlock()
hop = history.Operation(history.TypeDeleteHypha)
if err, errtitle := h.CanDelete(u); errtitle != "" {
hop.WithError(err)
return hop, errtitle
}
hop.
WithFilesRemoved(h.TextPath, h.BinaryPath).
WithMsg(fmt.Sprintf("Delete %s", h.Name)).
WithUser(u).
Apply()
if len(hop.Errs) == 0 {
h.delete()
}
return hop, ""
}

View File

@ -9,8 +9,28 @@ import (
"github.com/bouncepaw/mycorrhiza/util"
)
// Index finds all hypha files in the full `path` and sends them to the channel. Handling of duplicate entries and attachment and counting them is up to the caller.
func Index(path string, nestLevel uint, ch chan *Hypha) {
// Index finds all hypha files in the full `path` and saves them to the hypha storage.
func Index(path string) {
ch := make(chan *Hypha, 5)
go func(ch chan *Hypha) {
indexHelper(path, 0, ch)
close(ch)
}(ch)
for h := range ch {
// At this time it is safe to ignore the mutex, because there is only one worker.
if oldHypha, ok := byNames[h.Name]; ok {
oldHypha.MergeIn(h)
} else {
byNames[h.Name] = h
IncrementCount()
}
}
}
// indexHelper finds all hypha files in the full `path` and sends them to the channel. Handling of duplicate entries and attachment and counting them is up to the caller.
func indexHelper(path string, nestLevel uint, ch chan *Hypha) {
nodes, err := ioutil.ReadDir(path)
if err != nil {
log.Fatal(err)
@ -22,14 +42,14 @@ func Index(path string, nestLevel uint, ch chan *Hypha) {
util.IsCanonicalName(node.Name()) &&
node.Name() != ".git" &&
!(nestLevel == 0 && node.Name() == "static") {
Index(filepath.Join(path, node.Name()), nestLevel+1, ch)
indexHelper(filepath.Join(path, node.Name()), nestLevel+1, ch)
continue
}
var (
hyphaPartPath = filepath.Join(path, node.Name())
hyphaName, isText, skip = mimetype.DataFromFilename(hyphaPartPath)
hypha = &Hypha{Name: hyphaName}
hypha = &Hypha{Name: hyphaName, Exists: true}
)
if !skip {
if isText {
@ -39,6 +59,5 @@ func Index(path string, nestLevel uint, ch chan *Hypha) {
}
ch <- hypha
}
}
}

View File

@ -1,9 +1,47 @@
package hyphae
import (
"errors"
"log"
"regexp"
"strings"
"sync"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/util"
)
func init() {
markup.HyphaExists = func(hyphaName string) bool {
return ByName(hyphaName).Exists
}
markup.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) {
if h := ByName(hyphaName); h.Exists {
rawText, err = h.FetchTextPart()
if h.BinaryPath != "" {
binaryBlock = h.BinaryHtmlBlock()
}
} else {
err = errors.New("Hypha " + hyphaName + " does not exist")
}
return
}
markup.HyphaIterate = func(λ func(string)) {
for h := range YieldExistingHyphae() {
λ(h.Name)
}
}
markup.HyphaImageForOG = func(hyphaName string) string {
if h := ByName(hyphaName); h.Exists && h.BinaryPath != "" {
return util.URL + "/binary/" + hyphaName
}
return util.URL + "/favicon.ico"
}
}
// HyphaPattern is a pattern which all hyphae must match.
var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%{}]+`)
type Hypha struct {
sync.RWMutex
@ -11,33 +49,125 @@ type Hypha struct {
Exists bool
TextPath string
BinaryPath string
OutLinks []*Hypha
BackLinks []*Hypha
OutLinks []*Hypha // not used yet
BackLinks []*Hypha // not used yet
}
/*
// Insert inserts the hypha into the mycelium. It overwrites the previous record, if there was any, and returns false. If the was no previous record, return true.
var byNames = make(map[string]*Hypha)
var byNamesMutex = sync.Mutex{}
// YieldExistingHyphae iterates over all hyphae and yields all existing ones.
func YieldExistingHyphae() chan *Hypha {
ch := make(chan *Hypha)
go func(ch chan *Hypha) {
for _, h := range byNames {
if h.Exists {
ch <- h
}
}
close(ch)
}(ch)
return ch
}
// Subhyphae returns slice of subhyphae.
func (h *Hypha) Subhyphae() []*Hypha {
hyphae := []*Hypha{}
for subh := range YieldExistingHyphae() {
if strings.HasPrefix(subh.Name, h.Name+"/") {
hyphae = append(hyphae, subh)
}
}
return hyphae
}
// AreFreeNames checks if all given `hyphaNames` are not taken.
func AreFreeNames(hyphaNames ...string) (firstFailure string, ok bool) {
for h := range YieldExistingHyphae() {
for _, hn := range hyphaNames {
if hn == h.Name {
return hn, false
}
}
}
return "", true
}
// EmptyHypha returns an empty hypha struct with given name.
func EmptyHypha(hyphaName string) *Hypha {
return &Hypha{
Name: hyphaName,
Exists: false,
TextPath: "",
BinaryPath: "",
OutLinks: make([]*Hypha, 0),
BackLinks: make([]*Hypha, 0),
}
}
// ByName returns a hypha by name. If h.Exists, the returned hypha pointer is known to be part of the hypha index (byNames map).
func ByName(hyphaName string) (h *Hypha) {
byNamesMutex.Lock()
defer byNamesMutex.Unlock()
h, exists := byNames[hyphaName]
if exists {
return h
}
return EmptyHypha(hyphaName)
}
// Insert inserts the hypha into the storage. It overwrites the previous record, if there was any, and returns false. If the was no previous record, return true.
func (h *Hypha) Insert() (justCreated bool) {
var hp *Hypha
hp, justCreated = ByName(h.Name)
hp = ByName(h.Name)
mycm.Lock()
defer mycm.Unlock()
if justCreated {
mycm.byNames[hp.Name] = h
} else {
byNamesMutex.Lock()
defer byNamesMutex.Unlock()
if hp.Exists {
hp = h
} else {
byNames[hp.Name] = h
}
return justCreated
}*/
return !hp.Exists
}
// PhaseOut marks the hypha as non-existent. This is an idempotent operation.
func (h *Hypha) PhaseOut() {
func (h *Hypha) InsertIfNew() (justCreated bool) {
if h.Exists {
return h.Insert()
}
return false
}
func (h *Hypha) delete() {
byNamesMutex.Lock()
h.Lock()
h.Exists = false
h.OutLinks = make([]*Hypha, 0)
h.TextPath = ""
h.BinaryPath = ""
delete(byNames, h.Name)
DecrementCount()
byNamesMutex.Unlock()
h.Unlock()
}
func (h *Hypha) renameTo(newName string) {
byNamesMutex.Lock()
h.Lock()
delete(byNames, h.Name)
h.Name = newName
byNames[h.Name] = h
byNamesMutex.Unlock()
h.Unlock()
}
// MergeIn merges in content file paths from a different hypha object. Prints warnings sometimes.
func (h *Hypha) MergeIn(oh *Hypha) {
if h.TextPath == "" && oh.TextPath != "" {
h.TextPath = oh.TextPath
}
if oh.BinaryPath != "" {
if h.BinaryPath != "" {
log.Println("There is a file collision for binary part of a hypha:", h.BinaryPath, "and", oh.BinaryPath, "-- going on with the latter")
}
h.BinaryPath = oh.BinaryPath
}
}

122
hyphae/rename.go Normal file
View File

@ -0,0 +1,122 @@
package hyphae
import (
"errors"
"fmt"
"log"
"regexp"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
func rejectRenameLog(h *Hypha, u *user.User, errmsg string) {
log.Printf("Reject rename %s by @%s: %s\n", h.Name, u.Name, errmsg)
}
func (h *Hypha) CanRename(u *user.User) (err error, errtitle string) {
if !u.CanProceed("rename-confirm") {
rejectRenameLog(h, u, "no rights")
return errors.New("Not enough rights to rename, you must be a trusted editor"), "Not enough rights"
}
if !h.Exists {
rejectRenameLog(h, u, "does not exist")
return errors.New("Cannot rename this hypha because it does not exist"), "Does not exist"
}
return nil, ""
}
func canRenameThisToThat(oh *Hypha, nh *Hypha, u *user.User) (err error, errtitle string) {
if nh.Exists {
rejectRenameLog(oh, u, fmt.Sprintf("name %s taken already", nh.Name))
return errors.New(fmt.Sprintf("Hypha named <a href='/hypha/%[1]s'>%[1]s</a> already exists, cannot rename", nh.Name)), "Name taken"
}
if nh.Name == "" {
rejectRenameLog(oh, u, "no new name given")
return errors.New("No new name is given"), "No name given"
}
if !HyphaPattern.MatchString(nh.Name) {
rejectRenameLog(oh, u, fmt.Sprintf("new name %s invalid", nh.Name))
return errors.New("Invalid new name. Names cannot contain characters <code>^?!:#@&gt;&lt;*|\"\\'&amp;%</code>"), "Invalid name"
}
return nil, ""
}
// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. If `recursive` is `true`, its subhyphae will be renamed the same way.
func (h *Hypha) RenameHypha(newHypha *Hypha, recursive bool, u *user.User) (hop *history.HistoryOp, errtitle string) {
h.Lock()
defer h.Unlock()
newHypha.Lock()
defer newHypha.Unlock()
hop = history.Operation(history.TypeRenameHypha)
if err, errtitle := h.CanRename(u); errtitle != "" {
hop.WithError(err)
return hop, errtitle
}
if err, errtitle := canRenameThisToThat(h, newHypha, u); errtitle != "" {
hop.WithError(err)
return hop, errtitle
}
var (
re = regexp.MustCompile(`(?i)` + h.Name)
replaceName = func(str string) string {
return re.ReplaceAllString(util.CanonicalName(str), newHypha.Name)
}
hyphaeToRename = findHyphaeToRename(h, recursive)
renameMap, err = renamingPairs(hyphaeToRename, replaceName)
renameMsg = "Rename %s to %s"
)
if err != nil {
hop.Errs = append(hop.Errs, err)
return hop, hop.FirstErrorText()
}
if recursive && len(hyphaeToRename) > 0 {
renameMsg += " recursively"
}
hop.WithFilesRenamed(renameMap).
WithMsg(fmt.Sprintf(renameMsg, h.Name, newHypha.Name)).
WithUser(u).
Apply()
if len(hop.Errs) == 0 {
for _, h := range hyphaeToRename {
h.renameTo(replaceName(h.Name))
}
}
return hop, ""
}
func findHyphaeToRename(superhypha *Hypha, recursive bool) []*Hypha {
hyphae := []*Hypha{superhypha}
if recursive {
hyphae = append(hyphae, superhypha.Subhyphae()...)
}
return hyphae
}
func renamingPairs(hyphaeToRename []*Hypha, replaceName func(string) string) (map[string]string, error) {
renameMap := make(map[string]string)
newNames := make([]string, len(hyphaeToRename))
for _, h := range hyphaeToRename {
h.RLock()
newNames = append(newNames, replaceName(h.Name))
if h.TextPath != "" {
renameMap[h.TextPath] = replaceName(h.TextPath)
}
if h.BinaryPath != "" {
renameMap[h.BinaryPath] = replaceName(h.BinaryPath)
}
h.RUnlock()
}
if firstFailure, ok := AreFreeNames(newNames...); !ok {
return nil, errors.New("Hypha " + firstFailure + " already exists")
}
return renameMap, nil
}

66
hyphae/unattach.go Normal file
View File

@ -0,0 +1,66 @@
package hyphae
import (
"errors"
"fmt"
"log"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/user"
)
func rejectUnattachLog(h *Hypha, u *user.User, errmsg string) {
log.Printf("Reject unattach %s by @%s: %s\n", h.Name, u.Name, errmsg)
}
// CanUnattach checks if given user can unattach given hypha. If they can, `errtitle` is an empty string and `err` is nil. If they cannot, `errtitle` is not an empty string, and `err` is an error.
func (h *Hypha) CanUnattach(u *user.User) (err error, errtitle string) {
if !u.CanProceed("unattach-confirm") {
rejectUnattachLog(h, u, "no rights")
return errors.New("Not enough rights to unattach, you must be a trusted editor"), "Not enough rights"
}
if !h.Exists {
rejectUnattachLog(h, u, "does not exist")
return errors.New("Cannot unattach this hypha because it does not exist"), "Does not exist"
}
if h.BinaryPath == "" {
rejectUnattachLog(h, u, "no amnt")
return errors.New("Cannot unattach this hypha because it has no attachment"), "No attachment"
}
return nil, ""
}
// UnattachHypha unattaches hypha and makes a history record about that.
func (h *Hypha) UnattachHypha(u *user.User) (hop *history.HistoryOp, errtitle string) {
h.Lock()
defer h.Unlock()
hop = history.Operation(history.TypeUnattachHypha)
if err, errtitle := h.CanUnattach(u); errtitle != "" {
hop.WithError(err)
return hop, errtitle
}
hop.
WithFilesRemoved(h.BinaryPath).
WithMsg(fmt.Sprintf("Unattach %s", h.Name)).
WithUser(u).
Apply()
if len(hop.Errs) > 0 {
rejectUnattachLog(h, u, "fail")
return hop.WithError(errors.New(fmt.Sprintf("Could not unattach this hypha due to internal server errors: <code>%v</code>", hop.Errs))), "Error"
}
if h.BinaryPath != "" {
h.BinaryPath = ""
}
// If nothing is left of the hypha
if h.TextPath == "" {
h.delete()
}
return hop, ""
}

122
hyphae/upload.go Normal file
View File

@ -0,0 +1,122 @@
package hyphae
import (
"errors"
"fmt"
"io/ioutil"
"log"
"mime/multipart"
"os"
"path/filepath"
"github.com/bouncepaw/mycorrhiza/history"
"github.com/bouncepaw/mycorrhiza/mimetype"
"github.com/bouncepaw/mycorrhiza/user"
"github.com/bouncepaw/mycorrhiza/util"
)
func rejectEditLog(h *Hypha, u *user.User, errmsg string) {
log.Printf("Reject edit %s by @%s: %s\n", h.Name, u.Name, errmsg)
}
func rejectAttachLog(h *Hypha, u *user.User, errmsg string) {
log.Printf("Reject attach %s by @%s: %s\n", h.Name, u.Name, errmsg)
}
func (h *Hypha) CanEdit(u *user.User) (err error, errtitle string) {
if !u.CanProceed("edit") {
rejectEditLog(h, u, "no rights")
return errors.New("You must be an editor to edit pages."), "Not enough rights"
}
return nil, ""
}
func (h *Hypha) CanUploadThat(data []byte, u *user.User) (err error, errtitle string) {
if len(data) == 0 {
return errors.New("No text data passed"), "Empty"
}
return nil, ""
}
func (h *Hypha) UploadText(textData []byte, u *user.User) (hop *history.HistoryOp, errtitle string) {
hop = history.Operation(history.TypeEditText)
if h.Exists {
hop.WithMsg(fmt.Sprintf("Edit %s", h.Name))
} else {
hop.WithMsg(fmt.Sprintf("Create %s", h.Name))
}
if err, errtitle := h.CanEdit(u); err != nil {
return hop.WithError(err), errtitle
}
if err, errtitle := h.CanUploadThat(textData, u); err != nil {
return hop.WithError(err), errtitle
}
return h.uploadHelp(hop, ".myco", textData, u)
}
func (h *Hypha) CanAttach(err error, u *user.User) (error, string) {
if !u.CanProceed("upload-binary") {
rejectAttachLog(h, u, "no rights")
return errors.New("You must be an editor to upload attachments."), "Not enough rights"
}
if err != nil {
rejectAttachLog(h, u, err.Error())
return errors.New("No binary data passed"), err.Error()
}
return nil, ""
}
func (h *Hypha) UploadBinary(mime string, file multipart.File, u *user.User) (*history.HistoryOp, string) {
var (
hop = history.Operation(history.TypeEditBinary).WithMsg(fmt.Sprintf("Upload binary part for %s with type %s", h.Name, mime))
data, err = ioutil.ReadAll(file)
)
if err != nil {
return hop.WithError(err), err.Error()
}
if err, errtitle := h.CanEdit(u); err != nil {
return hop.WithError(err), errtitle
}
if err, errtitle := h.CanUploadThat(data, u); err != nil {
return hop.WithError(err), errtitle
}
return h.uploadHelp(hop, mimetype.ToExtension(mime), data, u)
}
// uploadHelp is a helper function for UploadText and UploadBinary
func (h *Hypha) uploadHelp(hop *history.HistoryOp, ext string, data []byte, u *user.User) (*history.HistoryOp, string) {
var (
fullPath = filepath.Join(util.WikiDir, h.Name+ext)
originalFullPath = &h.TextPath
)
if hop.Type == history.TypeEditBinary {
originalFullPath = &h.BinaryPath
}
if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil {
return hop.WithError(err), err.Error()
}
if err := ioutil.WriteFile(fullPath, data, 0644); err != nil {
return hop.WithError(err), err.Error()
}
if h.Exists && *originalFullPath != fullPath && *originalFullPath != "" {
if err := history.Rename(*originalFullPath, fullPath); err != nil {
return hop.WithError(err), err.Error()
}
log.Println("Move", *originalFullPath, "to", fullPath)
}
h.InsertIfNew()
*originalFullPath = fullPath
if h.Exists && hop.Type == history.TypeEditText && !history.FileChanged(fullPath) {
return hop.Abort(), "No changes"
}
return hop.WithFiles(fullPath).WithUser(u).Apply(), ""
}

72
hyphae/view.go Normal file
View File

@ -0,0 +1,72 @@
package hyphae
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/bouncepaw/mycorrhiza/markup"
"github.com/bouncepaw/mycorrhiza/util"
)
// FetchTextPart tries to read text file of the given hypha. If there is no file, empty string is returned.
func (h *Hypha) FetchTextPart() (string, error) {
if h.TextPath == "" {
return "", nil
}
text, err := ioutil.ReadFile(h.TextPath)
if os.IsNotExist(err) {
return "", nil
} else if err != nil {
return "", err
}
return string(text), nil
}
// binaryHtmlBlock creates an html block for binary part of the hypha.
func (h *Hypha) BinaryHtmlBlock() string {
switch filepath.Ext(h.BinaryPath) {
case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico":
return fmt.Sprintf(`
<div class="binary-container binary-container_with-img">
<a href="/binary/%[1]s"><img src="/binary/%[1]s"/></a>
</div>`, h.Name)
case ".ogg", ".webm", ".mp4":
return fmt.Sprintf(`
<div class="binary-container binary-container_with-video">
<video>
<source src="/binary/%[1]s"/>
<p>Your browser does not support video. <a href="/binary/%[1]s">Download video</a></p>
</video>
`, h.Name)
case ".mp3":
return fmt.Sprintf(`
<div class="binary-container binary-container_with-audio">
<audio>
<source src="/binary/%[1]s"/>
<p>Your browser does not support audio. <a href="/binary/%[1]s">Download audio</a></p>
</audio>
`, h.Name)
default:
return fmt.Sprintf(`
<div class="binary-container binary-container_with-nothing">
<p><a href="/binary/%s">Download media</a></p>
</div>
`, h.Name)
}
}
func SetHeaderLinks() {
if userLinksHypha := ByName(util.HeaderLinksHypha); !userLinksHypha.Exists {
util.SetDefaultHeaderLinks()
} else {
contents, err := ioutil.ReadFile(userLinksHypha.TextPath)
if err != nil || len(contents) == 0 {
util.SetDefaultHeaderLinks()
} else {
text := string(contents)
util.ParseHeaderLinks(text, markup.Rocketlink)
}
}
}

36
main.go
View File

@ -10,7 +10,6 @@ import (
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/bouncepaw/mycorrhiza/history"
@ -24,19 +23,6 @@ import (
// WikiDir is a rooted path to the wiki storage directory.
var WikiDir string
// HyphaPattern is a pattern which all hyphae must match.
var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%{}]+`)
// HyphaStorage is a mapping between canonical hypha names and their meta information.
var HyphaStorage = make(map[string]*HyphaData)
// IterateHyphaNamesWith is a closure to be passed to subpackages to let them iterate all hypha names read-only.
func IterateHyphaNamesWith(f func(string)) {
for hyphaName := range HyphaStorage {
f(hyphaName)
}
}
// HttpErr is used by many handlers to signal errors in a compact way.
func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) {
log.Println(errMsg, "for", name)
@ -64,8 +50,8 @@ func handlerList(w http.ResponseWriter, rq *http.Request) {
pageCount = hyphae.Count()
u = user.FromRequest(rq)
)
for hyphaName, data := range HyphaStorage {
tbody += templates.HyphaListRowHTML(hyphaName, mimetype.FromExtension(filepath.Ext(data.BinaryPath)), data.BinaryPath != "")
for h := range hyphae.YieldExistingHyphae() {
tbody += templates.HyphaListRowHTML(h.Name, mimetype.FromExtension(filepath.Ext(h.BinaryPath)), h.BinaryPath != "")
}
util.HTTP200Page(w, base("List of pages", templates.HyphaListHTML(tbody, pageCount), u))
}
@ -82,10 +68,9 @@ func handlerReindex(w http.ResponseWriter, rq *http.Request) {
return
}
hyphae.ResetCount()
HyphaStorage = make(map[string]*HyphaData)
log.Println("Wiki storage directory is", WikiDir)
log.Println("Start indexing hyphae...")
Index(WikiDir)
hyphae.Index(WikiDir)
log.Println("Indexed", hyphae.Count(), "hyphae")
http.Redirect(w, rq, "/", http.StatusSeeOther)
}
@ -98,7 +83,7 @@ func handlerUpdateHeaderLinks(w http.ResponseWriter, rq *http.Request) {
log.Println("Rejected", rq.URL)
return
}
setHeaderLinks()
hyphae.SetHeaderLinks()
http.Redirect(w, rq, "/", http.StatusSeeOther)
}
@ -107,14 +92,13 @@ func handlerRandom(w http.ResponseWriter, rq *http.Request) {
log.Println(rq.URL)
var randomHyphaName string
i := rand.Intn(hyphae.Count())
for hyphaName := range HyphaStorage {
for h := range hyphae.YieldExistingHyphae() {
if i == 0 {
randomHyphaName = hyphaName
break
randomHyphaName = h.Name
}
i--
}
http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther)
http.Redirect(w, rq, "/hypha/"+randomHyphaName, http.StatusSeeOther)
}
func handlerStyle(w http.ResponseWriter, rq *http.Request) {
@ -187,11 +171,11 @@ func main() {
log.Fatal(err)
}
log.Println("Wiki storage directory is", WikiDir)
Index(WikiDir)
hyphae.Index(WikiDir)
log.Println("Indexed", hyphae.Count(), "hyphae")
history.Start(WikiDir)
setHeaderLinks()
hyphae.SetHeaderLinks()
go handleGemini()
@ -212,7 +196,7 @@ func main() {
http.HandleFunc("/static/common.css", handlerStyle)
http.HandleFunc("/static/icon/", handlerIcon)
http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) {
http.Redirect(w, rq, "/page/"+util.HomePage, http.StatusSeeOther)
http.Redirect(w, rq, "/hypha/"+util.HomePage, http.StatusSeeOther)
})
http.HandleFunc("/robots.txt", handlerRobotsTxt)
log.Fatal(http.ListenAndServe("0.0.0.0:"+util.ServerPort, nil))

14
name.go
View File

@ -11,23 +11,13 @@ import (
"github.com/bouncepaw/mycorrhiza/util"
)
// isCanonicalName checks if the `name` is canonical.
func isCanonicalName(name string) bool {
return HyphaPattern.MatchString(name)
}
// CanonicalName makes sure the `name` is canonical. A name is canonical if it is lowercase and all spaces are replaced with underscores.
func CanonicalName(name string) string {
return strings.ToLower(strings.ReplaceAll(name, " ", "_"))
}
// naviTitle turns `canonicalName` into html string with each hypha path parts higlighted as links.
// TODO: rework as a template
func naviTitle(canonicalName string) string {
var (
html = fmt.Sprintf(`<h1 class="navi-title" id="navi-title">
<a href="/page/%s">%s</a><span aria-hidden="true" class="navi-title__colon">:</span>`, util.HomePage, util.SiteNavIcon)
prevAcc = `/page/`
<a href="/hypha/%s">%s</a><span aria-hidden="true" class="navi-title__colon">:</span>`, util.HomePage, util.SiteNavIcon)
prevAcc = `/hypha/`
parts = strings.Split(canonicalName, "/")
rel = "up"
)

View File

@ -6,6 +6,7 @@ import (
"sort"
"strings"
"github.com/bouncepaw/mycorrhiza/hyphae"
"github.com/bouncepaw/mycorrhiza/util"
)
@ -81,21 +82,21 @@ func mainFamilyFromPool(hyphaName string, subhyphaePool map[string]bool) *mainFa
}
// Tree generates a tree for `hyphaName` as html and returns next and previous hyphae if any.
func Tree(hyphaName string, hyphaIterator func(func(string))) (html, prev, next string) {
func Tree(hyphaName string) (html, prev, next string) {
var (
// One of the siblings is the hypha with name `hyphaName`
siblings = findSiblings(hyphaName, hyphaIterator)
siblings = findSiblings(hyphaName)
subhyphaePool = make(map[string]bool)
I int
)
hyphaIterator(func(otherHyphaName string) {
for h := range hyphae.YieldExistingHyphae() {
for _, s := range siblings {
s.checkThisChild(otherHyphaName)
s.checkThisChild(h.Name)
}
if strings.HasPrefix(otherHyphaName, hyphaName+"/") {
subhyphaePool[otherHyphaName] = true
if strings.HasPrefix(h.Name, hyphaName+"/") {
subhyphaePool[h.Name] = true
}
})
}
for i, s := range siblings {
if s.name == hyphaName {
I = i
@ -116,13 +117,13 @@ func Tree(hyphaName string, hyphaIterator func(func(string))) (html, prev, next
return fmt.Sprintf(`<ul class="navitree">%s</ul>`, html), prev, next
}
func findSiblings(hyphaName string, hyphaIterator func(func(string))) []*sibling {
func findSiblings(hyphaName string) []*sibling {
siblings := []*sibling{&sibling{name: hyphaName, hasChildren: true}}
hyphaIterator(func(otherHyphaName string) {
if path.Dir(hyphaName) == path.Dir(otherHyphaName) && hyphaName != otherHyphaName {
siblings = append(siblings, &sibling{name: otherHyphaName, hasChildren: false})
for h := range hyphae.YieldExistingHyphae() {
if path.Dir(hyphaName) == path.Dir(h.Name) && hyphaName != h.Name {
siblings = append(siblings, &sibling{name: h.Name, hasChildren: false})
}
})
}
sort.Slice(siblings, func(i, j int) bool {
return siblings[i].name < siblings[j].name
})

View File

@ -41,17 +41,6 @@ func HTTP200Page(w http.ResponseWriter, page string) {
w.Write([]byte(page))
}
// FindSubhyphae finds names of existing hyphae given the `hyphaIterator`.
func FindSubhyphae(hyphaName string, hyphaIterator func(func(string))) []string {
subhyphae := make([]string, 0)
hyphaIterator(func(otherHyphaName string) {
if strings.HasPrefix(otherHyphaName, hyphaName+"/") {
subhyphae = append(subhyphae, otherHyphaName)
}
})
return subhyphae
}
func RandomString(n int) (string, error) {
bytes := make([]byte, n)
if _, err := rand.Read(bytes); err != nil {