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