]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/admin/datastore.rs
datastore: include namespace in full_path
[proxmox-backup.git] / src / api2 / admin / datastore.rs
CommitLineData
bf78f708
DM
1//! Datastore Management
2
0d08fcee 3use std::collections::HashSet;
d33d8f4e
DC
4use std::ffi::OsStr;
5use std::os::unix::ffi::OsStrExt;
d6688884 6use std::path::PathBuf;
6da20161 7use std::sync::Arc;
cad540e9 8
6ef9bb59 9use anyhow::{bail, format_err, Error};
9e47c0a5 10use futures::*;
cad540e9
WB
11use hyper::http::request::Parts;
12use hyper::{header, Body, Response, StatusCode};
8c74349b 13use serde::Deserialize;
15e9b4ed 14use serde_json::{json, Value};
7c667013 15use tokio_stream::wrappers::ReceiverStream;
15e9b4ed 16
dc7a5b34
TL
17use proxmox_async::blocking::WrappedReaderStream;
18use proxmox_async::{io::AsyncChannelWriter, stream::AsyncReaderStream};
984ddb2f 19use proxmox_compression::zstd::ZstdEncoder;
6ef1b649 20use proxmox_router::{
dc7a5b34
TL
21 http_err, list_subdirs_api_method, ApiHandler, ApiMethod, ApiResponseFuture, Permission,
22 Router, RpcEnvironment, RpcEnvironmentType, SubdirMap,
6ef1b649
WB
23};
24use proxmox_schema::*;
dc7a5b34
TL
25use proxmox_sys::fs::{
26 file_read_firstline, file_read_optional_string, replace_file, CreateOptions,
27};
28use proxmox_sys::sortable;
d5790a9f 29use proxmox_sys::{task_log, task_warn};
e18a6c9e 30
2e219481 31use pxar::accessor::aio::Accessor;
d33d8f4e
DC
32use pxar::EntryKind;
33
dc7a5b34 34use pbs_api_types::{
8c74349b
WB
35 Authid, BackupContent, BackupNamespace, BackupType, Counts, CryptMode, DataStoreListItem,
36 DataStoreStatus, GarbageCollectionStatus, GroupListItem, Operation, PruneOptions, RRDMode,
37 RRDTimeFrame, SnapshotListItem, SnapshotVerifyState, BACKUP_ARCHIVE_NAME_SCHEMA,
33f2c2a1
WB
38 BACKUP_ID_SCHEMA, BACKUP_NAMESPACE_SCHEMA, BACKUP_TIME_SCHEMA, BACKUP_TYPE_SCHEMA,
39 DATASTORE_SCHEMA, IGNORE_VERIFIED_BACKUPS_SCHEMA, PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_BACKUP,
8c74349b
WB
40 PRIV_DATASTORE_MODIFY, PRIV_DATASTORE_PRUNE, PRIV_DATASTORE_READ, PRIV_DATASTORE_VERIFY,
41 UPID_SCHEMA, VERIFICATION_OUTDATED_AFTER_SCHEMA,
b2065dc7 42};
984ddb2f 43use pbs_client::pxar::{create_tar, create_zip};
dc7a5b34 44use pbs_config::CachedUserInfo;
b2065dc7
WB
45use pbs_datastore::backup_info::BackupInfo;
46use pbs_datastore::cached_chunk_reader::CachedChunkReader;
013b1e8b 47use pbs_datastore::catalog::{ArchiveEntry, CatalogReader};
b2065dc7
WB
48use pbs_datastore::data_blob::DataBlob;
49use pbs_datastore::data_blob_reader::DataBlobReader;
50use pbs_datastore::dynamic_index::{BufferedDynamicReader, DynamicIndexReader, LocalDynamicReadAt};
dc7a5b34 51use pbs_datastore::fixed_index::FixedIndexReader;
b2065dc7
WB
52use pbs_datastore::index::IndexFile;
53use pbs_datastore::manifest::{BackupManifest, CLIENT_LOG_BLOB_NAME, MANIFEST_BLOB_NAME};
89725197 54use pbs_datastore::prune::compute_prune_info;
dc7a5b34
TL
55use pbs_datastore::{
56 check_backup_owner, task_tracking, BackupDir, BackupGroup, DataStore, LocalChunkReader,
57 StoreProgress, CATALOG_NAME,
58};
8c74349b 59use pbs_tools::json::required_string_param;
dc7a5b34 60use proxmox_rest_server::{formatter, WorkerTask};
2b7f8dd5 61
431cc7b1 62use crate::api2::node::rrd::create_value_from_rrd;
dc7a5b34 63use crate::backup::{verify_all_backups, verify_backup_dir, verify_backup_group, verify_filter};
54552dda 64
b9700a9f 65use crate::server::jobstate::Job;
804f6143 66
d6688884
SR
67const GROUP_NOTES_FILE_NAME: &str = "notes";
68
db87d93e 69fn get_group_note_path(store: &DataStore, group: &pbs_api_types::BackupGroup) -> PathBuf {
d6688884 70 let mut note_path = store.base_path();
db87d93e 71 note_path.push(group.to_string());
d6688884
SR
72 note_path.push(GROUP_NOTES_FILE_NAME);
73 note_path
74}
75
bff85572 76fn check_priv_or_backup_owner(
e7cb4dc5 77 store: &DataStore,
db87d93e 78 group: &pbs_api_types::BackupGroup,
e6dc35ac 79 auth_id: &Authid,
bff85572
FG
80 required_privs: u64,
81) -> Result<(), Error> {
82 let user_info = CachedUserInfo::new()?;
9a37bd6c 83 let privs = user_info.lookup_privs(auth_id, &["datastore", store.name()]);
bff85572
FG
84
85 if privs & required_privs == 0 {
86 let owner = store.get_owner(group)?;
87 check_backup_owner(&owner, auth_id)?;
88 }
89 Ok(())
90}
91
e7cb4dc5
WB
92fn read_backup_index(
93 store: &DataStore,
94 backup_dir: &BackupDir,
95) -> Result<(BackupManifest, Vec<BackupContent>), Error> {
ff86ef00 96 let (manifest, index_size) = store.load_manifest(backup_dir)?;
8c70e3eb 97
09b1f7b2
DM
98 let mut result = Vec::new();
99 for item in manifest.files() {
100 result.push(BackupContent {
101 filename: item.filename.clone(),
f28d9088 102 crypt_mode: Some(item.crypt_mode),
09b1f7b2
DM
103 size: Some(item.size),
104 });
8c70e3eb
DM
105 }
106
09b1f7b2 107 result.push(BackupContent {
96d65fbc 108 filename: MANIFEST_BLOB_NAME.to_string(),
882c0823
FG
109 crypt_mode: match manifest.signature {
110 Some(_) => Some(CryptMode::SignOnly),
111 None => Some(CryptMode::None),
112 },
09b1f7b2
DM
113 size: Some(index_size),
114 });
4f1e40a2 115
70030b43 116 Ok((manifest, result))
8c70e3eb
DM
117}
118
1c090810
DC
119fn get_all_snapshot_files(
120 store: &DataStore,
121 info: &BackupInfo,
70030b43 122) -> Result<(BackupManifest, Vec<BackupContent>), Error> {
9a37bd6c 123 let (manifest, mut files) = read_backup_index(store, &info.backup_dir)?;
1c090810
DC
124
125 let file_set = files.iter().fold(HashSet::new(), |mut acc, item| {
126 acc.insert(item.filename.clone());
127 acc
128 });
129
130 for file in &info.files {
dc7a5b34
TL
131 if file_set.contains(file) {
132 continue;
133 }
f28d9088
WB
134 files.push(BackupContent {
135 filename: file.to_string(),
136 size: None,
137 crypt_mode: None,
138 });
1c090810
DC
139 }
140
70030b43 141 Ok((manifest, files))
1c090810
DC
142}
143
b31c8019
DM
144#[api(
145 input: {
146 properties: {
147 store: {
148 schema: DATASTORE_SCHEMA,
149 },
150 },
151 },
7b570c17 152 returns: pbs_api_types::ADMIN_DATASTORE_LIST_GROUPS_RETURN_TYPE,
bb34b589 153 access: {
54552dda
DM
154 permission: &Permission::Privilege(
155 &["datastore", "{store}"],
156 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP,
157 true),
bb34b589 158 },
b31c8019
DM
159)]
160/// List backup groups.
b2362a12 161pub fn list_groups(
b31c8019 162 store: String,
54552dda 163 rpcenv: &mut dyn RpcEnvironment,
b31c8019 164) -> Result<Vec<GroupListItem>, Error> {
e6dc35ac 165 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
54552dda 166 let user_info = CachedUserInfo::new()?;
e6dc35ac 167 let user_privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
54552dda 168
e9d2fc93 169 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
0d08fcee
FG
170 let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
171
249dde8b 172 datastore
8c74349b 173 .iter_backup_groups(Default::default())? // FIXME: Namespaces and recursion parameters!
249dde8b
TL
174 .try_fold(Vec::new(), |mut group_info, group| {
175 let group = group?;
db87d93e 176 let owner = match datastore.get_owner(group.as_ref()) {
249dde8b
TL
177 Ok(auth_id) => auth_id,
178 Err(err) => {
179 let id = &store;
180 eprintln!("Failed to get owner of group '{}/{}' - {}", id, group, err);
181 return Ok(group_info);
dc7a5b34 182 }
249dde8b
TL
183 };
184 if !list_all && check_backup_owner(&owner, &auth_id).is_err() {
185 return Ok(group_info);
186 }
0d08fcee 187
6da20161 188 let snapshots = match group.list_backups() {
249dde8b
TL
189 Ok(snapshots) => snapshots,
190 Err(_) => return Ok(group_info),
191 };
0d08fcee 192
249dde8b
TL
193 let backup_count: u64 = snapshots.len() as u64;
194 if backup_count == 0 {
195 return Ok(group_info);
196 }
0d08fcee 197
249dde8b
TL
198 let last_backup = snapshots
199 .iter()
200 .fold(&snapshots[0], |a, b| {
201 if a.is_finished() && a.backup_dir.backup_time() > b.backup_dir.backup_time() {
202 a
203 } else {
204 b
205 }
206 })
207 .to_owned();
208
db87d93e 209 let note_path = get_group_note_path(&datastore, group.as_ref());
249dde8b
TL
210 let comment = file_read_firstline(&note_path).ok();
211
212 group_info.push(GroupListItem {
988d575d 213 backup: group.into(),
249dde8b
TL
214 last_backup: last_backup.backup_dir.backup_time(),
215 owner: Some(owner),
216 backup_count,
217 files: last_backup.files,
218 comment,
0d08fcee
FG
219 });
220
249dde8b
TL
221 Ok(group_info)
222 })
812c6f87 223}
8f579717 224
f32791b4
DC
225#[api(
226 input: {
227 properties: {
988d575d 228 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
229 group: {
230 type: pbs_api_types::BackupGroup,
231 flatten: true,
232 },
f32791b4
DC
233 },
234 },
235 access: {
236 permission: &Permission::Privilege(
237 &["datastore", "{store}"],
238 PRIV_DATASTORE_MODIFY| PRIV_DATASTORE_PRUNE,
239 true),
240 },
241)]
242/// Delete backup group including all snapshots.
243pub fn delete_group(
244 store: String,
8c74349b 245 group: pbs_api_types::BackupGroup,
f32791b4
DC
246 _info: &ApiMethod,
247 rpcenv: &mut dyn RpcEnvironment,
248) -> Result<Value, Error> {
f32791b4
DC
249 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
250
e9d2fc93 251 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
f32791b4
DC
252
253 check_priv_or_backup_owner(&datastore, &group, &auth_id, PRIV_DATASTORE_MODIFY)?;
254
5cc7d891 255 if !datastore.remove_backup_group(&group)? {
171a00ca 256 bail!("group only partially deleted due to protected snapshots");
5cc7d891 257 }
f32791b4
DC
258
259 Ok(Value::Null)
260}
261
09b1f7b2
DM
262#[api(
263 input: {
264 properties: {
988d575d 265 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
266 backup_dir: {
267 type: pbs_api_types::BackupDir,
268 flatten: true,
269 },
09b1f7b2
DM
270 },
271 },
7b570c17 272 returns: pbs_api_types::ADMIN_DATASTORE_LIST_SNAPSHOT_FILES_RETURN_TYPE,
bb34b589 273 access: {
54552dda
DM
274 permission: &Permission::Privilege(
275 &["datastore", "{store}"],
276 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
277 true),
bb34b589 278 },
09b1f7b2
DM
279)]
280/// List snapshot files.
ea5f547f 281pub fn list_snapshot_files(
09b1f7b2 282 store: String,
8c74349b 283 backup_dir: pbs_api_types::BackupDir,
01a13423 284 _info: &ApiMethod,
54552dda 285 rpcenv: &mut dyn RpcEnvironment,
09b1f7b2 286) -> Result<Vec<BackupContent>, Error> {
e6dc35ac 287 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
e9d2fc93 288 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
54552dda 289
8c74349b 290 let snapshot = datastore.backup_dir(backup_dir)?;
01a13423 291
dc7a5b34
TL
292 check_priv_or_backup_owner(
293 &datastore,
db87d93e 294 snapshot.as_ref(),
dc7a5b34
TL
295 &auth_id,
296 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ,
297 )?;
54552dda 298
6da20161 299 let info = BackupInfo::new(snapshot)?;
01a13423 300
70030b43
DM
301 let (_manifest, files) = get_all_snapshot_files(&datastore, &info)?;
302
303 Ok(files)
01a13423
DM
304}
305
68a6a0ee
DM
306#[api(
307 input: {
308 properties: {
988d575d 309 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
310 backup_dir: {
311 type: pbs_api_types::BackupDir,
312 flatten: true,
313 },
68a6a0ee
DM
314 },
315 },
bb34b589 316 access: {
54552dda
DM
317 permission: &Permission::Privilege(
318 &["datastore", "{store}"],
319 PRIV_DATASTORE_MODIFY| PRIV_DATASTORE_PRUNE,
320 true),
bb34b589 321 },
68a6a0ee
DM
322)]
323/// Delete backup snapshot.
bf78f708 324pub fn delete_snapshot(
68a6a0ee 325 store: String,
8c74349b 326 backup_dir: pbs_api_types::BackupDir,
6f62c924 327 _info: &ApiMethod,
54552dda 328 rpcenv: &mut dyn RpcEnvironment,
6f62c924 329) -> Result<Value, Error> {
e6dc35ac 330 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
54552dda 331
e9d2fc93 332 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
8c74349b 333 let snapshot = datastore.backup_dir(backup_dir)?;
6f62c924 334
dc7a5b34
TL
335 check_priv_or_backup_owner(
336 &datastore,
db87d93e 337 snapshot.as_ref(),
dc7a5b34
TL
338 &auth_id,
339 PRIV_DATASTORE_MODIFY,
340 )?;
54552dda 341
db87d93e 342 datastore.remove_backup_dir(snapshot.as_ref(), false)?;
6f62c924
DM
343
344 Ok(Value::Null)
345}
346
fc189b19 347#[api(
b7c3eaa9 348 streaming: true,
fc189b19
DM
349 input: {
350 properties: {
988d575d 351 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
352 "backup-ns": {
353 type: BackupNamespace,
354 optional: true,
355 },
fc189b19
DM
356 "backup-type": {
357 optional: true,
988d575d 358 type: BackupType,
fc189b19
DM
359 },
360 "backup-id": {
361 optional: true,
362 schema: BACKUP_ID_SCHEMA,
363 },
364 },
365 },
7b570c17 366 returns: pbs_api_types::ADMIN_DATASTORE_LIST_SNAPSHOTS_RETURN_TYPE,
bb34b589 367 access: {
54552dda
DM
368 permission: &Permission::Privilege(
369 &["datastore", "{store}"],
370 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP,
371 true),
bb34b589 372 },
fc189b19
DM
373)]
374/// List backup snapshots.
dc7a5b34 375pub fn list_snapshots(
54552dda 376 store: String,
8c74349b 377 backup_ns: Option<BackupNamespace>,
988d575d 378 backup_type: Option<BackupType>,
54552dda
DM
379 backup_id: Option<String>,
380 _param: Value,
184f17af 381 _info: &ApiMethod,
54552dda 382 rpcenv: &mut dyn RpcEnvironment,
fc189b19 383) -> Result<Vec<SnapshotListItem>, Error> {
e6dc35ac 384 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
54552dda 385 let user_info = CachedUserInfo::new()?;
e6dc35ac 386 let user_privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
184f17af 387
0d08fcee
FG
388 let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
389
e9d2fc93 390 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
184f17af 391
8c74349b
WB
392 let backup_ns = backup_ns.unwrap_or_default();
393
249dde8b
TL
394 // FIXME: filter also owner before collecting, for doing that nicely the owner should move into
395 // backup group and provide an error free (Err -> None) accessor
0d08fcee 396 let groups = match (backup_type, backup_id) {
db87d93e 397 (Some(backup_type), Some(backup_id)) => {
8c74349b 398 vec![datastore.backup_group_from_parts(backup_ns, backup_type, backup_id)]
db87d93e 399 }
8c74349b 400 // FIXME: Recursion
7d9cb8c4 401 (Some(backup_type), None) => datastore
8c74349b 402 .iter_backup_groups_ok(backup_ns)?
dc7a5b34
TL
403 .filter(|group| group.backup_type() == backup_type)
404 .collect(),
8c74349b 405 // FIXME: Recursion
7d9cb8c4 406 (None, Some(backup_id)) => datastore
8c74349b 407 .iter_backup_groups_ok(backup_ns)?
dc7a5b34
TL
408 .filter(|group| group.backup_id() == backup_id)
409 .collect(),
8c74349b
WB
410 // FIXME: Recursion
411 (None, None) => datastore.list_backup_groups(backup_ns)?,
0d08fcee 412 };
54552dda 413
0d08fcee 414 let info_to_snapshot_list_item = |group: &BackupGroup, owner, info: BackupInfo| {
988d575d
WB
415 let backup = pbs_api_types::BackupDir {
416 group: group.into(),
417 time: info.backup_dir.backup_time(),
418 };
6da20161 419 let protected = info.backup_dir.is_protected();
1c090810 420
79c53595 421 match get_all_snapshot_files(&datastore, &info) {
70030b43 422 Ok((manifest, files)) => {
70030b43
DM
423 // extract the first line from notes
424 let comment: Option<String> = manifest.unprotected["notes"]
425 .as_str()
426 .and_then(|notes| notes.lines().next())
427 .map(String::from);
428
035c40e6
FG
429 let fingerprint = match manifest.fingerprint() {
430 Ok(fp) => fp,
431 Err(err) => {
432 eprintln!("error parsing fingerprint: '{}'", err);
433 None
dc7a5b34 434 }
035c40e6
FG
435 };
436
79c53595 437 let verification = manifest.unprotected["verify_state"].clone();
dc7a5b34
TL
438 let verification: Option<SnapshotVerifyState> =
439 match serde_json::from_value(verification) {
440 Ok(verify) => verify,
441 Err(err) => {
442 eprintln!("error parsing verification state : '{}'", err);
443 None
444 }
445 };
3b2046d2 446
0d08fcee
FG
447 let size = Some(files.iter().map(|x| x.size.unwrap_or(0)).sum());
448
79c53595 449 SnapshotListItem {
988d575d 450 backup,
79c53595
FG
451 comment,
452 verification,
035c40e6 453 fingerprint,
79c53595
FG
454 files,
455 size,
456 owner,
02db7267 457 protected,
79c53595 458 }
dc7a5b34 459 }
1c090810
DC
460 Err(err) => {
461 eprintln!("error during snapshot file listing: '{}'", err);
79c53595 462 let files = info
dc7a5b34
TL
463 .files
464 .into_iter()
465 .map(|filename| BackupContent {
466 filename,
467 size: None,
468 crypt_mode: None,
469 })
470 .collect();
79c53595
FG
471
472 SnapshotListItem {
988d575d 473 backup,
79c53595
FG
474 comment: None,
475 verification: None,
035c40e6 476 fingerprint: None,
79c53595
FG
477 files,
478 size: None,
479 owner,
02db7267 480 protected,
79c53595 481 }
dc7a5b34 482 }
0d08fcee
FG
483 }
484 };
184f17af 485
dc7a5b34 486 groups.iter().try_fold(Vec::new(), |mut snapshots, group| {
db87d93e 487 let owner = match datastore.get_owner(group.as_ref()) {
dc7a5b34
TL
488 Ok(auth_id) => auth_id,
489 Err(err) => {
490 eprintln!(
491 "Failed to get owner of group '{}/{}' - {}",
492 &store, group, err
493 );
0d08fcee
FG
494 return Ok(snapshots);
495 }
dc7a5b34 496 };
0d08fcee 497
dc7a5b34
TL
498 if !list_all && check_backup_owner(&owner, &auth_id).is_err() {
499 return Ok(snapshots);
500 }
0d08fcee 501
6da20161 502 let group_backups = group.list_backups()?;
0d08fcee 503
dc7a5b34
TL
504 snapshots.extend(
505 group_backups
506 .into_iter()
507 .map(|info| info_to_snapshot_list_item(group, Some(owner.clone()), info)),
508 );
509
510 Ok(snapshots)
511 })
184f17af
DM
512}
513
6da20161
WB
514fn get_snapshots_count(
515 store: &Arc<DataStore>,
516 filter_owner: Option<&Authid>,
517) -> Result<Counts, Error> {
7d9cb8c4 518 store
8c74349b 519 .iter_backup_groups_ok(Default::default())? // FIXME: Recurse!
fdfcb74d 520 .filter(|group| {
db87d93e 521 let owner = match store.get_owner(group.as_ref()) {
fdfcb74d
FG
522 Ok(owner) => owner,
523 Err(err) => {
72f81545
TL
524 let id = store.name();
525 eprintln!("Failed to get owner of group '{}/{}' - {}", id, group, err);
fdfcb74d 526 return false;
dc7a5b34 527 }
fdfcb74d 528 };
14e08625 529
fdfcb74d
FG
530 match filter_owner {
531 Some(filter) => check_backup_owner(&owner, filter).is_ok(),
532 None => true,
533 }
534 })
535 .try_fold(Counts::default(), |mut counts, group| {
6da20161 536 let snapshot_count = group.list_backups()?.len() as u64;
fdfcb74d 537
72f81545 538 // only include groups with snapshots, counting/displaying emtpy groups can confuse
b44483a8
DM
539 if snapshot_count > 0 {
540 let type_count = match group.backup_type() {
988d575d
WB
541 BackupType::Ct => counts.ct.get_or_insert(Default::default()),
542 BackupType::Vm => counts.vm.get_or_insert(Default::default()),
543 BackupType::Host => counts.host.get_or_insert(Default::default()),
b44483a8 544 };
14e08625 545
b44483a8
DM
546 type_count.groups += 1;
547 type_count.snapshots += snapshot_count;
548 }
16f9f244 549
fdfcb74d
FG
550 Ok(counts)
551 })
16f9f244
DC
552}
553
1dc117bb
DM
554#[api(
555 input: {
556 properties: {
557 store: {
558 schema: DATASTORE_SCHEMA,
559 },
98afc7b1
FG
560 verbose: {
561 type: bool,
562 default: false,
563 optional: true,
564 description: "Include additional information like snapshot counts and GC status.",
565 },
1dc117bb 566 },
98afc7b1 567
1dc117bb
DM
568 },
569 returns: {
14e08625 570 type: DataStoreStatus,
1dc117bb 571 },
bb34b589 572 access: {
54552dda 573 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
bb34b589 574 },
1dc117bb
DM
575)]
576/// Get datastore status.
ea5f547f 577pub fn status(
1dc117bb 578 store: String,
98afc7b1 579 verbose: bool,
0eecf38f 580 _info: &ApiMethod,
fdfcb74d 581 rpcenv: &mut dyn RpcEnvironment,
14e08625 582) -> Result<DataStoreStatus, Error> {
e9d2fc93 583 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
14e08625 584 let storage = crate::tools::disks::disk_usage(&datastore.base_path())?;
fdfcb74d
FG
585 let (counts, gc_status) = if verbose {
586 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
587 let user_info = CachedUserInfo::new()?;
588
589 let store_privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
590 let filter_owner = if store_privs & PRIV_DATASTORE_AUDIT != 0 {
591 None
592 } else {
593 Some(&auth_id)
594 };
595
596 let counts = Some(get_snapshots_count(&datastore, filter_owner)?);
597 let gc_status = Some(datastore.last_gc_status());
598
599 (counts, gc_status)
600 } else {
601 (None, None)
98afc7b1 602 };
16f9f244 603
14e08625
DC
604 Ok(DataStoreStatus {
605 total: storage.total,
606 used: storage.used,
607 avail: storage.avail,
608 gc_status,
609 counts,
610 })
0eecf38f
DM
611}
612
c2009e53
DM
613#[api(
614 input: {
615 properties: {
616 store: {
617 schema: DATASTORE_SCHEMA,
618 },
8c74349b
WB
619 "backup-ns": {
620 type: BackupNamespace,
621 optional: true,
622 },
c2009e53 623 "backup-type": {
988d575d 624 type: BackupType,
c2009e53
DM
625 optional: true,
626 },
627 "backup-id": {
628 schema: BACKUP_ID_SCHEMA,
629 optional: true,
630 },
dcbf29e7
HL
631 "ignore-verified": {
632 schema: IGNORE_VERIFIED_BACKUPS_SCHEMA,
633 optional: true,
634 },
635 "outdated-after": {
636 schema: VERIFICATION_OUTDATED_AFTER_SCHEMA,
637 optional: true,
638 },
c2009e53
DM
639 "backup-time": {
640 schema: BACKUP_TIME_SCHEMA,
641 optional: true,
642 },
643 },
644 },
645 returns: {
646 schema: UPID_SCHEMA,
647 },
648 access: {
09f6a240 649 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_VERIFY | PRIV_DATASTORE_BACKUP, true),
c2009e53
DM
650 },
651)]
652/// Verify backups.
653///
654/// This function can verify a single backup snapshot, all backup from a backup group,
655/// or all backups in the datastore.
656pub fn verify(
657 store: String,
8c74349b 658 backup_ns: Option<BackupNamespace>,
988d575d 659 backup_type: Option<BackupType>,
c2009e53
DM
660 backup_id: Option<String>,
661 backup_time: Option<i64>,
dcbf29e7
HL
662 ignore_verified: Option<bool>,
663 outdated_after: Option<i64>,
c2009e53
DM
664 rpcenv: &mut dyn RpcEnvironment,
665) -> Result<Value, Error> {
e9d2fc93 666 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
dcbf29e7 667 let ignore_verified = ignore_verified.unwrap_or(true);
c2009e53 668
09f6a240 669 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
8ea00f6e 670 let worker_id;
c2009e53
DM
671
672 let mut backup_dir = None;
673 let mut backup_group = None;
133042b5 674 let mut worker_type = "verify";
c2009e53 675
8c74349b
WB
676 // FIXME: Recursion
677 // FIXME: Namespaces and worker ID, could this be an issue?
678 let backup_ns = backup_ns.unwrap_or_default();
679
c2009e53
DM
680 match (backup_type, backup_id, backup_time) {
681 (Some(backup_type), Some(backup_id), Some(backup_time)) => {
dc7a5b34 682 worker_id = format!(
8c74349b
WB
683 "{}:{}/{}/{}/{:08X}",
684 store,
685 backup_ns.display_as_path(),
686 backup_type,
687 backup_id,
688 backup_time
dc7a5b34 689 );
8c74349b
WB
690 let dir =
691 datastore.backup_dir_from_parts(backup_ns, backup_type, backup_id, backup_time)?;
09f6a240 692
db87d93e 693 check_priv_or_backup_owner(&datastore, dir.as_ref(), &auth_id, PRIV_DATASTORE_VERIFY)?;
09f6a240 694
c2009e53 695 backup_dir = Some(dir);
133042b5 696 worker_type = "verify_snapshot";
c2009e53
DM
697 }
698 (Some(backup_type), Some(backup_id), None) => {
8c74349b
WB
699 worker_id = format!(
700 "{}:{}/{}/{}",
701 store,
702 backup_ns.display_as_path(),
703 backup_type,
704 backup_id
705 );
706 let group = pbs_api_types::BackupGroup::from((backup_ns, backup_type, backup_id));
09f6a240
FG
707
708 check_priv_or_backup_owner(&datastore, &group, &auth_id, PRIV_DATASTORE_VERIFY)?;
709
6b0c6492 710 backup_group = Some(datastore.backup_group(group));
133042b5 711 worker_type = "verify_group";
c2009e53
DM
712 }
713 (None, None, None) => {
8ea00f6e 714 worker_id = store.clone();
c2009e53 715 }
5a718dce 716 _ => bail!("parameters do not specify a backup group or snapshot"),
c2009e53
DM
717 }
718
39735609 719 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
c2009e53
DM
720
721 let upid_str = WorkerTask::new_thread(
133042b5 722 worker_type,
44288184 723 Some(worker_id),
049a22a3 724 auth_id.to_string(),
e7cb4dc5
WB
725 to_stdout,
726 move |worker| {
9c26a3d6 727 let verify_worker = crate::backup::VerifyWorker::new(worker.clone(), datastore);
adfdc369 728 let failed_dirs = if let Some(backup_dir) = backup_dir {
adfdc369 729 let mut res = Vec::new();
f6b1d1cc 730 if !verify_backup_dir(
9c26a3d6 731 &verify_worker,
f6b1d1cc 732 &backup_dir,
f6b1d1cc 733 worker.upid().clone(),
dc7a5b34 734 Some(&move |manifest| verify_filter(ignore_verified, outdated_after, manifest)),
f6b1d1cc 735 )? {
adfdc369
DC
736 res.push(backup_dir.to_string());
737 }
738 res
c2009e53 739 } else if let Some(backup_group) = backup_group {
7e25b9aa 740 let failed_dirs = verify_backup_group(
9c26a3d6 741 &verify_worker,
63d9aca9 742 &backup_group,
7e25b9aa 743 &mut StoreProgress::new(1),
f6b1d1cc 744 worker.upid(),
dc7a5b34 745 Some(&move |manifest| verify_filter(ignore_verified, outdated_after, manifest)),
63d9aca9
DM
746 )?;
747 failed_dirs
c2009e53 748 } else {
dc7a5b34 749 let privs = CachedUserInfo::new()?.lookup_privs(&auth_id, &["datastore", &store]);
09f6a240
FG
750
751 let owner = if privs & PRIV_DATASTORE_VERIFY == 0 {
752 Some(auth_id)
753 } else {
754 None
755 };
756
dcbf29e7
HL
757 verify_all_backups(
758 &verify_worker,
759 worker.upid(),
760 owner,
dc7a5b34 761 Some(&move |manifest| verify_filter(ignore_verified, outdated_after, manifest)),
dcbf29e7 762 )?
c2009e53 763 };
3984a5fd 764 if !failed_dirs.is_empty() {
1ec0d70d 765 task_log!(worker, "Failed to verify the following snapshots/groups:");
adfdc369 766 for dir in failed_dirs {
1ec0d70d 767 task_log!(worker, "\t{}", dir);
adfdc369 768 }
1ffe0301 769 bail!("verification failed - please check the log for details");
c2009e53
DM
770 }
771 Ok(())
e7cb4dc5
WB
772 },
773 )?;
c2009e53
DM
774
775 Ok(json!(upid_str))
776}
777
0a240aaa
DC
778#[api(
779 input: {
780 properties: {
8c74349b
WB
781 group: {
782 type: pbs_api_types::BackupGroup,
783 flatten: true,
784 },
0a240aaa
DC
785 "dry-run": {
786 optional: true,
787 type: bool,
788 default: false,
789 description: "Just show what prune would do, but do not delete anything.",
790 },
791 "prune-options": {
792 type: PruneOptions,
793 flatten: true,
794 },
795 store: {
796 schema: DATASTORE_SCHEMA,
797 },
798 },
799 },
7b570c17 800 returns: pbs_api_types::ADMIN_DATASTORE_PRUNE_RETURN_TYPE,
0a240aaa
DC
801 access: {
802 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE, true),
803 },
804)]
9805207a 805/// Prune a group on the datastore
bf78f708 806pub fn prune(
8c74349b 807 group: pbs_api_types::BackupGroup,
0a240aaa
DC
808 dry_run: bool,
809 prune_options: PruneOptions,
810 store: String,
811 _param: Value,
54552dda 812 rpcenv: &mut dyn RpcEnvironment,
83b7db02 813) -> Result<Value, Error> {
e6dc35ac 814 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
54552dda 815
e9d2fc93 816 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
54552dda 817
8c74349b 818 let group = datastore.backup_group(group);
db87d93e
WB
819
820 check_priv_or_backup_owner(&datastore, group.as_ref(), &auth_id, PRIV_DATASTORE_MODIFY)?;
83b7db02 821
8c74349b 822 let worker_id = format!("{}:{}", store, group);
503995c7 823
dda70154
DM
824 let mut prune_result = Vec::new();
825
6da20161 826 let list = group.list_backups()?;
dda70154
DM
827
828 let mut prune_info = compute_prune_info(list, &prune_options)?;
829
830 prune_info.reverse(); // delete older snapshots first
831
89725197 832 let keep_all = !pbs_datastore::prune::keeps_something(&prune_options);
dda70154
DM
833
834 if dry_run {
02db7267
DC
835 for (info, mark) in prune_info {
836 let keep = keep_all || mark.keep();
dda70154 837
33f2c2a1 838 let mut result = json!({
db87d93e
WB
839 "backup-type": info.backup_dir.backup_type(),
840 "backup-id": info.backup_dir.backup_id(),
841 "backup-time": info.backup_dir.backup_time(),
dda70154 842 "keep": keep,
02db7267 843 "protected": mark.protected(),
33f2c2a1
WB
844 });
845 let ns = info.backup_dir.backup_ns();
846 if !ns.is_root() {
847 result["backup-ns"] = serde_json::to_value(ns)?;
848 }
849 prune_result.push(result);
dda70154
DM
850 }
851 return Ok(json!(prune_result));
852 }
853
163e9bbe 854 // We use a WorkerTask just to have a task log, but run synchrounously
049a22a3 855 let worker = WorkerTask::new("prune", Some(worker_id), auth_id.to_string(), true)?;
dda70154 856
f1539300 857 if keep_all {
1ec0d70d 858 task_log!(worker, "No prune selection - keeping all files.");
f1539300 859 } else {
dc7a5b34
TL
860 task_log!(
861 worker,
862 "retention options: {}",
863 pbs_datastore::prune::cli_options_string(&prune_options)
864 );
865 task_log!(
866 worker,
8c74349b 867 "Starting prune on store \"{}\" group \"{}\"",
dc7a5b34 868 store,
8c74349b 869 group,
dc7a5b34 870 );
f1539300 871 }
3b03abfe 872
02db7267
DC
873 for (info, mark) in prune_info {
874 let keep = keep_all || mark.keep();
dda70154 875
f1539300
SR
876 let backup_time = info.backup_dir.backup_time();
877 let timestamp = info.backup_dir.backup_time_string();
db87d93e
WB
878 let group: &pbs_api_types::BackupGroup = info.backup_dir.as_ref();
879
880 let msg = format!("{}/{}/{} {}", group.ty, group.id, timestamp, mark,);
f1539300 881
1ec0d70d 882 task_log!(worker, "{}", msg);
f1539300 883
33f2c2a1 884 let mut result = json!({
db87d93e
WB
885 "backup-type": group.ty,
886 "backup-id": group.id,
f1539300
SR
887 "backup-time": backup_time,
888 "keep": keep,
02db7267 889 "protected": mark.protected(),
33f2c2a1
WB
890 });
891 if !group.ns.is_root() {
892 result["backup-ns"] = serde_json::to_value(&group.ns)?;
893 }
894 prune_result.push(result);
f1539300
SR
895
896 if !(dry_run || keep) {
db87d93e 897 if let Err(err) = datastore.remove_backup_dir(info.backup_dir.as_ref(), false) {
1ec0d70d
DM
898 task_warn!(
899 worker,
900 "failed to remove dir {:?}: {}",
901 info.backup_dir.relative_path(),
902 err,
f1539300 903 );
8f0b4c1f 904 }
8f579717 905 }
f1539300 906 }
dd8e744f 907
f1539300 908 worker.log_result(&Ok(()));
83b7db02 909
dda70154 910 Ok(json!(prune_result))
83b7db02
DM
911}
912
9805207a
DC
913#[api(
914 input: {
915 properties: {
916 "dry-run": {
917 optional: true,
918 type: bool,
919 default: false,
920 description: "Just show what prune would do, but do not delete anything.",
921 },
922 "prune-options": {
923 type: PruneOptions,
924 flatten: true,
925 },
926 store: {
927 schema: DATASTORE_SCHEMA,
928 },
929 },
930 },
931 returns: {
932 schema: UPID_SCHEMA,
933 },
934 access: {
935 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE, true),
936 },
937)]
938/// Prune the datastore
939pub fn prune_datastore(
940 dry_run: bool,
941 prune_options: PruneOptions,
942 store: String,
943 _param: Value,
944 rpcenv: &mut dyn RpcEnvironment,
945) -> Result<String, Error> {
9805207a
DC
946 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
947
e9d2fc93 948 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
9805207a 949
bfa942c0
DC
950 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
951
9805207a
DC
952 let upid_str = WorkerTask::new_thread(
953 "prune",
954 Some(store.clone()),
049a22a3 955 auth_id.to_string(),
bfa942c0 956 to_stdout,
dc7a5b34
TL
957 move |worker| {
958 crate::server::prune_datastore(
959 worker,
960 auth_id,
961 prune_options,
962 &store,
963 datastore,
964 dry_run,
965 )
966 },
9805207a
DC
967 )?;
968
969 Ok(upid_str)
970}
971
dfc58d47
DM
972#[api(
973 input: {
974 properties: {
975 store: {
976 schema: DATASTORE_SCHEMA,
977 },
978 },
979 },
980 returns: {
981 schema: UPID_SCHEMA,
982 },
bb34b589 983 access: {
54552dda 984 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY, false),
bb34b589 985 },
dfc58d47
DM
986)]
987/// Start garbage collection.
bf78f708 988pub fn start_garbage_collection(
dfc58d47 989 store: String,
6049b71f 990 _info: &ApiMethod,
dd5495d6 991 rpcenv: &mut dyn RpcEnvironment,
6049b71f 992) -> Result<Value, Error> {
e9d2fc93 993 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
e6dc35ac 994 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
15e9b4ed 995
dc7a5b34 996 let job = Job::new("garbage_collection", &store)
4fdf5ddf 997 .map_err(|_| format_err!("garbage collection already running"))?;
15e9b4ed 998
39735609 999 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
15e9b4ed 1000
dc7a5b34
TL
1001 let upid_str =
1002 crate::server::do_garbage_collection_job(job, datastore, &auth_id, None, to_stdout)
1003 .map_err(|err| {
1004 format_err!(
1005 "unable to start garbage collection job on datastore {} - {}",
1006 store,
1007 err
1008 )
1009 })?;
0f778e06
DM
1010
1011 Ok(json!(upid_str))
15e9b4ed
DM
1012}
1013
a92830dc
DM
1014#[api(
1015 input: {
1016 properties: {
1017 store: {
1018 schema: DATASTORE_SCHEMA,
1019 },
1020 },
1021 },
1022 returns: {
1023 type: GarbageCollectionStatus,
bb34b589
DM
1024 },
1025 access: {
1026 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
1027 },
a92830dc
DM
1028)]
1029/// Garbage collection status.
5eeea607 1030pub fn garbage_collection_status(
a92830dc 1031 store: String,
6049b71f 1032 _info: &ApiMethod,
dd5495d6 1033 _rpcenv: &mut dyn RpcEnvironment,
a92830dc 1034) -> Result<GarbageCollectionStatus, Error> {
e9d2fc93 1035 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
f2b99c34 1036
f2b99c34 1037 let status = datastore.last_gc_status();
691c89a0 1038
a92830dc 1039 Ok(status)
691c89a0
DM
1040}
1041
bb34b589 1042#[api(
30fb6025
DM
1043 returns: {
1044 description: "List the accessible datastores.",
1045 type: Array,
9b93c620 1046 items: { type: DataStoreListItem },
30fb6025 1047 },
bb34b589 1048 access: {
54552dda 1049 permission: &Permission::Anybody,
bb34b589
DM
1050 },
1051)]
1052/// Datastore list
bf78f708 1053pub fn get_datastore_list(
6049b71f
DM
1054 _param: Value,
1055 _info: &ApiMethod,
54552dda 1056 rpcenv: &mut dyn RpcEnvironment,
455e5f71 1057) -> Result<Vec<DataStoreListItem>, Error> {
e7d4be9d 1058 let (config, _digest) = pbs_config::datastore::config()?;
15e9b4ed 1059
e6dc35ac 1060 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
54552dda
DM
1061 let user_info = CachedUserInfo::new()?;
1062
30fb6025 1063 let mut list = Vec::new();
54552dda 1064
30fb6025 1065 for (store, (_, data)) in &config.sections {
9a37bd6c 1066 let user_privs = user_info.lookup_privs(&auth_id, &["datastore", store]);
dc7a5b34 1067 let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP)) != 0;
30fb6025 1068 if allowed {
dc7a5b34
TL
1069 list.push(DataStoreListItem {
1070 store: store.clone(),
1071 comment: data["comment"].as_str().map(String::from),
e022d13c 1072 maintenance: data["maintenance-mode"].as_str().map(String::from),
dc7a5b34 1073 });
30fb6025 1074 }
54552dda
DM
1075 }
1076
44288184 1077 Ok(list)
15e9b4ed
DM
1078}
1079
0ab08ac9
DM
1080#[sortable]
1081pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new(
1082 &ApiHandler::AsyncHttp(&download_file),
1083 &ObjectSchema::new(
1084 "Download single raw file from backup snapshot.",
1085 &sorted!([
66c49c21 1086 ("store", false, &DATASTORE_SCHEMA),
33f2c2a1 1087 ("backup-ns", true, &BACKUP_NAMESPACE_SCHEMA),
0ab08ac9 1088 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
dc7a5b34 1089 ("backup-id", false, &BACKUP_ID_SCHEMA),
0ab08ac9 1090 ("backup-time", false, &BACKUP_TIME_SCHEMA),
4191018c 1091 ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA),
0ab08ac9 1092 ]),
dc7a5b34
TL
1093 ),
1094)
1095.access(
1096 None,
1097 &Permission::Privilege(
1098 &["datastore", "{store}"],
1099 PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
1100 true,
1101 ),
54552dda 1102);
691c89a0 1103
bf78f708 1104pub fn download_file(
9e47c0a5
DM
1105 _parts: Parts,
1106 _req_body: Body,
1107 param: Value,
255f378a 1108 _info: &ApiMethod,
54552dda 1109 rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 1110) -> ApiResponseFuture {
ad51d02a 1111 async move {
3c8c2827 1112 let store = required_string_param(&param, "store")?;
e9d2fc93 1113 let datastore = DataStore::lookup_datastore(store, Some(Operation::Read))?;
f14a8c9a 1114
e6dc35ac 1115 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
54552dda 1116
3c8c2827 1117 let file_name = required_string_param(&param, "file-name")?.to_owned();
9e47c0a5 1118
8c74349b 1119 let backup_dir = datastore.backup_dir(Deserialize::deserialize(&param)?)?;
54552dda 1120
dc7a5b34
TL
1121 check_priv_or_backup_owner(
1122 &datastore,
db87d93e 1123 backup_dir.as_ref(),
dc7a5b34
TL
1124 &auth_id,
1125 PRIV_DATASTORE_READ,
1126 )?;
54552dda 1127
dc7a5b34
TL
1128 println!(
1129 "Download {} from {} ({}/{})",
1130 file_name, store, backup_dir, file_name
1131 );
9e47c0a5 1132
ad51d02a
DM
1133 let mut path = datastore.base_path();
1134 path.push(backup_dir.relative_path());
1135 path.push(&file_name);
1136
ba694720 1137 let file = tokio::fs::File::open(&path)
8aa67ee7
WB
1138 .await
1139 .map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
ad51d02a 1140
dc7a5b34
TL
1141 let payload =
1142 tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
1143 .map_ok(|bytes| bytes.freeze())
1144 .map_err(move |err| {
1145 eprintln!("error during streaming of '{:?}' - {}", &path, err);
1146 err
1147 });
ad51d02a 1148 let body = Body::wrap_stream(payload);
9e47c0a5 1149
ad51d02a
DM
1150 // fixme: set other headers ?
1151 Ok(Response::builder()
dc7a5b34
TL
1152 .status(StatusCode::OK)
1153 .header(header::CONTENT_TYPE, "application/octet-stream")
1154 .body(body)
1155 .unwrap())
1156 }
1157 .boxed()
9e47c0a5
DM
1158}
1159
6ef9bb59
DC
1160#[sortable]
1161pub const API_METHOD_DOWNLOAD_FILE_DECODED: ApiMethod = ApiMethod::new(
1162 &ApiHandler::AsyncHttp(&download_file_decoded),
1163 &ObjectSchema::new(
1164 "Download single decoded file from backup snapshot. Only works if it's not encrypted.",
1165 &sorted!([
1166 ("store", false, &DATASTORE_SCHEMA),
33f2c2a1 1167 ("backup-ns", true, &BACKUP_NAMESPACE_SCHEMA),
6ef9bb59 1168 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
dc7a5b34 1169 ("backup-id", false, &BACKUP_ID_SCHEMA),
6ef9bb59
DC
1170 ("backup-time", false, &BACKUP_TIME_SCHEMA),
1171 ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA),
1172 ]),
dc7a5b34
TL
1173 ),
1174)
1175.access(
1176 None,
1177 &Permission::Privilege(
1178 &["datastore", "{store}"],
1179 PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
1180 true,
1181 ),
6ef9bb59
DC
1182);
1183
bf78f708 1184pub fn download_file_decoded(
6ef9bb59
DC
1185 _parts: Parts,
1186 _req_body: Body,
1187 param: Value,
1188 _info: &ApiMethod,
1189 rpcenv: Box<dyn RpcEnvironment>,
1190) -> ApiResponseFuture {
6ef9bb59 1191 async move {
3c8c2827 1192 let store = required_string_param(&param, "store")?;
e9d2fc93 1193 let datastore = DataStore::lookup_datastore(store, Some(Operation::Read))?;
6ef9bb59 1194
e6dc35ac 1195 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
6ef9bb59 1196
3c8c2827 1197 let file_name = required_string_param(&param, "file-name")?.to_owned();
6ef9bb59 1198
8c74349b 1199 let backup_dir = datastore.backup_dir(Deserialize::deserialize(&param)?)?;
6ef9bb59 1200
dc7a5b34
TL
1201 check_priv_or_backup_owner(
1202 &datastore,
db87d93e 1203 backup_dir.as_ref(),
dc7a5b34
TL
1204 &auth_id,
1205 PRIV_DATASTORE_READ,
1206 )?;
6ef9bb59 1207
2d55beec 1208 let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
6ef9bb59 1209 for file in files {
f28d9088 1210 if file.filename == file_name && file.crypt_mode == Some(CryptMode::Encrypt) {
6ef9bb59
DC
1211 bail!("cannot decode '{}' - is encrypted", file_name);
1212 }
1213 }
1214
dc7a5b34
TL
1215 println!(
1216 "Download {} from {} ({}/{})",
1217 file_name, store, backup_dir, file_name
1218 );
6ef9bb59
DC
1219
1220 let mut path = datastore.base_path();
1221 path.push(backup_dir.relative_path());
1222 path.push(&file_name);
1223
1224 let extension = file_name.rsplitn(2, '.').next().unwrap();
1225
1226 let body = match extension {
1227 "didx" => {
dc7a5b34
TL
1228 let index = DynamicIndexReader::open(&path).map_err(|err| {
1229 format_err!("unable to read dynamic index '{:?}' - {}", &path, err)
1230 })?;
2d55beec
FG
1231 let (csum, size) = index.compute_csum();
1232 manifest.verify_file(&file_name, &csum, size)?;
6ef9bb59 1233
14f6c9cb 1234 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
1ef6e8b6 1235 let reader = CachedChunkReader::new(chunk_reader, index, 1).seekable();
dc7a5b34
TL
1236 Body::wrap_stream(AsyncReaderStream::new(reader).map_err(move |err| {
1237 eprintln!("error during streaming of '{:?}' - {}", path, err);
1238 err
1239 }))
1240 }
6ef9bb59 1241 "fidx" => {
dc7a5b34
TL
1242 let index = FixedIndexReader::open(&path).map_err(|err| {
1243 format_err!("unable to read fixed index '{:?}' - {}", &path, err)
1244 })?;
6ef9bb59 1245
2d55beec
FG
1246 let (csum, size) = index.compute_csum();
1247 manifest.verify_file(&file_name, &csum, size)?;
1248
14f6c9cb 1249 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
1ef6e8b6 1250 let reader = CachedChunkReader::new(chunk_reader, index, 1).seekable();
dc7a5b34
TL
1251 Body::wrap_stream(
1252 AsyncReaderStream::with_buffer_size(reader, 4 * 1024 * 1024).map_err(
1253 move |err| {
1254 eprintln!("error during streaming of '{:?}' - {}", path, err);
1255 err
1256 },
1257 ),
1258 )
1259 }
6ef9bb59
DC
1260 "blob" => {
1261 let file = std::fs::File::open(&path)
8aa67ee7 1262 .map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
6ef9bb59 1263
2d55beec
FG
1264 // FIXME: load full blob to verify index checksum?
1265
6ef9bb59 1266 Body::wrap_stream(
dc7a5b34
TL
1267 WrappedReaderStream::new(DataBlobReader::new(file, None)?).map_err(
1268 move |err| {
6ef9bb59
DC
1269 eprintln!("error during streaming of '{:?}' - {}", path, err);
1270 err
dc7a5b34
TL
1271 },
1272 ),
6ef9bb59 1273 )
dc7a5b34 1274 }
6ef9bb59
DC
1275 extension => {
1276 bail!("cannot download '{}' files", extension);
dc7a5b34 1277 }
6ef9bb59
DC
1278 };
1279
1280 // fixme: set other headers ?
1281 Ok(Response::builder()
dc7a5b34
TL
1282 .status(StatusCode::OK)
1283 .header(header::CONTENT_TYPE, "application/octet-stream")
1284 .body(body)
1285 .unwrap())
1286 }
1287 .boxed()
6ef9bb59
DC
1288}
1289
552c2259 1290#[sortable]
0ab08ac9
DM
1291pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new(
1292 &ApiHandler::AsyncHttp(&upload_backup_log),
255f378a 1293 &ObjectSchema::new(
54552dda 1294 "Upload the client backup log file into a backup snapshot ('client.log.blob').",
552c2259 1295 &sorted!([
66c49c21 1296 ("store", false, &DATASTORE_SCHEMA),
33f2c2a1 1297 ("backup-ns", true, &BACKUP_NAMESPACE_SCHEMA),
255f378a 1298 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
0ab08ac9 1299 ("backup-id", false, &BACKUP_ID_SCHEMA),
255f378a 1300 ("backup-time", false, &BACKUP_TIME_SCHEMA),
552c2259 1301 ]),
dc7a5b34
TL
1302 ),
1303)
1304.access(
54552dda 1305 Some("Only the backup creator/owner is allowed to do this."),
dc7a5b34 1306 &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_BACKUP, false),
54552dda 1307);
9e47c0a5 1308
bf78f708 1309pub fn upload_backup_log(
07ee2235
DM
1310 _parts: Parts,
1311 req_body: Body,
1312 param: Value,
255f378a 1313 _info: &ApiMethod,
54552dda 1314 rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 1315) -> ApiResponseFuture {
ad51d02a 1316 async move {
3c8c2827 1317 let store = required_string_param(&param, "store")?;
e9d2fc93 1318 let datastore = DataStore::lookup_datastore(store, Some(Operation::Write))?;
07ee2235 1319
dc7a5b34 1320 let file_name = CLIENT_LOG_BLOB_NAME;
07ee2235 1321
8c74349b 1322 let backup_dir = datastore.backup_dir(Deserialize::deserialize(&param)?)?;
07ee2235 1323
e6dc35ac 1324 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
db87d93e 1325 let owner = datastore.get_owner(backup_dir.as_ref())?;
bff85572 1326 check_backup_owner(&owner, &auth_id)?;
54552dda 1327
ad51d02a
DM
1328 let mut path = datastore.base_path();
1329 path.push(backup_dir.relative_path());
1330 path.push(&file_name);
07ee2235 1331
ad51d02a
DM
1332 if path.exists() {
1333 bail!("backup already contains a log.");
1334 }
e128d4e8 1335
8c74349b 1336 println!("Upload backup log to {store}/{backup_dir}/{file_name}");
ad51d02a
DM
1337
1338 let data = req_body
1339 .map_err(Error::from)
1340 .try_fold(Vec::new(), |mut acc, chunk| {
1341 acc.extend_from_slice(&*chunk);
1342 future::ok::<_, Error>(acc)
1343 })
1344 .await?;
1345
39f18b30
DM
1346 // always verify blob/CRC at server side
1347 let blob = DataBlob::load_from_reader(&mut &data[..])?;
1348
e0a19d33 1349 replace_file(&path, blob.raw_data(), CreateOptions::new(), false)?;
ad51d02a
DM
1350
1351 // fixme: use correct formatter
53daae8e 1352 Ok(formatter::JSON_FORMATTER.format_data(Value::Null, &*rpcenv))
dc7a5b34
TL
1353 }
1354 .boxed()
07ee2235
DM
1355}
1356
5b1cfa01
DC
1357#[api(
1358 input: {
1359 properties: {
988d575d 1360 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1361 backup_dir: {
1362 type: pbs_api_types::BackupDir,
1363 flatten: true,
1364 },
5b1cfa01
DC
1365 "filepath": {
1366 description: "Base64 encoded path.",
1367 type: String,
1368 }
1369 },
1370 },
1371 access: {
1372 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, true),
1373 },
1374)]
1375/// Get the entries of the given path of the catalog
bf78f708 1376pub fn catalog(
5b1cfa01 1377 store: String,
8c74349b 1378 backup_dir: pbs_api_types::BackupDir,
5b1cfa01 1379 filepath: String,
5b1cfa01 1380 rpcenv: &mut dyn RpcEnvironment,
227501c0 1381) -> Result<Vec<ArchiveEntry>, Error> {
e9d2fc93 1382 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
5b1cfa01 1383
e6dc35ac 1384 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
5b1cfa01 1385
8c74349b 1386 let backup_dir = datastore.backup_dir(backup_dir)?;
5b1cfa01 1387
dc7a5b34
TL
1388 check_priv_or_backup_owner(
1389 &datastore,
db87d93e 1390 backup_dir.as_ref(),
dc7a5b34
TL
1391 &auth_id,
1392 PRIV_DATASTORE_READ,
1393 )?;
5b1cfa01 1394
9238cdf5
FG
1395 let file_name = CATALOG_NAME;
1396
2d55beec 1397 let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
9238cdf5
FG
1398 for file in files {
1399 if file.filename == file_name && file.crypt_mode == Some(CryptMode::Encrypt) {
1400 bail!("cannot decode '{}' - is encrypted", file_name);
1401 }
1402 }
1403
5b1cfa01
DC
1404 let mut path = datastore.base_path();
1405 path.push(backup_dir.relative_path());
9238cdf5 1406 path.push(file_name);
5b1cfa01
DC
1407
1408 let index = DynamicIndexReader::open(&path)
1409 .map_err(|err| format_err!("unable to read dynamic index '{:?}' - {}", &path, err))?;
1410
2d55beec 1411 let (csum, size) = index.compute_csum();
9a37bd6c 1412 manifest.verify_file(file_name, &csum, size)?;
2d55beec 1413
14f6c9cb 1414 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
5b1cfa01
DC
1415 let reader = BufferedDynamicReader::new(index, chunk_reader);
1416
1417 let mut catalog_reader = CatalogReader::new(reader);
5b1cfa01 1418
5279ee74 1419 let path = if filepath != "root" && filepath != "/" {
227501c0
DC
1420 base64::decode(filepath)?
1421 } else {
1422 vec![b'/']
1423 };
5b1cfa01 1424
86582454 1425 catalog_reader.list_dir_contents(&path)
5b1cfa01
DC
1426}
1427
d33d8f4e
DC
1428#[sortable]
1429pub const API_METHOD_PXAR_FILE_DOWNLOAD: ApiMethod = ApiMethod::new(
1430 &ApiHandler::AsyncHttp(&pxar_file_download),
1431 &ObjectSchema::new(
1ffe0301 1432 "Download single file from pxar file of a backup snapshot. Only works if it's not encrypted.",
d33d8f4e
DC
1433 &sorted!([
1434 ("store", false, &DATASTORE_SCHEMA),
33f2c2a1 1435 ("backup-ns", true, &BACKUP_NAMESPACE_SCHEMA),
d33d8f4e
DC
1436 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
1437 ("backup-id", false, &BACKUP_ID_SCHEMA),
1438 ("backup-time", false, &BACKUP_TIME_SCHEMA),
1439 ("filepath", false, &StringSchema::new("Base64 encoded path").schema()),
984ddb2f 1440 ("tar", true, &BooleanSchema::new("Download as .tar.zst").schema()),
d33d8f4e
DC
1441 ]),
1442 )
1443).access(None, &Permission::Privilege(
1444 &["datastore", "{store}"],
1445 PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
1446 true)
1447);
1448
bf78f708 1449pub fn pxar_file_download(
d33d8f4e
DC
1450 _parts: Parts,
1451 _req_body: Body,
1452 param: Value,
1453 _info: &ApiMethod,
1454 rpcenv: Box<dyn RpcEnvironment>,
1455) -> ApiResponseFuture {
d33d8f4e 1456 async move {
3c8c2827 1457 let store = required_string_param(&param, "store")?;
e9d2fc93 1458 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
d33d8f4e 1459
e6dc35ac 1460 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
d33d8f4e 1461
3c8c2827 1462 let filepath = required_string_param(&param, "filepath")?.to_owned();
d33d8f4e 1463
984ddb2f
DC
1464 let tar = param["tar"].as_bool().unwrap_or(false);
1465
8c74349b 1466 let backup_dir = datastore.backup_dir(Deserialize::deserialize(&param)?)?;
d33d8f4e 1467
dc7a5b34
TL
1468 check_priv_or_backup_owner(
1469 &datastore,
db87d93e 1470 backup_dir.as_ref(),
dc7a5b34
TL
1471 &auth_id,
1472 PRIV_DATASTORE_READ,
1473 )?;
d33d8f4e 1474
d33d8f4e 1475 let mut components = base64::decode(&filepath)?;
3984a5fd 1476 if !components.is_empty() && components[0] == b'/' {
d33d8f4e
DC
1477 components.remove(0);
1478 }
1479
d8d8af98 1480 let mut split = components.splitn(2, |c| *c == b'/');
9238cdf5 1481 let pxar_name = std::str::from_utf8(split.next().unwrap())?;
0dfce17a 1482 let file_path = split.next().unwrap_or(b"/");
2d55beec 1483 let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
9238cdf5
FG
1484 for file in files {
1485 if file.filename == pxar_name && file.crypt_mode == Some(CryptMode::Encrypt) {
1486 bail!("cannot decode '{}' - is encrypted", pxar_name);
1487 }
1488 }
d33d8f4e 1489
9238cdf5
FG
1490 let mut path = datastore.base_path();
1491 path.push(backup_dir.relative_path());
1492 path.push(pxar_name);
d33d8f4e
DC
1493
1494 let index = DynamicIndexReader::open(&path)
1495 .map_err(|err| format_err!("unable to read dynamic index '{:?}' - {}", &path, err))?;
1496
2d55beec 1497 let (csum, size) = index.compute_csum();
9a37bd6c 1498 manifest.verify_file(pxar_name, &csum, size)?;
2d55beec 1499
14f6c9cb 1500 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
d33d8f4e
DC
1501 let reader = BufferedDynamicReader::new(index, chunk_reader);
1502 let archive_size = reader.archive_size();
1503 let reader = LocalDynamicReadAt::new(reader);
1504
1505 let decoder = Accessor::new(reader, archive_size).await?;
1506 let root = decoder.open_root().await?;
2e219481 1507 let path = OsStr::from_bytes(file_path).to_os_string();
d33d8f4e 1508 let file = root
dc7a5b34
TL
1509 .lookup(&path)
1510 .await?
2e219481 1511 .ok_or_else(|| format_err!("error opening '{:?}'", path))?;
d33d8f4e 1512
804f6143
DC
1513 let body = match file.kind() {
1514 EntryKind::File { .. } => Body::wrap_stream(
1515 AsyncReaderStream::new(file.contents().await?).map_err(move |err| {
1516 eprintln!("error during streaming of file '{:?}' - {}", filepath, err);
1517 err
1518 }),
1519 ),
1520 EntryKind::Hardlink(_) => Body::wrap_stream(
1521 AsyncReaderStream::new(decoder.follow_hardlink(&file).await?.contents().await?)
1522 .map_err(move |err| {
dc7a5b34 1523 eprintln!("error during streaming of hardlink '{:?}' - {}", path, err);
804f6143
DC
1524 err
1525 }),
1526 ),
1527 EntryKind::Directory => {
984ddb2f 1528 let (sender, receiver) = tokio::sync::mpsc::channel::<Result<_, Error>>(100);
804f6143 1529 let channelwriter = AsyncChannelWriter::new(sender, 1024 * 1024);
984ddb2f 1530 if tar {
dc7a5b34
TL
1531 proxmox_rest_server::spawn_internal_task(create_tar(
1532 channelwriter,
1533 decoder,
1534 path.clone(),
1535 false,
1536 ));
984ddb2f
DC
1537 let zstdstream = ZstdEncoder::new(ReceiverStream::new(receiver))?;
1538 Body::wrap_stream(zstdstream.map_err(move |err| {
1539 eprintln!("error during streaming of tar.zst '{:?}' - {}", path, err);
1540 err
1541 }))
1542 } else {
dc7a5b34
TL
1543 proxmox_rest_server::spawn_internal_task(create_zip(
1544 channelwriter,
1545 decoder,
1546 path.clone(),
1547 false,
1548 ));
984ddb2f
DC
1549 Body::wrap_stream(ReceiverStream::new(receiver).map_err(move |err| {
1550 eprintln!("error during streaming of zip '{:?}' - {}", path, err);
1551 err
1552 }))
1553 }
804f6143
DC
1554 }
1555 other => bail!("cannot download file of type {:?}", other),
1556 };
d33d8f4e
DC
1557
1558 // fixme: set other headers ?
1559 Ok(Response::builder()
dc7a5b34
TL
1560 .status(StatusCode::OK)
1561 .header(header::CONTENT_TYPE, "application/octet-stream")
1562 .body(body)
1563 .unwrap())
1564 }
1565 .boxed()
d33d8f4e
DC
1566}
1567
1a0d3d11
DM
1568#[api(
1569 input: {
1570 properties: {
1571 store: {
1572 schema: DATASTORE_SCHEMA,
1573 },
1574 timeframe: {
c68fa58a 1575 type: RRDTimeFrame,
1a0d3d11
DM
1576 },
1577 cf: {
1578 type: RRDMode,
1579 },
1580 },
1581 },
1582 access: {
1583 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
1584 },
1585)]
1586/// Read datastore stats
bf78f708 1587pub fn get_rrd_stats(
1a0d3d11 1588 store: String,
c68fa58a 1589 timeframe: RRDTimeFrame,
1a0d3d11
DM
1590 cf: RRDMode,
1591 _param: Value,
1592) -> Result<Value, Error> {
e9d2fc93 1593 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
f27b6086
DC
1594 let disk_manager = crate::tools::disks::DiskManage::new();
1595
1596 let mut rrd_fields = vec![
dc7a5b34
TL
1597 "total",
1598 "used",
1599 "read_ios",
1600 "read_bytes",
1601 "write_ios",
1602 "write_bytes",
f27b6086
DC
1603 ];
1604
1605 // we do not have io_ticks for zpools, so don't include them
1606 match disk_manager.find_mounted_device(&datastore.base_path()) {
dc7a5b34 1607 Ok(Some((fs_type, _, _))) if fs_type.as_str() == "zfs" => {}
f27b6086
DC
1608 _ => rrd_fields.push("io_ticks"),
1609 };
1610
dc7a5b34 1611 create_value_from_rrd(&format!("datastore/{}", store), &rrd_fields, timeframe, cf)
1a0d3d11
DM
1612}
1613
5fd823c3
HL
1614#[api(
1615 input: {
1616 properties: {
1617 store: {
1618 schema: DATASTORE_SCHEMA,
1619 },
1620 },
1621 },
1622 access: {
1623 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, true),
1624 },
1625)]
1626/// Read datastore stats
dc7a5b34 1627pub fn get_active_operations(store: String, _param: Value) -> Result<Value, Error> {
5fd823c3
HL
1628 let active_operations = task_tracking::get_active_operations(&store)?;
1629 Ok(json!({
1630 "read": active_operations.read,
1631 "write": active_operations.write,
1632 }))
1633}
1634
d6688884
SR
1635#[api(
1636 input: {
1637 properties: {
988d575d 1638 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1639 backup_group: {
1640 type: pbs_api_types::BackupGroup,
1641 flatten: true,
1642 },
d6688884
SR
1643 },
1644 },
1645 access: {
1646 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
1647 },
1648)]
1649/// Get "notes" for a backup group
1650pub fn get_group_notes(
1651 store: String,
8c74349b 1652 backup_group: pbs_api_types::BackupGroup,
d6688884
SR
1653 rpcenv: &mut dyn RpcEnvironment,
1654) -> Result<String, Error> {
e9d2fc93 1655 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
d6688884
SR
1656
1657 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
d6688884
SR
1658
1659 check_priv_or_backup_owner(&datastore, &backup_group, &auth_id, PRIV_DATASTORE_AUDIT)?;
1660
1661 let note_path = get_group_note_path(&datastore, &backup_group);
1662 Ok(file_read_optional_string(note_path)?.unwrap_or_else(|| "".to_owned()))
1663}
1664
1665#[api(
1666 input: {
1667 properties: {
988d575d 1668 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1669 backup_group: {
1670 type: pbs_api_types::BackupGroup,
1671 flatten: true,
1672 },
d6688884
SR
1673 notes: {
1674 description: "A multiline text.",
1675 },
1676 },
1677 },
1678 access: {
1679 permission: &Permission::Privilege(&["datastore", "{store}"],
1680 PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_BACKUP,
1681 true),
1682 },
1683)]
1684/// Set "notes" for a backup group
1685pub fn set_group_notes(
1686 store: String,
8c74349b 1687 backup_group: pbs_api_types::BackupGroup,
d6688884
SR
1688 notes: String,
1689 rpcenv: &mut dyn RpcEnvironment,
1690) -> Result<(), Error> {
e9d2fc93 1691 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
d6688884
SR
1692
1693 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
d6688884
SR
1694
1695 check_priv_or_backup_owner(&datastore, &backup_group, &auth_id, PRIV_DATASTORE_MODIFY)?;
1696
1697 let note_path = get_group_note_path(&datastore, &backup_group);
e0a19d33 1698 replace_file(note_path, notes.as_bytes(), CreateOptions::new(), false)?;
d6688884
SR
1699
1700 Ok(())
1701}
1702
912b3f5b
DM
1703#[api(
1704 input: {
1705 properties: {
988d575d 1706 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1707 backup_dir: {
1708 type: pbs_api_types::BackupDir,
1709 flatten: true,
1710 },
912b3f5b
DM
1711 },
1712 },
1713 access: {
1401f4be 1714 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
912b3f5b
DM
1715 },
1716)]
1717/// Get "notes" for a specific backup
bf78f708 1718pub fn get_notes(
912b3f5b 1719 store: String,
8c74349b 1720 backup_dir: pbs_api_types::BackupDir,
912b3f5b
DM
1721 rpcenv: &mut dyn RpcEnvironment,
1722) -> Result<String, Error> {
e9d2fc93 1723 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
912b3f5b 1724
e6dc35ac 1725 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
8c74349b 1726 let backup_dir = datastore.backup_dir(backup_dir)?;
912b3f5b 1727
dc7a5b34
TL
1728 check_priv_or_backup_owner(
1729 &datastore,
db87d93e 1730 backup_dir.as_ref(),
dc7a5b34
TL
1731 &auth_id,
1732 PRIV_DATASTORE_AUDIT,
1733 )?;
912b3f5b 1734
883aa6d5 1735 let (manifest, _) = datastore.load_manifest(&backup_dir)?;
912b3f5b 1736
dc7a5b34 1737 let notes = manifest.unprotected["notes"].as_str().unwrap_or("");
912b3f5b
DM
1738
1739 Ok(String::from(notes))
1740}
1741
1742#[api(
1743 input: {
1744 properties: {
988d575d 1745 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1746 backup_dir: {
1747 type: pbs_api_types::BackupDir,
1748 flatten: true,
1749 },
912b3f5b
DM
1750 notes: {
1751 description: "A multiline text.",
1752 },
1753 },
1754 },
1755 access: {
b728a69e
FG
1756 permission: &Permission::Privilege(&["datastore", "{store}"],
1757 PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_BACKUP,
1758 true),
912b3f5b
DM
1759 },
1760)]
1761/// Set "notes" for a specific backup
bf78f708 1762pub fn set_notes(
912b3f5b 1763 store: String,
8c74349b 1764 backup_dir: pbs_api_types::BackupDir,
912b3f5b
DM
1765 notes: String,
1766 rpcenv: &mut dyn RpcEnvironment,
1767) -> Result<(), Error> {
e9d2fc93 1768 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
912b3f5b 1769
e6dc35ac 1770 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
8c74349b 1771 let backup_dir = datastore.backup_dir(backup_dir)?;
912b3f5b 1772
dc7a5b34
TL
1773 check_priv_or_backup_owner(
1774 &datastore,
db87d93e 1775 backup_dir.as_ref(),
dc7a5b34
TL
1776 &auth_id,
1777 PRIV_DATASTORE_MODIFY,
1778 )?;
912b3f5b 1779
dc7a5b34
TL
1780 datastore
1781 .update_manifest(&backup_dir, |manifest| {
1782 manifest.unprotected["notes"] = notes.into();
1783 })
1784 .map_err(|err| format_err!("unable to update manifest blob - {}", err))?;
912b3f5b
DM
1785
1786 Ok(())
1787}
1788
8292d3d2
DC
1789#[api(
1790 input: {
1791 properties: {
988d575d 1792 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1793 backup_dir: {
1794 type: pbs_api_types::BackupDir,
1795 flatten: true,
1796 },
8292d3d2
DC
1797 },
1798 },
1799 access: {
1800 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
1801 },
1802)]
1803/// Query protection for a specific backup
1804pub fn get_protection(
1805 store: String,
8c74349b 1806 backup_dir: pbs_api_types::BackupDir,
8292d3d2
DC
1807 rpcenv: &mut dyn RpcEnvironment,
1808) -> Result<bool, Error> {
e9d2fc93 1809 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Read))?;
8292d3d2
DC
1810
1811 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
8c74349b 1812 let backup_dir = datastore.backup_dir(backup_dir)?;
8292d3d2 1813
dc7a5b34
TL
1814 check_priv_or_backup_owner(
1815 &datastore,
db87d93e 1816 backup_dir.as_ref(),
dc7a5b34
TL
1817 &auth_id,
1818 PRIV_DATASTORE_AUDIT,
1819 )?;
8292d3d2 1820
6da20161 1821 Ok(backup_dir.is_protected())
8292d3d2
DC
1822}
1823
1824#[api(
1825 input: {
1826 properties: {
988d575d 1827 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1828 backup_dir: {
1829 type: pbs_api_types::BackupDir,
1830 flatten: true,
1831 },
8292d3d2
DC
1832 protected: {
1833 description: "Enable/disable protection.",
1834 },
1835 },
1836 },
1837 access: {
1838 permission: &Permission::Privilege(&["datastore", "{store}"],
1839 PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_BACKUP,
1840 true),
1841 },
1842)]
1843/// En- or disable protection for a specific backup
1844pub fn set_protection(
1845 store: String,
8c74349b 1846 backup_dir: pbs_api_types::BackupDir,
8292d3d2
DC
1847 protected: bool,
1848 rpcenv: &mut dyn RpcEnvironment,
1849) -> Result<(), Error> {
e9d2fc93 1850 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
8292d3d2
DC
1851
1852 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
8c74349b 1853 let backup_dir = datastore.backup_dir(backup_dir)?;
8292d3d2 1854
dc7a5b34
TL
1855 check_priv_or_backup_owner(
1856 &datastore,
db87d93e 1857 backup_dir.as_ref(),
dc7a5b34
TL
1858 &auth_id,
1859 PRIV_DATASTORE_MODIFY,
1860 )?;
8292d3d2
DC
1861
1862 datastore.update_protection(&backup_dir, protected)
1863}
1864
72be0eb1 1865#[api(
4940012d 1866 input: {
72be0eb1 1867 properties: {
988d575d 1868 store: { schema: DATASTORE_SCHEMA },
8c74349b
WB
1869 backup_group: {
1870 type: pbs_api_types::BackupGroup,
1871 flatten: true,
1872 },
72be0eb1 1873 "new-owner": {
e6dc35ac 1874 type: Authid,
72be0eb1
DW
1875 },
1876 },
4940012d
FG
1877 },
1878 access: {
bff85572
FG
1879 permission: &Permission::Anybody,
1880 description: "Datastore.Modify on whole datastore, or changing ownership between user and a user's token for owned backups with Datastore.Backup"
4940012d 1881 },
72be0eb1
DW
1882)]
1883/// Change owner of a backup group
bf78f708 1884pub fn set_backup_owner(
72be0eb1 1885 store: String,
8c74349b 1886 backup_group: pbs_api_types::BackupGroup,
e6dc35ac 1887 new_owner: Authid,
bff85572 1888 rpcenv: &mut dyn RpcEnvironment,
72be0eb1 1889) -> Result<(), Error> {
e9d2fc93 1890 let datastore = DataStore::lookup_datastore(&store, Some(Operation::Write))?;
72be0eb1 1891
8c74349b 1892 let backup_group = datastore.backup_group(backup_group);
72be0eb1 1893
bff85572
FG
1894 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
1895
72be0eb1
DW
1896 let user_info = CachedUserInfo::new()?;
1897
bff85572
FG
1898 let privs = user_info.lookup_privs(&auth_id, &["datastore", &store]);
1899
1900 let allowed = if (privs & PRIV_DATASTORE_MODIFY) != 0 {
1901 // High-privilege user/token
1902 true
1903 } else if (privs & PRIV_DATASTORE_BACKUP) != 0 {
db87d93e 1904 let owner = datastore.get_owner(backup_group.as_ref())?;
bff85572
FG
1905
1906 match (owner.is_token(), new_owner.is_token()) {
1907 (true, true) => {
1908 // API token to API token, owned by same user
1909 let owner = owner.user();
1910 let new_owner = new_owner.user();
1911 owner == new_owner && Authid::from(owner.clone()) == auth_id
dc7a5b34 1912 }
bff85572
FG
1913 (true, false) => {
1914 // API token to API token owner
dc7a5b34
TL
1915 Authid::from(owner.user().clone()) == auth_id && new_owner == auth_id
1916 }
bff85572
FG
1917 (false, true) => {
1918 // API token owner to API token
dc7a5b34
TL
1919 owner == auth_id && Authid::from(new_owner.user().clone()) == auth_id
1920 }
bff85572
FG
1921 (false, false) => {
1922 // User to User, not allowed for unprivileged users
1923 false
dc7a5b34 1924 }
bff85572
FG
1925 }
1926 } else {
1927 false
1928 };
1929
1930 if !allowed {
dc7a5b34
TL
1931 return Err(http_err!(
1932 UNAUTHORIZED,
1933 "{} does not have permission to change owner of backup group '{}' to {}",
1934 auth_id,
1935 backup_group,
1936 new_owner,
bff85572
FG
1937 ));
1938 }
1939
e6dc35ac 1940 if !user_info.is_active_auth_id(&new_owner) {
dc7a5b34
TL
1941 bail!(
1942 "{} '{}' is inactive or non-existent",
1943 if new_owner.is_token() {
1944 "API token".to_string()
1945 } else {
1946 "user".to_string()
1947 },
1948 new_owner
1949 );
72be0eb1
DW
1950 }
1951
db87d93e 1952 datastore.set_owner(backup_group.as_ref(), &new_owner, true)?;
72be0eb1
DW
1953
1954 Ok(())
1955}
1956
552c2259 1957#[sortable]
255f378a 1958const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
5fd823c3
HL
1959 (
1960 "active-operations",
dc7a5b34 1961 &Router::new().get(&API_METHOD_GET_ACTIVE_OPERATIONS),
5b1cfa01 1962 ),
dc7a5b34 1963 ("catalog", &Router::new().get(&API_METHOD_CATALOG)),
72be0eb1
DW
1964 (
1965 "change-owner",
dc7a5b34 1966 &Router::new().post(&API_METHOD_SET_BACKUP_OWNER),
72be0eb1 1967 ),
255f378a
DM
1968 (
1969 "download",
dc7a5b34 1970 &Router::new().download(&API_METHOD_DOWNLOAD_FILE),
255f378a 1971 ),
6ef9bb59
DC
1972 (
1973 "download-decoded",
dc7a5b34 1974 &Router::new().download(&API_METHOD_DOWNLOAD_FILE_DECODED),
255f378a 1975 ),
dc7a5b34 1976 ("files", &Router::new().get(&API_METHOD_LIST_SNAPSHOT_FILES)),
255f378a
DM
1977 (
1978 "gc",
1979 &Router::new()
1980 .get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
dc7a5b34 1981 .post(&API_METHOD_START_GARBAGE_COLLECTION),
255f378a 1982 ),
d6688884
SR
1983 (
1984 "group-notes",
1985 &Router::new()
1986 .get(&API_METHOD_GET_GROUP_NOTES)
dc7a5b34 1987 .put(&API_METHOD_SET_GROUP_NOTES),
d6688884 1988 ),
255f378a
DM
1989 (
1990 "groups",
1991 &Router::new()
b31c8019 1992 .get(&API_METHOD_LIST_GROUPS)
dc7a5b34 1993 .delete(&API_METHOD_DELETE_GROUP),
255f378a 1994 ),
912b3f5b
DM
1995 (
1996 "notes",
1997 &Router::new()
1998 .get(&API_METHOD_GET_NOTES)
dc7a5b34 1999 .put(&API_METHOD_SET_NOTES),
912b3f5b 2000 ),
8292d3d2
DC
2001 (
2002 "protected",
2003 &Router::new()
2004 .get(&API_METHOD_GET_PROTECTION)
dc7a5b34 2005 .put(&API_METHOD_SET_PROTECTION),
255f378a 2006 ),
dc7a5b34 2007 ("prune", &Router::new().post(&API_METHOD_PRUNE)),
9805207a
DC
2008 (
2009 "prune-datastore",
dc7a5b34 2010 &Router::new().post(&API_METHOD_PRUNE_DATASTORE),
9805207a 2011 ),
d33d8f4e
DC
2012 (
2013 "pxar-file-download",
dc7a5b34 2014 &Router::new().download(&API_METHOD_PXAR_FILE_DOWNLOAD),
1a0d3d11 2015 ),
dc7a5b34 2016 ("rrd", &Router::new().get(&API_METHOD_GET_RRD_STATS)),
255f378a
DM
2017 (
2018 "snapshots",
2019 &Router::new()
fc189b19 2020 .get(&API_METHOD_LIST_SNAPSHOTS)
dc7a5b34 2021 .delete(&API_METHOD_DELETE_SNAPSHOT),
255f378a 2022 ),
dc7a5b34 2023 ("status", &Router::new().get(&API_METHOD_STATUS)),
255f378a
DM
2024 (
2025 "upload-backup-log",
dc7a5b34 2026 &Router::new().upload(&API_METHOD_UPLOAD_BACKUP_LOG),
c2009e53 2027 ),
dc7a5b34 2028 ("verify", &Router::new().post(&API_METHOD_VERIFY)),
255f378a
DM
2029];
2030
ad51d02a 2031const DATASTORE_INFO_ROUTER: Router = Router::new()
255f378a
DM
2032 .get(&list_subdirs_api_method!(DATASTORE_INFO_SUBDIRS))
2033 .subdirs(DATASTORE_INFO_SUBDIRS);
2034
255f378a 2035pub const ROUTER: Router = Router::new()
bb34b589 2036 .get(&API_METHOD_GET_DATASTORE_LIST)
255f378a 2037 .match_all("store", &DATASTORE_INFO_ROUTER);