Skip to main content

sui_tool/db_shell/
proxy.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! HTTP proxy backend for the db-shell.
5//! Delegates all operations to the running sui-node admin API.
6
7use anyhow::{Context, bail};
8use reqwest::blocking::Client;
9use serde::Deserialize;
10
11use crate::db_shell::{
12    backend::{Backend, DirEntry},
13    vfs::VfsPath,
14};
15
16pub struct ProxyBackend {
17    client: Client,
18    base_url: String,
19}
20
21impl ProxyBackend {
22    pub fn new(admin_url: &str) -> anyhow::Result<Self> {
23        let client = Client::builder()
24            .timeout(std::time::Duration::from_secs(30))
25            .build()
26            .context("failed to build HTTP client")?;
27        Ok(Self {
28            client,
29            base_url: admin_url.trim_end_matches('/').to_string(),
30        })
31    }
32
33    fn ls_impl(&self, path: &VfsPath, limit: usize, cursor: bool) -> anyhow::Result<Vec<DirEntry>> {
34        #[derive(Deserialize)]
35        struct Entry {
36            name: String,
37            is_dir: bool,
38        }
39
40        let url = format!("{}/db-shell/ls", self.base_url);
41        let resp = self
42            .client
43            .get(&url)
44            .query(&[
45                ("path", path.to_string()),
46                ("limit", limit.to_string()),
47                ("cursor", cursor.to_string()),
48            ])
49            .send()
50            .context("ls request failed")?;
51
52        if !resp.status().is_success() {
53            let status = resp.status();
54            let body = resp.text().unwrap_or_default();
55            bail!("ls failed ({status}): {body}");
56        }
57
58        let entries: Vec<Entry> = resp.json().context("failed to parse ls response")?;
59        Ok(entries
60            .into_iter()
61            .map(|e| DirEntry {
62                name: e.name,
63                is_dir: e.is_dir,
64            })
65            .collect())
66    }
67
68    fn read_impl(&self, path: &VfsPath, format: &str) -> anyhow::Result<Vec<u8>> {
69        let url = format!("{}/db-shell/read", self.base_url);
70        let resp = self
71            .client
72            .get(&url)
73            .query(&[("path", path.to_string()), ("format", format.to_string())])
74            .send()
75            .context("read request failed")?;
76
77        if !resp.status().is_success() {
78            let status = resp.status();
79            let body = resp.text().unwrap_or_default();
80            bail!("read failed ({status}): {body}");
81        }
82
83        Ok(resp
84            .bytes()
85            .context("failed to read response body")?
86            .to_vec())
87    }
88}
89
90impl Backend for ProxyBackend {
91    fn ls_children(&self, path: &VfsPath, limit: usize) -> anyhow::Result<Vec<DirEntry>> {
92        self.ls_impl(path, limit, false)
93    }
94
95    fn ls_cursor(&self, path: &VfsPath, limit: usize) -> anyhow::Result<Vec<DirEntry>> {
96        self.ls_impl(path, limit, true)
97    }
98
99    fn read_json(&self, path: &VfsPath) -> anyhow::Result<serde_json::Value> {
100        let bytes = self.read_impl(path, "json")?;
101        serde_json::from_slice(&bytes).context("failed to parse JSON response")
102    }
103
104    fn read_debug(&self, path: &VfsPath) -> anyhow::Result<String> {
105        let bytes = self.read_impl(path, "debug")?;
106        String::from_utf8(bytes).context("debug response is not valid UTF-8")
107    }
108
109    fn read_bcs(&self, path: &VfsPath) -> anyhow::Result<Vec<u8>> {
110        let bytes = self.read_impl(path, "raw-bcs")?;
111        Ok(bytes)
112    }
113
114    fn delete(&self, path: &VfsPath) -> anyhow::Result<()> {
115        let url = format!("{}/db-shell/delete", self.base_url);
116        let resp = self
117            .client
118            .delete(&url)
119            .query(&[("path", path.to_string())])
120            .send()
121            .context("delete request failed")?;
122
123        if !resp.status().is_success() {
124            let status = resp.status();
125            let body = resp.text().unwrap_or_default();
126            bail!("delete failed ({status}): {body}");
127        }
128        Ok(())
129    }
130}