]> git.proxmox.com Git - proxmox-backup.git/blame - proxmox-file-restore/src/main.rs
bump proxmox dependency to 0.14.0 and proxmox-http to 0.5.0
[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
DC
8
9use proxmox::api::{
10 api,
9fe3358c
SR
11 cli::{
12 default_table_format_options, format_and_print_result_full, get_output_format,
13 run_cli_command, CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT,
14 },
76425d84 15};
6c76aa43 16use proxmox::tools::fs::{create_path, CreateOptions};
76425d84 17use pxar::accessor::aio::Accessor;
b13089cd 18use pxar::decoder::aio::Decoder;
76425d84 19
bbdda58b 20use pbs_tools::crypt_config::CryptConfig;
b2065dc7 21use pbs_api_types::CryptMode;
bbdda58b 22use pbs_datastore::CATALOG_NAME;
b2065dc7 23use pbs_datastore::backup_info::BackupDir;
013b1e8b 24use pbs_datastore::catalog::{ArchiveEntry, CatalogReader, DirEntryAttribute};
b2065dc7 25use pbs_datastore::dynamic_index::{BufferedDynamicReader, LocalDynamicReadAt};
2b7f8dd5 26use pbs_datastore::index::IndexFile;
bbdda58b 27use pbs_config::key_config::decrypt_key;
2b7f8dd5
WB
28use pbs_client::{BackupReader, RemoteChunkReader};
29use pbs_client::pxar::{create_zip, extract_sub_dir, extract_sub_dir_seq};
30use pbs_client::tools::{
76425d84
DC
31 complete_group_or_snapshot, complete_repository, connect, extract_repository_from_value,
32 key_source::{
15998ed1 33 crypto_parameters_keep_fd, format_key_source, get_encryption_key_password, KEYFD_SCHEMA,
76425d84
DC
34 KEYFILE_SCHEMA,
35 },
36 REPO_URL_SCHEMA,
37};
38
6c76aa43
WB
39pub mod block_driver;
40pub use block_driver::*;
2b7f8dd5 41
6c76aa43
WB
42pub mod cpio;
43
44mod qemu_helper;
45mod block_driver_qemu;
58421ec1 46
76425d84
DC
47enum ExtractPath {
48 ListArchives,
49 Pxar(String, Vec<u8>),
801ec1db 50 VM(String, Vec<u8>),
76425d84
DC
51}
52
53fn parse_path(path: String, base64: bool) -> Result<ExtractPath, Error> {
54 let mut bytes = if base64 {
e045d154 55 base64::decode(&path)
6526709d 56 .map_err(|err| format_err!("Failed base64-decoding path '{}' - {}", path, err))?
76425d84
DC
57 } else {
58 path.into_bytes()
59 };
60
61 if bytes == b"/" {
62 return Ok(ExtractPath::ListArchives);
63 }
64
2fd2d292 65 while !bytes.is_empty() && bytes[0] == b'/' {
76425d84
DC
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))
801ec1db
SR
78 } else if file.ends_with(".img.fidx") {
79 Ok(ExtractPath::VM(file, path))
76425d84
DC
80 } else {
81 bail!("'{}' is not supported for file-restore", file);
82 }
83}
84
15998ed1
SR
85fn 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
76425d84
DC
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 },
801ec1db
SR
130 "driver": {
131 type: BlockDriverType,
132 optional: true,
133 },
9fe3358c
SR
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,
76425d84
DC
145 }
146 }
147)]
148/// List a directory from a backup snapshot.
149async fn list(
150 snapshot: String,
151 path: String,
152 base64: bool,
153 param: Value,
9fe3358c 154) -> Result<(), Error> {
76425d84
DC
155 let repo = extract_repository_from_value(&param)?;
156 let snapshot: BackupDir = snapshot.parse()?;
157 let path = parse_path(path, base64)?;
158
15998ed1
SR
159 let keyfile = keyfile_path(&param);
160 let crypto = crypto_parameters_keep_fd(&param)?;
76425d84
DC
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
9fe3358c 188 let result = match path {
76425d84
DC
189 ExtractPath::ListArchives => {
190 let mut entries = vec![];
191 for file in manifest.files() {
2fd2d292
SR
192 if !file.filename.ends_with(".pxar.didx") && !file.filename.ends_with(".img.fidx") {
193 continue;
76425d84
DC
194 }
195 let path = format!("/{}", file.filename);
4d0dc299
SR
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 };
6a59fa0e 202 entries.push(ArchiveEntry::new_with_size(path.as_bytes(), attr, Some(file.size)));
76425d84
DC
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
86582454 225 catalog_reader.list_dir_contents(&fullpath)
76425d84 226 }
801ec1db
SR
227 ExtractPath::VM(file, path) => {
228 let details = SnapRestoreDetails {
229 manifest,
230 repo,
231 snapshot,
15998ed1 232 keyfile,
801ec1db
SR
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 }
9fe3358c
SR
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(())
76425d84
DC
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,
b13089cd
SR
304 },
305 "driver": {
306 type: BlockDriverType,
307 optional: true,
308 },
76425d84
DC
309 }
310 }
311)]
312/// Restore files from a backup snapshot.
313async 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
15998ed1
SR
332 let keyfile = keyfile_path(&param);
333 let crypto = crypto_parameters_keep_fd(&param)?;
76425d84
DC
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
b13089cd
SR
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
76425d84
DC
359 match path {
360 ExtractPath::Pxar(archive_name, path) => {
76425d84
DC
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?;
b13089cd
SR
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,
15998ed1 384 keyfile,
b13089cd
SR
385 };
386 let driver: Option<BlockDriverType> = match param.get("driver") {
387 Some(drv) => Some(serde_json::from_value(drv.clone())?),
388 None => None,
389 };
76425d84 390
b13089cd
SR
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?;
76425d84 395
b13089cd
SR
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 })?;
76425d84 402 } else {
b13089cd
SR
403 let mut reader = data_extract(driver, details, file, path.clone(), false).await?;
404 tokio::io::copy(&mut reader, &mut tokio::io::stdout()).await?;
76425d84
DC
405 }
406 }
407 _ => {
408 bail!("cannot extract '{}'", orig_path);
409 }
410 }
411
412 Ok(())
413}
414
b13089cd
SR
415async fn extract_to_target<T>(
416 decoder: Accessor<T>,
417 path: &[u8],
418 target: Option<PathBuf>,
419 verbose: bool,
420) -> Result<(), Error>
421where
422 T: pxar::accessor::ReadAt + Clone + Send + Sync + Unpin + 'static,
423{
4adf47b6
SR
424 let path = if path.is_empty() { b"/" } else { path };
425
b13089cd
SR
426 let root = decoder.open_root().await?;
427 let file = root
4adf47b6 428 .lookup(OsStr::from_bytes(path))
b13089cd
SR
429 .await?
430 .ok_or_else(|| format_err!("error opening '{:?}'", path))?;
431
432 if let Some(target) = target {
4adf47b6 433 extract_sub_dir(target, decoder, OsStr::from_bytes(path), verbose).await?;
b13089cd
SR
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,
4adf47b6 443 OsStr::from_bytes(path),
b13089cd
SR
444 verbose,
445 )
446 .await?;
447 }
448 }
449 }
450
451 Ok(())
452}
453
76425d84
DC
454fn 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)
2b7f8dd5 464 .completion_cb("target", pbs_tools::fs::complete_file_name);
76425d84 465
58421ec1
SR
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
76425d84
DC
471 let cmd_def = CliCommandMap::new()
472 .insert("list", list_cmd_def)
58421ec1
SR
473 .insert("extract", restore_cmd_def)
474 .insert("status", status_cmd_def)
475 .insert("stop", stop_cmd_def);
76425d84
DC
476
477 let rpcenv = CliEnvironment::new();
478 run_cli_command(
479 cmd_def,
480 rpcenv,
d420962f 481 Some(|future| pbs_runtime::main(future)),
76425d84
DC
482 );
483}
2b7f8dd5
WB
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.
488pub 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());
6c76aa43 492 create_run_dir()?;
2b7f8dd5
WB
493 std::fs::create_dir_all(&path)?;
494 Ok(path)
495}
6c76aa43
WB
496
497/// FIXME: proxmox-file-restore should not depend on this!
498fn 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)``)
508pub fn backup_user() -> Result<nix::unistd::User, Error> {
509 pbs_tools::sys::query_user(pbs_buildcfg::BACKUP_USER_NAME)?
510 .ok_or_else(|| format_err!("Unable to lookup '{}' user.", pbs_buildcfg::BACKUP_USER_NAME))
511}
512