Skip to main content

breezyshim/
transport.rs

1//! Transport module
2use crate::error::Error;
3use pyo3::prelude::*;
4use pyo3::types::PyDict;
5use std::path::{Path, PathBuf};
6
7/// A transport represents a way to access content in a branch.
8pub struct Transport(Py<PyAny>);
9
10impl Transport {
11    /// Create a new transport from a Python object.
12    pub fn new(obj: Py<PyAny>) -> Self {
13        Transport(obj)
14    }
15
16    /// Get the underlying Py<PyAny>.
17    pub(crate) fn as_pyobject(&self) -> &Py<PyAny> {
18        &self.0
19    }
20
21    /// Get the base URL of this transport.
22    pub fn base(&self) -> url::Url {
23        pyo3::Python::attach(|py| {
24            self.as_pyobject()
25                .getattr(py, "base")
26                .unwrap()
27                .extract::<String>(py)
28                .unwrap()
29                .parse()
30                .unwrap()
31        })
32    }
33
34    /// Check if this is a local transport.
35    pub fn is_local(&self) -> bool {
36        pyo3::import_exception!(breezy.errors, NotLocalUrl);
37        pyo3::Python::attach(|py| {
38            self.0
39                .call_method1(py, "local_abspath", (".",))
40                .map(|_| true)
41                .or_else(|e| {
42                    if e.is_instance_of::<NotLocalUrl>(py) {
43                        Ok::<_, PyErr>(false)
44                    } else {
45                        panic!("Unexpected error: {:?}", e)
46                    }
47                })
48                .unwrap()
49        })
50    }
51
52    /// Get the local absolute path for a path within this transport.
53    pub fn local_abspath(&self, path: &Path) -> Result<PathBuf, Error> {
54        pyo3::Python::attach(|py| {
55            Ok(self
56                .0
57                .call_method1(py, "local_abspath", (path.to_string_lossy().as_ref(),))?
58                .extract::<PathBuf>(py)?)
59        })
60    }
61
62    /// Check if a path exists in this transport.
63    pub fn has(&self, path: &str) -> Result<bool, Error> {
64        pyo3::Python::attach(|py| {
65            Ok(self
66                .0
67                .call_method1(py, "has", (path,))?
68                .extract::<bool>(py)
69                .unwrap())
70        })
71    }
72
73    /// Ensure the base directory exists.
74    pub fn ensure_base(&self) -> Result<(), Error> {
75        pyo3::Python::attach(|py| {
76            self.0.call_method0(py, "ensure_base")?;
77            Ok(())
78        })
79    }
80
81    /// Create all the directories leading up to the final path component.
82    pub fn create_prefix(&self) -> Result<(), Error> {
83        pyo3::Python::attach(|py| {
84            self.0.call_method0(py, "create_prefix")?;
85            Ok(())
86        })
87    }
88
89    /// Create a new transport with a different path.
90    pub fn clone(&self, path: &str) -> Result<Transport, Error> {
91        pyo3::Python::attach(|py| {
92            let o = self.0.call_method1(py, "clone", (path,))?;
93            Ok(Transport(o))
94        })
95    }
96}
97
98impl<'a, 'py> FromPyObject<'a, 'py> for Transport {
99    type Error = PyErr;
100
101    fn extract(obj: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
102        Ok(Transport(obj.to_owned().unbind()))
103    }
104}
105
106impl<'py> IntoPyObject<'py> for Transport {
107    type Target = PyAny;
108    type Output = Bound<'py, Self::Target>;
109    type Error = std::convert::Infallible;
110
111    fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
112        Ok(self.0.into_bound(py))
113    }
114}
115
116/// Get a transport for a given URL.
117///
118/// # Arguments
119/// * `url` - The URL to get a transport for
120/// * `possible_transports` - Optional list of transports to try reusing
121pub fn get_transport(
122    url: &url::Url,
123    possible_transports: Option<&mut Vec<Transport>>,
124) -> Result<Transport, Error> {
125    pyo3::Python::attach(|py| {
126        let urlutils = py.import("breezy.transport").unwrap();
127        let kwargs = PyDict::new(py);
128        kwargs.set_item(
129            "possible_transports",
130            possible_transports.map(|t| {
131                t.iter()
132                    .map(|t| t.0.clone_ref(py))
133                    .collect::<Vec<Py<PyAny>>>()
134            }),
135        )?;
136        let o = urlutils.call_method("get_transport", (url.to_string(),), Some(&kwargs))?;
137        Ok(Transport(o.unbind()))
138    })
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::path::Path;
145
146    #[test]
147    fn test_get_transport() {
148        let td = tempfile::tempdir().unwrap();
149        let url = url::Url::from_file_path(td.path()).unwrap();
150        let transport = get_transport(&url, None).unwrap();
151
152        // Test base URL
153        let base = transport.base();
154        assert!(base.to_string().starts_with("file://"));
155    }
156
157    #[test]
158    fn test_transport_is_local() {
159        let td = tempfile::tempdir().unwrap();
160        let url = url::Url::from_file_path(td.path()).unwrap();
161        let transport = get_transport(&url, None).unwrap();
162
163        assert!(transport.is_local());
164    }
165
166    #[test]
167    fn test_transport_local_abspath() {
168        let td = tempfile::tempdir().unwrap();
169        let url = url::Url::from_file_path(td.path()).unwrap();
170        let transport = get_transport(&url, None).unwrap();
171
172        let path = Path::new("test.txt");
173        let abspath = transport.local_abspath(path).unwrap();
174        assert!(abspath.is_absolute());
175    }
176
177    #[test]
178    fn test_transport_has() {
179        let td = tempfile::tempdir().unwrap();
180        let url = url::Url::from_file_path(td.path()).unwrap();
181        let transport = get_transport(&url, None).unwrap();
182
183        // Test for non-existent file
184        let exists = transport.has("nonexistent.txt").unwrap();
185        assert!(!exists);
186    }
187
188    #[test]
189    fn test_transport_ensure_base() {
190        let td = tempfile::tempdir().unwrap();
191        let url = url::Url::from_file_path(td.path()).unwrap();
192        let transport = get_transport(&url, None).unwrap();
193
194        let result = transport.ensure_base();
195        assert!(result.is_ok());
196    }
197
198    #[test]
199    fn test_transport_create_prefix() {
200        let td = tempfile::tempdir().unwrap();
201        let url = url::Url::from_file_path(td.path()).unwrap();
202        let transport = get_transport(&url, None).unwrap();
203
204        let result = transport.create_prefix();
205        assert!(result.is_ok());
206    }
207
208    #[test]
209    fn test_transport_clone() {
210        let td = tempfile::tempdir().unwrap();
211        let url = url::Url::from_file_path(td.path()).unwrap();
212        let transport = get_transport(&url, None).unwrap();
213
214        let cloned = transport.clone("subdir").unwrap();
215        let cloned_base = cloned.base();
216        assert!(cloned_base.to_string().contains("subdir"));
217    }
218
219    #[test]
220    fn test_transport_into_pyobject() {
221        let td = tempfile::tempdir().unwrap();
222        let url = url::Url::from_file_path(td.path()).unwrap();
223        let transport = get_transport(&url, None).unwrap();
224
225        Python::attach(|py| {
226            let _pyobj = transport.into_pyobject(py).unwrap();
227        });
228    }
229
230    #[test]
231    fn test_get_transport_with_possible_transports() {
232        let td = tempfile::tempdir().unwrap();
233        let url = url::Url::from_file_path(td.path()).unwrap();
234
235        let mut possible_transports = vec![];
236        let transport = get_transport(&url, Some(&mut possible_transports)).unwrap();
237
238        let base = transport.base();
239        assert!(base.to_string().starts_with("file://"));
240    }
241}