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) {