use async_trait::async_trait;
use path_clean::PathClean;
use std::io::Result;
use std::path::{Path as StdPath, PathBuf as StdPathBuf};
use std::task::Poll;
use thiserror::Error;
use crate::types::{OpenOptions, OpenOptionsInner};
use crate::{
path::{Path as LunchboxPath, PathBuf as LunchboxPathBuf},
types::{
DirEntry, HasFileType, Metadata, PathType, Permissions, ReadDir, ReadDirPoller,
ReadableFile, WritableFile,
},
ReadableFileSystem, WritableFileSystem,
};
#[derive(Error, Debug, PartialEq)]
pub enum Error {
#[error("Base directory doesn't exist or is not a directory: {dir}")]
InvalidBaseDir { dir: StdPathBuf },
#[error("Path ({req}) was not within the base directory ({base})")]
PathTranslationError { base: StdPathBuf, req: StdPathBuf },
}
#[derive(PartialEq, Debug)]
pub struct LocalFS {
base_dir: StdPathBuf,
}
impl LocalFS {
pub fn new() -> std::result::Result<LocalFS, Error> {
Self::with_base_dir_unchecked("/")
}
pub async fn with_base_dir(
base_dir: impl Into<StdPathBuf>,
) -> std::result::Result<LocalFS, Error> {
let base_dir: StdPathBuf = base_dir.into();
let is_dir = tokio::fs::metadata(&base_dir)
.await
.map(|metadata| metadata.is_dir())
.unwrap_or(false);
if !is_dir {
return Err(Error::InvalidBaseDir { dir: base_dir });
}
Ok(LocalFS { base_dir })
}
pub fn with_base_dir_unchecked(
base_dir: impl Into<StdPathBuf>,
) -> std::result::Result<LocalFS, Error> {
let base_dir: StdPathBuf = base_dir.into();
Ok(LocalFS { base_dir })
}
fn to_std_path(&self, path: impl AsRef<LunchboxPath>) -> Result<StdPathBuf> {
let out = path.as_ref().to_path(&self.base_dir).clean();
if out.starts_with(&self.base_dir) {
Ok(out)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
Error::PathTranslationError {
base: self.base_dir.clone(),
req: out,
},
))
}
}
fn from_std_path(&self, path: impl AsRef<StdPath>) -> Result<LunchboxPathBuf> {
let path = path.as_ref();
if let Ok(relative) = path.strip_prefix(&self.base_dir) {
LunchboxPathBuf::from_path(relative)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
Error::PathTranslationError {
base: self.base_dir.clone(),
req: path.to_path_buf(),
},
))
}
}
}
impl HasFileType for LocalFS {
type FileType = tokio::fs::File;
}
#[async_trait]
impl ReadableFile for tokio::fs::File {
async fn metadata(&self) -> Result<Metadata> {
Ok(tokio::fs::File::metadata(&self).await?.into())
}
async fn try_clone(&self) -> Result<Self> {
tokio::fs::File::try_clone(&self).await
}
}
#[async_trait]
impl WritableFile for tokio::fs::File {
async fn sync_all(&self) -> Result<()> {
tokio::fs::File::sync_all(&self).await
}
async fn sync_data(&self) -> Result<()> {
tokio::fs::File::sync_data(&self).await
}
async fn set_len(&self, size: u64) -> Result<()> {
tokio::fs::File::set_len(&self, size).await
}
async fn set_permissions(&self, perm: Permissions) -> Result<()> {
let m = tokio::fs::File::metadata(&self).await?;
let mut permissions = m.permissions();
permissions.set_readonly(perm.readonly());
tokio::fs::File::set_permissions(&self, permissions).await
}
}
#[async_trait]
impl ReadableFileSystem for LocalFS {
async fn open(&self, path: impl PathType) -> std::io::Result<Self::FileType> {
let path = self.to_std_path(path)?;
tokio::fs::File::open(path).await
}
async fn canonicalize(&self, path: impl PathType) -> Result<LunchboxPathBuf> {
let path = self.to_std_path(path)?;
self.from_std_path(tokio::fs::canonicalize(path).await?)
}
async fn metadata(&self, path: impl PathType) -> Result<Metadata> {
let f = self.open(path).await?;
<tokio::fs::File as ReadableFile>::metadata(&f).await
}
async fn read(&self, path: impl PathType) -> Result<Vec<u8>> {
let path = self.to_std_path(path)?;
tokio::fs::read(path).await
}
type ReadDirPollerType = LocalFSReadDirPoller;
async fn read_dir(
&self,
path: impl PathType,
) -> Result<ReadDir<Self::ReadDirPollerType, Self>> {
let path = self.to_std_path(path)?;
let native = tokio::fs::read_dir(path).await?;
let poller = LocalFSReadDirPoller { inner: native };
Ok(ReadDir::new(poller, self))
}
async fn read_link(&self, path: impl PathType) -> Result<LunchboxPathBuf> {
let path = self.to_std_path(path)?;
let target = tokio::fs::read_link(&path).await?;
if target.is_absolute() {
self.from_std_path(target)
} else {
self.from_std_path(path.parent().unwrap().join(target).clean())
}
}
async fn read_to_string(&self, path: impl PathType) -> Result<String> {
let path = self.to_std_path(path)?;
tokio::fs::read_to_string(path).await
}
async fn symlink_metadata(&self, path: impl PathType) -> Result<Metadata> {
let path = self.to_std_path(path)?;
Ok(tokio::fs::symlink_metadata(path).await?.into())
}
}
#[doc(hidden)]
pub struct LocalFSReadDirPoller {
inner: tokio::fs::ReadDir,
}
impl ReadDirPoller<LocalFS> for LocalFSReadDirPoller {
fn poll_next_entry<'a>(
&mut self,
cx: &mut std::task::Context<'_>,
fs: &'a LocalFS,
) -> Poll<std::io::Result<Option<DirEntry<'a, LocalFS>>>> {
match self.inner.poll_next_entry(cx) {
Poll::Ready(Ok(Some(entry))) => {
let de = DirEntry::new(
fs,
entry.file_name().into_string().unwrap(),
fs.from_std_path(entry.path())?,
);
Poll::Ready(Ok(Some(de)))
}
Poll::Pending => Poll::Pending,
Poll::Ready(Ok(None)) => Poll::Ready(Ok(None)),
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
}
}
}
#[async_trait]
impl WritableFileSystem for LocalFS {
async fn open_with_opts(
&self,
opts: &OpenOptions,
path: impl PathType,
) -> Result<Self::FileType> {
let path = self.to_std_path(path)?;
let inner: OpenOptionsInner = opts.into();
tokio::fs::OpenOptions::new()
.append(inner.append)
.create(inner.create)
.create_new(inner.create_new)
.read(inner.read)
.truncate(inner.truncate)
.write(inner.write)
.open(path)
.await
}
async fn copy(&self, from: impl PathType, to: impl PathType) -> Result<u64> {
let from = self.to_std_path(from)?;
let to = self.to_std_path(to)?;
tokio::fs::copy(from, to).await
}
async fn create_dir(&self, path: impl PathType) -> Result<()> {
let path = self.to_std_path(path)?;
tokio::fs::create_dir(path).await
}
async fn create_dir_all(&self, path: impl PathType) -> Result<()> {
let path = self.to_std_path(path)?;
tokio::fs::create_dir_all(path).await
}
async fn hard_link(&self, src: impl PathType, dst: impl PathType) -> Result<()> {
let src = self.to_std_path(src)?;
let dst = self.to_std_path(dst)?;
tokio::fs::hard_link(src, dst).await
}
async fn remove_dir(&self, path: impl PathType) -> Result<()> {
let path = self.to_std_path(path)?;
tokio::fs::remove_dir(path).await
}
async fn remove_dir_all(&self, path: impl PathType) -> Result<()> {
let path = self.to_std_path(path)?;
tokio::fs::remove_dir_all(path).await
}
async fn remove_file(&self, path: impl PathType) -> Result<()> {
let path = self.to_std_path(path)?;
tokio::fs::remove_file(path).await
}
async fn rename(&self, from: impl PathType, to: impl PathType) -> Result<()> {
let from = self.to_std_path(from)?;
let to = self.to_std_path(to)?;
tokio::fs::rename(from, to).await
}
async fn set_permissions(&self, path: impl PathType, perm: Permissions) -> Result<()> {
let f = self.open(path).await?;
<tokio::fs::File as WritableFile>::set_permissions(&f, perm).await
}
async fn symlink(&self, src: impl PathType, dst: impl PathType) -> Result<()> {
let src = self.to_std_path(src)?;
let dst = self.to_std_path(dst)?;
tokio::fs::symlink(src, dst).await
}
async fn write(&self, path: impl PathType, contents: impl AsRef<[u8]> + Send) -> Result<()> {
let path = self.to_std_path(path)?;
tokio::fs::write(path, contents).await
}
}
#[cfg(test)]
mod tests {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::path::Path as LunchboxPath;
use crate::{LocalFS, ReadableFileSystem, WritableFileSystem};
use std::path::Path as StdPath;
#[tokio::test]
async fn test_base_dir() {
assert!(LocalFS::with_base_dir("").await.is_err());
assert!(LocalFS::with_base_dir("/mostlikelynotarealdirectory")
.await
.is_err());
assert!(LocalFS::with_base_dir("/").await.is_ok());
}
#[tokio::test]
async fn test_new() {
assert_eq!(LocalFS::new(), LocalFS::with_base_dir("/").await);
assert_eq!(LocalFS::new(), LocalFS::with_base_dir_unchecked("/"));
}
#[tokio::test]
async fn test_conversions() {
let fs = LocalFS::with_base_dir("/").await.unwrap();
assert_eq!(fs.to_std_path("/a/b/c").unwrap(), StdPath::new("/a/b/c"));
assert_eq!(fs.to_std_path("a/b/c").unwrap(), StdPath::new("/a/b/c"));
let fs = LocalFS::with_base_dir("/tmp").await.unwrap();
assert_eq!(
fs.to_std_path("/a/b/c").unwrap(),
StdPath::new("/tmp/a/b/c")
);
assert_eq!(fs.to_std_path("a/b/c").unwrap(), StdPath::new("/tmp/a/b/c"));
assert!(fs.to_std_path("../etc/passwd").is_err());
assert!(fs.from_std_path("/etc/passwd").is_err());
assert!(fs.from_std_path("/a/b/c").is_err());
assert!(fs.from_std_path("a/b/c").is_err());
assert_eq!(
fs.from_std_path("/tmp/a/b/c").unwrap(),
LunchboxPath::new("/a/b/c")
);
assert_eq!(
fs.from_std_path("/tmp/a/b/c").unwrap(),
LunchboxPath::new("a/b/c")
);
}
#[tokio::test]
async fn test_basic() {
let fs = LocalFS::with_base_dir("/tmp").await.unwrap();
let mut file = fs.create("applesauce.txt").await.unwrap();
file.write_all(b"some text").await.unwrap();
let mut file = fs.open("applesauce.txt").await.unwrap();
let mut buffer = String::new();
file.read_to_string(&mut buffer).await.unwrap();
assert_eq!(buffer, "some text");
}
}