blob: 477c9612b0863ff920f4a58cdea1380d38bf9010 [file] [log] [blame]
package du
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/fs"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/dustin/go-humanize"
"go.skia.org/infra/go/skerr"
)
// File represents a single file and its size in bytes.
type File struct {
Name string
Size uint64
}
// Dir represents a directory and all of its files and subdirectories, along
// with their total counts and sizes.
type Dir struct {
Name string
Dirs []*Dir
Files []*File
TotalSize uint64
TotalFiles uint64
}
// Usage produces a report of disk usage within the given directory.
func Usage(ctx context.Context, rootPath string) (*Dir, error) {
if err := ctx.Err(); err != nil {
return nil, skerr.Wrap(err)
}
// Find all of the files and directories.
var root *Dir
dirsByPath := map[string]*Dir{}
err := filepath.Walk(rootPath, func(path string, info fs.FileInfo, err error) error {
if err != nil {
// Skip directories we don't have permission to read.
if errors.Is(skerr.Unwrap(err), fs.ErrPermission) {
return nil
}
return skerr.Wrap(err)
}
if err := ctx.Err(); err != nil {
return skerr.Wrap(err)
}
if info.IsDir() {
dirEntry := &Dir{
Dirs: []*Dir{},
Files: []*File{},
TotalSize: uint64(info.Size()),
}
dirsByPath[path] = dirEntry
if path == rootPath || path == "" {
dirEntry.Name = rootPath
root = dirEntry
} else {
parentPath, base := filepath.Split(path)
if parentPath == "" {
parentPath = rootPath
}
parentPath = strings.TrimRight(parentPath, "/")
dirEntry.Name = base
parent, ok := dirsByPath[parentPath]
if !ok {
return skerr.Fmt("no directory entry found for %q", parentPath)
}
parent.Dirs = append(parent.Dirs, dirEntry)
}
return nil
}
parentPath, base := filepath.Split(path)
if parentPath == "" {
parentPath = rootPath
}
parentPath = strings.TrimRight(parentPath, "/")
parent, ok := dirsByPath[parentPath]
if !ok {
return skerr.Fmt("no directory entry found for %q", parentPath)
}
parent.Files = append(parent.Files, &File{
Name: base,
Size: uint64(info.Size()),
})
return nil
})
if err != nil {
return nil, skerr.Wrap(err)
}
// Produce summaries for total number of files and bytes for each directory
// and sort files and dirs by size for cleaner reporting.
var genSummaries func(dir *Dir)
genSummaries = func(dir *Dir) {
for _, subdir := range dir.Dirs {
genSummaries(subdir)
dir.TotalFiles += subdir.TotalFiles
dir.TotalSize += subdir.TotalSize
}
for _, file := range dir.Files {
dir.TotalFiles++
dir.TotalSize += file.Size
}
sort.Sort(DirSlice(dir.Dirs))
sort.Sort(FileSlice(dir.Files))
}
genSummaries(root)
return root, nil
}
// GenerateReport generates a textual report of the disk usage in the given
// directory. maxDepth controls how many directory levels are displayed;
// if zero, there is no maximum depth. human causes the report to use human-
// readable units instead of raw bytes.
func GenerateReport(ctx context.Context, rootDir *Dir, rootPath string, maxDepth int, human bool) (string, error) {
var buf strings.Builder
var print func(dir *Dir, parentPath string, depth int) error
print = func(dir *Dir, parentPath string, depth int) error {
dirPath := filepath.Join(parentPath, dir.Name)
if depth < maxDepth || maxDepth == 0 {
for _, subdir := range dir.Dirs {
if err := print(subdir, dirPath, depth+1); err != nil {
return err
}
}
}
var err error
if human {
_, err = fmt.Fprintf(&buf, "%s\t%s\n", humanize.Bytes(uint64(dir.TotalSize)), dirPath)
} else {
_, err = fmt.Fprintf(&buf, "%d\t%s\n", dir.TotalSize, dirPath)
}
if err != nil {
return err
}
return nil
}
if err := print(rootDir, rootPath, 0); err != nil {
return "", skerr.Wrap(err)
}
return strings.TrimRight(buf.String(), "\n"), nil
}
// PrintReport generates and prints a textual report of the disk usage in the
// given directory. maxDepth controls how many directory levels are displayed;
// if zero, there is no maximum depth. human causes the report to use human-
// readable units instead of raw bytes.
func PrintReport(ctx context.Context, rootPath string, maxDepth int, human bool) error {
rootDir, err := Usage(ctx, rootPath)
if err != nil {
return skerr.Wrap(err)
}
report, err := GenerateReport(ctx, rootDir, rootPath, maxDepth, human)
if err != nil {
return skerr.Wrap(err)
}
fmt.Println(report)
return nil
}
// GenerateJSONReport generates and prints a JSON report of the disk usage in
// the given directory. maxDepth controls how many directory levels are
// displayed; if zero, there is no maximum depth. human causes the report to use
// human-readable units instead of raw bytes.
func GenerateJSONReport(ctx context.Context, rootDir *Dir, rootPath string, maxDepth int, human bool) (string, error) {
type node struct {
Name string `json:"name"`
Dirs []*node `json:"dirs,omitempty"`
Size string `json:"size"`
}
var mkTree func(dir *Dir, parentPath string, depth int) *node
mkTree = func(dir *Dir, parentPath string, depth int) *node {
dirPath := filepath.Join(parentPath, dir.Name)
n := &node{
Name: dirPath,
}
if depth < maxDepth || maxDepth == 0 {
for _, subdir := range dir.Dirs {
n.Dirs = append(n.Dirs, mkTree(subdir, dirPath, depth+1))
}
}
if human {
n.Size = humanize.Bytes(uint64(dir.TotalSize))
} else {
n.Size = strconv.FormatUint(dir.TotalSize, 10)
}
return n
}
tree := mkTree(rootDir, rootPath, 0)
b, err := json.Marshal(tree)
if err != nil {
return "", skerr.Wrap(err)
}
return string(b), nil
}
// PrintJSONReport generates and prints a JSON report of the disk usage in the
// given directory. maxDepth controls how many directory levels are displayed;
// if zero, there is no maximum depth. human causes the report to use human-
// readable units instead of raw bytes.
func PrintJSONReport(ctx context.Context, rootPath string, maxDepth int, human bool) error {
rootDir, err := Usage(ctx, rootPath)
if err != nil {
return skerr.Wrap(err)
}
report, err := GenerateJSONReport(ctx, rootDir, rootPath, maxDepth, human)
if err != nil {
return skerr.Wrap(err)
}
fmt.Println(report)
return nil
}
// DirSlice is used for sorting.
type DirSlice []*Dir
// Len implements sort.Interface.
func (s DirSlice) Len() int {
return len(s)
}
// Less implements sort.Interface.
func (s DirSlice) Less(a, b int) bool {
return s[a].TotalSize < s[b].TotalSize
}
// Swap implements sort.Interface.
func (s DirSlice) Swap(a, b int) {
s[a], s[b] = s[b], s[a]
}
// FileSlice is used for sorting.
type FileSlice []*File
// Len implements sort.Interface.
func (s FileSlice) Len() int {
return len(s)
}
// Less implements sort.Interface.
func (s FileSlice) Less(a, b int) bool {
return s[a].Size < s[b].Size
}
// Swap implements sort.Interface.
func (s FileSlice) Swap(a, b int) {
s[a], s[b] = s[b], s[a]
}