From f1c6eb5d756f0679d7a1a2407dc7d35e4555ecd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Wed, 25 Jun 2025 23:15:33 +0000 Subject: [PATCH 1/7] create `struct sharry::api::FileID` --- src/error.rs | 6 ++-- src/file/chunk.rs | 8 +++-- src/file/uploading.rs | 11 ++++-- src/impl_ureq.rs | 14 +++----- src/sharry/api.rs | 81 +++++++++++++++++++++++++++++++++++++++++-- src/sharry/client.rs | 60 ++------------------------------ src/sharry/mod.rs | 2 +- 7 files changed, 104 insertions(+), 78 deletions(-) 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..afba37f 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,83 @@ 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, Debug, 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 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; From c9c21aa12840bde9efe722a32bdf39177ef36d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Wed, 25 Jun 2025 23:42:00 +0000 Subject: [PATCH 2/7] split `api.rs` into modules - `id` for multiple "ID" types - `json` for types directly interacting with the Sharry API - `uri` for the `Uri` type - activate testing --- .vscode/tasks.json | 42 +++++------ src/sharry/api.rs | 162 ----------------------------------------- src/sharry/api/id.rs | 83 +++++++++++++++++++++ src/sharry/api/json.rs | 41 +++++++++++ src/sharry/api/mod.rs | 7 ++ src/sharry/api/uri.rs | 47 ++++++++++++ 6 files changed, 199 insertions(+), 183 deletions(-) delete mode 100644 src/sharry/api.rs create mode 100644 src/sharry/api/id.rs create mode 100644 src/sharry/api/json.rs create mode 100644 src/sharry/api/mod.rs create mode 100644 src/sharry/api/uri.rs diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ceeafb4..574737d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -43,16 +43,16 @@ "problemMatcher": "$rustc", "group": "build" }, - // { - // "label": "Run Unit Tests", - // "type": "cargo", - // "command": "test", - // "args": [ - // "--lib" - // ], - // "problemMatcher": "$rustc", - // "group": "test" - // }, + { + "label": "Run Unit Tests", + "type": "cargo", + "command": "test", + "args": [ + "--lib" + ], + "problemMatcher": "$rustc", + "group": "test" + }, // { // "label": "Run Integration Tests", // "type": "cargo", @@ -64,16 +64,16 @@ // "problemMatcher": "$rustc", // "group": "test" // }, - // { - // "label": "Run All Tests", - // "type": "shell", - // "command": "echo All Tests successful!", - // "dependsOn": [ - // "Run Unit Tests", - // "Run Integration Tests" - // ], - // "dependsOrder": "sequence", - // "group": "test" - // } + { + "label": "Run All Tests", + "type": "shell", + "command": "echo All Tests successful!", + "dependsOn": [ + "Run Unit Tests", + "Run Integration Tests" + ], + "dependsOrder": "sequence", + "group": "test" + } ], } \ No newline at end of file diff --git a/src/sharry/api.rs b/src/sharry/api.rs deleted file mode 100644 index afba37f..0000000 --- a/src/sharry/api.rs +++ /dev/null @@ -1,162 +0,0 @@ -use std::{fmt, sync::LazyLock}; - -use log::{debug, trace}; -use regex::Regex; -use serde::{Deserialize, Serialize}; - -use crate::error; - -#[derive(Serialize, Deserialize, Debug)] -pub struct Uri(String); - -impl fmt::Display for Uri { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -impl AsRef<[u8]> for Uri { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Uri { - pub fn new(protocol: impl fmt::Display, base_url: impl fmt::Display) -> Self { - Self(format!("{protocol}://{base_url}")) - } - - fn endpoint(&self, path: fmt::Arguments) -> String { - let uri = format!("{}/api/v2/{path}", self.0); - trace!("endpoint: {uri:?}"); - uri - } - - pub fn share_create(&self) -> String { - self.endpoint(format_args!("alias/upload/new")) - } - - pub fn share_notify(&self, share_id: &str) -> String { - self.endpoint(format_args!("alias/mail/notify/{share_id}")) - } - - pub fn file_create(&self, share_id: &str) -> String { - self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) - } - - 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, Debug, 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 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 { - name: String, - validity: u32, - description: Option, - maxViews: u32, - password: Option, -} - -impl NewShareRequest { - pub fn new( - name: impl Into, - description: Option>, - max_views: u32, - ) -> Self { - Self { - name: name.into(), - validity: 0, - description: description.map(Into::into), - maxViews: max_views, - password: None, - } - } -} - -#[derive(Deserialize, Debug)] -pub struct NewShareResponse { - pub success: bool, - pub message: String, - pub id: String, -} - -#[derive(Deserialize, Debug)] -#[allow(dead_code)] -pub struct NotifyShareResponse { - pub success: bool, - pub message: String, -} diff --git a/src/sharry/api/id.rs b/src/sharry/api/id.rs new file mode 100644 index 0000000..71b7610 --- /dev/null +++ b/src/sharry/api/id.rs @@ -0,0 +1,83 @@ +use std::{fmt, sync::LazyLock}; + +use log::{debug, trace}; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +use crate::error; + +// pub struct AliasID(String); +// pub struct ShareID(String); + +#[derive(Serialize, Deserialize, Debug, 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 AsRef for FileID { + fn as_ref(&self) -> &str { + &self.0 + } +} + +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:?}" + ))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_fileid_tryfrom_string() { + let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456".to_owned(); + let good = FileID::try_from(good); + assert!(good.is_ok()); + assert_eq!(good.unwrap().as_ref(), "FID456"); + + let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456".to_owned(); // missing SID + assert!(FileID::try_from(bad).is_err()); + + let bad = "https://example.com/api/v2/alias/upload/SID123/files/tus/".to_owned(); // missing FID + assert!(FileID::try_from(bad).is_err()); + } +} diff --git a/src/sharry/api/json.rs b/src/sharry/api/json.rs new file mode 100644 index 0000000..2f6f680 --- /dev/null +++ b/src/sharry/api/json.rs @@ -0,0 +1,41 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Debug)] +#[allow(non_snake_case)] +pub struct NewShareRequest { + name: String, + validity: u32, + description: Option, + maxViews: u32, + password: Option, +} + +impl NewShareRequest { + pub fn new( + name: impl Into, + description: Option>, + max_views: u32, + ) -> Self { + Self { + name: name.into(), + validity: 0, + description: description.map(Into::into), + maxViews: max_views, + password: None, + } + } +} + +#[derive(Deserialize, Debug)] +pub struct NewShareResponse { + pub success: bool, + pub message: String, + pub id: String, +} + +#[derive(Deserialize, Debug)] +#[allow(dead_code)] +pub struct NotifyShareResponse { + pub success: bool, + pub message: String, +} diff --git a/src/sharry/api/mod.rs b/src/sharry/api/mod.rs new file mode 100644 index 0000000..5e17abf --- /dev/null +++ b/src/sharry/api/mod.rs @@ -0,0 +1,7 @@ +mod id; +mod json; +mod uri; + +pub use id::FileID; +pub use json::{NewShareRequest, NewShareResponse, NotifyShareResponse}; +pub use uri::Uri; diff --git a/src/sharry/api/uri.rs b/src/sharry/api/uri.rs new file mode 100644 index 0000000..e3c0c5f --- /dev/null +++ b/src/sharry/api/uri.rs @@ -0,0 +1,47 @@ +use std::fmt; + +use log::trace; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Uri(String); + +impl fmt::Display for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl AsRef<[u8]> for Uri { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl Uri { + pub fn new(protocol: impl fmt::Display, base_url: impl fmt::Display) -> Self { + Self(format!("{protocol}://{base_url}")) + } + + fn endpoint(&self, path: fmt::Arguments) -> String { + let uri = format!("{}/api/v2/{path}", self.0); + trace!("endpoint: {uri:?}"); + uri + } + + pub fn share_create(&self) -> String { + self.endpoint(format_args!("alias/upload/new")) + } + + pub fn share_notify(&self, share_id: &str) -> String { + self.endpoint(format_args!("alias/mail/notify/{share_id}")) + } + + pub fn file_create(&self, share_id: &str) -> String { + self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) + } + + pub fn file_patch(&self, share_id: &str, file_id: &super::FileID) -> String { + self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}")) + } +} From e0c5b5517f1fd59cde143e9788974dd88600dcff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:06:43 +0000 Subject: [PATCH 3/7] better unit testing for `FileID` --- src/sharry/api/id.rs | 64 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/src/sharry/api/id.rs b/src/sharry/api/id.rs index 4f88db3..074b4fe 100644 --- a/src/sharry/api/id.rs +++ b/src/sharry/api/id.rs @@ -69,16 +69,62 @@ mod tests { use super::*; #[test] - fn test_fileid_tryfrom_string() { - let good = "https://example.com/api/v2/alias/upload/SID123/files/tus/FID456".to_owned(); - let good = FileID::try_from(good); - assert!(good.is_ok()); - assert_eq!(good.unwrap().as_ref(), "FID456"); + fn valid_urls_produce_expected_file_id() { + // a handful of valid‐looking URLs + let cases = vec![ + ( + "http://example.com/api/v2/alias/upload/SID123/files/tus/FID456", + "FID456", + ), + ( + "https://my-host:8080/api/v2/alias/upload/another-SID/files/tus/some-file-id", + "some-file-id", + ), + ( + "custom+scheme://host/api/v2/alias/upload/x/files/tus/y", + "y", + ), + ]; - let bad = "https://example.com/api/v2/alias/upload//files/tus/FID456".to_owned(); // missing SID - assert!(FileID::try_from(bad).is_err()); + for (good, expected_fid) in cases { + let s = good.to_string(); + let file_id = FileID::try_from(s.clone()).expect("URL should parse successfully"); + assert_eq!( + file_id.0, expected_fid, + "Expected `{}` → FileID({}), got {:?}", + good, expected_fid, file_id + ); + } + } - let bad = "https://example.com/api/v2/alias/upload/SID123/files/tus/".to_owned(); // missing FID - assert!(FileID::try_from(bad).is_err()); + #[test] + fn invalid_urls_return_error() { + let bad_inputs = vec![ + // missing /api/v2/alias/upload + "http://example.com/files/tus/FID", + // missing /files/tus + "http://example.com/api/v2/alias/upload/SID123/FID456", + // trailing slash (doesn't match `$`) + "http://example.com/api/v2/alias/upload/SID/files/tus/FID/", + // empty fid + "http://example.com/api/v2/alias/upload/SID/files/tus/", + // random string + "just-a-random-string", + ]; + + for bad in bad_inputs { + let err = FileID::try_from(bad.to_string()).expect_err("URL should not parse"); + // make sure it's the Mismatch variant, and that it contains the original input + match err { + error::Error::Mismatch { expected, actual } => { + assert_eq!( + expected, ":///api/v2/alias/upload//files/tus/", + "Error should output expected format" + ); + assert_eq!(actual, bad.to_string(), "Error should echo back the input"); + } + _ => panic!("Expected Error::Mismatch for input `{bad}` but got {err:?}"), + } + } } } From d8c48b74ca247039c093f0463e2907dafcffc88e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:15:54 +0000 Subject: [PATCH 4/7] remove unnecessary `impl AsRef for FileID` --- src/sharry/api/id.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/sharry/api/id.rs b/src/sharry/api/id.rs index 074b4fe..a951f82 100644 --- a/src/sharry/api/id.rs +++ b/src/sharry/api/id.rs @@ -18,12 +18,6 @@ impl fmt::Display for FileID { } } -impl AsRef for FileID { - fn as_ref(&self) -> &str { - &self.0 - } -} - impl TryFrom for FileID { type Error = error::Error; From c7b24b12502898ba0d893a324159166716d102f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Fri, 27 Jun 2025 01:47:38 +0000 Subject: [PATCH 5/7] add `id::{AliasID, ShareID}` --- src/appstate.rs | 4 ++-- src/cachefile.rs | 8 ++++---- src/cli.rs | 6 +++--- src/error.rs | 4 ++-- src/file/checked.rs | 4 ++-- src/impl_ureq.rs | 32 ++++++++++++++++---------------- src/sharry/api/id.rs | 37 +++++++++++++++++++++++++++++++++++-- src/sharry/api/mod.rs | 2 +- src/sharry/api/uri.rs | 6 +++--- src/sharry/client.rs | 19 +++++++++++-------- src/sharry/mod.rs | 4 +++- 11 files changed, 82 insertions(+), 44 deletions(-) diff --git a/src/appstate.rs b/src/appstate.rs index db76e68..b3a080a 100644 --- a/src/appstate.rs +++ b/src/appstate.rs @@ -9,7 +9,7 @@ use crate::{ error, file::{Chunk, FileTrait}, output::new_progressbar, - sharry::Client, + sharry::{Client, ShareID}, }; pub struct AppState { @@ -33,7 +33,7 @@ fn new_http(args: &Cli) -> ureq::Agent { .into() } -fn new_share(args: &Cli) -> error::Result { +fn new_share(args: &Cli) -> error::Result { new_http(args).share_create(&args.get_uri(), &args.alias, args.get_share_request()) } diff --git a/src/cachefile.rs b/src/cachefile.rs index 9d0dba3..6ce65bd 100644 --- a/src/cachefile.rs +++ b/src/cachefile.rs @@ -15,7 +15,7 @@ use crate::{ error, file::{self, Chunk, FileTrait}, output::new_progressbar, - sharry::{Client, Uri}, + sharry::{AliasID, Client, ShareID, Uri}, }; #[derive(Serialize, Deserialize, Debug)] @@ -24,8 +24,8 @@ pub struct CacheFile { file_name: PathBuf, uri: Uri, - alias_id: String, - share_id: String, + alias_id: AliasID, + share_id: ShareID, uploading: Option, files: VecDeque, @@ -98,7 +98,7 @@ impl CacheFile { pub fn from_args( args: &Cli, - new_share: impl FnOnce(&Cli) -> error::Result, + new_share: impl FnOnce(&Cli) -> error::Result, ) -> error::Result { let mut files = args.files.clone(); diff --git a/src/cli.rs b/src/cli.rs index 491be5d..5b5f3c8 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,7 +11,7 @@ use log::LevelFilter; use crate::{ file::{Checked, FileTrait}, - sharry::{NewShareRequest, Uri}, + sharry::{AliasID, NewShareRequest, Uri}, }; #[derive(Parser)] @@ -69,7 +69,7 @@ pub struct Cli { url: String, /// ID of a public alias to use - pub alias: String, + pub alias: AliasID, /// Files to upload to the new share #[arg(value_name = "FILE", required = true, value_parser = parse_sharry_file)] @@ -159,7 +159,7 @@ impl Cli { let mut hasher = Blake2b::new().hash_length(16).to_state(); hasher.update(self.get_uri().as_ref()); - hasher.update(self.alias.as_bytes()); + hasher.update(self.alias.as_ref()); for chk in sorted(&self.files) { hasher.update(chk.as_ref()); diff --git a/src/error.rs b/src/error.rs index ffbe246..d708d96 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,10 +8,10 @@ pub enum Parameter { Uri(String), #[error("given Alias ID {0:?}")] - AliasID(String), + AliasID(sharry::AliasID), #[error("stored Share ID {0:?}")] - ShareID(String), + ShareID(sharry::ShareID), #[error("stored {0:?}")] FileID(sharry::FileID), diff --git a/src/file/checked.rs b/src/file/checked.rs index 92e9ef7..e5c8fc5 100644 --- a/src/file/checked.rs +++ b/src/file/checked.rs @@ -76,8 +76,8 @@ impl Checked { self, client: &impl sharry::Client, uri: &sharry::Uri, - alias_id: &str, - share_id: &str, + alias_id: &sharry::AliasID, + share_id: &sharry::ShareID, ) -> error::Result { let file_id = client.file_create(uri, alias_id, share_id, &self)?; diff --git a/src/impl_ureq.rs b/src/impl_ureq.rs index 0a2721c..2e50a8b 100644 --- a/src/impl_ureq.rs +++ b/src/impl_ureq.rs @@ -3,13 +3,13 @@ use log::{debug, trace}; use crate::{ error, file::{self, FileTrait}, - sharry::{self, FileID, Uri}, + sharry::{self, AliasID, FileID, ShareID, Uri}, }; fn find_cause( uri: &Uri, - alias_id: &str, - share_id: Option<&str>, + alias_id: &AliasID, + share_id: Option<&ShareID>, file_id: Option<&FileID>, ) -> impl FnOnce(ureq::Error) -> error::Error { move |error| match error { @@ -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.clone())) + error::Error::InvalidParameter(error::Parameter::FileID(file_id.to_owned())) } else if let Some(share_id) = share_id { error::Error::InvalidParameter(error::Parameter::ShareID(share_id.to_owned())) } else { @@ -50,15 +50,15 @@ impl sharry::Client for ureq::Agent { fn share_create( &self, uri: &Uri, - alias_id: &str, + alias_id: &AliasID, data: sharry::NewShareRequest, - ) -> error::Result { + ) -> error::Result { let res = { let endpoint = uri.share_create(); let mut res = self .post(&endpoint) - .header("Sharry-Alias", alias_id) + .header("Sharry-Alias", alias_id.as_ref()) .send_json(data) .map_err(find_cause(uri, alias_id, None, None))?; @@ -75,19 +75,19 @@ impl sharry::Client for ureq::Agent { if res.success && (res.message == "Share created.") { trace!("new share id: {:?}", res.id); - Ok(res.id) + Ok(res.id.into()) } else { Err(error::Error::response(format!("{res:?}"))) } } - fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> error::Result<()> { + fn share_notify(&self, uri: &Uri, alias_id: &AliasID, share_id: &ShareID) -> error::Result<()> { let res = { let endpoint = uri.share_notify(share_id); let mut res = self .post(&endpoint) - .header("Sharry-Alias", alias_id) + .header("Sharry-Alias", alias_id.as_ref()) .send_empty() .map_err(find_cause(uri, alias_id, Some(share_id), None))?; @@ -107,8 +107,8 @@ impl sharry::Client for ureq::Agent { fn file_create( &self, uri: &Uri, - alias_id: &str, - share_id: &str, + alias_id: &AliasID, + share_id: &ShareID, file: &file::Checked, ) -> error::Result { let res = { @@ -116,7 +116,7 @@ impl sharry::Client for ureq::Agent { let res = self .post(&endpoint) - .header("Sharry-Alias", alias_id) + .header("Sharry-Alias", alias_id.as_ref()) .header("Sharry-File-Name", file.get_name()) .header("Upload-Length", file.get_size()) .send_empty() @@ -139,8 +139,8 @@ impl sharry::Client for ureq::Agent { fn file_patch( &self, uri: &Uri, - alias_id: &str, - share_id: &str, + alias_id: &AliasID, + share_id: &ShareID, chunk: &file::Chunk, ) -> error::Result<()> { let res = { @@ -148,7 +148,7 @@ impl sharry::Client for ureq::Agent { let res = self .patch(&endpoint) - .header("Sharry-Alias", alias_id) + .header("Sharry-Alias", alias_id.as_ref()) .header("Upload-Offset", chunk.get_offset()) .send(chunk.get_data()) .map_err(find_cause( diff --git a/src/sharry/api/id.rs b/src/sharry/api/id.rs index a951f82..cd4d6a3 100644 --- a/src/sharry/api/id.rs +++ b/src/sharry/api/id.rs @@ -6,8 +6,41 @@ use serde::{Deserialize, Serialize}; use crate::error; -// pub struct AliasID(String); -// pub struct ShareID(String); +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AliasID(String); + +impl fmt::Display for AliasID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl AsRef<[u8]> for AliasID { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl From for AliasID { + fn from(value: String) -> Self { + Self(value) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct ShareID(String); + +impl fmt::Display for ShareID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.0) + } +} + +impl From for ShareID { + fn from(value: String) -> Self { + Self(value) + } +} #[derive(Serialize, Deserialize, Debug, Clone)] pub struct FileID(String); diff --git a/src/sharry/api/mod.rs b/src/sharry/api/mod.rs index 5e17abf..b272b6b 100644 --- a/src/sharry/api/mod.rs +++ b/src/sharry/api/mod.rs @@ -2,6 +2,6 @@ mod id; mod json; mod uri; -pub use id::FileID; +pub use id::{AliasID, FileID, ShareID}; pub use json::{NewShareRequest, NewShareResponse, NotifyShareResponse}; pub use uri::Uri; diff --git a/src/sharry/api/uri.rs b/src/sharry/api/uri.rs index e3c0c5f..ea3e647 100644 --- a/src/sharry/api/uri.rs +++ b/src/sharry/api/uri.rs @@ -33,15 +33,15 @@ impl Uri { self.endpoint(format_args!("alias/upload/new")) } - pub fn share_notify(&self, share_id: &str) -> String { + pub fn share_notify(&self, share_id: &super::ShareID) -> String { self.endpoint(format_args!("alias/mail/notify/{share_id}")) } - pub fn file_create(&self, share_id: &str) -> String { + pub fn file_create(&self, share_id: &super::ShareID) -> String { self.endpoint(format_args!("alias/upload/{share_id}/files/tus")) } - pub fn file_patch(&self, share_id: &str, file_id: &super::FileID) -> String { + pub fn file_patch(&self, share_id: &super::ShareID, file_id: &super::FileID) -> String { self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}")) } } diff --git a/src/sharry/client.rs b/src/sharry/client.rs index 8d45f40..5cf3e30 100644 --- a/src/sharry/client.rs +++ b/src/sharry/client.rs @@ -1,30 +1,33 @@ use crate::{error, file}; -use super::api::{FileID, NewShareRequest, Uri}; +use super::{ + AliasID, ShareID, + api::{FileID, NewShareRequest, Uri}, +}; pub trait Client { fn share_create( &self, uri: &Uri, - alias_id: &str, + alias_id: &AliasID, data: NewShareRequest, - ) -> error::Result; + ) -> error::Result; - fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> error::Result<()>; + fn share_notify(&self, uri: &Uri, alias_id: &AliasID, share_id: &ShareID) -> error::Result<()>; fn file_create( &self, uri: &Uri, - alias_id: &str, - share_id: &str, + alias_id: &AliasID, + share_id: &ShareID, file: &file::Checked, ) -> error::Result; fn file_patch( &self, uri: &Uri, - alias_id: &str, - share_id: &str, + alias_id: &AliasID, + share_id: &ShareID, chunk: &file::Chunk, ) -> error::Result<()>; } diff --git a/src/sharry/mod.rs b/src/sharry/mod.rs index a37d19c..8b53aca 100644 --- a/src/sharry/mod.rs +++ b/src/sharry/mod.rs @@ -1,5 +1,7 @@ mod api; mod client; -pub use api::{FileID, NewShareRequest, NewShareResponse, NotifyShareResponse, Uri}; +pub use api::{ + AliasID, FileID, NewShareRequest, NewShareResponse, NotifyShareResponse, ShareID, Uri, +}; pub use client::Client; From 357f455ec0f6137f52b4d818be39d432ac1eae89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Fri, 27 Jun 2025 02:03:20 +0000 Subject: [PATCH 6/7] import strategy for error module (cont.) --- src/sharry/api/id.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sharry/api/id.rs b/src/sharry/api/id.rs index cd4d6a3..6f9121d 100644 --- a/src/sharry/api/id.rs +++ b/src/sharry/api/id.rs @@ -4,8 +4,6 @@ use log::{debug, trace}; use regex::Regex; use serde::{Deserialize, Serialize}; -use crate::error; - #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AliasID(String); @@ -52,9 +50,9 @@ impl fmt::Display for FileID { } impl TryFrom for FileID { - type Error = error::Error; + type Error = crate::Error; - fn try_from(value: String) -> error::Result { + fn try_from(value: String) -> crate::Result { /// Pattern breakdown: /// - `^([^:/?#]+)://` – scheme (anything but `:/?#`) + `"://"` /// - `([^/?#]+)` – authority/host (anything but `/?#`) @@ -83,7 +81,7 @@ impl TryFrom for FileID { Ok(result) } else { - Err(error::Error::mismatch( + Err(crate::Error::mismatch( ":///api/v2/alias/upload//files/tus/", value, )) @@ -143,7 +141,7 @@ mod tests { let err = FileID::try_from(bad.to_string()).expect_err("URL should not parse"); // make sure it's the Mismatch variant, and that it contains the original input match err { - error::Error::Mismatch { expected, actual } => { + crate::Error::Mismatch { expected, actual } => { assert_eq!( expected, ":///api/v2/alias/upload//files/tus/", "Error should output expected format" From 3f00e084224199ca1f14a28ab7649c2efd8930f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rn-Michael=20Miehe?= <40151420+ldericher@users.noreply.github.com> Date: Fri, 27 Jun 2025 08:29:57 +0000 Subject: [PATCH 7/7] consolidate `sharry::api` module --- src/sharry/api/{id.rs => ids.rs} | 0 src/sharry/api/json.rs | 41 --------------------------- src/sharry/api/mod.rs | 48 +++++++++++++++++++++++++++++--- 3 files changed, 44 insertions(+), 45 deletions(-) rename src/sharry/api/{id.rs => ids.rs} (100%) delete mode 100644 src/sharry/api/json.rs diff --git a/src/sharry/api/id.rs b/src/sharry/api/ids.rs similarity index 100% rename from src/sharry/api/id.rs rename to src/sharry/api/ids.rs diff --git a/src/sharry/api/json.rs b/src/sharry/api/json.rs deleted file mode 100644 index 2f6f680..0000000 --- a/src/sharry/api/json.rs +++ /dev/null @@ -1,41 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Debug)] -#[allow(non_snake_case)] -pub struct NewShareRequest { - name: String, - validity: u32, - description: Option, - maxViews: u32, - password: Option, -} - -impl NewShareRequest { - pub fn new( - name: impl Into, - description: Option>, - max_views: u32, - ) -> Self { - Self { - name: name.into(), - validity: 0, - description: description.map(Into::into), - maxViews: max_views, - password: None, - } - } -} - -#[derive(Deserialize, Debug)] -pub struct NewShareResponse { - pub success: bool, - pub message: String, - pub id: String, -} - -#[derive(Deserialize, Debug)] -#[allow(dead_code)] -pub struct NotifyShareResponse { - pub success: bool, - pub message: String, -} diff --git a/src/sharry/api/mod.rs b/src/sharry/api/mod.rs index b272b6b..5c084ea 100644 --- a/src/sharry/api/mod.rs +++ b/src/sharry/api/mod.rs @@ -1,7 +1,47 @@ -mod id; -mod json; +mod ids; mod uri; -pub use id::{AliasID, FileID, ShareID}; -pub use json::{NewShareRequest, NewShareResponse, NotifyShareResponse}; +use serde::{Deserialize, Serialize}; + +pub use ids::{AliasID, FileID, ShareID}; pub use uri::Uri; + +#[derive(Serialize, Debug)] +#[allow(non_snake_case)] +pub struct NewShareRequest { + name: String, + validity: u32, + description: Option, + maxViews: u32, + password: Option, +} + +impl NewShareRequest { + pub fn new( + name: impl Into, + description: Option>, + max_views: u32, + ) -> Self { + Self { + name: name.into(), + validity: 0, + description: description.map(Into::into), + maxViews: max_views, + password: None, + } + } +} + +#[derive(Deserialize, Debug)] +pub struct NewShareResponse { + pub success: bool, + pub message: String, + pub id: String, +} + +#[derive(Deserialize, Debug)] +#[allow(dead_code)] +pub struct NotifyShareResponse { + pub success: bool, + pub message: String, +}