mirror of
				https://github.com/osmarks/mycorrhiza.git
				synced 2025-10-30 23:23:04 +00:00 
			
		
		
		
	Start the Great Refactoring
This commit is contained in:
		
							
								
								
									
										6
									
								
								flag.go
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								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": | ||||
|   | ||||
| @@ -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" | ||||
| ) | ||||
| @@ -29,12 +30,12 @@ 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 != "" | ||||
| 		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() | ||||
|   | ||||
| @@ -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() | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	) | ||||
|   | ||||
							
								
								
									
										303
									
								
								http_mutators.go
									
									
									
									
									
								
							
							
						
						
									
										303
									
								
								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) { | ||||
| 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, "unattach-ask") | ||||
| 		hd, isOld = HyphaStorage[hyphaName] | ||||
| 		hasAmnt   = hd != nil && hd.BinaryPath != "" | ||||
| 			hyphaName = HyphaNameFromRq(rq, actionPath) | ||||
| 			h         = hyphae.ByName(hyphaName) | ||||
| 			u         = user.FromRequest(rq) | ||||
| 		) | ||||
| 	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) | ||||
| 		if err, errtitle := asker(h, u); err != nil { | ||||
| 			HttpErr( | ||||
| 				w, | ||||
| 				http.StatusInternalServerError, | ||||
| 				hyphaName, | ||||
| 				errtitle, | ||||
| 				err.Error()) | ||||
| 			return | ||||
| 		} | ||||
| 	util.HTTP200Page(w, base("Unattach "+hyphaName+"?", templates.UnattachAskHTML(rq, hyphaName, isOld), user.FromRequest(rq))) | ||||
| 		util.HTTP200Page( | ||||
| 			w, | ||||
| 			base( | ||||
| 				fmt.Sprintf(succTitleTemplate, hyphaName), | ||||
| 				succPageTemplate(rq, hyphaName, h.Exists), | ||||
| 				u)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func handlerUnattachConfirm(w http.ResponseWriter, rq *http.Request) { | ||||
| 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") | ||||
| 		hyphaData, isOld = HyphaStorage[hyphaName] | ||||
| 		hasAmnt          = hyphaData != nil && hyphaData.BinaryPath != "" | ||||
| 			h         = hyphae.ByName(hyphaName) | ||||
| 			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 { | ||||
| 		if hop, errtitle := confirmer(h, u, rq); hop.HasErrors() { | ||||
| 			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)) | ||||
| 				errtitle, | ||||
| 				hop.FirstErrorText()) | ||||
| 			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 | ||||
| 		http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther) | ||||
| 	} | ||||
| 	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 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 ( | ||||
| 		hyphaName        = HyphaNameFromRq(rq, "rename-confirm") | ||||
| 		_, isOld         = HyphaStorage[hyphaName] | ||||
| 		newName          = CanonicalName(rq.PostFormValue("new-name")) | ||||
| 		_, newNameIsUsed = HyphaStorage[newName] | ||||
| 			newName   = util.CanonicalName(rq.PostFormValue("new-name")) | ||||
| 			recursive = rq.PostFormValue("recursive") == "true" | ||||
| 		u                = user.FromRequest(rq) | ||||
| 			newHypha  = hyphae.ByName(newName) | ||||
| 		) | ||||
| 	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>^?!:#@><*|\"\\'&%</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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // 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 | ||||
| 	} | ||||
| 	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) | ||||
| } | ||||
| 		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] | ||||
| 		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) | ||||
| 	rq.ParseMultipartForm(10 << 20) // Set upload limit | ||||
| 	var ( | ||||
| 		hyphaName          = HyphaNameFromRq(rq, "upload-binary") | ||||
| 		h                  = hyphae.ByName(hyphaName) | ||||
| 		u                  = user.FromRequest(rq) | ||||
| 		file, handler, err = rq.FormFile("binary") | ||||
| 	) | ||||
| 	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() | ||||
| 	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) | ||||
| 		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) | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -83,25 +84,25 @@ 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 != "" | ||||
| 		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
									
									
									
									
									
								
							
							
						
						
									
										326
									
								
								hypha.go
									
									
									
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										52
									
								
								hyphae/delete.go
									
									
									
									
									
										Normal 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, "" | ||||
| } | ||||
| @@ -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 | ||||
| 		} | ||||
|  | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										166
									
								
								hyphae/hyphae.go
									
									
									
									
									
								
							
							
						
						
									
										166
									
								
								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 | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										122
									
								
								hyphae/rename.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								hyphae/rename.go
									
									
									
									
									
										Normal 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>^?!:#@><*|\"\\'&%</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
									
								
							
							
						
						
									
										66
									
								
								hyphae/unattach.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										122
									
								
								hyphae/upload.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										72
									
								
								hyphae/view.go
									
									
									
									
									
										Normal 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
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								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)) | ||||
|   | ||||
							
								
								
									
										14
									
								
								name.go
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								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(`<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" | ||||
| 	) | ||||
|   | ||||
							
								
								
									
										25
									
								
								tree/tree.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								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(h.Name, hyphaName+"/") { | ||||
| 			subhyphaePool[h.Name] = true | ||||
| 		} | ||||
| 		if strings.HasPrefix(otherHyphaName, hyphaName+"/") { | ||||
| 			subhyphaePool[otherHyphaName] = 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 | ||||
| 	}) | ||||
|   | ||||
							
								
								
									
										11
									
								
								util/util.go
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								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 { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 bouncepaw
					bouncepaw