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