]> git.proxmox.com Git - proxmox-backup.git/blame - src/api2/admin/datastore.rs
switch from failure to anyhow
[proxmox-backup.git] / src / api2 / admin / datastore.rs
CommitLineData
cad540e9 1use std::collections::{HashSet, HashMap};
09b1f7b2 2use std::convert::TryFrom;
cad540e9 3
aeeac29b 4use chrono::{TimeZone, Local};
f7d4e4b5 5use anyhow::{bail, 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,
13 RpcEnvironment, RpcEnvironmentType, Permission};
cad540e9
WB
14use proxmox::api::router::SubdirMap;
15use proxmox::api::schema::*;
feaa1ad3 16use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
9ea4bce4
WB
17use proxmox::try_block;
18use proxmox::{http_err, identity, list_subdirs_api_method, sortable};
e18a6c9e 19
cad540e9 20use crate::api2::types::*;
e5064ba6 21use crate::backup::*;
cad540e9 22use crate::config::datastore;
0f778e06 23use crate::server::WorkerTask;
cad540e9 24use crate::tools;
bb34b589 25use crate::config::acl::{PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_ALLOCATE_SPACE};
1629d2ad 26
09b1f7b2 27fn read_backup_index(store: &DataStore, backup_dir: &BackupDir) -> Result<Vec<BackupContent>, Error> {
8c70e3eb
DM
28
29 let mut path = store.base_path();
30 path.push(backup_dir.relative_path());
31 path.push("index.json.blob");
32
33 let raw_data = file_get_contents(&path)?;
09b1f7b2
DM
34 let index_size = raw_data.len() as u64;
35 let blob = DataBlob::from_raw(raw_data)?;
8c70e3eb 36
09b1f7b2 37 let manifest = BackupManifest::try_from(blob)?;
8c70e3eb 38
09b1f7b2
DM
39 let mut result = Vec::new();
40 for item in manifest.files() {
41 result.push(BackupContent {
42 filename: item.filename.clone(),
43 size: Some(item.size),
44 });
8c70e3eb
DM
45 }
46
09b1f7b2
DM
47 result.push(BackupContent {
48 filename: "index.json.blob".to_string(),
49 size: Some(index_size),
50 });
4f1e40a2 51
8c70e3eb
DM
52 Ok(result)
53}
54
8f579717
DM
55fn group_backups(backup_list: Vec<BackupInfo>) -> HashMap<String, Vec<BackupInfo>> {
56
57 let mut group_hash = HashMap::new();
58
59 for info in backup_list {
9b492eb2 60 let group_id = info.backup_dir.group().group_path().to_str().unwrap().to_owned();
8f579717
DM
61 let time_list = group_hash.entry(group_id).or_insert(vec![]);
62 time_list.push(info);
63 }
64
65 group_hash
66}
67
b31c8019
DM
68#[api(
69 input: {
70 properties: {
71 store: {
72 schema: DATASTORE_SCHEMA,
73 },
74 },
75 },
76 returns: {
77 type: Array,
78 description: "Returns the list of backup groups.",
79 items: {
80 type: GroupListItem,
81 }
82 },
bb34b589
DM
83 access: {
84 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
85 },
b31c8019
DM
86)]
87/// List backup groups.
ad20d198 88fn list_groups(
b31c8019
DM
89 store: String,
90) -> Result<Vec<GroupListItem>, Error> {
812c6f87 91
b31c8019 92 let datastore = DataStore::lookup_datastore(&store)?;
812c6f87 93
c0977501 94 let backup_list = BackupInfo::list_backups(&datastore.base_path())?;
812c6f87
DM
95
96 let group_hash = group_backups(backup_list);
97
b31c8019 98 let mut groups = Vec::new();
812c6f87
DM
99
100 for (_group_id, mut list) in group_hash {
101
2b01a225 102 BackupInfo::sort_list(&mut list, false);
812c6f87
DM
103
104 let info = &list[0];
9b492eb2 105 let group = info.backup_dir.group();
812c6f87 106
b31c8019
DM
107 let result_item = GroupListItem {
108 backup_type: group.backup_type().to_string(),
109 backup_id: group.backup_id().to_string(),
110 last_backup: info.backup_dir.backup_time().timestamp(),
111 backup_count: list.len() as u64,
112 files: info.files.clone(),
113 };
114 groups.push(result_item);
812c6f87
DM
115 }
116
b31c8019 117 Ok(groups)
812c6f87 118}
8f579717 119
09b1f7b2
DM
120#[api(
121 input: {
122 properties: {
123 store: {
124 schema: DATASTORE_SCHEMA,
125 },
126 "backup-type": {
127 schema: BACKUP_TYPE_SCHEMA,
128 },
129 "backup-id": {
130 schema: BACKUP_ID_SCHEMA,
131 },
132 "backup-time": {
133 schema: BACKUP_TIME_SCHEMA,
134 },
135 },
136 },
137 returns: {
138 type: Array,
139 description: "Returns the list of archive files inside a backup snapshots.",
140 items: {
141 type: BackupContent,
142 }
143 },
bb34b589
DM
144 access: {
145 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
146 },
09b1f7b2
DM
147)]
148/// List snapshot files.
ea5f547f 149pub fn list_snapshot_files(
09b1f7b2
DM
150 store: String,
151 backup_type: String,
152 backup_id: String,
153 backup_time: i64,
01a13423 154 _info: &ApiMethod,
dd5495d6 155 _rpcenv: &mut dyn RpcEnvironment,
09b1f7b2 156) -> Result<Vec<BackupContent>, Error> {
01a13423 157
09b1f7b2 158 let datastore = DataStore::lookup_datastore(&store)?;
01a13423
DM
159 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
160
d7c24397
DM
161 let mut files = read_backup_index(&datastore, &snapshot)?;
162
163 let info = BackupInfo::new(&datastore.base_path(), snapshot)?;
01a13423 164
09b1f7b2
DM
165 let file_set = files.iter().fold(HashSet::new(), |mut acc, item| {
166 acc.insert(item.filename.clone());
d7c24397
DM
167 acc
168 });
169
170 for file in info.files {
171 if file_set.contains(&file) { continue; }
09b1f7b2 172 files.push(BackupContent { filename: file, size: None });
d7c24397 173 }
01a13423 174
8c70e3eb 175 Ok(files)
01a13423
DM
176}
177
68a6a0ee
DM
178#[api(
179 input: {
180 properties: {
181 store: {
182 schema: DATASTORE_SCHEMA,
183 },
184 "backup-type": {
185 schema: BACKUP_TYPE_SCHEMA,
186 },
187 "backup-id": {
188 schema: BACKUP_ID_SCHEMA,
189 },
190 "backup-time": {
191 schema: BACKUP_TIME_SCHEMA,
192 },
193 },
194 },
bb34b589
DM
195 access: {
196 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_ALLOCATE_SPACE, false),
197 },
68a6a0ee
DM
198)]
199/// Delete backup snapshot.
200fn delete_snapshot(
201 store: String,
202 backup_type: String,
203 backup_id: String,
204 backup_time: i64,
6f62c924 205 _info: &ApiMethod,
dd5495d6 206 _rpcenv: &mut dyn RpcEnvironment,
6f62c924
DM
207) -> Result<Value, Error> {
208
391d3107 209 let snapshot = BackupDir::new(backup_type, backup_id, backup_time);
6f62c924 210
68a6a0ee 211 let datastore = DataStore::lookup_datastore(&store)?;
6f62c924
DM
212
213 datastore.remove_backup_dir(&snapshot)?;
214
215 Ok(Value::Null)
216}
217
fc189b19
DM
218#[api(
219 input: {
220 properties: {
221 store: {
222 schema: DATASTORE_SCHEMA,
223 },
224 "backup-type": {
225 optional: true,
226 schema: BACKUP_TYPE_SCHEMA,
227 },
228 "backup-id": {
229 optional: true,
230 schema: BACKUP_ID_SCHEMA,
231 },
232 },
233 },
234 returns: {
235 type: Array,
236 description: "Returns the list of snapshots.",
237 items: {
238 type: SnapshotListItem,
239 }
240 },
bb34b589
DM
241 access: {
242 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
243 },
fc189b19
DM
244)]
245/// List backup snapshots.
f24fc116 246pub fn list_snapshots (
184f17af
DM
247 param: Value,
248 _info: &ApiMethod,
dd5495d6 249 _rpcenv: &mut dyn RpcEnvironment,
fc189b19 250) -> Result<Vec<SnapshotListItem>, Error> {
184f17af
DM
251
252 let store = tools::required_string_param(&param, "store")?;
15c847f1
DM
253 let backup_type = param["backup-type"].as_str();
254 let backup_id = param["backup-id"].as_str();
184f17af
DM
255
256 let datastore = DataStore::lookup_datastore(store)?;
257
c0977501 258 let base_path = datastore.base_path();
184f17af 259
15c847f1 260 let backup_list = BackupInfo::list_backups(&base_path)?;
184f17af
DM
261
262 let mut snapshots = vec![];
263
c0977501 264 for info in backup_list {
15c847f1
DM
265 let group = info.backup_dir.group();
266 if let Some(backup_type) = backup_type {
267 if backup_type != group.backup_type() { continue; }
268 }
a17a0e7a 269 if let Some(backup_id) = backup_id {
15c847f1
DM
270 if backup_id != group.backup_id() { continue; }
271 }
a17a0e7a 272
fc189b19
DM
273 let mut result_item = SnapshotListItem {
274 backup_type: group.backup_type().to_string(),
275 backup_id: group.backup_id().to_string(),
276 backup_time: info.backup_dir.backup_time().timestamp(),
277 files: info.files,
278 size: None,
279 };
a17a0e7a
DM
280
281 if let Ok(index) = read_backup_index(&datastore, &info.backup_dir) {
282 let mut backup_size = 0;
09b1f7b2
DM
283 for item in index.iter() {
284 if let Some(item_size) = item.size {
a17a0e7a
DM
285 backup_size += item_size;
286 }
287 }
fc189b19 288 result_item.size = Some(backup_size);
a17a0e7a
DM
289 }
290
291 snapshots.push(result_item);
184f17af
DM
292 }
293
fc189b19 294 Ok(snapshots)
184f17af
DM
295}
296
1dc117bb
DM
297#[api(
298 input: {
299 properties: {
300 store: {
301 schema: DATASTORE_SCHEMA,
302 },
303 },
304 },
305 returns: {
306 type: StorageStatus,
307 },
bb34b589
DM
308 access: {
309 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
310 },
1dc117bb
DM
311)]
312/// Get datastore status.
ea5f547f 313pub fn status(
1dc117bb 314 store: String,
0eecf38f
DM
315 _info: &ApiMethod,
316 _rpcenv: &mut dyn RpcEnvironment,
1dc117bb 317) -> Result<StorageStatus, Error> {
0eecf38f 318
1dc117bb 319 let datastore = DataStore::lookup_datastore(&store)?;
0eecf38f
DM
320
321 let base_path = datastore.base_path();
322
323 let mut stat: libc::statfs64 = unsafe { std::mem::zeroed() };
324
325 use nix::NixPath;
326
327 let res = base_path.with_nix_path(|cstr| unsafe { libc::statfs64(cstr.as_ptr(), &mut stat) })?;
328 nix::errno::Errno::result(res)?;
329
330 let bsize = stat.f_bsize as u64;
1dc117bb
DM
331
332 Ok(StorageStatus {
333 total: stat.f_blocks*bsize,
334 used: (stat.f_blocks-stat.f_bfree)*bsize,
335 avail: stat.f_bavail*bsize,
336 })
0eecf38f
DM
337}
338
255f378a
DM
339#[macro_export]
340macro_rules! add_common_prune_prameters {
552c2259
DM
341 ( [ $( $list1:tt )* ] ) => {
342 add_common_prune_prameters!([$( $list1 )* ] , [])
343 };
344 ( [ $( $list1:tt )* ] , [ $( $list2:tt )* ] ) => {
255f378a 345 [
552c2259 346 $( $list1 )*
255f378a 347 (
552c2259 348 "keep-daily",
255f378a 349 true,
552c2259 350 &IntegerSchema::new("Number of daily backups to keep.")
255f378a
DM
351 .minimum(1)
352 .schema()
353 ),
102d8d41
DM
354 (
355 "keep-hourly",
356 true,
357 &IntegerSchema::new("Number of hourly backups to keep.")
358 .minimum(1)
359 .schema()
360 ),
255f378a 361 (
552c2259 362 "keep-last",
255f378a 363 true,
552c2259 364 &IntegerSchema::new("Number of backups to keep.")
255f378a
DM
365 .minimum(1)
366 .schema()
367 ),
368 (
552c2259 369 "keep-monthly",
255f378a 370 true,
552c2259 371 &IntegerSchema::new("Number of monthly backups to keep.")
255f378a
DM
372 .minimum(1)
373 .schema()
374 ),
375 (
552c2259 376 "keep-weekly",
255f378a 377 true,
552c2259 378 &IntegerSchema::new("Number of weekly backups to keep.")
255f378a
DM
379 .minimum(1)
380 .schema()
381 ),
382 (
383 "keep-yearly",
384 true,
385 &IntegerSchema::new("Number of yearly backups to keep.")
386 .minimum(1)
387 .schema()
388 ),
552c2259 389 $( $list2 )*
255f378a
DM
390 ]
391 }
0eecf38f
DM
392}
393
0ab08ac9
DM
394const API_METHOD_PRUNE: ApiMethod = ApiMethod::new(
395 &ApiHandler::Sync(&prune),
255f378a 396 &ObjectSchema::new(
0ab08ac9
DM
397 "Prune the datastore.",
398 &add_common_prune_prameters!([
399 ("backup-id", false, &BACKUP_ID_SCHEMA),
400 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
3b03abfe
DM
401 ("dry-run", true, &BooleanSchema::new(
402 "Just show what prune would do, but do not delete anything.")
403 .schema()
404 ),
0ab08ac9 405 ],[
66c49c21 406 ("store", false, &DATASTORE_SCHEMA),
0ab08ac9 407 ])
255f378a 408 )
bb34b589 409).access(None, &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_ALLOCATE_SPACE, false));
255f378a 410
83b7db02
DM
411fn prune(
412 param: Value,
413 _info: &ApiMethod,
dd5495d6 414 _rpcenv: &mut dyn RpcEnvironment,
83b7db02
DM
415) -> Result<Value, Error> {
416
417 let store = param["store"].as_str().unwrap();
418
9fdc3ef4
DM
419 let backup_type = tools::required_string_param(&param, "backup-type")?;
420 let backup_id = tools::required_string_param(&param, "backup-id")?;
421
3b03abfe
DM
422 let dry_run = param["dry-run"].as_bool().unwrap_or(false);
423
9fdc3ef4
DM
424 let group = BackupGroup::new(backup_type, backup_id);
425
83b7db02
DM
426 let datastore = DataStore::lookup_datastore(store)?;
427
9e3f0088
DM
428 let prune_options = PruneOptions {
429 keep_last: param["keep-last"].as_u64(),
102d8d41 430 keep_hourly: param["keep-hourly"].as_u64(),
9e3f0088
DM
431 keep_daily: param["keep-daily"].as_u64(),
432 keep_weekly: param["keep-weekly"].as_u64(),
433 keep_monthly: param["keep-monthly"].as_u64(),
434 keep_yearly: param["keep-yearly"].as_u64(),
435 };
8f579717 436
503995c7
DM
437 let worker_id = format!("{}_{}_{}", store, backup_type, backup_id);
438
dda70154
DM
439 let mut prune_result = Vec::new();
440
441 let list = group.list_backups(&datastore.base_path())?;
442
443 let mut prune_info = compute_prune_info(list, &prune_options)?;
444
445 prune_info.reverse(); // delete older snapshots first
446
447 let keep_all = !prune_options.keeps_something();
448
449 if dry_run {
450 for (info, mut keep) in prune_info {
451 if keep_all { keep = true; }
452
453 let backup_time = info.backup_dir.backup_time();
454 let group = info.backup_dir.group();
455
456 prune_result.push(json!({
457 "backup-type": group.backup_type(),
458 "backup-id": group.backup_id(),
459 "backup-time": backup_time.timestamp(),
460 "keep": keep,
461 }));
462 }
463 return Ok(json!(prune_result));
464 }
465
466
163e9bbe 467 // We use a WorkerTask just to have a task log, but run synchrounously
503995c7 468 let worker = WorkerTask::new("prune", Some(worker_id), "root@pam", true)?;
dda70154 469
dd8e744f 470 let result = try_block! {
dda70154 471 if keep_all {
9fdc3ef4 472 worker.log("No prune selection - keeping all files.");
dd8e744f 473 } else {
236a396a 474 worker.log(format!("retention options: {}", prune_options.cli_options_string()));
dda70154
DM
475 worker.log(format!("Starting prune on store \"{}\" group \"{}/{}\"",
476 store, backup_type, backup_id));
dd8e744f 477 }
8f579717 478
dda70154
DM
479 for (info, mut keep) in prune_info {
480 if keep_all { keep = true; }
dd8e744f 481
3b03abfe
DM
482 let backup_time = info.backup_dir.backup_time();
483 let timestamp = BackupDir::backup_time_to_string(backup_time);
484 let group = info.backup_dir.group();
485
dda70154 486
3b03abfe
DM
487 let msg = format!(
488 "{}/{}/{} {}",
489 group.backup_type(),
490 group.backup_id(),
491 timestamp,
492 if keep { "keep" } else { "remove" },
493 );
494
495 worker.log(msg);
496
dda70154
DM
497 prune_result.push(json!({
498 "backup-type": group.backup_type(),
499 "backup-id": group.backup_id(),
500 "backup-time": backup_time.timestamp(),
501 "keep": keep,
502 }));
503
3b03abfe 504 if !(dry_run || keep) {
8f0b4c1f
DM
505 datastore.remove_backup_dir(&info.backup_dir)?;
506 }
8f579717 507 }
dd8e744f
DM
508
509 Ok(())
510 };
511
512 worker.log_result(&result);
513
514 if let Err(err) = result {
515 bail!("prune failed - {}", err);
dda70154 516 };
83b7db02 517
dda70154 518 Ok(json!(prune_result))
83b7db02
DM
519}
520
dfc58d47
DM
521#[api(
522 input: {
523 properties: {
524 store: {
525 schema: DATASTORE_SCHEMA,
526 },
527 },
528 },
529 returns: {
530 schema: UPID_SCHEMA,
531 },
bb34b589
DM
532 access: {
533 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_ALLOCATE_SPACE, false),
534 },
dfc58d47
DM
535)]
536/// Start garbage collection.
6049b71f 537fn start_garbage_collection(
dfc58d47 538 store: String,
6049b71f 539 _info: &ApiMethod,
dd5495d6 540 rpcenv: &mut dyn RpcEnvironment,
6049b71f 541) -> Result<Value, Error> {
15e9b4ed 542
3e6a7dee 543 let datastore = DataStore::lookup_datastore(&store)?;
15e9b4ed 544
5a778d92 545 println!("Starting garbage collection on store {}", store);
15e9b4ed 546
0f778e06 547 let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
15e9b4ed 548
0f778e06
DM
549 let upid_str = WorkerTask::new_thread(
550 "garbage_collection", Some(store.clone()), "root@pam", to_stdout, move |worker|
551 {
552 worker.log(format!("starting garbage collection on store {}", store));
d4b59ae0 553 datastore.garbage_collection(worker)
0f778e06
DM
554 })?;
555
556 Ok(json!(upid_str))
15e9b4ed
DM
557}
558
a92830dc
DM
559#[api(
560 input: {
561 properties: {
562 store: {
563 schema: DATASTORE_SCHEMA,
564 },
565 },
566 },
567 returns: {
568 type: GarbageCollectionStatus,
bb34b589
DM
569 },
570 access: {
571 permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT, false),
572 },
a92830dc
DM
573)]
574/// Garbage collection status.
5eeea607 575pub fn garbage_collection_status(
a92830dc 576 store: String,
6049b71f 577 _info: &ApiMethod,
dd5495d6 578 _rpcenv: &mut dyn RpcEnvironment,
a92830dc 579) -> Result<GarbageCollectionStatus, Error> {
691c89a0 580
f2b99c34
DM
581 let datastore = DataStore::lookup_datastore(&store)?;
582
f2b99c34 583 let status = datastore.last_gc_status();
691c89a0 584
a92830dc 585 Ok(status)
691c89a0
DM
586}
587
bb34b589
DM
588#[api(
589 access: {
590 permission: &Permission::Privilege(&["datastore"], PRIV_DATASTORE_AUDIT, false),
591 },
592)]
593/// Datastore list
6049b71f
DM
594fn get_datastore_list(
595 _param: Value,
596 _info: &ApiMethod,
dd5495d6 597 _rpcenv: &mut dyn RpcEnvironment,
6049b71f 598) -> Result<Value, Error> {
15e9b4ed 599
d0187a51 600 let (config, _digest) = datastore::config()?;
15e9b4ed 601
5eeea607 602 Ok(config.convert_to_array("store", None, &[]))
15e9b4ed
DM
603}
604
0ab08ac9
DM
605#[sortable]
606pub const API_METHOD_DOWNLOAD_FILE: ApiMethod = ApiMethod::new(
607 &ApiHandler::AsyncHttp(&download_file),
608 &ObjectSchema::new(
609 "Download single raw file from backup snapshot.",
610 &sorted!([
66c49c21 611 ("store", false, &DATASTORE_SCHEMA),
0ab08ac9
DM
612 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
613 ("backup-id", false, &BACKUP_ID_SCHEMA),
614 ("backup-time", false, &BACKUP_TIME_SCHEMA),
4191018c 615 ("file-name", false, &BACKUP_ARCHIVE_NAME_SCHEMA),
0ab08ac9
DM
616 ]),
617 )
bb34b589 618).access(None, &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_ALLOCATE_SPACE, false));
691c89a0 619
9e47c0a5
DM
620fn download_file(
621 _parts: Parts,
622 _req_body: Body,
623 param: Value,
255f378a 624 _info: &ApiMethod,
9e47c0a5 625 _rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 626) -> ApiResponseFuture {
9e47c0a5 627
ad51d02a
DM
628 async move {
629 let store = tools::required_string_param(&param, "store")?;
f14a8c9a 630
ad51d02a 631 let datastore = DataStore::lookup_datastore(store)?;
f14a8c9a 632
ad51d02a 633 let file_name = tools::required_string_param(&param, "file-name")?.to_owned();
9e47c0a5 634
ad51d02a
DM
635 let backup_type = tools::required_string_param(&param, "backup-type")?;
636 let backup_id = tools::required_string_param(&param, "backup-id")?;
637 let backup_time = tools::required_integer_param(&param, "backup-time")?;
9e47c0a5 638
ad51d02a
DM
639 println!("Download {} from {} ({}/{}/{}/{})", file_name, store,
640 backup_type, backup_id, Local.timestamp(backup_time, 0), file_name);
9e47c0a5 641
ad51d02a 642 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
9e47c0a5 643
ad51d02a
DM
644 let mut path = datastore.base_path();
645 path.push(backup_dir.relative_path());
646 path.push(&file_name);
647
648 let file = tokio::fs::File::open(path)
649 .map_err(|err| http_err!(BAD_REQUEST, format!("File open failed: {}", err)))
650 .await?;
651
db0cb9ce
WB
652 let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
653 .map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze()));
ad51d02a 654 let body = Body::wrap_stream(payload);
9e47c0a5 655
ad51d02a
DM
656 // fixme: set other headers ?
657 Ok(Response::builder()
658 .status(StatusCode::OK)
659 .header(header::CONTENT_TYPE, "application/octet-stream")
660 .body(body)
661 .unwrap())
662 }.boxed()
9e47c0a5
DM
663}
664
552c2259 665#[sortable]
0ab08ac9
DM
666pub const API_METHOD_UPLOAD_BACKUP_LOG: ApiMethod = ApiMethod::new(
667 &ApiHandler::AsyncHttp(&upload_backup_log),
255f378a
DM
668 &ObjectSchema::new(
669 "Download single raw file from backup snapshot.",
552c2259 670 &sorted!([
66c49c21 671 ("store", false, &DATASTORE_SCHEMA),
255f378a 672 ("backup-type", false, &BACKUP_TYPE_SCHEMA),
0ab08ac9 673 ("backup-id", false, &BACKUP_ID_SCHEMA),
255f378a 674 ("backup-time", false, &BACKUP_TIME_SCHEMA),
552c2259 675 ]),
9e47c0a5 676 )
bb34b589 677).access(None, &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_ALLOCATE_SPACE, false));
9e47c0a5 678
07ee2235
DM
679fn upload_backup_log(
680 _parts: Parts,
681 req_body: Body,
682 param: Value,
255f378a 683 _info: &ApiMethod,
07ee2235 684 _rpcenv: Box<dyn RpcEnvironment>,
bb084b9c 685) -> ApiResponseFuture {
07ee2235 686
ad51d02a
DM
687 async move {
688 let store = tools::required_string_param(&param, "store")?;
07ee2235 689
ad51d02a 690 let datastore = DataStore::lookup_datastore(store)?;
07ee2235 691
ad51d02a 692 let file_name = "client.log.blob";
07ee2235 693
ad51d02a
DM
694 let backup_type = tools::required_string_param(&param, "backup-type")?;
695 let backup_id = tools::required_string_param(&param, "backup-id")?;
696 let backup_time = tools::required_integer_param(&param, "backup-time")?;
07ee2235 697
ad51d02a 698 let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
07ee2235 699
ad51d02a
DM
700 let mut path = datastore.base_path();
701 path.push(backup_dir.relative_path());
702 path.push(&file_name);
07ee2235 703
ad51d02a
DM
704 if path.exists() {
705 bail!("backup already contains a log.");
706 }
e128d4e8 707
ad51d02a
DM
708 println!("Upload backup log to {}/{}/{}/{}/{}", store,
709 backup_type, backup_id, BackupDir::backup_time_to_string(backup_dir.backup_time()), file_name);
710
711 let data = req_body
712 .map_err(Error::from)
713 .try_fold(Vec::new(), |mut acc, chunk| {
714 acc.extend_from_slice(&*chunk);
715 future::ok::<_, Error>(acc)
716 })
717 .await?;
718
719 let blob = DataBlob::from_raw(data)?;
720 // always verify CRC at server side
721 blob.verify_crc()?;
722 let raw_data = blob.raw_data();
feaa1ad3 723 replace_file(&path, raw_data, CreateOptions::new())?;
ad51d02a
DM
724
725 // fixme: use correct formatter
726 Ok(crate::server::formatter::json_response(Ok(Value::Null)))
727 }.boxed()
07ee2235
DM
728}
729
552c2259 730#[sortable]
255f378a
DM
731const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
732 (
733 "download",
734 &Router::new()
735 .download(&API_METHOD_DOWNLOAD_FILE)
736 ),
737 (
738 "files",
739 &Router::new()
09b1f7b2 740 .get(&API_METHOD_LIST_SNAPSHOT_FILES)
255f378a
DM
741 ),
742 (
743 "gc",
744 &Router::new()
745 .get(&API_METHOD_GARBAGE_COLLECTION_STATUS)
746 .post(&API_METHOD_START_GARBAGE_COLLECTION)
747 ),
748 (
749 "groups",
750 &Router::new()
b31c8019 751 .get(&API_METHOD_LIST_GROUPS)
255f378a
DM
752 ),
753 (
754 "prune",
755 &Router::new()
756 .post(&API_METHOD_PRUNE)
757 ),
758 (
759 "snapshots",
760 &Router::new()
fc189b19 761 .get(&API_METHOD_LIST_SNAPSHOTS)
68a6a0ee 762 .delete(&API_METHOD_DELETE_SNAPSHOT)
255f378a
DM
763 ),
764 (
765 "status",
766 &Router::new()
767 .get(&API_METHOD_STATUS)
768 ),
769 (
770 "upload-backup-log",
771 &Router::new()
772 .upload(&API_METHOD_UPLOAD_BACKUP_LOG)
773 ),
774];
775
ad51d02a 776const DATASTORE_INFO_ROUTER: Router = Router::new()
255f378a
DM
777 .get(&list_subdirs_api_method!(DATASTORE_INFO_SUBDIRS))
778 .subdirs(DATASTORE_INFO_SUBDIRS);
779
780
781pub const ROUTER: Router = Router::new()
bb34b589 782 .get(&API_METHOD_GET_DATASTORE_LIST)
255f378a 783 .match_all("store", &DATASTORE_INFO_ROUTER);