1use std::{
2 fs::{self, File, Metadata},
3 io::{ErrorKind, Read},
4 os::unix::fs::{MetadataExt, PermissionsExt},
5 path::Path,
6};
7
8use anyhow::anyhow;
9use rayon::iter::{ParallelBridge, ParallelIterator};
10use walkdir::WalkDir;
11
12const INDENT: &str = " ";
13
14pub struct Options {
15 pub show_metadata: bool,
16 pub show_schema: bool,
17 pub batch_separator: String,
18 pub format_sql: bool,
19 pub format_sql_pretty: bool,
20}
21
22pub fn run(path: &Path, opt: Options) {
23 WalkDir::new(path)
24 .into_iter()
25 .par_bridge()
26 .filter_map(|entry_result| entry_result.ok())
27 .map(|entry| entry.into_path())
28 .filter_map(|path| {
29 fs::metadata(&path)
30 .inspect_err(|error| {
31 tracing::warn!(
32 ?error,
33 ?path,
34 "Failed to fetch file metadata."
35 );
36 })
37 .ok()
38 .map(|meta| (path, meta))
39 })
40 .filter(|(_path, meta)| meta.is_file())
41 .filter_map(|(path, meta)| {
42 file_has_sqlite_header(&path)
43 .inspect_err(|error| {
44 tracing::warn!(
45 ?error,
46 ?path,
47 "Failed to check file for SQLite header."
48 );
49 })
50 .ok()
51 .and_then(|has_header| has_header.then_some((path, meta)))
52 })
53 .filter_map(|(path, meta)| {
54 file_fetch_schema(&path, opt.format_sql, opt.format_sql_pretty)
55 .inspect_err(|error| {
56 tracing::warn!(?error, ?path, "Failed to fetch schema.");
57 })
58 .ok()
59 .map(|schema| (path, meta, schema))
60 })
61 .filter_map(|(path, meta, schema)| {
62 metadata_fmt(&meta)
63 .inspect_err(|error| {
64 tracing::warn!(
65 ?error,
66 ?path,
67 "Failed to format metadata."
68 );
69 })
70 .ok()
71 .map(|meta| (path, meta, schema))
72 })
73 .for_each(|(path, meta, schema)| {
74 let meta = if opt.show_metadata {
75 format!("\n{INDENT}meta\n{meta}")
76 } else {
77 String::new()
78 };
79 let schema = if opt.show_schema {
80 format!("\n{INDENT}schema\n{schema}")
81 } else {
82 String::new()
83 };
84 let batch_sep = if opt.show_metadata || opt.show_schema {
85 opt.batch_separator.as_str()
86 } else {
87 ""
88 };
89 println!("{path:?}{meta}{schema}{batch_sep}");
91 });
92}
93
94pub fn tracing_init(level: Option<tracing::Level>) -> anyhow::Result<()> {
95 use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter, Layer};
96
97 if let Some(level) = level {
98 let layer_stderr = fmt::Layer::new()
99 .with_writer(std::io::stderr)
100 .with_ansi(true)
101 .with_file(false)
102 .with_line_number(true)
103 .with_thread_ids(true)
104 .with_filter(
105 EnvFilter::from_default_env().add_directive(level.into()),
106 );
107 tracing::subscriber::set_global_default(
108 tracing_subscriber::registry().with(layer_stderr),
109 )?;
110 }
111 Ok(())
112}
113
114fn file_has_sqlite_header(path: &Path) -> anyhow::Result<bool> {
115 const SQLITE_HEADER: &[u8; 16] = b"SQLite format 3\0";
116
117 let mut file = File::open(path)?;
118 let mut buf = [0u8; SQLITE_HEADER.len()];
119 let read_result = file.read_exact(&mut buf);
120 match read_result.map_err(|e| e.kind()) {
121 Err(ErrorKind::UnexpectedEof) => {
122 return Ok(false);
123 }
124 Err(e) => {
125 return Err(anyhow!("{e:?} path={path:?}"));
126 }
127 Ok(()) => {}
128 }
129 Ok(buf[..].eq(SQLITE_HEADER))
130}
131
132fn metadata_fmt(meta: &Metadata) -> anyhow::Result<String> {
133 let user = meta.uid();
134 let group = meta.gid();
135 let size = human_units::Size(meta.len());
136 let perm = umask::Mode::from(meta.permissions().mode());
137 let mtime = humantime::format_rfc3339(meta.modified()?);
138 let atime = humantime::format_rfc3339(meta.accessed()?);
139 let btime = humantime::format_rfc3339(meta.created()?);
140 let lines = [
141 format!("btime {btime}"),
142 format!("mtime {mtime}"),
143 format!("atime {atime}"),
144 format!("size {size}"),
145 format!("perm {perm}"),
146 format!("owner {user}:{group}"),
147 ];
148 let meta = lines
149 .iter()
150 .map(|line| format!("{INDENT}{INDENT}{line}"))
151 .collect::<Vec<String>>()
152 .join("\n");
153 Ok(meta)
154}
155
156fn file_fetch_schema(
157 path: &Path,
158 format_sql: bool,
159 format_sql_pretty: bool,
160) -> anyhow::Result<String> {
161 let conn = rusqlite::Connection::open(path)?;
162 let sql = "SELECT sql FROM sqlite_master WHERE type IN ('table', 'view', 'index')";
163 let mut statement = conn.prepare(sql)?;
164 let mut schema = Vec::new();
165 let mut rows = statement.query([])?;
166 while let Some(row) = rows.next()? {
167 match row.get::<_, String>(0) {
168 Err(error) => {
169 tracing::warn!(?error, ?row, "Failed to access a row.");
170 }
171 Ok(sql) => {
172 let sql = if format_sql {
173 if format_sql_pretty {
174 sql_fmt_pretty(&sql)
175 } else {
176 sql_fmt(&sql)
177 }
178 } else {
179 sql
180 };
181 let sql = sql
182 .lines()
183 .map(|line| format!("{INDENT}{INDENT}{line}"))
184 .collect::<Vec<String>>()
185 .join("\n");
186 schema.push(sql);
187 }
188 }
189 }
190 schema.sort();
191 let schema = schema.join("\n");
192 Ok(schema)
193}
194
195fn sql_fmt(sql: &str) -> String {
197 sql.split_whitespace().collect::<Vec<&str>>().join(" ")
198}
199
200fn sql_fmt_pretty(sql: &str) -> String {
201 use sqlformat::{FormatOptions, Indent, QueryParams};
202
203 let mut opt = FormatOptions::default();
204 opt.indent = Indent::Spaces(4);
205 opt.uppercase = Some(true);
206 sqlformat::format(&sql, &QueryParams::None, &opt)
207}