Skip to content

Instantly share code, notes, and snippets.

@frizz925
Last active April 29, 2021 22:14
Show Gist options
  • Save frizz925/12f063328b4eba2ecc7307aef61096d4 to your computer and use it in GitHub Desktop.
Save frizz925/12f063328b4eba2ecc7307aef61096d4 to your computer and use it in GitHub Desktop.
Remove empty directories and non-media files
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