From cb47a01884e5ecb98fb504c0bf968d1a2e97a690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lilian=20J=C3=B3nsd=C3=B3ttir?= Date: Wed, 14 Aug 2024 18:52:45 -0700 Subject: [PATCH] add support for reading/writing $trashDir/directorysizes - show total trash size in header --- internal/files/directorysizes.go | 154 ++++++++++++++++++++++++++++ internal/files/disk.go | 45 +++++--- internal/files/files.go | 63 ++++++++++-- internal/files/trash.go | 127 ++++++++++++++--------- internal/interactive/interactive.go | 31 +++--- main.go | 1 + 6 files changed, 334 insertions(+), 87 deletions(-) create mode 100644 internal/files/directorysizes.go diff --git a/internal/files/directorysizes.go b/internal/files/directorysizes.go new file mode 100644 index 0000000..075d487 --- /dev/null +++ b/internal/files/directorysizes.go @@ -0,0 +1,154 @@ +package files + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "git.burning.moe/celediel/gt/internal/dirs" + "github.com/charmbracelet/log" +) + +const ( + directorysizes = "directorysizes" + length int = 3 +) + +var ( + loadedDirSizes directorySizes +) + +func init() { + loadedDirSizes = readDirectorySizesFromFile() +} + +type directorySize struct { + size int64 + mtime int64 + name string +} + +type directorySizes map[string]directorySize + +func WriteDirectorySizes() { + loadedDirSizes = updateDirectorySizes(loadedDirSizes) + writeDirectorySizes(loadedDirSizes) +} + +func readDirectorySizesFromFile() directorySizes { + dirSizes := directorySizes{} + for _, trash := range getAllTrashes() { + dsf := filepath.Join(trash, directorysizes) + if _, err := os.Lstat(dsf); os.IsNotExist(err) { + continue + } + + file, err := os.Open(dsf) + if err != nil { + log.Error(err) + continue + } + defer file.Close() + + var ( + size int64 + mtime int64 + name string + ) + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + split := strings.Split(line, " ") + if len(split) != length { + log.Errorf("malformed line '%s' in %s", line, dsf) + continue + } + + size, err = strconv.ParseInt(split[0], 10, 64) + if err != nil { + log.Errorf("size %s can't be int?", split[0]) + continue + } + + mtime, err = strconv.ParseInt(split[1], 10, 64) + if err != nil { + log.Errorf("mtime %s can't be int?", split[1]) + continue + } + + name = dirs.PercentDecode(split[2]) + + dirSize := directorySize{ + size: size, + mtime: mtime, + name: name, + } + dirSizes[name] = dirSize + } + } + + return dirSizes +} + +func updateDirectorySizes(ds directorySizes) directorySizes { + newDs := directorySizes{} + for k, v := range ds { + newDs[k] = v + } + for _, trash := range getAllTrashes() { + files, err := os.ReadDir(filepath.Join(trash, "files")) + if err != nil { + log.Error(err) + continue + } + for _, file := range files { + if _, ok := loadedDirSizes[file.Name()]; ok { + continue + } + + info, err := file.Info() + if err != nil { + log.Error(err) + continue + } + + if !info.IsDir() { + continue + } + + newDs[info.Name()] = directorySize{ + size: calculateDirSize(filepath.Join(trash, "files", info.Name())), + mtime: info.ModTime().Unix(), + name: info.Name(), + } + } + } + return newDs +} + +func writeDirectorySizes(dirSizes directorySizes) { + // TODO: make this less bad + for _, trash := range getAllTrashes() { + var lines []string + out := filepath.Join(trash, directorysizes) + files, err := os.ReadDir(filepath.Join(trash, "files")) + if err != nil { + log.Error(err) + continue + } + for _, file := range files { + if dirSize, ok := dirSizes[file.Name()]; ok { + lines = append(lines, fmt.Sprintf("%d %d ", dirSize.size, dirSize.mtime)+dirs.PercentEncode(file.Name())) + } + } + + err = os.WriteFile(out, []byte(strings.Join(lines, "\n")), noExecutePerm) + if err != nil { + log.Error(err) + } + } +} diff --git a/internal/files/disk.go b/internal/files/disk.go index 2b5e2d3..0b12b5b 100644 --- a/internal/files/disk.go +++ b/internal/files/disk.go @@ -26,12 +26,7 @@ func (f DiskFile) Path() string { return f.path } func (f DiskFile) Date() time.Time { return f.modified } func (f DiskFile) IsDir() bool { return f.isdir } func (f DiskFile) Mode() fs.FileMode { return f.mode } -func (f DiskFile) Filesize() int64 { - if f.isdir { - return 0 - } - return f.filesize -} +func (f DiskFile) Filesize() int64 { return f.filesize } func (f DiskFile) String() string { // this is unique enough because two files can't be named the same in the same directory @@ -53,14 +48,22 @@ func NewDisk(path string) (DiskFile, error) { name := filepath.Base(abs) basePath := filepath.Dir(abs) + actualPath := filepath.Join(basePath, name) + + var size int64 + if info.IsDir() { + size = calculateDirSize(actualPath) + } else { + size = info.Size() + } log.Debugf("%s (base:%s) (size:%s) (modified:%s) exists", - name, basePath, humanize.Bytes(uint64(info.Size())), info.ModTime()) + name, basePath, humanize.Bytes(uint64(size)), info.ModTime()) return DiskFile{ name: name, - path: filepath.Join(basePath, name), - filesize: info.Size(), + path: actualPath, + filesize: size, modified: info.ModTime(), isdir: info.IsDir(), mode: info.Mode(), @@ -130,10 +133,17 @@ func walkDir(dir string, fltr *filter.Filter) Files { name := dirEntry.Name() info, _ := dirEntry.Info() if fltr.Match(info) { + var size int64 + if info.IsDir() { + size = calculateDirSize(filepath.Join(actualPath, name)) + } else { + size = info.Size() + } + files = append(files, DiskFile{ path: actualPath, name: name, - filesize: info.Size(), + filesize: size, modified: info.ModTime(), isdir: info.IsDir(), mode: info.Mode(), @@ -167,13 +177,24 @@ func readDir(dir string, fltr *filter.Filter) Files { } path := filepath.Dir(filepath.Join(dir, name)) + actualPath, e := filepath.Abs(path) + if e != nil { + continue + } if fltr.Match(info) { + var size int64 + if info.IsDir() { + size = calculateDirSize(filepath.Join(actualPath, name)) + } else { + size = info.Size() + } + files = append(files, DiskFile{ name: name, - path: filepath.Join(path, name), + path: filepath.Join(actualPath, name), modified: info.ModTime(), - filesize: info.Size(), + filesize: size, isdir: info.IsDir(), mode: info.Mode(), }) diff --git a/internal/files/files.go b/internal/files/files.go index 16b96ac..4052140 100644 --- a/internal/files/files.go +++ b/internal/files/files.go @@ -5,10 +5,13 @@ import ( "cmp" "fmt" "io/fs" + "os" "path/filepath" "strconv" "strings" "time" + + "github.com/charmbracelet/log" ) type File interface { @@ -33,6 +36,23 @@ func (fls Files) String() string { return out.String() } +func (fls Files) TotalSize() int64 { + var size int64 + + for _, file := range fls { + if file.IsDir() { + if d, ok := loadedDirSizes[file.Name()]; ok { + log.Debugf("%s: got %d from directorysizes", file.Name(), d.size) + size += d.size + continue + } + } + size += file.Filesize() + } + + return size +} + func SortByModified(a, b File) int { if a.Date().Before(b.Date()) { return 1 @@ -52,15 +72,11 @@ func SortByModifiedReverse(a, b File) int { } func SortBySize(a, b File) int { - as := getSortingSize(a) - bs := getSortingSize(b) - return cmp.Compare(as, bs) + return cmp.Compare(a.Filesize(), b.Filesize()) } func SortBySizeReverse(a, b File) int { - as := getSortingSize(a) - bs := getSortingSize(b) - return cmp.Compare(bs, as) + return cmp.Compare(b.Filesize(), a.Filesize()) } func SortByName(a, b File) int { @@ -123,9 +139,36 @@ func doNameSort(a, b File) int { return cmp.Compare(aname, bname) } -func getSortingSize(f File) int64 { - if f.IsDir() { - return -1 +func calculateDirSize(path string) int64 { + var size int64 + info, err := os.Lstat(path) + if err != nil { + log.Error(err) + return 0 } - return f.Filesize() + if !info.IsDir() { + return 0 + } + + files, err := os.ReadDir(path) + if err != nil { + log.Error(err) + return 0 + } + + for _, file := range files { + filePath := filepath.Join(path, file.Name()) + info, err := os.Lstat(filePath) + if err != nil { + log.Error(err) + return 0 + } + if info.IsDir() { + size += calculateDirSize(filePath) + } else { + size += info.Size() + } + } + + return size } diff --git a/internal/files/trash.go b/internal/files/trash.go index 95f6373..a67d1d2 100644 --- a/internal/files/trash.go +++ b/internal/files/trash.go @@ -24,8 +24,8 @@ import ( ) const ( - sep = string(os.PathSeparator) executePerm = fs.FileMode(0755) + noExecutePerm = fs.FileMode(0644) noExecuteUserPerm = fs.FileMode(0600) randomStrLength int = 8 trashName string = ".Trash" @@ -40,6 +40,8 @@ DeletionDate={date} ` ) +var homeTrash = filepath.Join(xdg.DataHome, "Trash") + type TrashInfo struct { name, ogpath string path, trashinfo string @@ -56,12 +58,7 @@ func (t TrashInfo) TrashInfo() string { return t.trashinfo } func (t TrashInfo) Date() time.Time { return t.trashed } func (t TrashInfo) IsDir() bool { return t.isdir } func (t TrashInfo) Mode() fs.FileMode { return t.mode } -func (t TrashInfo) Filesize() int64 { - if t.isdir { - return 0 - } - return t.filesize -} +func (t TrashInfo) Filesize() int64 { return t.filesize } func (t TrashInfo) String() string { return t.name + t.path + t.ogpath + t.trashinfo @@ -70,12 +67,6 @@ func (t TrashInfo) String() string { func FindInAllTrashes(ogdir string, fltr *filter.Filter) Files { var files Files - personalTrash := filepath.Join(xdg.DataHome, "Trash") - - if fls, err := findTrash(personalTrash, ogdir, fltr); err == nil { - files = append(files, fls...) - } - for _, trash := range getAllTrashes() { fls, err := findTrash(trash, ogdir, fltr) if err != nil { @@ -142,17 +133,17 @@ func findTrash(trashdir, ogdir string, fltr *filter.Filter) (Files, error) { var files Files infodir := filepath.Join(trashdir, "info") - dirs, err := os.ReadDir(infodir) + entries, err := os.ReadDir(infodir) if err != nil { return nil, err } - for _, dir := range dirs { - if dir.IsDir() || filepath.Ext(dir.Name()) != trashInfoExt { + for _, entry := range entries { + if entry.IsDir() || filepath.Ext(entry.Name()) != trashInfoExt { continue } - path := filepath.Join(infodir, dir.Name()) + path := filepath.Join(infodir, entry.Name()) // trashinfo is just an ini file, so trashInfo, err := ini.Load(path) @@ -161,39 +152,57 @@ func findTrash(trashdir, ogdir string, fltr *filter.Filter) (Files, error) { continue } - if section := trashInfo.Section(trashInfoSec); section != nil { - basepath := section.Key(trashInfoPath).String() + section := trashInfo.Section(trashInfoSec) + if section == nil { + continue + } - filename := filepath.Base(basepath) - trashedpath := strings.Replace(strings.Replace(path, "info", "files", 1), trashInfoExt, "", 1) - info, err := os.Lstat(trashedpath) - if err != nil { - log.Errorf("error reading '%s': %s", trashedpath, err) - continue + basepath := dirs.PercentDecode(section.Key(trashInfoPath).String()) + if !strings.HasPrefix(basepath, string(os.PathSeparator)) { + root, err := getRoot(trashdir) + if err == nil { + basepath = filepath.Join(root, basepath) } + } - s := section.Key(trashInfoDate).Value() - date, err := time.ParseInLocation(trashInfoDateFmt, s, time.Local) - if err != nil { - log.Errorf("error parsing date '%s' in trashinfo file '%s': %s", s, path, err) - continue - } + filename := filepath.Base(basepath) + trashedpath := strings.Replace(strings.Replace(path, "info", "files", 1), trashInfoExt, "", 1) + info, err := os.Lstat(trashedpath) + if err != nil { + log.Errorf("error reading '%s': %s", trashedpath, err) + continue + } - if ogdir != "" && filepath.Dir(basepath) != ogdir { - continue - } + s := section.Key(trashInfoDate).Value() + date, err := time.ParseInLocation(trashInfoDateFmt, s, time.Local) + if err != nil { + log.Errorf("error parsing date '%s' in trashinfo file '%s': %s", s, path, err) + continue + } - if fltr.Match(info) { - files = append(files, TrashInfo{ - name: filename, - path: trashedpath, - ogpath: basepath, - trashinfo: path, - trashed: date, - isdir: info.IsDir(), - filesize: info.Size(), - }) - } + if ogdir != "" && filepath.Dir(basepath) != ogdir { + continue + } + + var size int64 + if d, ok := loadedDirSizes[info.Name()]; ok { + size = d.size + } else if info.IsDir() { + size = calculateDirSize(trashedpath) + } else { + size = info.Size() + } + + if fltr.Match(info) { + files = append(files, TrashInfo{ + name: filename, + path: trashedpath, + ogpath: basepath, + trashinfo: path, + trashed: date, + isdir: info.IsDir(), + filesize: size, + }) } } @@ -212,8 +221,21 @@ func trashFile(filename string) error { return err } + var path string + if trashDir == homeTrash { + path = filename + } else { + root, err := getRoot(trashDir) + if err != nil { + path = filename + } else { + path = strings.Replace(filename, root+string(os.PathSeparator), "", 1) + } + } + log.Debugf("fucking %s %s %s", filename, trashDir, path) + trashInfo, err := formatter.Format(trashInfoTemplate, formatter.Named{ - "path": filename, + "path": path, "date": time.Now().Format(trashInfoDateFmt), }) if err != nil { @@ -355,7 +377,7 @@ func getTrashDir(filename string) (string, error) { if strings.Contains(filename, xdg.Home) { trashDir = filepath.Join(xdg.DataHome, trashName[1:]) } else { - trashDir = filepath.Clean(root + sep + trashName) + trashDir = filepath.Clean(filepath.Join(root, trashName)) } if _, err := os.Lstat(trashDir); err != nil { @@ -406,12 +428,15 @@ func getRoot(path string) (string, error) { } func getAllTrashes() []string { - var trashes []string - usr, _ := user.Current() + trashes := []string{homeTrash} + usr, err := user.Current() + if err != nil { + log.Error(err) + } - _, err := mountinfo.GetMounts(func(mount *mountinfo.Info) (skip bool, stop bool) { + _, err = mountinfo.GetMounts(func(mount *mountinfo.Info) (skip bool, stop bool) { point := mount.Mountpoint - trashDir := filepath.Clean(point + sep + trashName) + trashDir := filepath.Clean(filepath.Join(point, trashName)) userTrashDir := trashDir + "-" + usr.Uid if _, err := os.Lstat(trashDir); err == nil { diff --git a/internal/interactive/interactive.go b/internal/interactive/interactive.go index 1cae836..1f59d45 100644 --- a/internal/interactive/interactive.go +++ b/internal/interactive/interactive.go @@ -73,6 +73,7 @@ type model struct { keys keyMap selected map[string]bool selectsize int64 + totalsize int64 readonly bool once bool filtering bool @@ -95,6 +96,7 @@ func newModel(fls files.Files, selectall, readonly, once bool, workdir string, m selected: map[string]bool{}, selectsize: 0, files: fls, + totalsize: fls.TotalSize(), } m.termwidth, m.termheight = termSizes() @@ -326,11 +328,13 @@ func (m model) header() string { styleKey(m.keys.clfl), styleKey(m.keys.apfl), } - dot = darkesttext.Render("•") - wideDot = darkesttext.Render(" • ") - keysFmt = strings.Join(keys, wideDot) - selectFmt = strings.Join(selectKeys, wideDot) - filterFmt = strings.Join(filterKeys, wideDot) + dot = darkesttext.Render("•") + wideDot = darkesttext.Render(" • ") + keysFmt = strings.Join(keys, wideDot) + selectFmt = strings.Join(selectKeys, wideDot) + filterFmt = strings.Join(filterKeys, wideDot) + totalSize = humanize.Bytes(uint64(m.totalsize)) + selectedSize = fmt.Sprintf("%s/%s", humanize.Bytes(uint64(m.selectsize)), totalSize) ) switch { @@ -338,24 +342,23 @@ func (m model) header() string { right = fmt.Sprintf(" Filtering %s %s", dot, filterFmt) case m.mode == modes.Interactive: right = fmt.Sprintf(" %s %s %s", keysFmt, dot, selectFmt) - left = fmt.Sprintf("%d/%d %s %s", len(m.selected), len(m.fltrfiles), dot, humanize.Bytes(uint64(m.selectsize))) + left = fmt.Sprintf("%d/%d %s %s", len(m.selected), len(m.fltrfiles), dot, selectedSize) case m.mode == modes.Listing: var filtered string if m.filter != "" || m.filtering { filtered = " (filtered)" } - right = fmt.Sprintf(" Showing%s %d files in trash", filtered, len(m.fltrfiles)) + right = fmt.Sprintf(" Showing%s %d files in trash (%s)", filtered, len(m.fltrfiles), totalSize) default: var wd string if m.workdir != "" { wd = " in " + dirs.UnExpand(m.workdir, "") } right = fmt.Sprintf(" %s%s %s %s", m.mode.String(), wd, dot, selectFmt) - left = fmt.Sprintf("%d/%d %s %s", len(m.selected), len(m.fltrfiles), dot, humanize.Bytes(uint64(m.selectsize))) + left = fmt.Sprintf("%d/%d %s %s", len(m.selected), len(m.fltrfiles), dot, selectedSize) } - // offset of 2 again because of table border - spacerWidth = m.termwidth - lipgloss.Width(right) - lipgloss.Width(left) - poffset + spacerWidth = (m.termwidth - lipgloss.Width(right) - lipgloss.Width(left)) + 1 if spacerWidth <= 0 { spacerWidth = 1 // always at least one space } @@ -662,14 +665,14 @@ func Show(fls files.Files, once bool, workdir string) error { func newRow(file files.File, workdir string) table.Row { var time, size string + name := file.Name() time = humanize.Time(file.Date()) if file.IsDir() { - size = bar - } else { - size = humanize.Bytes(uint64(file.Filesize())) + name += string(os.PathSeparator) } + size = humanize.Bytes(uint64(file.Filesize())) return table.Row{ - dirs.PercentDecode(file.Name()), + dirs.PercentDecode(name), dirs.UnExpand(filepath.Dir(file.Path()), workdir), time, size, diff --git a/main.go b/main.go index 3556198..241eb98 100644 --- a/main.go +++ b/main.go @@ -195,6 +195,7 @@ var ( } after = func(_ *cli.Context) error { + files.WriteDirectorySizes() return nil }