diff --git a/go.mod b/go.mod
index 9a89769..778c789 100644
--- a/go.mod
+++ b/go.mod
@@ -4,5 +4,6 @@ go 1.15
require (
github.com/fatih/color v1.10.0
+ github.com/otiai10/copy v1.4.2
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
)
diff --git a/go.sum b/go.sum
index 518ae0f..233d5b1 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,14 @@ github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/otiai10/copy v1.4.2 h1:RTiz2sol3eoXPLF4o+YWqEybwfUa/Q2Nkc4ZIUs3fwI=
+github.com/otiai10/copy v1.4.2/go.mod h1:XWfuS3CrI0R6IE0FbgHsEazaXO8G0LpMp9o8tos0x4E=
+github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
+github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
+github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
+github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
+github.com/otiai10/mint v1.3.2 h1:VYWnrP5fXmz1MXvjuUvcBrXSjGE6xjON+axB/UrpO3E=
+github.com/otiai10/mint v1.3.2/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
diff --git a/rem.go b/rem.go
index e096506..19f1d3b 100644
--- a/rem.go
+++ b/rem.go
@@ -9,9 +9,11 @@ import (
"os"
"path/filepath"
"strconv"
+ "syscall"
"time"
"github.com/fatih/color"
+ "github.com/otiai10/copy"
)
var (
@@ -29,12 +31,17 @@ Options:
-t/--set-trash
set trash to dir and continue
-h/--help print this help message
-v/--version print Rem version`
- home, _ = os.UserHomeDir()
- trashDir = home + "/.remTrash"
- logFileName = ".trash.log"
+ home, _ = os.UserHomeDir()
+ trashDir = home + "/.remTrash"
+ logFileName = ".trash.log"
+ logFile map[string]string
+ renameByCopyIsAllowed = true
//logSeparator = "\t==>\t"
)
+// TODO: Multiple Rem instances could clobber log file. Fix using either file locks or tcp port locks.
+// TODO: Check if files are on different fs and if so, copy it over
+
func main() {
trashDir, _ = filepath.Abs(trashDir)
if len(os.Args) == 1 {
@@ -76,6 +83,7 @@ func main() {
main()
return
}
+
if hasOption, _ := argsHaveOption("directory", "d"); hasOption {
fmt.Println(trashDir)
return
@@ -85,13 +93,15 @@ func main() {
return
}
if hasOption, _ := argsHaveOptionLong("empty"); hasOption {
- color.Red("Warning, permanently deleting these files in trash: ")
- printFormattedList(listFilesInTrash())
+ color.Red("Warning, permanently deleting all files in " + trashDir)
if promptBool("Confirm delete?") {
emptyTrash()
}
return
}
+ if hasOption, _ := argsHaveOptionLong("disable-copy"); hasOption {
+ renameByCopyIsAllowed = false
+ }
if hasOption, i := argsHaveOption("undo", "u"); hasOption {
if !(len(os.Args) > i+1) {
handleErrStr("not enough arguments for --undo")
@@ -109,64 +119,17 @@ func main() {
}
}
-func listFilesInTrash() []string {
- m := getLogFile()
- s := make([]string, len(m))
- i := 0
- for key := range m {
- s[i] = key
- i++
- }
- return s
-}
-
-func emptyTrash() {
- permanentlyDeleteFile(trashDir)
-}
-
-func getLogFile() map[string]string {
- ensureTrashDir()
- file, err := os.OpenFile(trashDir+"/"+logFileName, os.O_CREATE|os.O_RDONLY, 0644)
- if err != nil {
- handleErr(err)
- return nil
- }
- defer file.Close()
- lines := make(map[string]string)
- dec := gob.NewDecoder(file)
- err = dec.Decode(&lines)
- if err != nil && err != io.EOF {
- handleErr(err)
- }
- return lines
-}
-
-func setLogFile(m map[string]string) {
- //f, err := os.OpenFile(trashDir+"/"+logFileName, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) // truncate to empty, create if not exist, write only
- ensureTrashDir()
- f, err := os.Create(trashDir + "/" + logFileName)
- if err != nil {
- handleErr(err)
- return
- }
- defer f.Close()
- enc := gob.NewEncoder(f)
- err = enc.Encode(m)
- if err != nil && err != io.EOF {
- handleErr(err)
- }
-}
-
func restore(path string) {
- path, err := filepath.Abs(path)
+ path = filepath.Clean(path)
+ absPath, err := filepath.Abs(path)
if err != nil {
handleErr(err)
return
}
- logFile := getLogFile()
- fileInTrash, ok := logFile[path]
+ m := getLogFile()
+ fileInTrash, ok := m[absPath]
if ok {
- err = os.Rename(fileInTrash, path)
+ err = os.Rename(fileInTrash, absPath)
if err != nil {
handleErr(err)
return
@@ -175,40 +138,73 @@ func restore(path string) {
handleErrStr("file not in trash or missing restore data")
return
}
- delete(logFile, path)
- setLogFile(logFile) // we deleted an entry so save the new one
+ delete(logFile, absPath)
+ setLogFile(logFile) // we deleted an entry so save the edited logFile
fmt.Println(color.YellowString(path) + " restored")
}
func trashFile(path string) {
- path, err := filepath.Abs(path)
- if err != nil {
- handleErr(err)
- return
- }
- //toMoveTo := trashDir + "/" + filepath.Base(path+time.Now().String())
- toMoveTo := trashDir + "/" + filepath.Base(path)
+ var toMoveTo string
+ var err error
+ path = filepath.Clean(path)
+ toMoveTo = trashDir + "/" + filepath.Base(path)
if path == toMoveTo { // small edge case when trashing a file from trash
handleErrStr(color.YellowString(path) + " is already in trash")
return
}
- if _, err = os.Stat(path); os.IsNotExist(err) {
+ if !exists(path) {
handleErrStr(color.YellowString(path) + " does not exist")
return
}
- i := 0
- for exists(toMoveTo) { // while it exists (shouldn't) // big fiasco for avoiding clashes and using smallest timestamp possible along with easter eggs
+ toMoveTo = getTimestampedPath(toMoveTo, exists)
+ path = getTimestampedPath(path, existsInLog)
+ if renameByCopyIsAllowed {
+ err = renameByCopyAllowed(path, toMoveTo)
+ } else {
+ err = os.Rename(path, toMoveTo)
+ }
+ if err != nil {
+ handleErr(err)
+ return
+ }
+ m := getLogFile()
+ absPath, _ := filepath.Abs(path)
+ m[absPath] = toMoveTo // format is path where it came from ==> path in trash
+ setLogFile(m)
+ // if we've reached here, trashing is complete and successful
+ // TODO: Print with quotes only if it contains spaces
+ fmt.Println("Trashed " + color.YellowString(path) + "\nUndo using " + color.YellowString("rem --undo \""+path+"\""))
+}
+
+func renameByCopyAllowed(src, dst string) error {
+ err := os.Rename(src, dst)
+ if err == nil {
+ return nil
+ }
+ lerr := err.(*os.LinkError)
+ if lerr.Err == syscall.EXDEV {
+ // rename by copying and deleting
+ err = copy.Copy(src, dst)
+ permanentlyDeleteFile(src)
+ }
+ return err
+}
+
+// existsFunc() is used to determine if there is a conflict. It should return true if there is a conflict.
+func getTimestampedPath(path string, existsFunc func(string) bool) string {
+ var i int // make i accessible in function scope to check if it changed
+ oldPath := path
+ for ; existsFunc(path); i++ { // big fiasco for avoiding clashes and using smallest timestamp possible along with easter eggs
switch i {
case 0:
- toMoveTo = trashDir + "/" + filepath.Base(path) + " Deleted at " + time.Now().Format(time.Stamp)
+ path = oldPath + time.Now().Format(time.Stamp)
case 1: // seconds are the same
- toMoveTo = trashDir + "/" + filepath.Base(path) + " Deleted at " + time.Now().Format(time.StampMilli)
- fmt.Println("No way. This is super unlikely. Please contact my creator at igoel.mail@gmail.com or on github @quackduck and tell him what you were doing.")
+ path = oldPath + time.Now().Format(time.StampMilli)
case 2: // milliseconds are same
- toMoveTo = trashDir + "/" + filepath.Base(path) + " Deleted at " + time.Now().Format(time.StampMicro)
- fmt.Println("What the actual heck. Please contact him.")
+ path = oldPath + time.Now().Format(time.StampMicro)
+ fmt.Println("No way. This is super unlikely. Please contact my creator at igoel.mail@gmail.com or on github @quackduck and tell him what you were doing.")
case 3: // microseconds are same
- toMoveTo = trashDir + "/" + filepath.Base(path) + " Deleted at " + time.Now().Format(time.StampNano)
+ path = oldPath + time.Now().Format(time.StampNano)
fmt.Println("You are a god.")
case 4:
rand.Seed(time.Now().UTC().UnixNano()) // prep for default case
@@ -217,27 +213,66 @@ func trashFile(path string) {
if i == 4 { // seed once
rand.Seed(time.Now().UTC().UnixNano())
}
- toMoveTo = trashDir + "/" + filepath.Base(path) + strconv.FormatFloat(rand.Float64(), 'E', -1, 64) // add random stuff at the end
+ path = oldPath + strconv.FormatInt(rand.Int63(), 10) // add random stuff at the end
}
+ }
+ if i != 0 {
+ fmt.Println("To avoid conflicts, " + color.YellowString(oldPath) + " will now be called " + color.YellowString(path))
+ }
+ return path
+}
+
+func listFilesInTrash() []string {
+ m := getLogFile()
+ s := make([]string, len(m))
+ i := 0
+ // wd, _ := os.Getwd()
+ for key := range m {
+ // s[i] = strings.TrimPrefix(key, wd+string(filepath.Separator)) // list relative
+ s[i] = key
i++
}
- err = os.Rename(path, toMoveTo)
+ return s
+}
+
+func emptyTrash() {
+ permanentlyDeleteFile(trashDir)
+}
+
+func getLogFile() map[string]string {
+ if logFile != nil {
+ return logFile
+ }
+ ensureTrashDir()
+ file, err := os.OpenFile(trashDir+"/"+logFileName, os.O_CREATE|os.O_RDONLY, 0644)
if err != nil {
handleErr(err)
- return
+ return nil
}
- m := getLogFile()
- oldPath := path
- i = 1
- for ; existsInMap(m, path); i++ { // might be the same path as before
- path = oldPath + " " + strconv.Itoa(i)
+ defer file.Close()
+ lines := make(map[string]string)
+ dec := gob.NewDecoder(file)
+ err = dec.Decode(&lines)
+ if err != nil && err != io.EOF {
+ handleErr(err)
+ }
+ return lines
+}
+
+func setLogFile(m map[string]string) {
+ //f, err := os.OpenFile(trashDir+"/"+logFileName, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644) // truncate to empty, create if not exist, write only
+ ensureTrashDir()
+ f, err := os.Create(trashDir + "/" + logFileName)
+ if err != nil {
+ handleErr(err)
+ return
}
- if i != 1 {
- fmt.Println("A file of this exact path was deleted earlier. To avoid conflicts, this file will now be called " + color.YellowString(path))
+ defer f.Close()
+ enc := gob.NewEncoder(f)
+ err = enc.Encode(m)
+ if err != nil && err != io.EOF {
+ handleErr(err)
}
- m[path] = toMoveTo // format is path where it came from ==> path in trash
- setLogFile(m)
- fmt.Println("Trashed " + color.YellowString(path) + "\nUndo using " + color.YellowString("rem --undo \""+path+"\""))
}
func exists(path string) bool {
@@ -245,7 +280,8 @@ func exists(path string) bool {
return !(os.IsNotExist(err))
}
-func existsInMap(m map[string]string, elem string) bool {
+func existsInLog(elem string) bool {
+ m := getLogFile()
_, alreadyExists := m[elem]
return alreadyExists
}
@@ -273,6 +309,30 @@ func permanentlyDeleteFile(fileName string) {
}
}
+//
+//func renameByCopyIsAllowed(source, dest string) error {
+// inputFile, err := os.Open(source)
+// if err != nil {
+// return err
+// }
+// defer inputFile.Close()
+// outputFile, err := os.Create(dest)
+// if err != nil {
+// return err
+// }
+// defer outputFile.Close()
+// _, err = io.Copy(outputFile, inputFile)
+// if err != nil {
+// return err
+// }
+// // The copy was successful, so now delete the original file
+// err = os.RemoveAll(source)
+// if err != nil {
+// return err
+// }
+// return nil
+//}
+
// Utilities:
func promptBool(promptStr string) (yes bool) {