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}
;
11 use proxmox_router
::cli
::{
12 format_and_print_result
, get_output_format
, CliCommand
, CliCommandMap
, CommandLineInterface
,
15 use proxmox_schema
::api
;
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
,
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
;
31 /// Decodes a blob and writes its content either to stdout or into a file
33 mut output_path
: Option
<&Path
>,
34 key_file
: Option
<&Path
>,
35 digest
: Option
<&[u8; 32]>,
37 ) -> Result
<(), Error
> {
38 let mut crypt_conf_opt
= None
;
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
);
48 output_path
= match output_path
{
49 Some(path
) if path
.eq(Path
::new("-")) => None
,
53 outfile_or_stdout(output_path
)?
.write_all(blob
.decode(crypt_conf_opt
, digest
)?
.as_slice())?
;
61 description
: "The chunk file.",
65 description
: "Path to the directory that should be searched for references.",
70 description
: "Needed when searching for references, if set, it will be used for verification when decoding.",
75 description
: "Path to the file to which the chunk should be decoded, '-' -> decode to stdout.",
80 description
: "Path to the keyfile with which the chunk was encrypted.",
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.",
91 schema
: OUTPUT_FORMAT
,
100 reference_filter
: Option
<String
>,
101 mut digest
: Option
<String
>,
102 decode
: Option
<String
>,
103 keyfile
: Option
<String
>,
104 use_filename_as_digest
: bool
,
106 ) -> Result
<(), Error
> {
107 let output_format
= get_output_format(¶m
);
108 let chunk_path
= Path
::new(&chunk
);
110 if digest
.is_none() && use_filename_as_digest
{
111 digest
= Some(if let Some((_
, filename
)) = chunk
.rsplit_once("/") {
112 String
::from(filename
)
118 let digest_raw
: Option
<[u8; 32]> = digest
120 <[u8; 32]>::from_hex(d
)
121 .map_err(|e
| format_err
!("could not parse chunk - {}", e
))
123 .map_or(Ok(None
), |r
| r
.map(Some
))?
;
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
);
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
))?
,
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
)
139 .filter_map(|e
| e
.ok())
141 use std
::os
::unix
::ffi
::OsStrExt
;
142 let file_name
= entry
.file_name().as_bytes();
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
),
149 } else if file_name
.ends_with(b
".didx") {
150 match DynamicIndexReader
::open(entry
.path()) {
151 Ok(index
) => Box
::new(index
),
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());
167 if !references
.is_empty() {
176 if decode_output_path
.is_some() {
185 let crc_status
= format
!(
188 blob
.verify_crc().map_or("BAD", |_
| "OK")
191 let val
= match referenced_by
{
192 Some(references
) => json
!({
194 "encryption": blob
.crypt_mode()?
,
195 "referenced-by": references
199 "encryption": blob
.crypt_mode()?
,
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
);
213 format_and_print_result(&val
, &output_format
);
222 description
: "Path to the file.",
226 description
: "Path to the file to which the file should be decoded, '-' -> decode to stdout.",
231 description
: "Path to the keyfile with which the file was encrypted.",
236 schema
: OUTPUT_FORMAT
,
242 /// Inspect a file, for blob file without decode only the size and encryption mode is printed
245 decode
: Option
<String
>,
246 keyfile
: Option
<String
>,
248 ) -> Result
<(), Error
> {
249 let output_format
= get_output_format(¶m
);
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
);
263 let decode_output_path
= decode
.as_ref().map(Path
::new
);
265 if decode_output_path
.is_some() {
266 decode_blob(decode_output_path
, key_file_path
, None
, &data_blob
)?
;
269 let crypt_mode
= data_blob
.crypt_mode()?
;
271 "encryption": crypt_mode
,
272 "size": data_blob
.raw_size(),
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
>
280 DYNAMIC_SIZED_CHUNK_INDEX_1_0
=> {
281 Box
::new(DynamicIndexReader
::new(file
)?
) as Box
<dyn IndexFile
>
283 _
=> bail
!(format_err
!("This is technically not possible")),
286 let mut ctime_str
= index
.index_ctime().to_string();
287 if let Ok(s
) = proxmox_time
::strftime_local("%c", index
.index_ctime()) {
291 let mut chunk_digests
= HashSet
::new();
293 for pos
in 0..index
.index_count() {
294 let digest
= index
.index_digest(pos
).unwrap();
295 chunk_digests
.insert(hex
::encode(digest
));
299 "size": index
.index_size(),
301 "chunk-digests": chunk_digests
304 _
=> bail
!(format_err
!(
305 "Only .blob, .fidx and .didx files may be inspected"
309 if output_format
== "text" {
310 println
!("size: {}", val
["size"]);
311 if let Some(encryption
) = val
["encryption"].as_str() {
312 println
!("encryption: {}", encryption
);
314 if let Some(ctime
) = val
["ctime"].as_str() {
315 println
!("creation time: {}", ctime
);
317 if let Some(chunks
) = val
["chunk-digests"].as_array() {
319 for chunk
in chunks
{
320 println
!(" {}", chunk
);
324 format_and_print_result(&val
, &output_format
);
330 pub fn inspect_commands() -> CommandLineInterface
{
331 let cmd_def
= CliCommandMap
::new()
334 CliCommand
::new(&API_METHOD_INSPECT_CHUNK
).arg_param(&["chunk"]),
338 CliCommand
::new(&API_METHOD_INSPECT_FILE
).arg_param(&["file"]),