///|
pub(all) struct DirectoryStat {
path : String
direct_size : Int64
recursive_size : Int64
direct_files : Int
recursive_files : Int
} derive(Eq, Debug)
///|
pub(all) struct CategoryStat {
name : String
size : Int64
files : Int
} derive(Eq, Debug)
///|
pub(all) struct AgeBuckets {
day : CategoryStat
week : CategoryStat
month : CategoryStat
quarter : CategoryStat
year : CategoryStat
older : CategoryStat
} derive(Eq, Debug)
///|
fn map_add_size(map : Map[String, Int64], key : String, amount : Int64) -> Unit {
map[key] = map.get(key).unwrap_or(0L) + amount
}
///|
fn map_add_count(map : Map[String, Int], key : String, amount : Int) -> Unit {
map[key] = map.get(key).unwrap_or(0) + amount
}
///|
fn ancestor_paths(path : String) -> Array[String] {
let result = [""]
let normalized = normalize_relative_path(path)
if normalized == "" {
return result
}
let parts = normalized.split("/").to_array()
let mut current = ""
for part in parts {
current = if current == "" {
part.to_owned()
} else {
current + "/" + part.to_owned()
}
result.push(current)
}
result
}
///|
pub fn directory_stats(snapshot : Snapshot) -> Array[DirectoryStat] {
let direct_sizes : Map[String, Int64] = Map([])
let recursive_sizes : Map[String, Int64] = Map([])
let direct_counts : Map[String, Int] = Map([])
let recursive_counts : Map[String, Int] = Map([])
let directories : Map[String, Bool] = Map([("", true)])
for entry in snapshot.entries {
if entry.is_dir() {
directories[entry.path] = true
} else if entry.is_file() {
let parent = entry.parent()
directories[parent] = true
map_add_size(direct_sizes, parent, entry.size)
map_add_count(direct_counts, parent, 1)
for ancestor in ancestor_paths(parent) {
directories[ancestor] = true
map_add_size(recursive_sizes, ancestor, entry.size)
map_add_count(recursive_counts, ancestor, 1)
}
}
}
let result : Array[DirectoryStat] = []
for path, _ in directories {
result.push(DirectoryStat::{
path,
direct_size: direct_sizes.get(path).unwrap_or(0L),
recursive_size: recursive_sizes.get(path).unwrap_or(0L),
direct_files: direct_counts.get(path).unwrap_or(0),
recursive_files: recursive_counts.get(path).unwrap_or(0),
})
}
result.sort_by(fn(a, b) {
if a.path < b.path {
-1
} else if a.path > b.path {
1
} else {
0
}
})
result
}
///|
pub fn stat_for_directory(snapshot : Snapshot, path : String) -> DirectoryStat? {
let wanted = normalize_relative_path(path)
for stat in directory_stats(snapshot) {
if stat.path == wanted {
return Some(stat)
}
}
None
}
///|
pub fn largest_files(snapshot : Snapshot, limit : Int) -> Array[Entry] {
let result : Array[Entry] = []
for entry in snapshot.entries {
if entry.is_file() {
result.push(entry)
}
}
result.sort_by(fn(a, b) {
if a.size > b.size {
-1
} else if a.size < b.size {
1
} else if a.path < b.path {
-1
} else if a.path > b.path {
1
} else {
0
}
})
if limit < result.length() {
result[:limit].to_owned()
} else {
result
}
}
///|
pub fn largest_directories(
snapshot : Snapshot,
limit : Int,
) -> Array[DirectoryStat] {
let result = directory_stats(snapshot)
result.sort_by(fn(a, b) {
if a.recursive_size > b.recursive_size {
-1
} else if a.recursive_size < b.recursive_size {
1
} else if a.path < b.path {
-1
} else if a.path > b.path {
1
} else {
0
}
})
if limit < result.length() {
result[:limit].to_owned()
} else {
result
}
}
///|
pub fn extension_stats(snapshot : Snapshot) -> Array[CategoryStat] {
let sizes : Map[String, Int64] = Map([])
let counts : Map[String, Int] = Map([])
for entry in snapshot.entries {
if entry.is_file() {
let ext = entry.extension()
let category = if ext == "" { "(no extension)" } else { "." + ext }
map_add_size(sizes, category, entry.size)
map_add_count(counts, category, 1)
}
}
let result : Array[CategoryStat] = []
for name, size in sizes {
result.push(CategoryStat::{ name, size, files: counts[name], })
}
result.sort_by(fn(a, b) {
if a.size > b.size {
-1
} else if a.size < b.size {
1
} else if a.name < b.name {
-1
} else {
1
}
})
result
}
///|
fn empty_category(name : String) -> CategoryStat {
CategoryStat::{ name, size: 0L, files: 0, }
}
///|
fn category_with_entry(category : CategoryStat, entry : Entry) -> CategoryStat {
{ ..category, size: category.size + entry.size, files: category.files + 1, }
}
///|
pub fn age_buckets(snapshot : Snapshot, now : Int64) -> AgeBuckets {
let mut day = empty_category("less than 1 day")
let mut week = empty_category("1 to 7 days")
let mut month = empty_category("7 to 30 days")
let mut quarter = empty_category("30 to 90 days")
let mut year = empty_category("90 to 365 days")
let mut older = empty_category("older than 365 days")
for entry in snapshot.entries {
if !entry.is_file() {
continue
}
let age = if now > entry.modified_at { now - entry.modified_at } else { 0L }
if age < 86400L {
day = category_with_entry(day, entry)
} else if age < 604800L {
week = category_with_entry(week, entry)
} else if age < 2592000L {
month = category_with_entry(month, entry)
} else if age < 7776000L {
quarter = category_with_entry(quarter, entry)
} else if age < 31536000L {
year = category_with_entry(year, entry)
} else {
older = category_with_entry(older, entry)
}
}
AgeBuckets::{ day, week, month, quarter, year, older, }
}
///|
pub fn empty_directories(snapshot : Snapshot) -> Array[Entry] {
let stats = directory_stats(snapshot)
let result : Array[Entry] = []
for entry in snapshot.entries {
if entry.is_dir() {
for stat in stats {
if stat.path == entry.path && stat.recursive_files == 0 {
result.push(entry)
break
}
}
}
}
result.sort_by(fn(a, b) { if a.path < b.path { -1 } else { 1 } })
result
}