diff --git a/src/error.rs b/src/error.rs index 515f567..28cc047 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,5 +1,7 @@ use std::fmt; +use crate::sharry; + #[derive(Debug, thiserror::Error)] pub enum Parameter { #[error("given URI {0:?}")] @@ -11,8 +13,8 @@ pub enum Parameter { #[error("stored Share ID {0:?}")] ShareID(String), - #[error("stored File ID {0:?}")] - FileID(String), + #[error("stored {0:?}")] + FileID(sharry::FileID), } impl Parameter { diff --git a/src/file/chunk.rs b/src/file/chunk.rs index da46daf..809482e 100644 --- a/src/file/chunk.rs +++ b/src/file/chunk.rs @@ -1,7 +1,9 @@ use std::fmt; +use crate::sharry; + pub struct Chunk<'t> { - file_id: String, + file_id: sharry::FileID, offset: u64, data: &'t [u8], } @@ -17,7 +19,7 @@ impl fmt::Debug for Chunk<'_> { } impl<'t> Chunk<'t> { - pub fn new(file_id: String, offset: u64, data: &'t [u8]) -> Self { + pub fn new(file_id: sharry::FileID, offset: u64, data: &'t [u8]) -> Self { Self { file_id, offset, @@ -25,7 +27,7 @@ impl<'t> Chunk<'t> { } } - pub fn get_file_id(&self) -> &str { + pub fn get_file_id(&self) -> &sharry::FileID { &self.file_id } diff --git a/src/file/uploading.rs b/src/file/uploading.rs index 079317b..66f33d5 100644 --- a/src/file/uploading.rs +++ b/src/file/uploading.rs @@ -7,6 +7,8 @@ use std::{ use log::warn; use serde::{Deserialize, Serialize}; +use crate::sharry; + use super::{Checked, Chunk, FileTrait}; #[derive(Serialize, Deserialize, Debug)] @@ -17,14 +19,19 @@ pub struct Uploading { size: u64, /// hash of that file hash: Option, - file_id: String, + file_id: sharry::FileID, #[serde(skip)] last_offset: Option, offset: u64, } impl Uploading { - pub(super) fn new(path: PathBuf, size: u64, hash: Option, file_id: String) -> Self { + pub(super) fn new( + path: PathBuf, + size: u64, + hash: Option, + file_id: sharry::FileID, + ) -> Self { Self { path, size, diff --git a/src/impl_ureq.rs b/src/impl_ureq.rs index 1ffd188..8e8f87a 100644 --- a/src/impl_ureq.rs +++ b/src/impl_ureq.rs @@ -3,14 +3,14 @@ use log::{debug, trace}; use crate::{ error, file::{self, FileTrait}, - sharry::{self, Uri}, + sharry::{self, FileID, Uri}, }; fn find_cause( uri: &Uri, alias_id: &str, share_id: Option<&str>, - file_id: Option<&str>, + file_id: Option<&FileID>, ) -> impl FnOnce(ureq::Error) -> error::Error { move |error| match error { ureq::Error::StatusCode(403) => { @@ -22,7 +22,7 @@ fn find_cause( trace!("HTTP Error 404: Share and/or file may have been deleted!"); if let Some(file_id) = file_id { - error::Error::InvalidParameter(error::Parameter::FileID(file_id.to_owned())) + error::Error::InvalidParameter(error::Parameter::FileID(file_id.clone())) } else if let Some(share_id) = share_id { error::Error::InvalidParameter(error::Parameter::ShareID(share_id.to_owned())) } else { @@ -110,7 +110,7 @@ impl sharry::Client for ureq::Agent { alias_id: &str, share_id: &str, file: &file::Checked, - ) -> error::Result { + ) -> error::Result { let res = { let endpoint = uri.file_create(share_id); @@ -133,11 +133,7 @@ impl sharry::Client for ureq::Agent { .map_err(error::Error::response)? .to_string(); - let file_id = Self::get_file_id(&location)?; - - debug!("location: {location:?}, file_id: {file_id:?}"); - - Ok(file_id.to_owned()) + FileID::try_from(location) } fn file_patch( diff --git a/src/sharry/api.rs b/src/sharry/api.rs index c34ab31..f12894d 100644 --- a/src/sharry/api.rs +++ b/src/sharry/api.rs @@ -1,8 +1,11 @@ -use std::fmt; +use std::{fmt, sync::LazyLock}; -use log::trace; +use log::{debug, trace}; +use regex::Regex; use serde::{Deserialize, Serialize}; +use crate::error; + #[derive(Serialize, Deserialize, Debug)] pub struct Uri(String); @@ -41,11 +44,89 @@ impl Uri { self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) } - pub fn file_patch(&self, share_id: &str, file_id: &str) -> String { + pub fn file_patch(&self, share_id: &str, file_id: &FileID) -> String { self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}")) } } +// pub struct AliasID(String); +// pub struct ShareID(String); + +#[derive(Serialize, Deserialize, Clone)] +pub struct FileID(String); + +impl fmt::Display for FileID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl fmt::Debug for FileID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("File ID").field(&self.0).finish() + } +} + +impl TryFrom for FileID { + type Error = error::Error; + + fn try_from(value: String) -> error::Result { + /// 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") + }); + + trace!("TryFrom {value:?}"); + + if let Some(fid) = UPLOAD_URL_RE + .captures(&value) + .and_then(|caps| caps.name("fid").map(|m| m.as_str())) + { + let result = Self(fid.to_owned()); + debug!("{result:?}"); + + Ok(result) + } else { + Err(error::Error::unknown(format!( + "Could not extract File ID from {value:?}" + ))) + } + } +} + +// 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(Serialize, Debug)] #[allow(non_snake_case)] pub struct NewShareRequest { diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 517a40a..8d45f40 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -1,43 +1,8 @@ -use std::sync::LazyLock; - -use log::trace; -use regex::Regex; - use crate::{error, file}; -use super::api::{NewShareRequest, Uri}; +use super::api::{FileID, NewShareRequest, Uri}; pub trait Client { - fn get_file_id(uri: &str) -> error::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(error::Error::unknown(format!( - "Could not extract File ID from {uri:?}" - ))) - } - } - fn share_create( &self, uri: &Uri, @@ -53,7 +18,7 @@ pub trait Client { alias_id: &str, share_id: &str, file: &file::Checked, - ) -> error::Result; + ) -> error::Result; fn file_patch( &self, @@ -63,24 +28,3 @@ pub trait Client { chunk: &file::Chunk, ) -> error::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()); -// } -// } diff --git a/src/sharry/mod.rs b/src/sharry/mod.rs index 5da85d2..a37d19c 100644 --- a/src/sharry/mod.rs +++ b/src/sharry/mod.rs @@ -1,5 +1,5 @@ mod api; mod client; -pub use api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}; +pub use api::{FileID, NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}; pub use client::Client;