]>
Commit | Line | Data |
---|---|---|
9de69cdb DM |
1 | use std::os::unix::fs::OpenOptionsExt; |
2 | use std::io::{Seek, SeekFrom}; | |
9de69cdb DM |
3 | use std::sync::Arc; |
4 | ||
5 | use anyhow::{bail, format_err, Error}; | |
6 | use serde_json::Value; | |
7 | ||
8 | use proxmox::api::{api, cli::*}; | |
9 | ||
10 | use proxmox_backup::tools; | |
11 | ||
12 | use proxmox_backup::client::*; | |
13 | ||
14 | use crate::{ | |
15 | REPO_URL_SCHEMA, | |
d86034af | 16 | KEYFD_SCHEMA, |
9de69cdb DM |
17 | extract_repository_from_value, |
18 | record_repository, | |
d86034af DM |
19 | key::get_encryption_key_password, |
20 | decrypt_key, | |
9de69cdb DM |
21 | api_datastore_latest_snapshot, |
22 | complete_repository, | |
23 | complete_backup_snapshot, | |
24 | complete_group_or_snapshot, | |
25 | complete_pxar_archive_name, | |
26 | connect, | |
c6a7ea0a | 27 | crypto_parameters, |
9de69cdb DM |
28 | BackupDir, |
29 | BackupGroup, | |
30 | BufferedDynamicReader, | |
31 | BufferedDynamicReadAt, | |
32 | CatalogReader, | |
33 | CATALOG_NAME, | |
34 | CryptConfig, | |
35 | DynamicIndexReader, | |
36 | IndexFile, | |
37 | Shell, | |
38 | }; | |
39 | ||
9de69cdb DM |
40 | #[api( |
41 | input: { | |
42 | properties: { | |
43 | repository: { | |
44 | schema: REPO_URL_SCHEMA, | |
45 | optional: true, | |
46 | }, | |
47 | snapshot: { | |
48 | type: String, | |
49 | description: "Snapshot path.", | |
50 | }, | |
d86034af DM |
51 | "keyfile": { |
52 | optional: true, | |
53 | type: String, | |
54 | description: "Path to encryption key.", | |
55 | }, | |
56 | "keyfd": { | |
57 | schema: KEYFD_SCHEMA, | |
58 | optional: true, | |
59 | }, | |
9de69cdb DM |
60 | } |
61 | } | |
62 | )] | |
63 | /// Dump catalog. | |
64 | async fn dump_catalog(param: Value) -> Result<Value, Error> { | |
65 | ||
66 | let repo = extract_repository_from_value(¶m)?; | |
67 | ||
68 | let path = tools::required_string_param(¶m, "snapshot")?; | |
69 | let snapshot: BackupDir = path.parse()?; | |
70 | ||
c6a7ea0a | 71 | let crypto = crypto_parameters(¶m)?; |
9de69cdb | 72 | |
c6a7ea0a | 73 | let crypt_config = match crypto.enc_key { |
9de69cdb | 74 | None => None, |
d86034af | 75 | Some(key) => { |
37e60ddc | 76 | let (key, _created, _fingerprint) = decrypt_key(&key, &get_encryption_key_password)?; |
d86034af DM |
77 | let crypt_config = CryptConfig::new(key)?; |
78 | Some(Arc::new(crypt_config)) | |
9de69cdb DM |
79 | } |
80 | }; | |
81 | ||
f3fde36b | 82 | let client = connect(&repo)?; |
9de69cdb DM |
83 | |
84 | let client = BackupReader::start( | |
85 | client, | |
86 | crypt_config.clone(), | |
87 | repo.store(), | |
88 | &snapshot.group().backup_type(), | |
89 | &snapshot.group().backup_id(), | |
90 | snapshot.backup_time(), | |
91 | true, | |
92 | ).await?; | |
93 | ||
2107a5ae | 94 | let (manifest, _) = client.download_manifest().await?; |
23f9503a | 95 | manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?; |
9de69cdb DM |
96 | |
97 | let index = client.download_dynamic_index(&manifest, CATALOG_NAME).await?; | |
98 | ||
99 | let most_used = index.find_most_used_chunks(8); | |
100 | ||
14f6c9cb FG |
101 | let file_info = manifest.lookup_file_info(&CATALOG_NAME)?; |
102 | ||
103 | let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used); | |
9de69cdb DM |
104 | |
105 | let mut reader = BufferedDynamicReader::new(index, chunk_reader); | |
106 | ||
107 | let mut catalogfile = std::fs::OpenOptions::new() | |
108 | .write(true) | |
109 | .read(true) | |
110 | .custom_flags(libc::O_TMPFILE) | |
111 | .open("/tmp")?; | |
112 | ||
113 | std::io::copy(&mut reader, &mut catalogfile) | |
114 | .map_err(|err| format_err!("unable to download catalog - {}", err))?; | |
115 | ||
116 | catalogfile.seek(SeekFrom::Start(0))?; | |
117 | ||
118 | let mut catalog_reader = CatalogReader::new(catalogfile); | |
119 | ||
120 | catalog_reader.dump()?; | |
121 | ||
122 | record_repository(&repo); | |
123 | ||
124 | Ok(Value::Null) | |
125 | } | |
126 | ||
127 | #[api( | |
128 | input: { | |
129 | properties: { | |
130 | "snapshot": { | |
131 | type: String, | |
132 | description: "Group/Snapshot path.", | |
133 | }, | |
134 | "archive-name": { | |
135 | type: String, | |
136 | description: "Backup archive name.", | |
137 | }, | |
138 | "repository": { | |
139 | optional: true, | |
140 | schema: REPO_URL_SCHEMA, | |
141 | }, | |
142 | "keyfile": { | |
143 | optional: true, | |
144 | type: String, | |
145 | description: "Path to encryption key.", | |
146 | }, | |
d86034af DM |
147 | "keyfd": { |
148 | schema: KEYFD_SCHEMA, | |
149 | optional: true, | |
150 | }, | |
151 | }, | |
9de69cdb DM |
152 | }, |
153 | )] | |
154 | /// Shell to interactively inspect and restore snapshots. | |
155 | async fn catalog_shell(param: Value) -> Result<(), Error> { | |
156 | let repo = extract_repository_from_value(¶m)?; | |
f3fde36b | 157 | let client = connect(&repo)?; |
9de69cdb DM |
158 | let path = tools::required_string_param(¶m, "snapshot")?; |
159 | let archive_name = tools::required_string_param(¶m, "archive-name")?; | |
160 | ||
161 | let (backup_type, backup_id, backup_time) = if path.matches('/').count() == 1 { | |
162 | let group: BackupGroup = path.parse()?; | |
163 | api_datastore_latest_snapshot(&client, repo.store(), group).await? | |
164 | } else { | |
165 | let snapshot: BackupDir = path.parse()?; | |
166 | (snapshot.group().backup_type().to_owned(), snapshot.group().backup_id().to_owned(), snapshot.backup_time()) | |
167 | }; | |
168 | ||
c6a7ea0a | 169 | let crypto = crypto_parameters(¶m)?; |
d86034af | 170 | |
c6a7ea0a | 171 | let crypt_config = match crypto.enc_key { |
9de69cdb | 172 | None => None, |
d86034af | 173 | Some(key) => { |
37e60ddc | 174 | let (key, _created, _fingerprint) = decrypt_key(&key, &get_encryption_key_password)?; |
d86034af DM |
175 | let crypt_config = CryptConfig::new(key)?; |
176 | Some(Arc::new(crypt_config)) | |
9de69cdb DM |
177 | } |
178 | }; | |
179 | ||
180 | let server_archive_name = if archive_name.ends_with(".pxar") { | |
181 | format!("{}.didx", archive_name) | |
182 | } else { | |
183 | bail!("Can only mount pxar archives."); | |
184 | }; | |
185 | ||
186 | let client = BackupReader::start( | |
187 | client, | |
188 | crypt_config.clone(), | |
189 | repo.store(), | |
190 | &backup_type, | |
191 | &backup_id, | |
192 | backup_time, | |
193 | true, | |
194 | ).await?; | |
195 | ||
196 | let mut tmpfile = std::fs::OpenOptions::new() | |
197 | .write(true) | |
198 | .read(true) | |
199 | .custom_flags(libc::O_TMPFILE) | |
200 | .open("/tmp")?; | |
201 | ||
2107a5ae | 202 | let (manifest, _) = client.download_manifest().await?; |
23f9503a | 203 | manifest.check_fingerprint(crypt_config.as_ref().map(Arc::as_ref))?; |
9de69cdb DM |
204 | |
205 | let index = client.download_dynamic_index(&manifest, &server_archive_name).await?; | |
206 | let most_used = index.find_most_used_chunks(8); | |
14f6c9cb FG |
207 | |
208 | let file_info = manifest.lookup_file_info(&server_archive_name)?; | |
209 | let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config.clone(), file_info.chunk_crypt_mode(), most_used); | |
9de69cdb DM |
210 | let reader = BufferedDynamicReader::new(index, chunk_reader); |
211 | let archive_size = reader.archive_size(); | |
212 | let reader: proxmox_backup::pxar::fuse::Reader = | |
213 | Arc::new(BufferedDynamicReadAt::new(reader)); | |
214 | let decoder = proxmox_backup::pxar::fuse::Accessor::new(reader, archive_size).await?; | |
215 | ||
216 | client.download(CATALOG_NAME, &mut tmpfile).await?; | |
217 | let index = DynamicIndexReader::new(tmpfile) | |
218 | .map_err(|err| format_err!("unable to read catalog index - {}", err))?; | |
219 | ||
220 | // Note: do not use values stored in index (not trusted) - instead, computed them again | |
221 | let (csum, size) = index.compute_csum(); | |
222 | manifest.verify_file(CATALOG_NAME, &csum, size)?; | |
223 | ||
224 | let most_used = index.find_most_used_chunks(8); | |
14f6c9cb FG |
225 | |
226 | let file_info = manifest.lookup_file_info(&CATALOG_NAME)?; | |
227 | let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used); | |
9de69cdb DM |
228 | let mut reader = BufferedDynamicReader::new(index, chunk_reader); |
229 | let mut catalogfile = std::fs::OpenOptions::new() | |
230 | .write(true) | |
231 | .read(true) | |
232 | .custom_flags(libc::O_TMPFILE) | |
233 | .open("/tmp")?; | |
234 | ||
235 | std::io::copy(&mut reader, &mut catalogfile) | |
236 | .map_err(|err| format_err!("unable to download catalog - {}", err))?; | |
237 | ||
238 | catalogfile.seek(SeekFrom::Start(0))?; | |
239 | let catalog_reader = CatalogReader::new(catalogfile); | |
240 | let state = Shell::new( | |
241 | catalog_reader, | |
242 | &server_archive_name, | |
243 | decoder, | |
244 | ).await?; | |
245 | ||
246 | println!("Starting interactive shell"); | |
247 | state.shell().await?; | |
248 | ||
249 | record_repository(&repo); | |
250 | ||
251 | Ok(()) | |
252 | } | |
253 | ||
254 | pub fn catalog_mgmt_cli() -> CliCommandMap { | |
255 | let catalog_shell_cmd_def = CliCommand::new(&API_METHOD_CATALOG_SHELL) | |
256 | .arg_param(&["snapshot", "archive-name"]) | |
257 | .completion_cb("repository", complete_repository) | |
258 | .completion_cb("archive-name", complete_pxar_archive_name) | |
259 | .completion_cb("snapshot", complete_group_or_snapshot); | |
260 | ||
261 | let catalog_dump_cmd_def = CliCommand::new(&API_METHOD_DUMP_CATALOG) | |
262 | .arg_param(&["snapshot"]) | |
263 | .completion_cb("repository", complete_repository) | |
264 | .completion_cb("snapshot", complete_backup_snapshot); | |
265 | ||
266 | CliCommandMap::new() | |
267 | .insert("dump", catalog_dump_cmd_def) | |
268 | .insert("shell", catalog_shell_cmd_def) | |
269 | } |