Something went wrong. Try again.
Fork of micro-editor/micro
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414package buffer
import ( "bufio" "bytes" "errors" "io" "io/fs" "os" "os/exec" "os/signal" "path/filepath" "runtime" "time" "unicode"
"github.com/micro-editor/micro/v2/internal/config" "github.com/micro-editor/micro/v2/internal/screen" "github.com/micro-editor/micro/v2/internal/util" "golang.org/x/text/transform")
// LargeFileThreshold is the number of bytes when fastdirty is forced// because hashing is too slowconst LargeFileThreshold = 50000
type wrappedFile struct { writeCloser io.WriteCloser withSudo bool 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 saveRequestvar backupRequestChan chan backupRequest
func init() { // Both saveRequestChan and backupRequestChan need to be non-buffered // so the save/backup goroutine receives both save and backup requests // in the same order the main goroutine sends them. saveRequestChan = make(chan saveRequest) backupRequestChan = make(chan backupRequest)
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 br := <-backupRequestChan: handleBackupRequest(br) case <-backupTicker.C: periodicBackup() } } }()}
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 { args := []string{"dd", "bs=4k", "of=" + name} // TODO: both platforms do not support dd with conv=fsync yet if !(runtime.GOOS == "illumos" || runtime.GOOS == "netbsd") { args = append(args, "conv=fsync") }
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), args...) 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 { writeCloser.Close() 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 *SharedBuffer) (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 }
// 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 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 *SharedBuffer) 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 pathfunc (b *Buffer) Save() error { return b.SaveAs(b.Path)}
// AutoSave saves the buffer to its default pathfunc (b *Buffer) AutoSave() error { if !b.Modified() { return nil } return b.saveToFile(b.Path, false, true)}
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not existfunc (b *Buffer) SaveAs(filename string) error { return b.saveToFile(filename, false, false)}
func (b *Buffer) SaveWithSudo() error { return b.SaveAsWithSudo(b.Path)}
func (b *Buffer) SaveAsWithSudo(filename string) error { return b.saveToFile(filename, true, false)}
func (b *Buffer) saveToFile(filename string, withSudo bool, autoSave bool) error { var err error if b.Type.Readonly { return errors.New("Cannot save readonly buffer") } if b.Type.Scratch { return errors.New("Cannot save scratch buffer") }
if withSudo { _, err := exec.LookPath(config.GlobalSettings["sucmd"].(string)) if err != nil { return err } }
if !autoSave && b.Settings["rmtrailingws"].(bool) { for i, l := range b.lines { leftover := util.CharacterCount(bytes.TrimRightFunc(l.data, unicode.IsSpace))
linelen := util.CharacterCount(l.data) b.Remove(Loc{leftover, i}, Loc{linelen, i}) }
b.RelocateCursors() }
if b.Settings["eofnewline"].(bool) { end := b.End() if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' { b.insert(end, []byte{'\n'}) } }
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: " + 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 := util.ResolvePath(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); 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 if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil { // If there was an error creating the dirs return mkdirallErr } } else { return errors.New("Parent dirs don't exist, enable 'mkparents' for auto creation") } } }
saveResponseChan := make(chan saveResponse) saveRequestChan <- saveRequest{b, absFilename, withSudo, newFile, saveResponseChan} result := <-saveResponseChan err = result.err if err != nil { if errors.Is(err, util.ErrOverwrite) { screen.TermMessage(err) err = errors.Unwrap(err)
b.UpdateModTime() } return err }
if !b.Settings["fastdirty"].(bool) { if result.size > LargeFileThreshold { // For large files 'fastdirty' needs to be on b.Settings["fastdirty"] = true } else { b.calcHash(&b.origHash) } }
newPath := b.Path != filename if newPath { b.RemoveBackup() }
b.Path = filename b.AbsPath = absFilename b.isModified = false b.UpdateModTime()
if newPath { // need to update glob-based and filetype-based settings b.ReloadSettings(true) }
err = b.Serialize() 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 *SharedBuffer) safeWrite(path string, withSudo bool, newFile bool) (int, error) { var err error
defer func() { if newFile && err != nil { os.Remove(path) } }()
// When using sudo, create the backup before opening the file because // openFile() truncates the target immediately. For non-sudo saves, // openFile() does not truncate on open, i.e. open is non-destructive, // so only create the backup after open succeeds. var backupName, resolveName string if withSudo { backupName, resolveName, err = b.writeBackup(path) if err != nil { return 0, err } delete(requestedBackups, b) }
file, err := openFile(path, withSudo) if err != nil { if withSudo { return 0, util.OverwriteError{err, backupName} } return 0, err }
if !withSudo { backupName, resolveName, err = b.writeBackup(path) if err != nil { file.Close() return 0, err } // Backup saved, so cancel pending periodic backup, if any delete(requestedBackups, b) }
b.forceKeepBackup = true size := 0 { // If we failed to write or close, keep the backup we made size, err = file.Write(b) if err != nil { file.Close() return 0, util.OverwriteError{err, backupName} }
err = file.Close() if err != nil { return 0, util.OverwriteError{err, backupName} } } b.forceKeepBackup = false
if !b.keepBackup() { b.removeBackup(backupName, resolveName) }
return size, err}