about summary refs log tree commit diff
path: root/src/main.rs
blob: 0dc989926e148d9b03f0b375d0e4116da3e110d5 (plain)
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
use std::{
	fs::Metadata,
	time::{Duration, SystemTime},
};

use camino::{Utf8Path, Utf8PathBuf};

fn main() {
	// currently only accepts one argument, the path to the directory it will
	// recurse through and make the csv for.
	// spits the csv on stdout

	let path: Utf8PathBuf = std::env::args().nth(1).unwrap().parse().unwrap();
	process(&path, &path)
}

/// Loop through the provided directory printing CSV rows. The `root_path` is
/// used to make printed paths relative to it.
///
/// Does two passes: First pass prints files. Second pass recurs, printing directories.
fn process(root_path: &Utf8Path, path: &Utf8Path) {
	//TODO: do not panic, please
	let this_meta = std::fs::metadata(&path).unwrap();
	let this_times = Times::metadata(&this_meta);
	row(root_path, &path, &this_times);

	for entry in path.read_dir_utf8().unwrap() {
		let entry = entry.unwrap();

		match entry.file_type() {
			Err(_) => panic!(),
			Ok(ft) if ft.is_file() => {
				let meta = entry.metadata().unwrap();
				let times = Times::metadata(&meta);
				row(root_path, entry.path(), &times)
			}
			Ok(_) => {}
		}
	}

	for entry in path.read_dir_utf8().unwrap() {
		let entry = entry.unwrap();

		match entry.file_type() {
			Err(_) => panic!(),
			Ok(ft) if ft.is_dir() => process(root_path, entry.path()),
			Ok(_) => {}
		}
	}
}

fn row<P: AsRef<Utf8Path>>(root: &Utf8Path, path: P, times: &Times) {
	let relative = path
		.as_ref()
		.strip_prefix(root)
		.expect("row wasn't relative to the root; what happened?");

	/// Returns an owned string with the duration formatted as seconds, or a
	/// borrowed empty string
	fn time_string(time: Option<Duration>) -> String {
		time.map(|d| d.as_secs().to_string()).unwrap_or_default()
	}

	println!(
		"{},{},{},{}",
		escape(relative),
		time_string(times.created()),
		time_string(times.modified()),
		time_string(times.accessed())
	);
}

/// Blackslash-escape comma and backslash
fn escape<S: AsRef<str>>(raw: S) -> String {
	let raw = raw.as_ref();

	let mut escaped = String::with_capacity(raw.len());
	for c in raw.chars() {
		match c {
			',' | '\\' => {
				escaped.push('\\');
				escaped.push(c);
			}
			_ => escaped.push(c),
		}
	}

	escaped
}

struct Times {
	created: Option<SystemTime>,
	modified: Option<SystemTime>,
	accessed: Option<SystemTime>,
}

impl Times {
	/// Get the btime, mtime, atime from the Metadata. If a time cannot
	/// be attained, leave a None in it's place
	pub fn metadata(meta: &Metadata) -> Self {
		Self {
			created: meta.created().ok(),
			modified: meta.modified().ok(),
			accessed: meta.accessed().ok(),
		}
	}

	/// The time since the Unix Epoch the file was created
	pub fn created(&self) -> Option<Duration> {
		Self::from_epoch(self.created.as_ref())
	}

	/// The time since the Unix Epoch the file was last modified
	pub fn modified(&self) -> Option<Duration> {
		Self::from_epoch(self.modified.as_ref())
	}

	/// The time since the Unix Epoch the file was last accessed
	pub fn accessed(&self) -> Option<Duration> {
		Self::from_epoch(self.accessed.as_ref())
	}

	fn from_epoch(maybe_time: Option<&SystemTime>) -> Option<Duration> {
		maybe_time.map(|st| st.duration_since(SystemTime::UNIX_EPOCH).unwrap())
	}
}