use duckdb::arrow::error::ArrowError;
use std::io;
use std::num::ParseIntError;
use std::path::Path;
use std::path::PathBuf;
use tokio::task::JoinError;
use crate::model::ParsedResource;
use crate::model::RepoNew;
use crate::model::Schema;
use crate::model::Workspace;
pub mod path_buf_error;
pub mod string_error;
pub use crate::error::path_buf_error::PathBufError;
pub use crate::error::string_error::StringError;
pub const AUTH_TOKEN_NOT_FOUND: &str = "oxen authentication token not found, obtain one from your administrator and configure with:\n\noxen config --auth <HOST> <TOKEN>\n";
#[derive(thiserror::Error, Debug)]
pub enum OxenError {
#[error(
"oxen not configured, set email and name with:\n\noxen config --name YOUR_NAME --email YOUR_EMAIL\n"
)]
UserConfigNotFound,
#[error("Repository '{0}' not found")]
RepoNotFound(Box<RepoNew>),
#[error("No oxen repository found at {0}")]
LocalRepoNotFound(PathBufError),
#[error("Repository '{0}' already exists")]
RepoAlreadyExists(Box<RepoNew>),
#[error("Invalid repository or namespace name '{0}'. Must match [a-zA-Z0-9][a-zA-Z0-9_.-]+")]
InvalidRepoName(StringError),
#[error("No fork status found.")]
ForkStatusNotFound,
#[error("Remote repository not found: {0}")]
RemoteRepoNotFound(StringError),
#[error("{0}")]
UpstreamMergeConflict(StringError),
#[error(
"No remote named '{0}' is set. You can set a remote by running:\n\noxen config --set-remote '{0}' <url>\n"
)]
RemoteNotSet(String),
#[error("{0}")]
BranchNotFound(StringError),
#[error("Revision not found: {0}")]
RevisionNotFound(StringError),
#[error("No commits found.")]
NoCommitsFound,
#[error("HEAD not found.")]
HeadNotFound,
#[error("Workspace not found: {0}")]
WorkspaceNotFound(StringError),
#[error("No queryable workspace found")]
QueryableWorkspaceNotFound,
#[error("Workspace is behind: {0}")]
WorkspaceBehind(Box<Workspace>),
#[error("Resource not found: {0}")]
ResourceNotFound(StringError),
#[error("Path does not exist: {0}")]
PathDoesNotExist(PathBufError),
#[error("Resource not found: {0}")]
ParsedResourceNotFound(PathBufError),
#[error("{0}")]
MigrationRequired(StringError),
#[error("{0}")]
OxenUpdateRequired(StringError),
#[error("Invalid version: {0}")]
InvalidVersion(StringError),
#[error("{0}")]
CommitEntryNotFound(StringError),
#[error("Invalid schema: {0}")]
InvalidSchema(Box<Schema>),
#[error("Incompatible schemas: {0}")]
IncompatibleSchemas(Box<Schema>),
#[error("{0}")]
InvalidFileType(StringError),
#[error("{0}")]
ColumnNameAlreadyExists(StringError),
#[error("{0}")]
ColumnNameNotFound(StringError),
#[error("{0}")]
UnsupportedOperation(StringError),
#[error(
"Video thumbnail generation requires the 'ffmpeg' feature to be enabled. Build with --features liboxen/ffmpeg to enable this functionality."
)]
ThumbnailingNotEnabled,
#[error("Query returned no rows")]
NoRowsFound,
#[error("{0}")]
DataFrameError(StringError),
#[error("{0}")]
ImportFileError(StringError),
#[error("{0}")]
SQLParseError(StringError),
#[error("Error stripping prefix: {0}")]
StripPrefixError(#[from] std::path::StripPrefixError),
#[error("{0}")]
IO(#[from] io::Error),
#[error("Authentication failed: {0}")]
Authentication(StringError),
#[error("{0}")]
ArrowError(#[from] ArrowError),
#[error("{0}")]
BinCodeError(#[from] bincode::Error),
#[error("Configuration error: {0}")]
TomlSer(#[from] toml::ser::Error),
#[error("Configuration error: {0}")]
TomlDe(#[from] toml::de::Error),
#[error("Invalid URI: {0}")]
URI(#[from] http::uri::InvalidUri),
#[error("Invalid URL: {0}")]
URL(#[from] url::ParseError),
#[error("JSON error: {0}")]
JSON(#[from] serde_json::Error),
#[error("Network error: {0}")]
HTTP(#[from] reqwest::Error),
#[error("UTF-8 encoding error: {0}")]
UTF8Error(#[from] std::str::Utf8Error),
#[error("UTF-8 conversion error: {0}")]
Utf8ConvError(#[from] std::string::FromUtf8Error),
#[error("Database error: {0}")]
DB(#[from] rocksdb::Error),
#[error("Query error: {0}")]
DUCKDB(#[from] duckdb::Error),
#[error("Environment variable error: {0}")]
ENV(#[from] std::env::VarError),
#[error("Image processing error: {0}")]
ImageError(#[from] image::ImageError),
#[error("Redis error: {0}")]
RedisError(#[from] redis::RedisError),
#[error("Connection pool error: {0}")]
R2D2Error(#[from] r2d2::Error),
#[error("Directory traversal error: {0}")]
JwalkError(#[from] jwalk::Error),
#[error("Pattern error: {0}")]
PatternError(#[from] glob::PatternError),
#[error("Glob error: {0}")]
GlobError(#[from] glob::GlobError),
#[error("DataFrame error: {0}")]
PolarsError(#[from] polars::prelude::PolarsError),
#[error("Invalid integer: {0}")]
ParseIntError(#[from] ParseIntError),
#[error("Decode error: {0}")]
RmpDecodeError(#[from] rmp_serde::decode::Error),
#[error("{0}")]
JoinError(#[from] JoinError),
#[error("{0}")]
Basic(StringError),
}
impl OxenError {
pub fn hint(&self) -> Option<String> {
use OxenError::*;
use std::io::ErrorKind::PermissionDenied;
let hint = match self {
LocalRepoNotFound(_) => "Run `oxen init` to create a new repository here.",
Authentication(_) => {
"Check your token with `oxen config --auth <HOST> <TOKEN>` and try again."
}
RemoteRepoNotFound(_) => {
"Verify the remote URL is correct. Check your remotes with `oxen remote -v`."
}
BranchNotFound(_) => "List available branches with `oxen branch --all`.",
RevisionNotFound(_) => {
"Check available branches with `oxen branch --all` or commits with `oxen log`."
}
HeadNotFound | NoCommitsFound => {
"This repository has no commits yet. Add files and create your first commit."
}
PathDoesNotExist(_)
| ResourceNotFound(_)
| ParsedResourceNotFound(_)
| CommitEntryNotFound(_) => "Check the path and current branch with `oxen status`.",
HTTP(req_err) => {
if req_err.is_connect() || req_err.is_timeout() {
"Check your internet connection and that the remote host is reachable."
} else if req_err.is_status() {
if let Some(status) = req_err.status() {
return Some(format!("Server returned HTTP {status}."));
} else {
return None;
}
} else {
"Check your internet connection and remote configuration with `oxen remote -v`."
}
}
IO(io_err) if io_err.kind() == PermissionDenied => {
"Check file permissions and try again."
}
DB(_) | ArrowError(_) | BinCodeError(_) | RedisError(_) | R2D2Error(_)
| RmpDecodeError(_) => {
"This is an internal error. Run with RUST_LOG=debug for more details."
}
_ => return None,
}
.to_string();
Some(hint)
}
pub fn is_auth_error(&self) -> bool {
matches!(self, OxenError::Authentication(_))
}
pub fn is_not_found(&self) -> bool {
matches!(
self,
OxenError::PathDoesNotExist(_)
| OxenError::ResourceNotFound(_)
| OxenError::RemoteRepoNotFound(_)
| OxenError::LocalRepoNotFound(_)
| OxenError::ParsedResourceNotFound(_)
| OxenError::WorkspaceNotFound(_)
| OxenError::QueryableWorkspaceNotFound
)
}
pub fn authentication(s: impl AsRef<str>) -> Self {
OxenError::Authentication(StringError::from(s.as_ref()))
}
pub fn invalid_version(s: impl AsRef<str>) -> Self {
OxenError::InvalidVersion(StringError::from(s.as_ref()))
}
pub fn oxen_update_required(s: impl AsRef<str>) -> Self {
OxenError::OxenUpdateRequired(StringError::from(s.as_ref()))
}
pub fn repo_not_found(repo: RepoNew) -> Self {
OxenError::RepoNotFound(Box::new(repo))
}
pub fn file_import_error(s: impl AsRef<str>) -> Self {
OxenError::ImportFileError(StringError::from(s.as_ref()))
}
pub fn resource_not_found(value: impl AsRef<str>) -> Self {
OxenError::ResourceNotFound(StringError::from(value.as_ref()))
}
pub fn path_does_not_exist(path: impl AsRef<Path>) -> Self {
OxenError::PathDoesNotExist(path.as_ref().into())
}
pub fn parsed_resource_not_found(resource: ParsedResource) -> Self {
OxenError::ParsedResourceNotFound(resource.resource.into())
}
pub fn local_repo_not_found(dir: impl AsRef<Path>) -> OxenError {
OxenError::LocalRepoNotFound(dir.as_ref().into())
}
pub fn email_and_name_not_set() -> OxenError {
OxenError::UserConfigNotFound
}
pub fn remote_branch_not_found(name: impl AsRef<str>) -> OxenError {
let err = format!("Remote branch '{}' not found", name.as_ref());
OxenError::BranchNotFound(err.into())
}
pub fn local_branch_not_found(name: impl AsRef<str>) -> OxenError {
let err = format!("Branch '{}' not found", name.as_ref());
OxenError::BranchNotFound(err.into())
}
pub fn entry_does_not_exist(path: impl AsRef<Path>) -> OxenError {
OxenError::ParsedResourceNotFound(path.as_ref().into())
}
pub fn entry_does_not_exist_in_commit(
path: impl AsRef<Path>,
commit_id: impl AsRef<str>,
) -> OxenError {
let err = format!(
"Entry {:?} does not exist in commit {}",
path.as_ref(),
commit_id.as_ref()
);
OxenError::CommitEntryNotFound(err.into())
}
pub fn invalid_file_type(file_type: impl AsRef<str>) -> OxenError {
let err = format!("Invalid file type: {:?}", file_type.as_ref());
OxenError::InvalidFileType(StringError::from(err))
}
pub fn column_name_already_exists(column_name: &str) -> OxenError {
let err = format!("Column name already exists: {column_name:?}");
OxenError::ColumnNameAlreadyExists(StringError::from(err))
}
pub fn column_name_not_found(column_name: &str) -> OxenError {
let err = format!("Column name not found: {column_name:?}");
OxenError::ColumnNameNotFound(StringError::from(err))
}
pub fn incompatible_schemas(schema: Schema) -> OxenError {
OxenError::IncompatibleSchemas(Box::new(schema))
}
pub fn basic_str(s: impl AsRef<str>) -> Self {
OxenError::Basic(StringError::from(s.as_ref()))
}
pub fn home_dir_not_found() -> OxenError {
OxenError::basic_str("Home directory not found")
}
pub fn cache_dir_not_found() -> OxenError {
OxenError::basic_str("Cache directory not found")
}
pub fn must_be_on_valid_branch() -> OxenError {
OxenError::basic_str(
"Repository is in a detached HEAD state, checkout a valid branch to continue.\n\n oxen checkout <branch>\n",
)
}
pub fn no_schemas_staged() -> OxenError {
OxenError::basic_str(
"No schemas staged\n\nAuto detect schema on file with:\n\n oxen add path/to/file.csv\n\nOr manually add a schema override with:\n\n oxen schemas add path/to/file.csv 'name:str, age:i32'\n",
)
}
pub fn no_schemas_committed() -> OxenError {
OxenError::basic_str(
"No schemas committed\n\nAuto detect schema on file with:\n\n oxen add path/to/file.csv\n\nOr manually add a schema override with:\n\n oxen schemas add path/to/file.csv 'name:str, age:i32'\n\nThen commit the schema with:\n\n oxen commit -m 'Adding schema for path/to/file.csv'\n",
)
}
pub fn schema_does_not_exist_for_file(path: impl AsRef<Path>) -> OxenError {
OxenError::basic_str(format!(
"Schema does not exist for file {:?}",
path.as_ref()
))
}
pub fn schema_does_not_exist(path: impl AsRef<Path>) -> OxenError {
OxenError::basic_str(format!("Schema does not exist {:?}", path.as_ref()))
}
pub fn schema_does_not_have_field(field: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!("Schema does not have field {:?}", field.as_ref()))
}
pub fn schema_has_changed(old_schema: Schema, current_schema: Schema) -> OxenError {
OxenError::basic_str(format!(
"\nSchema has changed\n\nOld\n{old_schema}\n\nCurrent\n{current_schema}\n"
))
}
pub fn commit_db_corrupted(commit_id: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!(
"Commit db corrupted, could not find commit: {}",
commit_id.as_ref()
))
}
pub fn commit_id_does_not_exist(commit_id: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!("Could not find commit: {}", commit_id.as_ref()))
}
pub fn local_parent_link_broken(commit_id: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!(
"Broken link to parent commit: {}",
commit_id.as_ref()
))
}
pub fn file_error(path: impl AsRef<Path>, error: std::io::Error) -> OxenError {
OxenError::basic_str(format!(
"File does not exist: {:?} error {:?}",
path.as_ref(),
error
))
}
pub fn file_create_error(path: impl AsRef<Path>, error: std::io::Error) -> OxenError {
OxenError::basic_str(format!(
"Could not create file: {:?} error {:?}",
path.as_ref(),
error
))
}
pub fn dir_create_error(path: impl AsRef<Path>, error: std::io::Error) -> OxenError {
OxenError::basic_str(format!(
"Could not create directory: {:?} error {:?}",
path.as_ref(),
error
))
}
pub fn file_open_error(path: impl AsRef<Path>, error: std::io::Error) -> OxenError {
OxenError::basic_str(format!(
"Could not open file: {:?} error {:?}",
path.as_ref(),
error
))
}
pub fn file_read_error(path: impl AsRef<Path>, error: std::io::Error) -> OxenError {
OxenError::basic_str(format!(
"Could not read file: {:?} error {:?}",
path.as_ref(),
error
))
}
pub fn file_metadata_error(path: impl AsRef<Path>, error: std::io::Error) -> OxenError {
OxenError::basic_str(format!(
"Could not get file metadata: {:?} error {:?}",
path.as_ref(),
error
))
}
pub fn file_copy_error(
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
err: impl std::fmt::Debug,
) -> OxenError {
OxenError::basic_str(format!(
"File copy error: {err:?}\nCould not copy from `{:?}` to `{:?}`",
src.as_ref(),
dst.as_ref()
))
}
pub fn file_rename_error(
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
err: impl std::fmt::Debug,
) -> OxenError {
OxenError::basic_str(format!(
"File rename error: {err:?}\nCould not move from `{:?}` to `{:?}`",
src.as_ref(),
dst.as_ref()
))
}
pub fn workspace_add_file_not_in_repo(path: impl AsRef<Path>) -> OxenError {
OxenError::basic_str(format!(
"File is outside of the repo {:?}\n\nYou must specify a path you would like to add the file at with the -d flag.\n\n oxen workspace add /path/to/file.png -d my-images/\n",
path.as_ref()
))
}
pub fn cannot_overwrite_files(paths: &[PathBuf]) -> OxenError {
let paths_str = paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join("\n ");
OxenError::basic_str(format!(
"\nError: your local changes to the following files would be overwritten. Please commit the following changes before continuing:\n\n {paths_str}\n"
))
}
pub fn must_supply_valid_api_key() -> OxenError {
OxenError::basic_str(
"Must supply valid API key. Create an account at https://oxen.ai and then set the API key with:\n\n oxen config --auth hub.oxen.ai <API_KEY>\n",
)
}
pub fn file_has_no_parent(path: impl AsRef<Path>) -> OxenError {
OxenError::basic_str(format!("File has no parent: {:?}", path.as_ref()))
}
pub fn file_has_no_name(path: impl AsRef<Path>) -> OxenError {
OxenError::basic_str(format!("File has no file_name: {:?}", path.as_ref()))
}
pub fn could_not_convert_path_to_str(path: impl AsRef<Path>) -> OxenError {
OxenError::basic_str(format!("File has no name: {:?}", path.as_ref()))
}
pub fn local_revision_not_found(name: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!(
"Local branch or commit reference `{}` not found",
name.as_ref()
))
}
pub fn could_not_find_merge_conflict(path: impl AsRef<Path>) -> OxenError {
OxenError::basic_str(format!(
"Could not find merge conflict for path: {:?}",
path.as_ref()
))
}
pub fn could_not_decode_value_for_key_error(key: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!(
"Could not decode value for key: {:?}",
key.as_ref()
))
}
pub fn invalid_set_remote_url(url: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!(
"\nRemote invalid, must be fully qualified URL, got: {:?}\n\n oxen config --set-remote origin https://hub.oxen.ai/<namespace>/<reponame>\n",
url.as_ref()
))
}
pub fn parse_error(value: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!("Parse error: {:?}", value.as_ref()))
}
pub fn unknown_subcommand(parent: impl AsRef<str>, name: impl AsRef<str>) -> OxenError {
OxenError::basic_str(format!(
"Unknown {} subcommand '{}'",
parent.as_ref(),
name.as_ref()
))
}
}
impl From<String> for OxenError {
fn from(error: String) -> Self {
OxenError::Basic(StringError::from(error))
}
}