Merge pull request #3273 from JoeKar/fix/save-atomically
Some checks failed
Build and Test / test (1.19.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.19.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.19.x, windows-latest) (push) Has been cancelled
Build and Test / test (1.23.x, macos-latest) (push) Has been cancelled
Build and Test / test (1.23.x, ubuntu-latest) (push) Has been cancelled
Build and Test / test (1.23.x, windows-latest) (push) Has been cancelled

save: Perform write process safe
This commit is contained in:
Jöran Karl 2025-03-08 14:04:41 +01:00 committed by GitHub
commit 98356765c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 657 additions and 282 deletions

View File

@ -3,8 +3,8 @@ package main
import ( import (
"bufio" "bufio"
"encoding/gob" "encoding/gob"
"errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@ -12,6 +12,7 @@ import (
"github.com/zyedidia/micro/v2/internal/buffer" "github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util"
) )
func shouldContinue() bool { func shouldContinue() bool {
@ -39,7 +40,16 @@ func CleanConfig() {
} }
fmt.Println("Cleaning default settings") 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 {
if errors.Is(err, util.ErrOverwrite) {
fmt.Println(err.Error())
} else {
fmt.Println("Error writing settings.json file: " + err.Error())
}
}
// detect unused options // detect unused options
var unusedOptions []string var unusedOptions []string
@ -67,16 +77,20 @@ func CleanConfig() {
fmt.Printf("%s (value: %v)\n", s, config.GlobalSettings[s]) 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() { if shouldContinue() {
for _, s := range unusedOptions { for _, s := range unusedOptions {
delete(config.GlobalSettings, s) delete(config.GlobalSettings, s)
} }
err := config.OverwriteSettings(filepath.Join(config.ConfigDir, "settings.json")) err := config.OverwriteSettings(settingsFile)
if err != nil { 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 overwriting settings.json file: " + err.Error())
}
} }
fmt.Println("Removed unused options") fmt.Println("Removed unused options")
@ -85,12 +99,13 @@ func CleanConfig() {
} }
// detect incorrectly formatted buffer/ files // 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 { if err == nil {
var badFiles []string var badFiles []string
var buffer buffer.SerializedBuffer var buffer buffer.SerializedBuffer
for _, f := range files { for _, f := range files {
fname := filepath.Join(config.ConfigDir, "buffers", f.Name()) fname := filepath.Join(buffersPath, f.Name())
file, e := os.Open(fname) file, e := os.Open(fname)
if e == nil { if e == nil {
@ -105,9 +120,9 @@ func CleanConfig() {
} }
if len(badFiles) > 0 { 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.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() { if shouldContinue() {
removed := 0 removed := 0

View File

@ -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 // InitLog sets up the debug log system for micro if it has been enabled by compile-time variables
func InitLog() { func InitLog() {
if util.Debug == "ON" { 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 { if err != nil {
log.Fatalf("error opening file: %v", err) log.Fatalf("error opening file: %v", err)
} }

View File

@ -4,10 +4,10 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"runtime/pprof" "runtime/pprof"
@ -99,7 +99,7 @@ func InitFlags() {
fmt.Println("Version:", util.Version) fmt.Println("Version:", util.Version)
fmt.Println("Commit hash:", util.CommitHash) fmt.Println("Commit hash:", util.CommitHash)
fmt.Println("Compiled on", util.CompileDate) fmt.Println("Compiled on", util.CompileDate)
os.Exit(0) exit(0)
} }
if *flagOptions { if *flagOptions {
@ -115,7 +115,7 @@ func InitFlags() {
fmt.Printf("-%s value\n", k) fmt.Printf("-%s value\n", k)
fmt.Printf(" \tDefault value: '%v'\n", v) fmt.Printf(" \tDefault value: '%v'\n", v)
} }
os.Exit(0) exit(0)
} }
if util.Debug == "OFF" && *flagDebug { if util.Debug == "OFF" && *flagDebug {
@ -136,7 +136,7 @@ func DoPluginFlags() {
CleanConfig() CleanConfig()
} }
os.Exit(0) exit(0)
} }
} }
@ -209,7 +209,7 @@ func LoadInput(args []string) []*buffer.Buffer {
// Option 2 // Option 2
// The input is not a terminal, so something is being piped in // The input is not a terminal, so something is being piped in
// and we should read from stdin // and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin) input, err = io.ReadAll(os.Stdin)
if err != nil { if err != nil {
screen.TermMessage("Error reading from stdin: ", err) screen.TermMessage("Error reading from stdin: ", err)
input = []byte{} input = []byte{}
@ -223,12 +223,55 @@ func LoadInput(args []string) []*buffer.Buffer {
return buffers 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, 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 {
// 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() { func main() {
defer func() { defer func() {
if util.Stdout.Len() > 0 { if util.Stdout.Len() > 0 {
fmt.Fprint(os.Stdout, util.Stdout.String()) fmt.Fprint(os.Stdout, util.Stdout.String())
} }
os.Exit(0) exit(0)
}() }()
var err error var err error
@ -256,6 +299,12 @@ func main() {
config.InitRuntimeFiles(true) config.InitRuntimeFiles(true)
config.InitPlugins() config.InitPlugins()
err = checkBackup("settings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
err = config.ReadSettings() err = config.ReadSettings()
if err != nil { if err != nil {
screen.TermMessage(err) screen.TermMessage(err)
@ -288,7 +337,7 @@ func main() {
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
fmt.Println("Fatal: Micro could not initialize a Screen.") fmt.Println("Fatal: Micro could not initialize a Screen.")
os.Exit(1) exit(1)
} }
m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string)) m := clipboard.SetMethod(config.GetGlobalOption("clipboard").(string))
clipErr := clipboard.Initialize(m) clipErr := clipboard.Initialize(m)
@ -307,7 +356,7 @@ func main() {
for _, b := range buffer.OpenBuffers { for _, b := range buffer.OpenBuffers {
b.Backup() b.Backup()
} }
os.Exit(1) exit(1)
} }
}() }()
@ -316,6 +365,12 @@ func main() {
screen.TermMessage(err) screen.TermMessage(err)
} }
err = checkBackup("bindings.json")
if err != nil {
screen.TermMessage(err)
exit(1)
}
action.InitBindings() action.InitBindings()
action.InitCommands() action.InitCommands()
@ -434,24 +489,12 @@ func DoEvent() {
} }
case f := <-timerChan: case f := <-timerChan:
f() f()
case b := <-buffer.BackupCompleteChan:
b.RequestedBackup = false
case <-sighup: case <-sighup:
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() {
b.Fini()
}
}
os.Exit(0)
case <-util.Sigterm: case <-util.Sigterm:
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
} }
if e, ok := event.(*tcell.EventError); ok { if e, ok := event.(*tcell.EventError); ok {
@ -459,16 +502,7 @@ func DoEvent() {
if e.Err() == io.EOF { if e.Err() == io.EOF {
// shutdown due to terminal closing/becoming inaccessible // shutdown due to terminal closing/becoming inaccessible
for _, b := range buffer.OpenBuffers { exit(0)
if !b.Modified() {
b.Fini()
}
}
if screen.Screen != nil {
screen.Screen.Fini()
}
os.Exit(0)
} }
return return
} }

View File

@ -1003,6 +1003,9 @@ func (h *BufPane) SaveAsCB(action string, callback func()) bool {
h.completeAction(action) h.completeAction(action)
return return
} }
} else {
InfoBar.Error(err)
return
} }
} else { } else {
InfoBar.YNPrompt( InfoBar.YNPrompt(
@ -1039,7 +1042,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func())
if err != nil { if err != nil {
InfoBar.Error(err) InfoBar.Error(err)
} else { } else {
h.Buf.Path = filename
h.Buf.SetName(filename) h.Buf.SetName(filename)
InfoBar.Message("Saved " + filename) InfoBar.Message("Saved " + filename)
if callback != nil { if callback != nil {
@ -1065,7 +1067,6 @@ func (h *BufPane) saveBufToFile(filename string, action string, callback func())
InfoBar.Error(err) InfoBar.Error(err)
} }
} else { } else {
h.Buf.Path = filename
h.Buf.SetName(filename) h.Buf.SetName(filename)
InfoBar.Message("Saved " + filename) InfoBar.Message("Saved " + filename)
if callback != nil { if callback != nil {

View File

@ -4,7 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -14,6 +14,7 @@ import (
"github.com/micro-editor/json5" "github.com/micro-editor/json5"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util"
"github.com/micro-editor/tcell/v2" "github.com/micro-editor/tcell/v2"
) )
@ -23,9 +24,13 @@ var Binder = map[string]func(e Event, action string){
"terminal": TermMapEvent, "terminal": TermMapEvent,
} }
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
}
func createBindingsIfNotExist(fname 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) writeFile(fname, []byte("{}"))
} }
} }
@ -37,7 +42,7 @@ func InitBindings() {
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e := os.Stat(filename); e == nil { if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
screen.TermMessage("Error reading bindings.json file: " + err.Error()) screen.TermMessage("Error reading bindings.json file: " + err.Error())
return return
@ -265,7 +270,7 @@ func TryBindKey(k, v string, overwrite bool) (bool, error) {
filename := filepath.Join(config.ConfigDir, "bindings.json") filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil { if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
return false, errors.New("Error reading bindings.json file: " + err.Error()) 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"]) BindKey(k, v, Binder["buffer"])
txt, _ := json.MarshalIndent(parsed, "", " ") 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 return false, e
} }
@ -317,7 +323,7 @@ func UnbindKey(k string) error {
filename := filepath.Join(config.ConfigDir, "bindings.json") filename := filepath.Join(config.ConfigDir, "bindings.json")
createBindingsIfNotExist(filename) createBindingsIfNotExist(filename)
if _, e = os.Stat(filename); e == nil { if _, e = os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
return errors.New("Error reading bindings.json file: " + err.Error()) return errors.New("Error reading bindings.json file: " + err.Error())
} }
@ -354,7 +360,8 @@ func UnbindKey(k string) error {
} }
txt, _ := json.MarshalIndent(parsed, "", " ") txt, _ := json.MarshalIndent(parsed, "", " ")
return ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
return writeFile(filename, txt)
} }
return e return e
} }

