///|
pub(all) enum InsightSeverity {
Info
Notice
Warning
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) enum InsightKind {
ScanIncomplete
VeryLargeFile
DominantDirectory
StaleStorage
RegenerableOutput
EmptyDirectories
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct StorageInsight {
kind : InsightKind
severity : InsightSeverity
title : String
detail : String
path : String
size : Int64
} derive(Eq, Debug, ToJson, FromJson)
///|
pub(all) struct InsightOptions {
large_file_bytes : Int64
large_file_share_tenths : Int
dominant_directory_share_tenths : Int
stale_bytes : Int64
max_large_file_insights : Int
} derive(Eq, Debug, ToJson, FromJson)
///|
pub fn InsightOptions::default() -> InsightOptions {
InsightOptions::{
large_file_bytes: 100L * 1024L * 1024L,
large_file_share_tenths: 250,
dominant_directory_share_tenths: 600,
stale_bytes: 500L * 1024L * 1024L,
max_large_file_insights: 5,
}
}
///|
fn share_tenths(part : Int64, total : Int64) -> Int {
if total <= 0L {
0
} else {
(part * 1000L / total).to_int()
}
}
///|
fn severity_name(severity : InsightSeverity) -> String {
match severity {
Info => "info"
Notice => "notice"
Warning => "warning"
}
}
///|
fn large_file_insights(
snapshot : Snapshot,
options : InsightOptions,
) -> Array[StorageInsight] {
let result : Array[StorageInsight] = []
let total = snapshot.total_size()
for entry in largest_files(snapshot, options.max_large_file_insights) {
let share = share_tenths(entry.size, total)
if entry.size >= options.large_file_bytes ||
share >= options.large_file_share_tenths {
result.push(StorageInsight::{
kind: VeryLargeFile,
severity: if share >= 500 {
Warning
} else {
Notice
},
title: "Large individual file",
detail: entry.path +
" uses " +
format_bytes(entry.size) +
" (" +
format_percent(entry.size, total) +
") of the scanned data.",
path: entry.path,
size: entry.size,
})
}
}
result
}
///|
fn dominant_directory_insights(
snapshot : Snapshot,
options : InsightOptions,
) -> Array[StorageInsight] {
let result : Array[StorageInsight] = []
let total = snapshot.total_size()
for stat in directory_stats(snapshot) {
if stat.path != "" &&
path_depth(stat.path) == 1 &&
share_tenths(stat.recursive_size, total) >=
options.dominant_directory_share_tenths {
result.push(StorageInsight::{
kind: DominantDirectory,
severity: Notice,
title: "Directory dominates this scan",
detail: stat.path +
" accounts for " +
format_percent(stat.recursive_size, total) +
" of all file bytes.",
path: stat.path,
size: stat.recursive_size,
})
}
}
result
}
///|
fn regenerable_insights(
snapshot : Snapshot,
now : Int64,
) -> Array[StorageInsight] {
let result : Array[StorageInsight] = []
let matches = evaluate_rules(builtin_cleanup_rules(), snapshot, now)
for candidate in matches {
if candidate.regenerable {
result.push(StorageInsight::{
kind: RegenerableOutput,
severity: Info,
title: candidate.rule_title,
detail: candidate.reason,
path: candidate.path,
size: candidate.size,
})
}
}
result
}
///|
pub fn storage_insights(
snapshot : Snapshot,
now : Int64,
options? : InsightOptions = InsightOptions::default(),
) -> Array[StorageInsight] {
let result : Array[StorageInsight] = []
if !snapshot.errors.is_empty() {
result.push(StorageInsight::{
kind: ScanIncomplete,
severity: Warning,
title: "Scan completed with metadata errors",
detail: snapshot.errors.length().to_string() +
" paths could not be inspected, so totals may be incomplete.",
path: "",
size: 0L,
})
}
result.append(large_file_insights(snapshot, options))
result.append(dominant_directory_insights(snapshot, options))
let ages = age_buckets(snapshot, now)
if ages.older.size >= options.stale_bytes {
result.push(StorageInsight::{
kind: StaleStorage,
severity: Notice,
title: "Substantial storage is older than one year",
detail: format_bytes(ages.older.size) +
" across " +
ages.older.files.to_string() +
" files has not been modified for at least 365 days.",
path: "",
size: ages.older.size,
})
}
result.append(regenerable_insights(snapshot, now))
let empty = empty_directories(snapshot)
if !empty.is_empty() {
result.push(StorageInsight::{
kind: EmptyDirectories,
severity: Info,
title: "Empty directories found",
detail: empty.length().to_string() +
" directories contain no regular files.",
path: empty[0].path,
size: 0L,
})
}
result.sort_by(fn(a, b) {
let a_rank = match a.severity {
Warning => 0
Notice => 1
Info => 2
}
let b_rank = match b.severity {
Warning => 0
Notice => 1
Info => 2
}
if a_rank < b_rank {
-1
} else if a_rank > b_rank {
1
} else if a.size > b.size {
-1
} else if a.size < b.size {
1
} else if a.path < b.path {
-1
} else {
1
}
})
result
}
///|
pub fn render_insights(insights : Array[StorageInsight]) -> String {
let lines : Array[String] = ["MoonDisk storage insights"]
if insights.is_empty() {
lines.push("No notable storage patterns were detected.")
} else {
for insight in insights {
let location = if insight.path == "" {
""
} else {
" [" + insight.path + "]"
}
lines.push(
"[" + severity_name(insight.severity) + "] " + insight.title + location,
)
lines.push(" " + insight.detail)
}
}
lines.join("\n")
}