use std::time::Duration;
use git_lfs_api::{ApiError, ObjectError};
use git_lfs_pointer::OidParseError;
use git_lfs_store::StoreError;
#[derive(Debug, thiserror::Error)]
pub enum TransferError {
#[error("server error for object: {} ({})", .0.message, .0.code)]
ServerObject(ObjectError),
#[error("server returned no download action for object")]
NoDownloadAction,
#[error("{}", format_action_status(*.status, .url))]
ActionStatus {
status: u16,
url: String,
retry_after: Option<Duration>,
},
#[error("http error: {0}")]
Http(#[from] reqwest::Error),
#[error("local io error: {0}")]
Io(#[from] std::io::Error),
#[error("store error: {0}")]
Store(#[from] StoreError),
#[error("invalid oid from server: {0}")]
InvalidOid(#[from] OidParseError),
#[error("unsupported hash algorithm: {0}")]
UnsupportedHashAlgo(String),
#[error("batch response: {0}")]
BatchResponse(Box<ApiError>),
}
fn format_action_status(status: u16, url: &str) -> String {
let (fatal, prefix) = match status {
400 => (false, "Client error:"),
401 | 403 => (false, "Authorization error:"),
404 => (false, "Repository or object not found:"),
422 => (false, "Unprocessable entity:"),
429 => (false, "Rate limit exceeded:"),
500 => (true, "Server error:"),
501 => (false, "Not Implemented:"),
503 => (true, "LFS is temporarily unavailable:"),
507 => (false, "Insufficient server storage:"),
509 => (false, "Bandwidth limit exceeded:"),
_ if status < 500 => return format!("LFS: Client error {url} from HTTP {status}"),
_ => return format!("Fatal error: Server error {url} from HTTP {status}"),
};
if fatal {
format!("Fatal error: {prefix} {url}")
} else {
format!("LFS: {prefix} {url}")
}
}
impl TransferError {
pub fn is_retryable(&self) -> bool {
match self {
TransferError::Http(e) => {
!e.is_decode() && !e.is_builder()
}
TransferError::ActionStatus { status, .. } => {
matches!(status, 408 | 429 | 500..=599)
}
TransferError::Io(_) => true,
TransferError::ServerObject(_)
| TransferError::NoDownloadAction
| TransferError::Store(_)
| TransferError::InvalidOid(_)
| TransferError::UnsupportedHashAlgo(_) => false,
TransferError::BatchResponse(e) => e.is_retryable(),
}
}
pub fn retry_after(&self) -> Option<Duration> {
match self {
TransferError::ActionStatus { retry_after, .. } => *retry_after,
_ => None,
}
}
}
impl From<ApiError> for TransferError {
fn from(value: ApiError) -> Self {
match value {
ApiError::Transport(e) => TransferError::Http(e),
other => {
TransferError::Io(std::io::Error::other(other.to_string()))
}
}
}
}
#[derive(Debug, Default)]
pub struct Report {
pub succeeded: Vec<String>,
pub failed: Vec<(String, TransferError)>,
}
impl Report {
pub fn is_complete_success(&self) -> bool {
self.failed.is_empty()
}
}