Skip to content

Commit

Permalink
Added the ability to tail files from a connection
Browse files Browse the repository at this point in the history
This commit introduces one of the last major pieces for Connection, the
ability to trigger a tail.
I'm undecided if I want to expose the FileTailer class, so we'll leave
it out for now.
Also, the names of the attributes are subject to change pending
feedback.
  • Loading branch information
JacobCallahan committed Mar 27, 2024
1 parent 96db758 commit 5deaf84
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 10 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hussh"
version = "0.1.2"
version = "0.1.3"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,17 @@ conn.sftp_read(remote_path="/dest/path/file", local_path="/path/to/my/file")
contents = conn.sftp_read(remote_path="/dest/path/file")
```

## Copy files from one connection to another
Hussh offers a shortcut that allows you to copy a file between two established connections.
```python
source_conn = Connection("my.first.server")
dest_conn = Connection("my.second.server", password="secret")
# Copy from source to destination
source_conn.remote_copy(source_path="/root/myfile.txt", dest_conn=dest_conn)
```
By default, if you don't pass in an alternate `dest_path`, Hussh will copy it to the same path as it came from on source.


# SCP
For remote servers that support SCP, Hussh can do that to.

Expand All @@ -95,16 +106,15 @@ conn.scp_read(remote_path="/dest/path/file", local_path="/path/to/my/file")
contents = conn.scp_read(remote_path="/dest/path/file")
```

## Copy files from one connection to another
Hussh offers a shortcut that allows you to copy a file between two established connections.
# Tailing Files
Hussh offers a built-in method for tailing files on a `Connection` with the `tail` method.
```python
source_conn = Connection("my.first.server")
dest_conn = Connection("my.second.server", password="secret")
# Copy from source to destination
source_conn.remote_copy(source_path="/root/myfile.txt", dest_conn=dest_conn)
with conn.tail("/path/to/file.txt") as tf:
# perform some actions or wait
print(tf.read()) # at any time, you can read any unread contents
# when you're done tailing, exit the context manager
print(tf.tailed_contents)
```
By default, if you don't pass in an alternate `dest_path`, Hussh will copy it to the same path as it came from on source.


# Interactive Shell
If you need to keep a shell open to perform more complex interactions, you can get an `InteractiveShell` instance from the `Connection` class instance.
Expand Down
104 changes: 103 additions & 1 deletion src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ use pyo3::create_exception;
use pyo3::exceptions::PyTimeoutError;
use pyo3::prelude::*;
use ssh2::{Channel, Session};
use std::io::{BufReader, BufWriter, Read, Write};
use std::io::{BufReader, BufWriter, Read, Seek, Write};
use std::net::TcpStream;
use std::path::Path;

Expand Down Expand Up @@ -459,6 +459,19 @@ impl Connection {
Ok(())
}

/// Return a FileTailer instance given a remote file path
/// This is best used as a context manager, but can be used directly
/// ```python
/// with conn.tail("remote_file.log") as tailer:
/// time.sleep(5) # wait or perform other operations
/// print(tailer.read())
/// time.sleep(5) # wait or perform other operations
/// print(tailer.tailed_contents)
/// ```
fn tail(&self, remote_file: String) -> FileTailer {
FileTailer::new(self, remote_file, None)
}

fn __repr__(&self) -> PyResult<String> {
Ok(format!(
"Connection(host={}, port={}, username={}, password=*****)",
Expand Down Expand Up @@ -555,3 +568,92 @@ impl InteractiveShell {
Ok(())
}
}


/// `FileTailer` is a structure that represents a remote file tailer.
///
/// It maintains an SFTP connection and the path to a remote file,
/// and allows reading from a specified position in the file.
///
/// # Fields
///
/// * `sftp_conn`: An SFTP connection from the ssh2 crate.
/// * `remote_file`: A string representing the path to the remote file.
/// * `init_pos`: An optional initial position from where to start reading the file.
/// * `last_pos`: The last position read from the file.
/// * `tailed_contents`: The contents read from the file.
///
/// # Methods
///
/// * `new`: Constructs a new `FileTailer`.
/// * `seek_end`: Seeks to the end of the remote file.
/// * `read`: Reads the contents of the remote file from a given position.
/// * `__enter__`: Prepares the `FileTailer` for use in a `with` statement.
/// * `__exit__`: Cleans up after the `FileTailer` is used in a `with` statement.
#[pyclass]
struct FileTailer {
sftp_conn: ssh2::Sftp,
#[pyo3(get)]
remote_file: String,
init_pos: Option<u64>,
#[pyo3(get)]
last_pos: u64,
#[pyo3(get)]
tailed_contents: Option<String>,
}

#[pymethods]
impl FileTailer {
#[new]
fn new(
conn: &Connection,
remote_file: String,
init_pos: Option<u64>,
) -> FileTailer {
FileTailer {
sftp_conn: conn.session.sftp().unwrap(),
remote_file,
init_pos,
last_pos: 0,
tailed_contents: None,
}
}

// Determine the current end of the remote file
fn seek_end(&mut self) -> PyResult<Option<u64>> {
let len = self.sftp_conn.stat(Path::new(&self.remote_file)).unwrap().size;
self.last_pos = len.unwrap();
if !self.init_pos.is_some() {
self.init_pos = len;
}
Ok(len)
}

// Read the contents of the remote file from a given position
fn read(&mut self, from_pos: Option<u64>) -> String {
let from_pos = from_pos.unwrap_or(self.last_pos);
let mut remote_file = BufReader::new(
self.sftp_conn.open(Path::new(&self.remote_file)).unwrap(),
);
remote_file.seek(std::io::SeekFrom::Start(from_pos)).unwrap();
let mut contents = String::new();
remote_file.read_to_string(&mut contents).unwrap();
self.last_pos = remote_file.seek(std::io::SeekFrom::Current(0)).unwrap();
contents
}

fn __enter__(mut slf: PyRefMut<Self>) -> PyResult<PyRefMut<Self>> {
slf.seek_end()?;
Ok(slf)
}

fn __exit__(
&mut self,
_exc_type: Option<&Bound<'_, PyAny>>,
_exc_value: Option<&Bound<'_, PyAny>>,
_traceback: Option<&Bound<'_, PyAny>>,
) -> PyResult<()> {
self.tailed_contents = Some(self.read(self.init_pos));
Ok(())
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ fn hussh(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<connection::Connection>()?; // Add the Connection class
m.add_class::<connection::SSHResult>()?;
// m.add_class::<connection::InteractiveShell>()?;
// m.add_class::<connection::FileTailer>()?;
m.add(
"AuthenticationError",
_py.get_type_bound::<AuthenticationError>(),
Expand Down
10 changes: 10 additions & 0 deletions tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,3 +163,13 @@ def test_remote_copy(conn, run_second_server):
dest_conn = Connection(host="localhost", port=8023, password="toor")
conn.remote_copy("/root/hp.txt", dest_conn)
assert "hp.txt" in dest_conn.execute("ls /root").stdout


def test_tail(conn):
"""Test that we can tail a file."""
conn.scp_write_data("hello\nworld\n", "/root/hello.txt")
with conn.tail("/root/hello.txt") as tf:
assert tf.read(0) == "hello\nworld\n"
assert tf.last_pos == 12
conn.execute("echo goodbye >> /root/hello.txt")
assert tf.tailed_contents == "goodbye\n"

0 comments on commit 5deaf84

Please sign in to comment.