Compare commits
11 commits
2315c9cd2e
...
bc4c15d17c
| Author | SHA1 | Date | |
|---|---|---|---|
| bc4c15d17c | |||
| c10d86ff65 | |||
| 3f00e08422 | |||
| 357f455ec0 | |||
| e3fc06b019 | |||
| c7b24b1250 | |||
| d8c48b74ca | |||
| e0c5b5517f | |||
| 087cef5d6f | |||
| c9c21aa128 | |||
| f1c6eb5d75 |
14 changed files with 305 additions and 200 deletions
42
.vscode/tasks.json
vendored
42
.vscode/tasks.json
vendored
|
|
@ -43,16 +43,16 @@
|
||||||
"problemMatcher": "$rustc",
|
"problemMatcher": "$rustc",
|
||||||
"group": "build"
|
"group": "build"
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// "label": "Run Unit Tests",
|
"label": "Run Unit Tests",
|
||||||
// "type": "cargo",
|
"type": "cargo",
|
||||||
// "command": "test",
|
"command": "test",
|
||||||
// "args": [
|
"args": [
|
||||||
// "--lib"
|
"--lib"
|
||||||
// ],
|
],
|
||||||
// "problemMatcher": "$rustc",
|
"problemMatcher": "$rustc",
|
||||||
// "group": "test"
|
"group": "test"
|
||||||
// },
|
},
|
||||||
// {
|
// {
|
||||||
// "label": "Run Integration Tests",
|
// "label": "Run Integration Tests",
|
||||||
// "type": "cargo",
|
// "type": "cargo",
|
||||||
|
|
@ -64,16 +64,16 @@
|
||||||
// "problemMatcher": "$rustc",
|
// "problemMatcher": "$rustc",
|
||||||
// "group": "test"
|
// "group": "test"
|
||||||
// },
|
// },
|
||||||
// {
|
{
|
||||||
// "label": "Run All Tests",
|
"label": "Run All Tests",
|
||||||
// "type": "shell",
|
"type": "shell",
|
||||||
// "command": "echo All Tests successful!",
|
"command": "echo All Tests successful!",
|
||||||
// "dependsOn": [
|
"dependsOn": [
|
||||||
// "Run Unit Tests",
|
"Run Unit Tests",
|
||||||
// "Run Integration Tests"
|
"Run Integration Tests"
|
||||||
// ],
|
],
|
||||||
// "dependsOrder": "sequence",
|
"dependsOrder": "sequence",
|
||||||
// "group": "test"
|
"group": "test"
|
||||||
// }
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
file::{Chunk, FileTrait},
|
file::{Chunk, FileTrait},
|
||||||
output::new_progressbar,
|
output::new_progressbar,
|
||||||
sharry::Client,
|
sharry::{Client, ShareID},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
|
|
@ -32,7 +32,7 @@ fn new_http(args: &Cli) -> ureq::Agent {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_share(args: &Cli) -> crate::Result<String> {
|
fn new_share(args: &Cli) -> crate::Result<ShareID> {
|
||||||
new_http(args).share_create(&args.get_uri(), &args.alias, args.get_share_request())
|
new_http(args).share_create(&args.get_uri(), &args.alias, args.get_share_request())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
file::{self, Chunk, FileTrait},
|
file::{self, Chunk, FileTrait},
|
||||||
output::new_progressbar,
|
output::new_progressbar,
|
||||||
sharry::{Client, Uri},
|
sharry::{AliasID, Client, ShareID, Uri},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
@ -23,8 +23,8 @@ pub struct CacheFile {
|
||||||
file_name: PathBuf,
|
file_name: PathBuf,
|
||||||
|
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
alias_id: String,
|
alias_id: AliasID,
|
||||||
share_id: String,
|
share_id: ShareID,
|
||||||
|
|
||||||
uploading: Option<file::Uploading>,
|
uploading: Option<file::Uploading>,
|
||||||
files: VecDeque<file::Checked>,
|
files: VecDeque<file::Checked>,
|
||||||
|
|
@ -97,7 +97,7 @@ impl CacheFile {
|
||||||
|
|
||||||
pub fn from_args(
|
pub fn from_args(
|
||||||
args: &Cli,
|
args: &Cli,
|
||||||
new_share: impl FnOnce(&Cli) -> crate::Result<String>,
|
new_share: impl FnOnce(&Cli) -> crate::Result<ShareID>,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
let mut files = args.files.clone();
|
let mut files = args.files.clone();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use log::LevelFilter;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
file::{Checked, FileTrait},
|
file::{Checked, FileTrait},
|
||||||
sharry::{NewShareRequest, Uri},
|
sharry::{AliasID, Uri, json::NewShareRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
|
@ -69,7 +69,7 @@ pub struct Cli {
|
||||||
url: String,
|
url: String,
|
||||||
|
|
||||||
/// ID of a public alias to use
|
/// ID of a public alias to use
|
||||||
pub alias: String,
|
pub alias: AliasID,
|
||||||
|
|
||||||
/// Files to upload to the new share
|
/// Files to upload to the new share
|
||||||
#[arg(value_name = "FILE", required = true, value_parser = parse_sharry_file)]
|
#[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();
|
let mut hasher = Blake2b::new().hash_length(16).to_state();
|
||||||
|
|
||||||
hasher.update(self.get_uri().as_ref());
|
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) {
|
for chk in sorted(&self.files) {
|
||||||
hasher.update(chk.as_ref());
|
hasher.update(chk.as_ref());
|
||||||
|
|
|
||||||
10
src/error.rs
10
src/error.rs
|
|
@ -1,18 +1,20 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::sharry;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Parameter {
|
pub enum Parameter {
|
||||||
#[error("given URI {0:?}")]
|
#[error("given URI {0:?}")]
|
||||||
Uri(String),
|
Uri(String),
|
||||||
|
|
||||||
#[error("given Alias ID {0:?}")]
|
#[error("given Alias ID {0:?}")]
|
||||||
AliasID(String),
|
AliasID(sharry::AliasID),
|
||||||
|
|
||||||
#[error("stored Share ID {0:?}")]
|
#[error("stored Share ID {0:?}")]
|
||||||
ShareID(String),
|
ShareID(sharry::ShareID),
|
||||||
|
|
||||||
#[error("stored File ID {0:?}")]
|
#[error("stored {0:?}")]
|
||||||
FileID(String),
|
FileID(sharry::FileID),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Parameter {
|
impl Parameter {
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ impl Checked {
|
||||||
self,
|
self,
|
||||||
client: &impl sharry::Client,
|
client: &impl sharry::Client,
|
||||||
uri: &sharry::Uri,
|
uri: &sharry::Uri,
|
||||||
alias_id: &str,
|
alias_id: &sharry::AliasID,
|
||||||
share_id: &str,
|
share_id: &sharry::ShareID,
|
||||||
) -> crate::Result<Uploading> {
|
) -> crate::Result<Uploading> {
|
||||||
let file_id = client.file_create(uri, alias_id, share_id, &self)?;
|
let file_id = client.file_create(uri, alias_id, share_id, &self)?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use crate::sharry;
|
||||||
|
|
||||||
pub struct Chunk<'t> {
|
pub struct Chunk<'t> {
|
||||||
file_id: String,
|
file_id: sharry::FileID,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
data: &'t [u8],
|
data: &'t [u8],
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +19,7 @@ impl fmt::Debug for Chunk<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'t> Chunk<'t> {
|
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 {
|
Self {
|
||||||
file_id,
|
file_id,
|
||||||
offset,
|
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
|
&self.file_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use std::{
|
||||||
use log::warn;
|
use log::warn;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::sharry;
|
||||||
|
|
||||||
use super::{Checked, Chunk, FileTrait};
|
use super::{Checked, Chunk, FileTrait};
|
||||||
|
|
||||||
|
|
@ -18,14 +19,19 @@ pub struct Uploading {
|
||||||
size: u64,
|
size: u64,
|
||||||
/// hash of that file
|
/// hash of that file
|
||||||
hash: Option<String>,
|
hash: Option<String>,
|
||||||
file_id: String,
|
file_id: sharry::FileID,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
last_offset: Option<u64>,
|
last_offset: Option<u64>,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Uploading {
|
impl Uploading {
|
||||||
pub(super) fn new(path: PathBuf, size: u64, hash: Option<String>, file_id: String) -> Self {
|
pub(super) fn new(
|
||||||
|
path: PathBuf,
|
||||||
|
size: u64,
|
||||||
|
hash: Option<String>,
|
||||||
|
file_id: sharry::FileID,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path,
|
||||||
size,
|
size,
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,14 @@ use log::{debug, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
file::{self, FileTrait},
|
file::{self, FileTrait},
|
||||||
sharry::{self, Uri},
|
sharry::{self, AliasID, FileID, ShareID, Uri},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn find_cause(
|
fn find_cause(
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
alias_id: &str,
|
alias_id: &AliasID,
|
||||||
share_id: Option<&str>,
|
share_id: Option<&ShareID>,
|
||||||
file_id: Option<&str>,
|
file_id: Option<&FileID>,
|
||||||
) -> impl FnOnce(ureq::Error) -> crate::Error {
|
) -> impl FnOnce(ureq::Error) -> crate::Error {
|
||||||
move |error| match error {
|
move |error| match error {
|
||||||
ureq::Error::StatusCode(403) => {
|
ureq::Error::StatusCode(403) => {
|
||||||
|
|
@ -49,15 +49,15 @@ impl sharry::Client for ureq::Agent {
|
||||||
fn share_create(
|
fn share_create(
|
||||||
&self,
|
&self,
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
alias_id: &str,
|
alias_id: &AliasID,
|
||||||
data: sharry::NewShareRequest,
|
data: sharry::json::NewShareRequest,
|
||||||
) -> crate::Result<String> {
|
) -> crate::Result<ShareID> {
|
||||||
let res = {
|
let res = {
|
||||||
let endpoint = uri.share_create();
|
let endpoint = uri.share_create();
|
||||||
|
|
||||||
let mut res = self
|
let mut res = self
|
||||||
.post(&endpoint)
|
.post(&endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id.as_ref())
|
||||||
.send_json(data)
|
.send_json(data)
|
||||||
.map_err(find_cause(uri, alias_id, None, None))?;
|
.map_err(find_cause(uri, alias_id, None, None))?;
|
||||||
|
|
||||||
|
|
@ -65,7 +65,7 @@ impl sharry::Client for ureq::Agent {
|
||||||
crate::Error::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
crate::Error::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
||||||
|
|
||||||
res.body_mut()
|
res.body_mut()
|
||||||
.read_json::<sharry::NewShareResponse>()
|
.read_json::<sharry::json::NewShareResponse>()
|
||||||
.map_err(crate::Error::response)?
|
.map_err(crate::Error::response)?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,19 +74,19 @@ impl sharry::Client for ureq::Agent {
|
||||||
if res.success && (res.message == "Share created.") {
|
if res.success && (res.message == "Share created.") {
|
||||||
trace!("new share id: {:?}", res.id);
|
trace!("new share id: {:?}", res.id);
|
||||||
|
|
||||||
Ok(res.id)
|
Ok(res.id.into())
|
||||||
} else {
|
} else {
|
||||||
Err(crate::Error::response(format!("{res:?}")))
|
Err(crate::Error::response(format!("{res:?}")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> crate::Result<()> {
|
fn share_notify(&self, uri: &Uri, alias_id: &AliasID, share_id: &ShareID) -> crate::Result<()> {
|
||||||
let res = {
|
let res = {
|
||||||
let endpoint = uri.share_notify(share_id);
|
let endpoint = uri.share_notify(share_id);
|
||||||
|
|
||||||
let mut res = self
|
let mut res = self
|
||||||
.post(&endpoint)
|
.post(&endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id.as_ref())
|
||||||
.send_empty()
|
.send_empty()
|
||||||
.map_err(find_cause(uri, alias_id, Some(share_id), None))?;
|
.map_err(find_cause(uri, alias_id, Some(share_id), None))?;
|
||||||
|
|
||||||
|
|
@ -94,7 +94,7 @@ impl sharry::Client for ureq::Agent {
|
||||||
crate::Error::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
crate::Error::res_status_check(res.status(), ureq::http::StatusCode::OK)?;
|
||||||
|
|
||||||
res.body_mut()
|
res.body_mut()
|
||||||
.read_json::<sharry::NotifyShareResponse>()
|
.read_json::<sharry::json::NotifyShareResponse>()
|
||||||
.map_err(crate::Error::response)?
|
.map_err(crate::Error::response)?
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -106,16 +106,16 @@ impl sharry::Client for ureq::Agent {
|
||||||
fn file_create(
|
fn file_create(
|
||||||
&self,
|
&self,
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
alias_id: &str,
|
alias_id: &AliasID,
|
||||||
share_id: &str,
|
share_id: &ShareID,
|
||||||
file: &file::Checked,
|
file: &file::Checked,
|
||||||
) -> crate::Result<String> {
|
) -> crate::Result<FileID> {
|
||||||
let res = {
|
let res = {
|
||||||
let endpoint = uri.file_create(share_id);
|
let endpoint = uri.file_create(share_id);
|
||||||
|
|
||||||
let res = self
|
let res = self
|
||||||
.post(&endpoint)
|
.post(&endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id.as_ref())
|
||||||
.header("Sharry-File-Name", file.get_name())
|
.header("Sharry-File-Name", file.get_name())
|
||||||
.header("Upload-Length", file.get_size())
|
.header("Upload-Length", file.get_size())
|
||||||
.send_empty()
|
.send_empty()
|
||||||
|
|
@ -132,18 +132,14 @@ impl sharry::Client for ureq::Agent {
|
||||||
.map_err(crate::Error::response)?
|
.map_err(crate::Error::response)?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let file_id = Self::get_file_id(&location)?;
|
FileID::try_from(location)
|
||||||
|
|
||||||
debug!("location: {location:?}, file_id: {file_id:?}");
|
|
||||||
|
|
||||||
Ok(file_id.to_owned())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_patch(
|
fn file_patch(
|
||||||
&self,
|
&self,
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
alias_id: &str,
|
alias_id: &AliasID,
|
||||||
share_id: &str,
|
share_id: &ShareID,
|
||||||
chunk: &file::Chunk,
|
chunk: &file::Chunk,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let res = {
|
let res = {
|
||||||
|
|
@ -151,7 +147,7 @@ impl sharry::Client for ureq::Agent {
|
||||||
|
|
||||||
let res = self
|
let res = self
|
||||||
.patch(&endpoint)
|
.patch(&endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id.as_ref())
|
||||||
.header("Upload-Offset", chunk.get_offset())
|
.header("Upload-Offset", chunk.get_offset())
|
||||||
.send(chunk.get_data())
|
.send(chunk.get_data())
|
||||||
.map_err(find_cause(
|
.map_err(find_cause(
|
||||||
|
|
|
||||||
|
|
@ -1,87 +0,0 @@
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use log::trace;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::file;
|
|
||||||
|
|
||||||
use super::api::{NewShareRequest, Uri};
|
|
||||||
|
|
||||||
pub trait Client {
|
|
||||||
fn get_file_id(uri: &str) -> crate::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(crate::Error::mismatch(
|
|
||||||
"<proto>://<base>/api/v2/alias/upload/<share>/files/tus/<file>",
|
|
||||||
uri,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn share_create(
|
|
||||||
&self,
|
|
||||||
uri: &Uri,
|
|
||||||
alias_id: &str,
|
|
||||||
data: NewShareRequest,
|
|
||||||
) -> crate::Result<String>;
|
|
||||||
|
|
||||||
fn share_notify(&self, uri: &Uri, alias_id: &str, share_id: &str) -> crate::Result<()>;
|
|
||||||
|
|
||||||
fn file_create(
|
|
||||||
&self,
|
|
||||||
uri: &Uri,
|
|
||||||
alias_id: &str,
|
|
||||||
share_id: &str,
|
|
||||||
file: &file::Checked,
|
|
||||||
) -> crate::Result<String>;
|
|
||||||
|
|
||||||
fn file_patch(
|
|
||||||
&self,
|
|
||||||
uri: &Uri,
|
|
||||||
alias_id: &str,
|
|
||||||
share_id: &str,
|
|
||||||
chunk: &file::Chunk,
|
|
||||||
) -> crate::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());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
155
src/sharry/ids.rs
Normal file
155
src/sharry/ids.rs
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
use std::{fmt, sync::LazyLock};
|
||||||
|
|
||||||
|
use log::{debug, trace};
|
||||||
|
use regex::Regex;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[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<String> 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<String> for ShareID {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String> for FileID {
|
||||||
|
type Error = crate::Error;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> crate::Result<Self> {
|
||||||
|
/// 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")
|
||||||
|
});
|
||||||
|
|
||||||
|
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(crate::Error::mismatch(
|
||||||
|
"<proto>://<base>/api/v2/alias/upload/<share>/files/tus/<file>",
|
||||||
|
value,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
crate::Error::Mismatch { expected, actual } => {
|
||||||
|
assert_eq!(
|
||||||
|
expected, "<proto>://<base>/api/v2/alias/upload/<share>/files/tus/<file>",
|
||||||
|
"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:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/sharry/json.rs
Normal file
41
src/sharry/json.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub struct NewShareRequest {
|
||||||
|
name: String,
|
||||||
|
validity: u32,
|
||||||
|
description: Option<String>,
|
||||||
|
maxViews: u32,
|
||||||
|
password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewShareRequest {
|
||||||
|
pub fn new(
|
||||||
|
name: impl Into<String>,
|
||||||
|
description: Option<impl Into<String>>,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,35 @@
|
||||||
mod api;
|
mod ids;
|
||||||
mod client;
|
pub mod json;
|
||||||
|
mod uri;
|
||||||
|
|
||||||
pub use api::{NewShareRequest, NewShareResponse, NotifyShareResponse, Uri};
|
pub use ids::{AliasID, FileID, ShareID};
|
||||||
pub use client::Client;
|
pub use uri::Uri;
|
||||||
|
|
||||||
|
use crate::file;
|
||||||
|
|
||||||
|
pub trait Client {
|
||||||
|
fn share_create(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &AliasID,
|
||||||
|
data: json::NewShareRequest,
|
||||||
|
) -> crate::Result<ShareID>;
|
||||||
|
|
||||||
|
fn share_notify(&self, uri: &Uri, alias_id: &AliasID, share_id: &ShareID) -> crate::Result<()>;
|
||||||
|
|
||||||
|
fn file_create(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &AliasID,
|
||||||
|
share_id: &ShareID,
|
||||||
|
file: &file::Checked,
|
||||||
|
) -> crate::Result<FileID>;
|
||||||
|
|
||||||
|
fn file_patch(
|
||||||
|
&self,
|
||||||
|
uri: &Uri,
|
||||||
|
alias_id: &AliasID,
|
||||||
|
share_id: &ShareID,
|
||||||
|
chunk: &file::Chunk,
|
||||||
|
) -> crate::Result<()>;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,55 +33,15 @@ impl Uri {
|
||||||
self.endpoint(format_args!("alias/upload/new"))
|
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}"))
|
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"))
|
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: &super::ShareID, file_id: &super::FileID) -> String {
|
||||||
self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}"))
|
self.endpoint(format_args!("alias/upload/{share_id}/files/tus/{file_id}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub struct NewShareRequest {
|
|
||||||
name: String,
|
|
||||||
validity: u32,
|
|
||||||
description: Option<String>,
|
|
||||||
maxViews: u32,
|
|
||||||
password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NewShareRequest {
|
|
||||||
pub fn new(
|
|
||||||
name: impl Into<String>,
|
|
||||||
description: Option<impl Into<String>>,
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue