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