From 86d1a00bfcb128dd070ac2f83805adb2503f605c Mon Sep 17 00:00:00 2001 From: Timur Ismagilov Date: Mon, 31 Jan 2022 02:34:52 +0500 Subject: [PATCH] Implement the rocket link migration algorithm --- files/files.go | 3 + history/operations.go | 10 +-- hyphae/backlinks/backlinks.go | 2 +- hyphae/iterators.go | 5 +- main.go | 2 + migration/rockets.go | 117 ++++++++++++++++++++++++++++++++++ user/user.go | 10 +++ 7 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 migration/rockets.go diff --git a/files/files.go b/files/files.go index 37bda49..af913e9 100644 --- a/files/files.go +++ b/files/files.go @@ -38,6 +38,9 @@ func TokensJSON() string { return paths.tokensJSON } // UserCredentialsJSON returns the path to the JSON user credentials storage. func UserCredentialsJSON() string { return paths.userCredentialsJSON } +// FileInRoot returns full path for the given filename if it was placed in the root of the wiki structure. +func FileInRoot(filename string) string { return filepath.Join(cfg.WikiDir, filename) } + // PrepareWikiRoot ensures all needed directories and files exist and have // correct permissions. func PrepareWikiRoot() error { diff --git a/history/operations.go b/history/operations.go index 7a5d080..1d30871 100644 --- a/history/operations.go +++ b/history/operations.go @@ -31,6 +31,8 @@ const ( TypeRenameHypha // TypeUnattachHypha represents a hypha attachment deletion TypeUnattachHypha + // TypeMarkupMigration represents a wikimind-powered automatic markup migration procedure + TypeMarkupMigration ) // Op is an object representing a history operation. @@ -65,15 +67,15 @@ func (hop *Op) gitop(args ...string) *Op { return hop } -// WithErr appends the `err` to the list of errors. -func (hop *Op) WithErr(err error) *Op { +// withErr appends the `err` to the list of errors. +func (hop *Op) withErr(err error) *Op { hop.Errs = append(hop.Errs, err) return hop } // WithErrAbort appends the `err` to the list of errors and immediately aborts the operation. func (hop *Op) WithErrAbort(err error) *Op { - return hop.WithErr(err).Abort() + return hop.withErr(err).Abort() } // WithFilesRemoved git-rm-s all passed `paths`. Paths can be rooted or not. Paths that are empty strings are ignored. @@ -110,7 +112,7 @@ func (hop *Op) WithFiles(paths ...string) *Op { return hop.gitop(append([]string{"add"}, paths...)...) } -// Apply applies history operation by doing the commit. +// Apply applies history operation by doing the commit. You do not need to call Abort afterwards. func (hop *Op) Apply() *Op { hop.gitop( "commit", diff --git a/hyphae/backlinks/backlinks.go b/hyphae/backlinks/backlinks.go index f56dfee..6597538 100644 --- a/hyphae/backlinks/backlinks.go +++ b/hyphae/backlinks/backlinks.go @@ -41,7 +41,7 @@ var backlinkIndex = make(map[string]linkSet) // IndexBacklinks traverses all text hyphae, extracts links from them and forms an initial index. Call it when indexing and reindexing hyphae. func IndexBacklinks() { // It is safe to ignore the mutex, because there is only one worker. - for h := range hyphae.FilterTextHyphae(hyphae.YieldExistingHyphae()) { + for h := range hyphae.FilterHyphaeWithText(hyphae.YieldExistingHyphae()) { foundLinks := extractHyphaLinksFromContent(h.Name, fetchText(h)) for _, link := range foundLinks { if _, exists := backlinkIndex[link]; !exists { diff --git a/hyphae/iterators.go b/hyphae/iterators.go index ca89d08..a7b8250 100644 --- a/hyphae/iterators.go +++ b/hyphae/iterators.go @@ -21,8 +21,9 @@ func YieldExistingHyphae() chan *Hypha { return ch } -// FilterTextHyphae filters the source channel and yields only those hyphae than have text parts. -func FilterTextHyphae(src chan *Hypha) chan *Hypha { +// FilterHyphaeWithText filters the source channel and yields only those hyphae than have text parts. +func FilterHyphaeWithText(src chan *Hypha) chan *Hypha { + // TODO: reimplement as a function with a callback? sink := make(chan *Hypha) go func() { for h := range src { diff --git a/main.go b/main.go index 0aeba86..a9f1497 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ package main import ( + "github.com/bouncepaw/mycorrhiza/migration" "log" "os" @@ -45,6 +46,7 @@ func main() { user.InitUserDatabase() history.Start() history.InitGitRepo() + migration.MigrateRocketsMaybe() shroom.SetHeaderLinks() // Static files: diff --git a/migration/rockets.go b/migration/rockets.go new file mode 100644 index 0000000..2b157f5 --- /dev/null +++ b/migration/rockets.go @@ -0,0 +1,117 @@ +// Package migration holds the utilities for migrating from older incompatible Mycomarkup versions. +// +// As of, there is rocket link migration only. Migrations are meant to be removed couple of versions after being introduced. +package migration + +import ( + "github.com/bouncepaw/mycomarkup/v3/tools" + "io" + "io/ioutil" + "log" + "os" + "strings" + + "github.com/bouncepaw/mycorrhiza/files" + "github.com/bouncepaw/mycorrhiza/history" + "github.com/bouncepaw/mycorrhiza/hyphae" + "github.com/bouncepaw/mycorrhiza/user" +) + +// TODO: add heading migration too. + +var rocketMarkerPath string + +// MigrateRocketsMaybe checks if the rocket link migration marker exists. If it exists, nothing is done. If it does not, the migration takes place. +// +// This function writes logs and might terminate the program. Tons of side-effects, stay safe. +func MigrateRocketsMaybe() { + rocketMarkerPath = files.FileInRoot(".mycomarkup-rocket-link-migration-marker.txt") + if !shouldMigrateRockets() { + return + } + + var ( + hop = history. + Operation(history.TypeMarkupMigration). + WithMsg("Migrate rocket links to the new syntax"). + WithUser(user.WikimindUser()) + mycoFiles = []string{} + ) + + for hypha := range hyphae.FilterHyphaeWithText(hyphae.YieldExistingHyphae()) { + /// Open file, read from file, modify file. If anything goes wrong, scream and shout. + + file, err := os.OpenFile(hypha.TextPartPath(), os.O_RDWR, 0766) + if err != nil { + hop.WithErrAbort(err) + log.Fatal("Something went wrong when opening ", hypha.TextPartPath(), ": ", err.Error()) + } + + var buf strings.Builder + _, err = io.Copy(&buf, file) + if err != nil { + hop.WithErrAbort(err) + _ = file.Close() + log.Fatal("Something went wrong when reading ", hypha.TextPartPath(), ": ", err.Error()) + } + + var ( + oldText = buf.String() + newText = tools.MigrateRocketLinks(oldText) + ) + if oldText != newText { // This file right here is being migrated for real. + mycoFiles = append(mycoFiles, hypha.TextPartPath()) + + err = file.Truncate(0) + if err != nil { + hop.WithErrAbort(err) + _ = file.Close() + log.Fatal("Something went wrong when truncating ", hypha.TextPartPath(), ": ", err.Error()) + } + + _, err = file.Seek(0, 0) + if err != nil { + hop.WithErrAbort(err) + _ = file.Close() + log.Fatal("Something went wrong when seeking in ", hypha.TextPartPath(), ": ", err.Error()) + } + + _, err = file.WriteString(newText) + if err != nil { + hop.WithErrAbort(err) + _ = file.Close() + log.Fatal("Something went wrong when writing to ", hypha.TextPartPath(), ": ", err.Error()) + } + } + _ = file.Close() + } + + if hop.WithFiles(mycoFiles...).Apply().HasErrors() { + log.Fatal("Something went wrong when commiting rocket link migration: ", hop.FirstErrorText()) + } + log.Println("Migrated", len(mycoFiles), "Mycomarkup documents") + createRocketLinkMarker() +} + +func shouldMigrateRockets() bool { + file, err := os.Open(rocketMarkerPath) + if os.IsNotExist(err) { + return true + } + if err != nil { + log.Fatalln("When checking if rocket migration is needed:", err.Error()) + } + _ = file.Close() + return false +} + +func createRocketLinkMarker() { + err := ioutil.WriteFile( + rocketMarkerPath, + []byte(`This file is used to mark that the rocket link migration was made successfully. If this file is deleted, the migration might happen again depending on the version. You should probably not touch this file at all and let it be.`), + 0766, + ) + if err != nil { + log.Fatalln(err) + } +} diff --git a/user/user.go b/user/user.go index e8b8e5c..a49dade 100644 --- a/user/user.go +++ b/user/user.go @@ -88,6 +88,16 @@ func EmptyUser() *User { } } +// WikimindUser constructs the wikimind user, which is to be used for automated wiki edits and has admin privileges. +func WikimindUser() *User { + return &User{ + Name: "wikimind", + Group: "admin", + Password: "", + Source: "local", + } +} + // CanProceed checks whether user has rights to visit the provided path (and perform an action). func (user *User) CanProceed(route string) bool { if !cfg.UseAuth {