use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use tempfile::{NamedTempFile, TempDir};
#[derive(Debug)]
pub struct TempStorage {
dir: TempDir,
}
impl TempStorage {
pub fn new() -> io::Result<Self> {
let dir = TempDir::new()?;
Ok(Self { dir })
}
pub fn new_in(parent: &Path) -> io::Result<Self> {
let dir = TempDir::new_in(parent)?;
Ok(Self { dir })
}
pub fn with_prefix(prefix: &str) -> io::Result<Self> {
let dir = TempDir::with_prefix(prefix)?;
Ok(Self { dir })
}
pub fn path(&self) -> &Path {
self.dir.path()
}
pub fn store_bytes(&self, name: &str, data: &[u8]) -> io::Result<PathBuf> {
let path = self.dir.path().join(name);
std::fs::write(&path, data)?;
Ok(path)
}
pub async fn store_bytes_async(&self, name: &str, data: &[u8]) -> io::Result<PathBuf> {
let path = self.dir.path().join(name);
tokio::fs::write(&path, data).await?;
Ok(path)
}
pub fn store_string(&self, name: &str, data: &str) -> io::Result<PathBuf> {
self.store_bytes(name, data.as_bytes())
}
pub fn store_json(&self, name: &str, data: &serde_json::Value) -> io::Result<PathBuf> {
let json_str = serde_json::to_string(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
self.store_string(name, &json_str)
}
pub async fn store_json_async(
&self,
name: &str,
data: &serde_json::Value,
) -> io::Result<PathBuf> {
let json_str = serde_json::to_string(data)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
self.store_bytes_async(name, json_str.as_bytes()).await
}
pub fn read_bytes(&self, name: &str) -> io::Result<Vec<u8>> {
let path = self.dir.path().join(name);
std::fs::read(&path)
}
pub async fn read_bytes_async(&self, name: &str) -> io::Result<Vec<u8>> {
let path = self.dir.path().join(name);
tokio::fs::read(&path).await
}
pub fn read_string(&self, name: &str) -> io::Result<String> {
let path = self.dir.path().join(name);
std::fs::read_to_string(&path)
}
pub async fn read_string_async(&self, name: &str) -> io::Result<String> {
let data = self.read_bytes_async(name).await?;
String::from_utf8(data).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn read_json(&self, name: &str) -> io::Result<serde_json::Value> {
let content = self.read_string(name)?;
serde_json::from_str(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub async fn read_json_async(&self, name: &str) -> io::Result<serde_json::Value> {
let content = self.read_string_async(name).await?;
serde_json::from_str(&content).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn exists(&self, name: &str) -> bool {
self.dir.path().join(name).exists()
}
pub fn remove(&self, name: &str) -> io::Result<()> {
let path = self.dir.path().join(name);
if path.exists() {
std::fs::remove_file(&path)?;
}
Ok(())
}
pub async fn remove_async(&self, name: &str) -> io::Result<()> {
let path = self.dir.path().join(name);
if path.exists() {
tokio::fs::remove_file(&path).await?;
}
Ok(())
}
pub fn list(&self) -> io::Result<Vec<String>> {
let mut files = Vec::new();
for entry in std::fs::read_dir(self.dir.path())? {
let entry = entry?;
if let Some(name) = entry.file_name().to_str() {
files.push(name.to_string());
}
}
Ok(files)
}
pub fn total_size(&self) -> io::Result<u64> {
let mut total = 0;
for entry in std::fs::read_dir(self.dir.path())? {
let entry = entry?;
total += entry.metadata()?.len();
}
Ok(total)
}
pub fn create_temp_file(&self) -> io::Result<TempFile> {
let file = NamedTempFile::new_in(self.dir.path())?;
Ok(TempFile { file })
}
pub fn persist(self) -> PathBuf {
self.dir.keep()
}
}
impl Default for TempStorage {
fn default() -> Self {
match Self::new() {
Ok(s) => s,
Err(e) => {
log::error!("TempStorage::new() failed: {e}, falling back to std::env::temp_dir()");
let fallback =
std::env::temp_dir().join(format!("spider_tmp_{}", std::process::id()));
let _ = std::fs::create_dir_all(&fallback);
Self::new_in(&fallback)
.expect("TempStorage fallback in std::env::temp_dir() also failed")
}
}
}
}
#[derive(Debug)]
pub struct TempFile {
file: NamedTempFile,
}
impl TempFile {
pub fn new() -> io::Result<Self> {
let file = NamedTempFile::new()?;
Ok(Self { file })
}
pub fn path(&self) -> &Path {
self.file.path()
}
pub fn write_all(&mut self, data: &[u8]) -> io::Result<()> {
self.file.write_all(data)
}
pub fn write_str(&mut self, data: &str) -> io::Result<()> {
self.write_all(data.as_bytes())
}
pub fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
pub fn read_all(&mut self) -> io::Result<Vec<u8>> {
use std::io::Seek;
self.file.seek(std::io::SeekFrom::Start(0))?;
let mut contents = Vec::new();
self.file.read_to_end(&mut contents)?;
Ok(contents)
}
pub fn read_string(&mut self) -> io::Result<String> {
let bytes = self.read_all()?;
String::from_utf8(bytes).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn persist(self) -> io::Result<PathBuf> {
let (_, path) = self.file.keep()?;
Ok(path)
}
}
impl Write for TempFile {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}
}
impl Read for TempFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.file.read(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_temp_storage_basic() {
let storage = TempStorage::new().unwrap();
let data = b"hello world";
storage.store_bytes("test.txt", data).unwrap();
let read = storage.read_bytes("test.txt").unwrap();
assert_eq!(read, data);
assert!(storage.exists("test.txt"));
assert!(!storage.exists("nonexistent.txt"));
let files = storage.list().unwrap();
assert!(files.contains(&"test.txt".to_string()));
}
#[test]
fn test_temp_storage_json() {
let storage = TempStorage::new().unwrap();
let json = serde_json::json!({
"name": "test",
"value": 42
});
storage.store_json("data.json", &json).unwrap();
let read = storage.read_json("data.json").unwrap();
assert_eq!(read, json);
}
#[test]
fn test_temp_file() {
let mut file = TempFile::new().unwrap();
file.write_str("hello").unwrap();
file.flush().unwrap();
let content = file.read_string().unwrap();
assert_eq!(content, "hello");
}
}