mod ext;
pub use ext::ServerExt;
use async_trait::async_trait;
use cfg_if::cfg_if;
use libunftp::auth::UserDetail;
use libunftp::storage::{Error, ErrorKind, Fileinfo, Metadata, Result, StorageBackend};
use std::{
fmt::Debug,
path::{Path, PathBuf},
time::SystemTime,
};
cfg_if! {
if #[cfg(target_os = "linux")] {
use std::os::linux::fs::MetadataExt;
} else if #[cfg(target_os = "unix")] {
use std::os::unix::fs::MetadataExt;
}
}
#[derive(Debug)]
pub struct Filesystem {
root: PathBuf,
}
#[derive(Debug)]
pub struct Meta {
inner: std::fs::Metadata,
}
fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
use path_abs::PathAbs;
let p = PathAbs::new(path).map_err(|_| Error::from(ErrorKind::FileNameNotAllowedError))?;
Ok(p.as_path().to_path_buf())
}
impl Filesystem {
pub fn new<P: Into<PathBuf>>(root: P) -> Self {
let path = root.into();
Filesystem {
root: canonicalize(&path).unwrap_or(path),
}
}
async fn full_path<P: AsRef<Path>>(&self, path: P) -> Result<PathBuf> {
let path = path.as_ref();
let full_path = if path.starts_with("/") {
self.root.join(path.strip_prefix("/").unwrap())
} else {
self.root.join(path)
};
let real_full_path = tokio::task::spawn_blocking(move || canonicalize(full_path))
.await
.map_err(|e| Error::new(ErrorKind::LocalError, e))??;
if real_full_path.starts_with(&self.root) {
Ok(real_full_path)
} else {
Err(Error::from(ErrorKind::PermanentFileNotAvailable))
}
}
}
#[async_trait]
impl<User: UserDetail> StorageBackend<User> for Filesystem {
type Metadata = Meta;
fn supported_features(&self) -> u32 {
libunftp::storage::FEATURE_RESTART | libunftp::storage::FEATURE_SITEMD5
}
#[tracing_attributes::instrument]
async fn metadata<P: AsRef<Path> + Send + Debug>(&self, _user: &User, path: P) -> Result<Self::Metadata> {
let full_path = self.full_path(path).await?;
let fs_meta = tokio::fs::symlink_metadata(full_path)
.await
.map_err(|_| Error::from(ErrorKind::PermanentFileNotAvailable))?;
Ok(Meta { inner: fs_meta })
}
#[allow(clippy::type_complexity)]
#[tracing_attributes::instrument]
async fn list<P>(&self, _user: &User, path: P) -> Result<Vec<Fileinfo<std::path::PathBuf, Self::Metadata>>>
where
P: AsRef<Path> + Send + Debug,
<Self as StorageBackend<User>>::Metadata: Metadata,
{
let full_path: PathBuf = self.full_path(path).await?;
let prefix: PathBuf = self.root.clone();
let mut rd: tokio::fs::ReadDir = tokio::fs::read_dir(full_path).await?;
let mut fis: Vec<Fileinfo<std::path::PathBuf, Self::Metadata>> = vec![];
while let Ok(Some(dir_entry)) = rd.next_entry().await {
let prefix = prefix.clone();
let path = dir_entry.path();
let relpath = path.strip_prefix(prefix).unwrap();
let relpath: PathBuf = std::path::PathBuf::from(relpath);
let metadata = tokio::fs::symlink_metadata(dir_entry.path()).await?;
let meta: Self::Metadata = Meta { inner: metadata };
fis.push(Fileinfo { path: relpath, metadata: meta })
}
Ok(fis)
}
async fn get<P: AsRef<Path> + Send + Debug>(&self, _user: &User, path: P, start_pos: u64) -> Result<Box<dyn tokio::io::AsyncRead + Send + Sync + Unpin>> {
use tokio::io::AsyncSeekExt;
let full_path = self.full_path(path).await?;
let mut file = tokio::fs::File::open(full_path).await?;
if start_pos > 0 {
file.seek(std::io::SeekFrom::Start(start_pos)).await?;
}
Ok(Box::new(tokio::io::BufReader::with_capacity(4096, file)) as Box<dyn tokio::io::AsyncRead + Send + Sync + Unpin>)
}
async fn put<P: AsRef<Path> + Send, R: tokio::io::AsyncRead + Send + Sync + 'static + Unpin>(
&self,
_user: &User,
bytes: R,
path: P,
start_pos: u64,
) -> Result<u64> {
use tokio::io::AsyncSeekExt;
let path = path.as_ref();
let full_path = if path.starts_with("/") {
self.root.join(path.strip_prefix("/").unwrap())
} else {
self.root.join(path)
};
let mut file = tokio::fs::OpenOptions::new().write(true).create(true).open(full_path).await?;
file.set_len(start_pos).await?;
file.seek(std::io::SeekFrom::Start(start_pos)).await?;
let mut reader = tokio::io::BufReader::with_capacity(4096, bytes);
let mut writer = tokio::io::BufWriter::with_capacity(4096, file);
let bytes_copied = tokio::io::copy(&mut reader, &mut writer).await?;
Ok(bytes_copied)
}
#[tracing_attributes::instrument]
async fn del<P: AsRef<Path> + Send + Debug>(&self, _user: &User, path: P) -> Result<()> {
let full_path = self.full_path(path).await?;
tokio::fs::remove_file(full_path).await.map_err(|error: std::io::Error| error.into())
}
#[tracing_attributes::instrument]
async fn rmd<P: AsRef<Path> + Send + Debug>(&self, _user: &User, path: P) -> Result<()> {
let full_path = self.full_path(path).await?;
tokio::fs::remove_dir(full_path).await.map_err(|error: std::io::Error| error.into())
}
#[tracing_attributes::instrument]
async fn mkd<P: AsRef<Path> + Send + Debug>(&self, _user: &User, path: P) -> Result<()> {
tokio::fs::create_dir(self.full_path(path).await?)
.await
.map_err(|error: std::io::Error| error.into())
}
#[tracing_attributes::instrument]
async fn rename<P: AsRef<Path> + Send + Debug>(&self, _user: &User, from: P, to: P) -> Result<()> {
let from = self.full_path(from).await?;
let to = self.full_path(to).await?;
let from_rename = from.clone();
let r = tokio::fs::symlink_metadata(from).await;
match r {
Ok(metadata) => {
if metadata.is_file() || metadata.is_dir() {
let r = tokio::fs::rename(from_rename, to).await;
match r {
Ok(_) => Ok(()),
Err(e) => Err(Error::new(ErrorKind::PermanentFileNotAvailable, e)),
}
} else {
Err(Error::from(ErrorKind::PermanentFileNotAvailable))
}
}
Err(e) => Err(Error::new(ErrorKind::PermanentFileNotAvailable, e)),
}
}
#[tracing_attributes::instrument]
async fn cwd<P: AsRef<Path> + Send + Debug>(&self, _user: &User, path: P) -> Result<()> {
let full_path = self.full_path(path).await?;
tokio::fs::read_dir(full_path).await.map_err(|error: std::io::Error| error.into()).map(|_| ())
}
}
impl Metadata for Meta {
fn len(&self) -> u64 {
self.inner.len()
}
fn is_dir(&self) -> bool {
self.inner.is_dir()
}
fn is_file(&self) -> bool {
self.inner.is_file()
}
fn is_symlink(&self) -> bool {
self.inner.file_type().is_symlink()
}
fn modified(&self) -> Result<SystemTime> {
self.inner.modified().map_err(|e| e.into())
}
fn gid(&self) -> u32 {
cfg_if! {
if #[cfg(target_os = "linux")] {
self.inner.st_gid()
}
else if #[cfg(target_os = "unix")] {
self.inner.gid()
} else {
0
}
}
}
fn uid(&self) -> u32 {
cfg_if! {
if #[cfg(target_os = "linux")] {
self.inner.st_uid()
}
else if #[cfg(target_os = "unix")] {
self.inner.uid()
} else {
0
}
}
}
fn links(&self) -> u64 {
cfg_if! {
if #[cfg(target_os = "linux")] {
self.inner.st_nlink()
}
else if #[cfg(target_os = "unix")] {
self.inner.nlink()
} else {
1
}
}
}
}
#[cfg(test)]
mod tests;