]>
Commit | Line | Data |
---|---|---|
c60d34bd DM |
1 | extern crate proxmox_backup; |
2 | ||
f7d4e4b5 | 3 | use anyhow::{format_err, Error}; |
c60d34bd | 4 | |
552c2259 | 5 | use proxmox::{sortable, identity}; |
cad540e9 WB |
6 | use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment}; |
7 | use proxmox::api::schema::*; | |
7eea56ca | 8 | use proxmox::api::cli::*; |
552c2259 | 9 | |
ce7ba139 | 10 | use proxmox_backup::tools; |
c60d34bd DM |
11 | |
12 | use serde_json::{Value}; | |
13 | ||
37940aa1 | 14 | use std::io::Write; |
1ef46b81 | 15 | use std::path::{Path, PathBuf}; |
af309d4d | 16 | use std::fs::OpenOptions; |
f71e8cc9 | 17 | use std::ffi::OsStr; |
af309d4d | 18 | use std::os::unix::fs::OpenOptionsExt; |
129dda47 | 19 | use std::os::unix::io::AsRawFd; |
2eeaacb9 | 20 | use std::collections::HashSet; |
c60d34bd | 21 | |
3dbfe5b1 | 22 | use proxmox_backup::pxar; |
e86c4924 | 23 | |
6b9a0710 DM |
24 | fn dump_archive_from_reader<R: std::io::Read>( |
25 | reader: &mut R, | |
26 | feature_flags: u64, | |
27 | verbose: bool, | |
28 | ) -> Result<(), Error> { | |
f701d033 | 29 | let mut decoder = pxar::SequentialDecoder::new(reader, feature_flags); |
c6c9e093 DM |
30 | |
31 | let stdout = std::io::stdout(); | |
32 | let mut out = stdout.lock(); | |
33 | ||
34 | let mut path = PathBuf::new(); | |
6b9a0710 | 35 | decoder.dump_entry(&mut path, verbose, &mut out)?; |
c6c9e093 DM |
36 | |
37 | Ok(()) | |
38 | } | |
39 | ||
6049b71f DM |
40 | fn dump_archive( |
41 | param: Value, | |
42 | _info: &ApiMethod, | |
dd5495d6 | 43 | _rpcenv: &mut dyn RpcEnvironment, |
6049b71f | 44 | ) -> Result<Value, Error> { |
c60d34bd | 45 | |
6049b71f | 46 | let archive = tools::required_string_param(¶m, "archive")?; |
6b9a0710 | 47 | let verbose = param["verbose"].as_bool().unwrap_or(false); |
c60d34bd | 48 | |
47651f95 | 49 | let feature_flags = pxar::flags::DEFAULT; |
c60d34bd | 50 | |
c6c9e093 DM |
51 | if archive == "-" { |
52 | let stdin = std::io::stdin(); | |
53 | let mut reader = stdin.lock(); | |
6b9a0710 | 54 | dump_archive_from_reader(&mut reader, feature_flags, verbose)?; |
c6c9e093 | 55 | } else { |
aad2ee49 | 56 | if verbose { println!("PXAR dump: {}", archive); } |
c6c9e093 DM |
57 | let file = std::fs::File::open(archive)?; |
58 | let mut reader = std::io::BufReader::new(file); | |
6b9a0710 | 59 | dump_archive_from_reader(&mut reader, feature_flags, verbose)?; |
9eae781a | 60 | } |
c60d34bd DM |
61 | |
62 | Ok(Value::Null) | |
63 | } | |
64 | ||
129dda47 CE |
65 | fn extract_archive_from_reader<R: std::io::Read>( |
66 | reader: &mut R, | |
67 | target: &str, | |
68 | feature_flags: u64, | |
6a879109 | 69 | allow_existing_dirs: bool, |
129dda47 | 70 | verbose: bool, |
4d142ea7 | 71 | pattern: Option<Vec<pxar::MatchPattern>> |
129dda47 | 72 | ) -> Result<(), Error> { |
f701d033 DM |
73 | let mut decoder = pxar::SequentialDecoder::new(reader, feature_flags); |
74 | decoder.set_callback(move |path| { | |
9eae781a DM |
75 | if verbose { |
76 | println!("{:?}", path); | |
77 | } | |
78 | Ok(()) | |
79 | }); | |
6a879109 | 80 | decoder.set_allow_existing_dirs(allow_existing_dirs); |
9eae781a | 81 | |
11377a47 | 82 | let pattern = pattern.unwrap_or_else(Vec::new); |
129dda47 | 83 | decoder.restore(Path::new(target), &pattern)?; |
9eae781a DM |
84 | |
85 | Ok(()) | |
86 | } | |
87 | ||
1ef46b81 DM |
88 | fn extract_archive( |
89 | param: Value, | |
90 | _info: &ApiMethod, | |
dd5495d6 | 91 | _rpcenv: &mut dyn RpcEnvironment, |
1ef46b81 DM |
92 | ) -> Result<Value, Error> { |
93 | ||
94 | let archive = tools::required_string_param(¶m, "archive")?; | |
bdf0d82c | 95 | let target = param["target"].as_str().unwrap_or("."); |
1ef46b81 | 96 | let verbose = param["verbose"].as_bool().unwrap_or(false); |
0d9bab05 CE |
97 | let no_xattrs = param["no-xattrs"].as_bool().unwrap_or(false); |
98 | let no_fcaps = param["no-fcaps"].as_bool().unwrap_or(false); | |
9b384433 | 99 | let no_acls = param["no-acls"].as_bool().unwrap_or(false); |
81a9905e CE |
100 | let no_device_nodes = param["no-device-nodes"].as_bool().unwrap_or(false); |
101 | let no_fifos = param["no-fifos"].as_bool().unwrap_or(false); | |
102 | let no_sockets = param["no-sockets"].as_bool().unwrap_or(false); | |
6a879109 | 103 | let allow_existing_dirs = param["allow-existing-dirs"].as_bool().unwrap_or(false); |
129dda47 | 104 | let files_from = param["files-from"].as_str(); |
a0ec687c CE |
105 | let empty = Vec::new(); |
106 | let arg_pattern = param["pattern"].as_array().unwrap_or(&empty); | |
1ef46b81 | 107 | |
47651f95 | 108 | let mut feature_flags = pxar::flags::DEFAULT; |
b344461b | 109 | if no_xattrs { |
47651f95 | 110 | feature_flags ^= pxar::flags::WITH_XATTRS; |
b344461b CE |
111 | } |
112 | if no_fcaps { | |
47651f95 | 113 | feature_flags ^= pxar::flags::WITH_FCAPS; |
b344461b | 114 | } |
9b384433 | 115 | if no_acls { |
47651f95 | 116 | feature_flags ^= pxar::flags::WITH_ACL; |
9b384433 | 117 | } |
81a9905e | 118 | if no_device_nodes { |
47651f95 | 119 | feature_flags ^= pxar::flags::WITH_DEVICE_NODES; |
81a9905e CE |
120 | } |
121 | if no_fifos { | |
47651f95 | 122 | feature_flags ^= pxar::flags::WITH_FIFOS; |
81a9905e CE |
123 | } |
124 | if no_sockets { | |
47651f95 | 125 | feature_flags ^= pxar::flags::WITH_SOCKETS; |
81a9905e | 126 | } |
1ef46b81 | 127 | |
a0ec687c CE |
128 | let mut pattern_list = Vec::new(); |
129 | if let Some(filename) = files_from { | |
130 | let dir = nix::dir::Dir::open("./", nix::fcntl::OFlag::O_RDONLY, nix::sys::stat::Mode::empty())?; | |
4d142ea7 | 131 | if let Some((mut pattern, _, _)) = pxar::MatchPattern::from_file(dir.as_raw_fd(), filename)? { |
a0ec687c CE |
132 | pattern_list.append(&mut pattern); |
133 | } | |
134 | } | |
129dda47 | 135 | |
a0ec687c CE |
136 | for s in arg_pattern { |
137 | let l = s.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?; | |
4d142ea7 | 138 | let p = pxar::MatchPattern::from_line(l.as_bytes())? |
a0ec687c CE |
139 | .ok_or_else(|| format_err!("Invalid match pattern in arguments"))?; |
140 | pattern_list.push(p); | |
141 | } | |
142 | ||
11377a47 | 143 | let pattern = if pattern_list.is_empty() { |
a0ec687c | 144 | None |
11377a47 DM |
145 | } else { |
146 | Some(pattern_list) | |
129dda47 CE |
147 | }; |
148 | ||
9eae781a DM |
149 | if archive == "-" { |
150 | let stdin = std::io::stdin(); | |
151 | let mut reader = stdin.lock(); | |
6a879109 | 152 | extract_archive_from_reader(&mut reader, target, feature_flags, allow_existing_dirs, verbose, pattern)?; |
9eae781a | 153 | } else { |
40a13369 | 154 | if verbose { println!("PXAR extract: {}", archive); } |
9eae781a DM |
155 | let file = std::fs::File::open(archive)?; |
156 | let mut reader = std::io::BufReader::new(file); | |
6a879109 | 157 | extract_archive_from_reader(&mut reader, target, feature_flags, allow_existing_dirs, verbose, pattern)?; |
9eae781a | 158 | } |
1ef46b81 DM |
159 | |
160 | Ok(Value::Null) | |
161 | } | |
162 | ||
6049b71f DM |
163 | fn create_archive( |
164 | param: Value, | |
165 | _info: &ApiMethod, | |
dd5495d6 | 166 | _rpcenv: &mut dyn RpcEnvironment, |
6049b71f | 167 | ) -> Result<Value, Error> { |
c60d34bd | 168 | |
6049b71f DM |
169 | let archive = tools::required_string_param(¶m, "archive")?; |
170 | let source = tools::required_string_param(¶m, "source")?; | |
2689810c | 171 | let verbose = param["verbose"].as_bool().unwrap_or(false); |
e3c30c50 | 172 | let all_file_systems = param["all-file-systems"].as_bool().unwrap_or(false); |
0d9bab05 CE |
173 | let no_xattrs = param["no-xattrs"].as_bool().unwrap_or(false); |
174 | let no_fcaps = param["no-fcaps"].as_bool().unwrap_or(false); | |
9b384433 | 175 | let no_acls = param["no-acls"].as_bool().unwrap_or(false); |
81a9905e CE |
176 | let no_device_nodes = param["no-device-nodes"].as_bool().unwrap_or(false); |
177 | let no_fifos = param["no-fifos"].as_bool().unwrap_or(false); | |
178 | let no_sockets = param["no-sockets"].as_bool().unwrap_or(false); | |
62d123e5 CE |
179 | let empty = Vec::new(); |
180 | let exclude_pattern = param["exclude"].as_array().unwrap_or(&empty); | |
6fc053ed | 181 | let entries_max = param["entries-max"].as_u64().unwrap_or(pxar::ENCODER_MAX_ENTRIES as u64); |
02c7d8e5 | 182 | |
2eeaacb9 DM |
183 | let devices = if all_file_systems { None } else { Some(HashSet::new()) }; |
184 | ||
1ef46b81 | 185 | let source = PathBuf::from(source); |
02c7d8e5 DM |
186 | |
187 | let mut dir = nix::dir::Dir::open( | |
188 | &source, nix::fcntl::OFlag::O_NOFOLLOW, nix::sys::stat::Mode::empty())?; | |
189 | ||
af309d4d | 190 | let file = OpenOptions::new() |
02c7d8e5 DM |
191 | .create_new(true) |
192 | .write(true) | |
af309d4d | 193 | .mode(0o640) |
02c7d8e5 DM |
194 | .open(archive)?; |
195 | ||
196 | let mut writer = std::io::BufWriter::with_capacity(1024*1024, file); | |
47651f95 | 197 | let mut feature_flags = pxar::flags::DEFAULT; |
b344461b | 198 | if no_xattrs { |
47651f95 | 199 | feature_flags ^= pxar::flags::WITH_XATTRS; |
b344461b CE |
200 | } |
201 | if no_fcaps { | |
47651f95 | 202 | feature_flags ^= pxar::flags::WITH_FCAPS; |
b344461b | 203 | } |
9b384433 | 204 | if no_acls { |
47651f95 | 205 | feature_flags ^= pxar::flags::WITH_ACL; |
9b384433 | 206 | } |
81a9905e | 207 | if no_device_nodes { |
47651f95 | 208 | feature_flags ^= pxar::flags::WITH_DEVICE_NODES; |
81a9905e CE |
209 | } |
210 | if no_fifos { | |
47651f95 | 211 | feature_flags ^= pxar::flags::WITH_FIFOS; |
81a9905e CE |
212 | } |
213 | if no_sockets { | |
47651f95 | 214 | feature_flags ^= pxar::flags::WITH_SOCKETS; |
81a9905e | 215 | } |
b344461b | 216 | |
62d123e5 CE |
217 | let mut pattern_list = Vec::new(); |
218 | for s in exclude_pattern { | |
219 | let l = s.as_str().ok_or_else(|| format_err!("Invalid pattern string slice"))?; | |
220 | let p = pxar::MatchPattern::from_line(l.as_bytes())? | |
221 | .ok_or_else(|| format_err!("Invalid match pattern in arguments"))?; | |
222 | pattern_list.push(p); | |
223 | } | |
224 | ||
9d135fe6 | 225 | let catalog = None::<&mut pxar::catalog::DummyCatalogWriter>; |
62d123e5 CE |
226 | pxar::Encoder::encode( |
227 | source, | |
228 | &mut dir, | |
229 | &mut writer, | |
230 | catalog, | |
231 | devices, | |
232 | verbose, | |
233 | false, | |
234 | feature_flags, | |
235 | pattern_list, | |
6fc053ed | 236 | entries_max as usize, |
62d123e5 | 237 | )?; |
c60d34bd | 238 | |
02c7d8e5 | 239 | writer.flush()?; |
c60d34bd DM |
240 | |
241 | Ok(Value::Null) | |
242 | } | |
243 | ||
f71e8cc9 CE |
244 | /// Mount the archive to the provided mountpoint via FUSE. |
245 | fn mount_archive( | |
246 | param: Value, | |
247 | _info: &ApiMethod, | |
248 | _rpcenv: &mut dyn RpcEnvironment, | |
249 | ) -> Result<Value, Error> { | |
250 | let archive = tools::required_string_param(¶m, "archive")?; | |
251 | let mountpoint = tools::required_string_param(¶m, "mountpoint")?; | |
252 | let verbose = param["verbose"].as_bool().unwrap_or(false); | |
bcb664cb | 253 | let no_mt = param["no-mt"].as_bool().unwrap_or(false); |
f71e8cc9 CE |
254 | |
255 | let archive = Path::new(archive); | |
256 | let mountpoint = Path::new(mountpoint); | |
257 | let options = OsStr::new("ro,default_permissions"); | |
2a111910 | 258 | let mut session = pxar::fuse::Session::from_path(&archive, &options, verbose) |
f71e8cc9 | 259 | .map_err(|err| format_err!("pxar mount failed: {}", err))?; |
2fa91f52 CE |
260 | // Mount the session and deamonize if verbose is not set |
261 | session.mount(&mountpoint, !verbose)?; | |
bcb664cb | 262 | session.run_loop(!no_mt)?; |
f71e8cc9 CE |
263 | |
264 | Ok(Value::Null) | |
265 | } | |
266 | ||
552c2259 | 267 | #[sortable] |
255f378a DM |
268 | const API_METHOD_CREATE_ARCHIVE: ApiMethod = ApiMethod::new( |
269 | &ApiHandler::Sync(&create_archive), | |
270 | &ObjectSchema::new( | |
271 | "Create new .pxar archive.", | |
552c2259 | 272 | &sorted!([ |
255f378a DM |
273 | ( |
274 | "archive", | |
275 | false, | |
276 | &StringSchema::new("Archive name").schema() | |
277 | ), | |
278 | ( | |
279 | "source", | |
280 | false, | |
281 | &StringSchema::new("Source directory.").schema() | |
282 | ), | |
283 | ( | |
284 | "verbose", | |
285 | true, | |
286 | &BooleanSchema::new("Verbose output.") | |
287 | .default(false) | |
288 | .schema() | |
289 | ), | |
290 | ( | |
291 | "no-xattrs", | |
292 | true, | |
293 | &BooleanSchema::new("Ignore extended file attributes.") | |
294 | .default(false) | |
295 | .schema() | |
296 | ), | |
297 | ( | |
298 | "no-fcaps", | |
299 | true, | |
300 | &BooleanSchema::new("Ignore file capabilities.") | |
301 | .default(false) | |
302 | .schema() | |
303 | ), | |
304 | ( | |
305 | "no-acls", | |
306 | true, | |
307 | &BooleanSchema::new("Ignore access control list entries.") | |
308 | .default(false) | |
309 | .schema() | |
310 | ), | |
311 | ( | |
312 | "all-file-systems", | |
313 | true, | |
314 | &BooleanSchema::new("Include mounted sudirs.") | |
315 | .default(false) | |
316 | .schema() | |
317 | ), | |
318 | ( | |
319 | "no-device-nodes", | |
320 | true, | |
321 | &BooleanSchema::new("Ignore device nodes.") | |
322 | .default(false) | |
323 | .schema() | |
324 | ), | |
325 | ( | |
326 | "no-fifos", | |
327 | true, | |
328 | &BooleanSchema::new("Ignore fifos.") | |
329 | .default(false) | |
330 | .schema() | |
331 | ), | |
332 | ( | |
333 | "no-sockets", | |
334 | true, | |
335 | &BooleanSchema::new("Ignore sockets.") | |
336 | .default(false) | |
337 | .schema() | |
338 | ), | |
339 | ( | |
340 | "exclude", | |
341 | true, | |
342 | &ArraySchema::new( | |
343 | "List of paths or pattern matching files to exclude.", | |
344 | &StringSchema::new("Path or pattern matching files to restore.").schema() | |
345 | ).schema() | |
346 | ), | |
6fc053ed CE |
347 | ( |
348 | "entries-max", | |
349 | true, | |
350 | &IntegerSchema::new("Max number of entries loaded at once into memory") | |
351 | .default(pxar::ENCODER_MAX_ENTRIES as isize) | |
352 | .minimum(0) | |
353 | .maximum(std::isize::MAX) | |
354 | .schema() | |
355 | ), | |
552c2259 | 356 | ]), |
255f378a DM |
357 | ) |
358 | ); | |
359 | ||
552c2259 | 360 | #[sortable] |
255f378a DM |
361 | const API_METHOD_EXTRACT_ARCHIVE: ApiMethod = ApiMethod::new( |
362 | &ApiHandler::Sync(&extract_archive), | |
363 | &ObjectSchema::new( | |
364 | "Extract an archive.", | |
552c2259 | 365 | &sorted!([ |
255f378a DM |
366 | ( |
367 | "archive", | |
368 | false, | |
369 | &StringSchema::new("Archive name.").schema() | |
370 | ), | |
371 | ( | |
372 | "pattern", | |
373 | true, | |
374 | &ArraySchema::new( | |
375 | "List of paths or pattern matching files to restore", | |
376 | &StringSchema::new("Path or pattern matching files to restore.").schema() | |
377 | ).schema() | |
378 | ), | |
379 | ( | |
380 | "target", | |
381 | true, | |
382 | &StringSchema::new("Target directory.").schema() | |
383 | ), | |
384 | ( | |
385 | "verbose", | |
386 | true, | |
387 | &BooleanSchema::new("Verbose output.") | |
388 | .default(false) | |
389 | .schema() | |
390 | ), | |
391 | ( | |
392 | "no-xattrs", | |
393 | true, | |
394 | &BooleanSchema::new("Ignore extended file attributes.") | |
395 | .default(false) | |
396 | .schema() | |
397 | ), | |
398 | ( | |
399 | "no-fcaps", | |
400 | true, | |
401 | &BooleanSchema::new("Ignore file capabilities.") | |
402 | .default(false) | |
403 | .schema() | |
404 | ), | |
405 | ( | |
406 | "no-acls", | |
407 | true, | |
408 | &BooleanSchema::new("Ignore access control list entries.") | |
409 | .default(false) | |
410 | .schema() | |
411 | ), | |
412 | ( | |
413 | "allow-existing-dirs", | |
414 | true, | |
415 | &BooleanSchema::new("Allows directories to already exist on restore.") | |
416 | .default(false) | |
417 | .schema() | |
418 | ), | |
419 | ( | |
420 | "files-from", | |
421 | true, | |
422 | &StringSchema::new("Match pattern for files to restore.").schema() | |
423 | ), | |
424 | ( | |
425 | "no-device-nodes", | |
426 | true, | |
427 | &BooleanSchema::new("Ignore device nodes.") | |
428 | .default(false) | |
429 | .schema() | |
430 | ), | |
431 | ( | |
432 | "no-fifos", | |
433 | true, | |
434 | &BooleanSchema::new("Ignore fifos.") | |
435 | .default(false) | |
436 | .schema() | |
437 | ), | |
438 | ( | |
439 | "no-sockets", | |
440 | true, | |
441 | &BooleanSchema::new("Ignore sockets.") | |
442 | .default(false) | |
443 | .schema() | |
444 | ), | |
552c2259 | 445 | ]), |
255f378a DM |
446 | ) |
447 | ); | |
448 | ||
552c2259 | 449 | #[sortable] |
255f378a DM |
450 | const API_METHOD_MOUNT_ARCHIVE: ApiMethod = ApiMethod::new( |
451 | &ApiHandler::Sync(&mount_archive), | |
452 | &ObjectSchema::new( | |
453 | "Mount the archive as filesystem via FUSE.", | |
552c2259 | 454 | &sorted!([ |
255f378a DM |
455 | ( |
456 | "archive", | |
457 | false, | |
458 | &StringSchema::new("Archive name.").schema() | |
459 | ), | |
460 | ( | |
461 | "mountpoint", | |
462 | false, | |
463 | &StringSchema::new("Mountpoint for the filesystem root.").schema() | |
464 | ), | |
465 | ( | |
466 | "verbose", | |
467 | true, | |
468 | &BooleanSchema::new("Verbose output, keeps process running in foreground (for debugging).") | |
469 | .default(false) | |
470 | .schema() | |
471 | ), | |
472 | ( | |
473 | "no-mt", | |
474 | true, | |
475 | &BooleanSchema::new("Run in single threaded mode (for debugging).") | |
476 | .default(false) | |
477 | .schema() | |
478 | ), | |
552c2259 | 479 | ]), |
255f378a DM |
480 | ) |
481 | ); | |
482 | ||
552c2259 | 483 | #[sortable] |
255f378a DM |
484 | const API_METHOD_DUMP_ARCHIVE: ApiMethod = ApiMethod::new( |
485 | &ApiHandler::Sync(&dump_archive), | |
486 | &ObjectSchema::new( | |
487 | "List the contents of an archive.", | |
552c2259 | 488 | &sorted!([ |
255f378a DM |
489 | ( "archive", false, &StringSchema::new("Archive name.").schema()), |
490 | ( "verbose", true, &BooleanSchema::new("Verbose output.") | |
491 | .default(false) | |
492 | .schema() | |
493 | ), | |
552c2259 | 494 | ]) |
255f378a DM |
495 | ) |
496 | ); | |
497 | ||
c60d34bd DM |
498 | fn main() { |
499 | ||
500 | let cmd_def = CliCommandMap::new() | |
255f378a | 501 | .insert("create", CliCommand::new(&API_METHOD_CREATE_ARCHIVE) |
7e3d2e5b | 502 | .arg_param(&["archive", "source"]) |
ce7ba139 DM |
503 | .completion_cb("archive", tools::complete_file_name) |
504 | .completion_cb("source", tools::complete_file_name) | |
c60d34bd | 505 | ) |
255f378a | 506 | .insert("extract", CliCommand::new(&API_METHOD_EXTRACT_ARCHIVE) |
9a328319 | 507 | .arg_param(&["archive", "target"]) |
1ef46b81 DM |
508 | .completion_cb("archive", tools::complete_file_name) |
509 | .completion_cb("target", tools::complete_file_name) | |
129dda47 | 510 | .completion_cb("files-from", tools::complete_file_name) |
48ef3c33 | 511 | ) |
255f378a | 512 | .insert("mount", CliCommand::new(&API_METHOD_MOUNT_ARCHIVE) |
49fddd98 | 513 | .arg_param(&["archive", "mountpoint"]) |
f71e8cc9 CE |
514 | .completion_cb("archive", tools::complete_file_name) |
515 | .completion_cb("mountpoint", tools::complete_file_name) | |
f71e8cc9 | 516 | ) |
255f378a | 517 | .insert("list", CliCommand::new(&API_METHOD_DUMP_ARCHIVE) |
49fddd98 | 518 | .arg_param(&["archive"]) |
ce7ba139 | 519 | .completion_cb("archive", tools::complete_file_name) |
c60d34bd DM |
520 | ); |
521 | ||
7b22acd0 DM |
522 | let rpcenv = CliEnvironment::new(); |
523 | run_cli_command(cmd_def, rpcenv, None); | |
c60d34bd | 524 | } |