main.rs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2025 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use clap::{Parser, Subcommand};
  19. use log::error;
  20. use simplelog::{ColorChoice, TermLogger, TerminalMode};
  21. use smol::lock::RwLock;
  22. use std::{
  23. collections::HashMap,
  24. io::{stdout, Write},
  25. sync::Arc,
  26. };
  27. use termcolor::{StandardStream, WriteColor};
  28. use url::Url;
  29. use darkfi::{
  30. cli_desc,
  31. rpc::{
  32. client::RpcClient,
  33. jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResult},
  34. util::JsonValue,
  35. },
  36. system::{ExecutorPtr, Publisher, StoppableTask},
  37. util::cli::{get_log_config, get_log_level},
  38. Error, Result,
  39. };
  40. use fud::{
  41. resource::{Resource, ResourceStatus, ResourceType},
  42. util::hash_to_string,
  43. };
  44. mod util;
  45. use crate::util::{
  46. format_bytes, format_duration, format_progress_bytes, optional_value, print_tree,
  47. status_to_colorspec, type_to_colorspec, TreeNode,
  48. };
  49. #[derive(Parser)]
  50. #[clap(name = "fu", about = cli_desc!(), version)]
  51. #[clap(arg_required_else_help(true))]
  52. struct Args {
  53. #[clap(short, action = clap::ArgAction::Count)]
  54. /// Increase verbosity (-vvv supported)
  55. verbose: u8,
  56. #[clap(short, long, default_value = "tcp://127.0.0.1:13336")]
  57. /// fud JSON-RPC endpoint
  58. endpoint: Url,
  59. #[clap(subcommand)]
  60. command: Subcmd,
  61. }
  62. #[derive(Subcommand)]
  63. enum Subcmd {
  64. /// Retrieve provided resource from the fud network
  65. Get {
  66. /// Resource hash
  67. hash: String,
  68. /// Download path (relative or absolute)
  69. path: Option<String>,
  70. /// Optional list of files you want to download (only used for directories)
  71. #[arg(short, long, num_args = 1..)]
  72. files: Option<Vec<String>>,
  73. },
  74. /// Put a file or directory onto the fud network
  75. Put {
  76. /// File path or directory path
  77. path: String,
  78. },
  79. /// List resources
  80. Ls {},
  81. /// Watch
  82. Watch {},
  83. /// Remove a resource from fud
  84. Rm {
  85. /// Resource hash
  86. hash: String,
  87. },
  88. /// Get the current node buckets
  89. ListBuckets {},
  90. /// Get the router state
  91. ListSeeders {},
  92. /// Verify local files
  93. Verify {
  94. /// File hashes
  95. files: Option<Vec<String>>,
  96. },
  97. }
  98. struct Fu {
  99. pub rpc_client: Arc<RpcClient>,
  100. pub endpoint: Url,
  101. }
  102. impl Fu {
  103. async fn get(
  104. &self,
  105. hash: String,
  106. path: Option<String>,
  107. files: Option<Vec<String>>,
  108. ex: ExecutorPtr,
  109. ) -> Result<()> {
  110. let publisher = Publisher::new();
  111. let subscription = Arc::new(publisher.clone().subscribe().await);
  112. let subscriber_task = StoppableTask::new();
  113. let hash_ = hash.clone();
  114. let publisher_ = publisher.clone();
  115. let rpc_client_ = self.rpc_client.clone();
  116. subscriber_task.clone().start(
  117. async move {
  118. let req = JsonRequest::new("subscribe", JsonValue::Array(vec![]));
  119. rpc_client_.subscribe(req, publisher).await
  120. },
  121. move |res| async move {
  122. match res {
  123. Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
  124. Err(e) => {
  125. error!("{e}");
  126. publisher_
  127. .notify(JsonResult::Error(JsonError::new(
  128. ErrorCode::InternalError,
  129. None,
  130. 0,
  131. )))
  132. .await;
  133. }
  134. }
  135. },
  136. Error::DetachedTaskStopped,
  137. ex.clone(),
  138. );
  139. let progress_bar_width = 20;
  140. let mut started = false;
  141. let mut tstdout = StandardStream::stdout(ColorChoice::Auto);
  142. let mut print_progress = |info: &HashMap<String, JsonValue>| {
  143. started = true;
  144. let rs: Resource = info.get("resource").unwrap().clone().into();
  145. print!("\x1B[2K\r"); // Clear current line
  146. // Progress bar
  147. let percent = if rs.target_bytes_downloaded > rs.target_bytes_size {
  148. 1.0
  149. } else if rs.target_bytes_size > 0 {
  150. rs.target_bytes_downloaded as f64 / rs.target_bytes_size as f64
  151. } else {
  152. 0.0
  153. };
  154. let completed = (percent * progress_bar_width as f64) as usize;
  155. let remaining = match progress_bar_width > completed {
  156. true => progress_bar_width - completed,
  157. false => 0,
  158. };
  159. let bar = "=".repeat(completed) + &" ".repeat(remaining);
  160. print!("[{bar}] {:.1}% | ", percent * 100.0);
  161. // Downloaded / Total (in bytes)
  162. if rs.target_bytes_size > 0 {
  163. if rs.target_bytes_downloaded == rs.target_bytes_size {
  164. print!("{} | ", format_bytes(rs.target_bytes_size));
  165. } else {
  166. print!(
  167. "{} | ",
  168. format_progress_bytes(rs.target_bytes_downloaded, rs.target_bytes_size)
  169. );
  170. }
  171. }
  172. // Download speed (in bytes/sec)
  173. if !rs.speeds.is_empty() && rs.target_chunks_downloaded < rs.target_chunks_count {
  174. print!("{}/s | ", format_bytes(*rs.speeds.last().unwrap() as u64));
  175. }
  176. // Downloaded / Total (in chunks)
  177. if rs.target_chunks_count > 0 {
  178. let s = if rs.target_chunks_count > 1 { "s" } else { "" };
  179. if rs.target_chunks_downloaded == rs.target_chunks_count {
  180. print!("{} chunk{s} | ", rs.target_chunks_count);
  181. } else {
  182. print!(
  183. "{}/{} chunk{s} | ",
  184. rs.target_chunks_downloaded, rs.target_chunks_count
  185. );
  186. }
  187. }
  188. // ETA
  189. if !rs.speeds.is_empty() && rs.target_chunks_downloaded < rs.target_chunks_count {
  190. print!("ETA: {} | ", format_duration(rs.get_eta()));
  191. }
  192. // Status
  193. let is_done = rs.target_chunks_downloaded == rs.target_chunks_count &&
  194. rs.status.as_str() == "incomplete";
  195. let status = if is_done { ResourceStatus::Seeding } else { rs.status };
  196. tstdout.set_color(&status_to_colorspec(&status)).unwrap();
  197. print!(
  198. "{}",
  199. if let ResourceStatus::Seeding = status { "done" } else { status.as_str() }
  200. );
  201. tstdout.reset().unwrap();
  202. stdout().flush().unwrap();
  203. };
  204. let req = JsonRequest::new(
  205. "get",
  206. JsonValue::Array(vec![
  207. JsonValue::String(hash_.clone()),
  208. JsonValue::String(path.unwrap_or_default()),
  209. match files {
  210. Some(files) => {
  211. JsonValue::Array(files.into_iter().map(JsonValue::String).collect())
  212. }
  213. None => JsonValue::Null,
  214. },
  215. ]),
  216. );
  217. // Create a RPC client to send the `get` request
  218. let rpc_client_getter = RpcClient::new(self.endpoint.clone(), ex.clone()).await?;
  219. let _ = rpc_client_getter.request(req).await?;
  220. loop {
  221. match subscription.receive().await {
  222. JsonResult::Notification(n) => {
  223. let params = n.params.get::<HashMap<String, JsonValue>>().unwrap();
  224. let info =
  225. params.get("info").unwrap().get::<HashMap<String, JsonValue>>().unwrap();
  226. let hash = match info.get("hash") {
  227. Some(hash_value) => hash_value.get::<String>().unwrap(),
  228. None => continue,
  229. };
  230. if *hash != hash_ {
  231. continue;
  232. }
  233. match params.get("event").unwrap().get::<String>().unwrap().as_str() {
  234. "download_started" |
  235. "metadata_download_completed" |
  236. "chunk_download_completed" |
  237. "resource_updated" => {
  238. print_progress(info);
  239. }
  240. "download_completed" => {
  241. let resource_json = info
  242. .get("resource")
  243. .unwrap()
  244. .get::<HashMap<String, JsonValue>>()
  245. .unwrap();
  246. let path = resource_json.get("path").unwrap().get::<String>().unwrap();
  247. print_progress(info);
  248. println!("\nDownload completed:\n{path}");
  249. return Ok(());
  250. }
  251. "metadata_not_found" => {
  252. println!();
  253. return Err(Error::Custom(format!("Could not find {hash}")));
  254. }
  255. "chunk_not_found" => {
  256. // A seeder does not have a chunk we are looking for,
  257. // we will try another seeder so there is nothing to do
  258. }
  259. "missing_chunks" => {
  260. // We tried all seeders and some chunks are still missing
  261. println!();
  262. return Err(Error::Custom("Missing chunks".to_string()));
  263. }
  264. "download_error" => {
  265. // An error that caused the download to be unsuccessful
  266. if started {
  267. println!();
  268. }
  269. return Err(Error::Custom(
  270. info.get("error").unwrap().get::<String>().unwrap().to_string(),
  271. ));
  272. }
  273. _ => {}
  274. }
  275. }
  276. JsonResult::Error(e) => {
  277. return Err(Error::UnexpectedJsonRpc(format!("Got error from JSON-RPC: {e:?}")))
  278. }
  279. x => {
  280. return Err(Error::UnexpectedJsonRpc(format!(
  281. "Got unexpected data from JSON-RPC: {x:?}"
  282. )))
  283. }
  284. }
  285. }
  286. }
  287. async fn put(&self, path: String, ex: ExecutorPtr) -> Result<()> {
  288. let publisher = Publisher::new();
  289. let subscription = Arc::new(publisher.clone().subscribe().await);
  290. let subscriber_task = StoppableTask::new();
  291. let publisher_ = publisher.clone();
  292. let rpc_client_ = self.rpc_client.clone();
  293. subscriber_task.clone().start(
  294. async move {
  295. let req = JsonRequest::new("subscribe", JsonValue::Array(vec![]));
  296. rpc_client_.subscribe(req, publisher).await
  297. },
  298. move |res| async move {
  299. match res {
  300. Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
  301. Err(e) => {
  302. error!("{e}");
  303. publisher_
  304. .notify(JsonResult::Error(JsonError::new(
  305. ErrorCode::InternalError,
  306. None,
  307. 0,
  308. )))
  309. .await;
  310. }
  311. }
  312. },
  313. Error::DetachedTaskStopped,
  314. ex.clone(),
  315. );
  316. let rpc_client_putter = RpcClient::new(self.endpoint.clone(), ex.clone()).await?;
  317. let req = JsonRequest::new("put", JsonValue::Array(vec![JsonValue::String(path)]));
  318. let rep = rpc_client_putter.request(req).await?;
  319. let path_str = rep.get::<String>().unwrap().clone();
  320. loop {
  321. match subscription.receive().await {
  322. JsonResult::Notification(n) => {
  323. let params = n.params.get::<HashMap<String, JsonValue>>().unwrap();
  324. let info =
  325. params.get("info").unwrap().get::<HashMap<String, JsonValue>>().unwrap();
  326. let path = match info.get("path") {
  327. Some(path) => path.get::<String>().unwrap(),
  328. None => continue,
  329. };
  330. if *path != path_str {
  331. continue;
  332. }
  333. match params.get("event").unwrap().get::<String>().unwrap().as_str() {
  334. "insert_completed" => {
  335. let id = info.get("hash").unwrap().get::<String>().unwrap().to_string();
  336. println!("{id}");
  337. break Ok(())
  338. }
  339. "insert_error" => {
  340. return Err(Error::Custom(
  341. info.get("error").unwrap().get::<String>().unwrap().to_string(),
  342. ));
  343. }
  344. _ => {}
  345. }
  346. }
  347. JsonResult::Error(e) => {
  348. return Err(Error::UnexpectedJsonRpc(format!("Got error from JSON-RPC: {e:?}")))
  349. }
  350. x => {
  351. return Err(Error::UnexpectedJsonRpc(format!(
  352. "Got unexpected data from JSON-RPC: {x:?}"
  353. )))
  354. }
  355. }
  356. }
  357. }
  358. async fn list_resources(&self) -> Result<()> {
  359. let req = JsonRequest::new("list_resources", JsonValue::Array(vec![]));
  360. let rep = self.rpc_client.request(req).await?;
  361. let resources_json: Vec<JsonValue> = rep.clone().try_into().unwrap();
  362. let resources: Vec<Resource> = resources_json.into_iter().map(|v| v.into()).collect();
  363. for resource in resources.iter() {
  364. let tree: Vec<TreeNode<&str>> = vec![
  365. TreeNode::kv("ID", hash_to_string(&resource.hash)),
  366. TreeNode::kvc(
  367. "Type",
  368. resource.rtype.as_str().to_string(),
  369. type_to_colorspec(&resource.rtype),
  370. ),
  371. TreeNode::kvc(
  372. "Status",
  373. resource.status.as_str().to_string(),
  374. status_to_colorspec(&resource.status),
  375. ),
  376. TreeNode::kv("Chunks", {
  377. if let ResourceType::Directory = resource.rtype {
  378. format!(
  379. "{}/{} ({}/{})",
  380. resource.total_chunks_downloaded,
  381. optional_value!(resource.total_chunks_count),
  382. resource.target_chunks_downloaded,
  383. optional_value!(resource.target_chunks_count)
  384. )
  385. } else {
  386. format!(
  387. "{}/{}",
  388. resource.total_chunks_downloaded,
  389. optional_value!(resource.total_chunks_count)
  390. )
  391. }
  392. }),
  393. TreeNode::kv("Bytes", {
  394. if let ResourceType::Directory = resource.rtype {
  395. format!(
  396. "{} ({})",
  397. optional_value!(resource.total_bytes_size, |x: u64| {
  398. format_progress_bytes(resource.total_bytes_downloaded, x)
  399. }),
  400. optional_value!(resource.target_bytes_size, |x: u64| {
  401. format_progress_bytes(resource.target_bytes_downloaded, x)
  402. })
  403. )
  404. } else {
  405. optional_value!(resource.total_bytes_size, |x: u64| format_progress_bytes(
  406. resource.total_bytes_downloaded,
  407. x
  408. ))
  409. }
  410. }),
  411. ];
  412. print_tree(&resource.path.to_string_lossy(), &tree);
  413. }
  414. Ok(())
  415. }
  416. async fn list_buckets(&self) -> Result<()> {
  417. let req = JsonRequest::new("list_buckets", JsonValue::Array(vec![]));
  418. let rep = self.rpc_client.request(req).await?;
  419. let buckets: Vec<JsonValue> = rep.try_into().unwrap();
  420. let mut empty = true;
  421. for (bucket_i, bucket) in buckets.into_iter().enumerate() {
  422. let nodes: Vec<JsonValue> = bucket.try_into().unwrap();
  423. if nodes.is_empty() {
  424. continue
  425. }
  426. empty = false;
  427. let tree: Vec<TreeNode<String>> = nodes
  428. .into_iter()
  429. .map(|n| {
  430. let node: Vec<JsonValue> = n.try_into().unwrap();
  431. let node_id: JsonValue = node[0].clone();
  432. let addresses: Vec<JsonValue> = node[1].clone().try_into().unwrap();
  433. let addresses_vec: Vec<String> = addresses
  434. .into_iter()
  435. .map(|addr| TryInto::<String>::try_into(addr).unwrap())
  436. .collect();
  437. let node_id_string: String = node_id.try_into().unwrap();
  438. TreeNode {
  439. key: node_id_string,
  440. value: None,
  441. color: None,
  442. children: addresses_vec
  443. .into_iter()
  444. .map(|addr| TreeNode::key(addr.clone()))
  445. .collect(),
  446. }
  447. })
  448. .collect();
  449. print_tree(format!("Bucket {bucket_i}").as_str(), &tree);
  450. }
  451. if empty {
  452. println!("All buckets are empty");
  453. }
  454. Ok(())
  455. }
  456. async fn list_seeders(&self) -> Result<()> {
  457. let req = JsonRequest::new("list_seeders", JsonValue::Array(vec![]));
  458. let rep = self.rpc_client.request(req).await?;
  459. let resources: HashMap<String, JsonValue> = rep["seeders"].clone().try_into().unwrap();
  460. if resources.is_empty() {
  461. println!("No known seeders");
  462. return Ok(())
  463. }
  464. for (hash, nodes) in resources {
  465. let nodes: Vec<JsonValue> = nodes.try_into().unwrap();
  466. let tree: Vec<TreeNode<String>> = nodes
  467. .into_iter()
  468. .map(|n| {
  469. let node: Vec<JsonValue> = n.try_into().unwrap();
  470. let node_id: JsonValue = node[0].clone();
  471. let addresses: Vec<JsonValue> = node[1].clone().try_into().unwrap();
  472. let addresses_vec: Vec<String> = addresses
  473. .into_iter()
  474. .map(|addr| TryInto::<String>::try_into(addr).unwrap())
  475. .collect();
  476. let node_id_string: String = node_id.try_into().unwrap();
  477. TreeNode {
  478. key: node_id_string,
  479. value: None,
  480. color: None,
  481. children: addresses_vec
  482. .into_iter()
  483. .map(|addr| TreeNode::key(addr.clone()))
  484. .collect(),
  485. }
  486. })
  487. .collect();
  488. print_tree(&hash, &tree);
  489. }
  490. Ok(())
  491. }
  492. async fn watch(&self, ex: ExecutorPtr) -> Result<()> {
  493. let req = JsonRequest::new("list_resources", JsonValue::Array(vec![]));
  494. let rep = self.rpc_client.request(req).await?;
  495. let resources_json: Vec<JsonValue> = rep.clone().try_into().unwrap();
  496. let resources: Arc<RwLock<Vec<Resource>>> = Arc::new(RwLock::new(vec![]));
  497. let publisher = Publisher::new();
  498. let subscription = Arc::new(publisher.clone().subscribe().await);
  499. let subscriber_task = StoppableTask::new();
  500. let publisher_ = publisher.clone();
  501. let rpc_client_ = self.rpc_client.clone();
  502. subscriber_task.clone().start(
  503. async move {
  504. let req = JsonRequest::new("subscribe", JsonValue::Array(vec![]));
  505. rpc_client_.subscribe(req, publisher).await
  506. },
  507. move |res| async move {
  508. match res {
  509. Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
  510. Err(e) => {
  511. error!("{e}");
  512. publisher_
  513. .notify(JsonResult::Error(JsonError::new(
  514. ErrorCode::InternalError,
  515. None,
  516. 0,
  517. )))
  518. .await;
  519. }
  520. }
  521. },
  522. Error::DetachedTaskStopped,
  523. ex,
  524. );
  525. let mut tstdout = StandardStream::stdout(ColorChoice::Auto);
  526. let mut update_resource = async |resource: &Resource| {
  527. let mut resources_write = resources.write().await;
  528. let i = match resources_write.iter().position(|r| r.hash == resource.hash) {
  529. Some(i) => {
  530. resources_write.remove(i);
  531. resources_write.insert(i, resource.clone());
  532. i
  533. }
  534. None => {
  535. resources_write.push(resource.clone());
  536. resources_write.len() - 1
  537. }
  538. };
  539. // Move the cursor to the i-th line and clear it
  540. print!("\x1b[{};1H\x1B[2K", i + 2);
  541. // Hash
  542. print!("\r{:>44} ", hash_to_string(&resource.hash));
  543. // Type
  544. tstdout.set_color(&type_to_colorspec(&resource.rtype)).unwrap();
  545. print!(
  546. "{:>4} ",
  547. match resource.rtype.as_str() {
  548. "unknown" => "?",
  549. "directory" => "dir",
  550. _ => resource.rtype.as_str(),
  551. }
  552. );
  553. tstdout.reset().unwrap();
  554. // Status
  555. tstdout.set_color(&status_to_colorspec(&resource.status)).unwrap();
  556. print!("{:>11} ", resource.status.as_str());
  557. tstdout.reset().unwrap();
  558. // Downloaded / Total (in bytes)
  559. match resource.total_bytes_size {
  560. 0 => {
  561. print!("{:>5.1} {:>16} ", 0.0, "?");
  562. }
  563. _ => {
  564. let percent = resource.total_bytes_downloaded as f64 /
  565. resource.total_bytes_size as f64 *
  566. 100.0;
  567. if resource.total_bytes_downloaded == resource.total_bytes_size {
  568. print!("{:>5.1} {:>16} ", percent, format_bytes(resource.total_bytes_size));
  569. } else {
  570. print!(
  571. "{:>5.1} {:>16} ",
  572. percent,
  573. format_progress_bytes(
  574. resource.total_bytes_downloaded,
  575. resource.total_bytes_size
  576. )
  577. );
  578. }
  579. }
  580. };
  581. // Downloaded / Total (in chunks)
  582. match resource.total_chunks_count {
  583. 0 => {
  584. print!("{:>9} ", format!("{}/?", resource.total_chunks_downloaded));
  585. }
  586. _ => {
  587. if resource.total_chunks_downloaded == resource.total_chunks_count {
  588. print!("{:>9} ", resource.total_chunks_count.to_string());
  589. } else {
  590. print!(
  591. "{:>9} ",
  592. format!(
  593. "{}/{}",
  594. resource.total_chunks_downloaded, resource.total_chunks_count
  595. )
  596. );
  597. }
  598. }
  599. };
  600. // Download speed (in bytes/sec)
  601. let speed_available = resource.total_bytes_downloaded < resource.total_bytes_size &&
  602. resource.status.as_str() == "downloading" &&
  603. !resource.speeds.is_empty();
  604. print!(
  605. "{:>12} ",
  606. match speed_available {
  607. false => "-".to_string(),
  608. true => format!("{}/s", format_bytes(*resource.speeds.last().unwrap() as u64)),
  609. }
  610. );
  611. // ETA
  612. let eta = resource.get_eta();
  613. print!(
  614. "{:>6}",
  615. match eta {
  616. 0 => "-".to_string(),
  617. _ => format_duration(eta),
  618. }
  619. );
  620. println!();
  621. // Move the cursor to end
  622. print!("\x1b[{};1H", resources_write.len() + 2);
  623. stdout().flush().unwrap();
  624. };
  625. let print_begin = async || {
  626. // Clear
  627. print!("\x1B[2J\x1B[1;1H");
  628. // Print column headers
  629. println!(
  630. "\x1b[4m{:>44} {:>4} {:>11} {:>5} {:>16} {:>9} {:>12} {:>6}\x1b[0m",
  631. "Hash", "Type", "Status", "%", "Bytes", "Chunks", "Speed", "ETA"
  632. );
  633. };
  634. print_begin().await;
  635. if resources_json.is_empty() {
  636. println!("No known resources");
  637. } else {
  638. for resource in resources_json.iter() {
  639. let rs: Resource = resource.clone().into();
  640. update_resource(&rs).await;
  641. }
  642. }
  643. loop {
  644. match subscription.receive().await {
  645. JsonResult::Notification(n) => {
  646. let params = n.params.get::<HashMap<String, JsonValue>>().unwrap();
  647. let info =
  648. params.get("info").unwrap().get::<HashMap<String, JsonValue>>().unwrap();
  649. match params.get("event").unwrap().get::<String>().unwrap().as_str() {
  650. "download_started" |
  651. "metadata_download_completed" |
  652. "chunk_download_completed" |
  653. "download_completed" |
  654. "missing_chunks" |
  655. "metadata_not_found" |
  656. "resource_updated" => {
  657. let resource: Resource = info.get("resource").unwrap().clone().into();
  658. update_resource(&resource).await;
  659. }
  660. "resource_removed" => {
  661. {
  662. let hash = info.get("hash").unwrap().get::<String>().unwrap();
  663. let mut resources_write = resources.write().await;
  664. let i = resources_write
  665. .iter()
  666. .position(|r| hash_to_string(&r.hash) == *hash);
  667. if let Some(i) = i {
  668. resources_write.remove(i);
  669. }
  670. }
  671. let r = resources.read().await.clone();
  672. print_begin().await;
  673. for resource in r.iter() {
  674. update_resource(resource).await;
  675. }
  676. }
  677. "download_error" => {
  678. // An error that caused the download to be unsuccessful
  679. }
  680. _ => {}
  681. }
  682. }
  683. JsonResult::Error(e) => {
  684. return Err(Error::UnexpectedJsonRpc(format!("Got error from JSON-RPC: {e:?}")))
  685. }
  686. x => {
  687. return Err(Error::UnexpectedJsonRpc(format!(
  688. "Got unexpected data from JSON-RPC: {x:?}"
  689. )))
  690. }
  691. }
  692. }
  693. }
  694. async fn remove(&self, hash: String) -> Result<()> {
  695. let req = JsonRequest::new("remove", JsonValue::Array(vec![JsonValue::String(hash)]));
  696. self.rpc_client.request(req).await?;
  697. Ok(())
  698. }
  699. async fn verify(&self, files: Option<Vec<String>>) -> Result<()> {
  700. let files = files.unwrap_or_default().into_iter().map(JsonValue::String).collect();
  701. let req = JsonRequest::new("verify", JsonValue::Array(files));
  702. self.rpc_client.request(req).await?;
  703. Ok(())
  704. }
  705. }
  706. fn main() -> Result<()> {
  707. let args = Args::parse();
  708. let log_level = get_log_level(args.verbose);
  709. let log_config = get_log_config(args.verbose);
  710. TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
  711. let ex = Arc::new(smol::Executor::new());
  712. smol::block_on(async {
  713. ex.run(async {
  714. let rpc_client = Arc::new(RpcClient::new(args.endpoint.clone(), ex.clone()).await?);
  715. let fu = Fu { rpc_client, endpoint: args.endpoint.clone() };
  716. match args.command {
  717. Subcmd::Get { hash, path, files } => fu.get(hash, path, files, ex.clone()).await,
  718. Subcmd::Put { path } => fu.put(path, ex.clone()).await,
  719. Subcmd::Ls {} => fu.list_resources().await,
  720. Subcmd::Watch {} => fu.watch(ex.clone()).await,
  721. Subcmd::Rm { hash } => fu.remove(hash).await,
  722. Subcmd::ListBuckets {} => fu.list_buckets().await,
  723. Subcmd::ListSeeders {} => fu.list_seeders().await,
  724. Subcmd::Verify { files } => fu.verify(files).await,
  725. }?;
  726. Ok(())
  727. })
  728. .await
  729. })
  730. }