Undo copy (cp) with Python and Golang
The lesson of this post: It is okay to make mistakes, as long as you learn from them.
The first thing I want to point out is that macOS and linux treat cp
differently. On both OS, the -R
option is to copy a directory recursively(including subtree and everything). On macOS if the source
argument(a directory) has a /
, all its contents will be copied. If the /
is omitted, then the directory itself will be copied.
I messed up. Instead of copying a directory, I copied the entire content on my Desktop. Now it’s mixed up with a bunch of other stuff.
I’m gonna replay the scenario on a test directory. The content of source and target:
MacBook-Pro:test kavish$ ls -a source/
. .anothertest directory_in_source file2.txt file4.txt
.. .test file1.txt file3.txt file5.txt
MacBook-Pro:test kavish$
MacBook-Pro:test kavish$ ls -a target/
. directory_in_source file3.txt not_so_secret.yml secret_4.txt
.. directory_in_target file4.txt secret_1.txt secret_5.txt
.anothertest file1.txt file5.txt secret_2.txt
.test file2.txt .i_am_hidden secret_3.txt
Files and Directories from source was copied in the target directory. The target directory contains other files that I need to keep, which makes it impossible to simply remove all files.
The script below will look in the source directory, and keep a list of the filenames. Then it will concatenate the target directory with those filenames and remove them.
import os, shutil
source = "/tmp/test/source" # sys.argv[1]
target = "/tmp/test/target" # sys.argv[2]
files = os.listdir(source) # returns a list of top level directory contents
for file in files:
to_del = os.path.join(target, file) # join target with source file/directory
if os.path.isdir(to_del):
print(f"Removing Directory: {to_del}")
try:
shutil.rmtree(to_del) # remove directories and sub-directories
except FileNotFoundError:
pass
else:
print(f"Removing File: {to_del}")
try:
os.remove(to_del) # remove files
except FileNotFoundError:
pass
print("Done")
The FileNotFoundError
is very important here, because once shutil.rmtree()
encounters a directory, it will remove the whole tree.
Note: If
cp
overwrote some files in the destination with same names as the source, those files will be removed! You won’t be able to get the original.
Here’s the same thing written in Go:
package main
import (
"fmt"
"os"
path "path/filepath"
)
func check(err error) {
if err != nil {
panic(err)
}
}
func main() {
source := "/tmp/test/source" // os.Argv[1]
target := "/tmp/test/target" // os.Argv[1]
files, err := os.ReadDir(source)
check(err)
for _, file := range files {
to_del := path.Join(target, file.Name())
if file.IsDir() {
fmt.Printf("Removing Directory: %v\n", to_del)
err := os.RemoveAll(to_del)
check(err)
} else {
fmt.Printf("Removing File: %v\n", to_del)
err := os.Remove(to_del)
check(err)
}
}
}
Running the script:
MacBook-Pro:goworkshop kavish$ go run dirwalk.go
Removing File: /tmp/test/target/.anothertest
Removing Directory: /tmp/test/target/.test
Removing Directory: /tmp/test/target/directory_in_source
Removing File: /tmp/test/target/file1.txt
Removing File: /tmp/test/target/file2.txt
Removing File: /tmp/test/target/file3.txt
Removing File: /tmp/test/target/file4.txt
Removing File: /tmp/test/target/file5.txt
MacBook-Pro:goworkshop kavish$
MacBook-Pro:goworkshop kavish$ ls -a /tmp/test/target/
. directory_in_target not_so_secret.yml secret_2.txt secret_4.txt
.. .i_am_hidden secret_1.txt secret_3.txt secret_5.txt
Codes on Github: Go and Python
“When you ask God for a gift, Be thankful if he sends, Not diamonds, pearls or riches, but the love of real true friends.”
― Helen Steiner Rice