]>
Commit | Line | Data |
---|---|---|
76425d84 DC |
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}; | |
9fe3358c | 7 | use serde_json::{json, Value}; |
76425d84 DC |
8 | |
9 | use 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 | 16 | use proxmox::tools::fs::{create_path, CreateOptions}; |
76425d84 | 17 | use pxar::accessor::aio::Accessor; |
b13089cd | 18 | use pxar::decoder::aio::Decoder; |
76425d84 | 19 | |
bbdda58b | 20 | use pbs_tools::crypt_config::CryptConfig; |
b2065dc7 | 21 | use pbs_api_types::CryptMode; |
bbdda58b | 22 | use pbs_datastore::CATALOG_NAME; |
b2065dc7 | 23 | use pbs_datastore::backup_info::BackupDir; |
013b1e8b | 24 | use pbs_datastore::catalog::{ArchiveEntry, CatalogReader, DirEntryAttribute}; |
b2065dc7 | 25 | use pbs_datastore::dynamic_index::{BufferedDynamicReader, LocalDynamicReadAt}; |
2b7f8dd5 | 26 | use pbs_datastore::index::IndexFile; |
bbdda58b | 27 | use pbs_config::key_config::decrypt_key; |
2b7f8dd5 WB |
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::{ | |
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 |
39 | pub mod block_driver; |
40 | pub use block_driver::*; | |
2b7f8dd5 | 41 | |
6c76aa43 WB |
42 | pub mod cpio; |
43 | ||
44 | mod qemu_helper; | |
45 | mod block_driver_qemu; | |
58421ec1 | 46 | |
76425d84 DC |
47 | enum ExtractPath { |
48 | ListArchives, | |
49 | Pxar(String, Vec<u8>), | |
801ec1db | 50 | VM(String, Vec<u8>), |
76425d84 DC |
51 | } |
52 | ||
53 | fn 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 |
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 | ||
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. | |
149 | async 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(¶m)?; |
156 | let snapshot: BackupDir = snapshot.parse()?; | |
157 | let path = parse_path(path, base64)?; | |
158 | ||
15998ed1 SR |
159 | let keyfile = keyfile_path(¶m); |
160 | let crypto = crypto_parameters_keep_fd(¶m)?; | |
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(¶m); | |
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. | |
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(¶m)?; | |
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(¶m); |
333 | let crypto = crypto_parameters_keep_fd(¶m)?; | |
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 |
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 | { | |
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 |
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) | |
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. | |
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()); | |
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! | |
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 | 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 |