mirror of
https://github.com/zyedidia/micro.git
synced 2026-02-06 07:00:24 +09:00
Various methods of Buffer should be rather methods of SharedBuffer. This commit doesn't move all of them to SharedBuffer yet, only those that need to be moved to SharedBuffer in order to be able to request creating or removing backups in other SharedBuffer methods.
406 lines
8.9 KiB
Go
406 lines
8.9 KiB
Go
package buffer
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sync/atomic"
|
|
"time"
|
|
"unicode"
|
|
|
|
"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/transform"
|
|
)
|
|
|
|
// LargeFileThreshold is the number of bytes when fastdirty is forced
|
|
// because hashing is too slow
|
|
const 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 saveRequest
|
|
var backupRequestChan chan *SharedBuffer
|
|
|
|
func init() {
|
|
saveRequestChan = make(chan saveRequest, 10)
|
|
backupRequestChan = make(chan *SharedBuffer, 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 screenb bool
|
|
var cmd *exec.Cmd
|
|
var sigChan chan os.Signal
|
|
|
|
if withSudo {
|
|
cmd = exec.Command(config.GlobalSettings["sucmd"].(string), "dd", "bs=4k", "of="+name)
|
|
writeCloser, err = cmd.StdinPipe()
|
|
if err != nil {
|
|
return wrappedFile{}, err
|
|
}
|
|
|
|
sigChan = make(chan os.Signal, 1)
|
|
signal.Reset(os.Interrupt)
|
|
signal.Notify(sigChan, os.Interrupt)
|
|
|
|
screenb = screen.TempFini()
|
|
// need to start the process now, otherwise when we flush the file
|
|
// contents to its stdin it might hang because the kernel's pipe size
|
|
// is too small to handle the full file contents all at once
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
screen.TempStart(screenb)
|
|
|
|
signal.Notify(util.Sigterm, os.Interrupt)
|
|
signal.Stop(sigChan)
|
|
|
|
return wrappedFile{}, err
|
|
}
|
|
} else {
|
|
writeCloser, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, util.FileMode)
|
|
if err != nil {
|
|
return wrappedFile{}, err
|
|
}
|
|
}
|
|
|
|
return wrappedFile{writeCloser, withSudo, screenb, cmd, sigChan}, nil
|
|
}
|
|
|
|
func (wf wrappedFile) Write(b *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 path
|
|
func (b *Buffer) Save() error {
|
|
return b.SaveAs(b.Path)
|
|
}
|
|
|
|
// AutoSave saves the buffer to its default path
|
|
func (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 exist
|
|
func (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 && runtime.GOOS == "windows" {
|
|
return errors.New("Save with sudo not supported on Windows")
|
|
}
|
|
|
|
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, 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
|
|
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
|
|
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
|
|
}
|
|
|
|
func (b *SharedBuffer) writeBackup(path string) (string, error) {
|
|
backupDir := b.backupDir()
|
|
if _, err := os.Stat(backupDir); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
return "", err
|
|
}
|
|
if err = os.Mkdir(backupDir, os.ModePerm); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
backupName := util.DetermineEscapePath(backupDir, path)
|
|
_, err := b.overwriteFile(backupName)
|
|
if err != nil {
|
|
os.Remove(backupName)
|
|
return "", err
|
|
}
|
|
|
|
return backupName, nil
|
|
}
|
|
|
|
// 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) {
|
|
file, err := openFile(path, withSudo)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
defer func() {
|
|
if newFile && err != nil {
|
|
os.Remove(path)
|
|
}
|
|
}()
|
|
|
|
|
|
// Try to backup first before writing
|
|
backupName, err := b.writeBackup(path)
|
|
if err != nil {
|
|
file.Close()
|
|
return 0, err
|
|
}
|
|
|
|
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() {
|
|
os.Remove(backupName)
|
|
}
|
|
|
|
return size, err
|
|
}
|