]> git.proxmox.com Git - proxmox-backup.git/blob - src/bin/proxmox_backup_debug/inspect.rs
update to first proxmox crate split
[proxmox-backup.git] / src / bin / proxmox_backup_debug / inspect.rs
1 use std::collections::HashSet;
2 use std::fs::File;
3 use std::io::{Read, Seek, SeekFrom};
4 use std::path::Path;
5
6 use anyhow::{bail, format_err, Error};
7 use serde_json::{json, Value};
8 use walkdir::WalkDir;
9
10 use proxmox_router::cli::{
11 format_and_print_result, get_output_format, CliCommand, CliCommandMap, CommandLineInterface,
12 OUTPUT_FORMAT,
13 };
14 use proxmox_schema::api;
15
16 use pbs_tools::cli::outfile_or_stdout;
17 use pbs_tools::crypt_config::CryptConfig;
18 use pbs_datastore::dynamic_index::DynamicIndexReader;
19 use pbs_datastore::file_formats::{
20 COMPRESSED_BLOB_MAGIC_1_0, DYNAMIC_SIZED_CHUNK_INDEX_1_0, ENCRYPTED_BLOB_MAGIC_1_0,
21 ENCR_COMPR_BLOB_MAGIC_1_0, FIXED_SIZED_CHUNK_INDEX_1_0, UNCOMPRESSED_BLOB_MAGIC_1_0,
22 };
23 use pbs_datastore::fixed_index::FixedIndexReader;
24 use pbs_datastore::index::IndexFile;
25 use pbs_datastore::DataBlob;
26 use pbs_config::key_config::load_and_decrypt_key;
27 use pbs_client::tools::key_source::get_encryption_key_password;
28
29
30 /// Decodes a blob and writes its content either to stdout or into a file
31 fn decode_blob(
32 mut output_path: Option<&Path>,
33 key_file: Option<&Path>,
34 digest: Option<&[u8; 32]>,
35 blob: &DataBlob,
36 ) -> Result<(), Error> {
37 let mut crypt_conf_opt = None;
38 let crypt_conf;
39
40 if blob.is_encrypted() && key_file.is_some() {
41 let (key, _created, _fingerprint) =
42 load_and_decrypt_key(&key_file.unwrap(), &get_encryption_key_password)?;
43 crypt_conf = CryptConfig::new(key)?;
44 crypt_conf_opt = Some(&crypt_conf);
45 }
46
47 output_path = match output_path {
48 Some(path) if path.eq(Path::new("-")) => None,
49 _ => output_path,
50 };
51
52 outfile_or_stdout(output_path)?.write_all(blob.decode(crypt_conf_opt, digest)?.as_slice())?;
53 Ok(())
54 }
55
56 #[api(
57 input: {
58 properties: {
59 chunk: {
60 description: "The chunk file.",
61 type: String,
62 },
63 "reference-filter": {
64 description: "Path to the directory that should be searched for references.",
65 type: String,
66 optional: true,
67 },
68 "digest": {
69 description: "Needed when searching for references, if set, it will be used for verification when decoding.",
70 type: String,
71 optional: true,
72 },
73 "decode": {
74 description: "Path to the file to which the chunk should be decoded, '-' -> decode to stdout.",
75 type: String,
76 optional: true,
77 },
78 "keyfile": {
79 description: "Path to the keyfile with which the chunk was encrypted.",
80 type: String,
81 optional: true,
82 },
83 "use-filename-as-digest": {
84 description: "The filename should be used as digest for reference search and decode verification, if no digest is specified.",
85 type: bool,
86 optional: true,
87 default: true,
88 },
89 "output-format": {
90 schema: OUTPUT_FORMAT,
91 optional: true,
92 },
93 }
94 }
95 )]
96 /// Inspect a chunk
97 fn inspect_chunk(
98 chunk: String,
99 reference_filter: Option<String>,
100 mut digest: Option<String>,
101 decode: Option<String>,
102 keyfile: Option<String>,
103 use_filename_as_digest: bool,
104 param: Value,
105 ) -> Result<(), Error> {
106 let output_format = get_output_format(&param);
107 let chunk_path = Path::new(&chunk);
108
109 if digest.is_none() && use_filename_as_digest {
110 digest = Some(if let Some((_, filename)) = chunk.rsplit_once("/") {
111 String::from(filename)
112 } else {
113 chunk.clone()
114 });
115 };
116
117 let digest_raw: Option<[u8; 32]> = digest
118 .map(|ref d| {
119 proxmox::tools::hex_to_digest(d)
120 .map_err(|e| format_err!("could not parse chunk - {}", e))
121 })
122 .map_or(Ok(None), |r| r.map(Some))?;
123
124 let search_path = reference_filter.as_ref().map(Path::new);
125 let key_file_path = keyfile.as_ref().map(Path::new);
126 let decode_output_path = decode.as_ref().map(Path::new);
127
128 let blob = DataBlob::load_from_reader(
129 &mut std::fs::File::open(&chunk_path)
130 .map_err(|e| format_err!("could not open chunk file - {}", e))?,
131 )?;
132
133 let referenced_by = if let (Some(search_path), Some(digest_raw)) = (search_path, digest_raw) {
134 let mut references = Vec::new();
135 for entry in WalkDir::new(search_path)
136 .follow_links(false)
137 .into_iter()
138 .filter_map(|e| e.ok())
139 {
140 use std::os::unix::ffi::OsStrExt;
141 let file_name = entry.file_name().as_bytes();
142
143 let index: Box<dyn IndexFile> = if file_name.ends_with(b".fidx") {
144 match FixedIndexReader::open(entry.path()) {
145 Ok(index) => Box::new(index),
146 Err(_) => continue,
147 }
148 } else if file_name.ends_with(b".didx") {
149 match DynamicIndexReader::open(entry.path()) {
150 Ok(index) => Box::new(index),
151 Err(_) => continue,
152 }
153 } else {
154 continue;
155 };
156
157 for pos in 0..index.index_count() {
158 if let Some(index_chunk_digest) = index.index_digest(pos) {
159 if digest_raw.eq(index_chunk_digest) {
160 references.push(entry.path().to_string_lossy().into_owned());
161 break;
162 }
163 }
164 }
165 }
166 if !references.is_empty() {
167 Some(references)
168 } else {
169 None
170 }
171 } else {
172 None
173 };
174
175 if decode_output_path.is_some() {
176 decode_blob(
177 decode_output_path,
178 key_file_path,
179 digest_raw.as_ref(),
180 &blob,
181 )?;
182 }
183
184 let crc_status = format!(
185 "{}({})",
186 blob.compute_crc(),
187 blob.verify_crc().map_or("BAD", |_| "OK")
188 );
189
190 let val = match referenced_by {
191 Some(references) => json!({
192 "crc": crc_status,
193 "encryption": blob.crypt_mode()?,
194 "referenced-by": references
195 }),
196 None => json!({
197 "crc": crc_status,
198 "encryption": blob.crypt_mode()?,
199 }),
200 };
201
202 if output_format == "text" {
203 println!("CRC: {}", val["crc"]);
204 println!("encryption: {}", val["encryption"]);
205 if let Some(refs) = val["referenced-by"].as_array() {
206 println!("referenced by:");
207 for reference in refs {
208 println!(" {}", reference);
209 }
210 }
211 } else {
212 format_and_print_result(&val, &output_format);
213 }
214 Ok(())
215 }
216
217 #[api(
218 input: {
219 properties: {
220 file: {
221 description: "Path to the file.",
222 type: String,
223 },
224 "decode": {
225 description: "Path to the file to which the file should be decoded, '-' -> decode to stdout.",
226 type: String,
227 optional: true,
228 },
229 "keyfile": {
230 description: "Path to the keyfile with which the file was encrypted.",
231 type: String,
232 optional: true,
233 },
234 "output-format": {
235 schema: OUTPUT_FORMAT,
236 optional: true,
237 },
238 }
239 }
240 )]
241 /// Inspect a file, for blob file without decode only the size and encryption mode is printed
242 fn inspect_file(
243 file: String,
244 decode: Option<String>,
245 keyfile: Option<String>,
246 param: Value,
247 ) -> Result<(), Error> {
248 let output_format = get_output_format(&param);
249
250 let mut file = File::open(Path::new(&file))?;
251 let mut magic = [0; 8];
252 file.read_exact(&mut magic)?;
253 file.seek(SeekFrom::Start(0))?;
254 let val = match magic {
255 UNCOMPRESSED_BLOB_MAGIC_1_0
256 | COMPRESSED_BLOB_MAGIC_1_0
257 | ENCRYPTED_BLOB_MAGIC_1_0
258 | ENCR_COMPR_BLOB_MAGIC_1_0 => {
259 let data_blob = DataBlob::load_from_reader(&mut file)?;
260 let key_file_path = keyfile.as_ref().map(Path::new);
261
262 let decode_output_path = decode.as_ref().map(Path::new);
263
264 if decode_output_path.is_some() {
265 decode_blob(decode_output_path, key_file_path, None, &data_blob)?;
266 }
267
268 let crypt_mode = data_blob.crypt_mode()?;
269 json!({
270 "encryption": crypt_mode,
271 "size": data_blob.raw_size(),
272 })
273 }
274 FIXED_SIZED_CHUNK_INDEX_1_0 | DYNAMIC_SIZED_CHUNK_INDEX_1_0 => {
275 let index: Box<dyn IndexFile> = match magic {
276 FIXED_SIZED_CHUNK_INDEX_1_0 => {
277 Box::new(FixedIndexReader::new(file)?) as Box<dyn IndexFile>
278 }
279 DYNAMIC_SIZED_CHUNK_INDEX_1_0 => {
280 Box::new(DynamicIndexReader::new(file)?) as Box<dyn IndexFile>
281 }
282 _ => bail!(format_err!("This is technically not possible")),
283 };
284
285 let mut ctime_str = index.index_ctime().to_string();
286 if let Ok(s) = proxmox_time::strftime_local("%c", index.index_ctime()) {
287 ctime_str = s;
288 }
289
290 let mut chunk_digests = HashSet::new();
291
292 for pos in 0..index.index_count() {
293 let digest = index.index_digest(pos).unwrap();
294 chunk_digests.insert(proxmox::tools::digest_to_hex(digest));
295 }
296
297 json!({
298 "size": index.index_size(),
299 "ctime": ctime_str,
300 "chunk-digests": chunk_digests
301 })
302 }
303 _ => bail!(format_err!(
304 "Only .blob, .fidx and .didx files may be inspected"
305 )),
306 };
307
308 if output_format == "text" {
309 println!("size: {}", val["size"]);
310 if let Some(encryption) = val["encryption"].as_str() {
311 println!("encryption: {}", encryption);
312 }
313 if let Some(ctime) = val["ctime"].as_str() {
314 println!("creation time: {}", ctime);
315 }
316 if let Some(chunks) = val["chunk-digests"].as_array() {
317 println!("chunks:");
318 for chunk in chunks {
319 println!(" {}", chunk);
320 }
321 }
322 } else {
323 format_and_print_result(&val, &output_format);
324 }
325
326 Ok(())
327 }
328
329 pub fn inspect_commands() -> CommandLineInterface {
330 let cmd_def = CliCommandMap::new()
331 .insert(
332 "chunk",
333 CliCommand::new(&API_METHOD_INSPECT_CHUNK).arg_param(&["chunk"]),
334 )
335 .insert(
336 "file",
337 CliCommand::new(&API_METHOD_INSPECT_FILE).arg_param(&["file"]),
338 );
339
340 cmd_def.into()
341 }