322 lines
9.7 KiB
Rust
322 lines
9.7 KiB
Rust
use std::os::unix::fs::MetadataExt;
|
|
|
|
use anyhow::{Context, anyhow, bail};
|
|
use rustix::fs::statx;
|
|
use tokio::{
|
|
fs,
|
|
io::{self, AsyncWriteExt as _},
|
|
};
|
|
use tokio_util::io::ReaderStream;
|
|
|
|
use crate::{
|
|
config::Config,
|
|
domain::warren::{
|
|
models::file::{
|
|
AbsoluteFilePath, CreateDirectoryError, CreateDirectoryRequest, CreateFileError,
|
|
CreateFileRequest, DeleteDirectoryError, DeleteDirectoryRequest, DeleteFileError,
|
|
DeleteFileRequest, FetchFileError, FetchFileRequest, File, FileMimeType, FileName,
|
|
FilePath, FileStream, FileType, ListFilesError, ListFilesRequest, RenameEntryError,
|
|
RenameEntryRequest,
|
|
},
|
|
ports::FileSystemRepository,
|
|
},
|
|
};
|
|
|
|
const MAX_FILE_FETCH_BYTES: &str = "MAX_FILE_FETCH_BYTES";
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct FileSystemConfig {
|
|
base_directory: String,
|
|
max_file_fetch_bytes: u64,
|
|
}
|
|
|
|
impl FileSystemConfig {
|
|
pub fn new(base_directory: String, max_file_fetch_bytes: u64) -> Self {
|
|
Self {
|
|
base_directory,
|
|
max_file_fetch_bytes,
|
|
}
|
|
}
|
|
|
|
pub fn from_env(serve_dir: String) -> anyhow::Result<Self> {
|
|
// 268435456 bytes = 0.25GB
|
|
let max_file_fetch_bytes: u64 = match Config::load_env(MAX_FILE_FETCH_BYTES) {
|
|
Ok(value) => value.parse()?,
|
|
Err(_) => 268435456,
|
|
};
|
|
|
|
Ok(Self::new(serve_dir, max_file_fetch_bytes))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct FileSystem {
|
|
base_directory: FilePath,
|
|
max_file_fetch_bytes: u64,
|
|
}
|
|
|
|
impl FileSystem {
|
|
pub fn new(config: FileSystemConfig) -> anyhow::Result<Self> {
|
|
let file_system = Self {
|
|
base_directory: FilePath::new(&config.base_directory)?,
|
|
max_file_fetch_bytes: config.max_file_fetch_bytes,
|
|
};
|
|
|
|
Ok(file_system)
|
|
}
|
|
|
|
/// Combines `self.base_directory` with the specified path
|
|
///
|
|
/// * `path`: The absolute path (absolute in relation to the base directory)
|
|
fn get_target_path(&self, path: &AbsoluteFilePath) -> FilePath {
|
|
self.base_directory.join(&path.as_relative())
|
|
}
|
|
|
|
async fn get_all_files(&self, absolute_path: &AbsoluteFilePath) -> anyhow::Result<Vec<File>> {
|
|
let directory_path = self.get_target_path(absolute_path);
|
|
|
|
let mut dir = fs::read_dir(&directory_path).await?;
|
|
|
|
let mut files = Vec::new();
|
|
|
|
while let Ok(Some(entry)) = dir.next_entry().await {
|
|
let name = entry
|
|
.file_name()
|
|
.into_string()
|
|
.ok()
|
|
.context("Failed to get file name")?;
|
|
let file_type = {
|
|
let file_type = entry.file_type().await?;
|
|
|
|
if file_type.is_dir() {
|
|
FileType::Directory
|
|
} else if file_type.is_file() {
|
|
FileType::File
|
|
} else {
|
|
continue;
|
|
}
|
|
};
|
|
|
|
// TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5
|
|
// https://github.com/rust-lang/rust/pull/142682
|
|
let created_at = unsafe {
|
|
statx(
|
|
std::os::fd::BorrowedFd::borrow_raw(-100),
|
|
entry.path(),
|
|
rustix::fs::AtFlags::empty(),
|
|
rustix::fs::StatxFlags::BTIME,
|
|
)
|
|
}
|
|
.ok()
|
|
.map(|statx| statx.stx_btime.tv_sec as u64);
|
|
|
|
let mime_type = match file_type {
|
|
FileType::File => FileMimeType::from_name(&name),
|
|
_ => None,
|
|
};
|
|
|
|
files.push(File::new(
|
|
FileName::new(&name)?,
|
|
file_type,
|
|
mime_type,
|
|
created_at,
|
|
));
|
|
}
|
|
|
|
Ok(files)
|
|
}
|
|
|
|
/// Actually created a directory in the underlying file system
|
|
///
|
|
/// * `path`: The directory's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
|
async fn create_dir(&self, path: &AbsoluteFilePath) -> io::Result<FilePath> {
|
|
let file_path = self.get_target_path(path);
|
|
|
|
if fs::try_exists(&file_path).await? {
|
|
return Err(io::ErrorKind::AlreadyExists.into());
|
|
}
|
|
|
|
fs::create_dir(&file_path).await?;
|
|
|
|
Ok(file_path)
|
|
}
|
|
|
|
/// Actually removes a directory from the underlying file system
|
|
///
|
|
/// * `path`: The directory's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
|
/// * `force`: Whether to delete directories that are not empty
|
|
async fn remove_dir(&self, path: &AbsoluteFilePath, force: bool) -> io::Result<FilePath> {
|
|
let file_path = self.get_target_path(path);
|
|
|
|
if force {
|
|
fs::remove_dir_all(&file_path).await?;
|
|
} else {
|
|
fs::remove_dir(&file_path).await?;
|
|
}
|
|
|
|
Ok(file_path)
|
|
}
|
|
|
|
async fn write_file(&self, path: &AbsoluteFilePath, data: &[u8]) -> anyhow::Result<FilePath> {
|
|
let path = self.get_target_path(path);
|
|
|
|
let mut file = fs::OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.open(&path)
|
|
.await?;
|
|
|
|
file.write_all(data).await?;
|
|
|
|
Ok(path)
|
|
}
|
|
|
|
async fn fetch_file(&self, path: &AbsoluteFilePath) -> anyhow::Result<FileStream> {
|
|
let path = self.get_target_path(path);
|
|
|
|
let file = fs::OpenOptions::new()
|
|
.create(false)
|
|
.write(false)
|
|
.read(true)
|
|
.open(&path)
|
|
.await?;
|
|
|
|
let file_size = file.metadata().await?.size();
|
|
|
|
if file_size > self.max_file_fetch_bytes {
|
|
bail!("File size exceeds configured limit");
|
|
}
|
|
|
|
let stream = FileStream::new(ReaderStream::new(file));
|
|
|
|
Ok(stream)
|
|
}
|
|
|
|
/// Actually removes a file from the underlying file system
|
|
///
|
|
/// * `path`: The file's absolute path (absolute not in relation to the root file system but `self.base_directory`)
|
|
async fn remove_file(&self, path: &AbsoluteFilePath) -> anyhow::Result<FilePath> {
|
|
let path = self.get_target_path(path);
|
|
|
|
fs::remove_file(&path).await?;
|
|
|
|
Ok(path)
|
|
}
|
|
|
|
async fn rename(
|
|
&self,
|
|
path: &AbsoluteFilePath,
|
|
new_name: &FileName,
|
|
) -> anyhow::Result<FilePath> {
|
|
let current_path = self.get_target_path(path);
|
|
|
|
let new_path = {
|
|
let mut c = current_path.to_string();
|
|
let last_slash_index = c.rfind('/').unwrap();
|
|
c.drain((last_slash_index + 1)..);
|
|
c.push_str(new_name.as_str());
|
|
|
|
FilePath::new(&c)?
|
|
};
|
|
|
|
if fs::try_exists(&new_path).await? {
|
|
bail!("File exists");
|
|
}
|
|
|
|
fs::rename(current_path, &new_path).await?;
|
|
|
|
Ok(new_path)
|
|
}
|
|
}
|
|
|
|
impl FileSystemRepository for FileSystem {
|
|
async fn list_files(&self, request: ListFilesRequest) -> Result<Vec<File>, ListFilesError> {
|
|
let files = self.get_all_files(request.path()).await.map_err(|err| {
|
|
anyhow!(err).context(format!(
|
|
"Failed to get the files at path: {}",
|
|
request.path()
|
|
))
|
|
})?;
|
|
|
|
Ok(files)
|
|
}
|
|
|
|
async fn create_directory(
|
|
&self,
|
|
request: CreateDirectoryRequest,
|
|
) -> Result<FilePath, CreateDirectoryError> {
|
|
match self.create_dir(request.path()).await {
|
|
Ok(path) => Ok(path),
|
|
Err(e) => match e.kind() {
|
|
std::io::ErrorKind::AlreadyExists => Err(CreateDirectoryError::Exists),
|
|
_ => Err(anyhow!("Failed to create directory at path: {}", request.path()).into()),
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn delete_directory(
|
|
&self,
|
|
request: DeleteDirectoryRequest,
|
|
) -> Result<FilePath, DeleteDirectoryError> {
|
|
match self.remove_dir(request.path(), request.force()).await {
|
|
Ok(deleted_path) => return Ok(deleted_path),
|
|
Err(e) => match e.kind() {
|
|
std::io::ErrorKind::NotFound => Err(DeleteDirectoryError::NotFound),
|
|
std::io::ErrorKind::DirectoryNotEmpty => Err(DeleteDirectoryError::NotEmpty),
|
|
_ => Err(anyhow!("Failed to delete directory at {}: {e:?}", request.path()).into()),
|
|
},
|
|
}
|
|
}
|
|
|
|
async fn create_file(&self, request: CreateFileRequest) -> Result<FilePath, CreateFileError> {
|
|
let file_path = self
|
|
.write_file(request.path(), request.data())
|
|
.await
|
|
.map_err(|e| {
|
|
anyhow!(
|
|
"Failed to write {} byte(s) to path {}: {e:?}",
|
|
request.data().len(),
|
|
request.path()
|
|
)
|
|
})?;
|
|
|
|
Ok(file_path)
|
|
}
|
|
|
|
async fn fetch_file(&self, request: FetchFileRequest) -> Result<FileStream, FetchFileError> {
|
|
let contents = self
|
|
.fetch_file(request.path())
|
|
.await
|
|
.map_err(|e| anyhow!("Failed to fetch file {}: {e:?}", request.path()))?;
|
|
|
|
Ok(contents)
|
|
}
|
|
|
|
async fn delete_file(&self, request: DeleteFileRequest) -> Result<FilePath, DeleteFileError> {
|
|
let deleted_path = self
|
|
.remove_file(request.path())
|
|
.await
|
|
.context(format!("Failed to delete file at {}", request.path()))?;
|
|
|
|
Ok(deleted_path)
|
|
}
|
|
|
|
async fn rename_entry(
|
|
&self,
|
|
request: RenameEntryRequest,
|
|
) -> Result<FilePath, RenameEntryError> {
|
|
let new_path = self
|
|
.rename(request.path(), request.new_name())
|
|
.await
|
|
.map_err(|e| {
|
|
anyhow!(
|
|
"Failed to rename {} to {}: {e:?}",
|
|
request.path(),
|
|
request.new_name()
|
|
)
|
|
})?;
|
|
|
|
Ok(new_path)
|
|
}
|
|
}
|