View File

@ -658,7 +658,16 @@ func SetGlobalOptionNative(option string, nativeValue interface{}) error {
delete(b.LocalSettings, option) 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 { func SetGlobalOption(option, value string) error {
@ -783,7 +792,11 @@ func (h *BufPane) BindCmd(args []string) {
_, err := TryBindKey(parseKeyArg(args[0]), args[1], true) _, err := TryBindKey(parseKeyArg(args[0]), args[1], true)
if err != nil { 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])) err := UnbindKey(parseKeyArg(args[0]))
if err != nil { if err != nil {
InfoBar.Error(err) if errors.Is(err, util.ErrOverwrite) {
screen.TermMessage(err)
} else {
InfoBar.Error(err)
}
} }
} }
@ -890,7 +907,10 @@ func (h *BufPane) SaveCmd(args []string) {
if len(args) == 0 { if len(args) == 0 {
h.Save() h.Save()
} else { } else {
h.Buf.SaveAs(args[0]) err := h.Buf.SaveAs(args[0])
if err != nil {
InfoBar.Error(err)
}
} }
} }

View File

@ -2,7 +2,7 @@ package buffer
import ( import (
"bytes" "bytes"
"io/ioutil" "io/fs"
"os" "os"
"sort" "sort"
"strings" "strings"
@ -109,15 +109,15 @@ func FileComplete(b *Buffer) ([]string, []string) {
sep := string(os.PathSeparator) sep := string(os.PathSeparator)
dirs := strings.Split(input, sep) dirs := strings.Split(input, sep)
var files []os.FileInfo var files []fs.DirEntry
var err error var err error
if len(dirs) > 1 { if len(dirs) > 1 {
directories := strings.Join(dirs[:len(dirs)-1], sep) + sep directories := strings.Join(dirs[:len(dirs)-1], sep) + sep
directories, _ = util.ReplaceHome(directories) directories, _ = util.ReplaceHome(directories)
files, err = ioutil.ReadDir(directories) files, err = os.ReadDir(directories)
} else { } else {
files, err = ioutil.ReadDir(".") files, err = os.ReadDir(".")
} }
if err != nil { if err != nil {

View File

@ -1,24 +1,26 @@
package buffer package buffer
import ( import (
"errors"
"fmt" "fmt"
"io" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sync/atomic"
"time"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "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:
crashed while editing this file, or another instance of micro is currently
editing this file.
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 %s
@ -30,90 +32,80 @@ The backup was created on %s, and the file is
Options: [r]ecover, [i]gnore, [a]bort: ` Options: [r]ecover, [i]gnore, [a]bort: `
var backupRequestChan chan *Buffer const backupSeconds = 8
func backupThread() { var BackupCompleteChan chan *Buffer
for {
time.Sleep(time.Second * 8)
for len(backupRequestChan) > 0 {
b := <-backupRequestChan
bfini := atomic.LoadInt32(&(b.fini)) != 0
if !bfini {
b.Backup()
}
}
}
}
func init() { func init() {
backupRequestChan = make(chan *Buffer, 10) BackupCompleteChan = make(chan *Buffer, 10)
go backupThread()
} }
func (b *Buffer) RequestBackup() { func (b *Buffer) RequestBackup() {
if !b.requestedBackup { if !b.RequestedBackup {
select { select {
case backupRequestChan <- b: case backupRequestChan <- b:
default: default:
// channel is full // channel is full
} }
b.requestedBackup = true b.RequestedBackup = true
} }
} }
// Backup saves the current buffer to ConfigDir/backups 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 the backups directory
func (b *Buffer) Backup() error { func (b *Buffer) Backup() error {
if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault { if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault {
return nil return nil
} }
backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) backupdir := b.backupDir()
if backupdir == "" || err != nil { if _, err := os.Stat(backupdir); errors.Is(err, fs.ErrNotExist) {
backupdir = filepath.Join(config.ConfigDir, "backups")
}
if _, err := os.Stat(backupdir); os.IsNotExist(err) {
os.Mkdir(backupdir, os.ModePerm) os.Mkdir(backupdir, os.ModePerm)
} }
name := filepath.Join(backupdir, util.EscapePath(b.AbsPath)) name := util.DetermineEscapePath(backupdir, b.AbsPath)
if _, err := os.Stat(name); errors.Is(err, fs.ErrNotExist) {
err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { _, err = b.overwriteFile(name)
if len(b.lines) == 0 { if err == nil {
return BackupCompleteChan <- b
} }
return err
}
// end of line tmp := util.AppendBackupSuffix(name)
eol := []byte{'\n'} _, err := b.overwriteFile(tmp)
if err != nil {
os.Remove(tmp)
return err
}
err = os.Rename(tmp, name)
if err != nil {
os.Remove(tmp)
return err
}
// write lines BackupCompleteChan <- b
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)
b.requestedBackup = false
return err return err
} }
// RemoveBackup removes any backup file associated with this buffer // RemoveBackup removes any backup file associated with this buffer
func (b *Buffer) RemoveBackup() { 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 return
} }
f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) f := util.DetermineEscapePath(b.backupDir(), b.AbsPath)
os.Remove(f) os.Remove(f)
} }
@ -121,13 +113,13 @@ func (b *Buffer) RemoveBackup() {
// Returns true if a backup was applied // Returns true if a backup was applied
func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) { func (b *Buffer) ApplyBackup(fsize int64) (bool, bool) {
if b.Settings["backup"].(bool) && !b.Settings["permbackup"].(bool) && len(b.Path) > 0 && b.Type == BTDefault { 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 { if info, err := os.Stat(backupfile); err == nil {
backup, err := os.Open(backupfile) backup, err := os.Open(backupfile)
if err == nil { if err == nil {
defer backup.Close() defer backup.Close()
t := info.ModTime() t := info.ModTime()
msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 at 15:04, 2006"), util.EscapePath(b.AbsPath)) 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) choice := screen.TermPrompt(msg, []string{"r", "i", "a", "recover", "ignore", "abort"}, true)
if choice%3 == 0 { if choice%3 == 0 {

View File

@ -7,7 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/fs"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -25,13 +25,12 @@ import (
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"github.com/zyedidia/micro/v2/pkg/highlight" "github.com/zyedidia/micro/v2/pkg/highlight"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex" "golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode" "golang.org/x/text/encoding/unicode"
"golang.org/x/text/transform" "golang.org/x/text/transform"
) )
const backupTime = 8000
var ( var (
// OpenBuffers is a list of the currently open buffers // OpenBuffers is a list of the currently open buffers
OpenBuffers []*Buffer OpenBuffers []*Buffer
@ -89,6 +88,8 @@ type SharedBuffer struct {
// LocalSettings customized by the user for this buffer only // LocalSettings customized by the user for this buffer only
LocalSettings map[string]bool LocalSettings map[string]bool
encoding encoding.Encoding
Suggestions []string Suggestions []string
Completions []string Completions []string
CurSuggestion int CurSuggestion int
@ -101,7 +102,8 @@ type SharedBuffer struct {
diffLock sync.RWMutex diffLock sync.RWMutex
diff map[int]DiffStatus diff map[int]DiffStatus
requestedBackup bool RequestedBackup bool
forceKeepBackup bool
// ReloadDisabled allows the user to disable reloads if they // ReloadDisabled allows the user to disable reloads if they
// are viewing a file that is constantly changing // are viewing a file that is constantly changing
@ -237,17 +239,20 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
return nil, err return nil, err
} }
f, err := os.OpenFile(filename, os.O_WRONLY, 0)
readonly := os.IsPermission(err)
f.Close()
fileInfo, serr := os.Stat(filename) fileInfo, serr := os.Stat(filename)
if serr != nil && !os.IsNotExist(serr) { if serr != nil && !errors.Is(serr, fs.ErrNotExist) {
return nil, serr return nil, serr
} }
if serr == nil && fileInfo.IsDir() { if serr == nil && fileInfo.IsDir() {
return nil, errors.New("Error: " + filename + " is a directory and cannot be opened") 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) file, err := os.Open(filename)
if err == nil { if err == nil {
@ -255,7 +260,7 @@ func NewBufferFromFileAtLoc(path string, btype BufType, cursorLoc Loc) (*Buffer,
} }
var buf *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 // File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename, btype) buf = NewBufferFromString("", filename, btype)
} else if err != nil { } else if err != nil {
@ -335,9 +340,9 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
} }
config.UpdatePathGlobLocals(b.Settings, absPath) 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 { if err != nil {
enc = unicode.UTF8 b.encoding = unicode.UTF8
b.Settings["encoding"] = "utf-8" b.Settings["encoding"] = "utf-8"
} }
@ -348,7 +353,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
return NewBufferFromString("", "", btype) return NewBufferFromString("", "", btype)
} }
if !hasBackup { if !hasBackup {
reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder())) reader := bufio.NewReader(transform.NewReader(r, b.encoding.NewDecoder()))
var ff FileFormat = FFAuto var ff FileFormat = FFAuto
@ -389,7 +394,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT
// we know the filetype now, so update per-filetype settings // we know the filetype now, so update per-filetype settings
config.UpdateFileTypeLocals(b.Settings, b.Settings["filetype"].(string)) 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) os.Mkdir(filepath.Join(config.ConfigDir, "buffers"), os.ModePerm)
} }
@ -540,7 +545,7 @@ func (b *Buffer) ReOpen() error {
} }
reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder())) reader := bufio.NewReader(transform.NewReader(file, enc.NewDecoder()))
data, err := ioutil.ReadAll(reader) data, err := io.ReadAll(reader)
txt := string(data) txt := string(data)
if err != nil { if err != nil {

View File

@ -5,18 +5,19 @@ import (
"bytes" "bytes"
"errors" "errors"
"io" "io"
"io/fs"
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sync/atomic"
"time"
"unicode" "unicode"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/transform" "golang.org/x/text/transform"
) )
@ -24,74 +25,178 @@ import (
// because hashing is too slow // because hashing is too slow
const LargeFileThreshold = 50000 const LargeFileThreshold = 50000
// overwriteFile opens the given file for writing, truncating if one exists, and then calls type wrappedFile struct {
// the supplied function with the file as io.Writer object, also making sure the file is writeCloser io.WriteCloser
// closed afterwards. withSudo bool
func overwriteFile(name string, enc encoding.Encoding, fn func(io.Writer) error, withSudo bool) (err error) { screenb bool
cmd *exec.Cmd
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 var writeCloser io.WriteCloser
var screenb bool var screenb bool
var cmd *exec.Cmd var cmd *exec.Cmd
var c chan os.Signal var sigChan chan os.Signal
if withSudo { if withSudo {
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name) cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
writeCloser, err = cmd.StdinPipe()
if writeCloser, err = cmd.StdinPipe(); err != nil { if err != nil {
return return wrappedFile{}, err
} }
c = make(chan os.Signal, 1) sigChan = make(chan os.Signal, 1)
signal.Reset(os.Interrupt) signal.Reset(os.Interrupt)
signal.Notify(c, os.Interrupt) signal.Notify(sigChan, os.Interrupt)
screenb = screen.TempFini() screenb = screen.TempFini()
// need to start the process now, otherwise when we flush the file // 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 // 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 // is too small to handle the full file contents all at once
if err = cmd.Start(); err != nil { err = cmd.Start()
if err != nil {
screen.TempStart(screenb) screen.TempStart(screenb)
signal.Notify(util.Sigterm, os.Interrupt) signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(c) signal.Stop(sigChan)
return return wrappedFile{}, err
} }
} else if writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666); err != nil { } else {
return writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
} if err != nil {
return wrappedFile{}, err
w := bufio.NewWriter(transform.NewWriter(writeCloser, enc.NewEncoder()))
err = fn(w)
if err2 := w.Flush(); 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 return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
}
func (wf wrappedFile) Write(b *Buffer) (int, error) {
file := bufio.NewWriter(transform.NewWriter(wf.writeCloser, b.encoding.NewEncoder()))
b.Lock()
defer b.Unlock()
if len(b.lines) == 0 {
return 0, nil
} }
if withSudo { // end of line
var eol []byte
if b.Endings == FFDos {
eol = []byte{'\r', '\n'}
} else {
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 {
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)
}
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 (wf wrappedFile) Close() error {
err := wf.writeCloser.Close()
if wf.withSudo {
// wait for dd to finish and restart the screen if we used sudo // wait for dd to finish and restart the screen if we used sudo
err := cmd.Wait() err := wf.cmd.Wait()
screen.TempStart(screenb) screen.TempStart(wf.screenb)
signal.Notify(util.Sigterm, os.Interrupt) signal.Notify(util.Sigterm, os.Interrupt)
signal.Stop(c) signal.Stop(wf.sigChan)
if err != nil { if err != nil {
return err return err
} }
} }
return err
}
return func (b *Buffer) overwriteFile(name string) (int, error) {
file, err := openFile(name, false)
if err != nil {
return 0, err
}
size, err := file.Write(b)
err2 := file.Close()
if err2 != nil && err == nil {
err = err2
}
return size, err
} }
// Save saves the buffer to its default path // Save saves the buffer to its default path
@ -152,19 +257,35 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
// Update the last time this file was updated after saving filename, err = util.ReplaceHome(filename)
defer func() { if err != nil {
b.ModTime, _ = util.GetModTime(filename) return err
err = b.Serialize() }
}()
// Removes any tilde and replaces with the absolute path to home newFile := false
absFilename, _ := util.ReplaceHome(filename) 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: " + 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, 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 // Get the leading path to the file | "." is returned if there's no leading path provided
if dirname := filepath.Dir(absFilename); dirname != "." { if dirname := filepath.Dir(absFilename); dirname != "." {
// Check if the parent dirs don't exist // 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 // Prompt to make sure they want to create the dirs that are missing
if b.Settings["mkparents"].(bool) { if b.Settings["mkparents"].(bool) {
// Create all leading dir(s) since they don't exist // Create all leading dir(s) since they don't exist
@ -178,49 +299,22 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
} }
var fileSize int saveResponseChan := make(chan saveResponse)
saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan}
enc, err := htmlindex.Get(b.Settings["encoding"].(string)) result := <-saveResponseChan
err = result.err
if err != nil { if err != nil {
return err if errors.Is(err, util.ErrOverwrite) {
} screen.TermMessage(err)
err = errors.Unwrap(err)
fwriter := func(file io.Writer) (e error) { b.UpdateModTime()
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 return err
} }
if !b.Settings["fastdirty"].(bool) { if !b.Settings["fastdirty"].(bool) {
if fileSize > LargeFileThreshold { if result.size > LargeFileThreshold {
// For large files 'fastdirty' needs to be on // For large files 'fastdirty' needs to be on
b.Settings["fastdirty"] = true b.Settings["fastdirty"] = true
} else { } else {
@ -229,9 +323,64 @@ func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error
} }
b.Path = filename b.Path = filename
absPath, _ := filepath.Abs(filename) b.AbsPath = absFilename
b.AbsPath = absPath
b.isModified = false b.isModified = false
b.UpdateModTime()
b.ReloadSettings(true) b.ReloadSettings(true)
err = b.Serialize()
return err 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) {
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) {
return 0, err
}
if err = os.Mkdir(backupDir, os.ModePerm); err != nil {
return 0, err
}
}
backupName := util.DetermineEscapePath(backupDir, path)
_, err = b.overwriteFile(backupName)
if err != nil {
os.Remove(backupName)
return 0, err
}
b.forceKeepBackup = true
size, err := file.Write(b)
if err != nil {
err = util.OverwriteError{err, backupName}
return size, err
}
b.forceKeepBackup = false
if !b.keepBackup() {
os.Remove(backupName)
}
err2 := file.Close()
if err2 != nil && err == nil {
err = err2
}
return size, err
}

View File

@ -1,15 +1,13 @@
package buffer package buffer
import ( import (
"bytes"
"encoding/gob" "encoding/gob"
"errors" "errors"
"io"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"golang.org/x/text/encoding"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -31,16 +29,18 @@ func (b *Buffer) Serialize() error {
return nil return nil
} }
name := filepath.Join(config.ConfigDir, "buffers", util.EscapePath(b.AbsPath)) var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(SerializedBuffer{
return overwriteFile(name, encoding.Nop, func(file io.Writer) error { b.EventHandler,
err := gob.NewEncoder(file).Encode(SerializedBuffer{ b.GetActiveCursor().Loc,
b.EventHandler, b.ModTime,
b.GetActiveCursor().Loc, })
b.ModTime, if err != nil {
})
return err 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 // Unserialize loads the buffer info from config.ConfigDir/buffers
@ -50,7 +50,7 @@ func (b *Buffer) Unserialize() error {
if b.Path == "" { if b.Path == "" {
return nil 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 { if err == nil {
defer file.Close() defer file.Close()
var buffer SerializedBuffer var buffer SerializedBuffer

View File

@ -7,6 +7,8 @@ import (
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
ulua "github.com/zyedidia/micro/v2/internal/lua" ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen" "github.com/zyedidia/micro/v2/internal/screen"
"golang.org/x/text/encoding/htmlindex"
"golang.org/x/text/encoding/unicode"
luar "layeh.com/gopher-luar" luar "layeh.com/gopher-luar"
) )
@ -97,6 +99,12 @@ func (b *Buffer) DoSetOptionNative(option string, nativeValue interface{}) {
b.UpdateRules() b.UpdateRules()
} }
} else if option == "encoding" { } 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 b.isModified = true
} else if option == "readonly" && b.Type.Kind == BTDefault.Kind { } else if option == "readonly" && b.Type.Kind == BTDefault.Kind {
b.Type.Readonly = nativeValue.(bool) b.Type.Readonly = nativeValue.(bool)

View File

@ -5,7 +5,6 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -396,7 +395,7 @@ func (pv *PluginVersion) DownloadAndInstall(out io.Writer) error {
return err return err
} }
defer resp.Body.Close() defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err
} }

View File

@ -2,7 +2,6 @@ package config
import ( import (
"errors" "errors"
"io/ioutil"
"log" "log"
"os" "os"
"path" "path"
@ -81,7 +80,7 @@ func (rf realFile) Name() string {
} }
func (rf realFile) Data() ([]byte, error) { func (rf realFile) Data() ([]byte, error) {
return ioutil.ReadFile(string(rf)) return os.ReadFile(string(rf))
} }
func (af assetFile) Name() string { func (af assetFile) Name() string {
@ -107,7 +106,7 @@ func AddRealRuntimeFile(fileType RTFiletype, file RuntimeFile) {
// AddRuntimeFilesFromDirectory registers each file from the given directory for // AddRuntimeFilesFromDirectory registers each file from the given directory for
// the filetype which matches the file-pattern // the filetype which matches the file-pattern
func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) { func AddRuntimeFilesFromDirectory(fileType RTFiletype, directory, pattern string) {
files, _ := ioutil.ReadDir(directory) files, _ := os.ReadDir(directory)
for _, f := range files { for _, f := range files {
if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok { if ok, _ := filepath.Match(pattern, f.Name()); !f.IsDir() && ok {
fullPath := filepath.Join(directory, f.Name()) fullPath := filepath.Join(directory, f.Name())
@ -194,14 +193,14 @@ func InitPlugins() {
// Search ConfigDir for plugin-scripts // Search ConfigDir for plugin-scripts
plugdir := filepath.Join(ConfigDir, "plug") plugdir := filepath.Join(ConfigDir, "plug")
files, _ := ioutil.ReadDir(plugdir) files, _ := os.ReadDir(plugdir)
isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString isID := regexp.MustCompile(`^[_A-Za-z0-9]+$`).MatchString
for _, d := range files { for _, d := range files {
plugpath := filepath.Join(plugdir, d.Name()) plugpath := filepath.Join(plugdir, d.Name())
if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() { if stat, err := os.Stat(plugpath); err == nil && stat.IsDir() {
srcs, _ := ioutil.ReadDir(plugpath) srcs, _ := os.ReadDir(plugpath)
p := new(Plugin) p := new(Plugin)
p.Name = d.Name() p.Name = d.Name()
p.DirName = d.Name() p.DirName = d.Name()
@ -209,7 +208,7 @@ func InitPlugins() {
if strings.HasSuffix(f.Name(), ".lua") { if strings.HasSuffix(f.Name(), ".lua") {
p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name()))) p.Srcs = append(p.Srcs, realFile(filepath.Join(plugdir, d.Name(), f.Name())))
} else if strings.HasSuffix(f.Name(), ".json") { } 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 { if err != nil {
continue continue
} }

View File

@ -4,7 +4,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -156,6 +155,10 @@ var (
VolatileSettings map[string]bool VolatileSettings map[string]bool
) )
func writeFile(name string, txt []byte) error {
return util.SafeWrite(name, txt, false)
}
func init() { func init() {
ModifiedSettings = make(map[string]bool) ModifiedSettings = make(map[string]bool)
VolatileSettings = make(map[string]bool) VolatileSettings = make(map[string]bool)
@ -222,7 +225,7 @@ func ReadSettings() error {
parsedSettings = make(map[string]interface{}) parsedSettings = make(map[string]interface{})
filename := filepath.Join(ConfigDir, "settings.json") filename := filepath.Join(ConfigDir, "settings.json")
if _, e := os.Stat(filename); e == nil { if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename) input, err := os.ReadFile(filename)
if err != nil { if err != nil {
settingsParseError = true settingsParseError = true
return errors.New("Error reading settings.json file: " + err.Error()) return errors.New("Error reading settings.json file: " + err.Error())
@ -356,7 +359,8 @@ func WriteSettings(filename string) error {
} }
txt, _ := json.MarshalIndent(parsedSettings, "", " ") txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
err = writeFile(filename, txt)
} }
return err return err
} }
@ -377,8 +381,9 @@ func OverwriteSettings(filename string) error {
} }
} }
txt, _ := json.MarshalIndent(settings, "", " ") txt, _ := json.MarshalIndent(parsedSettings, "", " ")
err = ioutil.WriteFile(filename, append(txt, '\n'), 0644) txt = append(txt, '\n')
err = writeFile(filename, txt)
} }
return err return err
} }

View File

@ -1,12 +1,16 @@
package info package info
import ( import (
"bytes"
"encoding/gob" "encoding/gob"
"errors"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/zyedidia/micro/v2/internal/config" "github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/screen"
"github.com/zyedidia/micro/v2/internal/util" "github.com/zyedidia/micro/v2/internal/util"
) )
@ -17,24 +21,23 @@ func (i *InfoBuf) LoadHistory() {
if config.GetGlobalOption("savehistory").(bool) { if config.GetGlobalOption("savehistory").(bool) {
file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history")) file, err := os.Open(filepath.Join(config.ConfigDir, "buffers", "history"))
var decodedMap map[string][]string var decodedMap map[string][]string
if err == nil { if err != nil {
defer file.Close() if !errors.Is(err, fs.ErrNotExist) {
decoder := gob.NewDecoder(file) i.Error("Error loading history: ", err)
err = decoder.Decode(&decodedMap)
if err != nil {
i.Error("Error loading history:", err)
return
} }
return
}
defer file.Close()
err = gob.NewDecoder(file).Decode(&decodedMap)
if err != nil {
i.Error("Error decoding history: ", err)
return
} }
if decodedMap != nil { if decodedMap != nil {
i.History = decodedMap 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")) var buf bytes.Buffer
if err == nil { err := gob.NewEncoder(&buf).Encode(i.History)
defer file.Close() if err != nil {
encoder := gob.NewEncoder(file) screen.TermMessage("Error encoding history: ", err)
return
}
err = encoder.Encode(i.History) filename := filepath.Join(config.ConfigDir, "buffers", "history")
if err != nil { err = util.SafeWrite(filename, buf.Bytes(), true)
i.Error("Error saving history:", err) if err != nil {
return screen.TermMessage("Error saving history: ", err)
} return
} }
} }
} }

View File

@ -6,7 +6,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs"
"net/http" "net/http"
"net/url"
"os" "os"
"os/user" "os/user"
"path/filepath" "path/filepath"
@ -43,8 +45,44 @@ var (
Stdout *bytes.Buffer Stdout *bytes.Buffer
// Sigterm is a channel where micro exits when written // Sigterm is a channel where micro exits when written
Sigterm chan os.Signal 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() { func init() {
var err error var err error
SemVersion, err = semver.Make(Version) SemVersion, err = semver.Make(Version)
@ -408,8 +446,17 @@ func GetModTime(path string) (time.Time, error) {
return info.ModTime(), nil return info.ModTime(), nil
} }
// EscapePath replaces every path separator in a given path with a % func AppendBackupSuffix(path string) string {
func EscapePath(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) path = filepath.ToSlash(path)
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
// ':' is not valid in a path name on Windows but is ok on Unix // ':' is not valid in a path name on Windows but is ok on Unix
@ -418,6 +465,24 @@ func EscapePath(path string) string {
return strings.ReplaceAll(path, "/", "%") 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 // GetLeadingWhitespace returns the leading whitespace of the given byte array
func GetLeadingWhitespace(b []byte) []byte { func GetLeadingWhitespace(b []byte) []byte {
ws := []byte{} ws := []byte{}
@ -590,3 +655,77 @@ func HttpRequest(method string, url string, headers []string) (resp *http.Respon
} }
return client.Do(req) 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)
} else {
err = OverwriteError{err, tmp}
}
return err
}
if !rename {
os.Remove(tmp)
}
return nil
}

View File

@ -6,7 +6,6 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strings" "strings"
"time" "time"
@ -34,7 +33,7 @@ func main() {
if len(os.Args) > 1 { if len(os.Args) > 1 {
os.Chdir(os.Args[1]) os.Chdir(os.Args[1])
} }
files, _ := ioutil.ReadDir(".") files, _ := os.ReadDir(".")
for _, f := range files { for _, f := range files {
fname := f.Name() fname := f.Name()
if strings.HasSuffix(fname, ".yaml") { if strings.HasSuffix(fname, ".yaml") {
@ -46,7 +45,7 @@ func main() {
func convert(name string) { func convert(name string) {
filename := name + ".yaml" filename := name + ".yaml"
var hdr HeaderYaml var hdr HeaderYaml
source, err := ioutil.ReadFile(filename) source, err := os.ReadFile(filename)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -68,7 +67,7 @@ func encode(name string, c HeaderYaml) {
func decode(name string) Header { func decode(name string) Header {
start := time.Now() start := time.Now()
data, _ := ioutil.ReadFile(name + ".hdr") data, _ := os.ReadFile(name + ".hdr")
strs := bytes.Split(data, []byte{'\n'}) strs := bytes.Split(data, []byte{'\n'})
var hdr Header var hdr Header
hdr.FileType = string(strs[0]) hdr.FileType = string(strs[0])

View File

@ -4,7 +4,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"os" "os"
"regexp" "regexp"
"strings" "strings"
@ -161,6 +160,6 @@ func main() {
return return
} }
data, _ := ioutil.ReadFile(os.Args[1]) data, _ := os.ReadFile(os.Args[1])
fmt.Print(generateFile(parseFile(string(data), os.Args[1]))) fmt.Print(generateFile(parseFile(string(data), os.Args[1])))
} }

View File

@ -37,7 +37,7 @@ func main() {
</plist> </plist>
` `
err := os.WriteFile("/tmp/micro-info.plist", []byte(rawInfoPlist), 0644) err := os.WriteFile("/tmp/micro-info.plist", []byte(rawInfoPlist), 0666)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View File

@ -4,7 +4,7 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil" "io"
"net/http" "net/http"
"os/exec" "os/exec"
"strings" "strings"
@ -19,7 +19,7 @@ func main() {
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
var data interface{} var data interface{}

View File

@ -4,7 +4,6 @@ package main
import ( import (
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"regexp" "regexp"
@ -210,7 +209,7 @@ func main() {
var tests []test var tests []test
for _, filename := range os.Args[1:] { for _, filename := range os.Args[1:] {
source, err := ioutil.ReadFile(filename) source, err := os.ReadFile(filename)
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
} }