Repository: gokcehan/lf (language: go)

Issue:
Copy with preserved timestanp
Hello,

Is it possible to change the behavior of the "copy" function to preserve timestamps and attributes ? (creation date, modification date, permissions, etc). When I use `rsync -aAX file1 newlocation/file1` instead of `cp file1 newlocation/file1`, everything is preserved. I'm not sure where to change this in lf while preserving the nice looking progress percentage.

thank you


Discussion / hints:
There is an example for showing progress: https://github.com/gokcehan/lf/wiki/Tips#show-progress-for-file-copying

Fix patch:
diff --git a/complete.go b/complete.go
index b2698e3f..a943830f 100644
--- a/complete.go
+++ b/complete.go
@@ -174,6 +174,7 @@ var (
 		"noreverse",
 		"reverse!",
 		"ruler",
+		"preserve",
 		"smartcase",
 		"nosmartcase",
 		"smartcase!",
diff --git a/copy.go b/copy.go
index 07b21b5e..1365fe00 100644
--- a/copy.go
+++ b/copy.go
@@ -5,6 +5,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"github.com/djherbis/times"
 )
 
 func copySize(srcs []string) (int64, error) {
@@ -33,6 +34,17 @@ func copySize(srcs []string) (int64, error) {
 }
 
 func copyFile(src, dst string, info os.FileInfo, nums chan int64) error {
+	var dst_mode os.FileMode = 0666
+	preserve_timestamps := false
+	for _, s := range gOpts.preserve {
+		switch s {
+		case "timestamps":
+			preserve_timestamps = true
+		case "mode":
+			dst_mode = info.Mode()
+		}
+	}
+
 	buf := make([]byte, 4096)
 
 	r, err := os.Open(src)
@@ -41,7 +53,7 @@ func copyFile(src, dst string, info os.FileInfo, nums chan int64) error {
 	}
 	defer r.Close()
 
-	w, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
+	w, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, dst_mode)
 	if err != nil {
 		return err
 	}
@@ -70,6 +82,16 @@ func copyFile(src, dst string, info os.FileInfo, nums chan int64) error {
 		return err
 	}
 
+	if preserve_timestamps {
+		ts := times.Get(info)
+		mtime := info.ModTime();
+		atime := ts.AccessTime();
+		if err := os.Chtimes(dst, atime, mtime); err != nil {
+			os.Remove(dst)
+			return err
+		}
+	}
+
 	return nil
 }
 
diff --git a/doc.go b/doc.go
index dfe9cd2f..41210f32 100644
--- a/doc.go
+++ b/doc.go
@@ -147,6 +147,7 @@ The following options can be used to customize the behavior of lf:
 	number           bool      (default false)
 	numberfmt        string    (default "\033[33m")
 	period           int       (default 0)
+	preserve         []string  (default "mode")
 	preview          bool      (default true)
 	previewer        string    (default '')
 	promptfmt        string    (default "\033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m")
@@ -788,6 +789,12 @@ Note that directories are already updated automatically in many cases.
 This option can be useful when there is an external process changing the displayed directory and you are not doing anything in lf.
 Periodic checks are disabled when the value of this option is set to zero.
 
+	preserve       []string  (default 'mode')
+
+List of attributes that are preserved when copying files.
+Currently supported attributes are 'mode' (i.a. access mode) and 'timestamps' (i.e. modification time and access time).
+Note: Preserving other attribute like ownership of change/birth timestamp is desireable, but not portably supported in go.
+
 	preview        bool      (default true)
 
 Show previews of files and directories at the right most pane.
@@ -1321,7 +1328,7 @@ lf uses its own builtin copy and move operations by default.
 These are implemented as asynchronous operations and progress is shown in the bottom ruler.
 These commands do not overwrite existing files or directories with the same name.
 Instead, a suffix that is compatible with '--backup=numbered' option in GNU cp is added to the new files or directories.
