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

use camino::{Utf8Path, Utf8PathBuf};

// CSV definition:
// a row may contain any data, but comma are escaped with a backslash
// and blackslash are escaped, too. there is no header. if a time
// cannot be attained due to an error, it is left blank
// data is in the format: path, creation-time, modification-time
// the path is relative to the given path

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();

	// behavior:
	// reads directories twice. we're trying to be memory efficient rather than
	// computationally. i'm not quite sure why.
	// first pass: read and output file information
	// second pass: read and outut directory information
	process(&path, &path)
}

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).unwrap();

	println!(
		"{},{},{},{}",
		escape(relative),
		times
			.created()
			.map(|d| d.as_secs().to_string())
			.unwrap_or_default(),
		times
			.modified()
			.map(|d| d.as_secs().to_string())
			.unwrap_or_default(),
		times
			.accessed()
			.map(|d| d.as_secs().to_string())
			.unwrap_or_default()
	);
}

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 {
	pub fn metadata(meta: &Metadata) -> Self {
		Self {
			created: meta.created().ok(),
			modified: meta.modified().ok(),
			accessed: meta.accessed().ok(),
		}
	}

	// EPOCH created
	pub fn created(&self) -> Option<Duration> {
		self.created
			.map(|st| st.duration_since(SystemTime::UNIX_EPOCH).unwrap())
	}

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

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