#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportError {
NoSuchFile(String),
ReadOnly(String),
Io {
kind: std::io::ErrorKind,
message: String,
},
Other(String),
}
impl From<std::io::Error> for TransportError {
fn from(e: std::io::Error) -> Self {
if e.kind() == std::io::ErrorKind::NotFound {
TransportError::NoSuchFile(e.to_string())
} else {
TransportError::Io {
kind: e.kind(),
message: e.to_string(),
}
}
}
}
impl std::fmt::Display for TransportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportError::NoSuchFile(p) => write!(f, "No such file: {}", p),
TransportError::ReadOnly(p) => write!(f, "Read-only transport: {}", p),
TransportError::Io { kind, message } => {
write!(f, "I/O error ({:?}): {}", kind, message)
}
TransportError::Other(s) => write!(f, "Transport error: {}", s),
}
}
}
impl std::error::Error for TransportError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadRange {
pub offset: u64,
pub length: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadResult {
pub offset: u64,
pub length: usize,
pub bytes: Vec<u8>,
}
pub trait Transport {
fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError>;
fn put_file_non_atomic(
&self,
path: &str,
bytes: &[u8],
create_parent_dir: bool,
) -> Result<(), TransportError>;
fn put_bytes(&self, path: &str, bytes: &[u8], mode: Option<u32>) -> Result<(), TransportError> {
let _ = mode;
self.put_file_non_atomic(path, bytes, false)
}
fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError>;
fn mkdir(&self, path: &str) -> Result<(), TransportError>;
fn has(&self, path: &str) -> Result<bool, TransportError>;
fn readv(&self, path: &str, ranges: &[ReadRange]) -> Result<Vec<ReadResult>, TransportError> {
let data = self.get_bytes(path)?;
let mut out = Vec::with_capacity(ranges.len());
for r in ranges {
let start = r.offset as usize;
let end = start.checked_add(r.length).ok_or_else(|| {
TransportError::Other(format!(
"readv range overflow: offset={} length={}",
r.offset, r.length
))
})?;
if end > data.len() {
return Err(TransportError::Other(format!(
"readv range past end: offset={} length={} data_len={}",
r.offset,
r.length,
data.len()
)));
}
out.push(ReadResult {
offset: r.offset,
length: r.length,
bytes: data[start..end].to_vec(),
});
}
Ok(out)
}
fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError>;
fn abspath(&self, path: &str) -> Result<String, TransportError>;
fn rename(&self, from: &str, to: &str) -> Result<(), TransportError> {
let _ = (from, to);
Err(TransportError::Other(
"rename not supported by this transport".to_string(),
))
}
fn delete(&self, path: &str) -> Result<(), TransportError> {
let _ = path;
Err(TransportError::Other(
"delete not supported by this transport".to_string(),
))
}
fn rmdir(&self, path: &str) -> Result<(), TransportError> {
let _ = path;
Err(TransportError::Other(
"rmdir not supported by this transport".to_string(),
))
}
fn list_dir(&self, path: &str) -> Result<Vec<String>, TransportError> {
let _ = path;
Err(TransportError::Other(
"list_dir not supported by this transport".to_string(),
))
}
fn stat(&self, path: &str) -> Result<Stat, TransportError> {
let _ = path;
Err(TransportError::Other(
"stat not supported by this transport".to_string(),
))
}
fn subtransport(&self, path: &str) -> Result<SharedTransport, TransportError> {
let _ = path;
Err(TransportError::Other(
"subtransport not supported by this transport".to_string(),
))
}
fn local_path(&self, path: &str) -> Option<std::path::PathBuf> {
let _ = path;
None
}
}
impl Transport for std::sync::Arc<dyn Transport + Send + Sync> {
fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
(**self).get_bytes(path)
}
fn put_file_non_atomic(
&self,
path: &str,
bytes: &[u8],
create_parent_dir: bool,
) -> Result<(), TransportError> {
(**self).put_file_non_atomic(path, bytes, create_parent_dir)
}
fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
(**self).append_bytes(path, bytes)
}
fn mkdir(&self, path: &str) -> Result<(), TransportError> {
(**self).mkdir(path)
}
fn has(&self, path: &str) -> Result<bool, TransportError> {
(**self).has(path)
}
fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
(**self).iter_files_recursive()
}
fn abspath(&self, path: &str) -> Result<String, TransportError> {
(**self).abspath(path)
}
fn readv(&self, path: &str, ranges: &[ReadRange]) -> Result<Vec<ReadResult>, TransportError> {
(**self).readv(path, ranges)
}
fn put_bytes(&self, path: &str, bytes: &[u8], mode: Option<u32>) -> Result<(), TransportError> {
(**self).put_bytes(path, bytes, mode)
}
fn rename(&self, from: &str, to: &str) -> Result<(), TransportError> {
(**self).rename(from, to)
}
fn delete(&self, path: &str) -> Result<(), TransportError> {
(**self).delete(path)
}
fn rmdir(&self, path: &str) -> Result<(), TransportError> {
(**self).rmdir(path)
}
fn list_dir(&self, path: &str) -> Result<Vec<String>, TransportError> {
(**self).list_dir(path)
}
fn stat(&self, path: &str) -> Result<Stat, TransportError> {
(**self).stat(path)
}
fn subtransport(&self, path: &str) -> Result<SharedTransport, TransportError> {
(**self).subtransport(path)
}
fn local_path(&self, path: &str) -> Option<std::path::PathBuf> {
(**self).local_path(path)
}
}
pub type SharedTransport = std::sync::Arc<dyn Transport + Send + Sync>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stat {
pub size: u64,
pub is_dir: bool,
}
pub struct LocalTransport {
root: std::path::PathBuf,
}
impl LocalTransport {
pub fn new<P: Into<std::path::PathBuf>>(root: P) -> Self {
LocalTransport { root: root.into() }
}
pub fn root(&self) -> &std::path::Path {
&self.root
}
fn resolve(&self, path: &str) -> std::path::PathBuf {
self.root.join(crate::key_mapper::url_unquote(path))
}
}
impl Transport for LocalTransport {
fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
Ok(std::fs::read(self.resolve(path))?)
}
fn put_file_non_atomic(
&self,
path: &str,
bytes: &[u8],
create_parent_dir: bool,
) -> Result<(), TransportError> {
let full = self.resolve(path);
if create_parent_dir {
if let Some(parent) = full.parent() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::write(full, bytes)?;
Ok(())
}
fn put_bytes(&self, path: &str, bytes: &[u8], mode: Option<u32>) -> Result<(), TransportError> {
let _ = mode;
let full = self.resolve(path);
let parent = full.parent().ok_or_else(|| {
TransportError::Other(format!("path has no parent directory: {path}"))
})?;
let tmp = parent.join(format!(".{}.tmp", crate::osutils::rand_chars(16)));
std::fs::write(&tmp, bytes)?;
if let Err(e) = std::fs::rename(&tmp, &full) {
let _ = std::fs::remove_file(&tmp);
return Err(e.into());
}
Ok(())
}
fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
use std::io::{Seek, Write};
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(self.resolve(path))?;
let offset = f.seek(std::io::SeekFrom::End(0))?;
f.write_all(bytes)?;
Ok(offset)
}
fn mkdir(&self, path: &str) -> Result<(), TransportError> {
match std::fs::create_dir(self.resolve(path)) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => Ok(()),
Err(e) => Err(e.into()),
}
}
fn has(&self, path: &str) -> Result<bool, TransportError> {
Ok(self.resolve(path).exists())
}
fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
let mut out = Vec::new();
let mut stack = vec![self.root.clone()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir)? {
let entry = entry?;
let p = entry.path();
if p.is_dir() {
stack.push(p);
} else if let Ok(rel) = p.strip_prefix(&self.root) {
let rel = rel.to_string_lossy().replace('\\', "/");
out.push(crate::key_mapper::url_quote(&rel));
}
}
}
Ok(out)
}
fn abspath(&self, path: &str) -> Result<String, TransportError> {
Ok(self.resolve(path).to_string_lossy().into_owned())
}
fn rename(&self, from: &str, to: &str) -> Result<(), TransportError> {
let to_path = self.resolve(to);
if to_path.exists() {
return Err(TransportError::Io {
kind: std::io::ErrorKind::AlreadyExists,
message: format!("rename target already exists: {to}"),
});
}
std::fs::rename(self.resolve(from), to_path)?;
Ok(())
}
fn delete(&self, path: &str) -> Result<(), TransportError> {
std::fs::remove_file(self.resolve(path))?;
Ok(())
}
fn rmdir(&self, path: &str) -> Result<(), TransportError> {
std::fs::remove_dir(self.resolve(path))?;
Ok(())
}
fn list_dir(&self, path: &str) -> Result<Vec<String>, TransportError> {
let mut out = Vec::new();
for entry in std::fs::read_dir(self.resolve(path))? {
out.push(entry?.file_name().to_string_lossy().into_owned());
}
Ok(out)
}
fn stat(&self, path: &str) -> Result<Stat, TransportError> {
let meta = std::fs::metadata(self.resolve(path))?;
Ok(Stat {
size: meta.len(),
is_dir: meta.is_dir(),
})
}
fn subtransport(&self, path: &str) -> Result<SharedTransport, TransportError> {
Ok(std::sync::Arc::new(LocalTransport::new(self.resolve(path))))
}
fn local_path(&self, path: &str) -> Option<std::path::PathBuf> {
Some(self.resolve(path))
}
}
#[cfg(test)]
pub(crate) mod testing {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Default)]
pub struct MemoryTransport {
files: Mutex<HashMap<String, Vec<u8>>>,
root: String,
}
impl MemoryTransport {
pub fn new() -> Self {
Self {
files: Mutex::new(HashMap::new()),
root: "memory:///".to_string(),
}
}
}
impl Transport for MemoryTransport {
fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
let files = self.files.lock().unwrap();
files
.get(path)
.cloned()
.ok_or_else(|| TransportError::NoSuchFile(path.to_string()))
}
fn put_file_non_atomic(
&self,
path: &str,
bytes: &[u8],
_create_parent_dir: bool,
) -> Result<(), TransportError> {
let mut files = self.files.lock().unwrap();
files.insert(path.to_string(), bytes.to_vec());
Ok(())
}
fn mkdir(&self, _path: &str) -> Result<(), TransportError> {
Ok(())
}
fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
let mut files = self.files.lock().unwrap();
let entry = files.entry(path.to_string()).or_default();
let offset = entry.len() as u64;
entry.extend_from_slice(bytes);
Ok(offset)
}
fn has(&self, path: &str) -> Result<bool, TransportError> {
let files = self.files.lock().unwrap();
Ok(files.contains_key(path))
}
fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
let files = self.files.lock().unwrap();
Ok(files.keys().cloned().collect())
}
fn abspath(&self, path: &str) -> Result<String, TransportError> {
Ok(format!("{}{}", self.root, path))
}
}
#[test]
fn memory_transport_basic_round_trip() {
let t = MemoryTransport::new();
assert!(!t.has("foo").unwrap());
t.put_file_non_atomic("foo", b"hello", false).unwrap();
assert!(t.has("foo").unwrap());
assert_eq!(t.get_bytes("foo").unwrap(), b"hello".to_vec());
}
#[test]
fn memory_transport_append_returns_offset() {
let t = MemoryTransport::new();
assert_eq!(t.append_bytes("log", b"first ").unwrap(), 0);
assert_eq!(t.append_bytes("log", b"second").unwrap(), 6);
assert_eq!(t.get_bytes("log").unwrap(), b"first second".to_vec());
}
#[test]
fn memory_transport_get_bytes_missing_is_error() {
let t = MemoryTransport::new();
assert_eq!(
t.get_bytes("nope").unwrap_err(),
TransportError::NoSuchFile("nope".to_string())
);
}
#[test]
fn default_readv_slices_via_get_bytes() {
let t = MemoryTransport::new();
t.put_file_non_atomic("data", b"0123456789", false).unwrap();
let ranges = vec![
ReadRange {
offset: 0,
length: 3,
},
ReadRange {
offset: 5,
length: 2,
},
];
let results = t.readv("data", &ranges).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].bytes, b"012".to_vec());
assert_eq!(results[1].bytes, b"56".to_vec());
}
#[test]
fn default_readv_rejects_past_end() {
let t = MemoryTransport::new();
t.put_file_non_atomic("data", b"hi", false).unwrap();
let err = t
.readv(
"data",
&[ReadRange {
offset: 0,
length: 100,
}],
)
.unwrap_err();
assert!(matches!(err, TransportError::Other(_)));
}
}