1 use std
::collections
::HashSet
;
3 use std
::io
::{Read, Seek, SeekFrom}
;
6 use anyhow
::{bail, format_err, Error}
;
7 use serde_json
::{json, Value}
;
10 use proxmox
::api
::cli
::{
11 format_and_print_result
, get_output_format
, CliCommand
, CliCommandMap
, CommandLineInterface
,
13 use proxmox
::api
::{api, cli::*}
;
15 use pbs_tools
::cli
::outfile_or_stdout
;
16 use pbs_tools
::crypt_config
::CryptConfig
;
17 use pbs_datastore
::dynamic_index
::DynamicIndexReader
;
18 use pbs_datastore
::file_formats
::{
19 COMPRESSED_BLOB_MAGIC_1_0
, DYNAMIC_SIZED_CHUNK_INDEX_1_0
, ENCRYPTED_BLOB_MAGIC_1_0
,
20 ENCR_COMPR_BLOB_MAGIC_1_0
, FIXED_SIZED_CHUNK_INDEX_1_0
, UNCOMPRESSED_BLOB_MAGIC_1_0
,
22 use pbs_datastore
::fixed_index
::FixedIndexReader
;
23 use pbs_datastore
::index
::IndexFile
;
24 use pbs_datastore
::DataBlob
;
25 use pbs_config
::key_config
::load_and_decrypt_key
;
26 use pbs_client
::tools
::key_source
::get_encryption_key_password
;
29 /// Decodes a blob and writes its content either to stdout or into a file
31 mut output_path
: Option
<&Path
>,
32 key_file
: Option
<&Path
>,
33 digest
: Option
<&[u8; 32]>,
35 ) -> Result
<(), Error
> {
36 let mut crypt_conf_opt
= None
;
39 if blob
.is_encrypted() && key_file
.is_some() {
40 let (key
, _created
, _fingerprint
) =
41 load_and_decrypt_key(&key_file
.unwrap(), &get_encryption_key_password
)?
;
42 crypt_conf
= CryptConfig
::new(key
)?
;
43 crypt_conf_opt
= Some(&crypt_conf
);
46 output_path
= match output_path
{
47 Some(path
) if path
.eq(Path
::new("-")) => None
,
51 outfile_or_stdout(output_path
)?
.write_all(blob
.decode(crypt_conf_opt
, digest
)?
.as_slice())?
;
59 description
: "The chunk file.",
63 description
: "Path to the directory that should be searched for references.",
68 description
: "Needed when searching for references, if set, it will be used for verification when decoding.",
73 description
: "Path to the file to which the chunk should be decoded, '-' -> decode to stdout.",
78 description
: "Path to the keyfile with which the chunk was encrypted.",
82 "use-filename-as-digest": {
83 description
: "The filename should be used as digest for reference search and decode verification, if no digest is specified.",
89 schema
: OUTPUT_FORMAT
,
98 reference_filter
: Option
<String
>,
99 mut digest
: Option
<String
>,
100 decode
: Option
<String
>,
101 keyfile
: Option
<String
>,
102 use_filename_as_digest
: bool
,
104 ) -> Result
<(), Error
> {
105 let output_format
= get_output_format(¶m
);
106 let chunk_path
= Path
::new(&chunk
);
108 if digest
.is_none() && use_filename_as_digest
{
109 digest
= Some(if let Some((_
, filename
)) = chunk
.rsplit_once("/") {
110 String
::from(filename
)
116 let digest_raw
: Option
<[u8; 32]> = digest
118 proxmox
::tools
::hex_to_digest(d
)
119 .map_err(|e
| format_err
!("could not parse chunk - {}", e
))
121 .map_or(Ok(None
), |r
| r
.map(Some
))?
;
123 let search_path
= reference_filter
.as_ref().map(Path
::new
);
124 let key_file_path
= keyfile
.as_ref().map(Path
::new
);
125 let decode_output_path
= decode
.as_ref().map(Path
::new
);
127 let blob
= DataBlob
::load_from_reader(
128 &mut std
::fs
::File
::open(&chunk_path
)
129 .map_err(|e
| format_err
!("could not open chunk file - {}", e
))?
,
132 let referenced_by
= if let (Some(search_path
), Some(digest_raw
)) = (search_path
, digest_raw
) {
133 let mut references
= Vec
::new();
134 for entry
in WalkDir
::new(search_path
)
137 .filter_map(|e
| e
.ok())
139 use std
::os
::unix
::ffi
::OsStrExt
;
140 let file_name
= entry
.file_name().as_bytes();
142 let index
: Box
<dyn IndexFile
> = if file_name
.ends_with(b
".fidx") {
143 match FixedIndexReader
::open(entry
.path()) {
144 Ok(index
) => Box
::new(index
),
147 } else if file_name
.ends_with(b
".didx") {
148 match DynamicIndexReader
::open(entry
.path()) {
149 Ok(index
) => Box
::new(index
),
156 for pos
in 0..index
.index_count() {
157 if let Some(index_chunk_digest
) = index
.index_digest(pos
) {
158 if digest_raw
.eq(index_chunk_digest
) {
159 references
.push(entry
.path().to_string_lossy().into_owned());
165 if !references
.is_empty() {
174 if decode_output_path
.is_some() {
183 let crc_status
= format
!(
186 blob
.verify_crc().map_or("BAD", |_
| "OK")
189 let val
= match referenced_by
{
190 Some(references
) => json
!({
192 "encryption": blob
.crypt_mode()?
,
193 "referenced-by": references
197 "encryption": blob
.crypt_mode()?
,
201 if output_format
== "text" {
202 println
!("CRC: {}", val
["crc"]);
203 println
!("encryption: {}", val
["encryption"]);
204 if let Some(refs
) = val
["referenced-by"].as_array() {
205 println
!("referenced by:");
206 for reference
in refs
{
207 println
!(" {}", reference
);
211 format_and_print_result(&val
, &output_format
);
220 description
: "Path to the file.",
224 description
: "Path to the file to which the file should be decoded, '-' -> decode to stdout.",
229 description
: "Path to the keyfile with which the file was encrypted.",
234 schema
: OUTPUT_FORMAT
,
240 /// Inspect a file, for blob file without decode only the size and encryption mode is printed
243 decode
: Option
<String
>,
244 keyfile
: Option
<String
>,
246 ) -> Result
<(), Error
> {
247 let output_format
= get_output_format(¶m
);
249 let mut file
= File
::open(Path
::new(&file
))?
;
250 let mut magic
= [0; 8];
251 file
.read_exact(&mut magic
)?
;
252 file
.seek(SeekFrom
::Start(0))?
;
253 let val
= match magic
{
254 UNCOMPRESSED_BLOB_MAGIC_1_0
255 | COMPRESSED_BLOB_MAGIC_1_0
256 | ENCRYPTED_BLOB_MAGIC_1_0
257 | ENCR_COMPR_BLOB_MAGIC_1_0
=> {
258 let data_blob
= DataBlob
::load_from_reader(&mut file
)?
;
259 let key_file_path
= keyfile
.as_ref().map(Path
::new
);
261 let decode_output_path
= decode
.as_ref().map(Path
::new
);
263 if decode_output_path
.is_some() {
264 decode_blob(decode_output_path
, key_file_path
, None
, &data_blob
)?
;
267 let crypt_mode
= data_blob
.crypt_mode()?
;
269 "encryption": crypt_mode
,
270 "size": data_blob
.raw_size(),
273 FIXED_SIZED_CHUNK_INDEX_1_0
| DYNAMIC_SIZED_CHUNK_INDEX_1_0
=> {
274 let index
: Box
<dyn IndexFile
> = match magic
{
275 FIXED_SIZED_CHUNK_INDEX_1_0
=> {
276 Box
::new(FixedIndexReader
::new(file
)?
) as Box
<dyn IndexFile
>
278 DYNAMIC_SIZED_CHUNK_INDEX_1_0
=> {
279 Box
::new(DynamicIndexReader
::new(file
)?
) as Box
<dyn IndexFile
>
281 _
=> bail
!(format_err
!("This is technically not possible")),
284 let mut ctime_str
= index
.index_ctime().to_string();
285 if let Ok(s
) = proxmox
::tools
::time
::strftime_local("%c", index
.index_ctime()) {
289 let mut chunk_digests
= HashSet
::new();
291 for pos
in 0..index
.index_count() {
292 let digest
= index
.index_digest(pos
).unwrap();
293 chunk_digests
.insert(proxmox
::tools
::digest_to_hex(digest
));
297 "size": index
.index_size(),
299 "chunk-digests": chunk_digests
302 _
=> bail
!(format_err
!(
303 "Only .blob, .fidx and .didx files may be inspected"
307 if output_format
== "text" {
308 println
!("size: {}", val
["size"]);
309 if let Some(encryption
) = val
["encryption"].as_str() {
310 println
!("encryption: {}", encryption
);
312 if let Some(ctime
) = val
["ctime"].as_str() {
313 println
!("creation time: {}", ctime
);
315 if let Some(chunks
) = val
["chunk-digests"].as_array() {
317 for chunk
in chunks
{
318 println
!(" {}", chunk
);
322 format_and_print_result(&val
, &output_format
);
328 pub fn inspect_commands() -> CommandLineInterface
{
329 let cmd_def
= CliCommandMap
::new()
332 CliCommand
::new(&API_METHOD_INSPECT_CHUNK
).arg_param(&["chunk"]),
336 CliCommand
::new(&API_METHOD_INSPECT_FILE
).arg_param(&["file"]),