1 use std
::collections
::HashSet
;
3 use std
::io
::{stdout, Read, Seek, SeekFrom, Write}
;
5 use std
::panic
::{RefUnwindSafe, UnwindSafe}
;
7 use anyhow
::{bail, format_err, Error}
;
8 use serde_json
::{json, Value}
;
12 use proxmox_router
::cli
::{
13 format_and_print_result
, get_output_format
, CliCommand
, CliCommandMap
, CommandLineInterface
,
16 use proxmox_schema
::api
;
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
;
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
>>(
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
<_
>)
38 Ok(Box
::new(stdout()) as Box
<_
>)
42 /// Decodes a blob and writes its content either to stdout or into a file
44 mut output_path
: Option
<&Path
>,
45 key_file
: Option
<&Path
>,
46 digest
: Option
<&[u8; 32]>,
48 ) -> Result
<(), Error
> {
49 let mut crypt_conf_opt
= None
;
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
);
59 output_path
= match output_path
{
60 Some(path
) if path
.eq(Path
::new("-")) => None
,
64 outfile_or_stdout(output_path
)?
.write_all(blob
.decode(crypt_conf_opt
, digest
)?
.as_slice())?
;
72 description
: "The chunk file.",
76 description
: "Path to the directory that should be searched for references.",
81 description
: "Needed when searching for references, if set, it will be used for verification when decoding.",
86 description
: "Path to the file to which the chunk should be decoded, '-' -> decode to stdout.",
91 description
: "Path to the keyfile with which the chunk was encrypted.",
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.",
102 schema
: OUTPUT_FORMAT
,
111 reference_filter
: Option
<String
>,
112 mut digest
: Option
<String
>,
113 decode
: Option
<String
>,
114 keyfile
: Option
<String
>,
115 use_filename_as_digest
: bool
,
117 ) -> Result
<(), Error
> {
118 let output_format
= get_output_format(¶m
);
119 let chunk_path
= Path
::new(&chunk
);
121 if digest
.is_none() && use_filename_as_digest
{
122 digest
= Some(if let Some((_
, filename
)) = chunk
.rsplit_once("/") {
123 String
::from(filename
)
129 let digest_raw
: Option
<[u8; 32]> = digest
131 <[u8; 32]>::from_hex(d
)
132 .map_err(|e
| format_err
!("could not parse chunk - {}", e
))
134 .map_or(Ok(None
), |r
| r
.map(Some
))?
;
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
);
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
))?
,
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
)
150 .filter_map(|e
| e
.ok())
152 use std
::os
::unix
::ffi
::OsStrExt
;
153 let file_name
= entry
.file_name().as_bytes();
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
),
160 } else if file_name
.ends_with(b
".didx") {
161 match DynamicIndexReader
::open(entry
.path()) {
162 Ok(index
) => Box
::new(index
),
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());
178 if !references
.is_empty() {
187 if decode_output_path
.is_some() {
196 let crc_status
= format
!(
199 blob
.verify_crc().map_or("BAD", |_
| "OK")
202 let val
= match referenced_by
{
203 Some(references
) => json
!({
205 "encryption": blob
.crypt_mode()?
,
206 "referenced-by": references
210 "encryption": blob
.crypt_mode()?
,
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
);
224 format_and_print_result(&val
, &output_format
);
233 description
: "Path to the file.",
237 description
: "Path to the file to which the file should be decoded, '-' -> decode to stdout.",
242 description
: "Path to the keyfile with which the file was encrypted.",
247 schema
: OUTPUT_FORMAT
,
253 /// Inspect a file, for blob file without decode only the size and encryption mode is printed
256 decode
: Option
<String
>,
257 keyfile
: Option
<String
>,
259 ) -> Result
<(), Error
> {
260 let output_format
= get_output_format(¶m
);
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
);
274 let decode_output_path
= decode
.as_ref().map(Path
::new
);
276 if decode_output_path
.is_some() {
277 decode_blob(decode_output_path
, key_file_path
, None
, &data_blob
)?
;
280 let crypt_mode
= data_blob
.crypt_mode()?
;
282 "encryption": crypt_mode
,
283 "size": data_blob
.raw_size(),
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
>
291 DYNAMIC_SIZED_CHUNK_INDEX_1_0
=> {
292 Box
::new(DynamicIndexReader
::new(file
)?
) as Box
<dyn IndexFile
>
294 _
=> bail
!(format_err
!("This is technically not possible")),
297 let mut ctime_str
= index
.index_ctime().to_string();
298 if let Ok(s
) = proxmox_time
::strftime_local("%c", index
.index_ctime()) {
302 let mut chunk_digests
= HashSet
::new();
304 for pos
in 0..index
.index_count() {
305 let digest
= index
.index_digest(pos
).unwrap();
306 chunk_digests
.insert(hex
::encode(digest
));
310 "size": index
.index_size(),
312 "chunk-digests": chunk_digests
315 _
=> bail
!(format_err
!(
316 "Only .blob, .fidx and .didx files may be inspected"
320 if output_format
== "text" {
321 println
!("size: {}", val
["size"]);
322 if let Some(encryption
) = val
["encryption"].as_str() {
323 println
!("encryption: {}", encryption
);
325 if let Some(ctime
) = val
["ctime"].as_str() {
326 println
!("creation time: {}", ctime
);
328 if let Some(chunks
) = val
["chunk-digests"].as_array() {
330 for chunk
in chunks
{
331 println
!(" {}", chunk
);
335 format_and_print_result(&val
, &output_format
);
341 pub fn inspect_commands() -> CommandLineInterface
{
342 let cmd_def
= CliCommandMap
::new()
345 CliCommand
::new(&API_METHOD_INSPECT_CHUNK
).arg_param(&["chunk"]),
349 CliCommand
::new(&API_METHOD_INSPECT_FILE
).arg_param(&["file"]),