-
-
Notifications
You must be signed in to change notification settings - Fork 17
Expand file tree
/
Copy pathsub.rs
More file actions
330 lines (298 loc) · 11.2 KB
/
sub.rs
File metadata and controls
330 lines (298 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
use crate::{
args::{Depth, Fraction},
data_tree::DataTree,
fs_tree_builder::FsTreeBuilder,
get_size::GetSize,
hardlink::{DeduplicateSharedSize, HardlinkIgnorant, RecordHardlinks},
json_data::{BinaryVersion, JsonData, JsonDataBody, JsonShared, JsonTree, SchemaVersion},
ls_colors::LsColors,
os_string_display::OsStringDisplay,
reporter::ParallelReporter,
runtime_error::RuntimeError,
size,
status_board::GLOBAL_STATUS_BOARD,
visualizer::{BarAlignment, Color, Coloring, ColumnWidthDistribution, Direction, Visualizer},
};
use pipe_trait::Pipe;
use serde::Serialize;
use std::{
collections::HashMap,
io::stdout,
iter::once,
path::{Path, PathBuf},
};
/// The sub program of the main application.
pub struct Sub<Size, SizeGetter, HardlinksHandler, Report>
where
Report: ParallelReporter<Size> + Sync,
Size: size::Size + Into<u64> + Serialize + Send + Sync,
SizeGetter: GetSize<Size = Size> + Copy + Sync,
HardlinksHandler: RecordHardlinks<Size, Report> + HardlinkSubroutines<Size> + Sync,
JsonTree<Size>: Into<JsonDataBody>,
{
/// List of files and/or directories.
pub files: Vec<PathBuf>,
/// Print JSON data instead of an ASCII chart.
pub json_output: Option<JsonOutputParam>,
/// Format to be used to [`display`](size::Size::display) the sizes returned by [`size_getter`](Self::size_getter).
pub bytes_format: Size::DisplayFormat,
/// The direction of the visualization.
pub direction: Direction,
/// The alignment of the bars.
pub bar_alignment: BarAlignment,
/// Distribution and number of characters/blocks can be placed in a line.
pub column_width_distribution: ColumnWidthDistribution,
/// Maximum number of levels that should be visualized.
pub max_depth: Depth,
/// [Get the size](GetSize) of files/directories.
pub size_getter: SizeGetter,
/// Handle to detect, record, and deduplicate hardlinks.
pub hardlinks_handler: HardlinksHandler,
/// Reports measurement progress.
pub reporter: Report,
/// Minimal size proportion required to appear.
pub min_ratio: Fraction,
/// Preserve order of entries.
pub no_sort: bool,
/// Whether to color the output.
pub color: Option<LsColors>,
}
impl<Size, SizeGetter, HardlinksHandler, Report> Sub<Size, SizeGetter, HardlinksHandler, Report>
where
Size: size::Size + Into<u64> + Serialize + Send + Sync,
Report: ParallelReporter<Size> + Sync,
SizeGetter: GetSize<Size = Size> + Copy + Sync,
HardlinksHandler: RecordHardlinks<Size, Report> + HardlinkSubroutines<Size> + Sync,
JsonTree<Size>: Into<JsonDataBody>,
{
/// Run the sub program.
pub fn run(self) -> Result<(), RuntimeError> {
let Sub {
files,
json_output,
bytes_format,
direction,
bar_alignment,
column_width_distribution,
max_depth,
size_getter,
hardlinks_handler,
reporter,
min_ratio,
no_sort,
color,
} = self;
let max_depth = max_depth.get();
let mut iter = files
.into_iter()
.map(|root| -> DataTree<OsStringDisplay, Size> {
FsTreeBuilder {
reporter: &reporter,
root,
size_getter,
hardlinks_recorder: &hardlinks_handler,
max_depth,
}
.into()
});
let data_tree = if let Some(data_tree) = iter.next() {
data_tree
} else {
return Sub {
files: vec![".".into()],
hardlinks_handler,
reporter,
color,
..self
}
.run();
};
let only_one_arg = iter.len() == 0; // ExactSizeIterator::is_empty is unstable
let data_tree = if only_one_arg {
data_tree
} else {
let children: Vec<_> = once(data_tree).chain(iter).collect();
// This name is for hardlinks deduplication to work correctly as empty string is considered to be the start of any path.
// It would be changed into "(total)" later.
let fake_root_name = OsStringDisplay::os_string_from("");
DataTree::dir(fake_root_name, Size::default(), children)
.into_par_retained(|_, depth| depth + 1 < max_depth)
};
if reporter.destroy().is_err() {
eprintln!("[warning] Failed to destroy the thread that reports progress");
}
let min_ratio: f32 = min_ratio.into();
let (data_tree, deduplication_record) = {
let mut data_tree = data_tree;
if min_ratio > 0.0 {
data_tree.par_cull_insignificant_data(min_ratio);
}
if !no_sort {
data_tree.par_sort_by(|left, right| left.size().cmp(&right.size()).reverse());
}
let deduplication_record = hardlinks_handler.deduplicate(&mut data_tree);
if !only_one_arg {
assert_eq!(data_tree.name().as_os_str().to_str(), Some(""));
*data_tree.name_mut() = OsStringDisplay::os_string_from("(total)");
}
(data_tree, deduplication_record)
};
GLOBAL_STATUS_BOARD.clear_line(0);
if let Some(json_output) = json_output {
let JsonOutputParam {
shared_details,
shared_summary,
} = json_output;
let tree = data_tree
.into_reflection() // I really want to use std::mem::transmute here but can't.
.par_convert_names_to_utf8() // TODO: allow non-UTF8 somehow.
.expect("convert all names from raw string to UTF-8");
let deduplication_result = if !shared_details && !shared_summary {
Ok(JsonShared::default())
} else {
// `try` expression would be extremely useful right now but it sadly requires nightly
|| -> Result<_, RuntimeError> {
let mut shared = deduplication_record
.map_err(HardlinksHandler::convert_error)?
.pipe(HardlinksHandler::json_report)?
.unwrap_or_default();
if !shared_details {
shared.details = None;
}
if !shared_summary {
shared.summary = None;
}
Ok(shared)
}()
};
// errors caused by failing deduplication shouldn't prevent the JSON data from being printed
let (shared, deduplication_result) = match deduplication_result {
Ok(shared) => (shared, Ok(())),
Err(error) => (JsonShared::default(), Err(error)),
};
let json_tree = JsonTree { tree, shared };
let json_data = JsonData {
schema_version: SchemaVersion,
binary_version: Some(BinaryVersion::current()),
body: json_tree.into(),
};
return serde_json::to_writer(stdout(), &json_data)
.map_err(RuntimeError::SerializationFailure)
.or(deduplication_result);
}
let coloring: Option<Coloring> = color.map(|ls_colors| {
let mut map = HashMap::new();
build_coloring_map(&data_tree, PathBuf::new(), &mut map);
Coloring::new(ls_colors, map)
});
let visualizer = Visualizer {
data_tree: &data_tree,
bytes_format,
direction,
bar_alignment,
column_width_distribution,
coloring: coloring.as_ref(),
};
print!("{visualizer}"); // visualizer already ends with "\n", println! isn't needed here.
let deduplication_record = deduplication_record.map_err(HardlinksHandler::convert_error)?;
HardlinksHandler::print_report(deduplication_record, bytes_format)?;
Ok(())
}
}
/// Value to pass to [`Sub::json_output`] to decide how much details should be
/// put in the output JSON object.
#[derive(Debug, Clone, Copy)]
pub struct JsonOutputParam {
/// Whether to include `.shared.details` in the JSON output.
pub shared_details: bool,
/// Whether to include `.shared.summary` in the JSON output.
pub shared_summary: bool,
}
impl JsonOutputParam {
/// Infer from the CLI flags.
pub(super) fn from_cli_flags(
output_json: bool,
omit_shared_details: bool,
omit_shared_summary: bool,
) -> Option<Self> {
output_json.then_some(JsonOutputParam {
shared_details: !omit_shared_details,
shared_summary: !omit_shared_summary,
})
}
}
/// Subroutines used by [`Sub`] to deduplicate sizes of detected hardlinks and report about it.
pub trait HardlinkSubroutines<Size: size::Size>: DeduplicateSharedSize<Size> {
/// Convert the error to runtime error.
fn convert_error(error: Self::Error) -> RuntimeError;
/// Handle the report.
fn print_report(
report: Self::Report,
bytes_format: Size::DisplayFormat,
) -> Result<(), RuntimeError>;
/// Create a JSON serializable object from the report.
fn json_report(report: Self::Report) -> Result<Option<JsonShared<Size>>, RuntimeError>;
}
impl<Size> HardlinkSubroutines<Size> for HardlinkIgnorant
where
DataTree<OsStringDisplay, Size>: Send,
Size: size::Size + Sync,
{
#[inline]
fn convert_error(error: Self::Error) -> RuntimeError {
match error {}
}
#[inline]
fn print_report((): Self::Report, _: Size::DisplayFormat) -> Result<(), RuntimeError> {
Ok(())
}
#[inline]
fn json_report((): Self::Report) -> Result<Option<JsonShared<Size>>, RuntimeError> {
Ok(None)
}
}
/// Recursively walk a pruned [`DataTree`] and build a map of full paths to [`Color`] values.
///
/// The `path` argument should be the current path prefix for reconstructing full filesystem paths.
/// Leaf nodes (files or childless directories after pruning) are added to the map.
/// Nodes with children are skipped because the [`Visualizer`] uses the children count to
/// determine their color at render time.
fn build_coloring_map(
node: &DataTree<OsStringDisplay, impl size::Size>,
path: PathBuf,
map: &mut HashMap<PathBuf, Color>,
) {
let node_path = path.join(node.name().as_os_str());
if !node.children().is_empty() {
for child in node.children() {
build_coloring_map(child, node_path.clone(), map);
}
return;
}
let color = file_color(&node_path);
map.insert(node_path, color);
}
fn file_color(path: &Path) -> Color {
if path.is_symlink() {
Color::Symlink
} else if path.is_dir() {
Color::Directory
} else if is_executable(path) {
Color::Executable
} else {
Color::Normal
}
}
#[cfg(unix)]
fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
path.metadata()
.map(|stats| stats.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(_path: &Path) -> bool {
false
}
#[cfg(unix)]
mod unix_ext;