-Only file modes are preserved and all other attributes are ignored including ownership, timestamps, context, and xattr.
+Only file modes and (some) timestamps can be preserved (see `preserve` option), all other attributes are ignored including ownership, context, and xattr.
 Special files such as character and block devices, named pipes, and sockets are skipped and links are not followed.
 Moving is performed using the rename operation of the underlying OS.
 For cross-device moving, lf falls back to copying and then deletes the original files if there are no errors.
diff --git a/docstring.go b/docstring.go
index d67e17df..81516441 100644
--- a/docstring.go
+++ b/docstring.go
@@ -150,6 +150,7 @@ The following options can be used to customize the behavior of lf:
     number           bool      (default false)
     numberfmt        string    (default "\033[33m")
     period           int       (default 0)
+    preserve         []string  (default "mode")
     preview          bool      (default true)
     previewer        string    (default '')
     promptfmt        string    (default "\033[32;1m%u@%h\033[0m:\033[34;1m%d\033[0m\033[1m%f\033[0m")
@@ -838,6 +839,13 @@ external process changing the displayed directory and you are not doing anything
 in lf. Periodic checks are disabled when the value of this option is set to
 zero.
 
+    preserve       []string  (default 'mode')
+
+List of attributes that are preserved when copying files. Currently supported
+attributes are 'mode' (i.a. access mode) and 'timestamps' (i.e. modification
+time and access time). Note: Preserving other attribute like ownership of
+change/birth timestamp is desireable, but not portably supported in go.
+
     preview        bool      (default true)
 
 Show previews of files and directories at the right most pane. If the file
@@ -1420,16 +1428,17 @@ not be needed for users.
 
 lf uses its own builtin copy and move operations by default. These are
 implemented as asynchronous operations and progress is shown in the bottom
-ruler. These commands do not overwrite existing files or directories with
-the same name. Instead, a suffix that is compatible with '--backup=numbered'
-option in GNU cp is added to the new files or directories. Only file modes are
-preserved and all other attributes are ignored including ownership, timestamps,
-context, and xattr. Special files such as character and block devices, named
-pipes, and sockets are skipped and links are not followed. Moving is performed
-using the rename operation of the underlying OS. For cross-device moving,
-lf falls back to copying and then deletes the original files if there are no
-errors. Operation errors are shown in the message line as well as the log file
-and they do not preemptively finish the corresponding file operation.
+ruler. These commands do not overwrite existing files or directories with the
+same name. Instead, a suffix that is compatible with '--backup=numbered' option
+in GNU cp is added to the new files or directories. Only file modes and (some)
+timestamps can be preserved (see 'preserve' option), all other attributes are
+ignored including ownership, context, and xattr. Special files such as character
+and block devices, named pipes, and sockets are skipped and links are not
+followed. Moving is performed using the rename operation of the underlying OS.
+For cross-device moving, lf falls back to copying and then deletes the original
+files if there are no errors. Operation errors are shown in the message line as
+well as the log file and they do not preemptively finish the corresponding file
+operation.
 
 File operations can be performed on the current selected file or alternatively
 on multiple files by selecting them first. When you 'copy' a file, lf doesn't
diff --git a/eval.go b/eval.go
index 0a6ca6ed..c459530f 100644
--- a/eval.go
+++ b/eval.go
@@ -497,6 +497,21 @@ func (e *setExpr) eval(app *app, args []string) {
 			}
 		}
 		gOpts.ruler = toks
+	case "preserve":
+		if e.val == "" {
+			gOpts.preserve = nil
+			return
+		}
+		toks := strings.Split(e.val, ":")
+		for _,s := range toks {
+			switch s {
+			case "mode", "timestamps":
+			default:
+				app.ui.echoerr("preserve: should consist of 'mode' or 'timestamps separated with colon")
+				return
+			}
+		}
+		gOpts.preserve = toks
 	case "infotimefmtnew":
 		gOpts.infotimefmtnew = e.val
 	case "infotimefmtold":
