diff --git a/flag.go b/flag.go index 68e0c42..77a7d05 100644 --- a/flag.go +++ b/flag.go @@ -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": diff --git a/gemini.go b/gemini.go index 3ed4ceb..b86c3a4 100644 --- a/gemini.go +++ b/gemini.go @@ -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() diff --git a/history/operations.go b/history/operations.go index a8177ba..b815c28 100644 --- a/history/operations.go +++ b/history/operations.go @@ -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() +} diff --git a/http_auth.go b/http_auth.go index cfe0799..a9ba5de 100644 --- a/http_auth.go +++ b/http_auth.go @@ -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) ) diff --git a/http_mutators.go b/http_mutators.go index ea37b9c..e4d6bdb 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -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: %v", 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 %s 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 ^?!:#@><*|\"\\'&%") - 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: %v", 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: %v", 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 = `

You are creating a new hypha.

` + warning = `

You are creating a new hypha.

` } - 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) } diff --git a/http_readers.go b/http_readers.go index 08a5057..0146c52 100644 --- a/http_readers.go +++ b/http_readers.go @@ -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(`

This hypha had no text at this revision.

`) 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), diff --git a/hypha.go b/hypha.go deleted file mode 100644 index b1437b6..0000000 --- a/hypha.go +++ /dev/null @@ -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(` -
- -
`, hyphaName) - case ".ogg", ".webm", ".mp4": - return fmt.Sprintf(` -
- - `, hyphaName) - case ".mp3": - return fmt.Sprintf(` -
- - `, hyphaName) - default: - return fmt.Sprintf(` -
-

This hypha's media cannot be rendered. Download it

-
- `, 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() - } - } - -} diff --git a/hyphae/delete.go b/hyphae/delete.go new file mode 100644 index 0000000..b5c7939 --- /dev/null +++ b/hyphae/delete.go @@ -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, "" +} diff --git a/hyphae/files.go b/hyphae/files.go index 4c97a78..7a55090 100644 --- a/hyphae/files.go +++ b/hyphae/files.go @@ -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 } - } } diff --git a/hyphae/hyphae.go b/hyphae/hyphae.go index 968d1e6..6fa5853 100644 --- a/hyphae/hyphae.go +++ b/hyphae/hyphae.go @@ -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 + } +} diff --git a/hyphae/rename.go b/hyphae/rename.go new file mode 100644 index 0000000..e0f7f1c --- /dev/null +++ b/hyphae/rename.go @@ -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 %[1]s 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 ^?!:#@><*|\"\\'&%"), "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 +} diff --git a/hyphae/unattach.go b/hyphae/unattach.go new file mode 100644 index 0000000..dab8dce --- /dev/null +++ b/hyphae/unattach.go @@ -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: %v", hop.Errs))), "Error" + } + + if h.BinaryPath != "" { + h.BinaryPath = "" + } + // If nothing is left of the hypha + if h.TextPath == "" { + h.delete() + } + return hop, "" +} diff --git a/hyphae/upload.go b/hyphae/upload.go new file mode 100644 index 0000000..c66d942 --- /dev/null +++ b/hyphae/upload.go @@ -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(), "" +} diff --git a/hyphae/view.go b/hyphae/view.go new file mode 100644 index 0000000..e53ab4f --- /dev/null +++ b/hyphae/view.go @@ -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(` +
+ +
`, h.Name) + case ".ogg", ".webm", ".mp4": + return fmt.Sprintf(` +
+ + `, h.Name) + case ".mp3": + return fmt.Sprintf(` +
+ + `, h.Name) + default: + return fmt.Sprintf(` + + `, 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) + } + } +} diff --git a/main.go b/main.go index d35420e..00e5346 100644 --- a/main.go +++ b/main.go @@ -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)) diff --git a/name.go b/name.go index b3a296b..e81a25b 100644 --- a/name.go +++ b/name.go @@ -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(`

- %s`, util.HomePage, util.SiteNavIcon) - prevAcc = `/page/` + %s`, util.HomePage, util.SiteNavIcon) + prevAcc = `/hypha/` parts = strings.Split(canonicalName, "/") rel = "up" ) diff --git a/tree/tree.go b/tree/tree.go index 45046db..1c6ccb3 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -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(``, 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 }) diff --git a/util/util.go b/util/util.go index 108fa05..d7b90a4 100644 --- a/util/util.go +++ b/util/util.go @@ -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 {