X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2Fapi2%2Fadmin%2Fdatastore.rs;h=3fe919859ceccb45def2aa77c904bdc8e8227c72;hb=99641a6bbbb30ba67c36d4850c4c1d6ddd3e8a79;hp=3835149405670f404a605c3f495c36799d0546f7;hpb=102d8d4136b543f32301384f03fb893e73f31ea5;p=proxmox-backup.git diff --git a/src/api2/admin/datastore.rs b/src/api2/admin/datastore.rs index 38351494..3fe91985 100644 --- a/src/api2/admin/datastore.rs +++ b/src/api2/admin/datastore.rs @@ -1,46 +1,69 @@ use std::collections::{HashSet, HashMap}; +use std::convert::TryFrom; use chrono::{TimeZone, Local}; -use failure::*; +use anyhow::{bail, Error}; use futures::*; use hyper::http::request::Parts; use hyper::{header, Body, Response, StatusCode}; use serde_json::{json, Value}; -use proxmox::{sortable, identity}; -use proxmox::api::{http_err, list_subdirs_api_method}; -use proxmox::api::{ApiFuture, ApiHandler, ApiMethod, Router, RpcEnvironment, RpcEnvironmentType}; +use proxmox::api::{ + api, ApiResponseFuture, ApiHandler, ApiMethod, Router, + RpcEnvironment, RpcEnvironmentType, Permission, UserInformation}; use proxmox::api::router::SubdirMap; use proxmox::api::schema::*; -use proxmox::tools::{try_block, fs::file_get_contents, fs::file_set_contents}; +use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions}; +use proxmox::try_block; +use proxmox::{http_err, identity, list_subdirs_api_method, sortable}; use crate::api2::types::*; use crate::backup::*; use crate::config::datastore; +use crate::config::cached_user_info::CachedUserInfo; + use crate::server::WorkerTask; use crate::tools; +use crate::config::acl::{ + PRIV_DATASTORE_AUDIT, + PRIV_DATASTORE_MODIFY, + PRIV_DATASTORE_READ, + PRIV_DATASTORE_PRUNE, + PRIV_DATASTORE_BACKUP, +}; + +fn check_backup_owner(store: &DataStore, group: &BackupGroup, userid: &str) -> Result<(), Error> { + let owner = store.get_owner(group)?; + if &owner != userid { + bail!("backup owner check failed ({} != {})", userid, owner); + } + Ok(()) +} -fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result { +fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result, Error> { let mut path = store.base_path(); path.push(backup_dir.relative_path()); path.push("index.json.blob"); let raw_data = file_get_contents(&path)?; - let data = DataBlob::from_raw(raw_data)?.decode(None)?; - let index_size = data.len(); - let mut result: Value = serde_json::from_reader(&mut &data[..])?; + let index_size = raw_data.len() as u64; + let blob = DataBlob::from_raw(raw_data)?; - let mut result = result["files"].take(); + let manifest = BackupManifest::try_from(blob)?; - if result == Value::Null { - bail!("missing 'files' property in backup index {:?}", path); + let mut result = Vec::new(); + for item in manifest.files() { + result.push(BackupContent { + filename: item.filename.clone(), + size: Some(item.size), + }); } - result.as_array_mut().unwrap().push(json!({ - "filename": "index.json.blob", - "size": index_size, - })); + result.push(BackupContent { + filename: "index.json.blob".to_string(), + size: Some(index_size), + }); Ok(result) } @@ -58,103 +81,237 @@ fn group_backups(backup_list: Vec) -> HashMap Result { + store: String, + rpcenv: &mut dyn RpcEnvironment, +) -> Result, Error> { - let store = param["store"].as_str().unwrap(); + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); - let datastore = DataStore::lookup_datastore(store)?; + let datastore = DataStore::lookup_datastore(&store)?; let backup_list = BackupInfo::list_backups(&datastore.base_path())?; let group_hash = group_backups(backup_list); - let mut groups = vec![]; + let mut groups = Vec::new(); for (_group_id, mut list) in group_hash { BackupInfo::sort_list(&mut list, false); let info = &list[0]; + let group = info.backup_dir.group(); - groups.push(json!({ - "backup-type": group.backup_type(), - "backup-id": group.backup_id(), - "last-backup": info.backup_dir.backup_time().timestamp(), - "backup-count": list.len() as u64, - "files": info.files, - })); + let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0; + if !list_all { + let owner = datastore.get_owner(group)?; + if owner != username { continue; } + } + + let result_item = GroupListItem { + backup_type: group.backup_type().to_string(), + backup_id: group.backup_id().to_string(), + last_backup: info.backup_dir.backup_time().timestamp(), + backup_count: list.len() as u64, + files: info.files.clone(), + }; + groups.push(result_item); } - Ok(json!(groups)) + Ok(groups) } -fn list_snapshot_files ( - param: Value, +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + "backup-type": { + schema: BACKUP_TYPE_SCHEMA, + }, + "backup-id": { + schema: BACKUP_ID_SCHEMA, + }, + "backup-time": { + schema: BACKUP_TIME_SCHEMA, + }, + }, + }, + returns: { + type: Array, + description: "Returns the list of archive files inside a backup snapshots.", + items: { + type: BackupContent, + } + }, + access: { + permission: &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, + true), + }, +)] +/// List snapshot files. +pub fn list_snapshot_files( + store: String, + backup_type: String, + backup_id: String, + backup_time: i64, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { + rpcenv: &mut dyn RpcEnvironment, +) -> Result, Error> { - let store = tools::required_string_param(¶m, "store")?; - let backup_type = tools::required_string_param(¶m, "backup-type")?; - let backup_id = tools::required_string_param(¶m, "backup-id")?; - let backup_time = tools::required_integer_param(¶m, "backup-time")?; + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + + let datastore = DataStore::lookup_datastore(&store)?; - let datastore = DataStore::lookup_datastore(store)?; let snapshot = BackupDir::new(backup_type, backup_id, backup_time); + let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0; + if !allowed { check_backup_owner(&datastore, snapshot.group(), &username)?; } + let mut files = read_backup_index(&datastore, &snapshot)?; let info = BackupInfo::new(&datastore.base_path(), snapshot)?; - let file_set = files.as_array().unwrap().iter().fold(HashSet::new(), |mut acc, item| { - acc.insert(item["filename"].as_str().unwrap().to_owned()); + let file_set = files.iter().fold(HashSet::new(), |mut acc, item| { + acc.insert(item.filename.clone()); acc }); for file in info.files { if file_set.contains(&file) { continue; } - files.as_array_mut().unwrap().push(json!({ "filename": file })); + files.push(BackupContent { filename: file, size: None }); } Ok(files) } -fn delete_snapshots ( - param: Value, +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + "backup-type": { + schema: BACKUP_TYPE_SCHEMA, + }, + "backup-id": { + schema: BACKUP_ID_SCHEMA, + }, + "backup-time": { + schema: BACKUP_TIME_SCHEMA, + }, + }, + }, + access: { + permission: &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_MODIFY| PRIV_DATASTORE_PRUNE, + true), + }, +)] +/// Delete backup snapshot. +fn delete_snapshot( + store: String, + backup_type: String, + backup_id: String, + backup_time: i64, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let store = tools::required_string_param(¶m, "store")?; - let backup_type = tools::required_string_param(¶m, "backup-type")?; - let backup_id = tools::required_string_param(¶m, "backup-id")?; - let backup_time = tools::required_integer_param(¶m, "backup-time")?; + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); let snapshot = BackupDir::new(backup_type, backup_id, backup_time); - let datastore = DataStore::lookup_datastore(store)?; + let datastore = DataStore::lookup_datastore(&store)?; + + let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0; + if !allowed { check_backup_owner(&datastore, snapshot.group(), &username)?; } datastore.remove_backup_dir(&snapshot)?; Ok(Value::Null) } -fn list_snapshots ( - param: Value, +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + "backup-type": { + optional: true, + schema: BACKUP_TYPE_SCHEMA, + }, + "backup-id": { + optional: true, + schema: BACKUP_ID_SCHEMA, + }, + }, + }, + returns: { + type: Array, + description: "Returns the list of snapshots.", + items: { + type: SnapshotListItem, + } + }, + access: { + permission: &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, + true), + }, +)] +/// List backup snapshots. +pub fn list_snapshots ( + store: String, + backup_type: Option, + backup_id: Option, + _param: Value, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { + rpcenv: &mut dyn RpcEnvironment, +) -> Result, Error> { - let store = tools::required_string_param(¶m, "store")?; - let backup_type = param["backup-type"].as_str(); - let backup_id = param["backup-id"].as_str(); + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); - let datastore = DataStore::lookup_datastore(store)?; + let datastore = DataStore::lookup_datastore(&store)?; let base_path = datastore.base_path(); @@ -164,56 +321,66 @@ fn list_snapshots ( for info in backup_list { let group = info.backup_dir.group(); - if let Some(backup_type) = backup_type { + if let Some(ref backup_type) = backup_type { if backup_type != group.backup_type() { continue; } } - if let Some(backup_id) = backup_id { + if let Some(ref backup_id) = backup_id { if backup_id != group.backup_id() { continue; } } - let mut result_item = json!({ - "backup-type": group.backup_type(), - "backup-id": group.backup_id(), - "backup-time": info.backup_dir.backup_time().timestamp(), - "files": info.files, - }); + let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0; + if !list_all { + let owner = datastore.get_owner(group)?; + if owner != username { continue; } + } + + let mut result_item = SnapshotListItem { + backup_type: group.backup_type().to_string(), + backup_id: group.backup_id().to_string(), + backup_time: info.backup_dir.backup_time().timestamp(), + files: info.files, + size: None, + }; if let Ok(index) = read_backup_index(&datastore, &info.backup_dir) { let mut backup_size = 0; - for item in index.as_array().unwrap().iter() { - if let Some(item_size) = item["size"].as_u64() { + for item in index.iter() { + if let Some(item_size) = item.size { backup_size += item_size; } } - result_item["size"] = backup_size.into(); + result_item.size = Some(backup_size); } snapshots.push(result_item); } - Ok(json!(snapshots)) + Ok(snapshots) } -#[sortable] -const API_METHOD_STATUS: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&status), - &ObjectSchema::new( - "Get datastore status.", - &sorted!([ - ("store", false, &StringSchema::new("Datastore name.").schema()), - ]), - ) -); - -fn status( - param: Value, +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + }, + }, + returns: { + type: StorageStatus, + }, + access: { + permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true), + }, +)] +/// Get datastore status. +pub fn status( + store: String, _info: &ApiMethod, _rpcenv: &mut dyn RpcEnvironment, -) -> Result { - - let store = param["store"].as_str().unwrap(); +) -> Result { - let datastore = DataStore::lookup_datastore(store)?; + let datastore = DataStore::lookup_datastore(&store)?; let base_path = datastore.base_path(); @@ -225,11 +392,12 @@ fn status( nix::errno::Errno::result(res)?; let bsize = stat.f_bsize as u64; - Ok(json!({ - "total": stat.f_blocks*bsize, - "used": (stat.f_blocks-stat.f_bfree)*bsize, - "avail": stat.f_bavail*bsize, - })) + + Ok(StorageStatus { + total: stat.f_blocks*bsize, + used: (stat.f_blocks-stat.f_bfree)*bsize, + avail: stat.f_bavail*bsize, + }) } #[macro_export] @@ -287,33 +455,55 @@ macro_rules! add_common_prune_prameters { } } -const API_METHOD_TEST_PRUNE: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&test_prune), +pub const API_RETURN_SCHEMA_PRUNE: Schema = ArraySchema::new( + "Returns the list of snapshots and a flag indicating if there are kept or removed.", + PruneListItem::API_SCHEMA +).schema(); + +const API_METHOD_PRUNE: ApiMethod = ApiMethod::new( + &ApiHandler::Sync(&prune), &ObjectSchema::new( - "Test what prune would do.", + "Prune the datastore.", &add_common_prune_prameters!([ ("backup-id", false, &BACKUP_ID_SCHEMA), ("backup-type", false, &BACKUP_TYPE_SCHEMA), + ("dry-run", true, &BooleanSchema::new( + "Just show what prune would do, but do not delete anything.") + .schema() + ), ],[ - ("store", false, &StringSchema::new("Datastore name.").schema()), + ("store", false, &DATASTORE_SCHEMA), ]) - ) + )) + .returns(&API_RETURN_SCHEMA_PRUNE) + .access(None, &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE, + true) ); -fn test_prune( +fn prune( param: Value, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let store = param["store"].as_str().unwrap(); - + let store = tools::required_string_param(¶m, "store")?; let backup_type = tools::required_string_param(¶m, "backup-type")?; let backup_id = tools::required_string_param(¶m, "backup-id")?; + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + + let dry_run = param["dry-run"].as_bool().unwrap_or(false); + let group = BackupGroup::new(backup_type, backup_id); - let datastore = DataStore::lookup_datastore(store)?; + let datastore = DataStore::lookup_datastore(&store)?; + + let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0; + if !allowed { check_backup_owner(&datastore, &group, &username)?; } let prune_options = PruneOptions { keep_last: param["keep-last"].as_u64(), @@ -324,85 +514,74 @@ fn test_prune( keep_yearly: param["keep-yearly"].as_u64(), }; - let list = group.list_backups(&datastore.base_path())?; + let worker_id = format!("{}_{}_{}", store, backup_type, backup_id); - let result: Vec<(Value)> = if !prune_options.keeps_something() { - list.iter().map(|info| { - json!({ - "backup-time": info.backup_dir.backup_time().timestamp(), - "keep": true, - }) - }).collect() - } else { - let prune_info = compute_prune_info(list, &prune_options)?; - prune_info.iter().map(|(info, keep)| { - json!({ - "backup-time": info.backup_dir.backup_time().timestamp(), - "keep": keep, - }) - }).collect() - }; + let mut prune_result = Vec::new(); - Ok(json!(result)) -} + let list = group.list_backups(&datastore.base_path())?; -const API_METHOD_PRUNE: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&prune), - &ObjectSchema::new( - "Prune the datastore.", - &add_common_prune_prameters!([ - ("backup-id", false, &BACKUP_ID_SCHEMA), - ("backup-type", false, &BACKUP_TYPE_SCHEMA), - ],[ - ("store", false, &StringSchema::new("Datastore name.").schema()), - ]) - ) -); + let mut prune_info = compute_prune_info(list, &prune_options)?; -fn prune( - param: Value, - _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { + prune_info.reverse(); // delete older snapshots first - let store = param["store"].as_str().unwrap(); + let keep_all = !prune_options.keeps_something(); - let backup_type = tools::required_string_param(¶m, "backup-type")?; - let backup_id = tools::required_string_param(¶m, "backup-id")?; + if dry_run { + for (info, mut keep) in prune_info { + if keep_all { keep = true; } - let group = BackupGroup::new(backup_type, backup_id); + let backup_time = info.backup_dir.backup_time(); + let group = info.backup_dir.group(); + + prune_result.push(json!({ + "backup-type": group.backup_type(), + "backup-id": group.backup_id(), + "backup-time": backup_time.timestamp(), + "keep": keep, + })); + } + return Ok(json!(prune_result)); + } - let datastore = DataStore::lookup_datastore(store)?; - let prune_options = PruneOptions { - keep_last: param["keep-last"].as_u64(), - keep_hourly: param["keep-hourly"].as_u64(), - keep_daily: param["keep-daily"].as_u64(), - keep_weekly: param["keep-weekly"].as_u64(), - keep_monthly: param["keep-monthly"].as_u64(), - keep_yearly: param["keep-yearly"].as_u64(), - }; + // We use a WorkerTask just to have a task log, but run synchrounously + let worker = WorkerTask::new("prune", Some(worker_id), "root@pam", true)?; - let worker = WorkerTask::new("prune", Some(store.to_owned()), "root@pam", true)?; let result = try_block! { - if !prune_options.keeps_something() { + if keep_all { worker.log("No prune selection - keeping all files."); - return Ok(()); } else { - worker.log(format!("Starting prune on store {}", store)); + worker.log(format!("retention options: {}", prune_options.cli_options_string())); + worker.log(format!("Starting prune on store \"{}\" group \"{}/{}\"", + store, backup_type, backup_id)); } - let list = group.list_backups(&datastore.base_path())?; + for (info, mut keep) in prune_info { + if keep_all { keep = true; } + + let backup_time = info.backup_dir.backup_time(); + let timestamp = BackupDir::backup_time_to_string(backup_time); + let group = info.backup_dir.group(); + - let mut prune_info = compute_prune_info(list, &prune_options)?; + let msg = format!( + "{}/{}/{} {}", + group.backup_type(), + group.backup_id(), + timestamp, + if keep { "keep" } else { "remove" }, + ); - prune_info.reverse(); // delete older snapshots first + worker.log(msg); + + prune_result.push(json!({ + "backup-type": group.backup_type(), + "backup-id": group.backup_id(), + "backup-time": backup_time.timestamp(), + "keep": keep, + })); - for (info, keep) in prune_info { - if keep { - worker.log(format!("keep {:?}", info.backup_dir.relative_path())); - } else { - worker.log(format!("remove {:?}", info.backup_dir.relative_path())); + if !(dry_run || keep) { datastore.remove_backup_dir(&info.backup_dir)?; } } @@ -414,30 +593,33 @@ fn prune( if let Err(err) = result { bail!("prune failed - {}", err); - } + }; - Ok(json!(null)) + Ok(json!(prune_result)) } -#[sortable] -pub const API_METHOD_START_GARBAGE_COLLECTION: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&start_garbage_collection), - &ObjectSchema::new( - "Start garbage collection.", - &sorted!([ - ("store", false, &StringSchema::new("Datastore name.").schema()), - ]) - ) -); - +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + }, + }, + returns: { + schema: UPID_SCHEMA, + }, + access: { + permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY, false), + }, +)] +/// Start garbage collection. fn start_garbage_collection( - param: Value, + store: String, _info: &ApiMethod, rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let store = param["store"].as_str().unwrap().to_string(); - let datastore = DataStore::lookup_datastore(&store)?; println!("Starting garbage collection on store {}", store); @@ -448,50 +630,89 @@ fn start_garbage_collection( "garbage_collection", Some(store.clone()), "root@pam", to_stdout, move |worker| { worker.log(format!("starting garbage collection on store {}", store)); - datastore.garbage_collection(worker) + datastore.garbage_collection(&worker) })?; Ok(json!(upid_str)) } -#[sortable] -pub const API_METHOD_GARBAGE_COLLECTION_STATUS: ApiMethod = ApiMethod::new( - &ApiHandler::Sync(&garbage_collection_status), - &ObjectSchema::new( - "Garbage collection status.", - &sorted!([ - ("store", false, &StringSchema::new("Datastore name.").schema()), - ]) - ) -); - -fn garbage_collection_status( - param: Value, +#[api( + input: { + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + }, + }, + returns: { + type: GarbageCollectionStatus, + }, + access: { + permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false), + }, +)] +/// Garbage collection status. +pub fn garbage_collection_status( + store: String, _info: &ApiMethod, _rpcenv: &mut dyn RpcEnvironment, -) -> Result { - - let store = param["store"].as_str().unwrap(); +) -> Result { let datastore = DataStore::lookup_datastore(&store)?; - println!("Garbage collection status on store {}", store); - let status = datastore.last_gc_status(); - Ok(serde_json::to_value(&status)?) + Ok(status) } - +#[api( + returns: { + description: "List the accessible datastores.", + type: Array, + items: { + description: "Datastore name and description.", + properties: { + store: { + schema: DATASTORE_SCHEMA, + }, + comment: { + optional: true, + schema: SINGLE_LINE_COMMENT_SCHEMA, + }, + }, + }, + }, + access: { + permission: &Permission::Anybody, + }, +)] +/// Datastore list fn get_datastore_list( _param: Value, _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, + rpcenv: &mut dyn RpcEnvironment, ) -> Result { - let config = datastore::config()?; + let (config, _digest) = datastore::config()?; - Ok(config.convert_to_array("store")) + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + + let mut list = Vec::new(); + + for (store, (_, data)) in &config.sections { + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0; + if allowed { + let mut entry = json!({ "store": store }); + if let Some(comment) = data["comment"].as_str() { + entry["comment"] = comment.into(); + } + list.push(entry); + } + } + + Ok(list.into()) } #[sortable] @@ -500,16 +721,17 @@ pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new( &ObjectSchema::new( "Download single raw file from backup snapshot.", &sorted!([ - ("store", false, &StringSchema::new("Datastore name.").schema()), + ("store", false, &DATASTORE_SCHEMA), ("backup-type", false, &BACKUP_TYPE_SCHEMA), ("backup-id", false, &BACKUP_ID_SCHEMA), ("backup-time", false, &BACKUP_TIME_SCHEMA), - ("file-name", false, &StringSchema::new("Raw file name.") - .format(&FILENAME_FORMAT) - .schema() - ), + ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA), ]), ) +).access(None, &Permission::Privilege( + &["datastore", "{store}"], + PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, + true) ); fn download_file( @@ -517,25 +739,31 @@ fn download_file( _req_body: Body, param: Value, _info: &ApiMethod, - _rpcenv: Box, -) -> ApiFuture { + rpcenv: Box, +) -> ApiResponseFuture { async move { let store = tools::required_string_param(¶m, "store")?; - let datastore = DataStore::lookup_datastore(store)?; + let username = rpcenv.get_user().unwrap(); + let user_info = CachedUserInfo::new()?; + let user_privs = user_info.lookup_privs(&username, &["datastore", &store]); + let file_name = tools::required_string_param(¶m, "file-name")?.to_owned(); let backup_type = tools::required_string_param(¶m, "backup-type")?; let backup_id = tools::required_string_param(¶m, "backup-id")?; let backup_time = tools::required_integer_param(¶m, "backup-time")?; + let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); + + let allowed = (user_privs & PRIV_DATASTORE_READ) != 0; + if !allowed { check_backup_owner(&datastore, backup_dir.group(), &username)?; } + println!("Download {} from {} ({}/{}/{}/{})", file_name, store, backup_type, backup_id, Local.timestamp(backup_time, 0), file_name); - let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); - let mut path = datastore.base_path(); path.push(backup_dir.relative_path()); path.push(&file_name); @@ -544,8 +772,8 @@ fn download_file( .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err))) .await?; - let payload = tokio::codec::FramedRead::new(file, tokio::codec::BytesCodec::new()) - .map_ok(|bytes| hyper::Chunk::from(bytes.freeze())); + let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new()) + .map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze())); let body = Body::wrap_stream(payload); // fixme: set other headers ? @@ -561,14 +789,17 @@ fn download_file( pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new( &ApiHandler::AsyncHttp(&upload_backup_log), &ObjectSchema::new( - "Download single raw file from backup snapshot.", + "Upload the client backup log file into a backup snapshot ('client.log.blob').", &sorted!([ - ("store", false, &StringSchema::new("Datastore name.").schema()), + ("store", false, &DATASTORE_SCHEMA), ("backup-type", false, &BACKUP_TYPE_SCHEMA), ("backup-id", false, &BACKUP_ID_SCHEMA), ("backup-time", false, &BACKUP_TIME_SCHEMA), ]), ) +).access( + Some("Only the backup creator/owner is allowed to do this."), + &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_BACKUP, false) ); fn upload_backup_log( @@ -576,12 +807,11 @@ fn upload_backup_log( req_body: Body, param: Value, _info: &ApiMethod, - _rpcenv: Box, -) -> ApiFuture { + rpcenv: Box, +) -> ApiResponseFuture { async move { let store = tools::required_string_param(¶m, "store")?; - let datastore = DataStore::lookup_datastore(store)?; let file_name = "client.log.blob"; @@ -592,6 +822,9 @@ fn upload_backup_log( let backup_dir = BackupDir::new(backup_type, backup_id, backup_time); + let username = rpcenv.get_user().unwrap(); + check_backup_owner(&datastore, backup_dir.group(), &username)?; + let mut path = datastore.base_path(); path.push(backup_dir.relative_path()); path.push(&file_name); @@ -615,15 +848,13 @@ fn upload_backup_log( // always verify CRC at server side blob.verify_crc()?; let raw_data = blob.raw_data(); - file_set_contents(&path, raw_data, None)?; + replace_file(&path, raw_data, CreateOptions::new())?; // fixme: use correct formatter Ok(crate::server::formatter::json_response(Ok(Value::Null))) }.boxed() } -const STORE_SCHEMA: Schema = StringSchema::new("Datastore name.").schema(); - #[sortable] const DATASTORE_INFO_SUBDIRS: SubdirMap = &[ ( @@ -634,20 +865,7 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[ ( "files", &Router::new() - .get( - &ApiMethod::new( - &ApiHandler::Sync(&list_snapshot_files), - &ObjectSchema::new( - "List snapshot files.", - &sorted!([ - ("store", false, &STORE_SCHEMA), - ("backup-type", false, &BACKUP_TYPE_SCHEMA), - ("backup-id", false, &BACKUP_ID_SCHEMA), - ("backup-time", false, &BACKUP_TIME_SCHEMA), - ]), - ) - ) - ) + .get(&API_METHOD_LIST_SNAPSHOT_FILES) ), ( "gc", @@ -658,52 +876,18 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[ ( "groups", &Router::new() - .get( - &ApiMethod::new( - &ApiHandler::Sync(&list_groups), - &ObjectSchema::new( - "List backup groups.", - &sorted!([ ("store", false, &STORE_SCHEMA) ]), - ) - ) - ) + .get(&API_METHOD_LIST_GROUPS) ), ( "prune", &Router::new() - .get(&API_METHOD_TEST_PRUNE) .post(&API_METHOD_PRUNE) ), ( "snapshots", &Router::new() - .get( - &ApiMethod::new( - &ApiHandler::Sync(&list_snapshots), - &ObjectSchema::new( - "List backup groups.", - &sorted!([ - ("store", false, &STORE_SCHEMA), - ("backup-type", true, &BACKUP_TYPE_SCHEMA), - ("backup-id", true, &BACKUP_ID_SCHEMA), - ]), - ) - ) - ) - .delete( - &ApiMethod::new( - &ApiHandler::Sync(&delete_snapshots), - &ObjectSchema::new( - "Delete backup snapshot.", - &sorted!([ - ("store", false, &STORE_SCHEMA), - ("backup-type", false, &BACKUP_TYPE_SCHEMA), - ("backup-id", false, &BACKUP_ID_SCHEMA), - ("backup-time", false, &BACKUP_TIME_SCHEMA), - ]), - ) - ) - ) + .get(&API_METHOD_LIST_SNAPSHOTS) + .delete(&API_METHOD_DELETE_SNAPSHOT) ), ( "status", @@ -723,10 +907,5 @@ const DATASTORE_INFO_ROUTER: Router = Router::new() pub const ROUTER: Router = Router::new() - .get( - &ApiMethod::new( - &ApiHandler::Sync(&get_datastore_list), - &ObjectSchema::new("Directory index.", &[]) - ) - ) + .get(&API_METHOD_GET_DATASTORE_LIST) .match_all("store", &DATASTORE_INFO_ROUTER);