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