Skip to content

Commit 212ffe7

Browse files
committed
Add support for reading archives
1 parent aff860c commit 212ffe7

7 files changed

Lines changed: 643 additions & 21 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ build-backend = "maturin"
1515
[dependency-groups]
1616
dev = [
1717
"maturin>=1.9.6",
18+
"psutil>=7.1.2",
1819
"pytest>=8.3.5",
1920
"ruff>=0.14.2",
2021
]

src/lib.rs

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,10 @@ use pyo3::exceptions::PyRuntimeError;
55
use pyo3::prelude::*;
66
use pyo3::types::{PyAny, PyType};
77
use std::fs::File;
8-
use std::io::Write;
8+
use std::io::{Read, Write};
99
use std::path::{Path, PathBuf};
1010
use tar::Archive;
1111

12-
#[pyfunction]
13-
fn untar_gz(tar_gz_path: String, destination_path: String) -> PyResult<()> {
14-
let tar_gz = File::open(tar_gz_path)?;
15-
let tar = GzDecoder::new(tar_gz);
16-
let mut archive = Archive::new(tar);
17-
archive.unpack(destination_path)?;
18-
Ok(())
19-
}
20-
2112
#[pyclass]
2213
struct ArchiveWriter {
2314
builder: Option<tar::Builder<Box<dyn Write + Send + Sync>>>,
@@ -118,6 +109,81 @@ impl ArchiveWriter {
118109
}
119110
}
120111

112+
#[pyclass(unsendable)]
113+
struct ArchiveReader {
114+
archive: Option<Archive<Box<dyn Read>>>,
115+
}
116+
117+
#[pymethods]
118+
impl ArchiveReader {
119+
#[classmethod]
120+
#[pyo3(signature = (path, mode="r:gz"))]
121+
fn open(
122+
_cls: &Bound<'_, PyType>,
123+
py: Python<'_>,
124+
path: PathBuf,
125+
mode: &str,
126+
) -> PyResult<Py<ArchiveReader>> {
127+
match mode {
128+
"r:gz" => {
129+
let file = File::open(path)?;
130+
let decoder = GzDecoder::new(file);
131+
let reader: Box<dyn Read> = Box::new(decoder);
132+
let archive = Archive::new(reader);
133+
Py::new(
134+
py,
135+
ArchiveReader {
136+
archive: Some(archive),
137+
},
138+
)
139+
}
140+
"r" => {
141+
let file = File::open(path)?;
142+
let reader: Box<dyn Read> = Box::new(file);
143+
let archive = Archive::new(reader);
144+
Py::new(
145+
py,
146+
ArchiveReader {
147+
archive: Some(archive),
148+
},
149+
)
150+
}
151+
_ => Err(PyRuntimeError::new_err(
152+
"unsupported mode; only 'r' and 'r:gz' are supported",
153+
)),
154+
}
155+
}
156+
157+
fn extract(&mut self, to: PathBuf) -> PyResult<()> {
158+
let archive = self
159+
.archive
160+
.as_mut()
161+
.ok_or_else(|| PyRuntimeError::new_err("archive is already closed"))?;
162+
163+
archive.unpack(to)?;
164+
Ok(())
165+
}
166+
167+
fn close(&mut self) -> PyResult<()> {
168+
self.archive.take();
169+
Ok(())
170+
}
171+
172+
fn __enter__(py_self: PyRef<'_, Self>) -> PyRef<'_, Self> {
173+
py_self
174+
}
175+
176+
fn __exit__(
177+
&mut self,
178+
_exc_type: Option<Bound<'_, PyAny>>,
179+
_exc: Option<Bound<'_, PyAny>>,
180+
_tb: Option<Bound<'_, PyAny>>,
181+
) -> PyResult<bool> {
182+
self.close()?;
183+
Ok(false) // Propagate exceptions if any
184+
}
185+
}
186+
121187
#[pyfunction]
122188
#[pyo3(signature = (path, mode))]
123189
fn open(py: Python<'_>, path: PathBuf, mode: &str) -> PyResult<PyObject> {
@@ -126,16 +192,20 @@ fn open(py: Python<'_>, path: PathBuf, mode: &str) -> PyResult<PyObject> {
126192
let writer = ArchiveWriter::open(&py.get_type::<ArchiveWriter>(), py, path, mode)?;
127193
Ok(writer.into())
128194
}
195+
"r" | "r:gz" => {
196+
let reader = ArchiveReader::open(&py.get_type::<ArchiveReader>(), py, path, mode)?;
197+
Ok(reader.into())
198+
}
129199
_ => Err(PyRuntimeError::new_err(
130-
"unsupported mode; supported modes are 'w', 'w:gz'",
200+
"unsupported mode; supported modes are 'w', 'w:gz', 'r', 'r:gz'",
131201
)),
132202
}
133203
}
134204

135205
#[pymodule]
136206
fn fastar(m: &Bound<'_, PyModule>) -> PyResult<()> {
137207
m.add_class::<ArchiveWriter>()?;
208+
m.add_class::<ArchiveReader>()?;
138209
m.add_function(wrap_pyfunction!(open, m)?)?;
139-
m.add_function(wrap_pyfunction!(untar_gz, m)?)?;
140210
Ok(())
141211
}

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ def archive_path(tmp_path) -> Path:
1212
return tmp_path / "archive.tar.gz"
1313

1414

15+
@pytest.fixture
16+
def source_path(tmp_path) -> Path:
17+
path = tmp_path / "source"
18+
path.mkdir()
19+
return path
20+
21+
22+
@pytest.fixture
23+
def target_path(tmp_path) -> Path:
24+
path = tmp_path / "target"
25+
path.mkdir()
26+
return path
27+
28+
1529
@pytest.fixture(params=[("w", "r"), ("w:gz", "r:gz")])
1630
def modes(request) -> tuple[WriteMode, ReadMode]:
1731
return request.param

0 commit comments

Comments
 (0)