]> git.proxmox.com Git - proxmox-backup.git/blob - proxmox-file-restore/src/main.rs
tree-wide: fix needless borrows
[proxmox-backup.git] / proxmox-file-restore / src / main.rs
1 use std::ffi::OsStr;
2 use std::os::unix::ffi::OsStrExt;
3 use std::path::PathBuf;
4 use std::sync::Arc;
5
6 use anyhow::{bail, format_err, Error};
7 use serde_json::{json, Value};
8
9 use proxmox_sys::fs::{create_path, CreateOptions};
10 use proxmox_router::cli::{
11 complete_file_name, default_table_format_options,
12 format_and_print_result_full, get_output_format,
13 run_cli_command,
14 CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
15 };
16 use proxmox_schema::api;
17 use pxar::accessor::aio::Accessor;
18 use pxar::decoder::aio::Decoder;
19
20 use pbs_tools::crypt_config::CryptConfig;
21 use pbs_api_types::CryptMode;
22 use pbs_datastore::CATALOG_NAME;
23 use pbs_datastore::backup_info::BackupDir;
24 use pbs_datastore::catalog::{ArchiveEntry, CatalogReader, DirEntryAttribute};
25 use pbs_datastore::dynamic_index::{BufferedDynamicReader, LocalDynamicReadAt};
26 use pbs_datastore::index::IndexFile;
27 use pbs_config::key_config::decrypt_key;
28 use pbs_client::{BackupReader, RemoteChunkReader};
29 use pbs_client::pxar::{create_zip, extract_sub_dir, extract_sub_dir_seq};
30 use pbs_client::tools::{
31 complete_group_or_snapshot, complete_repository, connect, extract_repository_from_value,
32 key_source::{
33 crypto_parameters_keep_fd, format_key_source, get_encryption_key_password, KEYFD_SCHEMA,
34 KEYFILE_SCHEMA,
35 },
36 REPO_URL_SCHEMA,
37 };
38
39 pub mod block_driver;
40 pub use block_driver::*;
41
42 pub mod cpio;
43
44 mod qemu_helper;
45 mod block_driver_qemu;
46
47 enum ExtractPath {
48 ListArchives,
49 Pxar(String, Vec<u8>),
50 VM(String, Vec<u8>),
51 }
52
53 fn parse_path(path: String, base64: bool) -> Result<ExtractPath, Error> {
54 let mut bytes = if base64 {
55 base64::decode(&path)
56 .map_err(|err| format_err!("Failed base64-decoding path '{}' - {}", path, err))?
57 } else {
58 path.into_bytes()
59 };
60
61 if bytes == b"/" {
62 return Ok(ExtractPath::ListArchives);
63 }
64
65 while !bytes.is_empty() && bytes[0] == b'/' {
66 bytes.remove(0);
67 }
68
69 let (file, path) = {
70 let slash_pos = bytes.iter().position(|c| *c == b'/').unwrap_or(bytes.len());
71 let path = bytes.split_off(slash_pos);
72 let file = String::from_utf8(bytes)?;
73 (file, path)
74 };
75
76 if file.ends_with(".pxar.didx") {
77 Ok(ExtractPath::Pxar(file, path))
78 } else if file.ends_with(".img.fidx") {
79 Ok(ExtractPath::VM(file, path))
80 } else {
81 bail!("'{}' is not supported for file-restore", file);
82 }
83 }
84
85 fn keyfile_path(param: &Value) -> Option<String> {
86 if let Some(Value::String(keyfile)) = param.get("keyfile") {
87 return Some(keyfile.to_owned());
88 }
89
90 if let Some(Value::Number(keyfd)) = param.get("keyfd") {
91 return Some(format!("/dev/fd/{}", keyfd));
92 }
93
94 None
95 }
96
97 #[api(
98 input: {
99 properties: {
100 repository: {
101 schema: REPO_URL_SCHEMA,
102 optional: true,
103 },
104 snapshot: {
105 type: String,
106 description: "Group/Snapshot path.",
107 },
108 "path": {
109 description: "Path to restore. Directories will be restored as .zip files.",
110 type: String,
111 },
112 "base64": {
113 type: Boolean,
114 description: "If set, 'path' will be interpreted as base64 encoded.",
115 optional: true,
116 default: false,
117 },
118 keyfile: {
119 schema: KEYFILE_SCHEMA,
120 optional: true,
121 },
122 "keyfd": {
123 schema: KEYFD_SCHEMA,
124 optional: true,
125 },
126 "crypt-mode": {
127 type: CryptMode,
128 optional: true,
129 },
130 "driver": {
131 type: BlockDriverType,
132 optional: true,
133 },
134 "output-format": {
135 schema: OUTPUT_FORMAT,
136 optional: true,
137 },
138 }
139 },
140 returns: {
141 description: "A list of elements under the given path",
142 type: Array,
143 items: {
144 type: ArchiveEntry,
145 }
146 }
147 )]
148 /// List a directory from a backup snapshot.
149 async fn list(
150 snapshot: String,
151 path: String,
152 base64: bool,
153 param: Value,
154 ) -> Result<(), Error> {
155 let repo = extract_repository_from_value(&param)?;
156 let snapshot: BackupDir = snapshot.parse()?;
157 let path = parse_path(path, base64)?;
158
159 let keyfile = keyfile_path(&param);
160 let crypto = crypto_parameters_keep_fd(&param)?;
161 let crypt_config = match crypto.enc_key {
162 None => None,
163 Some(ref key) => {
164 let (key, _, _) =
165 decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| {
166 eprintln!("{}", format_key_source(&key.source, "encryption"));
167 err
168 })?;
169 Some(Arc::new(CryptConfig::new(key)?))
170 }
171 };
172
173 let client = connect(&repo)?;
174 let client = BackupReader::start(
175 client,
176 crypt_config.clone(),
177 repo.store(),
178 snapshot.group().backup_type(),
179 snapshot.group().backup_id(),
180 snapshot.backup_time(),
181 true,
182 )
183 .await?;
184
185 let (manifest, _) = client.download_manifest().await?;
186 manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?;
187
188 let result = match path {
189 ExtractPath::ListArchives => {
190 let mut entries = vec![];
191 for file in manifest.files() {
192 if !file.filename.ends_with(".pxar.didx") && !file.filename.ends_with(".img.fidx") {
193 continue;
194 }
195 let path = format!("/{}", file.filename);
196 let attr = if file.filename.ends_with(".pxar.didx") {
197 // a pxar file is a file archive, so it's root is also a directory root
198 Some(&DirEntryAttribute::Directory { start: 0 })
199 } else {
200 None
201 };
202 entries.push(ArchiveEntry::new_with_size(path.as_bytes(), attr, Some(file.size)));
203 }
204
205 Ok(entries)
206 }
207 ExtractPath::Pxar(file, mut path) => {
208 let index = client
209 .download_dynamic_index(&manifest, CATALOG_NAME)
210 .await?;
211 let most_used = index.find_most_used_chunks(8);
212 let file_info = manifest.lookup_file_info(CATALOG_NAME)?;
213 let chunk_reader = RemoteChunkReader::new(
214 client.clone(),
215 crypt_config,
216 file_info.chunk_crypt_mode(),
217 most_used,
218 );
219 let reader = BufferedDynamicReader::new(index, chunk_reader);
220 let mut catalog_reader = CatalogReader::new(reader);
221
222 let mut fullpath = file.into_bytes();
223 fullpath.append(&mut path);
224
225 catalog_reader.list_dir_contents(&fullpath)
226 }
227 ExtractPath::VM(file, path) => {
228 let details = SnapRestoreDetails {
229 manifest,
230 repo,
231 snapshot,
232 keyfile,
233 };
234 let driver: Option<BlockDriverType> = match param.get("driver") {
235 Some(drv) => Some(serde_json::from_value(drv.clone())?),
236 None => None,
237 };
238 data_list(driver, details, file, path).await
239 }
240 }?;
241
242 let options = default_table_format_options()
243 .sortby("type", false)
244 .sortby("text", false)
245 .column(ColumnConfig::new("type"))
246 .column(ColumnConfig::new("text").header("name"))
247 .column(ColumnConfig::new("mtime").header("last modified"))
248 .column(ColumnConfig::new("size"));
249
250 let output_format = get_output_format(&param);
251 format_and_print_result_full(
252 &mut json!(result),
253 &API_METHOD_LIST.returns,
254 &output_format,
255 &options,
256 );
257
258 Ok(())
259 }
260
261 #[api(
262 input: {
263 properties: {
264 repository: {
265 schema: REPO_URL_SCHEMA,
266 optional: true,
267 },
268 snapshot: {
269 type: String,
270 description: "Group/Snapshot path.",
271 },
272 "path": {
273 description: "Path to restore. Directories will be restored as .zip files if extracted to stdout.",
274 type: String,
275 },
276 "base64": {
277 type: Boolean,
278 description: "If set, 'path' will be interpreted as base64 encoded.",
279 optional: true,
280 default: false,
281 },
282 target: {
283 type: String,
284 optional: true,
285 description: "Target directory path. Use '-' to write to standard output.",
286 },
287 keyfile: {
288 schema: KEYFILE_SCHEMA,
289 optional: true,
290 },
291 "keyfd": {
292 schema: KEYFD_SCHEMA,
293 optional: true,
294 },
295 "crypt-mode": {
296 type: CryptMode,
297 optional: true,
298 },
299 verbose: {
300 type: Boolean,
301 description: "Print verbose information",
302 optional: true,
303 default: false,
304 },
305 "driver": {
306 type: BlockDriverType,
307 optional: true,
308 },
309 }
310 }
311 )]
312 /// Restore files from a backup snapshot.
313 async fn extract(
314 snapshot: String,
315 path: String,
316 base64: bool,
317 target: Option<String>,
318 verbose: bool,
319 param: Value,
320 ) -> Result<(), Error> {
321 let repo = extract_repository_from_value(&param)?;
322 let snapshot: BackupDir = snapshot.parse()?;
323 let orig_path = path;
324 let path = parse_path(orig_path.clone(), base64)?;
325
326 let target = match target {
327 Some(target) if target == "-" => None,
328 Some(target) => Some(PathBuf::from(target)),
329 None => Some(std::env::current_dir()?),
330 };
331
332 let keyfile = keyfile_path(&param);
333 let crypto = crypto_parameters_keep_fd(&param)?;
334 let crypt_config = match crypto.enc_key {
335 None => None,
336 Some(ref key) => {
337 let (key, _, _) =
338 decrypt_key(&key.key, &get_encryption_key_password).map_err(|err| {
339 eprintln!("{}", format_key_source(&key.source, "encryption"));
340 err
341 })?;
342 Some(Arc::new(CryptConfig::new(key)?))
343 }
344 };
345
346 let client = connect(&repo)?;
347 let client = BackupReader::start(
348 client,
349 crypt_config.clone(),
350 repo.store(),
351 snapshot.group().backup_type(),
352 snapshot.group().backup_id(),
353 snapshot.backup_time(),
354 true,
355 )
356 .await?;
357 let (manifest, _) = client.download_manifest().await?;
358
359 match path {
360 ExtractPath::Pxar(archive_name, path) => {
361 let file_info = manifest.lookup_file_info(&archive_name)?;
362 let index = client
363 .download_dynamic_index(&manifest, &archive_name)
364 .await?;
365 let most_used = index.find_most_used_chunks(8);
366 let chunk_reader = RemoteChunkReader::new(
367 client.clone(),
368 crypt_config,
369 file_info.chunk_crypt_mode(),
370 most_used,
371 );
372 let reader = BufferedDynamicReader::new(index, chunk_reader);
373
374 let archive_size = reader.archive_size();
375 let reader = LocalDynamicReadAt::new(reader);
376 let decoder = Accessor::new(reader, archive_size).await?;
377 extract_to_target(decoder, &path, target, verbose).await?;
378 }
379 ExtractPath::VM(file, path) => {
380 let details = SnapRestoreDetails {
381 manifest,
382 repo,
383 snapshot,
384 keyfile,
385 };
386 let driver: Option<BlockDriverType> = match param.get("driver") {
387 Some(drv) => Some(serde_json::from_value(drv.clone())?),
388 None => None,
389 };
390
391 if let Some(mut target) = target {
392 let reader = data_extract(driver, details, file, path.clone(), true).await?;
393 let decoder = Decoder::from_tokio(reader).await?;
394 extract_sub_dir_seq(&target, decoder, verbose).await?;
395
396 // we extracted a .pxarexclude-cli file auto-generated by the VM when encoding the
397 // archive, this file is of no use for the user, so try to remove it
398 target.push(".pxarexclude-cli");
399 std::fs::remove_file(target).map_err(|e| {
400 format_err!("unable to remove temporary .pxarexclude-cli file - {}", e)
401 })?;
402 } else {
403 let mut reader = data_extract(driver, details, file, path.clone(), false).await?;
404 tokio::io::copy(&mut reader, &mut tokio::io::stdout()).await?;
405 }
406 }
407 _ => {
408 bail!("cannot extract '{}'", orig_path);
409 }
410 }
411
412 Ok(())
413 }
414
415 async fn extract_to_target<T>(
416 decoder: Accessor<T>,
417 path: &[u8],
418 target: Option<PathBuf>,
419 verbose: bool,
420 ) -> Result<(), Error>
421 where
422 T: pxar::accessor::ReadAt + Clone + Send + Sync + Unpin + 'static,
423 {
424 let path = if path.is_empty() { b"/" } else { path };
425
426 let root = decoder.open_root().await?;
427 let file = root
428 .lookup(OsStr::from_bytes(path))
429 .await?
430 .ok_or_else(|| format_err!("error opening '{:?}'", path))?;
431
432 if let Some(target) = target {
433 extract_sub_dir(target, decoder, OsStr::from_bytes(path), verbose).await?;
434 } else {
435 match file.kind() {
436 pxar::EntryKind::File { .. } => {
437 tokio::io::copy(&mut file.contents().await?, &mut tokio::io::stdout()).await?;
438 }
439 _ => {
440 create_zip(
441 tokio::io::stdout(),
442 decoder,
443 OsStr::from_bytes(path),
444 verbose,
445 )
446 .await?;
447 }
448 }
449 }
450
451 Ok(())
452 }
453
454 fn main() {
455 let list_cmd_def = CliCommand::new(&API_METHOD_LIST)
456 .arg_param(&["snapshot", "path"])
457 .completion_cb("repository", complete_repository)
458 .completion_cb("snapshot", complete_group_or_snapshot);
459
460 let restore_cmd_def = CliCommand::new(&API_METHOD_EXTRACT)
461 .arg_param(&["snapshot", "path", "target"])
462 .completion_cb("repository", complete_repository)
463 .completion_cb("snapshot", complete_group_or_snapshot)
464 .completion_cb("target", complete_file_name);
465
466 let status_cmd_def = CliCommand::new(&API_METHOD_STATUS);
467 let stop_cmd_def = CliCommand::new(&API_METHOD_STOP)
468 .arg_param(&["name"])
469 .completion_cb("name", complete_block_driver_ids);
470
471 let cmd_def = CliCommandMap::new()
472 .insert("list", list_cmd_def)
473 .insert("extract", restore_cmd_def)
474 .insert("status", status_cmd_def)
475 .insert("stop", stop_cmd_def);
476
477 let rpcenv = CliEnvironment::new();
478 run_cli_command(
479 cmd_def,
480 rpcenv,
481 Some(|future| proxmox_async::runtime::main(future)),
482 );
483 }
484
485 /// Returns a runtime dir owned by the current user.
486 /// Note that XDG_RUNTIME_DIR is not always available, especially for non-login users like
487 /// "www-data", so we use a custom one in /run/proxmox-backup/<uid> instead.
488 pub fn get_user_run_dir() -> Result<std::path::PathBuf, Error> {
489 let uid = nix::unistd::Uid::current();
490 let mut path: std::path::PathBuf = pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR.into();
491 path.push(uid.to_string());
492 create_run_dir()?;
493 std::fs::create_dir_all(&path)?;
494 Ok(path)
495 }
496
497 /// FIXME: proxmox-file-restore should not depend on this!
498 fn create_run_dir() -> Result<(), Error> {
499 let backup_user = backup_user()?;
500 let opts = CreateOptions::new()
501 .owner(backup_user.uid)
502 .group(backup_user.gid);
503 let _: bool = create_path(pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M!(), None, Some(opts))?;
504 Ok(())
505 }
506
507 /// Return User info for the 'backup' user (``getpwnam_r(3)``)
508 pub fn backup_user() -> Result<nix::unistd::User, Error> {
509 nix::unistd::User::from_name(pbs_buildcfg::BACKUP_USER_NAME)?
510 .ok_or_else(|| format_err!("Unable to lookup '{}' user.", pbs_buildcfg::BACKUP_USER_NAME))
511 }
512