use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use futures::io::AsyncRead;
use pin_project_lite::pin_project;
pub enum FileInput {
Path(PathBuf),
Bytes {
data: Vec<u8>,
filename: Option<String>,
},
Stream {
reader: Arc<tokio::sync::Mutex<Box<dyn AsyncRead + Send + Unpin>>>,
size_hint: Option<u64>,
filename: Option<String>,
},
}
impl std::fmt::Debug for FileInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Path(path) => f.debug_tuple("Path").field(path).finish(),
Self::Bytes { data, filename } => f
.debug_struct("Bytes")
.field("data_len", &data.len())
.field("filename", filename)
.finish(),
Self::Stream {
size_hint,
filename,
..
} => f
.debug_struct("Stream")
.field("size_hint", size_hint)
.field("filename", filename)
.finish_non_exhaustive(),
}
}
}
impl Clone for FileInput {
fn clone(&self) -> Self {
match self {
Self::Path(path) => Self::Path(path.clone()),
Self::Bytes { data, filename } => Self::Bytes {
data: data.clone(),
filename: filename.clone(),
},
Self::Stream {
reader,
size_hint,
filename,
} => Self::Stream {
reader: Arc::clone(reader),
size_hint: *size_hint,
filename: filename.clone(),
},
}
}
}
impl FileInput {
pub fn from_path(path: impl Into<PathBuf>) -> Self {
Self::Path(path.into())
}
pub fn from_bytes(data: impl Into<Vec<u8>>) -> Self {
Self::Bytes {
data: data.into(),
filename: None,
}
}
pub fn from_stream(reader: impl AsyncRead + Send + Unpin + 'static) -> Self {
Self::Stream {
reader: Arc::new(tokio::sync::Mutex::new(Box::new(reader))),
size_hint: None,
filename: None,
}
}
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
match &mut self {
Self::Bytes { filename: f, .. } => *f = Some(filename.into()),
Self::Stream { filename: f, .. } => *f = Some(filename.into()),
Self::Path(_) => {} }
self
}
pub fn with_size_hint(mut self, size: u64) -> Self {
if let Self::Stream { size_hint, .. } = &mut self {
*size_hint = Some(size);
}
self
}
pub fn filename(&self) -> Option<&str> {
match self {
Self::Path(path) => path.file_name().and_then(|n| n.to_str()),
Self::Bytes { filename, .. } => filename.as_deref(),
Self::Stream { filename, .. } => filename.as_deref(),
}
}
pub fn size_hint(&self) -> Option<u64> {
match self {
Self::Path(_) => None, Self::Bytes { data, .. } => Some(data.len() as u64),
Self::Stream { size_hint, .. } => *size_hint,
}
}
pub fn is_path(&self) -> bool {
matches!(self, Self::Path(_))
}
pub fn is_bytes(&self) -> bool {
matches!(self, Self::Bytes { .. })
}
pub fn is_stream(&self) -> bool {
matches!(self, Self::Stream { .. })
}
pub fn as_path(&self) -> Option<&Path> {
match self {
Self::Path(path) => Some(path),
_ => None,
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Self::Bytes { data, .. } => Some(data),
_ => None,
}
}
}
impl From<PathBuf> for FileInput {
fn from(path: PathBuf) -> Self {
Self::Path(path)
}
}
impl From<&Path> for FileInput {
fn from(path: &Path) -> Self {
Self::Path(path.to_path_buf())
}
}
impl From<&str> for FileInput {
fn from(path: &str) -> Self {
Self::Path(PathBuf::from(path))
}
}
impl From<String> for FileInput {
fn from(path: String) -> Self {
Self::Path(PathBuf::from(path))
}
}
impl From<Vec<u8>> for FileInput {
fn from(data: Vec<u8>) -> Self {
Self::from_bytes(data)
}
}
impl From<&[u8]> for FileInput {
fn from(data: &[u8]) -> Self {
Self::from_bytes(data.to_vec())
}
}
pin_project! {
pub struct BytesReader {
data: Vec<u8>,
position: usize,
}
}
impl BytesReader {
pub fn new(data: Vec<u8>) -> Self {
Self { data, position: 0 }
}
}
impl AsyncRead for BytesReader {
fn poll_read(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &mut [u8],
) -> Poll<std::io::Result<usize>> {
let this = self.project();
let remaining = &this.data[*this.position..];
let to_copy = std::cmp::min(buf.len(), remaining.len());
buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
*this.position += to_copy;
Poll::Ready(Ok(to_copy))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_input_from_path() {
let input = FileInput::from_path("/test/file.exe");
assert!(input.is_path());
assert_eq!(input.filename(), Some("file.exe"));
assert_eq!(input.as_path(), Some(Path::new("/test/file.exe")));
}
#[test]
fn test_file_input_from_bytes() {
let data = vec![1, 2, 3, 4];
let input = FileInput::from_bytes(data.clone()).with_filename("test.bin");
assert!(input.is_bytes());
assert_eq!(input.filename(), Some("test.bin"));
assert_eq!(input.as_bytes(), Some(data.as_slice()));
assert_eq!(input.size_hint(), Some(4));
}
#[test]
fn test_file_input_conversions() {
let _: FileInput = PathBuf::from("/test").into();
let _: FileInput = "/test".into();
let _: FileInput = String::from("/test").into();
let _: FileInput = vec![1u8, 2, 3].into();
let _: FileInput = [1u8, 2, 3].as_slice().into();
}
}