use std::task::Context;
use anyhow::{bail, format_err, Error};
-use chrono::{Local, DateTime, Utc, TimeZone};
use futures::future::FutureExt;
use futures::stream::{StreamExt, TryStreamExt};
use serde_json::{json, Value};
use xdg::BaseDirectories;
use pathpatterns::{MatchEntry, MatchType, PatternFlag};
-use proxmox::tools::fs::{file_get_contents, file_get_json, replace_file, CreateOptions, image_size};
-use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment};
-use proxmox::api::schema::*;
-use proxmox::api::cli::*;
-use proxmox::api::api;
+use proxmox::{
+ tools::{
+ time::{strftime_local, epoch_i64},
+ fs::{file_get_contents, file_get_json, replace_file, CreateOptions, image_size},
+ },
+ api::{
+ api,
+ ApiHandler,
+ ApiMethod,
+ RpcEnvironment,
+ schema::*,
+ cli::*,
+ },
+};
use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
use proxmox_backup::tools;
use proxmox_backup::api2::version;
use proxmox_backup::client::*;
use proxmox_backup::pxar::catalog::*;
+use proxmox_backup::config::user::complete_user_name;
use proxmox_backup::backup::{
archive_type,
decrypt_key,
result
}
-fn connect(server: &str, userid: &Userid) -> Result<HttpClient, Error> {
+fn connect(server: &str, port: u16, userid: &Userid) -> Result<HttpClient, Error> {
let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
.fingerprint_cache(true)
.ticket_cache(true);
- HttpClient::new(server, userid, options)
+ HttpClient::new(server, port, userid, options)
}
async fn view_task_result(
client: &HttpClient,
store: &str,
group: BackupGroup,
-) -> Result<(String, String, DateTime<Utc>), Error> {
+) -> Result<(String, String, i64), Error> {
let list = api_datastore_list_snapshots(client, store, Some(group.clone())).await?;
let mut list: Vec<SnapshotListItem> = serde_json::from_value(list)?;
list.sort_unstable_by(|a, b| b.backup_time.cmp(&a.backup_time));
- let backup_time = Utc.timestamp(list[0].backup_time, 0);
+ let backup_time = list[0].backup_time;
Ok((group.backup_type().to_owned(), group.backup_id().to_owned(), backup_time))
}
let repo = extract_repository_from_value(¶m)?;
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
let render_last_backup = |_v: &Value, record: &Value| -> Result<String, Error> {
let item: GroupListItem = serde_json::from_value(record.to_owned())?;
- let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.last_backup);
+ let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.last_backup)?;
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
};
Ok(Value::Null)
}
+#[api(
+ input: {
+ properties: {
+ repository: {
+ schema: REPO_URL_SCHEMA,
+ optional: true,
+ },
+ group: {
+ type: String,
+ description: "Backup group.",
+ },
+ "new-owner": {
+ type: Userid,
+ },
+ }
+ }
+)]
+/// Change owner of a backup group
+async fn change_backup_owner(group: String, mut param: Value) -> Result<(), Error> {
+
+ let repo = extract_repository_from_value(¶m)?;
+
+ let mut client = connect(repo.host(), repo.port(), repo.user())?;
+
+ param.as_object_mut().unwrap().remove("repository");
+
+ let group: BackupGroup = group.parse()?;
+
+ param["backup-type"] = group.backup_type().into();
+ param["backup-id"] = group.backup_id().into();
+
+ let path = format!("api2/json/admin/datastore/{}/change-owner", repo.store());
+ client.post(&path, Some(param)).await?;
+
+ record_repository(&repo);
+
+ Ok(())
+}
+
#[api(
input: {
properties: {
let output_format = get_output_format(¶m);
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() {
Some(path.parse()?)
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
let item: SnapshotListItem = serde_json::from_value(record.to_owned())?;
- let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time);
+ let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
};
.sortby("backup-id", false)
.sortby("backup-time", false)
.column(ColumnConfig::new("backup-id").renderer(render_snapshot_path).header("snapshot"))
- .column(ColumnConfig::new("size"))
+ .column(ColumnConfig::new("size").renderer(tools::format::render_bytes_human_readable))
.column(ColumnConfig::new("files").renderer(render_files))
;
let path = tools::required_string_param(¶m, "snapshot")?;
let snapshot: BackupDir = path.parse()?;
- let mut client = connect(repo.host(), repo.user())?;
+ let mut client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
let result = client.delete(&path, Some(json!({
"backup-type": snapshot.group().backup_type(),
"backup-id": snapshot.group().backup_id(),
- "backup-time": snapshot.backup_time().timestamp(),
+ "backup-time": snapshot.backup_time(),
}))).await?;
record_repository(&repo);
let repo = extract_repository_from_value(¶m)?;
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
client.login().await?;
record_repository(&repo);
let repo = extract_repository_from_value(¶m);
if let Ok(repo) = repo {
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
match client.get("api2/json/version", None).await {
Ok(mut result) => version_info["server"] = result["data"].take(),
let output_format = get_output_format(¶m);
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/files", repo.store());
let mut result = client.get(&path, Some(json!({
"backup-type": snapshot.group().backup_type(),
"backup-id": snapshot.group().backup_id(),
- "backup-time": snapshot.backup_time().timestamp(),
+ "backup-time": snapshot.backup_time(),
}))).await?;
record_repository(&repo);
let output_format = get_output_format(¶m);
- let mut client = connect(repo.host(), repo.user())?;
+ let mut client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/gc", repo.store());
}
}
- let backup_time = Utc.timestamp(backup_time_opt.unwrap_or_else(|| Utc::now().timestamp()), 0);
+ let backup_time = backup_time_opt.unwrap_or_else(|| epoch_i64());
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
record_repository(&repo);
- println!("Starting backup: {}/{}/{}", backup_type, backup_id, BackupDir::backup_time_to_string(backup_time));
+ println!("Starting backup: {}/{}/{}", backup_type, backup_id, BackupDir::backup_time_to_string(backup_time)?);
println!("Client name: {}", proxmox::tools::nodename());
- let start_time = Local::now();
+ let start_time = std::time::Instant::now();
- println!("Starting protocol: {}", start_time.to_rfc3339_opts(chrono::SecondsFormat::Secs, false));
+ println!("Starting backup protocol: {}", strftime_local("%c", epoch_i64())?);
let (crypt_config, rsa_encrypted_key) = match keydata {
None => (None, None),
&backup_id,
backup_time,
verbose,
+ false
).await?;
let previous_manifest = if let Ok(previous_manifest) = client.download_previous_manifest().await {
None
};
- let snapshot = BackupDir::new(backup_type, backup_id, backup_time.timestamp());
+ let snapshot = BackupDir::new(backup_type, backup_id, backup_time)?;
let mut manifest = BackupManifest::new(snapshot);
let mut catalog = None;
client.finish().await?;
- let end_time = Local::now();
- let elapsed = end_time.signed_duration_since(start_time);
- println!("Duration: {}", elapsed);
+ let end_time = std::time::Instant::now();
+ let elapsed = end_time.duration_since(start_time);
+ println!("Duration: {:.2}s", elapsed.as_secs_f64());
- println!("End Time: {}", end_time.to_rfc3339_opts(chrono::SecondsFormat::Secs, false));
+ println!("End Time: {}", strftime_local("%c", epoch_i64())?);
Ok(Value::Null)
}
async fn dump_image<W: Write>(
client: Arc<BackupReader>,
crypt_config: Option<Arc<CryptConfig>>,
+ crypt_mode: CryptMode,
index: FixedIndexReader,
mut writer: W,
verbose: bool,
let most_used = index.find_most_used_chunks(8);
- let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, most_used);
+ let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, crypt_mode, most_used);
// Note: we avoid using BufferedFixedReader, because that add an additional buffer/copy
// and thus slows down reading. Instead, directly use RemoteChunkReader
let archive_name = tools::required_string_param(¶m, "archive-name")?;
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
record_repository(&repo);
.map_err(|err| format_err!("unable to pipe data - {}", err))?;
}
- } else if archive_type == ArchiveType::Blob {
+ return Ok(Value::Null);
+ }
+
+ let file_info = manifest.lookup_file_info(&archive_name)?;
+
+ if archive_type == ArchiveType::Blob {
let mut reader = client.download_blob(&manifest, &archive_name).await?;
let most_used = index.find_most_used_chunks(8);
- let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, most_used);
+ let chunk_reader = RemoteChunkReader::new(client.clone(), crypt_config, file_info.chunk_crypt_mode(), most_used);
let mut reader = BufferedDynamicReader::new(index, chunk_reader);
.map_err(|err| format_err!("unable to open /dev/stdout - {}", err))?
};
- dump_image(client.clone(), crypt_config.clone(), index, &mut writer, verbose).await?;
+ dump_image(client.clone(), crypt_config.clone(), file_info.chunk_crypt_mode(), index, &mut writer, verbose).await?;
}
Ok(Value::Null)
let snapshot = tools::required_string_param(¶m, "snapshot")?;
let snapshot: BackupDir = snapshot.parse()?;
- let mut client = connect(repo.host(), repo.user())?;
+ let mut client = connect(repo.host(), repo.port(), repo.user())?;
let (keydata, crypt_mode) = keyfile_parameters(¶m)?;
let args = json!({
"backup-type": snapshot.group().backup_type(),
"backup-id": snapshot.group().backup_id(),
- "backup-time": snapshot.backup_time().timestamp(),
+ "backup-time": snapshot.backup_time(),
});
let body = hyper::Body::from(raw_data);
async fn prune_async(mut param: Value) -> Result<Value, Error> {
let repo = extract_repository_from_value(¶m)?;
- let mut client = connect(repo.host(), repo.user())?;
+ let mut client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
let render_snapshot_path = |_v: &Value, record: &Value| -> Result<String, Error> {
let item: PruneListItem = serde_json::from_value(record.to_owned())?;
- let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time);
+ let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
Ok(snapshot.relative_path().to_str().unwrap().to_owned())
};
optional: true,
},
}
- }
+ },
+ returns: {
+ type: StorageStatus,
+ },
)]
/// Get repository status.
async fn status(param: Value) -> Result<Value, Error> {
let output_format = get_output_format(¶m);
- let client = connect(repo.host(), repo.user())?;
+ let client = connect(repo.host(), repo.port(), repo.user())?;
let path = format!("api2/json/admin/datastore/{}/status", repo.store());
let mut result = client.get(&path, None).await?;
- let mut data = result["data"].take();
+ let mut data = result["data"]["storage"].take();
record_repository(&repo);
.column(ColumnConfig::new("used").renderer(render_total_percentage))
.column(ColumnConfig::new("avail").renderer(render_total_percentage));
- let schema = &proxmox_backup::api2::admin::datastore::API_RETURN_SCHEMA_STATUS;
+ let schema = &API_RETURN_SCHEMA_STATUS;
format_and_print_result_full(&mut data, schema, &output_format, &options);
.fingerprint_cache(true)
.ticket_cache(true);
- let client = match HttpClient::new(repo.host(), repo.user(), options) {
+ let client = match HttpClient::new(repo.host(), repo.port(), repo.user(), options) {
Ok(v) => v,
_ => return Value::Null,
};
if let (Some(backup_id), Some(backup_type), Some(backup_time)) =
(item["backup-id"].as_str(), item["backup-type"].as_str(), item["backup-time"].as_i64())
{
- let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
- result.push(snapshot.relative_path().to_str().unwrap().to_owned());
+ if let Ok(snapshot) = BackupDir::new(backup_type, backup_id, backup_time) {
+ result.push(snapshot.relative_path().to_str().unwrap().to_owned());
+ }
}
}
}
let query = tools::json_object_to_query(json!({
"backup-type": snapshot.group().backup_type(),
"backup-id": snapshot.group().backup_id(),
- "backup-time": snapshot.backup_time().timestamp(),
+ "backup-time": snapshot.backup_time(),
})).unwrap();
let path = format!("api2/json/admin/datastore/{}/files?{}", repo.store(), query);
fn complete_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
complete_server_file_name(arg, param)
.iter()
- .map(|v| tools::format::strip_server_file_expenstion(&v))
+ .map(|v| tools::format::strip_server_file_extension(&v))
.collect()
}
pub fn complete_pxar_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
complete_server_file_name(arg, param)
.iter()
- .filter_map(|v| {
- let name = tools::format::strip_server_file_expenstion(&v);
- if name.ends_with(".pxar") {
- Some(name)
+ .filter_map(|name| {
+ if name.ends_with(".pxar.didx") {
+ Some(tools::format::strip_server_file_extension(name))
+ } else {
+ None
+ }
+ })
+ .collect()
+}
+
+pub fn complete_img_archive_name(arg: &str, param: &HashMap<String, String>) -> Vec<String> {
+ complete_server_file_name(arg, param)
+ .iter()
+ .filter_map(|name| {
+ if name.ends_with(".img.fidx") {
+ Some(tools::format::strip_server_file_extension(name))
} else {
None
}
let version_cmd_def = CliCommand::new(&API_METHOD_API_VERSION)
.completion_cb("repository", complete_repository);
+ let change_owner_cmd_def = CliCommand::new(&API_METHOD_CHANGE_BACKUP_OWNER)
+ .arg_param(&["group", "new-owner"])
+ .completion_cb("group", complete_backup_group)
+ .completion_cb("new-owner", complete_user_name)
+ .completion_cb("repository", complete_repository);
+
let cmd_def = CliCommandMap::new()
.insert("backup", backup_cmd_def)
.insert("upload-log", upload_log_cmd_def)
.insert("status", status_cmd_def)
.insert("key", key::cli())
.insert("mount", mount_cmd_def())
+ .insert("map", map_cmd_def())
+ .insert("unmap", unmap_cmd_def())
.insert("catalog", catalog_mgmt_cli())
.insert("task", task_mgmt_cli())
.insert("version", version_cmd_def)
- .insert("benchmark", benchmark_cmd_def);
+ .insert("benchmark", benchmark_cmd_def)
+ .insert("change-owner", change_owner_cmd_def);
let rpcenv = CliEnvironment::new();
run_cli_command(cmd_def, rpcenv, Some(|future| {