diff --git a/lf.1 b/lf.1
index 9dfbf0f9..090c9808 100644
--- a/lf.1
+++ b/lf.1
@@ -166,6 +166,7 @@ The following options can be used to customize the behavior of lf:
     number           bool      (default false)
     numberfmt        string    (default "\e033[33m")
     period           int       (default 0)
+    preserve         []string  (default "mode")
     preview          bool      (default true)
     previewer        string    (default '')
     promptfmt        string    (default "\e033[32;1m%u@%h\e033[0m:\e033[34;1m%d\e033[0m\e033[1m%f\e033[0m")
@@ -954,6 +955,12 @@ Format string of the position number for each line.
 .PP
 Set the interval in seconds for periodic checks of directory updates. This works by periodically calling the 'load' command. Note that directories are already updated automatically in many cases. This option can be useful when there is an external process changing the displayed directory and you are not doing anything in lf. Periodic checks are disabled when the value of this option is set to zero.
 .PP
+.EX
+    preserve       []string  (default 'mode')
+.EE
+.PP
+List of attributes that are preserved when copying files. Currently supported attributes are 'mode' (i.a. access mode) and 'timestamps' (i.e. modification time and access time). Note: Preserving other attribute like ownership of change/birth timestamp is desireable, but not portably supported in go.
+.PP
 .EX
     preview        bool      (default true)
 .EE
@@ -1549,7 +1556,7 @@ Besides 'send' command, there is a 'quit' command to quit the server when there
 .PP
 Lastly, there is a 'conn' command to connect the server as a client. This should not be needed for users.
 .SH FILE OPERATIONS
-lf uses its own builtin copy and move operations by default. These are implemented as asynchronous operations and progress is shown in the bottom ruler. These commands do not overwrite existing files or directories with the same name. Instead, a suffix that is compatible with '--backup=numbered' option in GNU cp is added to the new files or directories. Only file modes are preserved and all other attributes are ignored including ownership, timestamps, context, and xattr. Special files such as character and block devices, named pipes, and sockets are skipped and links are not followed. Moving is performed using the rename operation of the underlying OS. For cross-device moving, lf falls back to copying and then deletes the original files if there are no errors. Operation errors are shown in the message line as well as the log file and they do not preemptively finish the corresponding file operation.
+lf uses its own builtin copy and move operations by default. These are implemented as asynchronous operations and progress is shown in the bottom ruler. These commands do not overwrite existing files or directories with the same name. Instead, a suffix that is compatible with '--backup=numbered' option in GNU cp is added to the new files or directories. Only file modes and (some) timestamps can be preserved (see `preserve` option), all other attributes are ignored including ownership, context, and xattr. Special files such as character and block devices, named pipes, and sockets are skipped and links are not followed. Moving is performed using the rename operation of the underlying OS. For cross-device moving, lf falls back to copying and then deletes the original files if there are no errors. Operation errors are shown in the message line as well as the log file and they do not preemptively finish the corresponding file operation.
 .PP
 File operations can be performed on the current selected file or alternatively on multiple files by selecting them first. When you 'copy' a file, lf doesn't actually copy the file on the disk, but only records its name to a file. The actual file copying takes place when you 'paste'. Similarly 'paste' after a 'cut' operation moves the file.
 .PP
diff --git a/opts.go b/opts.go
index 301a3641..1ece066c 100644
--- a/opts.go
+++ b/opts.go
@@ -75,6 +75,7 @@ var gOpts struct {
 	history          bool
 	info             []string
 	ruler            []string
+	preserve         []string
 	shellopts        []string
 	keys             map[string]expr
 	cmdkeys          map[string]expr
@@ -134,6 +135,7 @@ func init() {
 	gOpts.history = true
 	gOpts.info = nil
 	gOpts.ruler = []string{"acc", "progress", "selection", "filter", "ind"}
+	gOpts.preserve= []string{"mode"}
 	gOpts.shellopts = nil
 	gOpts.sortType = sortType{naturalSort, dirfirstSort}
 	gOpts.tempmarks = "'"


Test patch: