]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/admin/datastore.rs
save last verify result in snapshot manifest
[proxmox-backup.git] / src / api2 / admin / datastore.rs
CommitLineData
cad540e9 1use std::collections::{HashSet, HashMap};
d33d8f4e
DC
2use std::ffi::OsStr;
3use std::os::unix::ffi::OsStrExt;
cad540e9 4
6ef9bb59 5use anyhow::{bail, format_err, Error};
9e47c0a5 6use futures::*;
cad540e9
WB
7use hyper::http::request::Parts;
8use hyper::{header, Body, Response, StatusCode};
15e9b4ed
DM
9use serde_json::{json, Value};
10
bb34b589
DM
11use proxmox::api::{
12 api, ApiResponseFuture, ApiHandler, ApiMethod, Router,
e7cb4dc5
WB
13 RpcEnvironment, RpcEnvironmentType, Permission
14};
cad540e9
WB
15use proxmox::api::router::SubdirMap;
16use proxmox::api::schema::*;
60f9a6ea 17use proxmox::tools::fs::{replace_file, CreateOptions};
9ea4bce4
WB
18use proxmox::try_block;
19use proxmox::{http_err, identity, list_subdirs_api_method, sortable};
e18a6c9e 20
d33d8f4e
DC
21use pxar::accessor::aio::Accessor;
22use pxar::EntryKind;
23
cad540e9 24use crate::api2::types::*;
431cc7b1 25use crate::api2::node::rrd::create_value_from_rrd;
e5064ba6 26use crate::backup::*;
cad540e9 27use crate::config::datastore;
54552dda
DM
28use crate::config::cached_user_info::CachedUserInfo;
29
0f778e06 30use crate::server::WorkerTask;
f386f512 31use crate::tools::{self, AsyncReaderStream, WrappedReaderStream};
d00e1a21
DM
32use crate::config::acl::{
33 PRIV_DATASTORE_AUDIT,
54552dda 34 PRIV_DATASTORE_MODIFY,
d00e1a21
DM
35 PRIV_DATASTORE_READ,
36 PRIV_DATASTORE_PRUNE,
54552dda 37 PRIV_DATASTORE_BACKUP,
d00e1a21 38};
1629d2ad 39
e7cb4dc5
WB
40fn check_backup_owner(
41 store: &DataStore,
42 group: &BackupGroup,
43 userid: &Userid,
44) -> Result<(), Error> {
54552dda
DM
45 let owner = store.get_owner(group)?;
46 if &owner != userid {
47 bail!("backup owner check failed ({} != {})", userid, owner);
48 }
49 Ok(())
50}
51
e7cb4dc5
WB
52fn read_backup_index(
53 store: &DataStore,
54 backup_dir: &BackupDir,
55) -> Result<(BackupManifest, Vec<BackupContent>), Error> {
8c70e3eb 56
ff86ef00 57 let (manifest, index_size) = store.load_manifest(backup_dir)?;
8c70e3eb 58
09b1f7b2
DM
59 let mut result = Vec::new();
60 for item in manifest.files() {
61 result.push(BackupContent {
62 filename: item.filename.clone(),
f28d9088 63 crypt_mode: Some(item.crypt_mode),
09b1f7b2
DM
64 size: Some(item.size),
65 });
8c70e3eb
DM
66 }
67
09b1f7b2 68 result.push(BackupContent {
96d65fbc 69 filename: MANIFEST_BLOB_NAME.to_string(),
882c0823
FG
70 crypt_mode: match manifest.signature {
71 Some(_) => Some(CryptMode::SignOnly),
72 None => Some(CryptMode::None),
73 },
09b1f7b2
DM
74 size: Some(index_size),
75 });
4f1e40a2 76
70030b43 77 Ok((manifest, result))
8c70e3eb
DM
78}
79
1c090810
DC
80fn get_all_snapshot_files(
81 store: &DataStore,
82 info: &BackupInfo,
70030b43
DM
83) -> Result<(BackupManifest, Vec<BackupContent>), Error> {
84
85 let (manifest, mut files) = read_backup_index(&store, &info.backup_dir)?;
1c090810
DC
86
87 let file_set = files.iter().fold(HashSet::new(), |mut acc, item| {
88 acc.insert(item.filename.clone());
89 acc
90 });
91
92 for file in &info.files {
93 if file_set.contains(file) { continue; }
f28d9088
WB
94 files.push(BackupContent {
95 filename: file.to_string(),
96 size: None,
97 crypt_mode: None,
98 });
1c090810
DC
99 }
100
70030b43 101 Ok((manifest, files))
1c090810
DC
102}
103
8f579717
DM
104fn group_backups(backup_list: Vec<BackupInfo>) -> HashMap<String, Vec<BackupInfo>> {
105
106 let mut group_hash = HashMap::new();
107
108 for info in backup_list {
9b492eb2 109 let group_id = info.backup_dir.group().group_path().to_str().unwrap().to_owned();
8f579717
DM
110 let time_list = group_hash.entry(group_id).or_insert(vec![]);
111 time_list.push(info);
112 }
113
114 group_hash
115}
116
b31c8019
DM
117#[api(
118 input: {
119 properties: {
120 store: {
121 schema: DATASTORE_SCHEMA,
122 },
123 },
124 },
125 returns: {
126 type: Array,
127 description: "Returns the list of backup groups.",
128 items: {
129 type: GroupListItem,
130 }
131 },
bb34b589 132 access: {
54552dda
DM
133 permission: &Permission::Privilege(
134 &["datastore", "{store}"],
135 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP,
136 true),
bb34b589 137 },
b31c8019
DM
138)]
139/// List backup groups.
ad20d198 140fn list_groups(
b31c8019 141 store: String,
54552dda 142 rpcenv: &mut dyn RpcEnvironment,
b31c8019 143) -> Result<Vec<GroupListItem>, Error> {
812c6f87 144
e7cb4dc5 145 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
54552dda 146 let user_info = CachedUserInfo::new()?;
e7cb4dc5 147 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
54552dda 148
b31c8019 149 let datastore = DataStore::lookup_datastore(&store)?;
812c6f87 150
c0977501 151 let backup_list = BackupInfo::list_backups(&datastore.base_path())?;
812c6f87
DM
152
153 let group_hash = group_backups(backup_list);
154
b31c8019 155 let mut groups = Vec::new();
812c6f87
DM
156
157 for (_group_id, mut list) in group_hash {
158
2b01a225 159 BackupInfo::sort_list(&mut list, false);
812c6f87
DM
160
161 let info = &list[0];
54552dda 162
9b492eb2 163 let group = info.backup_dir.group();
812c6f87 164
54552dda 165 let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
04b0ca8b 166 let owner = datastore.get_owner(group)?;
54552dda 167 if !list_all {
e7cb4dc5 168 if owner != userid { continue; }
54552dda
DM
169 }
170
b31c8019
DM
171 let result_item = GroupListItem {
172 backup_type: group.backup_type().to_string(),
173 backup_id: group.backup_id().to_string(),
174 last_backup: info.backup_dir.backup_time().timestamp(),
175 backup_count: list.len() as u64,
176 files: info.files.clone(),
04b0ca8b 177 owner: Some(owner),
b31c8019
DM
178 };
179 groups.push(result_item);
812c6f87
DM
180 }
181
b31c8019 182 Ok(groups)
812c6f87 183}
8f579717 184
09b1f7b2
DM
185#[api(
186 input: {
187 properties: {
188 store: {
189 schema: DATASTORE_SCHEMA,
190 },
191 "backup-type": {
192 schema: BACKUP_TYPE_SCHEMA,
193 },
194 "backup-id": {
195 schema: BACKUP_ID_SCHEMA,
196 },
197 "backup-time": {
198 schema: BACKUP_TIME_SCHEMA,
199 },
200 },
201 },
202 returns: {
203 type: Array,
204 description: "Returns the list of archive files inside a backup snapshots.",
205 items: {
206 type: BackupContent,
207 }
208 },
bb34b589 209 access: {
54552dda
DM
210 permission: &Permission::Privilege(
211 &["datastore", "{store}"],
212 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
213 true),
bb34b589 214 },
09b1f7b2
DM
215)]
216/// List snapshot files.
ea5f547f 217pub fn list_snapshot_files(
09b1f7b2
DM
218 store: String,
219 backup_type: String,
220 backup_id: String,
221 backup_time: i64,
01a13423 222 _info: &ApiMethod,
54552dda 223 rpcenv: &mut dyn RpcEnvironment,
09b1f7b2 224) -> Result<Vec<BackupContent>, Error> {
01a13423 225
e7cb4dc5 226 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
54552dda 227 let user_info = CachedUserInfo::new()?;
e7cb4dc5 228 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
54552dda 229
09b1f7b2 230 let datastore = DataStore::lookup_datastore(&store)?;
54552dda 231
01a13423
DM
232 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
233
54552dda 234 let allowed = (user_privs & (PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_READ)) != 0;
e7cb4dc5 235 if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; }
54552dda 236
d7c24397 237 let info = BackupInfo::new(&datastore.base_path(), snapshot)?;
01a13423 238
70030b43
DM
239 let (_manifest, files) = get_all_snapshot_files(&datastore, &info)?;
240
241 Ok(files)
01a13423
DM
242}
243
68a6a0ee
DM
244#[api(
245 input: {
246 properties: {
247 store: {
248 schema: DATASTORE_SCHEMA,
249 },
250 "backup-type": {
251 schema: BACKUP_TYPE_SCHEMA,
252 },
253 "backup-id": {
254 schema: BACKUP_ID_SCHEMA,
255 },
256 "backup-time": {
257 schema: BACKUP_TIME_SCHEMA,
258 },
259 },
260 },
bb34b589 261 access: {
54552dda
DM
262 permission: &Permission::Privilege(
263 &["datastore", "{store}"],
264 PRIV_DATASTORE_MODIFY| PRIV_DATASTORE_PRUNE,
265 true),
bb34b589 266 },
68a6a0ee
DM
267)]
268/// Delete backup snapshot.
269fn delete_snapshot(
270 store: String,
271 backup_type: String,
272 backup_id: String,
273 backup_time: i64,
6f62c924 274 _info: &ApiMethod,
54552dda 275 rpcenv: &mut dyn RpcEnvironment,
6f62c924
DM
276) -> Result<Value, Error> {
277
e7cb4dc5 278 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
54552dda 279 let user_info = CachedUserInfo::new()?;
e7cb4dc5 280 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
54552dda 281
391d3107 282 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
6f62c924 283
68a6a0ee 284 let datastore = DataStore::lookup_datastore(&store)?;
6f62c924 285
54552dda 286 let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0;
e7cb4dc5 287 if !allowed { check_backup_owner(&datastore, snapshot.group(), &userid)?; }
54552dda 288
c9756b40 289 datastore.remove_backup_dir(&snapshot, false)?;
6f62c924
DM
290
291 Ok(Value::Null)
292}
293
fc189b19
DM
294#[api(
295 input: {
296 properties: {
297 store: {
298 schema: DATASTORE_SCHEMA,
299 },
300 "backup-type": {
301 optional: true,
302 schema: BACKUP_TYPE_SCHEMA,
303 },
304 "backup-id": {
305 optional: true,
306 schema: BACKUP_ID_SCHEMA,
307 },
308 },
309 },
310 returns: {
311 type: Array,
312 description: "Returns the list of snapshots.",
313 items: {
314 type: SnapshotListItem,
315 }
316 },
bb34b589 317 access: {
54552dda
DM
318 permission: &Permission::Privilege(
319 &["datastore", "{store}"],
320 PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP,
321 true),
bb34b589 322 },
fc189b19
DM
323)]
324/// List backup snapshots.
f24fc116 325pub fn list_snapshots (
54552dda
DM
326 store: String,
327 backup_type: Option<String>,
328 backup_id: Option<String>,
329 _param: Value,
184f17af 330 _info: &ApiMethod,
54552dda 331 rpcenv: &mut dyn RpcEnvironment,
fc189b19 332) -> Result<Vec<SnapshotListItem>, Error> {
184f17af 333
e7cb4dc5 334 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
54552dda 335 let user_info = CachedUserInfo::new()?;
e7cb4dc5 336 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
184f17af 337
54552dda 338 let datastore = DataStore::lookup_datastore(&store)?;
184f17af 339
c0977501 340 let base_path = datastore.base_path();
184f17af 341
15c847f1 342 let backup_list = BackupInfo::list_backups(&base_path)?;
184f17af
DM
343
344 let mut snapshots = vec![];
345
c0977501 346 for info in backup_list {
15c847f1 347 let group = info.backup_dir.group();
54552dda 348 if let Some(ref backup_type) = backup_type {
15c847f1
DM
349 if backup_type != group.backup_type() { continue; }
350 }
54552dda 351 if let Some(ref backup_id) = backup_id {
15c847f1
DM
352 if backup_id != group.backup_id() { continue; }
353 }
a17a0e7a 354
54552dda 355 let list_all = (user_privs & PRIV_DATASTORE_AUDIT) != 0;
04b0ca8b
DC
356 let owner = datastore.get_owner(group)?;
357
54552dda 358 if !list_all {
e7cb4dc5 359 if owner != userid { continue; }
54552dda
DM
360 }
361
1c090810
DC
362 let mut size = None;
363
3b2046d2 364 let (comment, verification, files) = match get_all_snapshot_files(&datastore, &info) {
70030b43 365 Ok((manifest, files)) => {
1c090810 366 size = Some(files.iter().map(|x| x.size.unwrap_or(0)).sum());
70030b43
DM
367 // extract the first line from notes
368 let comment: Option<String> = manifest.unprotected["notes"]
369 .as_str()
370 .and_then(|notes| notes.lines().next())
371 .map(String::from);
372
3b2046d2
TL
373 let verify = manifest.unprotected["verify_state"].clone();
374 let verify: Option<SnapshotVerifyState> = match serde_json::from_value(verify) {
375 Ok(verify) => verify,
376 Err(err) => {
377 eprintln!("error parsing verification state : '{}'", err);
378 None
379 }
380 };
381
382 (comment, verify, files)
1c090810
DC
383 },
384 Err(err) => {
385 eprintln!("error during snapshot file listing: '{}'", err);
70030b43 386 (
3b2046d2 387 None,
70030b43
DM
388 None,
389 info
390 .files
391 .iter()
392 .map(|x| BackupContent {
393 filename: x.to_string(),
394 size: None,
395 crypt_mode: None,
396 })
397 .collect()
398 )
1c090810
DC
399 },
400 };
401
402 let result_item = SnapshotListItem {
fc189b19
DM
403 backup_type: group.backup_type().to_string(),
404 backup_id: group.backup_id().to_string(),
405 backup_time: info.backup_dir.backup_time().timestamp(),
70030b43 406 comment,
3b2046d2 407 verification,
1c090810
DC
408 files,
409 size,
04b0ca8b 410 owner: Some(owner),
fc189b19 411 };
a17a0e7a 412
a17a0e7a 413 snapshots.push(result_item);
184f17af
DM
414 }
415
fc189b19 416 Ok(snapshots)
184f17af
DM
417}
418
1dc117bb
DM
419#[api(
420 input: {
421 properties: {
422 store: {
423 schema: DATASTORE_SCHEMA,
424 },
425 },
426 },
427 returns: {
428 type: StorageStatus,
429 },
bb34b589 430 access: {
54552dda 431 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
bb34b589 432 },
1dc117bb
DM
433)]
434/// Get datastore status.
ea5f547f 435pub fn status(
1dc117bb 436 store: String,
0eecf38f
DM
437 _info: &ApiMethod,
438 _rpcenv: &mut dyn RpcEnvironment,
1dc117bb 439) -> Result<StorageStatus, Error> {
1dc117bb 440 let datastore = DataStore::lookup_datastore(&store)?;
33070956 441 crate::tools::disks::disk_usage(&datastore.base_path())
0eecf38f
DM
442}
443
c2009e53
DM
444#[api(
445 input: {
446 properties: {
447 store: {
448 schema: DATASTORE_SCHEMA,
449 },
450 "backup-type": {
451 schema: BACKUP_TYPE_SCHEMA,
452 optional: true,
453 },
454 "backup-id": {
455 schema: BACKUP_ID_SCHEMA,
456 optional: true,
457 },
458 "backup-time": {
459 schema: BACKUP_TIME_SCHEMA,
460 optional: true,
461 },
462 },
463 },
464 returns: {
465 schema: UPID_SCHEMA,
466 },
467 access: {
468 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, true), // fixme
469 },
470)]
471/// Verify backups.
472///
473/// This function can verify a single backup snapshot, all backup from a backup group,
474/// or all backups in the datastore.
475pub fn verify(
476 store: String,
477 backup_type: Option<String>,
478 backup_id: Option<String>,
479 backup_time: Option<i64>,
480 rpcenv: &mut dyn RpcEnvironment,
481) -> Result<Value, Error> {
482 let datastore = DataStore::lookup_datastore(&store)?;
483
8ea00f6e 484 let worker_id;
c2009e53
DM
485
486 let mut backup_dir = None;
487 let mut backup_group = None;
488
489 match (backup_type, backup_id, backup_time) {
490 (Some(backup_type), Some(backup_id), Some(backup_time)) => {
2162e2c1 491 worker_id = format!("{}_{}_{}_{:08X}", store, backup_type, backup_id, backup_time);
c2009e53 492 let dir = BackupDir::new(backup_type, backup_id, backup_time);
c2009e53
DM
493 backup_dir = Some(dir);
494 }
495 (Some(backup_type), Some(backup_id), None) => {
2162e2c1 496 worker_id = format!("{}_{}_{}", store, backup_type, backup_id);
c2009e53 497 let group = BackupGroup::new(backup_type, backup_id);
c2009e53
DM
498 backup_group = Some(group);
499 }
500 (None, None, None) => {
8ea00f6e 501 worker_id = store.clone();
c2009e53 502 }
5a718dce 503 _ => bail!("parameters do not specify a backup group or snapshot"),
c2009e53
DM
504 }
505
e7cb4dc5 506 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
c2009e53
DM
507 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
508
509 let upid_str = WorkerTask::new_thread(
e7cb4dc5
WB
510 "verify",
511 Some(worker_id.clone()),
512 userid,
513 to_stdout,
514 move |worker| {
adfdc369 515 let failed_dirs = if let Some(backup_dir) = backup_dir {
2aaae970 516 let mut verified_chunks = HashSet::with_capacity(1024*16);
d8594d87 517 let mut corrupt_chunks = HashSet::with_capacity(64);
adfdc369
DC
518 let mut res = Vec::new();
519 if !verify_backup_dir(&datastore, &backup_dir, &mut verified_chunks, &mut corrupt_chunks, &worker)? {
520 res.push(backup_dir.to_string());
521 }
522 res
c2009e53 523 } else if let Some(backup_group) = backup_group {
8ea00f6e 524 verify_backup_group(&datastore, &backup_group, &worker)?
c2009e53 525 } else {
8ea00f6e 526 verify_all_backups(&datastore, &worker)?
c2009e53 527 };
adfdc369
DC
528 if failed_dirs.len() > 0 {
529 worker.log("Failed to verify following snapshots:");
530 for dir in failed_dirs {
531 worker.log(format!("\t{}", dir));
532 }
1ffe0301 533 bail!("verification failed - please check the log for details");
c2009e53
DM
534 }
535 Ok(())
e7cb4dc5
WB
536 },
537 )?;
c2009e53
DM
538
539 Ok(json!(upid_str))
540}
541
255f378a
DM
542#[macro_export]
543macro_rules! add_common_prune_prameters {
552c2259
DM
544 ( [ $( $list1:tt )* ] ) => {
545 add_common_prune_prameters!([$( $list1 )* ] , [])
546 };
547 ( [ $( $list1:tt )* ] , [ $( $list2:tt )* ] ) => {
255f378a 548 [
552c2259 549 $( $list1 )*
255f378a 550 (
552c2259 551 "keep-daily",
255f378a 552 true,
49ff1092 553 &PRUNE_SCHEMA_KEEP_DAILY,
255f378a 554 ),
102d8d41
DM
555 (
556 "keep-hourly",
557 true,
49ff1092 558 &PRUNE_SCHEMA_KEEP_HOURLY,
102d8d41 559 ),
255f378a 560 (
552c2259 561 "keep-last",
255f378a 562 true,
49ff1092 563 &PRUNE_SCHEMA_KEEP_LAST,
255f378a
DM
564 ),
565 (
552c2259 566 "keep-monthly",
255f378a 567 true,
49ff1092 568 &PRUNE_SCHEMA_KEEP_MONTHLY,
255f378a
DM
569 ),
570 (
552c2259 571 "keep-weekly",
255f378a 572 true,
49ff1092 573 &PRUNE_SCHEMA_KEEP_WEEKLY,
255f378a
DM
574 ),
575 (
576 "keep-yearly",
577 true,
49ff1092 578 &PRUNE_SCHEMA_KEEP_YEARLY,
255f378a 579 ),
552c2259 580 $( $list2 )*
255f378a
DM
581 ]
582 }
0eecf38f
DM
583}
584
db1e061d
DM
585pub const API_RETURN_SCHEMA_PRUNE: Schema = ArraySchema::new(
586 "Returns the list of snapshots and a flag indicating if there are kept or removed.",
660a3489 587 &PruneListItem::API_SCHEMA
db1e061d
DM
588).schema();
589
0ab08ac9
DM
590const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
591 &ApiHandler::Sync(&prune),
255f378a 592 &ObjectSchema::new(
0ab08ac9
DM
593 "Prune the datastore.",
594 &add_common_prune_prameters!([
595 ("backup-id", false, &BACKUP_ID_SCHEMA),
596 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
3b03abfe
DM
597 ("dry-run", true, &BooleanSchema::new(
598 "Just show what prune would do, but do not delete anything.")
599 .schema()
600 ),
0ab08ac9 601 ],[
66c49c21 602 ("store", false, &DATASTORE_SCHEMA),
0ab08ac9 603 ])
db1e061d
DM
604 ))
605 .returns(&API_RETURN_SCHEMA_PRUNE)
606 .access(None, &Permission::Privilege(
54552dda
DM
607 &["datastore", "{store}"],
608 PRIV_DATASTORE_MODIFY | PRIV_DATASTORE_PRUNE,
609 true)
610);
255f378a 611
83b7db02
DM
612fn prune(
613 param: Value,
614 _info: &ApiMethod,
54552dda 615 rpcenv: &mut dyn RpcEnvironment,
83b7db02
DM
616) -> Result<Value, Error> {
617
54552dda 618 let store = tools::required_string_param(&param, "store")?;
9fdc3ef4
DM
619 let backup_type = tools::required_string_param(&param, "backup-type")?;
620 let backup_id = tools::required_string_param(&param, "backup-id")?;
621
e7cb4dc5 622 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
54552dda 623 let user_info = CachedUserInfo::new()?;
e7cb4dc5 624 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
54552dda 625
3b03abfe
DM
626 let dry_run = param["dry-run"].as_bool().unwrap_or(false);
627
9fdc3ef4
DM
628 let group = BackupGroup::new(backup_type, backup_id);
629
54552dda
DM
630 let datastore = DataStore::lookup_datastore(&store)?;
631
632 let allowed = (user_privs & PRIV_DATASTORE_MODIFY) != 0;
e7cb4dc5 633 if !allowed { check_backup_owner(&datastore, &group, &userid)?; }
83b7db02 634
9e3f0088
DM
635 let prune_options = PruneOptions {
636 keep_last: param["keep-last"].as_u64(),
102d8d41 637 keep_hourly: param["keep-hourly"].as_u64(),
9e3f0088
DM
638 keep_daily: param["keep-daily"].as_u64(),
639 keep_weekly: param["keep-weekly"].as_u64(),
640 keep_monthly: param["keep-monthly"].as_u64(),
641 keep_yearly: param["keep-yearly"].as_u64(),
642 };
8f579717 643
503995c7
DM
644 let worker_id = format!("{}_{}_{}", store, backup_type, backup_id);
645
dda70154
DM
646 let mut prune_result = Vec::new();
647
648 let list = group.list_backups(&datastore.base_path())?;
649
650 let mut prune_info = compute_prune_info(list, &prune_options)?;
651
652 prune_info.reverse(); // delete older snapshots first
653
654 let keep_all = !prune_options.keeps_something();
655
656 if dry_run {
657 for (info, mut keep) in prune_info {
658 if keep_all { keep = true; }
659
660 let backup_time = info.backup_dir.backup_time();
661 let group = info.backup_dir.group();
662
663 prune_result.push(json!({
664 "backup-type": group.backup_type(),
665 "backup-id": group.backup_id(),
666 "backup-time": backup_time.timestamp(),
667 "keep": keep,
668 }));
669 }
670 return Ok(json!(prune_result));
671 }
672
673
163e9bbe 674 // We use a WorkerTask just to have a task log, but run synchrounously
e7cb4dc5 675 let worker = WorkerTask::new("prune", Some(worker_id), Userid::root_userid().clone(), true)?;
dda70154 676
dd8e744f 677 let result = try_block! {
dda70154 678 if keep_all {
9fdc3ef4 679 worker.log("No prune selection - keeping all files.");
dd8e744f 680 } else {
236a396a 681 worker.log(format!("retention options: {}", prune_options.cli_options_string()));
dda70154
DM
682 worker.log(format!("Starting prune on store \"{}\" group \"{}/{}\"",
683 store, backup_type, backup_id));
dd8e744f 684 }
8f579717 685
dda70154
DM
686 for (info, mut keep) in prune_info {
687 if keep_all { keep = true; }
dd8e744f 688
3b03abfe
DM
689 let backup_time = info.backup_dir.backup_time();
690 let timestamp = BackupDir::backup_time_to_string(backup_time);
691 let group = info.backup_dir.group();
692
dda70154 693
3b03abfe
DM
694 let msg = format!(
695 "{}/{}/{} {}",
696 group.backup_type(),
697 group.backup_id(),
698 timestamp,
699 if keep { "keep" } else { "remove" },
700 );
701
702 worker.log(msg);
703
dda70154
DM
704 prune_result.push(json!({
705 "backup-type": group.backup_type(),
706 "backup-id": group.backup_id(),
707 "backup-time": backup_time.timestamp(),
708 "keep": keep,
709 }));
710
3b03abfe 711 if !(dry_run || keep) {
c9756b40 712 datastore.remove_backup_dir(&info.backup_dir, true)?;
8f0b4c1f 713 }
8f579717 714 }
dd8e744f
DM
715
716 Ok(())
717 };
718
719 worker.log_result(&result);
720
721 if let Err(err) = result {
722 bail!("prune failed - {}", err);
dda70154 723 };
83b7db02 724
dda70154 725 Ok(json!(prune_result))
83b7db02
DM
726}
727
dfc58d47
DM
728#[api(
729 input: {
730 properties: {
731 store: {
732 schema: DATASTORE_SCHEMA,
733 },
734 },
735 },
736 returns: {
737 schema: UPID_SCHEMA,
738 },
bb34b589 739 access: {
54552dda 740 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY, false),
bb34b589 741 },
dfc58d47
DM
742)]
743/// Start garbage collection.
6049b71f 744fn start_garbage_collection(
dfc58d47 745 store: String,
6049b71f 746 _info: &ApiMethod,
dd5495d6 747 rpcenv: &mut dyn RpcEnvironment,
6049b71f 748) -> Result<Value, Error> {
15e9b4ed 749
3e6a7dee 750 let datastore = DataStore::lookup_datastore(&store)?;
15e9b4ed 751
5a778d92 752 println!("Starting garbage collection on store {}", store);
15e9b4ed 753
0f778e06 754 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
15e9b4ed 755
0f778e06 756 let upid_str = WorkerTask::new_thread(
e7cb4dc5
WB
757 "garbage_collection",
758 Some(store.clone()),
759 Userid::root_userid().clone(),
760 to_stdout,
761 move |worker| {
0f778e06 762 worker.log(format!("starting garbage collection on store {}", store));
99641a6b 763 datastore.garbage_collection(&worker)
e7cb4dc5
WB
764 },
765 )?;
0f778e06
DM
766
767 Ok(json!(upid_str))
15e9b4ed
DM
768}
769
a92830dc
DM
770#[api(
771 input: {
772 properties: {
773 store: {
774 schema: DATASTORE_SCHEMA,
775 },
776 },
777 },
778 returns: {
779 type: GarbageCollectionStatus,
bb34b589
DM
780 },
781 access: {
782 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
783 },
a92830dc
DM
784)]
785/// Garbage collection status.
5eeea607 786pub fn garbage_collection_status(
a92830dc 787 store: String,
6049b71f 788 _info: &ApiMethod,
dd5495d6 789 _rpcenv: &mut dyn RpcEnvironment,
a92830dc 790) -> Result<GarbageCollectionStatus, Error> {
691c89a0 791
f2b99c34
DM
792 let datastore = DataStore::lookup_datastore(&store)?;
793
f2b99c34 794 let status = datastore.last_gc_status();
691c89a0 795
a92830dc 796 Ok(status)
691c89a0
DM
797}
798
bb34b589 799#[api(
30fb6025
DM
800 returns: {
801 description: "List the accessible datastores.",
802 type: Array,
803 items: {
804 description: "Datastore name and description.",
805 properties: {
806 store: {
807 schema: DATASTORE_SCHEMA,
808 },
809 comment: {
810 optional: true,
811 schema: SINGLE_LINE_COMMENT_SCHEMA,
812 },
813 },
814 },
815 },
bb34b589 816 access: {
54552dda 817 permission: &Permission::Anybody,
bb34b589
DM
818 },
819)]
820/// Datastore list
6049b71f
DM
821fn get_datastore_list(
822 _param: Value,
823 _info: &ApiMethod,
54552dda 824 rpcenv: &mut dyn RpcEnvironment,
6049b71f 825) -> Result<Value, Error> {
15e9b4ed 826
d0187a51 827 let (config, _digest) = datastore::config()?;
15e9b4ed 828
e7cb4dc5 829 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
54552dda
DM
830 let user_info = CachedUserInfo::new()?;
831
30fb6025 832 let mut list = Vec::new();
54552dda 833
30fb6025 834 for (store, (_, data)) in &config.sections {
e7cb4dc5 835 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
54552dda 836 let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0;
30fb6025
DM
837 if allowed {
838 let mut entry = json!({ "store": store });
839 if let Some(comment) = data["comment"].as_str() {
840 entry["comment"] = comment.into();
841 }
842 list.push(entry);
843 }
54552dda
DM
844 }
845
30fb6025 846 Ok(list.into())
15e9b4ed
DM
847}
848
0ab08ac9
DM
849#[sortable]
850pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new(
851 &ApiHandler::AsyncHttp(&download_file),
852 &ObjectSchema::new(
853 "Download single raw file from backup snapshot.",
854 &sorted!([
66c49c21 855 ("store", false, &DATASTORE_SCHEMA),
0ab08ac9
DM
856 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
857 ("backup-id", false, &BACKUP_ID_SCHEMA),
858 ("backup-time", false, &BACKUP_TIME_SCHEMA),
4191018c 859 ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA),
0ab08ac9
DM
860 ]),
861 )
54552dda
DM
862).access(None, &Permission::Privilege(
863 &["datastore", "{store}"],
864 PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
865 true)
866);
691c89a0 867
9e47c0a5
DM
868fn download_file(
869 _parts: Parts,
870 _req_body: Body,
871 param: Value,
255f378a 872 _info: &ApiMethod,
54552dda 873 rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 874) -> ApiResponseFuture {
9e47c0a5 875
ad51d02a
DM
876 async move {
877 let store = tools::required_string_param(&param, "store")?;
ad51d02a 878 let datastore = DataStore::lookup_datastore(store)?;
f14a8c9a 879
e7cb4dc5 880 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
54552dda 881 let user_info = CachedUserInfo::new()?;
e7cb4dc5 882 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
54552dda 883
ad51d02a 884 let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
9e47c0a5 885
ad51d02a
DM
886 let backup_type = tools::required_string_param(&param, "backup-type")?;
887 let backup_id = tools::required_string_param(&param, "backup-id")?;
888 let backup_time = tools::required_integer_param(&param, "backup-time")?;
9e47c0a5 889
54552dda
DM
890 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
891
892 let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
e7cb4dc5 893 if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
54552dda 894
abdb9763 895 println!("Download {} from {} ({}/{})", file_name, store, backup_dir, file_name);
9e47c0a5 896
ad51d02a
DM
897 let mut path = datastore.base_path();
898 path.push(backup_dir.relative_path());
899 path.push(&file_name);
900
ba694720 901 let file = tokio::fs::File::open(&path)
8aa67ee7
WB
902 .await
903 .map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
ad51d02a 904
db0cb9ce 905 let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
ba694720
DC
906 .map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze()))
907 .map_err(move |err| {
908 eprintln!("error during streaming of '{:?}' - {}", &path, err);
909 err
910 });
ad51d02a 911 let body = Body::wrap_stream(payload);
9e47c0a5 912
ad51d02a
DM
913 // fixme: set other headers ?
914 Ok(Response::builder()
915 .status(StatusCode::OK)
916 .header(header::CONTENT_TYPE, "application/octet-stream")
917 .body(body)
918 .unwrap())
919 }.boxed()
9e47c0a5
DM
920}
921
6ef9bb59
DC
922#[sortable]
923pub const API_METHOD_DOWNLOAD_FILE_DECODED: ApiMethod = ApiMethod::new(
924 &ApiHandler::AsyncHttp(&download_file_decoded),
925 &ObjectSchema::new(
926 "Download single decoded file from backup snapshot. Only works if it's not encrypted.",
927 &sorted!([
928 ("store", false, &DATASTORE_SCHEMA),
929 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
930 ("backup-id", false, &BACKUP_ID_SCHEMA),
931 ("backup-time", false, &BACKUP_TIME_SCHEMA),
932 ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA),
933 ]),
934 )
935).access(None, &Permission::Privilege(
936 &["datastore", "{store}"],
937 PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
938 true)
939);
940
941fn download_file_decoded(
942 _parts: Parts,
943 _req_body: Body,
944 param: Value,
945 _info: &ApiMethod,
946 rpcenv: Box<dyn RpcEnvironment>,
947) -> ApiResponseFuture {
948
949 async move {
950 let store = tools::required_string_param(&param, "store")?;
951 let datastore = DataStore::lookup_datastore(store)?;
952
e7cb4dc5 953 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
6ef9bb59 954 let user_info = CachedUserInfo::new()?;
e7cb4dc5 955 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
6ef9bb59
DC
956
957 let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
958
959 let backup_type = tools::required_string_param(&param, "backup-type")?;
960 let backup_id = tools::required_string_param(&param, "backup-id")?;
961 let backup_time = tools::required_integer_param(&param, "backup-time")?;
962
963 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
964
965 let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
e7cb4dc5 966 if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
6ef9bb59 967
2d55beec 968 let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
6ef9bb59 969 for file in files {
f28d9088 970 if file.filename == file_name && file.crypt_mode == Some(CryptMode::Encrypt) {
6ef9bb59
DC
971 bail!("cannot decode '{}' - is encrypted", file_name);
972 }
973 }
974
975 println!("Download {} from {} ({}/{})", file_name, store, backup_dir, file_name);
976
977 let mut path = datastore.base_path();
978 path.push(backup_dir.relative_path());
979 path.push(&file_name);
980
981 let extension = file_name.rsplitn(2, '.').next().unwrap();
982
983 let body = match extension {
984 "didx" => {
985 let index = DynamicIndexReader::open(&path)
986 .map_err(|err| format_err!("unable to read dynamic index '{:?}' - {}", &path, err))?;
2d55beec
FG
987 let (csum, size) = index.compute_csum();
988 manifest.verify_file(&file_name, &csum, size)?;
6ef9bb59 989
14f6c9cb 990 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
6ef9bb59 991 let reader = AsyncIndexReader::new(index, chunk_reader);
f386f512 992 Body::wrap_stream(AsyncReaderStream::new(reader)
6ef9bb59
DC
993 .map_err(move |err| {
994 eprintln!("error during streaming of '{:?}' - {}", path, err);
995 err
996 }))
997 },
998 "fidx" => {
999 let index = FixedIndexReader::open(&path)
1000 .map_err(|err| format_err!("unable to read fixed index '{:?}' - {}", &path, err))?;
1001
2d55beec
FG
1002 let (csum, size) = index.compute_csum();
1003 manifest.verify_file(&file_name, &csum, size)?;
1004
14f6c9cb 1005 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
6ef9bb59 1006 let reader = AsyncIndexReader::new(index, chunk_reader);
f386f512 1007 Body::wrap_stream(AsyncReaderStream::with_buffer_size(reader, 4*1024*1024)
6ef9bb59
DC
1008 .map_err(move |err| {
1009 eprintln!("error during streaming of '{:?}' - {}", path, err);
1010 err
1011 }))
1012 },
1013 "blob" => {
1014 let file = std::fs::File::open(&path)
8aa67ee7 1015 .map_err(|err| http_err!(BAD_REQUEST, "File open failed: {}", err))?;
6ef9bb59 1016
2d55beec
FG
1017 // FIXME: load full blob to verify index checksum?
1018
6ef9bb59
DC
1019 Body::wrap_stream(
1020 WrappedReaderStream::new(DataBlobReader::new(file, None)?)
1021 .map_err(move |err| {
1022 eprintln!("error during streaming of '{:?}' - {}", path, err);
1023 err
1024 })
1025 )
1026 },
1027 extension => {
1028 bail!("cannot download '{}' files", extension);
1029 },
1030 };
1031
1032 // fixme: set other headers ?
1033 Ok(Response::builder()
1034 .status(StatusCode::OK)
1035 .header(header::CONTENT_TYPE, "application/octet-stream")
1036 .body(body)
1037 .unwrap())
1038 }.boxed()
1039}
1040
552c2259 1041#[sortable]
0ab08ac9
DM
1042pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new(
1043 &ApiHandler::AsyncHttp(&upload_backup_log),
255f378a 1044 &ObjectSchema::new(
54552dda 1045 "Upload the client backup log file into a backup snapshot ('client.log.blob').",
552c2259 1046 &sorted!([
66c49c21 1047 ("store", false, &DATASTORE_SCHEMA),
255f378a 1048 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
0ab08ac9 1049 ("backup-id", false, &BACKUP_ID_SCHEMA),
255f378a 1050 ("backup-time", false, &BACKUP_TIME_SCHEMA),
552c2259 1051 ]),
9e47c0a5 1052 )
54552dda
DM
1053).access(
1054 Some("Only the backup creator/owner is allowed to do this."),
1055 &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_BACKUP, false)
1056);
9e47c0a5 1057
07ee2235
DM
1058fn upload_backup_log(
1059 _parts: Parts,
1060 req_body: Body,
1061 param: Value,
255f378a 1062 _info: &ApiMethod,
54552dda 1063 rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 1064) -> ApiResponseFuture {
07ee2235 1065
ad51d02a
DM
1066 async move {
1067 let store = tools::required_string_param(&param, "store")?;
ad51d02a 1068 let datastore = DataStore::lookup_datastore(store)?;
07ee2235 1069
96d65fbc 1070 let file_name = CLIENT_LOG_BLOB_NAME;
07ee2235 1071
ad51d02a
DM
1072 let backup_type = tools::required_string_param(&param, "backup-type")?;
1073 let backup_id = tools::required_string_param(&param, "backup-id")?;
1074 let backup_time = tools::required_integer_param(&param, "backup-time")?;
07ee2235 1075
ad51d02a 1076 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
07ee2235 1077
e7cb4dc5
WB
1078 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
1079 check_backup_owner(&datastore, backup_dir.group(), &userid)?;
54552dda 1080
ad51d02a
DM
1081 let mut path = datastore.base_path();
1082 path.push(backup_dir.relative_path());
1083 path.push(&file_name);
07ee2235 1084
ad51d02a
DM
1085 if path.exists() {
1086 bail!("backup already contains a log.");
1087 }
e128d4e8 1088
ad51d02a
DM
1089 println!("Upload backup log to {}/{}/{}/{}/{}", store,
1090 backup_type, backup_id, BackupDir::backup_time_to_string(backup_dir.backup_time()), file_name);
1091
1092 let data = req_body
1093 .map_err(Error::from)
1094 .try_fold(Vec::new(), |mut acc, chunk| {
1095 acc.extend_from_slice(&*chunk);
1096 future::ok::<_, Error>(acc)
1097 })
1098 .await?;
1099
39f18b30
DM
1100 // always verify blob/CRC at server side
1101 let blob = DataBlob::load_from_reader(&mut &data[..])?;
1102
1103 replace_file(&path, blob.raw_data(), CreateOptions::new())?;
ad51d02a
DM
1104
1105 // fixme: use correct formatter
1106 Ok(crate::server::formatter::json_response(Ok(Value::Null)))
1107 }.boxed()
07ee2235
DM
1108}
1109
5b1cfa01
DC
1110#[api(
1111 input: {
1112 properties: {
1113 store: {
1114 schema: DATASTORE_SCHEMA,
1115 },
1116 "backup-type": {
1117 schema: BACKUP_TYPE_SCHEMA,
1118 },
1119 "backup-id": {
1120 schema: BACKUP_ID_SCHEMA,
1121 },
1122 "backup-time": {
1123 schema: BACKUP_TIME_SCHEMA,
1124 },
1125 "filepath": {
1126 description: "Base64 encoded path.",
1127 type: String,
1128 }
1129 },
1130 },
1131 access: {
1132 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, true),
1133 },
1134)]
1135/// Get the entries of the given path of the catalog
1136fn catalog(
1137 store: String,
1138 backup_type: String,
1139 backup_id: String,
1140 backup_time: i64,
1141 filepath: String,
1142 _param: Value,
1143 _info: &ApiMethod,
1144 rpcenv: &mut dyn RpcEnvironment,
1145) -> Result<Value, Error> {
1146 let datastore = DataStore::lookup_datastore(&store)?;
1147
e7cb4dc5 1148 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
5b1cfa01 1149 let user_info = CachedUserInfo::new()?;
e7cb4dc5 1150 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
5b1cfa01
DC
1151
1152 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
1153
1154 let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
e7cb4dc5 1155 if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
5b1cfa01 1156
9238cdf5
FG
1157 let file_name = CATALOG_NAME;
1158
2d55beec 1159 let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
9238cdf5
FG
1160 for file in files {
1161 if file.filename == file_name && file.crypt_mode == Some(CryptMode::Encrypt) {
1162 bail!("cannot decode '{}' - is encrypted", file_name);
1163 }
1164 }
1165
5b1cfa01
DC
1166 let mut path = datastore.base_path();
1167 path.push(backup_dir.relative_path());
9238cdf5 1168 path.push(file_name);
5b1cfa01
DC
1169
1170 let index = DynamicIndexReader::open(&path)
1171 .map_err(|err| format_err!("unable to read dynamic index '{:?}' - {}", &path, err))?;
1172
2d55beec
FG
1173 let (csum, size) = index.compute_csum();
1174 manifest.verify_file(&file_name, &csum, size)?;
1175
14f6c9cb 1176 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
5b1cfa01
DC
1177 let reader = BufferedDynamicReader::new(index, chunk_reader);
1178
1179 let mut catalog_reader = CatalogReader::new(reader);
1180 let mut current = catalog_reader.root()?;
1181 let mut components = vec![];
1182
1183
1184 if filepath != "root" {
1185 components = base64::decode(filepath)?;
1186 if components.len() > 0 && components[0] == '/' as u8 {
1187 components.remove(0);
1188 }
1189 for component in components.split(|c| *c == '/' as u8) {
1190 if let Some(entry) = catalog_reader.lookup(&current, component)? {
1191 current = entry;
1192 } else {
1193 bail!("path {:?} not found in catalog", &String::from_utf8_lossy(&components));
1194 }
1195 }
1196 }
1197
1198 let mut res = Vec::new();
1199
1200 for direntry in catalog_reader.read_dir(&current)? {
1201 let mut components = components.clone();
1202 components.push('/' as u8);
1203 components.extend(&direntry.name);
1204 let path = base64::encode(components);
1205 let text = String::from_utf8_lossy(&direntry.name);
1206 let mut entry = json!({
1207 "filepath": path,
1208 "text": text,
1209 "type": CatalogEntryType::from(&direntry.attr).to_string(),
1210 "leaf": true,
1211 });
1212 match direntry.attr {
1213 DirEntryAttribute::Directory { start: _ } => {
1214 entry["leaf"] = false.into();
1215 },
1216 DirEntryAttribute::File { size, mtime } => {
1217 entry["size"] = size.into();
1218 entry["mtime"] = mtime.into();
1219 },
1220 _ => {},
1221 }
1222 res.push(entry);
1223 }
1224
1225 Ok(res.into())
1226}
1227
d33d8f4e
DC
1228#[sortable]
1229pub const API_METHOD_PXAR_FILE_DOWNLOAD: ApiMethod = ApiMethod::new(
1230 &ApiHandler::AsyncHttp(&pxar_file_download),
1231 &ObjectSchema::new(
1ffe0301 1232 "Download single file from pxar file of a backup snapshot. Only works if it's not encrypted.",
d33d8f4e
DC
1233 &sorted!([
1234 ("store", false, &DATASTORE_SCHEMA),
1235 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
1236 ("backup-id", false, &BACKUP_ID_SCHEMA),
1237 ("backup-time", false, &BACKUP_TIME_SCHEMA),
1238 ("filepath", false, &StringSchema::new("Base64 encoded path").schema()),
1239 ]),
1240 )
1241).access(None, &Permission::Privilege(
1242 &["datastore", "{store}"],
1243 PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP,
1244 true)
1245);
1246
1247fn pxar_file_download(
1248 _parts: Parts,
1249 _req_body: Body,
1250 param: Value,
1251 _info: &ApiMethod,
1252 rpcenv: Box<dyn RpcEnvironment>,
1253) -> ApiResponseFuture {
1254
1255 async move {
1256 let store = tools::required_string_param(&param, "store")?;
1257 let datastore = DataStore::lookup_datastore(&store)?;
1258
e7cb4dc5 1259 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
d33d8f4e 1260 let user_info = CachedUserInfo::new()?;
e7cb4dc5 1261 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
d33d8f4e
DC
1262
1263 let filepath = tools::required_string_param(&param, "filepath")?.to_owned();
1264
1265 let backup_type = tools::required_string_param(&param, "backup-type")?;
1266 let backup_id = tools::required_string_param(&param, "backup-id")?;
1267 let backup_time = tools::required_integer_param(&param, "backup-time")?;
1268
1269 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
1270
1271 let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
e7cb4dc5 1272 if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
d33d8f4e 1273
d33d8f4e
DC
1274 let mut components = base64::decode(&filepath)?;
1275 if components.len() > 0 && components[0] == '/' as u8 {
1276 components.remove(0);
1277 }
1278
1279 let mut split = components.splitn(2, |c| *c == '/' as u8);
9238cdf5 1280 let pxar_name = std::str::from_utf8(split.next().unwrap())?;
d33d8f4e 1281 let file_path = split.next().ok_or(format_err!("filepath looks strange '{}'", filepath))?;
2d55beec 1282 let (manifest, files) = read_backup_index(&datastore, &backup_dir)?;
9238cdf5
FG
1283 for file in files {
1284 if file.filename == pxar_name && file.crypt_mode == Some(CryptMode::Encrypt) {
1285 bail!("cannot decode '{}' - is encrypted", pxar_name);
1286 }
1287 }
d33d8f4e 1288
9238cdf5
FG
1289 let mut path = datastore.base_path();
1290 path.push(backup_dir.relative_path());
1291 path.push(pxar_name);
d33d8f4e
DC
1292
1293 let index = DynamicIndexReader::open(&path)
1294 .map_err(|err| format_err!("unable to read dynamic index '{:?}' - {}", &path, err))?;
1295
2d55beec
FG
1296 let (csum, size) = index.compute_csum();
1297 manifest.verify_file(&pxar_name, &csum, size)?;
1298
14f6c9cb 1299 let chunk_reader = LocalChunkReader::new(datastore, None, CryptMode::None);
d33d8f4e
DC
1300 let reader = BufferedDynamicReader::new(index, chunk_reader);
1301 let archive_size = reader.archive_size();
1302 let reader = LocalDynamicReadAt::new(reader);
1303
1304 let decoder = Accessor::new(reader, archive_size).await?;
1305 let root = decoder.open_root().await?;
1306 let file = root
1307 .lookup(OsStr::from_bytes(file_path)).await?
1308 .ok_or(format_err!("error opening '{:?}'", file_path))?;
1309
1310 let file = match file.kind() {
1311 EntryKind::File { .. } => file,
1312 EntryKind::Hardlink(_) => {
1313 decoder.follow_hardlink(&file).await?
1314 },
1315 // TODO symlink
1316 other => bail!("cannot download file of type {:?}", other),
1317 };
1318
1319 let body = Body::wrap_stream(
1320 AsyncReaderStream::new(file.contents().await?)
1321 .map_err(move |err| {
1322 eprintln!("error during streaming of '{:?}' - {}", filepath, err);
1323 err
1324 })
1325 );
1326
1327 // fixme: set other headers ?
1328 Ok(Response::builder()
1329 .status(StatusCode::OK)
1330 .header(header::CONTENT_TYPE, "application/octet-stream")
1331 .body(body)
1332 .unwrap())
1333 }.boxed()
1334}
1335
1a0d3d11
DM
1336#[api(
1337 input: {
1338 properties: {
1339 store: {
1340 schema: DATASTORE_SCHEMA,
1341 },
1342 timeframe: {
1343 type: RRDTimeFrameResolution,
1344 },
1345 cf: {
1346 type: RRDMode,
1347 },
1348 },
1349 },
1350 access: {
1351 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
1352 },
1353)]
1354/// Read datastore stats
1355fn get_rrd_stats(
1356 store: String,
1357 timeframe: RRDTimeFrameResolution,
1358 cf: RRDMode,
1359 _param: Value,
1360) -> Result<Value, Error> {
1361
431cc7b1
DC
1362 create_value_from_rrd(
1363 &format!("datastore/{}", store),
1a0d3d11
DM
1364 &[
1365 "total", "used",
c94e1f65
DM
1366 "read_ios", "read_bytes",
1367 "write_ios", "write_bytes",
1368 "io_ticks",
1a0d3d11
DM
1369 ],
1370 timeframe,
1371 cf,
1372 )
1373}
1374
912b3f5b
DM
1375#[api(
1376 input: {
1377 properties: {
1378 store: {
1379 schema: DATASTORE_SCHEMA,
1380 },
1381 "backup-type": {
1382 schema: BACKUP_TYPE_SCHEMA,
1383 },
1384 "backup-id": {
1385 schema: BACKUP_ID_SCHEMA,
1386 },
1387 "backup-time": {
1388 schema: BACKUP_TIME_SCHEMA,
1389 },
1390 },
1391 },
1392 access: {
1393 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_READ | PRIV_DATASTORE_BACKUP, true),
1394 },
1395)]
1396/// Get "notes" for a specific backup
1397fn get_notes(
1398 store: String,
1399 backup_type: String,
1400 backup_id: String,
1401 backup_time: i64,
1402 rpcenv: &mut dyn RpcEnvironment,
1403) -> Result<String, Error> {
1404 let datastore = DataStore::lookup_datastore(&store)?;
1405
e7cb4dc5 1406 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
912b3f5b 1407 let user_info = CachedUserInfo::new()?;
e7cb4dc5 1408 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
912b3f5b
DM
1409
1410 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
1411
1412 let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
e7cb4dc5 1413 if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
912b3f5b
DM
1414
1415 let manifest = datastore.load_manifest_json(&backup_dir)?;
1416
1417 let notes = manifest["unprotected"]["notes"]
1418 .as_str()
1419 .unwrap_or("");
1420
1421 Ok(String::from(notes))
1422}
1423
1424#[api(
1425 input: {
1426 properties: {
1427 store: {
1428 schema: DATASTORE_SCHEMA,
1429 },
1430 "backup-type": {
1431 schema: BACKUP_TYPE_SCHEMA,
1432 },
1433 "backup-id": {
1434 schema: BACKUP_ID_SCHEMA,
1435 },
1436 "backup-time": {
1437 schema: BACKUP_TIME_SCHEMA,
1438 },
1439 notes: {
1440 description: "A multiline text.",
1441 },
1442 },
1443 },
1444 access: {
1445 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_MODIFY, true),
1446 },
1447)]
1448/// Set "notes" for a specific backup
1449fn set_notes(
1450 store: String,
1451 backup_type: String,
1452 backup_id: String,
1453 backup_time: i64,
1454 notes: String,
1455 rpcenv: &mut dyn RpcEnvironment,
1456) -> Result<(), Error> {
1457 let datastore = DataStore::lookup_datastore(&store)?;
1458
e7cb4dc5 1459 let userid: Userid = rpcenv.get_user().unwrap().parse()?;
912b3f5b 1460 let user_info = CachedUserInfo::new()?;
e7cb4dc5 1461 let user_privs = user_info.lookup_privs(&userid, &["datastore", &store]);
912b3f5b
DM
1462
1463 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
1464
1465 let allowed = (user_privs & PRIV_DATASTORE_READ) != 0;
e7cb4dc5 1466 if !allowed { check_backup_owner(&datastore, backup_dir.group(), &userid)?; }
912b3f5b
DM
1467
1468 let mut manifest = datastore.load_manifest_json(&backup_dir)?;
1469
1470 manifest["unprotected"]["notes"] = notes.into();
1471
1472 datastore.store_manifest(&backup_dir, manifest)?;
1473
1474 Ok(())
1475}
1476
552c2259 1477#[sortable]
255f378a 1478const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
5b1cfa01
DC
1479 (
1480 "catalog",
1481 &Router::new()
1482 .get(&API_METHOD_CATALOG)
1483 ),
255f378a
DM
1484 (
1485 "download",
1486 &Router::new()
1487 .download(&API_METHOD_DOWNLOAD_FILE)
1488 ),
6ef9bb59
DC
1489 (
1490 "download-decoded",
1491 &Router::new()
1492 .download(&API_METHOD_DOWNLOAD_FILE_DECODED)
1493 ),
255f378a
DM
1494 (
1495 "files",
1496 &Router::new()
09b1f7b2 1497 .get(&API_METHOD_LIST_SNAPSHOT_FILES)
255f378a
DM
1498 ),
1499 (
1500 "gc",
1501 &Router::new()
1502 .get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
1503 .post(&API_METHOD_START_GARBAGE_COLLECTION)
1504 ),
1505 (
1506 "groups",
1507 &Router::new()
b31c8019 1508 .get(&API_METHOD_LIST_GROUPS)
255f378a 1509 ),
912b3f5b
DM
1510 (
1511 "notes",
1512 &Router::new()
1513 .get(&API_METHOD_GET_NOTES)
1514 .put(&API_METHOD_SET_NOTES)
1515 ),
255f378a
DM
1516 (
1517 "prune",
1518 &Router::new()
1519 .post(&API_METHOD_PRUNE)
1520 ),
d33d8f4e
DC
1521 (
1522 "pxar-file-download",
1523 &Router::new()
1524 .download(&API_METHOD_PXAR_FILE_DOWNLOAD)
1525 ),
1a0d3d11
DM
1526 (
1527 "rrd",
1528 &Router::new()
1529 .get(&API_METHOD_GET_RRD_STATS)
1530 ),
255f378a
DM
1531 (
1532 "snapshots",
1533 &Router::new()
fc189b19 1534 .get(&API_METHOD_LIST_SNAPSHOTS)
68a6a0ee 1535 .delete(&API_METHOD_DELETE_SNAPSHOT)
255f378a
DM
1536 ),
1537 (
1538 "status",
1539 &Router::new()
1540 .get(&API_METHOD_STATUS)
1541 ),
1542 (
1543 "upload-backup-log",
1544 &Router::new()
1545 .upload(&API_METHOD_UPLOAD_BACKUP_LOG)
1546 ),
c2009e53
DM
1547 (
1548 "verify",
1549 &Router::new()
1550 .post(&API_METHOD_VERIFY)
1551 ),
255f378a
DM
1552];
1553
ad51d02a 1554const DATASTORE_INFO_ROUTER: Router = Router::new()
255f378a
DM
1555 .get(&list_subdirs_api_method!(DATASTORE_INFO_SUBDIRS))
1556 .subdirs(DATASTORE_INFO_SUBDIRS);
1557
1558
1559pub const ROUTER: Router = Router::new()
bb34b589 1560 .get(&API_METHOD_GET_DATASTORE_LIST)
255f378a 1561 .match_all("store", &DATASTORE_INFO_ROUTER);