mod staged_file;
use crate::error::{IoPathError, IoPathResult, WithPath};
use crate::storage::filesystem::staged_file::{StagedFile, clean_leftover_tmp_files};
use crate::storage::{EntryType, FileHandle, Storage, StorageEntry};
use fs2::FileExt;
use rand::rngs::ThreadRng;
use std::borrow::Cow;
use std::fs::File;
use std::io::ErrorKind;
use std::path::{Component, PathBuf};
use std::{fs, io};
#[derive(Clone)]
pub struct FilesystemStorage {
root: PathBuf,
}
impl FilesystemStorage {
pub fn new(root: PathBuf) -> Self {
FilesystemStorage { root }
}
pub fn clean_leftover_tmp_files(&mut self) -> io::Result<()> {
clean_leftover_tmp_files(&self.root)
}
}
impl Storage for FilesystemStorage {
type Reader = File;
type Writer = StagedFile<PathBuf>;
fn delete(&self, path: &str) -> IoPathResult<()> {
let full_path = self.canonical_path(path)?;
if full_path.is_dir() {
fs::remove_dir(&full_path).with_path(&full_path)
} else {
fs::remove_file(&full_path).with_path(&full_path)
}
}
fn get(&self, path: &str) -> IoPathResult<FileHandle<Self::Reader>> {
let canonical_path = self.canonical_path(path)?;
let file = File::open(&canonical_path).with_path(&canonical_path)?;
Ok(FileHandle {
size_hint: file.allocated_size().with_path(&canonical_path)?,
reader: file,
})
}
fn exists_file(&self, path: &str) -> IoPathResult<bool> {
Ok(self.canonical_path(path)?.is_file())
}
fn list(
&self,
path: &str,
) -> IoPathResult<impl Iterator<Item = IoPathResult<StorageEntry<'_>>>> {
let canonical_path = self.canonical_path(path)?;
Ok(canonical_path
.read_dir()
.with_path(&canonical_path)?
.map(move |entry| {
let entry = entry.with_path(&canonical_path)?;
if let Some(entry_type) = match entry.file_type().with_path(entry.path())? {
file_type if file_type.is_file() => Some(EntryType::File),
file_type if file_type.is_dir() => Some(EntryType::Directory),
_ => None,
} {
Ok(Some(StorageEntry {
name: Cow::Owned(
entry
.file_name()
.into_string()
.map_err(|_| {
io::Error::new(
ErrorKind::InvalidData,
"File name is not valid Unicode",
)
})
.with_path(entry.path())?,
),
entry_type,
size: if entry.file_type().with_path(entry.path())?.is_file() {
entry.metadata().with_path(entry.path())?.len()
} else {
0
},
}))
} else {
Ok(None)
}
})
.filter_map(Result::transpose))
}
fn put(&self, path: &str) -> IoPathResult<Self::Writer> {
let canonical_path = self.canonical_path(path)?;
if self.root.exists()
&& let Some(parent_dir) = canonical_path.parent()
{
let mut path = PathBuf::new();
for component in parent_dir.components() {
if component == Component::ParentDir {
return Err(IoPathError::new(
io::Error::new(
ErrorKind::InvalidInput,
"Path must not contain parent directory components",
),
path,
));
}
path = path.join(component);
if !path.exists() {
fs::create_dir(&path).with_path(&path)?;
}
}
}
StagedFile::new(canonical_path, &mut ThreadRng::default())
}
}
impl FilesystemStorage {
fn canonical_path(&self, path: &str) -> IoPathResult<PathBuf> {
if !path.starts_with('/') {
return Err(IoPathError::new(
io::Error::new(
ErrorKind::InvalidInput,
"Path must be absolute, i.e. start with a slash '/'",
),
path,
));
}
Ok(self.root.join(&path[1..]))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::FileHandle;
use crate::storage::in_memory::InMemoryStorage;
use crate::storage::tests::{read_file_from_storage_to_string, write_file_to_storage};
use crate::test_storage;
use std::fs::create_dir_all;
use std::path::Path;
use tempfile::{TempDir, tempdir};
struct FilesystemStorageTestFixture {
storage: FilesystemStorage,
_tempdir: TempDir,
}
impl FilesystemStorageTestFixture {
fn new() -> Self {
let tempdir = tempdir().unwrap();
Self {
storage: FilesystemStorage::new(tempdir.path().to_path_buf()),
_tempdir: tempdir,
}
}
}
impl Storage for FilesystemStorageTestFixture {
type Reader = <FilesystemStorage as Storage>::Reader;
type Writer = <FilesystemStorage as Storage>::Writer;
fn delete(&self, path: &str) -> IoPathResult<()> {
self.storage.delete(path)
}
fn get(&self, path: &str) -> IoPathResult<FileHandle<Self::Reader>> {
self.storage.get(path)
}
fn exists_file(&self, path: &str) -> IoPathResult<bool> {
self.storage.exists_file(path)
}
fn list(
&self,
path: &str,
) -> IoPathResult<impl Iterator<Item = IoPathResult<StorageEntry<'_>>>> {
self.storage.list(path)
}
fn put(&self, path: &str) -> IoPathResult<Self::Writer> {
self.storage.put(path)
}
}
test_storage!(filesystem_tests, FilesystemStorageTestFixture::new());
#[test]
fn test_provides_size_hint() {
let storage = InMemoryStorage::new();
write_file_to_storage(&storage, "/dir/file.txt", "Hello, world!").unwrap();
assert!(storage.get("/dir/file.txt").unwrap().size_hint > 0);
}
#[test]
fn test_does_not_create_non_existent_root() {
let tempdir = tempdir().unwrap();
let storage_path = tempdir.path().join("non-existent");
let mut storage = FilesystemStorage::new(storage_path.clone());
assert_eq!(
write_file_to_storage(&mut storage, "/file.txt", "Hello, world!")
.unwrap_err()
.io_error()
.kind(),
ErrorKind::NotFound
);
assert!(!storage_path.exists());
}
#[test]
fn test_disallows_putting_files_above_root() {
let tempdir = tempdir().unwrap();
let storage_root = tempdir.path().join("storage-root");
fs::create_dir(&storage_root).unwrap();
let mut storage = FilesystemStorage::new(storage_root.clone());
assert!(write_file_to_storage(&mut storage, "/../file.txt", "file-content").is_err());
assert!(!storage_root.join("file.txt").exists());
}
struct PushCwd {
old_cwd: PathBuf,
}
impl PushCwd {
fn new<P: AsRef<Path>>(new_cwd: P) -> io::Result<Self> {
let old_cwd = std::env::current_dir()?;
std::env::set_current_dir(new_cwd)?;
Ok(Self { old_cwd })
}
}
impl Drop for PushCwd {
fn drop(&mut self) {
std::env::set_current_dir(&self.old_cwd).unwrap();
}
}
#[test]
fn test_with_relative_path() {
let tempdir = tempdir().unwrap();
let _push_cwd = PushCwd::new(tempdir.path()).unwrap();
let storage_path = PathBuf::from("dir/storage-root");
create_dir_all(&storage_path).unwrap();
let mut storage = FilesystemStorage::new(storage_path.clone());
write_file_to_storage(&mut storage, "/some/subdir/file.txt", "Hello, world!").unwrap();
read_file_from_storage_to_string(&mut storage, "/some/subdir/file.txt").unwrap();
}
}