initial commit
This commit is contained in:
parent
bfed7a14c0
commit
0055f56291
121
commands.go
Normal file
121
commands.go
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func help(args []string) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(Translate("Commands:"))
|
||||||
|
fmt.Println(Translate("ls: list files"))
|
||||||
|
fmt.Println(Translate("cd: change directory"))
|
||||||
|
fmt.Println(Translate("rm: mark files for removal"))
|
||||||
|
fmt.Println(Translate("urm: unmark files for removal"))
|
||||||
|
fmt.Println(Translate("exit: delete marked files and exit"))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ls(args []string) {
|
||||||
|
path := strings.Join(args, " ")
|
||||||
|
if path == "" {
|
||||||
|
path = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := getEntriesForPath(path)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
showError("'%s' could not be found", path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println(entry.StringRecursive(1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func rm(args []string, removalMark bool) {
|
||||||
|
path := strings.Join(args, " ")
|
||||||
|
|
||||||
|
entries := getEntriesForPath(path)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
showError("'%s' could not be found", path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
entry.SetRemovalMark(removalMark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cd(args []string) {
|
||||||
|
path := "/"
|
||||||
|
if len(args) > 0 {
|
||||||
|
path = args[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := getEntriesForPath(path)
|
||||||
|
if len(entries) == 0 {
|
||||||
|
showError("'%s' could not be found", path)
|
||||||
|
return
|
||||||
|
} else if len(entries) > 1 {
|
||||||
|
b := new(strings.Builder)
|
||||||
|
for _, entry := range entries {
|
||||||
|
path := strings.TrimPrefix(entry.Path(), Current.Path()+"/")
|
||||||
|
path = strings.TrimPrefix(path, Root.Path())
|
||||||
|
b.WriteString(path)
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
showError("ambiguous path: %s", path)
|
||||||
|
fmt.Println(Translate("possible paths:\n%s", b.String()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := entries[0]
|
||||||
|
if entry.IsDir() {
|
||||||
|
Current = entry
|
||||||
|
fmt.Println()
|
||||||
|
} else {
|
||||||
|
showError("'%s' is not a directory", entry.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exit() {
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
stats := new(RemovalStats)
|
||||||
|
Root.RemovalStats(stats)
|
||||||
|
|
||||||
|
if len(stats.Paths) == 0 {
|
||||||
|
s := bufio.NewScanner(os.Stdin)
|
||||||
|
s.Split(bufio.ScanRunes)
|
||||||
|
|
||||||
|
fmt.Print(Translate("exit? [y/N]: "))
|
||||||
|
if s.Scan() {
|
||||||
|
text := strings.ToLower(s.Text())
|
||||||
|
if text == "y" || text == "yes" {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(Translate("The following items will be removed:"))
|
||||||
|
fmt.Println(stats)
|
||||||
|
|
||||||
|
s := bufio.NewScanner(os.Stdin)
|
||||||
|
s.Split(bufio.ScanRunes)
|
||||||
|
|
||||||
|
fmt.Print(Translate("Delete files? [y/N/c]: "))
|
||||||
|
if s.Scan() {
|
||||||
|
text := strings.ToLower(s.Text())
|
||||||
|
if text == "y" || text == "yes" {
|
||||||
|
removeMarkedEntries(Root)
|
||||||
|
os.Exit(0)
|
||||||
|
} else if text == "n" || text == "no" {
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
183
dir_entry.go
Normal file
183
dir_entry.go
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DirectoryEntry struct {
|
||||||
|
path string
|
||||||
|
size *uint64
|
||||||
|
modSize *uint64
|
||||||
|
entries []Entry
|
||||||
|
parent Entry
|
||||||
|
|
||||||
|
removal bool
|
||||||
|
noPerm bool
|
||||||
|
isMount bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Entry = &DirectoryEntry{}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) Name() string {
|
||||||
|
return filepath.Base(e.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) Path() string {
|
||||||
|
return e.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) Size() uint64 {
|
||||||
|
if e.size == nil {
|
||||||
|
size := uint64(0)
|
||||||
|
for _, c := range e.Entries() {
|
||||||
|
size += c.Size()
|
||||||
|
}
|
||||||
|
e.size = &size
|
||||||
|
|
||||||
|
e.modSize = new(uint64)
|
||||||
|
*e.modSize = *e.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return *e.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) Entries() []Entry {
|
||||||
|
if e.entries == nil {
|
||||||
|
e.Scan(nil, nil)
|
||||||
|
}
|
||||||
|
return e.entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) String() string {
|
||||||
|
if e.removal {
|
||||||
|
return ColorRed.Sprintf("%s %s (%s)", icon("folder"), e.Name(), fmtSize(e.SizeModified()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Size() == e.SizeModified() {
|
||||||
|
return ColorBlue.Sprintf("%s %s (%s)", icon("folder"), e.Name(), fmtSize(e.Size()))
|
||||||
|
} else {
|
||||||
|
return ColorBlue.Sprintf("%s %s (%s / %s)", icon("folder"), e.Name(), ColorRed.Sprintf(fmtSize(e.SizeModified())), fmtSize(e.Size()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) stringRecursive(depth, maxDepth int, b *strings.Builder) {
|
||||||
|
if depth > maxDepth {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(strings.Repeat(" ", depth))
|
||||||
|
b.WriteString(e.String())
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
for _, entry := range e.Entries() {
|
||||||
|
entry.stringRecursive(depth+1, maxDepth, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) StringRecursive(depth int) string {
|
||||||
|
b := new(strings.Builder)
|
||||||
|
e.stringRecursive(0, depth, b)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) Scan(ch chan<- string, mounts map[string]struct{}) {
|
||||||
|
dirEntries, err := os.ReadDir(e.Path())
|
||||||
|
if errors.Is(err, os.ErrPermission) {
|
||||||
|
e.noPerm = true
|
||||||
|
e.entries = []Entry{}
|
||||||
|
return
|
||||||
|
} else if err != nil {
|
||||||
|
e.noPerm = true
|
||||||
|
e.entries = []Entry{}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.entries = make([]Entry, 0, len(dirEntries))
|
||||||
|
for _, dirEntry := range dirEntries {
|
||||||
|
entry := NewEntryFromDirEntry(e, dirEntry)
|
||||||
|
if _, isMount := mounts[entry.Path()]; !isMount {
|
||||||
|
e.entries = append(e.entries, entry)
|
||||||
|
entry.Scan(ch, mounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch != nil {
|
||||||
|
ch <- e.Path()
|
||||||
|
}
|
||||||
|
sort.Slice(e.entries, func(i, j int) bool {
|
||||||
|
return e.entries[i].SizeModified() < e.entries[j].SizeModified()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) Parent() Entry {
|
||||||
|
return e.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) Entry(path string) Entry {
|
||||||
|
if path == "." || path == "" {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
splits := strings.Split(path, "/")
|
||||||
|
for _, entry := range e.Entries() {
|
||||||
|
if entry.Name() == splits[0] {
|
||||||
|
return entry.Entry(strings.Join(splits[1:], "/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) IsDir() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) SetRemovalMark(mark bool) {
|
||||||
|
e.removal = mark
|
||||||
|
for _, entry := range e.Entries() {
|
||||||
|
entry.SetRemovalMark(mark)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.Parent() != nil {
|
||||||
|
pentries := e.Parent().Entries()
|
||||||
|
sort.Slice(pentries, func(i, j int) bool {
|
||||||
|
return pentries[i].SizeModified() < pentries[j].SizeModified()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) RemovalMark() bool {
|
||||||
|
return e.removal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) SizeModified() uint64 {
|
||||||
|
if e.size == nil {
|
||||||
|
e.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
return *e.modSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) modifySize(modifier uint64) {
|
||||||
|
*e.modSize += modifier
|
||||||
|
if e.parent != nil {
|
||||||
|
e.parent.modifySize(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *DirectoryEntry) RemovalStats(stats *RemovalStats) {
|
||||||
|
if e.RemovalMark() {
|
||||||
|
stats.Dirs++
|
||||||
|
if e.Parent() != nil && !e.Parent().RemovalMark() {
|
||||||
|
stats.Paths = append(stats.Paths, e.Path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range e.Entries() {
|
||||||
|
entry.RemovalStats(stats)
|
||||||
|
}
|
||||||
|
}
|
72
entry.go
Normal file
72
entry.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry interface {
|
||||||
|
fmt.Stringer
|
||||||
|
stringRecursive(depth, maxDepth int, b *strings.Builder)
|
||||||
|
StringRecursive(depth int) string
|
||||||
|
|
||||||
|
Name() string
|
||||||
|
Path() string
|
||||||
|
Size() uint64
|
||||||
|
SizeModified() uint64
|
||||||
|
Entries() []Entry
|
||||||
|
Entry(path string) Entry
|
||||||
|
|
||||||
|
SetRemovalMark(mark bool)
|
||||||
|
RemovalMark() bool
|
||||||
|
RemovalStats(stats *RemovalStats)
|
||||||
|
|
||||||
|
modifySize(modifier uint64)
|
||||||
|
|
||||||
|
IsDir() bool
|
||||||
|
|
||||||
|
Parent() Entry
|
||||||
|
|
||||||
|
Scan(ch chan<- string, mounts map[string]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntryFromDirEntry(parentEntry Entry, dirEntry fs.DirEntry) Entry {
|
||||||
|
if dirEntry.IsDir() {
|
||||||
|
return &DirectoryEntry{
|
||||||
|
path: filepath.Join(parentEntry.Path(), dirEntry.Name()),
|
||||||
|
parent: parentEntry,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
size := uint64(0)
|
||||||
|
noPerm := false
|
||||||
|
if fi, err := dirEntry.Info(); err == nil {
|
||||||
|
size = uint64(fi.Size())
|
||||||
|
} else {
|
||||||
|
noPerm = true
|
||||||
|
}
|
||||||
|
return &FileEntry{
|
||||||
|
path: filepath.Join(parentEntry.Path(), dirEntry.Name()),
|
||||||
|
size: &size,
|
||||||
|
noPerm: noPerm,
|
||||||
|
parent: parentEntry,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEntry(path string) (Entry, error) {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
|
||||||
|
fi, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.IsDir() {
|
||||||
|
return &DirectoryEntry{path: path}, nil
|
||||||
|
} else {
|
||||||
|
return &FileEntry{path: path}, nil
|
||||||
|
}
|
||||||
|
}
|
149
file_entry.go
Normal file
149
file_entry.go
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileEntry struct {
|
||||||
|
path string
|
||||||
|
size *uint64
|
||||||
|
modSize *uint64
|
||||||
|
parent Entry
|
||||||
|
|
||||||
|
removal bool
|
||||||
|
noPerm bool
|
||||||
|
isMount bool
|
||||||
|
isExec bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Entry = &FileEntry{}
|
||||||
|
|
||||||
|
func (e *FileEntry) Name() string {
|
||||||
|
return filepath.Base(e.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) Path() string {
|
||||||
|
return e.path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) Size() uint64 {
|
||||||
|
if e.size == nil {
|
||||||
|
e.Scan(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *e.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) Entries() []Entry {
|
||||||
|
return []Entry{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) String() string {
|
||||||
|
var ic string
|
||||||
|
if e.isExec {
|
||||||
|
ic = icon("app")
|
||||||
|
} else {
|
||||||
|
ic = icon(filepath.Ext(e.Path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.removal {
|
||||||
|
return ColorRed.Sprintf(
|
||||||
|
"%s %s (%s / %s)",
|
||||||
|
ic, e.Name(), fmtSize(e.SizeModified()), fmtSize(e.Size()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.isExec {
|
||||||
|
return ColorGreen.Sprintf("%s %s (%s)", ic, e.Name(), fmtSize(e.Size()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s %s (%s)", ic, e.Name(), fmtSize(e.Size()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) stringRecursive(depth, maxDepth int, b *strings.Builder) {
|
||||||
|
if depth > maxDepth {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString(strings.Repeat(" ", depth))
|
||||||
|
b.WriteString(e.String())
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) StringRecursive(depth int) string {
|
||||||
|
b := new(strings.Builder)
|
||||||
|
e.stringRecursive(0, depth, b)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) Scan(ch chan<- string, mounts map[string]struct{}) {
|
||||||
|
if ch != nil {
|
||||||
|
ch <- e.Path()
|
||||||
|
}
|
||||||
|
|
||||||
|
s := uint64(0)
|
||||||
|
if fi, err := os.Lstat(e.Path()); err == nil {
|
||||||
|
s = uint64(fi.Size())
|
||||||
|
e.isExec = fi.Mode()&0x41 > 0 // 0x41 == 0b001000001 == rwXrwxrwX
|
||||||
|
} else if errors.Is(err, os.ErrPermission) {
|
||||||
|
e.noPerm = true
|
||||||
|
}
|
||||||
|
e.size = &s
|
||||||
|
|
||||||
|
e.modSize = new(uint64)
|
||||||
|
*e.modSize = *e.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) Parent() Entry {
|
||||||
|
return e.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) Entry(path string) Entry {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) IsDir() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) SetRemovalMark(mark bool) {
|
||||||
|
e.removal = mark
|
||||||
|
if mark {
|
||||||
|
e.modifySize(-*e.size)
|
||||||
|
} else {
|
||||||
|
e.modifySize(*e.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) RemovalMark() bool {
|
||||||
|
return e.removal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) SizeModified() uint64 {
|
||||||
|
if e.size == nil {
|
||||||
|
e.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
return *e.modSize
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) modifySize(modifier uint64) {
|
||||||
|
*e.modSize += modifier
|
||||||
|
if e.parent != nil {
|
||||||
|
e.parent.modifySize(modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *FileEntry) RemovalStats(stats *RemovalStats) {
|
||||||
|
if e.RemovalMark() {
|
||||||
|
stats.Files++
|
||||||
|
stats.Bytes += *e.size
|
||||||
|
if e.Parent() != nil && !e.Parent().RemovalMark() {
|
||||||
|
stats.Paths = append(stats.Paths, e.Path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
go.mod
Normal file
15
go.mod
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module git.tordarus.net/Tordarus/diskspace
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.13.0
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-colorable v0.1.9 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
|
||||||
|
)
|
16
go.sum
Normal file
16
go.sum
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||||
|
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||||
|
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
|
||||||
|
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||||
|
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
185
main.go
Normal file
185
main.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Current Entry
|
||||||
|
Root Entry
|
||||||
|
|
||||||
|
ColorRed = color.New(color.FgRed)
|
||||||
|
ColorBlue = color.New(color.FgBlue)
|
||||||
|
ColorGreen = color.New(color.FgGreen)
|
||||||
|
|
||||||
|
IconTheme = flag.String("i", "unicode", "icon theme (currently supported: default, unicode, nerd)")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
rootPath := flag.Arg(0)
|
||||||
|
|
||||||
|
mounts, err := Mounts()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "Mount points could not be read! All mount points will be calculated as well")
|
||||||
|
}
|
||||||
|
|
||||||
|
if abs, err := filepath.Abs(rootPath); err == nil {
|
||||||
|
rootPath = abs
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
root, err := NewEntry(rootPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
ScanEntryLive(root, mounts)
|
||||||
|
fmt.Println(Translate("Scanning took %s", time.Since(start)))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
for range c {
|
||||||
|
fmt.Println()
|
||||||
|
showError("Use command 'exit' for exiting properly")
|
||||||
|
fmt.Print("> ")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
Root = root
|
||||||
|
Current = root
|
||||||
|
|
||||||
|
s := bufio.NewScanner(os.Stdin)
|
||||||
|
showCurrentDir := true
|
||||||
|
for {
|
||||||
|
if showCurrentDir {
|
||||||
|
fmt.Println(Current.StringRecursive(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("> ")
|
||||||
|
|
||||||
|
s.Scan()
|
||||||
|
input := s.Text()
|
||||||
|
showCurrentDir = handleInput(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleInput(input string) bool {
|
||||||
|
cmd := strings.Split(strings.TrimSpace(input), " ")
|
||||||
|
switch cmd[0] {
|
||||||
|
case "ls":
|
||||||
|
ls(cmd[1:])
|
||||||
|
return false
|
||||||
|
case "cd":
|
||||||
|
cd(cmd[1:])
|
||||||
|
case "rm":
|
||||||
|
rm(cmd[1:], true)
|
||||||
|
case "urm":
|
||||||
|
rm(cmd[1:], false)
|
||||||
|
case "exit":
|
||||||
|
exit()
|
||||||
|
case "help":
|
||||||
|
help(cmd[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func showError(str string, args ...interface{}) {
|
||||||
|
fmt.Println()
|
||||||
|
ColorRed.Println(Translate(str, args...))
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryByPath(path string) Entry {
|
||||||
|
if !strings.HasPrefix(path, Root.Path()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path = strings.TrimPrefix(path, Root.Path())
|
||||||
|
if path == "/" || path == "" {
|
||||||
|
return Root
|
||||||
|
} else {
|
||||||
|
path = strings.TrimPrefix(path, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
return Root.Entry(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEntriesForPath(path string) []Entry {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
path = filepath.Join(Root.Path(), path)
|
||||||
|
} else {
|
||||||
|
path = filepath.Join(Current.Path(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
paths, err := filepath.Glob(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]Entry, 0, len(paths))
|
||||||
|
for _, path := range paths {
|
||||||
|
if entry := entryByPath(path); entry != nil {
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScanEntryLive(entry Entry, mounts map[string]struct{}) {
|
||||||
|
ch := make(chan string, 10)
|
||||||
|
|
||||||
|
wg := new(sync.WaitGroup)
|
||||||
|
|
||||||
|
// TODO cap string size to terminal width
|
||||||
|
//width, _, _ := terminal.GetSize(int(os.Stdin.Fd()))
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(ch <-chan string) {
|
||||||
|
defer wg.Done()
|
||||||
|
for path := range ch {
|
||||||
|
os.Stdout.Write([]byte{'\r'})
|
||||||
|
clearEOL()
|
||||||
|
fmt.Print(Translate("Scanning ") + path)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Stdout.Write([]byte{'\r'})
|
||||||
|
clearEOL()
|
||||||
|
}(ch)
|
||||||
|
|
||||||
|
entry.Scan(ch, mounts)
|
||||||
|
close(ch)
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeMarkedEntries(entry Entry) {
|
||||||
|
if entry.RemovalMark() {
|
||||||
|
err := os.RemoveAll(entry.Path())
|
||||||
|
if err != nil {
|
||||||
|
ColorRed.Println(Translate("Could not delete '%s': %s", entry.Path(), err.Error()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, entry := range entry.Entries() {
|
||||||
|
removeMarkedEntries(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
mount.go
Normal file
32
mount.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
mountLineRegex = regexp.MustCompile(`^.*? on (.*?) type (.*?) \((?:.*?[,)])*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func Mounts() (map[string]struct{}, error) {
|
||||||
|
mounts := map[string]struct{}{}
|
||||||
|
|
||||||
|
out, err := exec.Command("mount").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s := bufio.NewScanner(bytes.NewReader(out))
|
||||||
|
for s.Scan() {
|
||||||
|
matches := mountLineRegex.FindAllStringSubmatch(s.Text(), -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mounts[matches[0][1]] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mounts, nil
|
||||||
|
}
|
34
stats.go
Normal file
34
stats.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RemovalStats struct {
|
||||||
|
Files int
|
||||||
|
Dirs int
|
||||||
|
Bytes uint64
|
||||||
|
Paths []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RemovalStats) String() string {
|
||||||
|
b := new(strings.Builder)
|
||||||
|
|
||||||
|
for _, path := range s.Paths {
|
||||||
|
b.WriteString(path)
|
||||||
|
b.WriteRune('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
b.WriteString(Translate("Files: %d", s.Files))
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
b.WriteString(Translate("Directories: %d", s.Dirs))
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
b.WriteString(Translate("Total size: %s", fmtSize(s.Bytes)))
|
||||||
|
b.WriteRune('\n')
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
73
translate.go
Normal file
73
translate.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var Languages = map[string]map[string]string{
|
||||||
|
"de": map[string]string{
|
||||||
|
"Scanning ": "Verarbeite ",
|
||||||
|
"Scanning took %s": "Verarbeitung dauerte %s",
|
||||||
|
"'%s' is not a directory": "'%s' ist kein Verzeichnis",
|
||||||
|
"The following items will be removed:": "Die folgenden Elemente werden entfernt:",
|
||||||
|
"Files: %d": "Dateien: %d",
|
||||||
|
"Directories: %d": "Ordner: %d",
|
||||||
|
"Total size: %s": "Gesamtgröße: %s",
|
||||||
|
"'%s' could not be found": "'%s' konnte nicht gefunden werden",
|
||||||
|
"Could not delete '%s': %s": "'%s' konnte nicht gelöscht werden: %s",
|
||||||
|
"Delete files? [y/N/c]: ": "Dateien löschen? [y/N/c]: ",
|
||||||
|
"ambiguous path: %s": "Nicht eindeutiger Pfad: %s",
|
||||||
|
"possible paths:\n%s": "Mögliche Pfade:\n%s",
|
||||||
|
"Commands:": "Befehle:",
|
||||||
|
"ls: list files": "ls: Dateien auflisten",
|
||||||
|
"cd: change directory": "cd: Verzeichnis wechseln",
|
||||||
|
"rm: mark files for removal": "rm: Markiere Dateien zur Löschung",
|
||||||
|
"urm: unmark files for removal": "urm: Entmarkiere Dateien zur Löschung",
|
||||||
|
"exit: delete marked files and exit": "exit: Lösche markierte Dateien und beende Programm",
|
||||||
|
"Use command 'exit' for exiting properly": "Nutze den Befehl 'exit', um das Programm ordnungsgemäß zu beenden",
|
||||||
|
"exit? [y/N]: ": "Beenden? [y/N]: ",
|
||||||
|
},
|
||||||
|
"ja": map[string]string{
|
||||||
|
"Scanning ": "処理 ",
|
||||||
|
"Scanning took %s": "処理が %s かかりました",
|
||||||
|
"'%s' is not a directory": "「%s」はディレクトリではありません",
|
||||||
|
"The following items will be removed:": "このファイルは削除します:",
|
||||||
|
"Files: %d": "ファイル: %d",
|
||||||
|
"Directories: %d": "デイレクトリ: %d",
|
||||||
|
"Total size: %s": "全額: %s",
|
||||||
|
"'%s' could not be found": "「%s」が見つかりませんでした",
|
||||||
|
"Could not delete '%s': %s": "「%s」が削除できませんでした: %s",
|
||||||
|
"Delete files? [y/N/c]: ": "削除してほしいですか? [y/N/c]: ",
|
||||||
|
"ambiguous path: %s": "あいまいなパス: %s",
|
||||||
|
"possible paths:\n%s": "可能なパス:\n%s",
|
||||||
|
"Commands:": "コマンド:",
|
||||||
|
"ls: list files": "ls: ファイルを並べます",
|
||||||
|
"cd: change directory": "cd: ディレクトリを変更",
|
||||||
|
"rm: mark files for removal": "rm: ファイルに削除のマークを付けます",
|
||||||
|
"urm: unmark files for removal": "urm: ファイルに削除のマークを除きます",
|
||||||
|
"exit: delete marked files and exit": "exit: 削除のマークされたファイルを削除して終了",
|
||||||
|
"Use command 'exit' for exiting properly": "「exit」コマンドを使って終了します",
|
||||||
|
"exit? [y/N]: ": "終了してほしいですか? [y/N]: ",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var langmap map[string]string
|
||||||
|
|
||||||
|
func Translate(str string, args ...interface{}) string {
|
||||||
|
if langmap == nil {
|
||||||
|
langVar, _ := os.LookupEnv("LANG")
|
||||||
|
langmap = map[string]string{}
|
||||||
|
for lname, lmap := range Languages {
|
||||||
|
if strings.HasPrefix(langVar, lname) {
|
||||||
|
langmap = lmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if translatedText, ok := langmap[str]; ok {
|
||||||
|
return fmt.Sprintf(translatedText, args...)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(str, args...)
|
||||||
|
}
|
77
utils.go
Normal file
77
utils.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fmtSize(size uint64) string {
|
||||||
|
sf := float64(size)
|
||||||
|
if size/1000000000000 >= 1 {
|
||||||
|
return strconv.FormatFloat(round(sf/1000000000000), 'G', -1, 64) + " TB"
|
||||||
|
} else if size/1000000000 >= 1 {
|
||||||
|
return strconv.FormatFloat(round(sf/1000000000), 'G', -1, 64) + " GB"
|
||||||
|
} else if size/1000000 >= 1 {
|
||||||
|
return strconv.FormatFloat(round(sf/1000000), 'G', -1, 64) + " MB"
|
||||||
|
} else if size/1000 >= 1 {
|
||||||
|
return strconv.FormatFloat(round(sf/1000), 'G', -1, 64) + " KB"
|
||||||
|
} else {
|
||||||
|
return strconv.FormatFloat(round(sf), 'G', -1, 64) + " B"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func round(value float64) float64 {
|
||||||
|
return math.Round(value*100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func icon(ext string) string {
|
||||||
|
switch *IconTheme {
|
||||||
|
default:
|
||||||
|
fallthrough
|
||||||
|
case "unicode":
|
||||||
|
switch ext {
|
||||||
|
case "folder":
|
||||||
|
return "📂"
|
||||||
|
default:
|
||||||
|
return "📄"
|
||||||
|
}
|
||||||
|
case "nerd":
|
||||||
|
switch ext {
|
||||||
|
case "app":
|
||||||
|
return "ﬓ"
|
||||||
|
case "folder":
|
||||||
|
return ""
|
||||||
|
case ".doc", ".docx", ".odt", ".rtx", ".pdf", ".tex":
|
||||||
|
return ""
|
||||||
|
case ".xls", ".xlsx", ".xlsm", ".dex", ".ods", ".csv":
|
||||||
|
return ""
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif", ".ppm", ".svg", ".tiff", ".bmp", ".webp", ".blend", ".obj":
|
||||||
|
return ""
|
||||||
|
case ".go", ".c", ".cpp", ".rs", ".h", ".java", ".asm", ".js", ".html", ".css", ".ts", ".sql":
|
||||||
|
return ""
|
||||||
|
case ".mp3", ".flac", ".m4a", ".opus", ".wav", ".wma":
|
||||||
|
return ""
|
||||||
|
case ".mp4", ".webm", ".mkv", ".flv", ".ogg", ".ogv", ".gifv", ".avi", ".mov", ".wmv", ".mp2", ".mpg", ".mpv", ".mpe", ".mpeg", ".m2v", ".3gp":
|
||||||
|
return ""
|
||||||
|
case ".txt":
|
||||||
|
return ""
|
||||||
|
case ".zip", ".rar", ".7z", ".tar", ".bz2", ".gz", ".xz", ".tgz", ".tbz2", ".txz":
|
||||||
|
return ""
|
||||||
|
case ".iso":
|
||||||
|
return ""
|
||||||
|
case ".apk":
|
||||||
|
return ""
|
||||||
|
case ".jar":
|
||||||
|
return ""
|
||||||
|
case ".json", ".yaml", ".yml":
|
||||||
|
return ""
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearEOL() {
|
||||||
|
os.Stdout.Write([]byte{0x1b, 0x5b, 0x4b})
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user