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_router
::cli
::{
11 format_and_print_result
, get_output_format
, CliCommand
, CliCommandMap
, CommandLineInterface
,
14 use proxmox_schema
::api
;
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
,
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
;
30 /// Decodes a blob and writes its content either to stdout or into a file
32 mut output_path
: Option
<&Path
>,
33 key_file
: Option
<&Path
>,
34 digest
: Option
<&[u8; 32]>,
36 ) -> Result
<(), Error
> {
37 let mut crypt_conf_opt
= None
;
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
);
47 output_path
= match output_path
{
48 Some(path
) if path
.eq(Path
::new("-")) => None
,
52 outfile_or_stdout(output_path
)?
.write_all(blob
.decode(crypt_conf_opt
, digest
)?
.as_slice())?
;
60 description
: "The chunk file.",
64 description
: "Path to the directory that should be searched for references.",
69 description
: "Needed when searching for references, if set, it will be used for verification when decoding.",
74 description
: "Path to the file to which the chunk should be decoded, '-' -> decode to stdout.",
79 description
: "Path to the keyfile with which the chunk was encrypted.",
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.",
90 schema
: OUTPUT_FORMAT
,
99 reference_filter
: Option
<String
>,
100 mut digest
: Option
<String
>,
101 decode
: Option
<String
>,
102 keyfile
: Option
<String
>,
103 use_filename_as_digest
: bool
,
105 ) -> Result
<(), Error
> {
106 let output_format
= get_output_format(¶m
);
107 let chunk_path
= Path
::new(&chunk
);
109 if digest
.is_none() && use_filename_as_digest
{
110 digest
= Some(if let Some((_
, filename
)) = chunk
.rsplit_once("/") {
111 String
::from(filename
)
117 let digest_raw
: Option
<[u8; 32]> = digest
119 proxmox
::tools
::hex_to_digest(d
)
120 .map_err(|e
| format_err
!("could not parse chunk - {}", e
))
122 .map_or(Ok(None
), |r
| r
.map(Some
))?
;
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
);
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
))?
,
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
)
138 .filter_map(|e
| e
.ok())
140 use std
::os
::unix
::ffi
::OsStrExt
;
141 let file_name
= entry
.file_name().as_bytes();
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
),
148 } else if file_name
.ends_with(b
".didx") {
149 match DynamicIndexReader
::open(entry
.path()) {
150 Ok(index
) => Box
::new(index
),
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());
166 if !references
.is_empty() {
175 if decode_output_path
.is_some() {
184 let crc_status
= format
!(
187 blob
.verify_crc().map_or("BAD", |_
| "OK")
190 let val
= match referenced_by
{
191 Some(references
) => json
!({
193 "encryption": blob
.crypt_mode()?
,
194 "referenced-by": references
198 "encryption": blob
.crypt_mode()?
,
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
);
212 format_and_print_result(&val
, &output_format
);
221 description
: "Path to the file.",
225 description
: "Path to the file to which the file should be decoded, '-' -> decode to stdout.",
230 description
: "Path to the keyfile with which the file was encrypted.",
235 schema
: OUTPUT_FORMAT
,
241 /// Inspect a file, for blob file without decode only the size and encryption mode is printed
244 decode
: Option
<String
>,
245 keyfile
: Option
<String
>,
247 ) -> Result
<(), Error
> {
248 let output_format
= get_output_format(¶m
);
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
);
262 let decode_output_path
= decode
.as_ref().map(Path
::new
);
264 if decode_output_path
.is_some() {
265 decode_blob(decode_output_path
, key_file_path
, None
, &data_blob
)?
;
268 let crypt_mode
= data_blob
.crypt_mode()?
;
270 "encryption": crypt_mode
,
271 "size": data_blob
.raw_size(),
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
>
279 DYNAMIC_SIZED_CHUNK_INDEX_1_0
=> {
280 Box
::new(DynamicIndexReader
::new(file
)?
) as Box
<dyn IndexFile
>
282 _
=> bail
!(format_err
!("This is technically not possible")),
285 let mut ctime_str
= index
.index_ctime().to_string();
286 if let Ok(s
) = proxmox_time
::strftime_local("%c", index
.index_ctime()) {
290 let mut chunk_digests
= HashSet
::new();
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
));
298 "size": index
.index_size(),
300 "chunk-digests": chunk_digests
303 _
=> bail
!(format_err
!(
304 "Only .blob, .fidx and .didx files may be inspected"
308 if output_format
== "text" {
309 println
!("size: {}", val
["size"]);
310 if let Some(encryption
) = val
["encryption"].as_str() {
311 println
!("encryption: {}", encryption
);
313 if let Some(ctime
) = val
["ctime"].as_str() {
314 println
!("creation time: {}", ctime
);
316 if let Some(chunks
) = val
["chunk-digests"].as_array() {
318 for chunk
in chunks
{
319 println
!(" {}", chunk
);
323 format_and_print_result(&val
, &output_format
);
329 pub fn inspect_commands() -> CommandLineInterface
{
330 let cmd_def
= CliCommandMap
::new()
333 CliCommand
::new(&API_METHOD_INSPECT_CHUNK
).arg_param(&["chunk"]),
337 CliCommand
::new(&API_METHOD_INSPECT_FILE
).arg_param(&["file"]),