]>
Commit | Line | Data |
---|---|---|
43abba4b DM |
1 | use std::path::PathBuf; |
2 | use std::sync::Arc; | |
3 | use std::os::unix::io::RawFd; | |
4 | use std::path::Path; | |
5 | use std::ffi::OsStr; | |
6 | ||
7 | use anyhow::{bail, format_err, Error}; | |
8 | use serde_json::Value; | |
9 | use tokio::signal::unix::{signal, SignalKind}; | |
10 | use nix::unistd::{fork, ForkResult, pipe}; | |
11 | use futures::select; | |
12 | use futures::future::FutureExt; | |
13 | ||
14 | use proxmox::{sortable, identity}; | |
cc7995ac | 15 | use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment, schema::*, cli::*}; |
43abba4b DM |
16 | |
17 | ||
18 | use proxmox_backup::tools; | |
19 | use proxmox_backup::backup::{ | |
20 | load_and_decrypt_key, | |
21 | CryptConfig, | |
22 | IndexFile, | |
23 | BackupDir, | |
24 | BackupGroup, | |
25 | BufferedDynamicReader, | |
26 | }; | |
27 | ||
28 | use proxmox_backup::client::*; | |
43abba4b DM |
29 | |
30 | use crate::{ | |
31 | REPO_URL_SCHEMA, | |
32 | extract_repository_from_value, | |
43abba4b DM |
33 | complete_pxar_archive_name, |
34 | complete_group_or_snapshot, | |
35 | complete_repository, | |
36 | record_repository, | |
37 | connect, | |
38 | api_datastore_latest_snapshot, | |
39 | BufferedDynamicReadAt, | |
40 | }; | |
41 | ||
42 | #[sortable] | |
43 | const API_METHOD_MOUNT: ApiMethod = ApiMethod::new( | |
44 | &ApiHandler::Sync(&mount), | |
45 | &ObjectSchema::new( | |
46 | "Mount pxar archive.", | |
47 | &sorted!([ | |
48 | ("snapshot", false, &StringSchema::new("Group/Snapshot path.").schema()), | |
49 | ("archive-name", false, &StringSchema::new("Backup archive name.").schema()), | |
50 | ("target", false, &StringSchema::new("Target directory path.").schema()), | |
51 | ("repository", true, &REPO_URL_SCHEMA), | |
52 | ("keyfile", true, &StringSchema::new("Path to encryption key.").schema()), | |
53 | ("verbose", true, &BooleanSchema::new("Verbose output.").default(false).schema()), | |
54 | ]), | |
55 | ) | |
56 | ); | |
57 | ||
58 | pub fn mount_cmd_def() -> CliCommand { | |
59 | ||
60 | CliCommand::new(&API_METHOD_MOUNT) | |
61 | .arg_param(&["snapshot", "archive-name", "target"]) | |
62 | .completion_cb("repository", complete_repository) | |
63 | .completion_cb("snapshot", complete_group_or_snapshot) | |
64 | .completion_cb("archive-name", complete_pxar_archive_name) | |
65 | .completion_cb("target", tools::complete_file_name) | |
66 | } | |
67 | ||
68 | fn mount( | |
69 | param: Value, | |
70 | _info: &ApiMethod, | |
71 | _rpcenv: &mut dyn RpcEnvironment, | |
72 | ) -> Result<Value, Error> { | |
73 | ||
74 | let verbose = param["verbose"].as_bool().unwrap_or(false); | |
75 | if verbose { | |
76 | // This will stay in foreground with debug output enabled as None is | |
77 | // passed for the RawFd. | |
78 | return proxmox_backup::tools::runtime::main(mount_do(param, None)); | |
79 | } | |
80 | ||
81 | // Process should be deamonized. | |
82 | // Make sure to fork before the async runtime is instantiated to avoid troubles. | |
83 | let pipe = pipe()?; | |
84 | match fork() { | |
85 | Ok(ForkResult::Parent { .. }) => { | |
86 | nix::unistd::close(pipe.1).unwrap(); | |
87 | // Blocks the parent process until we are ready to go in the child | |
88 | let _res = nix::unistd::read(pipe.0, &mut [0]).unwrap(); | |
89 | Ok(Value::Null) | |
90 | } | |
91 | Ok(ForkResult::Child) => { | |
92 | nix::unistd::close(pipe.0).unwrap(); | |
93 | nix::unistd::setsid().unwrap(); | |
94 | proxmox_backup::tools::runtime::main(mount_do(param, Some(pipe.1))) | |
95 | } | |
96 | Err(_) => bail!("failed to daemonize process"), | |
97 | } | |
98 | } | |
99 | ||
100 | async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> { | |
101 | let repo = extract_repository_from_value(¶m)?; | |
102 | let archive_name = tools::required_string_param(¶m, "archive-name")?; | |
103 | let target = tools::required_string_param(¶m, "target")?; | |
104 | let client = connect(repo.host(), repo.user())?; | |
105 | ||
106 | record_repository(&repo); | |
107 | ||
108 | let path = tools::required_string_param(¶m, "snapshot")?; | |
109 | let (backup_type, backup_id, backup_time) = if path.matches('/').count() == 1 { | |
110 | let group: BackupGroup = path.parse()?; | |
111 | api_datastore_latest_snapshot(&client, repo.store(), group).await? | |
112 | } else { | |
113 | let snapshot: BackupDir = path.parse()?; | |
114 | (snapshot.group().backup_type().to_owned(), snapshot.group().backup_id().to_owned(), snapshot.backup_time()) | |
115 | }; | |
116 | ||
117 | let keyfile = param["keyfile"].as_str().map(PathBuf::from); | |
118 | let crypt_config = match keyfile { | |
119 | None => None, | |
120 | Some(path) => { | |
9696f519 | 121 | let (key, _) = load_and_decrypt_key(&path, &crate::key::get_encryption_key_password)?; |
43abba4b DM |
122 | Some(Arc::new(CryptConfig::new(key)?)) |
123 | } | |
124 | }; | |
125 | ||
126 | let server_archive_name = if archive_name.ends_with(".pxar") { | |
127 | format!("{}.didx", archive_name) | |
128 | } else { | |
129 | bail!("Can only mount pxar archives."); | |
130 | }; | |
131 | ||
132 | let client = BackupReader::start( | |
133 | client, | |
134 | crypt_config.clone(), | |
135 | repo.store(), | |
136 | &backup_type, | |
137 | &backup_id, | |
138 | backup_time, | |
139 | true, | |
140 | ).await?; | |
141 | ||
142 | let manifest = client.download_manifest().await?; | |
143 | ||
144 | if server_archive_name.ends_with(".didx") { | |
145 | let index = client.download_dynamic_index(&manifest, &server_archive_name).await?; | |
146 | let most_used = index.find_most_used_chunks(8); | |
147 | let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, most_used); | |
148 | let reader = BufferedDynamicReader::new(index, chunk_reader); | |
149 | let archive_size = reader.archive_size(); | |
150 | let reader: proxmox_backup::pxar::fuse::Reader = | |
151 | Arc::new(BufferedDynamicReadAt::new(reader)); | |
152 | let decoder = proxmox_backup::pxar::fuse::Accessor::new(reader, archive_size).await?; | |
153 | let options = OsStr::new("ro,default_permissions"); | |
154 | ||
155 | let session = proxmox_backup::pxar::fuse::Session::mount( | |
156 | decoder, | |
157 | &options, | |
158 | false, | |
159 | Path::new(target), | |
160 | ) | |
161 | .map_err(|err| format_err!("pxar mount failed: {}", err))?; | |
162 | ||
163 | if let Some(pipe) = pipe { | |
164 | nix::unistd::chdir(Path::new("/")).unwrap(); | |
165 | // Finish creation of daemon by redirecting filedescriptors. | |
166 | let nullfd = nix::fcntl::open( | |
167 | "/dev/null", | |
168 | nix::fcntl::OFlag::O_RDWR, | |
169 | nix::sys::stat::Mode::empty(), | |
170 | ).unwrap(); | |
171 | nix::unistd::dup2(nullfd, 0).unwrap(); | |
172 | nix::unistd::dup2(nullfd, 1).unwrap(); | |
173 | nix::unistd::dup2(nullfd, 2).unwrap(); | |
174 | if nullfd > 2 { | |
175 | nix::unistd::close(nullfd).unwrap(); | |
176 | } | |
177 | // Signal the parent process that we are done with the setup and it can | |
178 | // terminate. | |
179 | nix::unistd::write(pipe, &[0u8])?; | |
180 | nix::unistd::close(pipe).unwrap(); | |
181 | } | |
182 | ||
183 | let mut interrupt = signal(SignalKind::interrupt())?; | |
184 | select! { | |
185 | res = session.fuse() => res?, | |
186 | _ = interrupt.recv().fuse() => { | |
187 | // exit on interrupted | |
188 | } | |
189 | } | |
190 | } else { | |
191 | bail!("unknown archive file extension (expected .pxar)"); | |
192 | } | |
193 | ||
194 | Ok(Value::Null) | |
195 | } |