Last active
April 29, 2021 22:14
-
-
Save frizz925/12f063328b4eba2ecc7307aef61096d4 to your computer and use it in GitHub Desktop.
Remove empty directories and non-media files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"errors" | |
"fmt" | |
"io/fs" | |
"io/ioutil" | |
"log" | |
"os" | |
"os/signal" | |
"path" | |
"runtime" | |
"sync" | |
"syscall" | |
"time" | |
) | |
var keepExts = []string{ | |
".avi", ".mp4", ".mkv", | |
".png", ".jpg", ".jpeg", | |
".mp3", ".wav", ".flac", ".aac", | |
} | |
var keepExtMap map[string]struct{} | |
type worker struct { | |
dirname string | |
deleteDir string | |
} | |
func init() { | |
keepExtMap = make(map[string]struct{}) | |
for _, ext := range keepExts { | |
keepExtMap[ext] = struct{}{} | |
} | |
} | |
func main() { | |
dirname := "." | |
if len(os.Args) >= 2 { | |
dirname = os.Args[1] | |
} | |
if err := run(dirname); err != nil { | |
panic(err) | |
} | |
} | |
func run(dirname string) error { | |
ctx, cancel := context.WithCancel(context.Background()) | |
defer cancel() | |
deleteDir := path.Join(dirname, ".deleted") | |
stat, err := os.Stat(deleteDir) | |
if os.IsNotExist(err) { | |
if err := os.Mkdir(deleteDir, 0755); err != nil { | |
return err | |
} | |
} else if !stat.IsDir() { | |
return fmt.Errorf("%s is not a directory", deleteDir) | |
} | |
ch := make(chan os.Signal, 1) | |
signal.Notify(ch, os.Interrupt, syscall.SIGTERM) | |
go func() { | |
sig := <-ch | |
log.Printf("Received signal: %+v", sig) | |
cancel() | |
}() | |
wg := &sync.WaitGroup{} | |
queue := make(chan fs.FileInfo) | |
for i := 0; i < runtime.NumCPU(); i++ { | |
wg.Add(1) | |
worker := &worker{ | |
dirname: dirname, | |
deleteDir: deleteDir, | |
} | |
go worker.loop(ctx, wg, queue) | |
} | |
files, err := ioutil.ReadDir(dirname) | |
if err != nil { | |
return err | |
} | |
for _, file := range files { | |
if file.Name() == ".deleted" { | |
continue | |
} | |
queue <- file | |
} | |
close(queue) | |
done := make(chan struct{}, 1) | |
timeout := time.After(30 * time.Second) | |
go func() { | |
wg.Wait() | |
close(done) | |
}() | |
select { | |
case <-timeout: | |
return errors.New("timeout waiting workers to finish") | |
case <-done: | |
} | |
return nil | |
} | |
func (w *worker) loop(ctx context.Context, wg *sync.WaitGroup, queue <-chan os.FileInfo) { | |
defer wg.Done() | |
for { | |
select { | |
case file, ok := <-queue: | |
if !ok { | |
// Queue is closed | |
return | |
} | |
if err := w.doWork(file); err != nil { | |
log.Println(err) | |
} | |
case <-ctx.Done(): | |
return | |
} | |
} | |
} | |
func (w *worker) doWork(parent fs.FileInfo) error { | |
if !parent.IsDir() { | |
return nil | |
} | |
dirname := parent.Name() | |
files, err := ioutil.ReadDir(w.fullPath(dirname)) | |
if err != nil { | |
return err | |
} | |
if len(files) <= 0 { | |
return w.softDeleteDir(dirname) | |
} | |
for _, file := range files { | |
filename := file.Name() | |
ext := path.Ext(filename) | |
if _, ok := keepExtMap[ext]; ok { | |
continue | |
} | |
if err := w.softDelete(dirname, filename); err != nil { | |
return err | |
} | |
} | |
return nil | |
} | |
func (w *worker) softDelete(dirname string, filename string) error { | |
source := w.fullPath(dirname, filename) | |
targetDir, target := w.deletedPath(dirname), w.deletedPath(dirname, filename) | |
if err := w.mkdir(targetDir); err != nil { | |
return err | |
} | |
log.Printf("Removing file %s", w.fullPath(dirname, filename)) | |
return os.Rename(source, target) | |
} | |
func (w *worker) softDeleteDir(dirname string) error { | |
source, target := w.fullPath(dirname), w.deletedPath(dirname) | |
log.Printf("Removing directory %s", source) | |
return os.Rename(source, target) | |
} | |
func (w *worker) mkdir(dirname string) error { | |
stat, err := os.Stat(dirname) | |
if os.IsNotExist(err) { | |
return os.Mkdir(dirname, 0755) | |
} else if !stat.IsDir() { | |
return fmt.Errorf("%s exists but not a directory", dirname) | |
} | |
return err | |
} | |
func (w *worker) fullPath(paths ...string) string { | |
return w.prefixPath(w.dirname, paths...) | |
} | |
func (w *worker) deletedPath(paths ...string) string { | |
return w.prefixPath(w.deleteDir, paths...) | |
} | |
func (w *worker) prefixPath(prefix string, paths ...string) string { | |
merged := make([]string, len(paths)+1) | |
merged[0] = prefix | |
for idx, path := range paths { | |
merged[idx+1] = path | |
} | |
return path.Join(merged...) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment