From a8332fd316d3a26b0c7923a72999000edaeae5b9 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Mon, 22 Jun 2020 17:54:56 -0400 Subject: [PATCH] Improve backup system This commit introduces several improvements to the backup system. * Backups are made every 8 seconds for buffers that have been modified since the last backup. * The `permbackup` option allows users to specify that backups should be kept permanently. * `The backupdir` option allows users to store backups in a custom directory. Fixes #1641 Fixes #1536 Ref #1539 (removes possibility of race condition for backups) --- cmd/micro/micro.go | 2 +- internal/buffer/backup.go | 52 +++++++++++++++++++++++++++---------- internal/buffer/buffer.go | 15 ++++++----- internal/config/settings.go | 2 ++ runtime/help/options.md | 24 ++++++++++++----- 5 files changed, 68 insertions(+), 27 deletions(-) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 7c43c4a7..62b3290c 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -274,7 +274,7 @@ func main() { fmt.Println("Micro encountered an error:", err) // backup all open buffers for _, b := range buffer.OpenBuffers { - b.Backup(false) + b.Backup() } // Print the stack trace too fmt.Print(errors.Wrap(err, 2).ErrorStack()) diff --git a/internal/buffer/backup.go b/internal/buffer/backup.go index bf5e6b71..f23627ab 100644 --- a/internal/buffer/backup.go +++ b/internal/buffer/backup.go @@ -28,29 +28,53 @@ The backup was created on %s, and the file is Options: [r]ecover, [i]gnore: ` +var backupRequestChan chan *Buffer + +func backupThread() { + for { + time.Sleep(time.Second * 8) + + for len(backupRequestChan) > 0 { + b := <-backupRequestChan + b.Backup() + } + } +} + +func init() { + backupRequestChan = make(chan *Buffer, 10) + + go backupThread() +} + +func (b *Buffer) RequestBackup() { + if !b.requestedBackup { + select { + case backupRequestChan <- b: + default: + // channel is full + } + b.requestedBackup = true + } +} + // Backup saves the current buffer to ConfigDir/backups -func (b *Buffer) Backup(checkTime bool) error { +func (b *Buffer) Backup() error { if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault { return nil } - if checkTime { - sub := time.Now().Sub(b.lastbackup) - if sub < time.Duration(backupTime)*time.Millisecond { - return nil - } + backupdir, err := util.ReplaceHome(b.Settings["backupdir"].(string)) + if len(backupdir) == 0 || err != nil { + backupdir = filepath.Join(config.ConfigDir, "backups") } - - b.lastbackup = time.Now() - - backupdir := filepath.Join(config.ConfigDir, "backups") if _, err := os.Stat(backupdir); os.IsNotExist(err) { os.Mkdir(backupdir, os.ModePerm) } name := filepath.Join(backupdir, util.EscapePath(b.AbsPath)) - err := overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { + err = overwriteFile(name, encoding.Nop, func(file io.Writer) (e error) { if len(b.lines) == 0 { return } @@ -74,12 +98,14 @@ func (b *Buffer) Backup(checkTime bool) error { return }, false) + b.requestedBackup = false + return err } // RemoveBackup removes any backup file associated with this buffer func (b *Buffer) RemoveBackup() { - if !b.Settings["backup"].(bool) || b.Path == "" || b.Type != BTDefault { + if !b.Settings["backup"].(bool) || b.Settings["permbackup"].(bool) || b.Path == "" || b.Type != BTDefault { return } f := filepath.Join(config.ConfigDir, "backups", util.EscapePath(b.AbsPath)) @@ -89,7 +115,7 @@ func (b *Buffer) RemoveBackup() { // ApplyBackup applies the corresponding backup file to this buffer (if one exists) // Returns true if a backup was applied func (b *Buffer) ApplyBackup(fsize int64) bool { - if b.Settings["backup"].(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)) if info, err := os.Stat(backupfile); err == nil { backup, err := os.Open(backupfile) diff --git a/internal/buffer/buffer.go b/internal/buffer/buffer.go index 598a9fdf..cf754975 100644 --- a/internal/buffer/buffer.go +++ b/internal/buffer/buffer.go @@ -102,9 +102,7 @@ type SharedBuffer struct { diffLock sync.RWMutex diff map[int]DiffStatus - // counts the number of edits - // resets every backupTime edits - lastbackup time.Time + requestedBackup bool // ReloadDisabled allows the user to disable reloads if they // are viewing a file that is constantly changing @@ -271,6 +269,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT } } + hasBackup := false if !found { b.SharedBuffer = new(SharedBuffer) b.Type = btype @@ -293,7 +292,7 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT b.Settings["encoding"] = "utf-8" } - hasBackup := b.ApplyBackup(size) + hasBackup = b.ApplyBackup(size) if !hasBackup { reader := bufio.NewReader(transform.NewReader(r, enc.NewDecoder())) @@ -356,7 +355,9 @@ func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufT if size > LargeFileThreshold { // If the file is larger than LargeFileThreshold fastdirty needs to be on b.Settings["fastdirty"] = true - } else { + } else if !hasBackup { + // since applying a backup does not save the applied backup to disk, we should + // not calculate the original hash based on the backup data calcHash(b, &b.origHash) } } @@ -425,7 +426,7 @@ func (b *Buffer) Insert(start Loc, text string) { b.EventHandler.active = b.curCursor b.EventHandler.Insert(start, text) - go b.Backup(true) + b.RequestBackup() } } @@ -436,7 +437,7 @@ func (b *Buffer) Remove(start, end Loc) { b.EventHandler.active = b.curCursor b.EventHandler.Remove(start, end) - go b.Backup(true) + b.RequestBackup() } } diff --git a/internal/config/settings.go b/internal/config/settings.go index 7b45d12f..d2ebbd16 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -247,6 +247,7 @@ var defaultCommonSettings = map[string]interface{}{ "autoindent": true, "autosu": false, "backup": true, + "backupdir": "", "basename": false, "colorcolumn": float64(0), "cursorline": true, @@ -261,6 +262,7 @@ var defaultCommonSettings = map[string]interface{}{ "keepautoindent": false, "matchbrace": true, "mkparents": false, + "permbackup": false, "readonly": false, "rmtrailingws": false, "ruler": true, diff --git a/runtime/help/options.md b/runtime/help/options.md index e2a8a574..c8b20d07 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -37,21 +37,26 @@ Here are the available options: closed cleanly. In the case of a system crash or a micro crash, the contents of the buffer can be recovered automatically by opening the file that was being edited before the crash, or manually by searching for the backup in - the backup directory. Backups are made in the background when a buffer is - modified and the latest backup is more than 8 seconds old, or when micro - detects a crash. It is highly recommended that you leave this feature - enabled. + the backup directory. Backups are made in the background for newly modified + buffers every 8 seconds, or when micro detects a crash. default value: `true` +* `backupdir`: the directory micro should place backups in. For the default + value of `""` (empty string), the backup directory will be + `ConfigDir/backups`, which is `~/.config/micro/backups` by default. The + directory specified for backups will be created if it does not exist. + + default value: `""` (empty string) + * `basename`: in the infobar and tabbar, show only the basename of the file being edited rather than the full path. default value: `false` * `colorcolumn`: if this is not set to 0, it will display a column at the - specified column. This is useful if you want column 80 to be highlighted - special for example. + specified column. This is useful if you want column 80 to be highlighted + special for example. default value: `0` @@ -200,6 +205,13 @@ Here are the available options: default value: `false` +* `permbackup`: this option causes backups (see `backup` option) to be + permanently saved. With permanent backups, micro will not remove backups when + files are closed and will never apply them to existing files. Use this option + if you are interested in manually managing your backup files. + + default value: `false` + * `pluginchannels`: list of URLs pointing to plugin channels for downloading and installing plugins. A plugin channel consists of a json file with links to plugin repos, which store information about plugin versions and download URLs.