From 5c21241fc4942780f2a9e49da8f759cf797917cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Mon, 29 Apr 2024 21:11:16 +0200 Subject: [PATCH 01/34] actions: SaveAs: Print the error of `os.Stat()` to the `InfoBar` --- internal/action/actions.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/action/actions.go b/internal/action/actions.go index e06b7815..3505f2e1 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1003,6 +1003,9 @@ func (h *BufPane) SaveAsCB(action string, callback func()) bool { h.completeAction(action) return } + } else { + InfoBar.Error(err) + return } } else { InfoBar.YNPrompt( From 3fcaf160743f4339c0f4e4fab31dbe76b5c69833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sat, 28 Dec 2024 14:14:00 +0100 Subject: [PATCH 02/34] actions: SaveCmd: Print the error of `SaveAs` to the `InfoBar` --- internal/action/command.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/action/command.go b/internal/action/command.go index 89ac71f0..8189e598 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -890,7 +890,10 @@ func (h *BufPane) SaveCmd(args []string) { if len(args) == 0 { h.Save() } else { - h.Buf.SaveAs(args[0]) + err := h.Buf.SaveAs(args[0]) + if err != nil { + InfoBar.Error(err) + } } } From edc5ff75e34a9ec3dc36d4ef04c6bb3199c41fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Wed, 1 May 2024 17:56:22 +0200 Subject: [PATCH 03/34] save: Convert `os.IsNotExist()` into `errors.Is()` --- internal/buffer/save.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/buffer/save.go b/internal/buffer/save.go index d012eb74..1eb2c089 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -5,6 +5,7 @@ import ( "bytes" "errors" "io" + "io/fs" "os" "os/exec" "os/signal" @@ -164,7 +165,7 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error // Get the leading path to the file | "." is returned if there's no leading path provided if dirname := filepath.Dir(absFilename); dirname != "." { // Check if the parent dirs don't exist - if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) { + if _, statErr := os.Stat(dirname); errors.Is(statErr, fs.ErrNotExist) { // Prompt to make sure they want to create the dirs that are missing if b.Settings["mkparents"].(bool) { // Create all leading dir(s) since they don't exist From 6bcec2100c200576173edcc05f4827333f795495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 12 May 2024 20:54:38 +0200 Subject: [PATCH 04/34] open & write: Process regular files only --- internal/buffer/buffer.go | 11 +++++++---- internal/buffer/save.go | 11 +++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 092a5dcc..68f5824c 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -237,10 +237,6 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, return nil, err } - f, err := os.OpenFile(filename, os.O_WRONLY, 0) - readonly := os.IsPermission(err) - f.Close() - fileInfo, serr := os.Stat(filename) if serr != nil && !os.IsNotExist(serr) { return nil, serr @@ -248,6 +244,13 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, if serr == nil && fileInfo.IsDir() { return nil, errors.New("Error: " + filename + " is a directory and cannot be opened") } + if serr == nil && !fileInfo.Mode().IsRegular() { + return nil, errors.New("Error: " + filename + " is not a regular file and cannot be opened") + } + + f, err := os.OpenFile(filename, os.O_WRONLY, 0) + readonly := os.IsPermission(err) + f.Close() file, err := os.Open(filename) if err == nil { diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 1eb2c089..e927ae14 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -162,6 +162,17 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error // Removes any tilde and replaces with the absolute path to home absFilename, _ := util.ReplaceHome(filename) + fileInfo, err := os.Stat(absFilename) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } + if err == nil && fileInfo.IsDir() { + return errors.New("Error: " + absFilename + " is a directory and cannot be saved") + } + if err == nil && !fileInfo.Mode().IsRegular() { + return errors.New("Error: " + absFilename + " is not a regular file and cannot be saved") + } + // Get the leading path to the file | "." is returned if there's no leading path provided if dirname := filepath.Dir(absFilename); dirname != "." { // Check if the parent dirs don't exist From 6066c1a10e6eeef41b6b949b2e423386a4ed46ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Wed, 29 May 2024 20:33:46 +0200 Subject: [PATCH 05/34] buffer: Convert `os.Is()` into `errors.Is()` --- internal/buffer/buffer.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 68f5824c..d8d37883 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "io" + "io/fs" "io/ioutil" "os" "path" @@ -238,7 +239,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, } fileInfo, serr := os.Stat(filename) - if serr != nil && !os.IsNotExist(serr) { + if serr != nil && !errors.Is(serr, fs.ErrNotExist) { return nil, serr } if serr == nil && fileInfo.IsDir() { @@ -249,7 +250,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, } f, err := os.OpenFile(filename, os.O_WRONLY, 0) - readonly := os.IsPermission(err) + readonly := errors.Is(err, fs.ErrPermission) f.Close() file, err := os.Open(filename) @@ -258,7 +259,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer, } var buf *Buffer - if os.IsNotExist(err) { + if errors.Is(err, fs.ErrNotExist) { // File does not exist -- create an empty buffer with that name buf = NewBufferFromString("", filename, btype) } else if err != nil { @@ -392,7 +393,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT // we know the filetype now, so update per-filetype settings config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string)) - if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(config.ConfigDir, "buffers")); errors.Is(err, fs.ErrNotExist) { os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm) } From 7c659d1820a2fd7ab8513735951d82f0cdad225f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 12 May 2024 13:02:09 +0200 Subject: [PATCH 06/34] backup: Convert `os.IsNotExist()` into `errors.Is()` --- internal/buffer/backup.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index a043651e..0e254053 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -1,8 +1,10 @@ package buffer import ( + "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "sync/atomic" @@ -73,7 +75,7 @@ func (b *Buffer) Backup() error { if backupdir == "" || err != nil { backupdir = filepath.Join(config.ConfigDir, "backups") } - if _, err := os.Stat(backupdir); os.IsNotExist(err) { + if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) { os.Mkdir(backupdir, os.ModePerm) } From 0b871e174f3ce81f934ce0875d7f2b4ee93db788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 23 May 2024 23:18:56 +0200 Subject: [PATCH 07/34] backup: Store the file with the endings of the buffer --- internal/buffer/backup.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 0e254053..62e692db 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -87,7 +87,12 @@ func (b *Buffer) Backup() error { } // end of line - eol := []byte{'\n'} + var eol []byte + if b.Endings == FFDos { + eol = []byte{'\r', '\n'} + } else { + eol = []byte{'\n'} + } // write lines if _, e = file.Write(b.lines[0].data); e != nil { From 42ae05b08230ef8fbd48e14bd948182857411ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Fri, 24 May 2024 20:19:55 +0200 Subject: [PATCH 08/34] backup: Lock the buffer lines in `Backup()` --- internal/buffer/backup.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 62e692db..8d6997f0 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -82,6 +82,9 @@ func (b *Buffer) Backup() error { name := filepath.Join(backupdir, util.EscapePath(b.AbsPath)) err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { + b.Lock() + defer b.Unlock() + if len(b.lines) == 0 { return } From 5aac42dbe71cae4d94294860166dcf95d5e92aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 12 May 2024 11:49:49 +0200 Subject: [PATCH 09/34] bindings: Convert `os.IsNotExist()` into `errors.Is()` --- internal/action/bindings.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/action/bindings.go b/internal/action/bindings.go index 67c4634c..1eb2a6f0 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "io/fs" "io/ioutil" "os" "path/filepath" @@ -24,7 +25,7 @@ var Binder = map[string]func(e Event, action string){ } func createBindingsIfNotExist(fname string) { - if _, e := os.Stat(fname); os.IsNotExist(e) { + if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) { ioutil.WriteFile(fname, []byte("{}"), 0644) } } From c2bc4688ddbdafeca6391fbd7ddeaad0c0f856f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 12 May 2024 21:45:01 +0200 Subject: [PATCH 10/34] clean: Inform about all failed write steps --- cmd/micro/clean.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cmd/micro/clean.go b/cmd/micro/clean.go index e4aa7240..29e8174a 100644 --- a/cmd/micro/clean.go +++ b/cmd/micro/clean.go @@ -39,7 +39,12 @@ func CleanConfig() { } fmt.Println("Cleaning default settings") - config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) + + settingsFile := filepath.Join(config.ConfigDir, "settings.json") + err := config.WriteSettings(settingsFile) + if err != nil { + fmt.Println("Error writing settings.json file: " + err.Error()) + } // detect unused options var unusedOptions []string @@ -74,9 +79,9 @@ func CleanConfig() { delete(config.GlobalSettings, s) } - err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json")) + err := config.OverwriteSettings(settingsFile) if err != nil { - fmt.Println("Error writing settings.json file: " + err.Error()) + fmt.Println("Error overwriting settings.json file: " + err.Error()) } fmt.Println("Removed unused options") From e828027cc06b4b162318c6c0c129f49a0579141e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 12 May 2024 14:57:16 +0200 Subject: [PATCH 11/34] clean: Remove some unneeded `filepath.Join()` calls --- cmd/micro/clean.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cmd/micro/clean.go b/cmd/micro/clean.go index 29e8174a..9cf9653e 100644 --- a/cmd/micro/clean.go +++ b/cmd/micro/clean.go @@ -72,7 +72,7 @@ func CleanConfig() { fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s]) } - fmt.Printf("These options will be removed from %s\n", filepath.Join(config.ConfigDir, "settings.json")) + fmt.Printf("These options will be removed from %s\n", settingsFile) if shouldContinue() { for _, s := range unusedOptions { @@ -90,12 +90,13 @@ func CleanConfig() { } // detect incorrectly formatted buffer/ files - files, err := ioutil.ReadDir(filepath.Join(config.ConfigDir, "buffers")) + buffersPath := filepath.Join(config.ConfigDir, "buffers") + files, err := ioutil.ReadDir(buffersPath) if err == nil { var badFiles []string var buffer buffer.SerializedBuffer for _, f := range files { - fname := filepath.Join(config.ConfigDir, "buffers", f.Name()) + fname := filepath.Join(buffersPath, f.Name()) file, e := os.Open(fname) if e == nil { @@ -110,9 +111,9 @@ func CleanConfig() { } if len(badFiles) > 0 { - fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), filepath.Join(config.ConfigDir, "buffers")) + fmt.Printf("Detected %d files with an invalid format in %s\n", len(badFiles), buffersPath) fmt.Println("These files store cursor and undo history.") - fmt.Printf("Removing badly formatted files in %s\n", filepath.Join(config.ConfigDir, "buffers")) + fmt.Printf("Removing badly formatted files in %s\n", buffersPath) if shouldContinue() { removed := 0 From 69064cf808eb16979b985067a67198805a7066af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Fri, 24 May 2024 20:41:58 +0200 Subject: [PATCH 12/34] util: Improve and rename `EscapePath()` to `DetermineEscapePath()` If the new URL encoded path is found then it has precedence over the '%' escaped path. In case none of both is found the new URL approach is used. --- internal/buffer/backup.go | 8 ++++---- internal/buffer/serialize.go | 4 ++-- internal/util/util.go | 28 ++++++++++++++++++++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 8d6997f0..dfbc698c 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -79,7 +79,7 @@ func (b *Buffer) Backup() error { os.Mkdir(backupdir, os.ModePerm) } - name := filepath.Join(backupdir, util.EscapePath(b.AbsPath)) + name := util.DetermineEscapePath(backupdir, b.AbsPath) err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { b.Lock() @@ -123,7 +123,7 @@ func (b *Buffer) RemoveBackup() { if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault { return } - f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) + f := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "backups"), b.AbsPath) os.Remove(f) } @@ -131,13 +131,13 @@ func (b *Buffer) RemoveBackup() { // Returns true if a backup was applied func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault { - backupfile := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) + backupfile := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "backups"), b.AbsPath) if info, err := os.Stat(backupfile); err == nil { backup, err := os.Open(backupfile) if err == nil { defer backup.Close() t := info.ModTime() - msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath)) + msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile) choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true) if choice%3 == 0 { diff --git a/internal/buffer/serialize.go b/internal/buffer/serialize.go index e72311da..06906f76 100644 --- a/internal/buffer/serialize.go +++ b/internal/buffer/serialize.go @@ -31,7 +31,7 @@ func (b *Buffer) Serialize() error { return nil } - name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)) + name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath) return overwriteFile(name, encoding.Nop, func(file io.Writer) error { err := gob.NewEncoder(file).Encode(SerializedBuffer{ @@ -50,7 +50,7 @@ func (b *Buffer) Unserialize() error { if b.Path == "" { return nil } - file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath))) + file, err := os.Open(util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath)) if err == nil { defer file.Close() var buffer SerializedBuffer diff --git a/internal/util/util.go b/internal/util/util.go index bcfeca07..6f8f6325 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "os/user" "path/filepath" @@ -408,8 +409,13 @@ func GetModTime(path string) (time.Time, error) { return info.ModTime(), nil } -// EscapePath replaces every path separator in a given path with a % -func EscapePath(path string) string { +// EscapePathUrl encodes the path in URL query form +func EscapePathUrl(path string) string { + return url.QueryEscape(filepath.ToSlash(path)) +} + +// EscapePathLegacy replaces every path separator in a given path with a % +func EscapePathLegacy(path string) string { path = filepath.ToSlash(path) if runtime.GOOS == "windows" { // ':' is not valid in a path name on Windows but is ok on Unix @@ -418,6 +424,24 @@ func EscapePath(path string) string { return strings.ReplaceAll(path, "/", "%") } +// DetermineEscapePath escapes a path, determining whether it should be escaped +// using URL encoding (preferred, since it encodes unambiguously) or +// legacy encoding with '%' (for backward compatibility, if the legacy-escaped +// path exists in the given directory). +func DetermineEscapePath(dir string, path string) string { + url := filepath.Join(dir, EscapePathUrl(path)) + if _, err := os.Stat(url); err == nil { + return url + } + + legacy := filepath.Join(dir, EscapePathLegacy(path)) + if _, err := os.Stat(legacy); err == nil { + return legacy + } + + return url +} + // GetLeadingWhitespace returns the leading whitespace of the given byte array func GetLeadingWhitespace(b []byte) []byte { ws := []byte{} From 18a81f043ca4e3471fd5992eff2ea624531bc5e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 30 May 2024 21:14:04 +0200 Subject: [PATCH 13/34] util: Generalize the file mode of 0666 with `util.FileMode` --- cmd/micro/debug.go | 2 +- internal/action/bindings.go | 7 ++++--- internal/buffer/save.go | 2 +- internal/config/settings.go | 4 ++-- internal/util/util.go | 3 +++ tools/info-plist.go | 2 +- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/cmd/micro/debug.go b/cmd/micro/debug.go index 5dc708ab..1504a03d 100644 --- a/cmd/micro/debug.go +++ b/cmd/micro/debug.go @@ -18,7 +18,7 @@ func (NullWriter) Write(data []byte) (n int, err error) { // InitLog sets up the debug log system for micro if it has been enabled by compile-time variables func InitLog() { if util.Debug == "ON" { - f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_TRUNC, util.FileMode) if err != nil { log.Fatalf("error opening file: %v", err) } diff --git a/internal/action/bindings.go b/internal/action/bindings.go index 1eb2a6f0..02804bb6 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -15,6 +15,7 @@ import ( "github.com/micro-editor/json5" "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/screen" + "github.com/zyedidia/micro/v2/internal/util" "github.com/micro-editor/tcell/v2" ) @@ -26,7 +27,7 @@ var Binder = map[string]func(e Event, action string){ func createBindingsIfNotExist(fname string) { if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) { - ioutil.WriteFile(fname, []byte("{}"), 0644) + ioutil.WriteFile(fname, []byte("{}"), util.FileMode) } } @@ -305,7 +306,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) { BindKey(k, v, Binder["buffer"]) txt, _ := json.MarshalIndent(parsed, "", " ") - return true, ioutil.WriteFile(filename, append(txt, '\n'), 0644) + return true, ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) } return false, e } @@ -355,7 +356,7 @@ func UnbindKey(k string) error { } txt, _ := json.MarshalIndent(parsed, "", " ") - return ioutil.WriteFile(filename, append(txt, '\n'), 0644) + return ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) } return e } diff --git a/internal/buffer/save.go b/internal/buffer/save.go index e927ae14..29143e4d 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -57,7 +57,7 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, return } - } else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666); err != nil { + } else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, util.FileMode); err != nil { return } diff --git a/internal/config/settings.go b/internal/config/settings.go index 7630915f..cebf1c92 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -356,7 +356,7 @@ func WriteSettings(filename string) error { } txt, _ := json.MarshalIndent(parsedSettings, "", " ") - err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) + err = ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) } return err } @@ -378,7 +378,7 @@ func OverwriteSettings(filename string) error { } txt, _ := json.MarshalIndent(settings, "", " ") - err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) + err = ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) } return err } diff --git a/internal/util/util.go b/internal/util/util.go index 6f8f6325..34f49652 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -46,6 +46,9 @@ var ( Sigterm chan os.Signal ) +// To be used for file writes before umask is applied +const FileMode os.FileMode = 0666 + func init() { var err error SemVersion, err = semver.Make(Version) diff --git a/tools/info-plist.go b/tools/info-plist.go index 1707d6de..33e55291 100644 --- a/tools/info-plist.go +++ b/tools/info-plist.go @@ -37,7 +37,7 @@ func main() { ` - err := os.WriteFile("/tmp/micro-info.plist", []byte(rawInfoPlist), 0644) + err := os.WriteFile("/tmp/micro-info.plist", []byte(rawInfoPlist), 0666) if err != nil { panic(err) } From 6e8daa117a0c5909b794f5f14b15241927026064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 30 May 2024 21:34:11 +0200 Subject: [PATCH 14/34] ioutil: Remove deprecated functions where possible --- cmd/micro/clean.go | 3 +-- cmd/micro/micro.go | 3 +-- internal/action/bindings.go | 13 ++++++------- internal/buffer/autocomplete.go | 8 ++++---- internal/buffer/buffer.go | 3 +-- internal/config/plugin_installer.go | 3 +-- internal/config/rtfiles.go | 11 +++++------ internal/config/settings.go | 7 +++---- runtime/syntax/make_headers.go | 7 +++---- runtime/syntax/syntax_converter.go | 3 +-- tools/remove-nightly-assets.go | 4 ++-- tools/testgen.go | 3 +-- 12 files changed, 29 insertions(+), 39 deletions(-) diff --git a/cmd/micro/clean.go b/cmd/micro/clean.go index 9cf9653e..a4ba076f 100644 --- a/cmd/micro/clean.go +++ b/cmd/micro/clean.go @@ -4,7 +4,6 @@ import ( "bufio" "encoding/gob" "fmt" - "io/ioutil" "os" "path/filepath" "sort" @@ -91,7 +90,7 @@ func CleanConfig() { // detect incorrectly formatted buffer/ files buffersPath := filepath.Join(config.ConfigDir, "buffers") - files, err := ioutil.ReadDir(buffersPath) + files, err := os.ReadDir(buffersPath) if err == nil { var badFiles []string var buffer buffer.SerializedBuffer diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index e90843ba..093b0ef7 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -4,7 +4,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "log" "os" "os/signal" @@ -209,7 +208,7 @@ func LoadInput(args []string) []*buffer.Buffer { // Option 2 // The input is not a terminal, so something is being piped in // and we should read from stdin - input, err = ioutil.ReadAll(os.Stdin) + input, err = io.ReadAll(os.Stdin) if err != nil { screen.TermMessage("Error reading from stdin: ", err) input = []byte{} diff --git a/internal/action/bindings.go b/internal/action/bindings.go index 02804bb6..f4e1579e 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io/fs" - "io/ioutil" "os" "path/filepath" "regexp" @@ -27,7 +26,7 @@ var Binder = map[string]func(e Event, action string){ func createBindingsIfNotExist(fname string) { if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) { - ioutil.WriteFile(fname, []byte("{}"), util.FileMode) + os.WriteFile(fname, []byte("{}"), util.FileMode) } } @@ -39,7 +38,7 @@ func InitBindings() { createBindingsIfNotExist(filename) if _, e := os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) + input, err := os.ReadFile(filename) if err != nil { screen.TermMessage("Error reading bindings.json file: " + err.Error()) return @@ -267,7 +266,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) { filename := filepath.Join(config.ConfigDir, "bindings.json") createBindingsIfNotExist(filename) if _, e = os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) + input, err := os.ReadFile(filename) if err != nil { return false, errors.New("Error reading bindings.json file: " + err.Error()) } @@ -306,7 +305,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) { BindKey(k, v, Binder["buffer"]) txt, _ := json.MarshalIndent(parsed, "", " ") - return true, ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) + return true, os.WriteFile(filename, append(txt, '\n'), util.FileMode) } return false, e } @@ -319,7 +318,7 @@ func UnbindKey(k string) error { filename := filepath.Join(config.ConfigDir, "bindings.json") createBindingsIfNotExist(filename) if _, e = os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) + input, err := os.ReadFile(filename) if err != nil { return errors.New("Error reading bindings.json file: " + err.Error()) } @@ -356,7 +355,7 @@ func UnbindKey(k string) error { } txt, _ := json.MarshalIndent(parsed, "", " ") - return ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) + return os.WriteFile(filename, append(txt, '\n'), util.FileMode) } return e } diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index 8a1c3742..0cd83eab 100644 --- a/internal/buffer/autocomplete.go +++ b/internal/buffer/autocomplete.go @@ -2,7 +2,7 @@ package buffer import ( "bytes" - "io/ioutil" + "io/fs" "os" "sort" "strings" @@ -109,15 +109,15 @@ func FileComplete(b *Buffer) ([]string, []string) { sep := string(os.PathSeparator) dirs := strings.Split(input, sep) - var files []os.FileInfo + var files []fs.DirEntry var err error if len(dirs) > 1 { directories := strings.Join(dirs[:len(dirs)-1], sep) + sep directories, _ = util.ReplaceHome(directories) - files, err = ioutil.ReadDir(directories) + files, err = os.ReadDir(directories) } else { - files, err = ioutil.ReadDir(".") + files, err = os.ReadDir(".") } if err != nil { diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index d8d37883..64894fa6 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "io/fs" - "io/ioutil" "os" "path" "path/filepath" @@ -544,7 +543,7 @@ func (b *Buffer) ReOpen() error { } reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder())) - data, err := ioutil.ReadAll(reader) + data, err := io.ReadAll(reader) txt := string(data) if err != nil { diff --git a/internal/config/plugin_installer.go b/internal/config/plugin_installer.go index 573bcfbe..63b5d5ff 100644 --- a/internal/config/plugin_installer.go +++ b/internal/config/plugin_installer.go @@ -5,7 +5,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "net/http" "os" "path/filepath" @@ -396,7 +395,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error { return err } defer resp.Body.Close() - data, err := ioutil.ReadAll(resp.Body) + data, err := io.ReadAll(resp.Body) if err != nil { return err } diff --git a/internal/config/rtfiles.go b/internal/config/rtfiles.go index 7a34d324..93743cdd 100644 --- a/internal/config/rtfiles.go +++ b/internal/config/rtfiles.go @@ -2,7 +2,6 @@ package config import ( "errors" - "io/ioutil" "log" "os" "path" @@ -81,7 +80,7 @@ func (rf realFile) Name() string { } func (rf realFile) Data() ([]byte, error) { - return ioutil.ReadFile(string(rf)) + return os.ReadFile(string(rf)) } func (af assetFile) Name() string { @@ -107,7 +106,7 @@ func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) { // AddRuntimeFilesFromDirectory registers each file from the given directory for // the filetype which matches the file-pattern func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) { - files, _ := ioutil.ReadDir(directory) + files, _ := os.ReadDir(directory) for _, f := range files { if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok { fullPath := filepath.Join(directory, f.Name()) @@ -194,14 +193,14 @@ func InitPlugins() { // Search ConfigDir for plugin-scripts plugdir := filepath.Join(ConfigDir, "plug") - files, _ := ioutil.ReadDir(plugdir) + files, _ := os.ReadDir(plugdir) isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString for _, d := range files { plugpath := filepath.Join(plugdir, d.Name()) if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() { - srcs, _ := ioutil.ReadDir(plugpath) + srcs, _ := os.ReadDir(plugpath) p := new(Plugin) p.Name = d.Name() p.DirName = d.Name() @@ -209,7 +208,7 @@ func InitPlugins() { if strings.HasSuffix(f.Name(), ".lua") { p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name()))) } else if strings.HasSuffix(f.Name(), ".json") { - data, err := ioutil.ReadFile(filepath.Join(plugdir, d.Name(), f.Name())) + data, err := os.ReadFile(filepath.Join(plugdir, d.Name(), f.Name())) if err != nil { continue } diff --git a/internal/config/settings.go b/internal/config/settings.go index cebf1c92..e49b55e7 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "os" "path/filepath" "reflect" @@ -222,7 +221,7 @@ func ReadSettings() error { parsedSettings = make(map[string]interface{}) filename := filepath.Join(ConfigDir, "settings.json") if _, e := os.Stat(filename); e == nil { - input, err := ioutil.ReadFile(filename) + input, err := os.ReadFile(filename) if err != nil { settingsParseError = true return errors.New("Error reading settings.json file: " + err.Error()) @@ -356,7 +355,7 @@ func WriteSettings(filename string) error { } txt, _ := json.MarshalIndent(parsedSettings, "", " ") - err = ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) + err = os.WriteFile(filename, append(txt, '\n'), util.FileMode) } return err } @@ -378,7 +377,7 @@ func OverwriteSettings(filename string) error { } txt, _ := json.MarshalIndent(settings, "", " ") - err = ioutil.WriteFile(filename, append(txt, '\n'), util.FileMode) + err = os.WriteFile(filename, append(txt, '\n'), util.FileMode) } return err } diff --git a/runtime/syntax/make_headers.go b/runtime/syntax/make_headers.go index c00c27da..dba810c7 100644 --- a/runtime/syntax/make_headers.go +++ b/runtime/syntax/make_headers.go @@ -6,7 +6,6 @@ package main import ( "bytes" "fmt" - "io/ioutil" "os" "strings" "time" @@ -34,7 +33,7 @@ func main() { if len(os.Args) > 1 { os.Chdir(os.Args[1]) } - files, _ := ioutil.ReadDir(".") + files, _ := os.ReadDir(".") for _, f := range files { fname := f.Name() if strings.HasSuffix(fname, ".yaml") { @@ -46,7 +45,7 @@ func main() { func convert(name string) { filename := name + ".yaml" var hdr HeaderYaml - source, err := ioutil.ReadFile(filename) + source, err := os.ReadFile(filename) if err != nil { panic(err) } @@ -68,7 +67,7 @@ func encode(name string, c HeaderYaml) { func decode(name string) Header { start := time.Now() - data, _ := ioutil.ReadFile(name + ".hdr") + data, _ := os.ReadFile(name + ".hdr") strs := bytes.Split(data, []byte{'\n'}) var hdr Header hdr.FileType = string(strs[0]) diff --git a/runtime/syntax/syntax_converter.go b/runtime/syntax/syntax_converter.go index c8af2f35..1bf9ca31 100644 --- a/runtime/syntax/syntax_converter.go +++ b/runtime/syntax/syntax_converter.go @@ -4,7 +4,6 @@ package main import ( "fmt" - "io/ioutil" "os" "regexp" "strings" @@ -161,6 +160,6 @@ func main() { return } - data, _ := ioutil.ReadFile(os.Args[1]) + data, _ := os.ReadFile(os.Args[1]) fmt.Print(generateFile(parseFile(string(data), os.Args[1]))) } diff --git a/tools/remove-nightly-assets.go b/tools/remove-nightly-assets.go index eee39b30..0e0411f7 100644 --- a/tools/remove-nightly-assets.go +++ b/tools/remove-nightly-assets.go @@ -4,7 +4,7 @@ package main import ( "fmt" - "io/ioutil" + "io" "net/http" "os/exec" "strings" @@ -19,7 +19,7 @@ func main() { return } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) var data interface{} diff --git a/tools/testgen.go b/tools/testgen.go index f110202f..580b5a65 100644 --- a/tools/testgen.go +++ b/tools/testgen.go @@ -4,7 +4,6 @@ package main import ( "fmt" - "io/ioutil" "log" "os" "regexp" @@ -210,7 +209,7 @@ func main() { var tests []test for _, filename := range os.Args[1:] { - source, err := ioutil.ReadFile(filename) + source, err := os.ReadFile(filename) if err != nil { log.Fatalln(err) } From 9b53257e50b2959d04d6eff9434c99a09137d7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Wed, 29 May 2024 22:33:33 +0200 Subject: [PATCH 15/34] save: Perform write process safe --- internal/buffer/backup.go | 21 ++++-- internal/buffer/buffer.go | 1 + internal/buffer/save.go | 151 ++++++++++++++++++++++++++------------ 3 files changed, 120 insertions(+), 53 deletions(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index dfbc698c..313761d9 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -65,23 +65,32 @@ func (b *Buffer) RequestBackup() { } } +func (b *Buffer) backupDir() string { + backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) + if backupdir == "" || err != nil { + backupdir = filepath.Join(config.ConfigDir, "backups") + } + return backupdir +} + +func (b *Buffer) keepBackup() bool { + return b.forceKeepBackup || b.Settings["permbackup"].(bool) +} + // Backup saves the current buffer to ConfigDir/backups func (b *Buffer) Backup() error { if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault { return nil } - backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) - if backupdir == "" || err != nil { - backupdir = filepath.Join(config.ConfigDir, "backups") - } + backupdir := b.backupDir() if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) { os.Mkdir(backupdir, os.ModePerm) } name := util.DetermineEscapePath(backupdir, b.AbsPath) - err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { + err := overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { b.Lock() defer b.Unlock() @@ -120,7 +129,7 @@ func (b *Buffer) Backup() error { // RemoveBackup removes any backup file associated with this buffer func (b *Buffer) RemoveBackup() { - if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault { + if !b.Settings["backup"].(bool) || b.keepBackup() || b.Path == "" || b.Type != BTDefault { return } f := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "backups"), b.AbsPath) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 64894fa6..eb8176cf 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -102,6 +102,7 @@ type SharedBuffer struct { diff map[int]DiffStatus requestedBackup bool + forceKeepBackup bool // ReloadDisabled allows the user to disable reloads if they // are viewing a file that is constantly changing diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 29143e4d..6e6fc6e5 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -95,6 +95,50 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, return } +func (b *Buffer) overwrite(name string, withSudo bool) (int, error) { + enc, err := htmlindex.Get(b.Settings["encoding"].(string)) + if err != nil { + return 0, err + } + + var size int + fwriter := func(file io.Writer) error { + if len(b.lines) == 0 { + return err + } + + // end of line + var eol []byte + if b.Endings == FFDos { + eol = []byte{'\r', '\n'} + } else { + eol = []byte{'\n'} + } + + // write lines + if size, err = file.Write(b.lines[0].data); err != nil { + return err + } + + for _, l := range b.lines[1:] { + if _, err = file.Write(eol); err != nil { + return err + } + if _, err = file.Write(l.data); err != nil { + return err + } + size += len(eol) + len(l.data) + } + return err + } + + if err = overwriteFile(name, enc, fwriter, withSudo); err != nil { + return size, err + } + + return size, err +} + // Save saves the buffer to its default path func (b *Buffer) Save() error { return b.SaveAs(b.Path) @@ -159,18 +203,29 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error err = b.Serialize() }() - // Removes any tilde and replaces with the absolute path to home - absFilename, _ := util.ReplaceHome(filename) - - fileInfo, err := os.Stat(absFilename) - if err != nil && !errors.Is(err, fs.ErrNotExist) { + filename, err = util.ReplaceHome(filename) + if err != nil { return err } + + newFile := false + fileInfo, err := os.Stat(filename) + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + newFile = true + } if err == nil && fileInfo.IsDir() { - return errors.New("Error: " + absFilename + " is a directory and cannot be saved") + return errors.New("Error: " + filename + " is a directory and cannot be saved") } if err == nil && !fileInfo.Mode().IsRegular() { - return errors.New("Error: " + absFilename + " is not a regular file and cannot be saved") + return errors.New("Error: " + filename + " is not a regular file and cannot be saved") + } + + absFilename, err := filepath.Abs(filename) + if err != nil { + return err } // Get the leading path to the file | "." is returned if there's no leading path provided @@ -190,49 +245,13 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error } } - var fileSize int - - enc, err := htmlindex.Get(b.Settings["encoding"].(string)) + size, err := b.safeWrite(absFilename, withSudo, newFile) if err != nil { return err } - fwriter := func(file io.Writer) (e error) { - if len(b.lines) == 0 { - return - } - - // end of line - var eol []byte - if b.Endings == FFDos { - eol = []byte{'\r', '\n'} - } else { - eol = []byte{'\n'} - } - - // write lines - if fileSize, e = file.Write(b.lines[0].data); e != nil { - return - } - - for _, l := range b.lines[1:] { - if _, e = file.Write(eol); e != nil { - return - } - if _, e = file.Write(l.data); e != nil { - return - } - fileSize += len(eol) + len(l.data) - } - return - } - - if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil { - return err - } - if !b.Settings["fastdirty"].(bool) { - if fileSize > LargeFileThreshold { + if size > LargeFileThreshold { // For large files 'fastdirty' needs to be on b.Settings["fastdirty"] = true } else { @@ -241,9 +260,47 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error } b.Path = filename - absPath, _ := filepath.Abs(filename) - b.AbsPath = absPath + b.AbsPath = absFilename b.isModified = false b.ReloadSettings(true) return err } + +// safeWrite writes the buffer to a file in a "safe" way, preventing loss of the +// contents of the file if it fails to write the new contents. +// This means that the file is not overwritten directly but by writing to the +// backup file first. +func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) { + backupDir := b.backupDir() + if _, err := os.Stat(backupDir); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return 0, err + } + if err = os.Mkdir(backupDir, os.ModePerm); err != nil { + return 0, err + } + } + + backupName := util.DetermineEscapePath(backupDir, path) + _, err := b.overwrite(backupName, false) + if err != nil { + os.Remove(backupName) + return 0, err + } + + b.forceKeepBackup = true + size, err := b.overwrite(path, withSudo) + if err != nil { + if newFile { + os.Remove(path) + } + return size, err + } + b.forceKeepBackup = false + + if !b.keepBackup() { + os.Remove(backupName) + } + + return size, err +} From 1663a1a6e4957e47404e6c47de9c78a29f79efe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:40:24 +0200 Subject: [PATCH 16/34] actions: Don't overwrite the buffers `Path` This is fully handled within the buffers `save` domain. --- internal/action/actions.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/action/actions.go b/internal/action/actions.go index 3505f2e1..87e81d1b 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -1042,7 +1042,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func()) if err != nil { InfoBar.Error(err) } else { - h.Buf.Path = filename h.Buf.SetName(filename) InfoBar.Message("Saved " + filename) if callback != nil { @@ -1068,7 +1067,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func()) InfoBar.Error(err) } } else { - h.Buf.Path = filename h.Buf.SetName(filename) InfoBar.Message("Saved " + filename) if callback != nil { From 21b7080935d095dd62ea4aa14e78f515327ebabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:29:24 +0200 Subject: [PATCH 17/34] util: Provide `AppendBackupSuffix()` for further transformations --- internal/util/util.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/util/util.go b/internal/util/util.go index 34f49652..f5af968d 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -412,6 +412,10 @@ func GetModTime(path string) (time.Time, error) { return info.ModTime(), nil } +func AppendBackupSuffix(path string) string { + return path + ".micro-backup" +} + // EscapePathUrl encodes the path in URL query form func EscapePathUrl(path string) string { return url.QueryEscape(filepath.ToSlash(path)) From 4ac8c786f5690ba01bbfcce46a9100ebbe67cc12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Fri, 31 May 2024 20:44:38 +0200 Subject: [PATCH 18/34] backup: Perform write process safe --- internal/buffer/backup.go | 55 ++++++++++++++------------------------- internal/buffer/save.go | 3 +++ 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 313761d9..9744e607 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -3,7 +3,6 @@ package buffer import ( "errors" "fmt" - "io" "io/fs" "os" "path/filepath" @@ -13,7 +12,6 @@ import ( "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/util" - "golang.org/x/text/encoding" ) const backupMsg = `A backup was detected for this file. This likely means that micro @@ -77,7 +75,7 @@ func (b *Buffer) keepBackup() bool { return b.forceKeepBackup || b.Settings["permbackup"].(bool) } -// Backup saves the current buffer to ConfigDir/backups +// Backup saves the current buffer to the backups directory func (b *Buffer) Backup() error { if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault { return nil @@ -89,38 +87,25 @@ func (b *Buffer) Backup() error { } name := util.DetermineEscapePath(backupdir, b.AbsPath) - - err := overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { - b.Lock() - defer b.Unlock() - - if len(b.lines) == 0 { - return + if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) { + _, err = b.overwrite(name, false) + if err == nil { + b.requestedBackup = false } + return err + } - // end of line - var eol []byte - if b.Endings == FFDos { - eol = []byte{'\r', '\n'} - } else { - eol = []byte{'\n'} - } - - // write lines - if _, e = file.Write(b.lines[0].data); e != nil { - return - } - - for _, l := range b.lines[1:] { - if _, e = file.Write(eol); e != nil { - return - } - if _, e = file.Write(l.data); e != nil { - return - } - } - return - }, false) + tmp := util.AppendBackupSuffix(name) + _, err := b.overwrite(tmp, false) + if err != nil { + os.Remove(tmp) + return err + } + err = os.Rename(tmp, name) + if err != nil { + os.Remove(tmp) + return err + } b.requestedBackup = false @@ -132,7 +117,7 @@ func (b *Buffer) RemoveBackup() { if !b.Settings["backup"].(bool) || b.keepBackup() || b.Path == "" || b.Type != BTDefault { return } - f := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "backups"), b.AbsPath) + f := util.DetermineEscapePath(b.backupDir(), b.AbsPath) os.Remove(f) } @@ -140,7 +125,7 @@ func (b *Buffer) RemoveBackup() { // Returns true if a backup was applied func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault { - backupfile := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "backups"), b.AbsPath) + backupfile := util.DetermineEscapePath(b.backupDir(), b.AbsPath) if info, err := os.Stat(backupfile); err == nil { backup, err := os.Open(backupfile) if err == nil { diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 6e6fc6e5..13d2a9b7 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -103,6 +103,9 @@ func (b *Buffer) overwrite(name string, withSudo bool) (int, error) { var size int fwriter := func(file io.Writer) error { + b.Lock() + defer b.Unlock() + if len(b.lines) == 0 { return err } From 022ec0228a7acea2e6f7a911885d6b2ae230f13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:52:55 +0200 Subject: [PATCH 19/34] util: Provide `SafeWrite()` to generalize the internal file write process SafeWrite() will create a temporary intermediate file. --- internal/util/util.go | 73 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/internal/util/util.go b/internal/util/util.go index f5af968d..38f3a12d 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" "net/url" "os" @@ -621,3 +622,75 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon } return client.Do(req) } + +// SafeWrite writes bytes to a file in a "safe" way, preventing loss of the +// contents of the file if it fails to write the new contents. +// This means that the file is not overwritten directly but by writing to a +// temporary file first. +// +// If rename is true, write is performed atomically, by renaming the temporary +// file to the target file after the data is successfully written to the +// temporary file. This guarantees that the file will not remain in a corrupted +// state, but it also has limitations, e.g. the file should not be a symlink +// (otherwise SafeWrite silently replaces this symlink with a regular file), +// the file creation date in Linux is not preserved (since the file inode +// changes) etc. Use SafeWrite with rename=true for files that are only created +// and used by micro for its own needs and are not supposed to be used directly +// by the user. +// +// If rename is false, write is performed by overwriting the target file after +// the data is successfully written to the temporary file. +// This means that the target file may remain corrupted if overwrite fails, +// but in such case the temporary file is preserved as a backup so the file +// can be recovered later. So it is less convenient than atomic write but more +// universal. Use SafeWrite with rename=false for files that may be managed +// directly by the user, like settings.json and bindings.json. +func SafeWrite(path string, bytes []byte, rename bool) error { + var err error + if _, err = os.Stat(path); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + // Force rename for new files! + rename = true + } + + var file *os.File + if !rename { + file, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, FileMode) + if err != nil { + return err + } + defer file.Close() + } + + tmp := AppendBackupSuffix(path) + err = os.WriteFile(tmp, bytes, FileMode) + if err != nil { + os.Remove(tmp) + return err + } + + if rename { + err = os.Rename(tmp, path) + } else { + err = file.Truncate(0) + if err == nil { + _, err = file.Write(bytes) + } + if err == nil { + err = file.Sync() + } + } + if err != nil { + if rename { + os.Remove(tmp) + } + return err + } + + if !rename { + os.Remove(tmp) + } + return nil +} From c9723603866853439d15104c31915211a5e3cd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sat, 1 Jun 2024 15:27:06 +0200 Subject: [PATCH 20/34] serialize: Perform write process safe --- internal/buffer/serialize.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/buffer/serialize.go b/internal/buffer/serialize.go index 06906f76..bedac2ac 100644 --- a/internal/buffer/serialize.go +++ b/internal/buffer/serialize.go @@ -1,15 +1,13 @@ package buffer import ( + "bytes" "encoding/gob" "errors" - "io" "os" "path/filepath" "time" - "golang.org/x/text/encoding" - "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/util" ) @@ -31,16 +29,18 @@ func (b *Buffer) Serialize() error { return nil } - name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath) - - return overwriteFile(name, encoding.Nop, func(file io.Writer) error { - err := gob.NewEncoder(file).Encode(SerializedBuffer{ - b.EventHandler, - b.GetActiveCursor().Loc, - b.ModTime, - }) + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(SerializedBuffer{ + b.EventHandler, + b.GetActiveCursor().Loc, + b.ModTime, + }) + if err != nil { return err - }, false) + } + + name := util.DetermineEscapePath(filepath.Join(config.ConfigDir, "buffers"), b.AbsPath) + return util.SafeWrite(name, buf.Bytes(), true) } // Unserialize loads the buffer info from config.ConfigDir/buffers From 63d68ec4412b9cfd9f636538f2181dbdeb283639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sat, 1 Jun 2024 16:27:43 +0200 Subject: [PATCH 21/34] bindings: Perform write process safe --- internal/action/bindings.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/action/bindings.go b/internal/action/bindings.go index f4e1579e..5283afc8 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -24,9 +24,13 @@ var Binder = map[string]func(e Event, action string){ "terminal": TermMapEvent, } +func writeFile(name string, txt []byte) error { + return util.SafeWrite(name, txt, false) +} + func createBindingsIfNotExist(fname string) { if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) { - os.WriteFile(fname, []byte("{}"), util.FileMode) + writeFile(fname, []byte("{}")) } } @@ -305,7 +309,8 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) { BindKey(k, v, Binder["buffer"]) txt, _ := json.MarshalIndent(parsed, "", " ") - return true, os.WriteFile(filename, append(txt, '\n'), util.FileMode) + txt = append(txt, '\n') + return true, writeFile(filename, txt) } return false, e } @@ -355,7 +360,8 @@ func UnbindKey(k string) error { } txt, _ := json.MarshalIndent(parsed, "", " ") - return os.WriteFile(filename, append(txt, '\n'), util.FileMode) + txt = append(txt, '\n') + return writeFile(filename, txt) } return e } From c926649496f0107bfc3b5803fd578389993a6326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sat, 1 Jun 2024 16:38:57 +0200 Subject: [PATCH 22/34] settings: Perform write process safe --- internal/config/settings.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/internal/config/settings.go b/internal/config/settings.go index e49b55e7..6061e49a 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -155,6 +155,10 @@ var ( VolatileSettings map[string]bool ) +func writeFile(name string, txt []byte) error { + return util.SafeWrite(name, txt, false) +} + func init() { ModifiedSettings = make(map[string]bool) VolatileSettings = make(map[string]bool) @@ -355,7 +359,8 @@ func WriteSettings(filename string) error { } txt, _ := json.MarshalIndent(parsedSettings, "", " ") - err = os.WriteFile(filename, append(txt, '\n'), util.FileMode) + txt = append(txt, '\n') + err = writeFile(filename, txt) } return err } @@ -376,8 +381,9 @@ func OverwriteSettings(filename string) error { } } - txt, _ := json.MarshalIndent(settings, "", " ") - err = os.WriteFile(filename, append(txt, '\n'), util.FileMode) + txt, _ := json.MarshalIndent(parsedSettings, "", " ") + txt = append(txt, '\n') + err = writeFile(filename, txt) } return err } From f8d98558f03e363bc645a31e4e3407f83a30d6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:33:45 +0200 Subject: [PATCH 23/34] save: Merge `overwrite()` into `overwriteFile()` and extract `writeFile()` --- internal/buffer/backup.go | 4 +- internal/buffer/save.go | 106 +++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 61 deletions(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 9744e607..2d114620 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -88,7 +88,7 @@ func (b *Buffer) Backup() error { name := util.DetermineEscapePath(backupdir, b.AbsPath) if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) { - _, err = b.overwrite(name, false) + _, err = b.overwriteFile(name, false) if err == nil { b.requestedBackup = false } @@ -96,7 +96,7 @@ func (b *Buffer) Backup() error { } tmp := util.AppendBackupSuffix(name) - _, err := b.overwrite(tmp, false) + _, err := b.overwriteFile(tmp, false) if err != nil { os.Remove(tmp) return err diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 13d2a9b7..e40e5386 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -16,7 +16,6 @@ import ( "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/util" - "golang.org/x/text/encoding" "golang.org/x/text/encoding/htmlindex" "golang.org/x/text/transform" ) @@ -25,10 +24,46 @@ import ( // because hashing is too slow const LargeFileThreshold = 50000 -// overwriteFile opens the given file for writing, truncating if one exists, and then calls -// the supplied function with the file as io.Writer object, also making sure the file is -// closed afterwards. -func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) { +func (b *Buffer) writeFile(file io.Writer) (int, error) { + b.Lock() + defer b.Unlock() + + if len(b.lines) == 0 { + return 0, nil + } + + // end of line + var eol []byte + if b.Endings == FFDos { + eol = []byte{'\r', '\n'} + } else { + eol = []byte{'\n'} + } + + // write lines + size, err := file.Write(b.lines[0].data) + if err != nil { + return 0, err + } + + for _, l := range b.lines[1:] { + if _, err = file.Write(eol); err != nil { + return 0, err + } + if _, err = file.Write(l.data); err != nil { + return 0, err + } + size += len(eol) + len(l.data) + } + return size, nil +} + +func (b *Buffer) overwriteFile(name string, withSudo bool) (int, error) { + enc, err := htmlindex.Get(b.Settings["encoding"].(string)) + if err != nil { + return 0, err + } + var writeCloser io.WriteCloser var screenb bool var cmd *exec.Cmd @@ -38,7 +73,7 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name) if writeCloser, err = cmd.StdinPipe(); err != nil { - return + return 0, err } c = make(chan os.Signal, 1) @@ -55,14 +90,14 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, signal.Notify(util.Sigterm, os.Interrupt) signal.Stop(c) - return + return 0, err } } else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, util.FileMode); err != nil { - return + return 0, err } w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder())) - err = fn(w) + size, err := b.writeFile(w) if err2 := w.Flush(); err2 != nil && err == nil { err = err2 @@ -88,57 +123,10 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, signal.Stop(c) if err != nil { - return err + return size, err } } - return -} - -func (b *Buffer) overwrite(name string, withSudo bool) (int, error) { - enc, err := htmlindex.Get(b.Settings["encoding"].(string)) - if err != nil { - return 0, err - } - - var size int - fwriter := func(file io.Writer) error { - b.Lock() - defer b.Unlock() - - if len(b.lines) == 0 { - return err - } - - // end of line - var eol []byte - if b.Endings == FFDos { - eol = []byte{'\r', '\n'} - } else { - eol = []byte{'\n'} - } - - // write lines - if size, err = file.Write(b.lines[0].data); err != nil { - return err - } - - for _, l := range b.lines[1:] { - if _, err = file.Write(eol); err != nil { - return err - } - if _, err = file.Write(l.data); err != nil { - return err - } - size += len(eol) + len(l.data) - } - return err - } - - if err = overwriteFile(name, enc, fwriter, withSudo); err != nil { - return size, err - } - return size, err } @@ -285,14 +273,14 @@ func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error } backupName := util.DetermineEscapePath(backupDir, path) - _, err := b.overwrite(backupName, false) + _, err := b.overwriteFile(backupName, false) if err != nil { os.Remove(backupName) return 0, err } b.forceKeepBackup = true - size, err := b.overwrite(path, withSudo) + size, err := b.overwriteFile(path, withSudo) if err != nil { if newFile { os.Remove(path) From 9592bb1549e4863f57f4a58ad62f7db15d404fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Tue, 19 Nov 2024 23:12:26 +0100 Subject: [PATCH 24/34] save: Further rework of `overwriteFile()` - extract the open logic into `openFile()` and return a `wrappedFile` - extract the closing logic into `Close()` and make a method of `wrappedFile` - rename `writeFile()` into `Write()` and make a method of `wrappedFile` This allows to use the split parts alone while keeping overwriteFile() as simple interface to use all in a row. --- internal/buffer/backup.go | 4 +- internal/buffer/save.go | 182 +++++++++++++++++++++++--------------- 2 files changed, 115 insertions(+), 71 deletions(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 2d114620..1e0d9518 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -88,7 +88,7 @@ func (b *Buffer) Backup() error { name := util.DetermineEscapePath(backupdir, b.AbsPath) if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) { - _, err = b.overwriteFile(name, false) + _, err = b.overwriteFile(name) if err == nil { b.requestedBackup = false } @@ -96,7 +96,7 @@ func (b *Buffer) Backup() error { } tmp := util.AppendBackupSuffix(name) - _, err := b.overwriteFile(tmp, false) + _, err := b.overwriteFile(tmp) if err != nil { os.Remove(tmp) return err diff --git a/internal/buffer/save.go b/internal/buffer/save.go index e40e5386..a119b6a1 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -24,7 +24,63 @@ import ( // because hashing is too slow const LargeFileThreshold = 50000 -func (b *Buffer) writeFile(file io.Writer) (int, error) { +type wrappedFile struct { + writeCloser io.WriteCloser + withSudo bool + screenb bool + cmd *exec.Cmd + sigChan chan os.Signal +} + +func openFile(name string, withSudo bool) (wrappedFile, error) { + var err error + var writeCloser io.WriteCloser + var screenb bool + var cmd *exec.Cmd + var sigChan chan os.Signal + + if withSudo { + cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name) + writeCloser, err = cmd.StdinPipe() + if err != nil { + return wrappedFile{}, err + } + + sigChan = make(chan os.Signal, 1) + signal.Reset(os.Interrupt) + signal.Notify(sigChan, os.Interrupt) + + screenb = screen.TempFini() + // need to start the process now, otherwise when we flush the file + // contents to its stdin it might hang because the kernel's pipe size + // is too small to handle the full file contents all at once + err = cmd.Start() + if err != nil { + screen.TempStart(screenb) + + signal.Notify(util.Sigterm, os.Interrupt) + signal.Stop(sigChan) + + return wrappedFile{}, err + } + } else { + writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode) + if err != nil { + return wrappedFile{}, err + } + } + + return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil +} + +func (wf wrappedFile) Write(b *Buffer) (int, error) { + enc, err := htmlindex.Get(b.Settings["encoding"].(string)) + if err != nil { + return 0, err + } + + file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, enc.NewEncoder())) + b.Lock() defer b.Unlock() @@ -40,6 +96,14 @@ func (b *Buffer) writeFile(file io.Writer) (int, error) { eol = []byte{'\n'} } + if !wf.withSudo { + f := wf.writeCloser.(*os.File) + err = f.Truncate(0) + if err != nil { + return 0, err + } + } + // write lines size, err := file.Write(b.lines[0].data) if err != nil { @@ -55,78 +119,45 @@ func (b *Buffer) writeFile(file io.Writer) (int, error) { } size += len(eol) + len(l.data) } - return size, nil + + err = file.Flush() + if err == nil && !wf.withSudo { + // Call Sync() on the file to make sure the content is safely on disk. + f := wf.writeCloser.(*os.File) + err = f.Sync() + } + return size, err } -func (b *Buffer) overwriteFile(name string, withSudo bool) (int, error) { - enc, err := htmlindex.Get(b.Settings["encoding"].(string)) +func (wf wrappedFile) Close() error { + err := wf.writeCloser.Close() + if wf.withSudo { + // wait for dd to finish and restart the screen if we used sudo + err := wf.cmd.Wait() + screen.TempStart(wf.screenb) + + signal.Notify(util.Sigterm, os.Interrupt) + signal.Stop(wf.sigChan) + + if err != nil { + return err + } + } + return err +} + +func (b *Buffer) overwriteFile(name string) (int, error) { + file, err := openFile(name, false) if err != nil { return 0, err } - var writeCloser io.WriteCloser - var screenb bool - var cmd *exec.Cmd - var c chan os.Signal + size, err := file.Write(b) - if withSudo { - cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name) - - if writeCloser, err = cmd.StdinPipe(); err != nil { - return 0, err - } - - c = make(chan os.Signal, 1) - signal.Reset(os.Interrupt) - signal.Notify(c, os.Interrupt) - - screenb = screen.TempFini() - // need to start the process now, otherwise when we flush the file - // contents to its stdin it might hang because the kernel's pipe size - // is too small to handle the full file contents all at once - if err = cmd.Start(); err != nil { - screen.TempStart(screenb) - - signal.Notify(util.Sigterm, os.Interrupt) - signal.Stop(c) - - return 0, err - } - } else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, util.FileMode); err != nil { - return 0, err - } - - w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder())) - size, err := b.writeFile(w) - - if err2 := w.Flush(); err2 != nil && err == nil { + err2 := file.Close() + if err2 != nil && err == nil { err = err2 } - // Call Sync() on the file to make sure the content is safely on disk. - // Does not work with sudo as we don't have direct access to the file. - if !withSudo { - f := writeCloser.(*os.File) - if err2 := f.Sync(); err2 != nil && err == nil { - err = err2 - } - } - if err2 := writeCloser.Close(); err2 != nil && err == nil { - err = err2 - } - - if withSudo { - // wait for dd to finish and restart the screen if we used sudo - err := cmd.Wait() - screen.TempStart(screenb) - - signal.Notify(util.Sigterm, os.Interrupt) - signal.Stop(c) - - if err != nil { - return size, err - } - } - return size, err } @@ -262,6 +293,17 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error // This means that the file is not overwritten directly but by writing to the // backup file first. func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) { + file, err := openFile(path, withSudo) + if err != nil { + return 0, err + } + + defer func() { + if newFile && err != nil { + os.Remove(path) + } + }() + backupDir := b.backupDir() if _, err := os.Stat(backupDir); err != nil { if !errors.Is(err, fs.ErrNotExist) { @@ -273,18 +315,15 @@ func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error } backupName := util.DetermineEscapePath(backupDir, path) - _, err := b.overwriteFile(backupName, false) + _, err = b.overwriteFile(backupName) if err != nil { os.Remove(backupName) return 0, err } b.forceKeepBackup = true - size, err := b.overwriteFile(path, withSudo) + size, err := file.Write(b) if err != nil { - if newFile { - os.Remove(path) - } return size, err } b.forceKeepBackup = false @@ -293,5 +332,10 @@ func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error os.Remove(backupName) } + err2 := file.Close() + if err2 != nil && err == nil { + err = err2 + } + return size, err } From e15bb88270a4cb6048f5f1354dd134f97c0cde38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:38:03 +0200 Subject: [PATCH 25/34] micro: Generalize exit behavior --- cmd/micro/micro.go | 55 +++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 093b0ef7..6d0cb1e6 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -98,7 +98,7 @@ func InitFlags() { fmt.Println("Version:", util.Version) fmt.Println("Commit hash:", util.CommitHash) fmt.Println("Compiled on", util.CompileDate) - os.Exit(0) + exit(0) } if *flagOptions { @@ -114,7 +114,7 @@ func InitFlags() { fmt.Printf("-%s value\n", k) fmt.Printf(" \tDefault value: '%v'\n", v) } - os.Exit(0) + exit(0) } if util.Debug == "OFF" && *flagDebug { @@ -135,7 +135,7 @@ func DoPluginFlags() { CleanConfig() } - os.Exit(0) + exit(0) } } @@ -222,12 +222,26 @@ func LoadInput(args []string) []*buffer.Buffer { return buffers } +func exit(rc int) { + for _, b := range buffer.OpenBuffers { + if !b.Modified() { + b.Fini() + } + } + + if screen.Screen != nil { + screen.Screen.Fini() + } + + os.Exit(rc) +} + func main() { defer func() { if util.Stdout.Len() > 0 { fmt.Fprint(os.Stdout, util.Stdout.String()) } - os.Exit(0) + exit(0) }() var err error @@ -287,7 +301,7 @@ func main() { if err != nil { fmt.Println(err) fmt.Println("Fatal: Micro could not initialize a Screen.") - os.Exit(1) + exit(1) } m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string)) clipErr := clipboard.Initialize(m) @@ -306,7 +320,7 @@ func main() { for _, b := range buffer.OpenBuffers { b.Backup() } - os.Exit(1) + exit(1) } }() @@ -434,23 +448,9 @@ func DoEvent() { case f := <-timerChan: f() case <-sighup: - for _, b := range buffer.OpenBuffers { - if !b.Modified() { - b.Fini() - } - } - os.Exit(0) + exit(0) case <-util.Sigterm: - for _, b := range buffer.OpenBuffers { - if !b.Modified() { - b.Fini() - } - } - - if screen.Screen != nil { - screen.Screen.Fini() - } - os.Exit(0) + exit(0) } if e, ok := event.(*tcell.EventError); ok { @@ -458,16 +458,7 @@ func DoEvent() { if e.Err() == io.EOF { // shutdown due to terminal closing/becoming inaccessible - for _, b := range buffer.OpenBuffers { - if !b.Modified() { - b.Fini() - } - } - - if screen.Screen != nil { - screen.Screen.Fini() - } - os.Exit(0) + exit(0) } return } From c4dcef3e66c724a206b05f755120e2891a41971c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 8 Sep 2024 12:42:18 +0200 Subject: [PATCH 26/34] micro: Provide recovery of `settings.json` & `bindings.json` --- cmd/micro/micro.go | 42 +++++++++++++++++++++++++++++++++++++++ internal/buffer/backup.go | 4 ++-- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 6d0cb1e6..c1ceb796 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -7,6 +7,7 @@ import ( "log" "os" "os/signal" + "path/filepath" "regexp" "runtime" "runtime/pprof" @@ -222,6 +223,35 @@ func LoadInput(args []string) []*buffer.Buffer { return buffers } +func checkBackup(name string) error { + target := filepath.Join(config.ConfigDir, name) + backup := util.AppendBackupSuffix(target) + if info, err := os.Stat(backup); err == nil { + input, err := os.ReadFile(backup) + if err == nil { + t := info.ModTime() + msg := fmt.Sprintf(buffer.BackupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), backup) + choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true) + + if choice%3 == 0 { + // recover + err := os.WriteFile(target, input, util.FileMode) + if err != nil { + return err + } + return os.Remove(backup) + } else if choice%3 == 1 { + // delete + return os.Remove(backup) + } else if choice%3 == 2 { + // abort + return errors.New("Aborted") + } + } + } + return nil +} + func exit(rc int) { for _, b := range buffer.OpenBuffers { if !b.Modified() { @@ -269,6 +299,12 @@ func main() { config.InitRuntimeFiles(true) config.InitPlugins() + err = checkBackup("settings.json") + if err != nil { + screen.TermMessage(err) + exit(1) + } + err = config.ReadSettings() if err != nil { screen.TermMessage(err) @@ -329,6 +365,12 @@ func main() { screen.TermMessage(err) } + err = checkBackup("bindings.json") + if err != nil { + screen.TermMessage(err) + exit(1) + } + action.InitBindings() action.InitCommands() diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 1e0d9518..2a05c38c 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -14,7 +14,7 @@ import ( "github.com/zyedidia/micro/v2/internal/util" ) -const backupMsg = `A backup was detected for this file. This likely means that micro +const BackupMsg = `A backup was detected for this file. This likely means that micro crashed while editing this file, or another instance of micro is currently editing this file. @@ -131,7 +131,7 @@ func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { if err == nil { defer backup.Close() t := info.ModTime() - msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile) + msg := fmt.Sprintf(BackupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile) choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true) if choice%3 == 0 { From 8c883c6210d229b47488a85041afcd41d2e65de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:36:14 +0100 Subject: [PATCH 27/34] backup: Rearrange and extend `BackupMsg` --- cmd/micro/micro.go | 2 +- internal/buffer/backup.go | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index c1ceb796..b9cb76c2 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -230,7 +230,7 @@ func checkBackup(name string) error { input, err := os.ReadFile(backup) if err == nil { t := info.ModTime() - msg := fmt.Sprintf(buffer.BackupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), backup) + msg := fmt.Sprintf(buffer.BackupMsg, target, t.Format("Mon Jan _2 at 15:04, 2006"), backup) choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true) if choice%3 == 0 { diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 2a05c38c..305d5694 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -14,11 +14,15 @@ import ( "github.com/zyedidia/micro/v2/internal/util" ) -const BackupMsg = `A backup was detected for this file. This likely means that micro -crashed while editing this file, or another instance of micro is currently -editing this file. +const BackupMsg = `A backup was detected for: -The backup was created on %s, and the file is +%s + +This likely means that micro crashed while editing this file, +or another instance of micro is currently editing this file, +or an error occurred while saving this file so it may be corrupted. + +The backup was created on %s and its path is: %s @@ -131,7 +135,7 @@ func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { if err == nil { defer backup.Close() t := info.ModTime() - msg := fmt.Sprintf(BackupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile) + msg := fmt.Sprintf(BackupMsg, b.Path, t.Format("Mon Jan _2 at 15:04, 2006"), backupfile) choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true) if choice%3 == 0 { From 35d295dd045addb71ecedf7dee2ae7e92d46deb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sat, 12 Oct 2024 14:16:57 +0200 Subject: [PATCH 28/34] buffer: Remove superfluous `backupTime` --- internal/buffer/buffer.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index eb8176cf..0c908c32 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -30,8 +30,6 @@ import ( "golang.org/x/text/transform" ) -const backupTime = 8000 - var ( // OpenBuffers is a list of the currently open buffers OpenBuffers []*Buffer From 771aab251c9373fdc94024f898e974b4f254e607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Tue, 1 Oct 2024 21:53:47 +0200 Subject: [PATCH 29/34] save+backup: Process the `save` & `backup` with a sequential channel As advantage we don't need to synchonize them any longer and don't need further insufficient lock mechanisms. --- internal/buffer/backup.go | 24 +----------------- internal/buffer/save.go | 51 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 25 deletions(-) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index 305d5694..e2cd29f0 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -6,8 +6,6 @@ import ( "io/fs" "os" "path/filepath" - "sync/atomic" - "time" "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/screen" @@ -34,27 +32,7 @@ The backup was created on %s and its path is: Options: [r]ecover, [i]gnore, [a]bort: ` -var backupRequestChan chan *Buffer - -func backupThread() { - for { - time.Sleep(time.Second * 8) - - for len(backupRequestChan) > 0 { - b := <-backupRequestChan - bfini := atomic.LoadInt32(&(b.fini)) != 0 - if !bfini { - b.Backup() - } - } - } -} - -func init() { - backupRequestChan = make(chan *Buffer, 10) - - go backupThread() -} +const backupSeconds = 8 func (b *Buffer) RequestBackup() { if !b.requestedBackup { diff --git a/internal/buffer/save.go b/internal/buffer/save.go index a119b6a1..5ade92e8 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -11,6 +11,8 @@ import ( "os/signal" "path/filepath" "runtime" + "sync/atomic" + "time" "unicode" "github.com/zyedidia/micro/v2/internal/config" @@ -32,6 +34,48 @@ type wrappedFile struct { sigChan chan os.Signal } +type saveResponse struct { + size int + err error +} + +type saveRequest struct { + buf *Buffer + path string + withSudo bool + newFile bool + saveResponseChan chan saveResponse +} + +var saveRequestChan chan saveRequest +var backupRequestChan chan *Buffer + +func init() { + saveRequestChan = make(chan saveRequest, 10) + backupRequestChan = make(chan *Buffer, 10) + + go func() { + duration := backupSeconds * float64(time.Second) + backupTicker := time.NewTicker(time.Duration(duration)) + + for { + select { + case sr := <-saveRequestChan: + size, err := sr.buf.safeWrite(sr.path, sr.withSudo, sr.newFile) + sr.saveResponseChan <- saveResponse{size, err} + case <-backupTicker.C: + for len(backupRequestChan) > 0 { + b := <-backupRequestChan + bfini := atomic.LoadInt32(&(b.fini)) != 0 + if !bfini { + b.Backup() + } + } + } + } + }() +} + func openFile(name string, withSudo bool) (wrappedFile, error) { var err error var writeCloser io.WriteCloser @@ -267,13 +311,16 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error } } - size, err := b.safeWrite(absFilename, withSudo, newFile) + saveResponseChan := make(chan saveResponse) + saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan} + result := <-saveResponseChan + err = result.err if err != nil { return err } if !b.Settings["fastdirty"].(bool) { - if size > LargeFileThreshold { + if result.size > LargeFileThreshold { // For large files 'fastdirty' needs to be on b.Settings["fastdirty"] = true } else { From 79ce93fb7dc4603dd303e78242db2e9a0e2d94cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sat, 12 Oct 2024 16:35:47 +0200 Subject: [PATCH 30/34] backup: Clear the requested backup upon completion notification Now the main go routine takes care of the backup synchronization. --- cmd/micro/micro.go | 2 ++ internal/buffer/backup.go | 14 ++++++++++---- internal/buffer/buffer.go | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index b9cb76c2..9e86a41f 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -489,6 +489,8 @@ func DoEvent() { } case f := <-timerChan: f() + case b := <-buffer.BackupCompleteChan: + b.RequestedBackup = false case <-sighup: exit(0) case <-util.Sigterm: diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index e2cd29f0..cda7a0eb 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -34,14 +34,20 @@ Options: [r]ecover, [i]gnore, [a]bort: ` const backupSeconds = 8 +var BackupCompleteChan chan *Buffer + +func init() { + BackupCompleteChan = make(chan *Buffer, 10) +} + func (b *Buffer) RequestBackup() { - if !b.requestedBackup { + if !b.RequestedBackup { select { case backupRequestChan <- b: default: // channel is full } - b.requestedBackup = true + b.RequestedBackup = true } } @@ -72,7 +78,7 @@ func (b *Buffer) Backup() error { if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) { _, err = b.overwriteFile(name) if err == nil { - b.requestedBackup = false + BackupCompleteChan <- b } return err } @@ -89,7 +95,7 @@ func (b *Buffer) Backup() error { return err } - b.requestedBackup = false + BackupCompleteChan <- b return err } diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 0c908c32..7c41ff95 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -99,7 +99,7 @@ type SharedBuffer struct { diffLock sync.RWMutex diff map[int]DiffStatus - requestedBackup bool + RequestedBackup bool forceKeepBackup bool // ReloadDisabled allows the user to disable reloads if they From 49aebe8aca8308266316d1c9ee9680f3595c3517 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:35:41 +0100 Subject: [PATCH 31/34] save+util: Provide a meaningful error message for safe (over-)write fails --- cmd/micro/clean.go | 14 ++++++++++++-- internal/action/command.go | 23 ++++++++++++++++++++--- internal/buffer/save.go | 5 +++++ internal/util/util.go | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/cmd/micro/clean.go b/cmd/micro/clean.go index a4ba076f..26726ba9 100644 --- a/cmd/micro/clean.go +++ b/cmd/micro/clean.go @@ -3,6 +3,7 @@ package main import ( "bufio" "encoding/gob" + "errors" "fmt" "os" "path/filepath" @@ -11,6 +12,7 @@ import ( "github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/config" + "github.com/zyedidia/micro/v2/internal/util" ) func shouldContinue() bool { @@ -42,7 +44,11 @@ func CleanConfig() { settingsFile := filepath.Join(config.ConfigDir, "settings.json") err := config.WriteSettings(settingsFile) if err != nil { - fmt.Println("Error writing settings.json file: " + err.Error()) + if errors.Is(err, util.ErrOverwrite) { + fmt.Println(err.Error()) + } else { + fmt.Println("Error writing settings.json file: " + err.Error()) + } } // detect unused options @@ -80,7 +86,11 @@ func CleanConfig() { err := config.OverwriteSettings(settingsFile) if err != nil { - fmt.Println("Error overwriting settings.json file: " + err.Error()) + if errors.Is(err, util.ErrOverwrite) { + fmt.Println(err.Error()) + } else { + fmt.Println("Error overwriting settings.json file: " + err.Error()) + } } fmt.Println("Removed unused options") diff --git a/internal/action/command.go b/internal/action/command.go index 8189e598..1970ef10 100644 --- a/internal/action/command.go +++ b/internal/action/command.go @@ -658,7 +658,16 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error { delete(b.LocalSettings, option) } - return config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) + err := config.WriteSettings(filepath.Join(config.ConfigDir, "settings.json")) + if err != nil { + if errors.Is(err, util.ErrOverwrite) { + screen.TermMessage(err) + err = errors.Unwrap(err) + } + return err + } + + return nil } func SetGlobalOption(option, value string) error { @@ -783,7 +792,11 @@ func (h *BufPane) BindCmd(args []string) { _, err := TryBindKey(parseKeyArg(args[0]), args[1], true) if err != nil { - InfoBar.Error(err) + if errors.Is(err, util.ErrOverwrite) { + screen.TermMessage(err) + } else { + InfoBar.Error(err) + } } } @@ -796,7 +809,11 @@ func (h *BufPane) UnbindCmd(args []string) { err := UnbindKey(parseKeyArg(args[0])) if err != nil { - InfoBar.Error(err) + if errors.Is(err, util.ErrOverwrite) { + screen.TermMessage(err) + } else { + InfoBar.Error(err) + } } } diff --git a/internal/buffer/save.go b/internal/buffer/save.go index 5ade92e8..c6346508 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -316,6 +316,10 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error result := <-saveResponseChan err = result.err if err != nil { + if errors.Is(err, util.ErrOverwrite) { + screen.TermMessage(err) + err = errors.Unwrap(err) + } return err } @@ -371,6 +375,7 @@ func (b *Buffer) safeWrite(path string, withSudo bool, newFile bool) (int, error b.forceKeepBackup = true size, err := file.Write(b) if err != nil { + err = util.OverwriteError{err, backupName} return size, err } b.forceKeepBackup = false diff --git a/internal/util/util.go b/internal/util/util.go index 38f3a12d..f2cb2a99 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -45,11 +45,44 @@ var ( Stdout *bytes.Buffer // Sigterm is a channel where micro exits when written Sigterm chan os.Signal + + // To be used for fails on (over-)write with safe writes + ErrOverwrite = OverwriteError{} ) // To be used for file writes before umask is applied const FileMode os.FileMode = 0666 +const OverwriteFailMsg = `An error occurred while writing to the file: + +%s + +The file may be corrupted now. The good news is that it has been +successfully backed up. Next time you open this file with Micro, +Micro will ask if you want to recover it from the backup. + +The backup path is: + +%s` + +// OverwriteError is a custom error to add additional information +type OverwriteError struct { + What error + BackupName string +} + +func (e OverwriteError) Error() string { + return fmt.Sprintf(OverwriteFailMsg, e.What, e.BackupName) +} + +func (e OverwriteError) Is(target error) bool { + return target == ErrOverwrite +} + +func (e OverwriteError) Unwrap() error { + return e.What +} + func init() { var err error SemVersion, err = semver.Make(Version) @@ -685,6 +718,8 @@ func SafeWrite(path string, bytes []byte, rename bool) error { if err != nil { if rename { os.Remove(tmp) + } else { + err = OverwriteError{err, tmp} } return err } From 61640504251e65161a508660354f463231f91c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Sun, 19 Jan 2025 13:38:29 +0100 Subject: [PATCH 32/34] save: Update the modification time of the buffer only in case of file changes --- internal/buffer/save.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/internal/buffer/save.go b/internal/buffer/save.go index c6346508..fd56c8ab 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -263,12 +263,6 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error } } - // Update the last time this file was updated after saving - defer func() { - b.ModTime, _ = util.GetModTime(filename) - err = b.Serialize() - }() - filename, err = util.ReplaceHome(filename) if err != nil { return err @@ -319,6 +313,8 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error if errors.Is(err, util.ErrOverwrite) { screen.TermMessage(err) err = errors.Unwrap(err) + + b.UpdateModTime() } return err } @@ -335,7 +331,10 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error b.Path = filename b.AbsPath = absFilename b.isModified = false + b.UpdateModTime() b.ReloadSettings(true) + + err = b.Serialize() return err } From fe134b92d5ee841bef92a463778b65c54c398fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:26:47 +0100 Subject: [PATCH 33/34] history: Perform write process safe --- internal/info/history.go | 47 ++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/internal/info/history.go b/internal/info/history.go index a09a58cf..ec946957 100644 --- a/internal/info/history.go +++ b/internal/info/history.go @@ -1,12 +1,16 @@ package info import ( + "bytes" "encoding/gob" + "errors" + "io/fs" "os" "path/filepath" "strings" "github.com/zyedidia/micro/v2/internal/config" + "github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/util" ) @@ -17,24 +21,23 @@ func (i *InfoBuf) LoadHistory() { if config.GetGlobalOption("savehistory").(bool) { file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history")) var decodedMap map[string][]string - if err == nil { - defer file.Close() - decoder := gob.NewDecoder(file) - err = decoder.Decode(&decodedMap) - - if err != nil { - i.Error("Error loading history:", err) - return + if err != nil { + if !errors.Is(err, fs.ErrNotExist) { + i.Error("Error loading history: ", err) } + return + } + + defer file.Close() + err = gob.NewDecoder(file).Decode(&decodedMap) + if err != nil { + i.Error("Error decoding history: ", err) + return } if decodedMap != nil { i.History = decodedMap - } else { - i.History = make(map[string][]string) } - } else { - i.History = make(map[string][]string) } } @@ -49,16 +52,18 @@ func (i *InfoBuf) SaveHistory() { } } - file, err := os.Create(filepath.Join(config.ConfigDir, "buffers", "history")) - if err == nil { - defer file.Close() - encoder := gob.NewEncoder(file) + var buf bytes.Buffer + err := gob.NewEncoder(&buf).Encode(i.History) + if err != nil { + screen.TermMessage("Error encoding history: ", err) + return + } - err = encoder.Encode(i.History) - if err != nil { - i.Error("Error saving history:", err) - return - } + filename := filepath.Join(config.ConfigDir, "buffers", "history") + err = util.SafeWrite(filename, buf.Bytes(), true) + if err != nil { + screen.TermMessage("Error saving history: ", err) + return } } } From 8b21724c6eda3cde28b43c8f37771340f223cf6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6ran=20Karl?= <3951388+JoeKar@users.noreply.github.com> Date: Thu, 6 Feb 2025 19:54:47 +0100 Subject: [PATCH 34/34] buffer: Store the `encoding` inside the `buffer` --- internal/buffer/buffer.go | 9 ++++++--- internal/buffer/save.go | 10 ++-------- internal/buffer/settings.go | 8 ++++++++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 7c41ff95..4226d972 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -25,6 +25,7 @@ import ( "github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/pkg/highlight" + "golang.org/x/text/encoding" "golang.org/x/text/encoding/htmlindex" "golang.org/x/text/encoding/unicode" "golang.org/x/text/transform" @@ -87,6 +88,8 @@ type SharedBuffer struct { // LocalSettings customized by the user for this buffer only LocalSettings map[string]bool + encoding encoding.Encoding + Suggestions []string Completions []string CurSuggestion int @@ -337,9 +340,9 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT } config.UpdatePathGlobLocals(b.Settings, absPath) - enc, err := htmlindex.Get(b.Settings["encoding"].(string)) + b.encoding, err = htmlindex.Get(b.Settings["encoding"].(string)) if err != nil { - enc = unicode.UTF8 + b.encoding = unicode.UTF8 b.Settings["encoding"] = "utf-8" } @@ -350,7 +353,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT return NewBufferFromString("", "", btype) } if !hasBackup { - reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder())) + reader := bufio.NewReader(transform.NewReader(r, b.encoding.NewDecoder())) var ff FileFormat = FFAuto diff --git a/internal/buffer/save.go b/internal/buffer/save.go index fd56c8ab..89a88b8e 100644 --- a/internal/buffer/save.go +++ b/internal/buffer/save.go @@ -18,7 +18,6 @@ import ( "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/util" - "golang.org/x/text/encoding/htmlindex" "golang.org/x/text/transform" ) @@ -118,12 +117,7 @@ func openFile(name string, withSudo bool) (wrappedFile, error) { } func (wf wrappedFile) Write(b *Buffer) (int, error) { - enc, err := htmlindex.Get(b.Settings["encoding"].(string)) - if err != nil { - return 0, err - } - - file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, enc.NewEncoder())) + file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder())) b.Lock() defer b.Unlock() @@ -142,7 +136,7 @@ func (wf wrappedFile) Write(b *Buffer) (int, error) { if !wf.withSudo { f := wf.writeCloser.(*os.File) - err = f.Truncate(0) + err := f.Truncate(0) if err != nil { return 0, err } diff --git a/internal/buffer/settings.go b/internal/buffer/settings.go index 3db35e97..9dd46a95 100644 --- a/internal/buffer/settings.go +++ b/internal/buffer/settings.go @@ -7,6 +7,8 @@ import ( "github.com/zyedidia/micro/v2/internal/config" ulua "github.com/zyedidia/micro/v2/internal/lua" "github.com/zyedidia/micro/v2/internal/screen" + "golang.org/x/text/encoding/htmlindex" + "golang.org/x/text/encoding/unicode" luar "layeh.com/gopher-luar" ) @@ -97,6 +99,12 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) { b.UpdateRules() } } else if option == "encoding" { + enc, err := htmlindex.Get(b.Settings["encoding"].(string)) + if err != nil { + enc = unicode.UTF8 + b.Settings["encoding"] = "utf-8" + } + b.encoding = enc b.isModified = true } else if option == "readonly" && b.Type.Kind == BTDefault.Kind { b.Type.Readonly = nativeValue.(bool)