1 //! Shared tools useful for common CLI clients.
2 use std
::collections
::HashMap
;
4 use anyhow
::{bail, format_err, Context, Error}
;
5 use serde_json
::{json, Value}
;
6 use xdg
::BaseDirectories
;
10 tools
::fs
::file_get_json
,
13 use pbs_api_types
::{BACKUP_REPO_URL, Authid, UserWithTokens}
;
14 use pbs_datastore
::BackupDir
;
15 use pbs_tools
::json
::json_object_to_query
;
17 use crate::{BackupRepository, HttpClient, HttpClientOptions}
;
21 const ENV_VAR_PBS_FINGERPRINT
: &str = "PBS_FINGERPRINT";
22 const ENV_VAR_PBS_PASSWORD
: &str = "PBS_PASSWORD";
24 pub const REPO_URL_SCHEMA
: Schema
= StringSchema
::new("Repository URL.")
25 .format(&BACKUP_REPO_URL
)
29 pub const CHUNK_SIZE_SCHEMA
: Schema
= IntegerSchema
::new("Chunk size in KB. Must be a power of 2.")
35 pub fn get_default_repository() -> Option
<String
> {
36 std
::env
::var("PBS_REPOSITORY").ok()
39 pub fn extract_repository_from_value(param
: &Value
) -> Result
<BackupRepository
, Error
> {
40 let repo_url
= param
["repository"]
43 .or_else(get_default_repository
)
44 .ok_or_else(|| format_err
!("unable to get (default) repository"))?
;
46 let repo
: BackupRepository
= repo_url
.parse()?
;
51 pub fn extract_repository_from_map(param
: &HashMap
<String
, String
>) -> Option
<BackupRepository
> {
55 .or_else(get_default_repository
)
56 .and_then(|repo_url
| repo_url
.parse
::<BackupRepository
>().ok())
59 pub fn connect(repo
: &BackupRepository
) -> Result
<HttpClient
, Error
> {
60 connect_do(repo
.host(), repo
.port(), repo
.auth_id())
61 .map_err(|err
| format_err
!("error building client for repository {} - {}", repo
, err
))
64 fn connect_do(server
: &str, port
: u16, auth_id
: &Authid
) -> Result
<HttpClient
, Error
> {
65 let fingerprint
= std
::env
::var(ENV_VAR_PBS_FINGERPRINT
).ok();
67 use std
::env
::VarError
::*;
68 let password
= match std
::env
::var(ENV_VAR_PBS_PASSWORD
) {
70 Err(NotUnicode(_
)) => bail
!(format
!("{} contains bad characters", ENV_VAR_PBS_PASSWORD
)),
71 Err(NotPresent
) => None
,
74 let options
= HttpClientOptions
::new_interactive(password
, fingerprint
);
76 HttpClient
::new(server
, port
, auth_id
, options
)
79 /// like get, but simply ignore errors and return Null instead
80 pub async
fn try_get(repo
: &BackupRepository
, url
: &str) -> Value
{
82 let fingerprint
= std
::env
::var(ENV_VAR_PBS_FINGERPRINT
).ok();
83 let password
= std
::env
::var(ENV_VAR_PBS_PASSWORD
).ok();
85 // ticket cache, but no questions asked
86 let options
= HttpClientOptions
::new_interactive(password
, fingerprint
)
89 let client
= match HttpClient
::new(repo
.host(), repo
.port(), repo
.auth_id(), options
) {
91 _
=> return Value
::Null
,
94 let mut resp
= match client
.get(url
, None
).await
{
96 _
=> return Value
::Null
,
99 if let Some(map
) = resp
.as_object_mut() {
100 if let Some(data
) = map
.remove("data") {
107 pub fn complete_backup_group(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
108 pbs_runtime
::main(async { complete_backup_group_do(param).await }
)
111 pub async
fn complete_backup_group_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
113 let mut result
= vec
![];
115 let repo
= match extract_repository_from_map(param
) {
120 let path
= format
!("api2/json/admin/datastore/{}/groups", repo
.store());
122 let data
= try_get(&repo
, &path
).await
;
124 if let Some(list
) = data
.as_array() {
126 if let (Some(backup_id
), Some(backup_type
)) =
127 (item
["backup-id"].as_str(), item
["backup-type"].as_str())
129 result
.push(format
!("{}/{}", backup_type
, backup_id
));
137 pub fn complete_group_or_snapshot(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
138 pbs_runtime
::main(async { complete_group_or_snapshot_do(arg, param).await }
)
141 pub async
fn complete_group_or_snapshot_do(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
143 if arg
.matches('
/'
).count() < 2 {
144 let groups
= complete_backup_group_do(param
).await
;
145 let mut result
= vec
![];
146 for group
in groups
{
147 result
.push(group
.to_string());
148 result
.push(format
!("{}/", group
));
153 complete_backup_snapshot_do(param
).await
156 pub fn complete_backup_snapshot(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
157 pbs_runtime
::main(async { complete_backup_snapshot_do(param).await }
)
160 pub async
fn complete_backup_snapshot_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
162 let mut result
= vec
![];
164 let repo
= match extract_repository_from_map(param
) {
169 let path
= format
!("api2/json/admin/datastore/{}/snapshots", repo
.store());
171 let data
= try_get(&repo
, &path
).await
;
173 if let Some(list
) = data
.as_array() {
175 if let (Some(backup_id
), Some(backup_type
), Some(backup_time
)) =
176 (item
["backup-id"].as_str(), item
["backup-type"].as_str(), item
["backup-time"].as_i64())
178 if let Ok(snapshot
) = BackupDir
::new(backup_type
, backup_id
, backup_time
) {
179 result
.push(snapshot
.relative_path().to_str().unwrap().to_owned());
188 pub fn complete_server_file_name(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
189 pbs_runtime
::main(async { complete_server_file_name_do(param).await }
)
192 pub async
fn complete_server_file_name_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
194 let mut result
= vec
![];
196 let repo
= match extract_repository_from_map(param
) {
201 let snapshot
: BackupDir
= match param
.get("snapshot") {
211 let query
= json_object_to_query(json
!({
212 "backup-type": snapshot
.group().backup_type(),
213 "backup-id": snapshot
.group().backup_id(),
214 "backup-time": snapshot
.backup_time(),
217 let path
= format
!("api2/json/admin/datastore/{}/files?{}", repo
.store(), query
);
219 let data
= try_get(&repo
, &path
).await
;
221 if let Some(list
) = data
.as_array() {
223 if let Some(filename
) = item
["filename"].as_str() {
224 result
.push(filename
.to_owned());
232 pub fn complete_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
233 complete_server_file_name(arg
, param
)
235 .map(|v
| pbs_tools
::format
::strip_server_file_extension(&v
))
239 pub fn complete_pxar_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
240 complete_server_file_name(arg
, param
)
243 if name
.ends_with(".pxar.didx") {
244 Some(pbs_tools
::format
::strip_server_file_extension(name
))
252 pub fn complete_img_archive_name(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
253 complete_server_file_name(arg
, param
)
256 if name
.ends_with(".img.fidx") {
257 Some(pbs_tools
::format
::strip_server_file_extension(name
))
265 pub fn complete_chunk_size(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
267 let mut result
= vec
![];
271 result
.push(size
.to_string());
273 if size
> 4096 { break; }
279 pub fn complete_auth_id(_arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
280 pbs_runtime
::main(async { complete_auth_id_do(param).await }
)
283 pub async
fn complete_auth_id_do(param
: &HashMap
<String
, String
>) -> Vec
<String
> {
285 let mut result
= vec
![];
287 let repo
= match extract_repository_from_map(param
) {
292 let data
= try_get(&repo
, "api2/json/access/users?include_tokens=true").await
;
294 if let Ok(parsed
) = serde_json
::from_value
::<Vec
<UserWithTokens
>>(data
) {
296 result
.push(user
.userid
.to_string());
297 for token
in user
.tokens
{
298 result
.push(token
.tokenid
.to_string());
306 pub fn complete_repository(_arg
: &str, _param
: &HashMap
<String
, String
>) -> Vec
<String
> {
307 let mut result
= vec
![];
309 let base
= match BaseDirectories
::with_prefix("proxmox-backup") {
314 // usually $HOME/.cache/proxmox-backup/repo-list
315 let path
= match base
.place_cache_file("repo-list") {
320 let data
= file_get_json(&path
, None
).unwrap_or_else(|_
| json
!({}
));
322 if let Some(map
) = data
.as_object() {
323 for (repo
, _count
) in map
{
324 result
.push(repo
.to_owned());
331 pub fn complete_backup_source(arg
: &str, param
: &HashMap
<String
, String
>) -> Vec
<String
> {
332 let mut result
= vec
![];
334 let data
: Vec
<&str> = arg
.splitn(2, '
:'
).collect();
337 result
.push(String
::from("root.pxar:/"));
338 result
.push(String
::from("etc.pxar:/etc"));
342 let files
= pbs_tools
::fs
::complete_file_name(data
[1], param
);
345 result
.push(format
!("{}:{}", data
[0], file
));
351 pub fn base_directories() -> Result
<xdg
::BaseDirectories
, Error
> {
352 xdg
::BaseDirectories
::with_prefix("proxmox-backup").map_err(Error
::from
)
355 /// Convenience helper for better error messages:
356 pub fn find_xdg_file(
357 file_name
: impl AsRef
<std
::path
::Path
>,
358 description
: &'
static str,
359 ) -> Result
<Option
<std
::path
::PathBuf
>, Error
> {
360 let file_name
= file_name
.as_ref();
362 .map(|base
| base
.find_config_file(file_name
))
363 .with_context(|| format
!("error searching for {}", description
))
366 pub fn place_xdg_file(
367 file_name
: impl AsRef
<std
::path
::Path
>,
368 description
: &'
static str,
369 ) -> Result
<std
::path
::PathBuf
, Error
> {
370 let file_name
= file_name
.as_ref();
372 .and_then(|base
| base
.place_config_file(file_name
).map_err(Error
::from
))
373 .with_context(|| format
!("failed to place {} in xdg home", description
))