diff --git a/cmd/micro/clean.go b/cmd/micro/clean.go index e4aa72402..a4ba076f6 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" @@ -39,7 +38,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 @@ -67,16 +71,16 @@ 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 { 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") @@ -85,12 +89,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 := os.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 { @@ -105,9 +110,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 diff --git a/cmd/micro/debug.go b/cmd/micro/debug.go index 5dc708abc..1504a03df 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/cmd/micro/micro.go b/cmd/micro/micro.go index 4445a3481..c7d75cdce 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -4,10 +4,10 @@ import ( "flag" "fmt" "io" - "io/ioutil" "log" "os" "os/signal" + "path/filepath" "regexp" "runtime" "runtime/pprof" @@ -99,7 +99,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 { @@ -115,7 +115,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 { @@ -136,7 +136,7 @@ func DoPluginFlags() { CleanConfig() } - os.Exit(0) + exit(0) } } @@ -209,7 +209,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{} @@ -223,12 +223,53 @@ func LoadInput(args []string) []*buffer.Buffer { return buffers } +func processBackup(backup string, target string) error { + 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() { + 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 @@ -256,6 +297,14 @@ func main() { config.InitRuntimeFiles(true) config.InitPlugins() + filename := filepath.Join(config.ConfigDir, "settings.json") + backupname := util.AppendBackupSuffix(filename) + err = processBackup(backupname, filename) + if err != nil { + screen.TermMessage(err) + exit(1) + } + err = config.ReadSettings() if err != nil { screen.TermMessage(err) @@ -288,7 +337,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) @@ -307,7 +356,7 @@ func main() { for _, b := range buffer.OpenBuffers { b.Backup() } - os.Exit(1) + exit(1) } }() @@ -316,6 +365,14 @@ func main() { screen.TermMessage(err) } + filename = filepath.Join(config.ConfigDir, "bindings.json") + backupname = util.AppendBackupSuffix(filename) + err = processBackup(backupname, filename) + if err != nil { + screen.TermMessage(err) + exit(1) + } + action.InitBindings() action.InitCommands() @@ -435,23 +492,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 { @@ -459,16 +502,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 } diff --git a/internal/action/actions.go b/internal/action/actions.go index cf6d954f0..b07d4ce7d 100644 --- a/internal/action/actions.go +++ b/internal/action/actions.go @@ -946,6 +946,9 @@ func (h *BufPane) SaveAsCB(action string, callback func()) bool { h.completeAction(action) return } + } else { + InfoBar.Error(err) + return } } else { InfoBar.YNPrompt( @@ -982,7 +985,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 { @@ -1008,7 +1010,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 { diff --git a/internal/action/bindings.go b/internal/action/bindings.go index 2462d7300..c1422bb5a 100644 --- a/internal/action/bindings.go +++ b/internal/action/bindings.go @@ -4,7 +4,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io/fs" "os" "path/filepath" "regexp" @@ -14,6 +14,7 @@ import ( "github.com/zyedidia/json5" "github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/screen" + "github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/tcell/v2" ) @@ -23,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); os.IsNotExist(e) { - ioutil.WriteFile(fname, []byte("{}"), 0644) + if _, e := os.Stat(fname); errors.Is(e, fs.ErrNotExist) { + writeFile(fname, []byte("{}")) } } @@ -37,7 +42,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 @@ -265,7 +270,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()) } @@ -304,7 +309,8 @@ 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) + txt = append(txt, '\n') + return true, writeFile(filename, txt) } return false, e } @@ -317,7 +323,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()) } @@ -354,7 +360,8 @@ func UnbindKey(k string) error { } txt, _ := json.MarshalIndent(parsed, "", " ") - return ioutil.WriteFile(filename, append(txt, '\n'), 0644) + txt = append(txt, '\n') + return writeFile(filename, txt) } return e } diff --git a/internal/buffer/autocomplete.go b/internal/buffer/autocomplete.go index 8a1c3742a..0cd83eabc 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/backup.go b/internal/buffer/backup.go index a043651eb..27abe3d81 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -1,8 +1,9 @@ package buffer import ( + "errors" "fmt" - "io" + "io/fs" "os" "path/filepath" "sync/atomic" @@ -11,10 +12,9 @@ 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 +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. @@ -63,45 +63,49 @@ func (b *Buffer) RequestBackup() { } } -// 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 - } - +func (b *Buffer) BackupDir() string { backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) if backupdir == "" || err != nil { backupdir = filepath.Join(config.ConfigDir, "backups") } - if _, err := os.Stat(backupdir); os.IsNotExist(err) { - os.Mkdir(backupdir, os.ModePerm) - } + return backupdir +} - name := filepath.Join(backupdir, util.EscapePath(b.AbsPath)) +func (b *Buffer) KeepBackup() bool { + return b.Settings["permbackup"].(bool) || b.keepBackup +} - err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { - if len(b.lines) == 0 { - return - } +// 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 + } - // end of line - eol := []byte{'\n'} + backupdir := b.BackupDir() + if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) { + os.Mkdir(backupdir, os.ModePerm) + } - // write lines - if _, e = file.Write(b.lines[0].data); e != nil { - return + name := util.DetermineEscapePath(backupdir, b.AbsPath) + if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) { + err = b.Overwrite(name, true, false) + if err == nil { + b.requestedBackup = false } + return err + } - 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, true, 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 @@ -110,10 +114,10 @@ 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 := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) + f := util.DetermineEscapePath(b.BackupDir(), b.AbsPath) os.Remove(f) } @@ -121,13 +125,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(b.BackupDir(), 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/buffer.go b/internal/buffer/buffer.go index c623fd586..5f0e0e3ef 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -7,7 +7,7 @@ import ( "errors" "fmt" "io" - "io/ioutil" + "io/fs" "os" "path" "path/filepath" @@ -102,6 +102,7 @@ type SharedBuffer struct { diff map[int]DiffStatus requestedBackup bool + keepBackup bool // ReloadDisabled allows the user to disable reloads if they // are viewing a file that is constantly changing @@ -232,17 +233,20 @@ 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) { + if serr != nil && !errors.Is(serr, fs.ErrNotExist) { return nil, serr } 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 := errors.Is(err, fs.ErrPermission) + f.Close() file, err := os.Open(filename) if err == nil { @@ -250,7 +254,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 { @@ -395,7 +399,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT // init local settings again now that we know the filetype config.InitLocalSettings(b.Settings, b.Path) - 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) } @@ -546,7 +550,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/buffer/save.go b/internal/buffer/save.go index efbf04f4c..bf13335b7 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" @@ -54,7 +55,7 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, screen.TempStart(screenb) return err } - } 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 } @@ -88,6 +89,62 @@ func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, return } +func (b *Buffer) Overwrite(name string, isBackup bool, withSudo bool) (err error) { + enc, err := htmlindex.Get(b.Settings["encoding"].(string)) + if err != nil { + return + } + + var size int + fwriter := func(file io.Writer) (e error) { + b.Lock() + defer b.Unlock() + + 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 size, err = file.Write(b.lines[0].data); err != nil { + return + } + + for _, l := range b.lines[1:] { + if _, err = file.Write(eol); err != nil { + return + } + if _, err = file.Write(l.data); err != nil { + return + } + size += len(eol) + len(l.data) + } + return + } + + if err = overwriteFile(name, enc, fwriter, withSudo); err != nil { + return + } + + if !isBackup && !b.Settings["fastdirty"].(bool) { + if size > LargeFileThreshold { + // For large files 'fastdirty' needs to be on + b.Settings["fastdirty"] = true + } else { + calcHash(b, &b.origHash) + } + } + + return err +} + // Save saves the buffer to its default path func (b *Buffer) Save() error { return b.SaveAs(b.Path) @@ -152,13 +209,23 @@ 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(filename) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return err + } + if err == nil && fileInfo.IsDir() { + return errors.New("Error: " + filename + " is a directory and cannot be saved") + } + if err == nil && !fileInfo.Mode().IsRegular() { + return errors.New("Error: " + filename + " is not a regular file and cannot be saved") + } + + absFilename, _ := filepath.Abs(filename) // 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 @@ -172,60 +239,50 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error } } - var fileSize int - - enc, err := htmlindex.Get(b.Settings["encoding"].(string)) - if err != nil { + if err = b.safeWrite(absFilename, withSudo); err != nil { return err } - fwriter := func(file io.Writer) (e error) { - if len(b.lines) == 0 { - return - } + b.Path, _ = util.ReplaceHome(filename) + b.AbsPath = absFilename + b.isModified = false + b.UpdateRules() + return err +} - // end of line - var eol []byte - if b.Endings == FFDos { - eol = []byte{'\r', '\n'} - } else { - eol = []byte{'\n'} +// safeWrite performs the following actions: +// 1. Create a backup directory if it doesn't exist +// 2. Create or update a backup file first at the given location +// 2.1. If this fails remove the corrupted backup file and return with error +// 3. Create or update the target file +// 3.1. If this fails keep the backup file and return with error +// 4. Remove the backup file, in case it shouldn't be kept and return +func (b *Buffer) safeWrite(name string, withSudo bool) error { + backupDir := b.BackupDir() + if _, err := os.Stat(backupDir); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err } - - // write lines - if fileSize, e = file.Write(b.lines[0].data); e != nil { - return + if err = os.Mkdir(backupDir, os.ModePerm); err != nil { + return err } + } - 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 + backupName := util.DetermineEscapePath(backupDir, name) + if err := b.Overwrite(backupName, true, withSudo); err != nil { + os.Remove(backupName) + return err } + b.keepBackup = true - if err = overwriteFile(absFilename, enc, fwriter, withSudo); err != nil { + if err := b.Overwrite(name, false, withSudo); err != nil { return err } + b.keepBackup = false - if !b.Settings["fastdirty"].(bool) { - if fileSize > LargeFileThreshold { - // For large files 'fastdirty' needs to be on - b.Settings["fastdirty"] = true - } else { - calcHash(b, &b.origHash) - } + if !b.KeepBackup() { + os.Remove(backupName) } - b.Path = filename - absPath, _ := filepath.Abs(filename) - b.AbsPath = absPath - b.isModified = false - b.UpdateRules() - return err + return nil } diff --git a/internal/buffer/serialize.go b/internal/buffer/serialize.go index e72311da8..bedac2ac5 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 := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(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 @@ -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/config/plugin_installer.go b/internal/config/plugin_installer.go index ef2d4cb49..d13a79971 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 24adaebfa..27cb07d24 100644 --- a/internal/config/rtfiles.go +++ b/internal/config/rtfiles.go @@ -2,7 +2,6 @@ package config import ( "errors" - "io/ioutil" "log" "os" "path" @@ -87,7 +86,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 { @@ -117,7 +116,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()) @@ -204,14 +203,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() @@ -219,7 +218,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 b8cfcd516..23a7078ab 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" @@ -151,6 +150,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) @@ -217,7 +220,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()) @@ -342,7 +345,8 @@ func WriteSettings(filename string) error { } txt, _ := json.MarshalIndent(parsedSettings, "", " ") - err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) + txt = append(txt, '\n') + err = writeFile(filename, txt) } return err } @@ -363,8 +367,9 @@ func OverwriteSettings(filename string) error { } } - txt, _ := json.MarshalIndent(settings, "", " ") - err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) + txt, _ := json.MarshalIndent(parsedSettings, "", " ") + txt = append(txt, '\n') + err = writeFile(filename, txt) } return err } diff --git a/internal/util/util.go b/internal/util/util.go index 83dc4458a..546ee9e46 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" "io" + "io/fs" "net/http" + "net/url" "os" "os/user" "path/filepath" @@ -43,6 +45,9 @@ var ( Stdout *bytes.Buffer // Sigterm is a channel where micro exits when written Sigterm chan os.Signal + + // To be used for file writes before umask is applied + FileMode os.FileMode = 0666 ) func init() { @@ -398,8 +403,17 @@ 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 { +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)) +} + +// 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 @@ -408,6 +422,21 @@ func EscapePath(path string) string { return strings.ReplaceAll(path, "/", "%") } +// DetermineEscapePath is a helper to apply the URL or legacy approach +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{} @@ -585,3 +614,41 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon } return client.Do(req) } + +// SafeWrite performs the following actions: +// 1. If not exists try to write the file and return +// 2. Create a derived temporary file first +// 2.1. If this fails remove the corrupted temporary file and return with error +// 3. Rename the temporary to the target file or overwrite the target file +// 3.1. If this fails remove the temporary in case of rename otherwise keep the file and return with error +// 4. Remove the temporary file +func SafeWrite(name string, bytes []byte, rename bool) error { + if _, err := os.Stat(name); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + return os.WriteFile(name, bytes, FileMode) + } + + tmp := AppendBackupSuffix(name) + err := os.WriteFile(tmp, bytes, FileMode) + if err != nil { + os.Remove(tmp) + return err + } + + if rename { + err = os.Rename(tmp, name) + } else { + err = os.WriteFile(name, bytes, FileMode) + } + if err != nil { + if rename { + os.Remove(tmp) + } + return err + } + + os.Remove(tmp) + return nil +} diff --git a/runtime/syntax/make_headers.go b/runtime/syntax/make_headers.go index c00c27da8..dba810c73 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 c8af2f35f..1bf9ca313 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/info-plist.go b/tools/info-plist.go index 1707d6de1..33e552914 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) } diff --git a/tools/remove-nightly-assets.go b/tools/remove-nightly-assets.go index 9367408c7..4c81b4c6d 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 f110202fa..580b5a65e 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) }