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