Skip to content

Commit

Permalink
Show deleted files not as abs path so message is short, refactor, fix…
Browse files Browse the repository at this point in the history
… some bugs
  • Loading branch information
quackduck committed Feb 17, 2021
1 parent b8eb472 commit e24b57d
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 89 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
238 changes: 149 additions & 89 deletions rem.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"os"
"path/filepath"
"strconv"
"syscall"
"time"

"github.com/fatih/color"
"github.com/otiai10/copy"
)

var (
Expand All @@ -29,12 +31,17 @@ Options:
-t/--set-trash <dir> 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 {
Expand Down Expand Up @@ -76,6 +83,7 @@ func main() {
main()
return
}

if hasOption, _ := argsHaveOption("directory", "d"); hasOption {
fmt.Println(trashDir)
return
Expand All @@ -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")
Expand All @@ -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
Expand All @@ -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
Expand All @@ -217,35 +213,75 @@ 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 {
_, err := os.Stat(path)
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
}
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit e24b57d

Please sign in to comment.