]>
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 | 8 | |
6ef1b649 | 9 | use 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 | 14 | use proxmox_router::{http_err, HttpError}; |
6ef1b649 | 15 | use proxmox_schema::api; |
40ea990c | 16 | use proxmox_sys::fs::{create_path, CreateOptions}; |
76425d84 | 17 | use pxar::accessor::aio::Accessor; |
b13089cd | 18 | use pxar::decoder::aio::Decoder; |
76425d84 | 19 | |
133d718f | 20 | use pbs_api_types::{BackupDir, BackupNamespace, CryptMode}; |
2b7f8dd5 WB |
21 | use pbs_client::pxar::{create_zip, extract_sub_dir, extract_sub_dir_seq}; |
22 | use 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 | 30 | use pbs_client::{BackupReader, BackupRepository, RemoteChunkReader}; |
40ea990c | 31 | use pbs_config::key_config::decrypt_key; |
40ea990c TL |
32 | use pbs_datastore::catalog::{ArchiveEntry, CatalogReader, DirEntryAttribute}; |
33 | use pbs_datastore::dynamic_index::{BufferedDynamicReader, LocalDynamicReadAt}; | |
34 | use pbs_datastore::index::IndexFile; | |
35 | use pbs_datastore::CATALOG_NAME; | |
36 | use pbs_tools::crypt_config::CryptConfig; | |
76425d84 | 37 | |
6c76aa43 WB |
38 | pub mod block_driver; |
39 | pub use block_driver::*; | |
2b7f8dd5 | 40 | |
6c76aa43 WB |
41 | pub mod cpio; |
42 | ||
6c76aa43 | 43 | mod block_driver_qemu; |
40ea990c | 44 | mod qemu_helper; |
58421ec1 | 45 | |
76425d84 DC |
46 | enum ExtractPath { |
47 | ListArchives, | |
48 | Pxar(String, Vec<u8>), | |
801ec1db | 49 | VM(String, Vec<u8>), |
76425d84 DC |
50 | } |
51 | ||
52 | fn 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 |
84 | fn 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 |
96 | async 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 | 242 | async 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(¶m)?; |
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(¶m); |
257 | let crypto = crypto_parameters_keep_fd(¶m)?; | |
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(¶m); | |
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(¶m); | |
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. | |
388 | async 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(¶m)?; | |
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(¶m); |
410 | let crypto = crypto_parameters_keep_fd(¶m)?; | |
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 |
491 | async fn extract_to_target<T>( |
492 | decoder: Accessor<T>, | |
493 | path: &[u8], | |
494 | target: Option<PathBuf>, | |
495 | verbose: bool, | |
496 | ) -> Result<(), Error> | |
497 | where | |
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 |
530 | fn 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. | |
564 | pub 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! | |
574 | fn 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)``) | |
584 | pub 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 | } |