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