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.URL = "http://0.0.0.0:" + util.ServerPort | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	util.HomePage = CanonicalName(util.HomePage) | 	util.HomePage = util.CanonicalName(util.HomePage) | ||||||
| 	util.UserHypha = CanonicalName(util.UserHypha) | 	util.UserHypha = util.CanonicalName(util.UserHypha) | ||||||
| 	util.HeaderLinksHypha = CanonicalName(util.HeaderLinksHypha) | 	util.HeaderLinksHypha = util.CanonicalName(util.HeaderLinksHypha) | ||||||
|  |  | ||||||
| 	switch util.AuthMethod { | 	switch util.AuthMethod { | ||||||
| 	case "none": | 	case "none": | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								gemini.go
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								gemini.go
									
									
									
									
									
								
							| @@ -11,6 +11,7 @@ import ( | |||||||
| 	"git.sr.ht/~adnano/go-gemini" | 	"git.sr.ht/~adnano/go-gemini" | ||||||
| 	"git.sr.ht/~adnano/go-gemini/certificate" | 	"git.sr.ht/~adnano/go-gemini/certificate" | ||||||
|  |  | ||||||
|  | 	"github.com/bouncepaw/mycorrhiza/hyphae" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/markup" | 	"github.com/bouncepaw/mycorrhiza/markup" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/util" | 	"github.com/bouncepaw/mycorrhiza/util" | ||||||
| ) | ) | ||||||
| @@ -28,13 +29,13 @@ Visit home hypha: | |||||||
| func geminiHypha(w *gemini.ResponseWriter, rq *gemini.Request) { | func geminiHypha(w *gemini.ResponseWriter, rq *gemini.Request) { | ||||||
| 	log.Println(rq.URL) | 	log.Println(rq.URL) | ||||||
| 	var ( | 	var ( | ||||||
| 		hyphaName         = geminiHyphaNameFromRq(rq, "page", "hypha") | 		hyphaName = geminiHyphaNameFromRq(rq, "page", "hypha") | ||||||
| 		data, hyphaExists = HyphaStorage[hyphaName] | 		h         = hyphae.ByName(hyphaName) | ||||||
| 		hasAmnt           = hyphaExists && data.BinaryPath != "" | 		hasAmnt   = h.Exists && h.BinaryPath != "" | ||||||
| 		contents          string | 		contents  string | ||||||
| 	) | 	) | ||||||
| 	if hyphaExists { | 	if h.Exists { | ||||||
| 		fileContentsT, errT := ioutil.ReadFile(data.TextPath) | 		fileContentsT, errT := ioutil.ReadFile(h.TextPath) | ||||||
| 		if errT == nil { | 		if errT == nil { | ||||||
| 			md := markup.Doc(hyphaName, string(fileContentsT)) | 			md := markup.Doc(hyphaName, string(fileContentsT)) | ||||||
| 			contents = md.AsGemtext() | 			contents = md.AsGemtext() | ||||||
|   | |||||||
| @@ -134,3 +134,11 @@ func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp { | |||||||
| 	} | 	} | ||||||
| 	return hop | 	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/templates" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/user" | 	"github.com/bouncepaw/mycorrhiza/user" | ||||||
|  | 	"github.com/bouncepaw/mycorrhiza/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func init() { | func init() { | ||||||
| @@ -39,7 +40,7 @@ func handlerLogoutConfirm(w http.ResponseWriter, rq *http.Request) { | |||||||
| func handlerLoginData(w http.ResponseWriter, rq *http.Request) { | func handlerLoginData(w http.ResponseWriter, rq *http.Request) { | ||||||
| 	log.Println(rq.URL) | 	log.Println(rq.URL) | ||||||
| 	var ( | 	var ( | ||||||
| 		username = CanonicalName(rq.PostFormValue("username")) | 		username = util.CanonicalName(rq.PostFormValue("username")) | ||||||
| 		password = rq.PostFormValue("password") | 		password = rq.PostFormValue("password") | ||||||
| 		err      = user.LoginDataHTTP(w, rq, username, password) | 		err      = user.LoginDataHTTP(w, rq, username, password) | ||||||
| 	) | 	) | ||||||
|   | |||||||
							
								
								
									
										349
									
								
								http_mutators.go
									
									
									
									
									
								
							
							
						
						
									
										349
									
								
								http_mutators.go
									
									
									
									
									
								
							| @@ -5,6 +5,8 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  |  | ||||||
|  | 	"github.com/bouncepaw/mycorrhiza/history" | ||||||
|  | 	"github.com/bouncepaw/mycorrhiza/hyphae" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/markup" | 	"github.com/bouncepaw/mycorrhiza/markup" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/templates" | 	"github.com/bouncepaw/mycorrhiza/templates" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/user" | 	"github.com/bouncepaw/mycorrhiza/user" | ||||||
| @@ -25,182 +27,146 @@ func init() { | |||||||
| 	http.HandleFunc("/unattach-confirm/", handlerUnattachConfirm) | 	http.HandleFunc("/unattach-confirm/", handlerUnattachConfirm) | ||||||
| } | } | ||||||
|  |  | ||||||
| func handlerUnattachAsk(w http.ResponseWriter, rq *http.Request) { | func factoryHandlerAsker( | ||||||
| 	log.Println(rq.URL) | 	actionPath string, | ||||||
| 	var ( | 	asker func(*hyphae.Hypha, *user.User) (error, string), | ||||||
| 		hyphaName = HyphaNameFromRq(rq, "unattach-ask") | 	succTitleTemplate string, | ||||||
| 		hd, isOld = HyphaStorage[hyphaName] | 	succPageTemplate func(*http.Request, string, bool) string, | ||||||
| 		hasAmnt   = hd != nil && hd.BinaryPath != "" | ) func(http.ResponseWriter, *http.Request) { | ||||||
| 	) | 	return func(w http.ResponseWriter, rq *http.Request) { | ||||||
| 	if !hasAmnt { | 		log.Println(rq.URL) | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach") | 		var ( | ||||||
| 		log.Println("Rejected (no amnt):", rq.URL) | 			hyphaName = HyphaNameFromRq(rq, actionPath) | ||||||
| 		return | 			h         = hyphae.ByName(hyphaName) | ||||||
| 	} else if ok := user.CanProceed(rq, "unattach-confirm"); !ok { | 			u         = user.FromRequest(rq) | ||||||
| 		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 { | ||||||
| 		return | 			HttpErr( | ||||||
| 	} | 				w, | ||||||
| 	util.HTTP200Page(w, base("Unattach "+hyphaName+"?", templates.UnattachAskHTML(rq, hyphaName, isOld), user.FromRequest(rq))) | 				http.StatusInternalServerError, | ||||||
| } | 				hyphaName, | ||||||
|  | 				errtitle, | ||||||
| func handlerUnattachConfirm(w http.ResponseWriter, rq *http.Request) { | 				err.Error()) | ||||||
| 	log.Println(rq.URL) | 			return | ||||||
| 	var ( |  | ||||||
| 		hyphaName        = HyphaNameFromRq(rq, "unattach-confirm") |  | ||||||
| 		hyphaData, isOld = HyphaStorage[hyphaName] |  | ||||||
| 		hasAmnt          = hyphaData != nil && hyphaData.BinaryPath != "" |  | ||||||
| 		u                = user.FromRequest(rq) |  | ||||||
| 	) |  | ||||||
| 	if !u.CanProceed("unattach-confirm") { |  | ||||||
| 		HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to unattach attachments") |  | ||||||
| 		log.Println("Rejected (no rights):", rq.URL) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if !hasAmnt { |  | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Cannot unattach", "No attachment attached yet, therefore you cannot unattach") |  | ||||||
| 		log.Println("Rejected (no amnt):", rq.URL) |  | ||||||
| 		return |  | ||||||
| 	} else if !isOld { |  | ||||||
| 		// The precondition is to have the hypha in the first place. |  | ||||||
| 		HttpErr(w, http.StatusPreconditionFailed, hyphaName, |  | ||||||
| 			"Error: no such hypha", |  | ||||||
| 			"Could not unattach this hypha because it does not exist") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if hop := hyphaData.UnattachHypha(hyphaName, u); len(hop.Errs) != 0 { |  | ||||||
| 		HttpErr(w, http.StatusInternalServerError, hyphaName, |  | ||||||
| 			"Error: could not unattach hypha", |  | ||||||
| 			fmt.Sprintf("Could not unattach this hypha due to internal errors. Server errors: <code>%v</code>", hop.Errs)) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) { |  | ||||||
| 	log.Println(rq.URL) |  | ||||||
| 	var ( |  | ||||||
| 		hyphaName = HyphaNameFromRq(rq, "rename-ask") |  | ||||||
| 		_, isOld  = HyphaStorage[hyphaName] |  | ||||||
| 		u         = user.FromRequest(rq) |  | ||||||
| 	) |  | ||||||
| 	if !u.CanProceed("rename-confirm") { |  | ||||||
| 		HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.") |  | ||||||
| 		log.Println("Rejected", rq.URL) |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(rq, hyphaName, isOld), u)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) { |  | ||||||
| 	log.Println(rq.URL) |  | ||||||
| 	var ( |  | ||||||
| 		hyphaName        = HyphaNameFromRq(rq, "rename-confirm") |  | ||||||
| 		_, isOld         = HyphaStorage[hyphaName] |  | ||||||
| 		newName          = CanonicalName(rq.PostFormValue("new-name")) |  | ||||||
| 		_, newNameIsUsed = HyphaStorage[newName] |  | ||||||
| 		recursive        = rq.PostFormValue("recursive") == "true" |  | ||||||
| 		u                = user.FromRequest(rq) |  | ||||||
| 	) |  | ||||||
| 	switch { |  | ||||||
| 	case !u.CanProceed("rename-confirm"): |  | ||||||
| 		HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a trusted editor to rename pages.") |  | ||||||
| 		log.Println("Rejected", rq.URL) |  | ||||||
| 	case newNameIsUsed: |  | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Error: hypha exists", |  | ||||||
| 			fmt.Sprintf("Hypha named <a href='/page/%s'>%s</a> already exists.", hyphaName, hyphaName)) |  | ||||||
| 	case newName == "": |  | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no name", |  | ||||||
| 			"No new name is given.") |  | ||||||
| 	case !isOld: |  | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no such hypha", |  | ||||||
| 			"Cannot rename a hypha that does not exist yet.") |  | ||||||
| 	case !HyphaPattern.MatchString(newName): |  | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Error: invalid name", |  | ||||||
| 			"Invalid new name. Names cannot contain characters <code>^?!:#@><*|\"\\'&%</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) |  | ||||||
| 		} | 		} | ||||||
|  | 		util.HTTP200Page( | ||||||
|  | 			w, | ||||||
|  | 			base( | ||||||
|  | 				fmt.Sprintf(succTitleTemplate, hyphaName), | ||||||
|  | 				succPageTemplate(rq, hyphaName, h.Exists), | ||||||
|  | 				u)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // handlerDeleteAsk shows a delete dialog. | var handlerUnattachAsk = factoryHandlerAsker( | ||||||
| func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) { | 	"unattach-ask", | ||||||
| 	log.Println(rq.URL) | 	func(h *hyphae.Hypha, u *user.User) (error, string) { | ||||||
| 	var ( | 		return h.CanUnattach(u) | ||||||
| 		hyphaName = HyphaNameFromRq(rq, "delete-ask") | 	}, | ||||||
| 		_, isOld  = HyphaStorage[hyphaName] | 	"Unattach %s?", | ||||||
| 		u         = user.FromRequest(rq) | 	templates.UnattachAskHTML, | ||||||
| 	) | ) | ||||||
| 	if !u.CanProceed("delete-ask") { |  | ||||||
| 		HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.") | var handlerDeleteAsk = factoryHandlerAsker( | ||||||
| 		log.Println("Rejected", rq.URL) | 	"delete-ask", | ||||||
| 		return | 	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 | var handlerUnattachConfirm = factoryHandlerConfirmer( | ||||||
| func handlerDeleteConfirm(w http.ResponseWriter, rq *http.Request) { | 	"unattach-confirm", | ||||||
| 	log.Println(rq.URL) | 	func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) { | ||||||
| 	var ( | 		return h.UnattachHypha(u) | ||||||
| 		hyphaName        = HyphaNameFromRq(rq, "delete-confirm") | 	}, | ||||||
| 		hyphaData, isOld = HyphaStorage[hyphaName] | ) | ||||||
| 		u                = user.FromRequest(rq) |  | ||||||
| 	) | var handlerDeleteConfirm = factoryHandlerConfirmer( | ||||||
| 	if !u.CanProceed("delete-confirm") { | 	"delete-confirm", | ||||||
| 		HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be a moderator to delete pages.") | 	func(h *hyphae.Hypha, u *user.User, _ *http.Request) (*history.HistoryOp, string) { | ||||||
| 		log.Println("Rejected", rq.URL) | 		return h.DeleteHypha(u) | ||||||
| 		return | 	}, | ||||||
| 	} | ) | ||||||
| 	if !isOld { |  | ||||||
| 		// The precondition is to have the hypha in the first place. | var handlerRenameConfirm = factoryHandlerConfirmer( | ||||||
| 		HttpErr(w, http.StatusPreconditionFailed, hyphaName, | 	"rename-confirm", | ||||||
| 			"Error: no such hypha", | 	func(oldHypha *hyphae.Hypha, u *user.User, rq *http.Request) (*history.HistoryOp, string) { | ||||||
| 			"Could not delete this hypha because it does not exist.") | 		var ( | ||||||
| 		return | 			newName   = util.CanonicalName(rq.PostFormValue("new-name")) | ||||||
| 	} | 			recursive = rq.PostFormValue("recursive") == "true" | ||||||
| 	if hop := hyphaData.DeleteHypha(hyphaName, u); len(hop.Errs) != 0 { | 			newHypha  = hyphae.ByName(newName) | ||||||
| 		HttpErr(w, http.StatusInternalServerError, hyphaName, | 		) | ||||||
| 			"Error: could not delete hypha", | 		return oldHypha.RenameHypha(newHypha, recursive, u) | ||||||
| 			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) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // handlerEdit shows the edit form. It doesn't edit anything actually. | // handlerEdit shows the edit form. It doesn't edit anything actually. | ||||||
| func handlerEdit(w http.ResponseWriter, rq *http.Request) { | func handlerEdit(w http.ResponseWriter, rq *http.Request) { | ||||||
| 	log.Println(rq.URL) | 	log.Println(rq.URL) | ||||||
| 	var ( | 	var ( | ||||||
| 		hyphaName        = HyphaNameFromRq(rq, "edit") | 		hyphaName    = HyphaNameFromRq(rq, "edit") | ||||||
| 		hyphaData, isOld = HyphaStorage[hyphaName] | 		h            = hyphae.ByName(hyphaName) | ||||||
| 		warning          string | 		warning      string | ||||||
| 		textAreaFill     string | 		textAreaFill string | ||||||
| 		err              error | 		err          error | ||||||
| 		u                = user.FromRequest(rq) | 		u            = user.FromRequest(rq) | ||||||
| 	) | 	) | ||||||
| 	if !u.CanProceed("edit") { | 	if err, errtitle := h.CanEdit(u); err != nil { | ||||||
| 		HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.") | 		HttpErr(w, http.StatusInternalServerError, hyphaName, | ||||||
| 		log.Println("Rejected", rq.URL) | 			errtitle, | ||||||
|  | 			err.Error()) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if isOld { | 	if h.Exists { | ||||||
| 		textAreaFill, err = FetchTextPart(hyphaData) | 		textAreaFill, err = h.FetchTextPart() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Println(err) | 			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 | 			return | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} 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. | // 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) | 	log.Println(rq.URL) | ||||||
| 	var ( | 	var ( | ||||||
| 		hyphaName = HyphaNameFromRq(rq, "upload-text") | 		hyphaName = HyphaNameFromRq(rq, "upload-text") | ||||||
|  | 		h         = hyphae.ByName(hyphaName) | ||||||
| 		textData  = rq.PostFormValue("text") | 		textData  = rq.PostFormValue("text") | ||||||
| 		action    = rq.PostFormValue("action") | 		action    = rq.PostFormValue("action") | ||||||
| 		u         = user.FromRequest(rq) | 		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.") | 	if action != "Preview" { | ||||||
| 		log.Println("Rejected", rq.URL) | 		hop, errtitle = h.UploadText([]byte(textData), u) | ||||||
| 		return | 	} | ||||||
| 	} |  | ||||||
| 	if textData == "" { | 	if hop.HasErrors() { | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed") | 		HttpErr(w, http.StatusForbidden, hyphaName, | ||||||
|  | 			errtitle, | ||||||
|  | 			hop.FirstErrorText()) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if action == "Preview" { | 	if action == "Preview" { | ||||||
| 		util.HTTP200Page(w, base("Preview "+hyphaName, templates.PreviewHTML(rq, hyphaName, textData, "", markup.Doc(hyphaName, textData).AsHTML()), u)) | 		util.HTTP200Page( | ||||||
| 	} else if hop := UploadText(hyphaName, textData, u); len(hop.Errs) != 0 { | 			w, | ||||||
| 		HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) | 			base( | ||||||
|  | 				"Preview "+hyphaName, | ||||||
|  | 				templates.PreviewHTML( | ||||||
|  | 					rq, | ||||||
|  | 					hyphaName, | ||||||
|  | 					textData, | ||||||
|  | 					"", | ||||||
|  | 					markup.Doc(hyphaName, textData).AsHTML()), | ||||||
|  | 				u)) | ||||||
| 	} else { | 	} 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. | // handlerUploadBinary uploads a new binary part for the hypha. | ||||||
| func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { | func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { | ||||||
| 	log.Println(rq.URL) | 	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 | 	rq.ParseMultipartForm(10 << 20) // Set upload limit | ||||||
| 	file, handler, err := rq.FormFile("binary") | 	var ( | ||||||
| 	if file != nil { | 		hyphaName          = HyphaNameFromRq(rq, "upload-binary") | ||||||
| 		defer file.Close() | 		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 file is not passed: | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No binary data passed") |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If file is passed: | 	// If file is passed: | ||||||
|  | 	if file != nil { | ||||||
|  | 		defer file.Close() | ||||||
|  | 	} | ||||||
| 	var ( | 	var ( | ||||||
| 		mime = handler.Header.Get("Content-Type") | 		mime          = handler.Header.Get("Content-Type") | ||||||
| 		hop  = UploadBinary(hyphaName, mime, file, u) | 		hop, errtitle = h.UploadBinary(mime, file, u) | ||||||
| 	) | 	) | ||||||
|  |  | ||||||
| 	if len(hop.Errs) != 0 { | 	if hop.HasErrors() { | ||||||
| 		HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) | 		HttpErr(w, http.StatusInternalServerError, hyphaName, errtitle, hop.FirstErrorText()) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) | 	http.Redirect(w, rq, "/hypha/"+hyphaName, http.StatusSeeOther) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/bouncepaw/mycorrhiza/history" | 	"github.com/bouncepaw/mycorrhiza/history" | ||||||
|  | 	"github.com/bouncepaw/mycorrhiza/hyphae" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/markup" | 	"github.com/bouncepaw/mycorrhiza/markup" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/mimetype" | 	"github.com/bouncepaw/mycorrhiza/mimetype" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/templates" | 	"github.com/bouncepaw/mycorrhiza/templates" | ||||||
| @@ -33,7 +34,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { | |||||||
| 		shorterUrl        = strings.TrimPrefix(rq.URL.Path, "/rev/") | 		shorterUrl        = strings.TrimPrefix(rq.URL.Path, "/rev/") | ||||||
| 		firstSlashIndex   = strings.IndexRune(shorterUrl, '/') | 		firstSlashIndex   = strings.IndexRune(shorterUrl, '/') | ||||||
| 		revHash           = shorterUrl[:firstSlashIndex] | 		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>`) | 		contents          = fmt.Sprintf(`<p>This hypha had no text at this revision.</p>`) | ||||||
| 		TextPath          = hyphaName + ".myco" | 		TextPath          = hyphaName + ".myco" | ||||||
| 		textContents, err = history.FileAtRevision(TextPath, revHash) | 		textContents, err = history.FileAtRevision(TextPath, revHash) | ||||||
| @@ -42,7 +43,7 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { | |||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		contents = markup.Doc(hyphaName, textContents).AsHTML() | 		contents = markup.Doc(hyphaName, textContents).AsHTML() | ||||||
| 	} | 	} | ||||||
| 	treeHTML, _, _ := tree.Tree(hyphaName, IterateHyphaNamesWith) | 	treeHTML, _, _ := tree.Tree(hyphaName) | ||||||
| 	page := templates.RevisionHTML( | 	page := templates.RevisionHTML( | ||||||
| 		rq, | 		rq, | ||||||
| 		hyphaName, | 		hyphaName, | ||||||
| @@ -60,10 +61,10 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { | |||||||
| func handlerText(w http.ResponseWriter, rq *http.Request) { | func handlerText(w http.ResponseWriter, rq *http.Request) { | ||||||
| 	log.Println(rq.URL) | 	log.Println(rq.URL) | ||||||
| 	hyphaName := HyphaNameFromRq(rq, "text") | 	hyphaName := HyphaNameFromRq(rq, "text") | ||||||
| 	if data, ok := HyphaStorage[hyphaName]; ok { | 	if h := hyphae.ByName(hyphaName); h.Exists { | ||||||
| 		log.Println("Serving", data.TextPath) | 		log.Println("Serving", h.TextPath) | ||||||
| 		w.Header().Set("Content-Type", "text/plain; charset=utf-8") | 		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) { | func handlerBinary(w http.ResponseWriter, rq *http.Request) { | ||||||
| 	log.Println(rq.URL) | 	log.Println(rq.URL) | ||||||
| 	hyphaName := HyphaNameFromRq(rq, "binary") | 	hyphaName := HyphaNameFromRq(rq, "binary") | ||||||
| 	if data, ok := HyphaStorage[hyphaName]; ok { | 	if h := hyphae.ByName(hyphaName); h.Exists { | ||||||
| 		log.Println("Serving", data.BinaryPath) | 		log.Println("Serving", h.BinaryPath) | ||||||
| 		w.Header().Set("Content-Type", mimetype.FromExtension(filepath.Ext(data.BinaryPath))) | 		w.Header().Set("Content-Type", mimetype.FromExtension(filepath.Ext(h.BinaryPath))) | ||||||
| 		http.ServeFile(w, rq, data.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) { | func handlerHypha(w http.ResponseWriter, rq *http.Request) { | ||||||
| 	log.Println(rq.URL) | 	log.Println(rq.URL) | ||||||
| 	var ( | 	var ( | ||||||
| 		hyphaName         = HyphaNameFromRq(rq, "page", "hypha") | 		hyphaName = HyphaNameFromRq(rq, "page", "hypha") | ||||||
| 		data, hyphaExists = HyphaStorage[hyphaName] | 		h         = hyphae.ByName(hyphaName) | ||||||
| 		hasAmnt           = hyphaExists && data.BinaryPath != "" | 		hasAmnt   = h.Exists && h.BinaryPath != "" | ||||||
| 		contents          string | 		contents  string | ||||||
| 		openGraph         string | 		openGraph string | ||||||
| 		u                 = user.FromRequest(rq) | 		u         = user.FromRequest(rq) | ||||||
| 	) | 	) | ||||||
| 	if hyphaExists { | 	if h.Exists { | ||||||
| 		fileContentsT, errT := ioutil.ReadFile(data.TextPath) | 		fileContentsT, errT := ioutil.ReadFile(h.TextPath) | ||||||
| 		_, errB := os.Stat(data.BinaryPath) | 		_, errB := os.Stat(h.BinaryPath) | ||||||
| 		if errT == nil { | 		if errT == nil { | ||||||
| 			md := markup.Doc(hyphaName, string(fileContentsT)) | 			md := markup.Doc(hyphaName, string(fileContentsT)) | ||||||
| 			contents = md.AsHTML() | 			contents = md.AsHTML() | ||||||
| 			openGraph = md.OpenGraphHTML() | 			openGraph = md.OpenGraphHTML() | ||||||
| 		} | 		} | ||||||
| 		if !os.IsNotExist(errB) { | 		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, | 	util.HTTP200Page(w, | ||||||
| 		templates.BaseHTML( | 		templates.BaseHTML( | ||||||
| 			util.BeautifulName(hyphaName), | 			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" | 	"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. | // Index finds all hypha files in the full `path` and saves them to the hypha storage. | ||||||
| func Index(path string, nestLevel uint, ch chan *Hypha) { | 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) | 	nodes, err := ioutil.ReadDir(path) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| @@ -22,14 +42,14 @@ func Index(path string, nestLevel uint, ch chan *Hypha) { | |||||||
| 			util.IsCanonicalName(node.Name()) && | 			util.IsCanonicalName(node.Name()) && | ||||||
| 			node.Name() != ".git" && | 			node.Name() != ".git" && | ||||||
| 			!(nestLevel == 0 && node.Name() == "static") { | 			!(nestLevel == 0 && node.Name() == "static") { | ||||||
| 			Index(filepath.Join(path, node.Name()), nestLevel+1, ch) | 			indexHelper(filepath.Join(path, node.Name()), nestLevel+1, ch) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		var ( | 		var ( | ||||||
| 			hyphaPartPath           = filepath.Join(path, node.Name()) | 			hyphaPartPath           = filepath.Join(path, node.Name()) | ||||||
| 			hyphaName, isText, skip = mimetype.DataFromFilename(hyphaPartPath) | 			hyphaName, isText, skip = mimetype.DataFromFilename(hyphaPartPath) | ||||||
| 			hypha                   = &Hypha{Name: hyphaName} | 			hypha                   = &Hypha{Name: hyphaName, Exists: true} | ||||||
| 		) | 		) | ||||||
| 		if !skip { | 		if !skip { | ||||||
| 			if isText { | 			if isText { | ||||||
| @@ -39,6 +59,5 @@ func Index(path string, nestLevel uint, ch chan *Hypha) { | |||||||
| 			} | 			} | ||||||
| 			ch <- hypha | 			ch <- hypha | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										166
									
								
								hyphae/hyphae.go
									
									
									
									
									
								
							
							
						
						
									
										166
									
								
								hyphae/hyphae.go
									
									
									
									
									
								
							| @@ -1,9 +1,47 @@ | |||||||
| package hyphae | package hyphae | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"log" | ||||||
|  | 	"regexp" | ||||||
|  | 	"strings" | ||||||
| 	"sync" | 	"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 { | type Hypha struct { | ||||||
| 	sync.RWMutex | 	sync.RWMutex | ||||||
|  |  | ||||||
| @@ -11,33 +49,125 @@ type Hypha struct { | |||||||
| 	Exists     bool | 	Exists     bool | ||||||
| 	TextPath   string | 	TextPath   string | ||||||
| 	BinaryPath string | 	BinaryPath string | ||||||
| 	OutLinks   []*Hypha | 	OutLinks   []*Hypha // not used yet | ||||||
| 	BackLinks  []*Hypha | 	BackLinks  []*Hypha // not used yet | ||||||
| } | } | ||||||
|  |  | ||||||
| /* | var byNames = make(map[string]*Hypha) | ||||||
| // 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 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) { | func (h *Hypha) Insert() (justCreated bool) { | ||||||
| 	var hp *Hypha | 	var hp *Hypha | ||||||
| 	hp, justCreated = ByName(h.Name) | 	hp = ByName(h.Name) | ||||||
|  |  | ||||||
| 	mycm.Lock() | 	byNamesMutex.Lock() | ||||||
| 	defer mycm.Unlock() | 	defer byNamesMutex.Unlock() | ||||||
| 	if justCreated { | 	if hp.Exists { | ||||||
| 		mycm.byNames[hp.Name] = h |  | ||||||
| 	} else { |  | ||||||
| 		hp = h | 		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) InsertIfNew() (justCreated bool) { | ||||||
| func (h *Hypha) PhaseOut() { | 	if h.Exists { | ||||||
|  | 		return h.Insert() | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (h *Hypha) delete() { | ||||||
|  | 	byNamesMutex.Lock() | ||||||
| 	h.Lock() | 	h.Lock() | ||||||
| 	h.Exists = false | 	delete(byNames, h.Name) | ||||||
| 	h.OutLinks = make([]*Hypha, 0) | 	DecrementCount() | ||||||
| 	h.TextPath = "" | 	byNamesMutex.Unlock() | ||||||
| 	h.BinaryPath = "" |  | ||||||
| 	h.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" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"regexp" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"github.com/bouncepaw/mycorrhiza/history" | 	"github.com/bouncepaw/mycorrhiza/history" | ||||||
| @@ -24,19 +23,6 @@ import ( | |||||||
| // WikiDir is a rooted path to the wiki storage directory. | // WikiDir is a rooted path to the wiki storage directory. | ||||||
| var WikiDir string | 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. | // HttpErr is used by many handlers to signal errors in a compact way. | ||||||
| func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) { | func HttpErr(w http.ResponseWriter, status int, name, title, errMsg string) { | ||||||
| 	log.Println(errMsg, "for", name) | 	log.Println(errMsg, "for", name) | ||||||
| @@ -64,8 +50,8 @@ func handlerList(w http.ResponseWriter, rq *http.Request) { | |||||||
| 		pageCount = hyphae.Count() | 		pageCount = hyphae.Count() | ||||||
| 		u         = user.FromRequest(rq) | 		u         = user.FromRequest(rq) | ||||||
| 	) | 	) | ||||||
| 	for hyphaName, data := range HyphaStorage { | 	for h := range hyphae.YieldExistingHyphae() { | ||||||
| 		tbody += templates.HyphaListRowHTML(hyphaName, mimetype.FromExtension(filepath.Ext(data.BinaryPath)), data.BinaryPath != "") | 		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)) | 	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 | 		return | ||||||
| 	} | 	} | ||||||
| 	hyphae.ResetCount() | 	hyphae.ResetCount() | ||||||
| 	HyphaStorage = make(map[string]*HyphaData) |  | ||||||
| 	log.Println("Wiki storage directory is", WikiDir) | 	log.Println("Wiki storage directory is", WikiDir) | ||||||
| 	log.Println("Start indexing hyphae...") | 	log.Println("Start indexing hyphae...") | ||||||
| 	Index(WikiDir) | 	hyphae.Index(WikiDir) | ||||||
| 	log.Println("Indexed", hyphae.Count(), "hyphae") | 	log.Println("Indexed", hyphae.Count(), "hyphae") | ||||||
| 	http.Redirect(w, rq, "/", http.StatusSeeOther) | 	http.Redirect(w, rq, "/", http.StatusSeeOther) | ||||||
| } | } | ||||||
| @@ -98,7 +83,7 @@ func handlerUpdateHeaderLinks(w http.ResponseWriter, rq *http.Request) { | |||||||
| 		log.Println("Rejected", rq.URL) | 		log.Println("Rejected", rq.URL) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	setHeaderLinks() | 	hyphae.SetHeaderLinks() | ||||||
| 	http.Redirect(w, rq, "/", http.StatusSeeOther) | 	http.Redirect(w, rq, "/", http.StatusSeeOther) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -107,14 +92,13 @@ func handlerRandom(w http.ResponseWriter, rq *http.Request) { | |||||||
| 	log.Println(rq.URL) | 	log.Println(rq.URL) | ||||||
| 	var randomHyphaName string | 	var randomHyphaName string | ||||||
| 	i := rand.Intn(hyphae.Count()) | 	i := rand.Intn(hyphae.Count()) | ||||||
| 	for hyphaName := range HyphaStorage { | 	for h := range hyphae.YieldExistingHyphae() { | ||||||
| 		if i == 0 { | 		if i == 0 { | ||||||
| 			randomHyphaName = hyphaName | 			randomHyphaName = h.Name | ||||||
| 			break |  | ||||||
| 		} | 		} | ||||||
| 		i-- | 		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) { | func handlerStyle(w http.ResponseWriter, rq *http.Request) { | ||||||
| @@ -187,11 +171,11 @@ func main() { | |||||||
| 		log.Fatal(err) | 		log.Fatal(err) | ||||||
| 	} | 	} | ||||||
| 	log.Println("Wiki storage directory is", WikiDir) | 	log.Println("Wiki storage directory is", WikiDir) | ||||||
| 	Index(WikiDir) | 	hyphae.Index(WikiDir) | ||||||
| 	log.Println("Indexed", hyphae.Count(), "hyphae") | 	log.Println("Indexed", hyphae.Count(), "hyphae") | ||||||
|  |  | ||||||
| 	history.Start(WikiDir) | 	history.Start(WikiDir) | ||||||
| 	setHeaderLinks() | 	hyphae.SetHeaderLinks() | ||||||
|  |  | ||||||
| 	go handleGemini() | 	go handleGemini() | ||||||
|  |  | ||||||
| @@ -212,7 +196,7 @@ func main() { | |||||||
| 	http.HandleFunc("/static/common.css", handlerStyle) | 	http.HandleFunc("/static/common.css", handlerStyle) | ||||||
| 	http.HandleFunc("/static/icon/", handlerIcon) | 	http.HandleFunc("/static/icon/", handlerIcon) | ||||||
| 	http.HandleFunc("/", func(w http.ResponseWriter, rq *http.Request) { | 	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) | 	http.HandleFunc("/robots.txt", handlerRobotsTxt) | ||||||
| 	log.Fatal(http.ListenAndServe("0.0.0.0:"+util.ServerPort, nil)) | 	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" | 	"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. | // naviTitle turns `canonicalName` into html string with each hypha path parts higlighted as links. | ||||||
| // TODO: rework as a template | // TODO: rework as a template | ||||||
| func naviTitle(canonicalName string) string { | func naviTitle(canonicalName string) string { | ||||||
| 	var ( | 	var ( | ||||||
| 		html = fmt.Sprintf(`<h1 class="navi-title" id="navi-title"> | 		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) | 	<a href="/hypha/%s">%s</a><span aria-hidden="true" class="navi-title__colon">:</span>`, util.HomePage, util.SiteNavIcon) | ||||||
| 		prevAcc = `/page/` | 		prevAcc = `/hypha/` | ||||||
| 		parts   = strings.Split(canonicalName, "/") | 		parts   = strings.Split(canonicalName, "/") | ||||||
| 		rel     = "up" | 		rel     = "up" | ||||||
| 	) | 	) | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								tree/tree.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								tree/tree.go
									
									
									
									
									
								
							| @@ -6,6 +6,7 @@ import ( | |||||||
| 	"sort" | 	"sort" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"github.com/bouncepaw/mycorrhiza/hyphae" | ||||||
| 	"github.com/bouncepaw/mycorrhiza/util" | 	"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. | // 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 ( | 	var ( | ||||||
| 		// One of the siblings is the hypha with name `hyphaName` | 		// One of the siblings is the hypha with name `hyphaName` | ||||||
| 		siblings      = findSiblings(hyphaName, hyphaIterator) | 		siblings      = findSiblings(hyphaName) | ||||||
| 		subhyphaePool = make(map[string]bool) | 		subhyphaePool = make(map[string]bool) | ||||||
| 		I             int | 		I             int | ||||||
| 	) | 	) | ||||||
| 	hyphaIterator(func(otherHyphaName string) { | 	for h := range hyphae.YieldExistingHyphae() { | ||||||
| 		for _, s := range siblings { | 		for _, s := range siblings { | ||||||
| 			s.checkThisChild(otherHyphaName) | 			s.checkThisChild(h.Name) | ||||||
| 		} | 		} | ||||||
| 		if strings.HasPrefix(otherHyphaName, hyphaName+"/") { | 		if strings.HasPrefix(h.Name, hyphaName+"/") { | ||||||
| 			subhyphaePool[otherHyphaName] = true | 			subhyphaePool[h.Name] = true | ||||||
| 		} | 		} | ||||||
| 	}) | 	} | ||||||
| 	for i, s := range siblings { | 	for i, s := range siblings { | ||||||
| 		if s.name == hyphaName { | 		if s.name == hyphaName { | ||||||
| 			I = i | 			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 | 	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}} | 	siblings := []*sibling{&sibling{name: hyphaName, hasChildren: true}} | ||||||
| 	hyphaIterator(func(otherHyphaName string) { | 	for h := range hyphae.YieldExistingHyphae() { | ||||||
| 		if path.Dir(hyphaName) == path.Dir(otherHyphaName) && hyphaName != otherHyphaName { | 		if path.Dir(hyphaName) == path.Dir(h.Name) && hyphaName != h.Name { | ||||||
| 			siblings = append(siblings, &sibling{name: otherHyphaName, hasChildren: false}) | 			siblings = append(siblings, &sibling{name: h.Name, hasChildren: false}) | ||||||
| 		} | 		} | ||||||
| 	}) | 	} | ||||||
| 	sort.Slice(siblings, func(i, j int) bool { | 	sort.Slice(siblings, func(i, j int) bool { | ||||||
| 		return siblings[i].name < siblings[j].name | 		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)) | 	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) { | func RandomString(n int) (string, error) { | ||||||
| 	bytes := make([]byte, n) | 	bytes := make([]byte, n) | ||||||
| 	if _, err := rand.Read(bytes); err != nil { | 	if _, err := rand.Read(bytes); err != nil { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 bouncepaw
					bouncepaw