Сканирование директории
Author
module DirTreeMap
using PlotlyJS
using Printf
export scan_directory, build_treemap
"""
scan_directory(root; max_depth=6, min_size=0, follow_symlinks=false)
Рекурсивно обходит `root` и возвращает плоские векторы
`labels`, `parents`, `values`, `ids`, `texts`, готовые для Plotly.treemap.
- `root` — корневая папка.
- `max_depth` — максимальная глубина обхода (0 — только корень).
- `min_size` — минимальный размер файла/папки в байтах;
элементы меньше порога отбрасываются (кроме корня).
- `follow_symlinks` — следовать ли по симлинкам.
"""
function scan_directory(root::AbstractString;
max_depth::Int = 6,
min_size::Integer = 0,
follow_symlinks::Bool = false)
isdir(root) || error("Не директория: $root")
labels = String[]
parents = String[]
values = Int[]
ids = String[]
texts = String[]
# Уникальный идентификатор каждого узла — полный путь.
# В labels кладём короткое имя, в ids — полный путь,
# parents ссылается на ids родителя (Plotly допускает это).
root_id = abspath(root)
push!(labels, basename(root_id) == "" ? root_id : basename(root_id))
push!(parents, "")
push!(values, 0) # заполним позже
push!(ids, root_id)
push!(texts, root_id)
# Обход в ширину с ограничением глубины
queue = Tuple{String, Int}[(root_id, 0)] # (путь, глубина)
while !isempty(queue)
dir, depth = popfirst!(queue)
depth >= max_depth && continue
entries = try
readdir(dir; join = true)
catch
continue
end
for path in entries
name = basename(path)
startswith(name, ".") && continue # скрытые файлы/папки
if islink(path) && !follow_symlinks
continue
end
if isdir(path)
size = directory_size(path; follow_symlinks = follow_symlinks)
size < min_size && continue
push!(labels, name)
push!(parents, dir)
push!(values, size)
push!(ids, abspath(path))
push!(texts, abspath(path))
push!(queue, (abspath(path), depth + 1))
elseif isfile(path)
size = filesize(path)
size < min_size && continue
push!(labels, name)
push!(parents, dir)
push!(values, size)
push!(ids, abspath(path))
push!(texts, "$(abspath(path))\n$(human_size(size))")
end
end
end
# Значение корня = сумма его прямых детей.
values[1] = sum(values[i] for i in eachindex(parents) if parents[i] == root_id)
return (labels = labels, parents = parents, values = values,
ids = ids, texts = texts, root = root_id)
end
"""
directory_size(path; follow_symlinks=false)
Суммарный размер всех файлов внутри `path` (рекурсивно).
Симлинки по умолчанию не разворачиваются, чтобы избежать циклов.
"""
function directory_size(path::AbstractString; follow_symlinks::Bool = false)
total = 0
stack = String[path]
visited = Set{String}()
while !isempty(stack)
dir = pop!(stack)
dir in visited && continue
push!(visited, dir)
entries = try
readdir(dir; join = true)
catch
continue
end
for p in entries
if islink(p) && !follow_symlinks
continue
elseif isdir(p)
push!(stack, p)
elseif isfile(p)
total += try filesize(p) catch; 0 end
end
end
end
return total
end
"""
human_size(bytes)
Человекочитаемый размер: 1.2 KB, 3.4 MB, ...
"""
function human_size(bytes::Integer)
units = ("B", "KB", "MB", "GB", "TB", "PB")
s = float(bytes)
i = 1
while s >= 1024 && i < length(units)
s /= 1024
i += 1
end
return @sprintf("%.1f %s", s, units[i])
end
const ALLOWED_COLORSCALES = Set([
"Viridis", "Cividis", "Inferno", "Magma", "Plasma",
"Blues", "Greens", "Greys", "Reds", "Purples", "Oranges",
"YlGnBu", "YlOrRd", "BuGn", "PuBu", "RdBu", "RdYlBu", "RdYlGn",
"PiYG", "PRGn", "BrBG", "PuOr", "Twilight", "HSV", "IceFire",
"Jet", "Rainbow", "Hot", "Earth", "Electric",
])
"""
build_treemap(data; title="")
Принимает результат `scan_directory` и возвращает готовый PlotlyJS.Plot.
"""
function build_treemap(data;
title::AbstractString = "",
colorscale::AbstractString = "Viridis")
base = endswith(colorscale, "_r") ? colorscale[1:end-2] : colorscale
base in ALLOWED_COLORSCALES ||
error("Неизвестная палитра: $colorscale. Доступные: " *
join(sort(collect(ALLOWED_COLORSCALES)), ", "))
# Подписи на блоках: имя + размер.
# Значения values уже в байтах — Plotly сам нормирует их в доли.
textinfo = "label+text+percent parent"
plot(
treemap(
labels = data.labels,
parents = data.parents,
values = data.values,
ids = data.ids,
text = [human_size(v) for v in data.values],
textinfo = textinfo,
branchvalues = "total",
hovertemplate = "%{label}<br>%{text}<extra></extra>",
marker = attr(
colorscale = colorscale, # <- принимаем из аргумента
line = attr(width = 0.5, color = "white"),
),
),
Layout(
title = title == "" ? "Disk usage: $(data.root)" : title,
margin = attr(l = 10, r = 10, t = 40, b = 10),
height = 700,
),
)
end
end # module