use std::{fmt, sync::LazyLock}; use log::trace; use regex::Regex; use thiserror::Error; use crate::file; use super::api::{NewShareRequest, Uri}; pub trait Client { fn get_file_id(uri: &str) -> super::Result<&str> { /// Pattern breakdown: /// - `^([^:/?#]+)://` – scheme (anything but `:/?#`) + `"://"` /// - `([^/?#]+)` – authority/host (anything but `/?#`) /// - `/api/v2/alias/upload/` – literal path segment /// - `([^/]+)` – capture SID (one or more non-slash chars) /// - `/files/tus/` – literal path segment /// - `(?P[^/]+)` – capture FID (one or more non-slash chars) /// - `$` – end of string static UPLOAD_URL_RE: LazyLock = LazyLock::new(|| { trace!("compiling UPLOAD_URL_RE"); Regex::new( r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P[^/]+)$", ) .expect("Regex compilation failed") }); if let Some(fid) = UPLOAD_URL_RE .captures(uri) .and_then(|caps| caps.name("fid").map(|m| m.as_str())) { Ok(fid) } else { Err(super::ClientError::unknown(format!( "Could not extract File ID from {uri:?}" ))) } } fn share_create( &self, uri: &Uri, alias_id: &str, data: NewShareRequest, ) -> super::Result; fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> super::Result<()>; fn file_create( &self, uri: &Uri, alias_id: &str, share_id: &str, file: &file::Checked, ) -> super::Result; fn file_patch( &self, uri: &Uri, alias_id: &str, share_id: &str, chunk: &file::Chunk, ) -> super::Result<()>; } // TODO move into tests subdir // #[cfg(test)] // mod tests { // use super::*; // #[test] // fn test_get_file_id() { // let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456"; // let good = Client::get_file_id(good); // assert!(good.is_ok()); // assert_eq!(good.unwrap(), "FID456"); // let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456"; // missing SID // assert!(Client::get_file_id(bad).is_err()); // let bad: &'static str = "https://example.com/api/v2/alias/upload/SID123/files/tus/"; // missing FID // assert!(Client::get_file_id(bad).is_err()); // } // } #[derive(Debug, Error)] pub enum Parameter { #[error("given URI {0:?}")] Uri(String), #[error("given Alias ID {0:?}")] AliasID(String), #[error("stored Share ID {0:?}")] ShareID(String), #[error("stored File ID {0:?}")] FileID(String), } impl Parameter { fn is_fatal(&self) -> bool { matches!(self, Self::Uri(_) | Self::AliasID(_)) } } #[derive(Debug, Error)] pub enum ClientError { #[error(transparent)] StdIo(#[from] std::io::Error), #[error("response error: {0}")] Response(String), #[error("Invalid {0}")] InvalidParameter(Parameter), #[error("Unknown error: {0}")] Unknown(String), } #[allow(clippy::needless_pass_by_value)] fn into_string(val: impl ToString) -> String { val.to_string() } impl ClientError { pub fn res_status_check(actual: T, expected: T) -> super::Result<()> where T: PartialEq + fmt::Display + Copy, { if actual == expected { Ok(()) } else { Err(Self::Response(format!( "unexpected status: {actual} (expected {expected})" ))) } } pub fn response(e: impl ToString) -> Self { Self::Response(into_string(e)) } pub fn unknown(e: impl ToString) -> Self { Self::Unknown(into_string(e)) } pub fn is_fatal(&self) -> bool { match self { Self::InvalidParameter(p) => p.is_fatal(), Self::Unknown(_) => true, _ => false, } } }