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