extern crate std;
extern crate bytes;
extern crate tokio;
extern crate tokio_io;
extern crate futures;
extern crate chrono;
extern crate path_abs;
use std::{fmt,result};
use std::path::{Path,PathBuf};
use std::time::SystemTime;
use self::futures::{Future, Stream};
use self::chrono::prelude::*;
pub trait Metadata {
fn len(&self) -> u64;
fn is_empty(&self) -> bool;
fn is_dir(&self) -> bool;
fn is_file(&self) -> bool;
fn modified(&self) -> Result<SystemTime>;
fn gid(&self) -> u32;
fn uid(&self) -> u32;
}
pub struct Fileinfo<P, M>
where P: AsRef<Path>,
M: Metadata,
{
pub path: P,
pub metadata: M,
}
impl<P, M> std::fmt::Display for Fileinfo<P, M>
where P: AsRef<Path>,
M: Metadata,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let modified: DateTime<Local> = DateTime::from(self.metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH));
write!(f, "{filetype}{permissions} {owner} {group} {size} {modified} {path}",
filetype = if self.metadata.is_dir() {
"d"
} else {
"-"
},
permissions = "rwxr-xr-x",
owner = self.metadata.uid(),
group = self.metadata.gid(),
size = self.metadata.len(),
modified = modified.format("%b %d %Y"),
path = self.path.as_ref().components().last().unwrap().as_os_str().to_string_lossy(),
)
}
}
pub trait StorageBackend {
type File;
type Metadata;
type Error;
fn stat<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = Self::Metadata, Error = Self::Error> + Send>;
fn list<P: AsRef<Path>>(&self, path: P) -> Box<Stream<Item = Fileinfo<std::path::PathBuf, Self::Metadata>, Error = Self::Error> + Send> where <Self as StorageBackend>::Metadata: Metadata;
fn list_fmt<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = std::io::Cursor<Vec<u8>>, Error = std::io::Error> + Send>
where <Self as StorageBackend>::Metadata: Metadata + 'static,
<Self as StorageBackend>::Error: Send + 'static,
{
let res = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let stream: Box<Stream<Item = Fileinfo<std::path::PathBuf, Self::Metadata>, Error = Self::Error> + Send> = self.list(path);
let res_work = res.clone();
let fut = stream.for_each(move |file: Fileinfo<std::path::PathBuf, Self::Metadata>| {
let mut res = res_work.lock().unwrap();
let fmt = format!("{}\r\n", file);
let fmt_vec = fmt.into_bytes();
res.extend_from_slice(&fmt_vec);
Ok(())
}).and_then(|_| {
Ok(())
}).
map(move |_| {
std::sync::Arc::try_unwrap(res).expect("failed try_unwrap").into_inner().unwrap()
}).map(move |res| {
std::io::Cursor::new(res)
}).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "shut up")
});
Box::new(fut)
}
fn nlst<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = std::io::Cursor<Vec<u8>>, Error = std::io::Error> + Send>
where <Self as StorageBackend>::Metadata: Metadata + 'static,
<Self as StorageBackend>::Error: Send + 'static,
{
let res = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let stream: Box<Stream<Item = Fileinfo<std::path::PathBuf, Self::Metadata>, Error = Self::Error> + Send> = self.list(path);
let res_work = res.clone();
let fut = stream.for_each(move |file: Fileinfo<std::path::PathBuf, Self::Metadata>| {
let mut res = res_work.lock().unwrap();
let fmt = format!("{}\r\n", file.path.file_name().unwrap_or(std::ffi::OsStr::new("")).to_str().unwrap_or(""));
let fmt_vec = fmt.into_bytes();
res.extend_from_slice(&fmt_vec);
Ok(())
}).and_then(|_| {
Ok(())
}).
map(move |_| {
std::sync::Arc::try_unwrap(res).expect("failed try_unwrap").into_inner().unwrap()
}).map(move |res| {
std::io::Cursor::new(res)
}).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "shut up")
});
Box::new(fut)
}
fn get<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = Self::File, Error = Self::Error> + Send>;
fn put<P: AsRef<Path>, R: self::tokio::prelude::AsyncRead + Send + 'static>(&self, bytes: R, path: P) -> Box<Future<Item = u64, Error = Self::Error> + Send>;
fn del<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = (), Error = Self::Error> + Send>;
fn mkd<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = (), Error = Self::Error> + Send>;
}
pub struct Filesystem {
root: PathBuf,
}
fn canonicalize<P: AsRef<Path>>(path: P) -> Result<PathBuf> {
use self::path_abs::PathAbs;
let p = PathAbs::new(path)?;
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::PathError)
}
}
}
impl StorageBackend for Filesystem {
type File = self::tokio::fs::File;
type Metadata = std::fs::Metadata;
type Error = Error;
fn stat<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = Self::Metadata, Error = Self::Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(err) => return Box::new(futures::future::err(err)),
};
Box::new(tokio::fs::symlink_metadata(full_path).map_err(|_| Error::IOError))
}
fn list<P: AsRef<Path>>(&self, path: P) -> Box<Stream<Item = Fileinfo<std::path::PathBuf, Self::Metadata>, Error = Self::Error> + Send>
where <Self as StorageBackend>::Metadata: Metadata
{
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(futures::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::metadata(dir_entry.path()) {
Ok(stat) => Some(Fileinfo{path: relpath, metadata: stat}),
Err(_) => None,
}
});
Box::new(fut.map_err(|_| Error::IOError))
}
fn get<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = self::tokio::fs::File, Error = Self::Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(futures::future::err(e)),
};
Box::new(self::tokio::fs::file::File::open(full_path).map_err(|_| Error::IOError))
}
fn put<P: AsRef<Path>, R: self::tokio::prelude::AsyncRead + Send + 'static>(&self, bytes: R, path: P) -> Box<Future<Item = u64, Error = Self::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)
};
let fut = self::tokio::fs::file::File::create(full_path)
.and_then(|f| {
self::tokio_io::io::copy(bytes, f)
})
.map(|(n, _, _)| n)
.map_err(|_| Error::IOError);
Box::new(fut)
}
fn del<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = (), Error = Self::Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(futures::future::err(e)),
};
Box::new(self::tokio::fs::remove_file(full_path).map_err(|_| Error::IOError))
}
fn mkd<P: AsRef<Path>>(&self, path: P) -> Box<Future<Item = (), Error = Self::Error> + Send> {
let full_path = match self.full_path(path) {
Ok(path) => path,
Err(e) => return Box::new(futures::future::err(e)),
};
Box::new(self::tokio::fs::create_dir(full_path).map_err(|e| {println!("error: {}", e); Error::IOError}))
}
}
use std::os::unix::fs::MetadataExt;
impl Metadata for std::fs::Metadata {
fn len(&self) -> u64 {
self.len()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn is_dir(&self) -> bool {
self.is_dir()
}
fn is_file(&self) -> bool {
self.is_file()
}
fn modified(&self) -> Result<SystemTime> {
self.modified().map_err(|e| e.into())
}
fn gid(&self) -> u32 {
MetadataExt::gid(self)
}
fn uid(&self) -> u32 {
MetadataExt::uid(self)
}
}
#[derive(Debug, PartialEq)]
pub enum Error {
IOError,
PathError,
}
impl Error {
fn description_str(&self) -> &'static str {
""
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.description_str())
}
}
impl std::error::Error for Error {
fn description(&self) -> &str {
self.description_str()
}
}
impl From<std::io::Error> for Error {
fn from(_err: std::io::Error) -> Error {
Error::IOError
}
}
impl From<path_abs::Error> for Error {
fn from(_err: path_abs::Error) -> Error {
Error::PathError
}
}
type Result<T> = result::Result<T, Error>;
#[cfg(test)]
mod tests {
extern crate tempfile;
use super::*;
use std::fs::File;
use std::io::prelude::*;
#[test]
fn fs_stat() {
let root = std::env::temp_dir();
let file = tempfile::NamedTempFile::new_in(&root).unwrap();
let path = file.path().clone();
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.stat(filename)).unwrap();
assert_eq!(meta.is_dir(), my_meta.is_dir());
assert_eq!(meta.is_file(), my_meta.is_file());
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().clone();
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("/").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.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().clone();
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("/")).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(filename)).unwrap();
let mut my_content = Vec::new();
rt.block_on(
self::futures::future::lazy(move || {
self::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(orig_content.as_ref(), "greeting.txt")).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 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 format = format!("-rwxr-xr-x 1 2 5 Jan 01 1970 {}", dir.strip_prefix("/").unwrap().to_str().unwrap());
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(new_dir_name)).expect("Failed to mkd");
let full_path = root.join(new_dir_name);
let metadata = std::fs::metadata(full_path).unwrap();
assert!(metadata.is_dir());
}
}