diff --git a/README.md b/README.md index 1ff54b30..21b45034 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🍄 MycorrhizaWiki 0.9 +# 🍄 MycorrhizaWiki 0.10 A wiki engine. ## Building @@ -11,12 +11,25 @@ make # * create an executable called `mycorrhiza`. Run it with path to your wiki. ``` +## Usage +``` +mycorrhiza [OPTIONS...] WIKI_PATH + +Options: + -home string + The home page (default "home") + -port string + Port to serve the wiki at (default "1737") + -title string + How to call your wiki in the navititle (default "🍄") +``` + ## Features * Edit pages through html forms * Responsive design * Works in text browsers -* Wiki pages (called hyphae) are in gemtext -* Everything is stored as simple files, no database required +* Wiki pages (called hyphae) are written in mycomarkup +* Everything is stored as simple files, no database required. You can run a wiki on almost any directory and get something to work with. * Page trees * Changes are saved to git * List of hyphae page @@ -34,4 +47,3 @@ Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where som * Tagging system * Authorization * Better history viewing -* More markups diff --git a/flag.go b/flag.go new file mode 100644 index 00000000..d5d6df4c --- /dev/null +++ b/flag.go @@ -0,0 +1,36 @@ +package main + +import ( + "flag" + "log" + "path/filepath" + + "github.com/bouncepaw/mycorrhiza/util" +) + +func init() { + flag.StringVar(&util.ServerPort, "port", "1737", "Port to serve the wiki at") + flag.StringVar(&util.HomePage, "home", "home", "The home page") + flag.StringVar(&util.SiteTitle, "title", "🍄", "How to call your wiki in the navititle") +} + +// Do the things related to cli args and die maybe +func parseCliArgs() { + flag.Parse() + + args := flag.Args() + if len(args) == 0 { + log.Fatal("Error: pass a wiki directory") + } + + var err error + WikiDir, err = filepath.Abs(args[0]) + util.WikiDir = WikiDir + if err != nil { + log.Fatal(err) + } + + if !isCanonicalName(util.HomePage) { + log.Fatal("Error: you must use a proper name for the homepage") + } +} diff --git a/history/history.go b/history/history.go index c57ef96a..ec409c29 100644 --- a/history/history.go +++ b/history/history.go @@ -59,16 +59,16 @@ func (rev Revision) HyphaeLinks() (html string) { } for _, filename := range strings.Split(out.String(), "\n") { // If filename has an ampersand: - if strings.IndexRune(filename, '&') >= 0 { + if strings.IndexRune(filename, '.') >= 0 { // Remove ampersanded suffix from filename: - ampersandPos := strings.LastIndexByte(filename, '&') + ampersandPos := strings.LastIndexByte(filename, '.') hyphaName := string([]byte(filename)[0:ampersandPos]) // is it safe? if isNewName(hyphaName) { // Entries are separated by commas if len(set) > 1 { html += `` } - html += fmt.Sprintf(`%[2]s`, rev.Hash, hyphaName) + html += fmt.Sprintf(`%[1]s`, hyphaName) } } } @@ -77,10 +77,10 @@ func (rev Revision) HyphaeLinks() (html string) { func (rev Revision) RecentChangesEntry() (html string) { return fmt.Sprintf(` -
  • -
  • %s
  • -
  • %s
  • -
  • %s
  • +
  • +
  • %s
  • +
  • %s
  • +
  • %s
  • `, rev.TimeString(), rev.Hash, rev.HyphaeLinks(), rev.Message) } @@ -125,6 +125,6 @@ func unixTimestampAsTime(ts string) *time.Time { // Rename renames from `from` to `to` using `git mv`. func Rename(from, to string) error { log.Println(util.ShorterPath(from), util.ShorterPath(to)) - _, err := gitsh("mv", from, to) + _, err := gitsh("mv", "--force", from, to) return err } diff --git a/history/information.go b/history/information.go index 469d9ab9..164e3a96 100644 --- a/history/information.go +++ b/history/information.go @@ -39,7 +39,7 @@ func Revisions(hyphaName string) ([]Revision, error) { "log", "--oneline", "--no-merges", // Hash, Commiter email, Commiter time, Commit msg separated by tab "--pretty=format:\"%h\t%ce\t%ct\t%s\"", - "--", hyphaName+"&.*", + "--", hyphaName+".*", ) revs []Revision ) diff --git a/history/operations.go b/history/operations.go index 4388916b..05074836 100644 --- a/history/operations.go +++ b/history/operations.go @@ -55,6 +55,12 @@ func (hop *HistoryOp) gitop(args ...string) *HistoryOp { return hop } +// WithError appends the `err` to the list of errors. +func (hop *HistoryOp) WithError(err error) *HistoryOp { + hop.Errs = append(hop.Errs, err) + return hop +} + // WithFilesRemoved git-rm-s all passed `paths`. Paths can be rooted or not. Paths that are empty strings are ignored. func (hop *HistoryOp) WithFilesRemoved(paths ...string) *HistoryOp { args := []string{"rm", "--quiet", "--"} @@ -71,7 +77,9 @@ func (hop *HistoryOp) WithFilesRenamed(pairs map[string]string) *HistoryOp { for from, to := range pairs { if from != "" { os.MkdirAll(filepath.Dir(to), 0777) - hop.gitop(append([]string{"mv"}, from, to)...) + if err := Rename(from, to); err != nil { + hop.Errs = append(hop.Errs, err) + } } } return hop diff --git a/http_mutators.go b/http_mutators.go index aa669c07..beb43e60 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -2,13 +2,9 @@ package main import ( "fmt" - "io/ioutil" "log" "net/http" - "os" - "path/filepath" - "github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/util" ) @@ -116,14 +112,13 @@ func handlerEdit(w http.ResponseWriter, rq *http.Request) { textAreaFill, err = FetchTextPart(hyphaData) if err != nil { log.Println(err) - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", - "Could not fetch text data") + HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", "Could not fetch text data") return } } else { warning = `

    You are creating a new hypha.

    ` } - util.HTTP200Page(w, base("Edit"+hyphaName, templates.EditHTML(hyphaName, textAreaFill, warning))) + util.HTTP200Page(w, base("Edit "+hyphaName, templates.EditHTML(hyphaName, textAreaFill, warning))) } // handlerUploadText uploads a new text part for the hypha. @@ -133,42 +128,19 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) { hyphaName = HyphaNameFromRq(rq, "upload-text") hyphaData, isOld = HyphaStorage[hyphaName] textData = rq.PostFormValue("text") - textDataBytes = []byte(textData) - fullPath = filepath.Join(WikiDir, hyphaName+"&.gmi") ) - if textData == "" { - HttpErr(w, http.StatusBadRequest, hyphaName, "Error", - "No text data passed") - return - } - // For some reason, only 0777 works. Why? - if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil { - log.Println(err) + if !isOld { + hyphaData = &HyphaData{} } - if err := ioutil.WriteFile(fullPath, textDataBytes, 0644); err != nil { - log.Println(err) - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", - fmt.Sprintf("Failed to write %d bytes to %s", - len(textDataBytes), fullPath)) + if textData == "" { + HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No text data passed") return } - if !isOld { - hd := HyphaData{ - textType: TextGemini, - textPath: fullPath, - } - HyphaStorage[hyphaName] = &hd - hyphaData = &hd + if hop := hyphaData.UploadText(hyphaName, textData, isOld); len(hop.Errs) != 0 { + HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) } else { - hyphaData.textType = TextGemini - hyphaData.textPath = fullPath + http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) } - history.Operation(history.TypeEditText). - WithFiles(fullPath). - WithMsg(fmt.Sprintf("Edit ‘%s’", hyphaName)). - WithSignature("anon"). - Apply() - http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) } // handlerUploadBinary uploads a new binary part for the hypha. @@ -176,62 +148,29 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) hyphaName := HyphaNameFromRq(rq, "upload-binary") rq.ParseMultipartForm(10 << 20) - // Read file + file, handler, err := rq.FormFile("binary") if file != nil { defer file.Close() } // If file is not passed: if err != nil { - HttpErr(w, http.StatusBadRequest, hyphaName, "Error", - "No binary data passed") + HttpErr(w, http.StatusBadRequest, hyphaName, "Error", "No binary data passed") return } // If file is passed: var ( hyphaData, isOld = HyphaStorage[hyphaName] - mimeType = MimeToBinaryType(handler.Header.Get("Content-Type")) - ext = mimeType.Extension() - fullPath = filepath.Join(WikiDir, hyphaName+"&"+ext) + mime = handler.Header.Get("Content-Type") ) - - data, err := ioutil.ReadAll(file) - if err != nil { - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", - "Could not read passed data") - return - } - if err := os.MkdirAll(filepath.Dir(fullPath), 0777); err != nil { - log.Println(err) - } if !isOld { - hd := HyphaData{ - binaryPath: fullPath, - binaryType: mimeType, - } - HyphaStorage[hyphaName] = &hd - hyphaData = &hd - } else { - if hyphaData.binaryPath != fullPath { - if err := history.Rename(hyphaData.binaryPath, fullPath); err != nil { - log.Println(err) - } else { - log.Println("Moved", hyphaData.binaryPath, "to", fullPath) - } - } - hyphaData.binaryPath = fullPath - hyphaData.binaryType = mimeType + hyphaData = &HyphaData{} } - if err = ioutil.WriteFile(fullPath, data, 0644); err != nil { - HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", - "Could not save passed data") - return + hop := hyphaData.UploadBinary(hyphaName, mime, file, isOld) + + if len(hop.Errs) != 0 { + HttpErr(w, http.StatusInternalServerError, hyphaName, "Error", hop.Errs[0].Error()) + } else { + http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) } - log.Println("Written", len(data), "of binary data for", hyphaName, "to path", fullPath) - history.Operation(history.TypeEditText). - WithFiles(fullPath, hyphaData.binaryPath). - WithMsg(fmt.Sprintf("Upload binary part for ‘%s’ with type ‘%s’", hyphaName, mimeType.Mime())). - WithSignature("anon"). - Apply() - http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) } diff --git a/http_readers.go b/http_readers.go index 5843e4d2..7870d3ed 100644 --- a/http_readers.go +++ b/http_readers.go @@ -6,10 +6,11 @@ import ( "log" "net/http" "os" + "path/filepath" "strings" - "github.com/bouncepaw/mycorrhiza/gemtext" "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/markup" "github.com/bouncepaw/mycorrhiza/templates" "github.com/bouncepaw/mycorrhiza/tree" "github.com/bouncepaw/mycorrhiza/util" @@ -32,11 +33,11 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { revHash = shorterUrl[:firstSlashIndex] hyphaName = CanonicalName(shorterUrl[firstSlashIndex+1:]) contents = fmt.Sprintf(`

    This hypha had no text at this revision.

    `) - textPath = hyphaName + "&.gmi" + textPath = hyphaName + ".myco" textContents, err = history.FileAtRevision(textPath, revHash) ) if err == nil { - contents = gemtext.ToHtml(hyphaName, textContents) + contents = markup.ToHtml(hyphaName, textContents) } page := templates.RevisionHTML( hyphaName, @@ -75,7 +76,7 @@ func handlerText(w http.ResponseWriter, rq *http.Request) { hyphaName := HyphaNameFromRq(rq, "text") if data, ok := HyphaStorage[hyphaName]; ok { log.Println("Serving", data.textPath) - w.Header().Set("Content-Type", data.textType.Mime()) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") http.ServeFile(w, rq, data.textPath) } } @@ -86,7 +87,7 @@ func handlerBinary(w http.ResponseWriter, rq *http.Request) { hyphaName := HyphaNameFromRq(rq, "binary") if data, ok := HyphaStorage[hyphaName]; ok { log.Println("Serving", data.binaryPath) - w.Header().Set("Content-Type", data.binaryType.Mime()) + w.Header().Set("Content-Type", ExtensionToMime(filepath.Ext(data.binaryPath))) http.ServeFile(w, rq, data.binaryPath) } } @@ -103,7 +104,7 @@ func handlerPage(w http.ResponseWriter, rq *http.Request) { fileContentsT, errT := ioutil.ReadFile(data.textPath) _, errB := os.Stat(data.binaryPath) if errT == nil { - contents = gemtext.ToHtml(hyphaName, string(fileContentsT)) + contents = markup.ToHtml(hyphaName, string(fileContentsT)) } if !os.IsNotExist(errB) { contents = binaryHtmlBlock(hyphaName, data) + contents diff --git a/hypha.go b/hypha.go index 74a86850..a49b785d 100644 --- a/hypha.go +++ b/hypha.go @@ -5,21 +5,22 @@ import ( "fmt" "io/ioutil" "log" + "mime/multipart" "os" "path/filepath" "strings" - "github.com/bouncepaw/mycorrhiza/gemtext" "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/markup" "github.com/bouncepaw/mycorrhiza/util" ) func init() { - gemtext.HyphaExists = func(hyphaName string) bool { + markup.HyphaExists = func(hyphaName string) bool { _, hyphaExists := HyphaStorage[hyphaName] return hyphaExists } - gemtext.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) { + markup.HyphaAccess = func(hyphaName string) (rawText, binaryBlock string, err error) { if hyphaData, ok := HyphaStorage[hyphaName]; ok { rawText, err = FetchTextPart(hyphaData) if hyphaData.binaryPath != "" { @@ -35,9 +36,54 @@ func init() { // HyphaData represents a hypha's meta information: binary and text parts rooted paths and content types. type HyphaData struct { textPath string - textType TextType binaryPath string - binaryType BinaryType +} + +// uploadHelp is a helper function for UploadText and UploadBinary +func (hd *HyphaData) uploadHelp(hop *history.HistoryOp, hyphaName, ext string, originalFullPath *string, isOld bool, data []byte) *history.HistoryOp { + var ( + fullPath = filepath.Join(WikiDir, hyphaName+ext) + ) + 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) + } + if !isOld { + HyphaStorage[hyphaName] = hd + } + *originalFullPath = fullPath + log.Printf("%v\n", *hd) + return hop.WithFiles(fullPath). + WithSignature("anon"). + Apply() +} + +// UploadText loads a new text part from `textData` for hypha `hyphaName` with `hd`. It must be marked if the hypha `isOld`. +func (hd *HyphaData) UploadText(hyphaName, textData string, isOld bool) *history.HistoryOp { + hop := history.Operation(history.TypeEditText).WithMsg(fmt.Sprintf("Edit ‘%s’", hyphaName)) + return hd.uploadHelp(hop, hyphaName, ".myco", &hd.textPath, isOld, []byte(textData)) +} + +// 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 (hd *HyphaData) UploadBinary(hyphaName, mime string, file multipart.File, isOld bool) *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 hd.uploadHelp(hop, hyphaName, MimeToExtension(mime), &hd.binaryPath, isOld, data) } // DeleteHypha deletes hypha and makes a history record about that. @@ -61,10 +107,13 @@ func findHyphaeToRename(hyphaName string, recursive bool) []string { return hyphae } -func renamingPairs(hyphaNames []string, replaceName func(string) string) map[string]string { +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) } @@ -73,7 +122,7 @@ func renamingPairs(hyphaNames []string, replaceName func(string) string) map[str } } } - return renameMap + return renameMap, nil } // word Data is plural here @@ -94,11 +143,15 @@ func (hd *HyphaData) RenameHypha(hyphaName, newName string, recursive bool) *his replaceName = func(str string) string { return strings.Replace(str, hyphaName, newName, 1) } - hyphaNames = findHyphaeToRename(hyphaName, recursive) - renameMap = renamingPairs(hyphaNames, replaceName) - renameMsg = "Rename ‘%s’ to ‘%s’" - hop = history.Operation(history.TypeRenameHypha) + 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" } @@ -113,14 +166,14 @@ func (hd *HyphaData) RenameHypha(hyphaName, newName string, recursive bool) *his } // binaryHtmlBlock creates an html block for binary part of the hypha. -func binaryHtmlBlock(hyphaName string, d *HyphaData) string { - switch d.binaryType { - case BinaryJpeg, BinaryGif, BinaryPng, BinaryWebp, BinarySvg, BinaryIco: +func binaryHtmlBlock(hyphaName string, hd *HyphaData) string { + switch filepath.Ext(hd.binaryPath) { + case ".jpg", ".gif", ".png", ".webp", ".svg", ".ico": return fmt.Sprintf(`
    - +
    `, hyphaName) - case BinaryOgg, BinaryWebm, BinaryMp4: + case ".ogg", ".webm", ".mp4": return fmt.Sprintf(`
    `, hyphaName) - case BinaryMp3: + case ".mp3": return fmt.Sprintf(`