use std::borrow::{Borrow, Cow};
use std::collections::HashMap;
use std::fmt;
use async_trait::async_trait;
pub use crate::generated::api_types::SessionFsSqliteQueryType;
use crate::generated::api_types::{
SessionFsError, SessionFsErrorCode, SessionFsReaddirWithTypesEntry,
SessionFsReaddirWithTypesEntryType, SessionFsSetProviderConventions, SessionFsStatResult,
};
use crate::{Custom, Repr};
#[non_exhaustive]
#[derive(Debug, Clone, Default)]
pub struct SessionFsCapabilities {
pub sqlite: bool,
}
impl SessionFsCapabilities {
pub fn new() -> Self {
Self::default()
}
pub fn with_sqlite(mut self, sqlite: bool) -> Self {
self.sqlite = sqlite;
self
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct SessionFsConfig {
pub initial_cwd: String,
pub session_state_path: String,
pub conventions: SessionFsConventions,
pub capabilities: Option<SessionFsCapabilities>,
}
impl SessionFsConfig {
pub fn new(
initial_cwd: impl Into<String>,
session_state_path: impl Into<String>,
conventions: SessionFsConventions,
) -> Self {
Self {
initial_cwd: initial_cwd.into(),
session_state_path: session_state_path.into(),
conventions,
capabilities: None,
}
}
pub fn with_capabilities(mut self, capabilities: SessionFsCapabilities) -> Self {
self.capabilities = Some(capabilities);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionFsConventions {
Posix,
Windows,
}
impl SessionFsConventions {
pub(crate) fn into_wire(self) -> SessionFsSetProviderConventions {
match self {
Self::Posix => SessionFsSetProviderConventions::Posix,
Self::Windows => SessionFsSetProviderConventions::Windows,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum FsErrorKind {
NotFound(String),
Other,
}
impl fmt::Display for FsErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FsErrorKind::NotFound(path) => write!(f, "not found: {path}"),
FsErrorKind::Other => write!(f, "filesystem error"),
}
}
}
#[derive(Debug)]
pub struct FsError {
repr: Repr<FsErrorKind>,
}
impl FsError {
pub fn new<E>(kind: FsErrorKind, error: E) -> Self
where
E: Into<Box<dyn std::error::Error + Send + Sync>>,
{
Self {
repr: Repr::Custom(Custom {
kind,
error: error.into(),
}),
}
}
pub fn kind(&self) -> &FsErrorKind {
match &self.repr {
Repr::Simple(k) | Repr::SimpleMessage(k, ..) | Repr::Custom(Custom { kind: k, .. }) => {
k
}
}
}
pub fn message(&self) -> Option<&str> {
match &self.repr {
Repr::SimpleMessage(_, m) => Some(m.borrow()),
_ => None,
}
}
#[must_use]
pub fn with_message<C>(kind: FsErrorKind, message: C) -> Self
where
C: Into<Cow<'static, str>>,
{
Self {
repr: Repr::SimpleMessage(kind, message.into()),
}
}
pub(crate) fn into_wire(self) -> SessionFsError {
match self.kind() {
FsErrorKind::NotFound(message) => SessionFsError {
code: SessionFsErrorCode::ENOENT,
message: Some(message.clone()),
},
FsErrorKind::Other => SessionFsError {
code: SessionFsErrorCode::UNKNOWN,
message: Some(self.to_string()),
},
}
}
}
impl fmt::Display for FsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.repr {
Repr::Simple(k) => write!(f, "{k}"),
Repr::SimpleMessage(_, m) => write!(f, "{m}"),
Repr::Custom(Custom { error, .. }) => write!(f, "{error}"),
}
}
}
impl std::error::Error for FsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.repr {
Repr::Custom(Custom { error, .. }) => Some(&**error),
_ => None,
}
}
}
impl From<FsErrorKind> for FsError {
fn from(kind: FsErrorKind) -> Self {
Self {
repr: Repr::Simple(kind),
}
}
}
impl From<std::io::Error> for FsError {
fn from(err: std::io::Error) -> Self {
match err.kind() {
std::io::ErrorKind::NotFound => Self::new(FsErrorKind::NotFound(err.to_string()), err),
_ => Self::new(FsErrorKind::Other, err),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct FileInfo {
pub is_file: bool,
pub is_directory: bool,
pub size: i64,
pub mtime: String,
pub birthtime: String,
}
impl FileInfo {
pub fn new(
is_file: bool,
is_directory: bool,
size: i64,
mtime: impl Into<String>,
birthtime: impl Into<String>,
) -> Self {
Self {
is_file,
is_directory,
size,
mtime: mtime.into(),
birthtime: birthtime.into(),
}
}
pub(crate) fn into_wire(self) -> SessionFsStatResult {
SessionFsStatResult {
is_file: self.is_file,
is_directory: self.is_directory,
size: self.size,
mtime: self.mtime,
birthtime: self.birthtime,
error: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DirEntryKind {
File,
Directory,
}
impl DirEntryKind {
fn into_wire(self) -> SessionFsReaddirWithTypesEntryType {
match self {
Self::File => SessionFsReaddirWithTypesEntryType::File,
Self::Directory => SessionFsReaddirWithTypesEntryType::Directory,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct DirEntry {
pub name: String,
pub kind: DirEntryKind,
}
impl DirEntry {
pub fn new(name: impl Into<String>, kind: DirEntryKind) -> Self {
Self {
name: name.into(),
kind,
}
}
pub(crate) fn into_wire(self) -> SessionFsReaddirWithTypesEntry {
SessionFsReaddirWithTypesEntry {
name: self.name,
r#type: self.kind.into_wire(),
}
}
}
#[async_trait]
pub trait SessionFsProvider: Send + Sync + 'static {
async fn read_file(&self, path: &str) -> Result<String, FsError> {
let _ = path;
Err(FsError::with_message(
FsErrorKind::Other,
"read_file not supported",
))
}
async fn write_file(
&self,
path: &str,
content: &str,
mode: Option<i64>,
) -> Result<(), FsError> {
let _ = (path, content, mode);
Err(FsError::with_message(
FsErrorKind::Other,
"write_file not supported",
))
}
async fn append_file(
&self,
path: &str,
content: &str,
mode: Option<i64>,
) -> Result<(), FsError> {
let _ = (path, content, mode);
Err(FsError::with_message(
FsErrorKind::Other,
"append_file not supported",
))
}
async fn exists(&self, path: &str) -> Result<bool, FsError> {
let _ = path;
Err(FsError::with_message(
FsErrorKind::Other,
"exists not supported",
))
}
async fn stat(&self, path: &str) -> Result<FileInfo, FsError> {
let _ = path;
Err(FsError::with_message(
FsErrorKind::Other,
"stat not supported",
))
}
async fn mkdir(&self, path: &str, recursive: bool, mode: Option<i64>) -> Result<(), FsError> {
let _ = (path, recursive, mode);
Err(FsError::with_message(
FsErrorKind::Other,
"mkdir not supported",
))
}
async fn readdir(&self, path: &str) -> Result<Vec<String>, FsError> {
let _ = path;
Err(FsError::with_message(
FsErrorKind::Other,
"readdir not supported",
))
}
async fn readdir_with_types(&self, path: &str) -> Result<Vec<DirEntry>, FsError> {
let _ = path;
Err(FsError::with_message(
FsErrorKind::Other,
"readdir_with_types not supported",
))
}
async fn rm(&self, path: &str, recursive: bool, force: bool) -> Result<(), FsError> {
let _ = (path, recursive, force);
Err(FsError::with_message(
FsErrorKind::Other,
"rm not supported",
))
}
async fn rename(&self, src: &str, dest: &str) -> Result<(), FsError> {
let _ = (src, dest);
Err(FsError::with_message(
FsErrorKind::Other,
"rename not supported",
))
}
fn sqlite(&self) -> Option<&dyn SessionFsSqliteProvider> {
None
}
}
#[async_trait]
pub trait SessionFsSqliteProvider: Send + Sync {
async fn sqlite_query(
&self,
query_type: SessionFsSqliteQueryType,
query: &str,
params: Option<&HashMap<String, serde_json::Value>>,
) -> Result<Option<SessionFsSqliteQueryResult>, FsError>;
async fn sqlite_exists(&self) -> Result<bool, FsError>;
}
#[derive(Debug, Clone, Default)]
pub struct SessionFsSqliteQueryResult {
pub columns: Vec<String>,
pub rows: Vec<HashMap<String, serde_json::Value>>,
pub rows_affected: i64,
pub last_insert_rowid: Option<i64>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fs_error_maps_io_not_found_to_enoent() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt");
let fs_err: FsError = io_err.into();
assert!(
matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt")
);
let wire = fs_err.into_wire();
assert_eq!(wire.code, SessionFsErrorCode::ENOENT);
}
#[test]
fn fs_error_maps_other_io_to_unknown() {
let io_err = std::io::Error::other("disk full");
let fs_err: FsError = io_err.into();
assert!(matches!(fs_err.kind(), FsErrorKind::Other));
let wire = fs_err.into_wire();
assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN);
assert!(wire.message.unwrap().contains("disk full"));
}
#[test]
fn conventions_maps_to_wire() {
assert_eq!(
SessionFsConventions::Posix.into_wire(),
SessionFsSetProviderConventions::Posix
);
assert_eq!(
SessionFsConventions::Windows.into_wire(),
SessionFsSetProviderConventions::Windows
);
}
struct DefaultProvider;
#[async_trait]
impl SessionFsProvider for DefaultProvider {}
#[tokio::test]
async fn default_impls_return_unsupported() {
let p = DefaultProvider;
let err = p.read_file("/x").await.unwrap_err();
assert!(
matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported")
);
}
}