use crate::storage::{Error, ErrorKind, Fileinfo, Metadata, Result, StorageBackend};
use futures::{future, Future, Stream};
use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
pub struct Filesystem {
root: PathBuf,
}
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 {
Filesystem { root: root.into() }
}
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 = match canonicalize(full_path) {
Ok(path) => path,
Err(e) => return Err(e),
};
if real_full_path.starts_with(&self.root) {
Ok(real_full_path)
} else {
Err(Error::from(ErrorKind::PermanentFileNotAvailable))
}
}
}
impl<U: Send> StorageBackend<U> for Filesystem {
type File = tokio::fs::File;
type Metadata = std::fs::Metadata;
fn supported_features(&self) -> u32 {
crate::storage::FEATURE_RESTART
}
fn metadata<P: AsRef<Path>>(&self, _user: &Option<U>, path: P) -> Box<dyn Future<Item = Self::Metadata, Error = Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(err) => return Box::new(future::err(err)),
};
Box::new(tokio::fs::symlink_metadata(full_path).map_err(|_| Error::from(ErrorKind::PermanentFileNotAvailable)))
}
fn list<P: AsRef<Path>>(&self, _user: &Option<U>, path: P) -> Box<dyn Stream<Item = Fileinfo<std::path::PathBuf, Self::Metadata>, Error = Error> + Send>
where
<Self as StorageBackend<U>>::Metadata: Metadata,
{
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(future::err(e).into_stream()),
};
let prefix = self.root.clone();
let fut = tokio::fs::read_dir(full_path).flatten_stream().filter_map(move |dir_entry| {
let prefix = prefix.clone();
let path = dir_entry.path();
let relpath = path.strip_prefix(prefix).unwrap();
let relpath = std::path::PathBuf::from(relpath);
match std::fs::symlink_metadata(dir_entry.path()) {
Ok(stat) => Some(Fileinfo { path: relpath, metadata: stat }),
Err(_) => None,
}
});
Box::new(fut.map_err(|_| Error::from(ErrorKind::PermanentFileNotAvailable)))
}
fn get<P: AsRef<Path>>(&self, _user: &Option<U>, path: P, start_pos: u64) -> Box<dyn Future<Item = tokio::fs::File, Error = Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(future::err(e)),
};
Box::new(
tokio::fs::file::File::open(full_path)
.and_then(move |file| file.seek(std::io::SeekFrom::Start(start_pos)))
.map(|res| res.0)
.map_err(|error| match error.kind() {
std::io::ErrorKind::NotFound => Error::from(ErrorKind::PermanentFileNotAvailable),
std::io::ErrorKind::PermissionDenied => Error::from(ErrorKind::PermissionDenied),
_ => Error::from(ErrorKind::LocalError),
}),
)
}
fn put<P: AsRef<Path>, R: tokio::prelude::AsyncRead + Send + 'static>(
&self,
_user: &Option<U>,
bytes: R,
path: P,
start_pos: u64,
) -> Box<dyn Future<Item = u64, Error = Error> + Send> {
let path = path.as_ref();
let full_path = if path.starts_with("/") {
self.root.join(path.strip_prefix("/").unwrap())
} else {
self.root.join(path)
};
use futures::future::IntoFuture;
let file = std::fs::OpenOptions::new().write(true).create(true).open(full_path);
let fut = file
.into_future()
.map(tokio::fs::file::File::from_std)
.and_then(move |mut file| file.poll_set_len(start_pos).map(|_| file))
.and_then(move |file| file.seek(std::io::SeekFrom::Start(start_pos)))
.map(|f| f.0)
.and_then(|f| tokio_io::io::copy(bytes, f))
.map(|(n, _, _)| n)
.map_err(|error| match error.kind() {
std::io::ErrorKind::NotFound => Error::from(ErrorKind::PermanentFileNotAvailable),
std::io::ErrorKind::PermissionDenied => Error::from(ErrorKind::PermissionDenied),
_ => Error::from(ErrorKind::LocalError),
});
Box::new(fut)
}
fn del<P: AsRef<Path>>(&self, _user: &Option<U>, path: P) -> Box<dyn Future<Item = (), Error = Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(_) => return Box::new(future::err(Error::from(ErrorKind::PermanentFileNotAvailable))),
};
Box::new(tokio::fs::remove_file(full_path).map_err(|error| match error.kind() {
std::io::ErrorKind::NotFound => Error::from(ErrorKind::PermanentFileNotAvailable),
std::io::ErrorKind::PermissionDenied => Error::from(ErrorKind::PermissionDenied),
_ => Error::from(ErrorKind::LocalError),
}))
}
fn rmd<P: AsRef<Path>>(&self, _user: &Option<U>, path: P) -> Box<dyn Future<Item = (), Error = Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(future::err(e)),
};
Box::new(tokio::fs::remove_dir(full_path).map_err(|error| match error.kind() {
std::io::ErrorKind::NotFound => Error::from(ErrorKind::PermanentFileNotAvailable),
std::io::ErrorKind::PermissionDenied => Error::from(ErrorKind::PermissionDenied),
_ => Error::from(ErrorKind::LocalError),
}))
}
fn mkd<P: AsRef<Path>>(&self, _user: &Option<U>, path: P) -> Box<dyn Future<Item = (), Error = Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(future::err(e)),
};
Box::new(tokio::fs::create_dir(full_path).map_err(|error| match error.kind() {
std::io::ErrorKind::NotFound => Error::from(ErrorKind::PermanentFileNotAvailable),
std::io::ErrorKind::PermissionDenied => Error::from(ErrorKind::PermissionDenied),
_ => Error::from(ErrorKind::LocalError),
}))
}
fn rename<P: AsRef<Path>>(&self, _user: &Option<U>, from: P, to: P) -> Box<dyn Future<Item = (), Error = Error> + Send> {
let from = match self.full_path(from) {
Ok(path) => path,
Err(e) => return Box::new(future::err(e)),
};
let to = match self.full_path(to) {
Ok(path) => path,
Err(e) => return Box::new(future::err(e)),
};
let from_rename = from.clone(); let fut = tokio::fs::symlink_metadata(from)
.map_err(|_| Error::from(ErrorKind::PermanentFileNotAvailable))
.and_then(move |metadata| {
if metadata.is_file() {
future::Either::A(tokio::fs::rename(from_rename, to).map_err(|_| Error::from(ErrorKind::PermanentFileNotAvailable)))
} else {
future::Either::B(future::err(Error::from(ErrorKind::PermanentFileNotAvailable)))
}
});
Box::new(fut)
}
}
impl Metadata for std::fs::Metadata {
fn len(&self) -> u64 {
self.len()
}
fn is_dir(&self) -> bool {
self.is_dir()
}
fn is_file(&self) -> bool {
self.is_file()
}
fn is_symlink(&self) -> bool {
self.file_type().is_symlink()
}
fn modified(&self) -> Result<SystemTime> {
self.modified().map_err(|_| Error::from(ErrorKind::PermanentFileNotAvailable))
}
fn gid(&self) -> u32 {
MetadataExt::gid(self)
}
fn uid(&self) -> u32 {
MetadataExt::uid(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::AnonymousUser;
use pretty_assertions::assert_eq;
use std::fs::File;
use std::io::prelude::*;
use std::io::Write;
#[test]
fn fs_stat() {
let root = std::env::temp_dir();
let file = tempfile::NamedTempFile::new_in(&root).unwrap();
let path = file.path();
let file = file.as_file();
let meta = file.metadata().unwrap();
let fs = Filesystem::new(&root);
let mut rt = tokio::runtime::Runtime::new().unwrap();
let filename = path.file_name().unwrap();
let my_meta = rt.block_on(fs.metadata(&Some(AnonymousUser {}), filename)).unwrap();
assert_eq!(meta.is_dir(), my_meta.is_dir());
assert_eq!(meta.is_file(), my_meta.is_file());
assert_eq!(meta.is_symlink(), my_meta.is_symlink());
assert_eq!(meta.len(), my_meta.len());
assert_eq!(meta.modified().unwrap(), my_meta.modified().unwrap());
}
#[test]
fn fs_list() {
let root = tempfile::tempdir().unwrap();
let file = tempfile::NamedTempFile::new_in(&root.path()).unwrap();
let path = file.path();
let relpath = path.strip_prefix(&root.path()).unwrap();
let file = file.as_file();
let meta = file.metadata().unwrap();
let fs = Filesystem::new(&root.path());
let mut rt = tokio::runtime::Runtime::new().unwrap();
let my_list = rt.block_on(fs.list(&Some(AnonymousUser {}), "/").collect()).unwrap();
assert_eq!(my_list.len(), 1);
let my_fileinfo = &my_list[0];
assert_eq!(my_fileinfo.path, relpath);
assert_eq!(my_fileinfo.metadata.is_dir(), meta.is_dir());
assert_eq!(my_fileinfo.metadata.is_file(), meta.is_file());
assert_eq!(my_fileinfo.metadata.is_symlink(), meta.is_symlink());
assert_eq!(my_fileinfo.metadata.len(), meta.len());
assert_eq!(my_fileinfo.metadata.modified().unwrap(), meta.modified().unwrap());
}
#[test]
fn fs_list_fmt() {
let root = tempfile::tempdir().unwrap();
let file = tempfile::NamedTempFile::new_in(&root.path()).unwrap();
let path = file.path();
let relpath = path.strip_prefix(&root.path()).unwrap();
let fs = Filesystem::new(&root.path());
let mut rt = tokio::runtime::Runtime::new().unwrap();
let my_list = rt.block_on(fs.list_fmt(&Some(AnonymousUser {}), "/")).unwrap();
let my_list = std::string::String::from_utf8(my_list.into_inner()).unwrap();
assert!(my_list.contains(relpath.to_str().unwrap()));
}
#[test]
fn fs_get() {
let root = std::env::temp_dir();
let mut file = tempfile::NamedTempFile::new_in(&root).unwrap();
let path = file.path().to_owned();
let data = b"Koen was here\n";
file.write_all(data).unwrap();
let filename = path.file_name().unwrap();
let fs = Filesystem::new(&root);
let mut rt = tokio::runtime::Runtime::new().unwrap();
let mut my_file = rt.block_on(fs.get(&Some(AnonymousUser {}), filename, 0)).unwrap();
let mut my_content = Vec::new();
rt.block_on(future::lazy(move || {
tokio::prelude::AsyncRead::read_to_end(&mut my_file, &mut my_content).unwrap();
assert_eq!(data.as_ref(), &*my_content);
if true {
Ok(())
} else {
Err(())
}
}))
.unwrap();
}
#[test]
fn fs_put() {
let root = std::env::temp_dir();
let orig_content = b"hallo";
let fs = Filesystem::new(&root);
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(fs.put(&Some(AnonymousUser {}), orig_content.as_ref(), "greeting.txt", 0))
.expect("Failed to `put` file");
let mut written_content = Vec::new();
let mut f = File::open(root.join("greeting.txt")).unwrap();
f.read_to_end(&mut written_content).unwrap();
assert_eq!(orig_content, written_content.as_slice());
}
#[test]
fn fileinfo_fmt() {
struct MockMetadata {};
impl Metadata for MockMetadata {
fn len(&self) -> u64 {
5
}
fn is_empty(&self) -> bool {
false
}
fn is_dir(&self) -> bool {
false
}
fn is_file(&self) -> bool {
true
}
fn is_symlink(&self) -> bool {
false
}
fn modified(&self) -> Result<SystemTime> {
Ok(std::time::SystemTime::UNIX_EPOCH)
}
fn uid(&self) -> u32 {
1
}
fn gid(&self) -> u32 {
2
}
}
let dir = std::env::temp_dir();
let meta = MockMetadata {};
let fileinfo = Fileinfo {
path: dir.to_str().unwrap(),
metadata: meta,
};
let my_format = format!("{}", fileinfo);
let basename = std::path::Path::new(&dir).file_name().unwrap().to_string_lossy();
let format = format!("-rwxr-xr-x 1 2 5 Jan 01 00:00 {}", basename);
assert_eq!(my_format, format);
}
#[test]
fn fs_mkd() {
let root = tempfile::TempDir::new().unwrap().into_path();
let fs = Filesystem::new(&root);
let new_dir_name = "bla";
let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(fs.mkd(&Some(AnonymousUser {}), new_dir_name)).expect("Failed to mkd");
let full_path = root.join(new_dir_name);
let metadata = std::fs::symlink_metadata(full_path).unwrap();
assert!(metadata.is_dir());
}
#[test]
fn fs_rename() {
let root = tempfile::TempDir::new().unwrap().into_path();
let file = tempfile::NamedTempFile::new_in(&root).unwrap();
let old_filename = file.path().file_name().unwrap().to_str().unwrap();
let new_filename = "hello.txt";
let mut rt = tokio::runtime::Runtime::new().unwrap();
let fs = Filesystem::new(&root);
rt.block_on(fs.rename(&Some(AnonymousUser {}), &old_filename, &new_filename))
.expect("Failed to rename");
let new_full_path = root.join(new_filename);
assert!(std::fs::metadata(new_full_path).expect("new filename not found").is_file());
let old_full_path = root.join(old_filename);
std::fs::symlink_metadata(old_full_path).expect_err("Old filename should not exists anymore");
}
}