2025-06-18 13:09:34 +00:00
|
|
|
|
use std::{fmt, sync::LazyLock};
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
use log::trace;
|
|
|
|
|
|
use regex::Regex;
|
2025-06-08 21:31:50 +00:00
|
|
|
|
use thiserror::Error;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
use crate::file;
|
2025-06-10 18:20:52 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
use super::api::{NewShareRequest, Uri};
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
|
|
|
|
|
pub trait Client {
|
2025-06-18 13:09:34 +00:00
|
|
|
|
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<fid>[^/]+)` – capture FID (one or more non-slash chars)
|
|
|
|
|
|
/// - `$` – end of string
|
|
|
|
|
|
static UPLOAD_URL_RE: LazyLock<Regex> = LazyLock::new(|| {
|
|
|
|
|
|
trace!("compiling UPLOAD_URL_RE");
|
|
|
|
|
|
|
|
|
|
|
|
Regex::new(
|
|
|
|
|
|
r"^([^:/?#]+)://([^/?#]+)/api/v2/alias/upload/[^/]+/files/tus/(?P<fid>[^/]+)$",
|
|
|
|
|
|
)
|
|
|
|
|
|
.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!(
|
2025-06-18 14:49:30 +00:00
|
|
|
|
"Could not extract File ID from {uri:?}"
|
2025-06-18 13:09:34 +00:00
|
|
|
|
)))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
fn share_create(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
uri: &Uri,
|
|
|
|
|
|
alias_id: &str,
|
|
|
|
|
|
data: NewShareRequest,
|
|
|
|
|
|
) -> super::Result<String>;
|
|
|
|
|
|
|
|
|
|
|
|
fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> super::Result<()>;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-10 18:20:52 +00:00
|
|
|
|
fn file_create(
|
2025-06-08 17:13:01 +00:00
|
|
|
|
&self,
|
2025-06-18 13:09:34 +00:00
|
|
|
|
uri: &Uri,
|
2025-06-08 17:13:01 +00:00
|
|
|
|
alias_id: &str,
|
2025-06-18 13:09:34 +00:00
|
|
|
|
share_id: &str,
|
|
|
|
|
|
file: &file::Checked,
|
|
|
|
|
|
) -> super::Result<String>;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
fn file_patch(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
uri: &Uri,
|
|
|
|
|
|
alias_id: &str,
|
|
|
|
|
|
share_id: &str,
|
|
|
|
|
|
chunk: &file::Chunk,
|
|
|
|
|
|
) -> super::Result<()>;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
// TODO move into tests subdir
|
2025-06-12 00:54:26 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
// #[cfg(test)]
|
|
|
|
|
|
// mod tests {
|
|
|
|
|
|
// use super::*;
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
// #[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");
|
2025-06-12 00:54:26 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
// let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456"; // missing SID
|
|
|
|
|
|
// assert!(Client::get_file_id(bad).is_err());
|
2025-06-08 17:13:01 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
// let bad: &'static str = "https://example.com/api/v2/alias/upload/SID123/files/tus/"; // missing FID
|
|
|
|
|
|
// assert!(Client::get_file_id(bad).is_err());
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
2025-06-08 21:31:50 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
|
|
pub enum Parameter {
|
|
|
|
|
|
#[error("given URI {0:?}")]
|
2025-06-18 14:49:30 +00:00
|
|
|
|
Uri(String),
|
2025-06-08 21:31:50 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
#[error("given Alias ID {0:?}")]
|
|
|
|
|
|
AliasID(String),
|
2025-06-08 21:31:50 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
#[error("stored Share ID {0:?}")]
|
|
|
|
|
|
ShareID(String),
|
|
|
|
|
|
|
|
|
|
|
|
#[error("stored File ID {0:?}")]
|
|
|
|
|
|
FileID(String),
|
2025-06-08 01:20:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
impl Parameter {
|
|
|
|
|
|
fn is_fatal(&self) -> bool {
|
2025-06-18 14:49:30 +00:00
|
|
|
|
matches!(self, Self::Uri(_) | Self::AliasID(_))
|
2025-06-11 18:17:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
#[derive(Debug, Error)]
|
|
|
|
|
|
pub enum ClientError {
|
|
|
|
|
|
#[error(transparent)]
|
|
|
|
|
|
StdIo(#[from] std::io::Error),
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
#[error("response error: {0}")]
|
|
|
|
|
|
Response(String),
|
2025-06-10 18:20:52 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
#[error("Invalid {0}")]
|
|
|
|
|
|
InvalidParameter(Parameter),
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
#[error("Unknown error: {0}")]
|
|
|
|
|
|
Unknown(String),
|
|
|
|
|
|
}
|
2025-06-08 01:20:41 +00:00
|
|
|
|
|
2025-06-18 18:14:39 +00:00
|
|
|
|
#[allow(clippy::needless_pass_by_value)]
|
2025-06-18 14:49:30 +00:00
|
|
|
|
fn into_string(val: impl ToString) -> String {
|
2025-06-18 18:14:39 +00:00
|
|
|
|
val.to_string()
|
2025-06-18 14:49:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
impl ClientError {
|
|
|
|
|
|
pub fn res_status_check<T>(actual: T, expected: T) -> super::Result<()>
|
|
|
|
|
|
where
|
|
|
|
|
|
T: PartialEq + fmt::Display + Copy,
|
|
|
|
|
|
{
|
|
|
|
|
|
if actual == expected {
|
|
|
|
|
|
Ok(())
|
2025-06-08 01:20:41 +00:00
|
|
|
|
} else {
|
2025-06-18 13:09:34 +00:00
|
|
|
|
Err(Self::Response(format!(
|
|
|
|
|
|
"unexpected status: {actual} (expected {expected})"
|
|
|
|
|
|
)))
|
2025-06-08 01:20:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
pub fn response(e: impl ToString) -> Self {
|
2025-06-18 14:49:30 +00:00
|
|
|
|
Self::Response(into_string(e))
|
2025-06-08 01:20:41 +00:00
|
|
|
|
}
|
2025-06-08 17:13:01 +00:00
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
pub fn unknown(e: impl ToString) -> Self {
|
2025-06-18 14:49:30 +00:00
|
|
|
|
Self::Unknown(into_string(e))
|
2025-06-10 01:17:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-18 13:09:34 +00:00
|
|
|
|
pub fn is_fatal(&self) -> bool {
|
|
|
|
|
|
match self {
|
|
|
|
|
|
Self::InvalidParameter(p) => p.is_fatal(),
|
|
|
|
|
|
Self::Unknown(_) => true,
|
|
|
|
|
|
_ => false,
|
2025-06-10 01:17:17 +00:00
|
|
|
|
}
|
2025-06-08 17:13:01 +00:00
|
|
|
|
}
|
2025-06-08 01:20:41 +00:00
|
|
|
|
}
|