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