mirror of
https://github.com/localsend/localsend.git
synced 2026-06-23 04:10:07 +00:00
feat: add thiserror to client, prepare_upload should also return status code
This commit is contained in:
@@ -6,13 +6,52 @@ pub use v2::LsHttpClientV2;
|
||||
pub use v3::LsHttpClientV3;
|
||||
|
||||
use crate::crypto;
|
||||
use crate::http::StatusCodeError;
|
||||
use reqwest::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
pub enum LsHttpClient {
|
||||
V2(LsHttpClientV2),
|
||||
V3(LsHttpClientV3),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ClientError {
|
||||
#[error(transparent)]
|
||||
StatusCode(StatusCodeError),
|
||||
|
||||
#[error(transparent)]
|
||||
Reqwest(#[from] reqwest::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub(super) fn create_reqwest_client(private_key: &str, cert: &str) -> Result<reqwest::Client, ClientError> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let identity = {
|
||||
let pem = &[cert.as_bytes(), "\n".as_bytes(), private_key.as_bytes()].concat();
|
||||
reqwest::Identity::from_pem(pem)?
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.use_rustls_tls()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.tls_info(true)
|
||||
.identity(identity)
|
||||
.build()?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// Verifies the certificate from the response.
|
||||
/// Returns the public key extracted from the certificate.
|
||||
pub(super) fn verify_cert_from_res(
|
||||
@@ -33,3 +72,27 @@ pub(super) fn verify_cert_from_res(
|
||||
};
|
||||
Ok(public_key)
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ErrorResponse {
|
||||
message: String,
|
||||
}
|
||||
|
||||
pub(super) trait ResponseExt {
|
||||
async fn into_error<T>(self) -> Result<T, ClientError>;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
async fn into_error<T>(self) -> Result<T, ClientError> {
|
||||
let status = self.status().as_u16();
|
||||
let body = self.text().await.unwrap_or_default();
|
||||
let message = match serde_json::from_str::<ErrorResponse>(&body) {
|
||||
Ok(error) => error.message,
|
||||
Err(_) => body,
|
||||
};
|
||||
Err(ClientError::StatusCode(StatusCodeError {
|
||||
status,
|
||||
message: if message.is_empty() { None } else { Some(message) },
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,3 +46,50 @@ impl<'a> TargetUrl<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ApiVersion, TargetUrl};
|
||||
|
||||
#[test]
|
||||
fn test_build_url_ipv4() {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: "https",
|
||||
host: "192.168.1.1".to_string(),
|
||||
port: 53317,
|
||||
path: "/register",
|
||||
params: &[],
|
||||
}
|
||||
.to_string();
|
||||
assert_eq!(url, "https://192.168.1.1:53317/api/localsend/v2/register");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_ipv6() {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: "https",
|
||||
host: "::1".to_string(),
|
||||
port: 53317,
|
||||
path: "/register",
|
||||
params: &[],
|
||||
}
|
||||
.to_string();
|
||||
assert_eq!(url, "https://[::1]:53317/api/localsend/v2/register");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_http() {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: "http",
|
||||
host: "192.168.1.1".to_string(),
|
||||
port: 53317,
|
||||
path: "/info",
|
||||
params: &[],
|
||||
}
|
||||
.to_string();
|
||||
assert_eq!(url, "http://192.168.1.1:53317/api/localsend/v2/info");
|
||||
}
|
||||
}
|
||||
|
||||
+53
-147
@@ -1,12 +1,8 @@
|
||||
use super::{ClientError, ResponseExt};
|
||||
use crate::http::client::url::{ApiVersion, TargetUrl};
|
||||
use crate::http::dto_v2::{
|
||||
InfoResponseDtoV2, PrepareDownloadResponseDtoV2, PrepareUploadRequestDtoV2,
|
||||
PrepareUploadResponseDtoV2, ProtocolTypeV2, RegisterDtoV2, RegisterResponseDtoV2,
|
||||
};
|
||||
use crate::http::StatusCodeError;
|
||||
use crate::http::dto_v2::{InfoResponseDtoV2, PrepareDownloadResponseDtoV2, PrepareUploadRequestDtoV2, PrepareUploadResponseDtoV2, PrepareUploadResultV2, ProtocolTypeV2, RegisterDtoV2, RegisterResponseDtoV2};
|
||||
use futures_util::StreamExt;
|
||||
use reqwest::{Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
@@ -36,30 +32,16 @@ impl LsHttpClientV2 {
|
||||
///
|
||||
/// # Returns
|
||||
/// A new client instance or an error if TLS setup fails.
|
||||
pub fn try_new(private_key: &str, cert: &str) -> anyhow::Result<Self> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let identity = {
|
||||
let pem = &[cert.as_bytes(), "\n".as_bytes(), private_key.as_bytes()].concat();
|
||||
reqwest::Identity::from_pem(pem)?
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.use_rustls_tls()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.tls_info(true)
|
||||
.identity(identity)
|
||||
.build()?;
|
||||
|
||||
pub fn try_new(private_key: &str, cert: &str) -> Result<Self, ClientError> {
|
||||
Ok(Self {
|
||||
client,
|
||||
client: super::create_reqwest_client(private_key, cert)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new HTTP client without TLS client certificate.
|
||||
///
|
||||
/// Use this for HTTP-only connections or when client authentication is not needed.
|
||||
pub fn try_new_without_cert() -> anyhow::Result<Self> {
|
||||
pub fn try_new_without_cert() -> Result<Self, ClientError> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
@@ -68,21 +50,7 @@ impl LsHttpClientV2 {
|
||||
.tls_info(true)
|
||||
.build()?;
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
fn url(&self, protocol: &ProtocolTypeV2, ip: &str, port: u16, path: &'static str) -> String {
|
||||
TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path,
|
||||
params: &[],
|
||||
}
|
||||
.to_string()
|
||||
Ok(Self { client })
|
||||
}
|
||||
|
||||
/// Registers with another device for discovery.
|
||||
@@ -103,8 +71,16 @@ impl LsHttpClientV2 {
|
||||
ip: &str,
|
||||
port: u16,
|
||||
payload: RegisterDtoV2,
|
||||
) -> anyhow::Result<RegisterResultV2> {
|
||||
let url = self.url(protocol, ip, port, "/register");
|
||||
) -> Result<RegisterResultV2, ClientError> {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/register",
|
||||
params: &[],
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let res = self
|
||||
.client
|
||||
@@ -115,7 +91,7 @@ impl LsHttpClientV2 {
|
||||
.await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
let public_key = match protocol {
|
||||
@@ -146,7 +122,7 @@ impl LsHttpClientV2 {
|
||||
/// Session ID and accepted file tokens, or an error.
|
||||
///
|
||||
/// # Errors
|
||||
/// * 204 - No file transfer needed (all files already exist)
|
||||
/// * 204 - No file transfer needed (e.g. text-only transfer)
|
||||
/// * 400 - Invalid body
|
||||
/// * 401 - PIN required or invalid
|
||||
/// * 403 - Rejected by user
|
||||
@@ -161,7 +137,7 @@ impl LsHttpClientV2 {
|
||||
public_key: Option<String>,
|
||||
payload: PrepareUploadRequestDtoV2,
|
||||
pin: Option<&str>,
|
||||
) -> anyhow::Result<PrepareUploadResponseDtoV2> {
|
||||
) -> Result<PrepareUploadResultV2, ClientError> {
|
||||
let pin_params: &[(&'static str, &str)] = match &pin {
|
||||
Some(pin) => &[("pin", pin)],
|
||||
None => &[],
|
||||
@@ -188,21 +164,18 @@ impl LsHttpClientV2 {
|
||||
super::verify_cert_from_res(&res, Some(public_key))?;
|
||||
}
|
||||
|
||||
if res.status() == StatusCode::NO_CONTENT {
|
||||
// 204 - No file transfer needed
|
||||
return Ok(PrepareUploadResponseDtoV2 {
|
||||
session_id: String::new(),
|
||||
files: std::collections::HashMap::new(),
|
||||
});
|
||||
}
|
||||
let status = res.status();
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
if status.as_u16() >= 400 {
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
let body = res.json::<PrepareUploadResponseDtoV2>().await?;
|
||||
|
||||
Ok(body)
|
||||
Ok(PrepareUploadResultV2 {
|
||||
status_code: status.as_u16(),
|
||||
response: body,
|
||||
})
|
||||
}
|
||||
|
||||
/// Uploads a file to the receiver.
|
||||
@@ -235,14 +208,18 @@ impl LsHttpClientV2 {
|
||||
file_id: &str,
|
||||
token: &str,
|
||||
binary: mpsc::Receiver<Vec<u8>>,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<(), ClientError> {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/upload",
|
||||
params: &[("sessionId", session_id), ("fileId", file_id), ("token", token)],
|
||||
params: &[
|
||||
("sessionId", session_id),
|
||||
("fileId", file_id),
|
||||
("token", token),
|
||||
],
|
||||
}
|
||||
.to_string();
|
||||
|
||||
@@ -252,39 +229,7 @@ impl LsHttpClientV2 {
|
||||
let res = self.client.post(&url).body(body).send().await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uploads a file from bytes.
|
||||
///
|
||||
/// Convenience method that wraps upload() for simple byte array uploads.
|
||||
pub async fn upload_bytes(
|
||||
&self,
|
||||
protocol: &ProtocolTypeV2,
|
||||
ip: &str,
|
||||
port: u16,
|
||||
session_id: &str,
|
||||
file_id: &str,
|
||||
token: &str,
|
||||
data: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/upload",
|
||||
params: &[("sessionId", session_id), ("fileId", file_id), ("token", token)],
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let res = self.client.post(&url).body(data).send().await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -305,7 +250,7 @@ impl LsHttpClientV2 {
|
||||
ip: &str,
|
||||
port: u16,
|
||||
session_id: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<(), ClientError> {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: protocol.as_str(),
|
||||
@@ -339,13 +284,21 @@ impl LsHttpClientV2 {
|
||||
protocol: &ProtocolTypeV2,
|
||||
ip: &str,
|
||||
port: u16,
|
||||
) -> anyhow::Result<InfoResponseDtoV2> {
|
||||
let url = self.url(protocol, ip, port, "/info");
|
||||
) -> Result<InfoResponseDtoV2, ClientError> {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/info",
|
||||
params: &[],
|
||||
}
|
||||
.to_string();
|
||||
|
||||
let res = self.client.get(&url).send().await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
let body = res.json::<InfoResponseDtoV2>().await?;
|
||||
@@ -382,7 +335,7 @@ impl LsHttpClientV2 {
|
||||
port: u16,
|
||||
session_id: Option<&str>,
|
||||
pin: Option<&str>,
|
||||
) -> anyhow::Result<PrepareDownloadResponseDtoV2> {
|
||||
) -> Result<PrepareDownloadResponseDtoV2, ClientError> {
|
||||
let mut params: Vec<(&'static str, &str)> = Vec::new();
|
||||
if let Some(session_id) = session_id {
|
||||
params.push(("sessionId", session_id));
|
||||
@@ -403,7 +356,7 @@ impl LsHttpClientV2 {
|
||||
let res = self.client.post(&url).send().await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
let body = res.json::<PrepareDownloadResponseDtoV2>().await?;
|
||||
@@ -433,7 +386,7 @@ impl LsHttpClientV2 {
|
||||
port: u16,
|
||||
session_id: &str,
|
||||
file_id: &str,
|
||||
) -> anyhow::Result<Response> {
|
||||
) -> Result<Response, ClientError> {
|
||||
let url = TargetUrl {
|
||||
version: ApiVersion::V2,
|
||||
protocol: protocol.as_str(),
|
||||
@@ -447,7 +400,7 @@ impl LsHttpClientV2 {
|
||||
let res = self.client.get(&url).send().await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
@@ -473,8 +426,10 @@ impl LsHttpClientV2 {
|
||||
session_id: &str,
|
||||
file_id: &str,
|
||||
writer: &mut W,
|
||||
) -> anyhow::Result<u64> {
|
||||
let response = self.download(protocol, ip, port, session_id, file_id).await?;
|
||||
) -> Result<u64, ClientError> {
|
||||
let response = self
|
||||
.download(protocol, ip, port, session_id, file_id)
|
||||
.await?;
|
||||
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut total_bytes = 0u64;
|
||||
@@ -490,52 +445,3 @@ impl LsHttpClientV2 {
|
||||
Ok(total_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ErrorResponse {
|
||||
message: String,
|
||||
}
|
||||
|
||||
async fn status_code_error_from_res(response: Response) -> anyhow::Result<anyhow::Error> {
|
||||
let status = response.status().as_u16();
|
||||
let body = response.text().await?;
|
||||
let body = match serde_json::from_str::<ErrorResponse>(&body) {
|
||||
Ok(error) => error.message,
|
||||
Err(_) => body,
|
||||
};
|
||||
|
||||
Ok(anyhow::Error::new(StatusCodeError {
|
||||
status,
|
||||
message: match body {
|
||||
_ if body.is_empty() => None,
|
||||
_ => Some(body),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_build_url_ipv4() {
|
||||
let client = LsHttpClientV2::try_new_without_cert().unwrap();
|
||||
let url = client.url(&ProtocolTypeV2::Https, "192.168.1.1", 53317, "/register");
|
||||
assert_eq!(url, "https://192.168.1.1:53317/api/localsend/v2/register");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_ipv6() {
|
||||
let client = LsHttpClientV2::try_new_without_cert().unwrap();
|
||||
let url = client.url(&ProtocolTypeV2::Https, "::1", 53317, "/register");
|
||||
assert_eq!(url, "https://[::1]:53317/api/localsend/v2/register");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_http() {
|
||||
let client = LsHttpClientV2::try_new_without_cert().unwrap();
|
||||
let url = client.url(&ProtocolTypeV2::Http, "192.168.1.1", 53317, "/info");
|
||||
assert_eq!(url, "http://192.168.1.1:53317/api/localsend/v2/info");
|
||||
}
|
||||
}
|
||||
|
||||
+64
-85
@@ -1,12 +1,11 @@
|
||||
use super::{ClientError, ResponseExt};
|
||||
use crate::http;
|
||||
use crate::http::client::url::{ApiVersion, TargetUrl};
|
||||
use crate::http::dto::ProtocolType;
|
||||
use crate::http::StatusCodeError;
|
||||
use crate::{crypto, util};
|
||||
use futures_util::StreamExt;
|
||||
use lru::LruCache;
|
||||
use reqwest::{Response, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
@@ -33,23 +32,9 @@ pub struct RegisterResult {
|
||||
}
|
||||
|
||||
impl LsHttpClientV3 {
|
||||
pub fn try_new(private_key: &str, cert: &str) -> anyhow::Result<Self> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
|
||||
let identity = {
|
||||
let pem = &[cert.as_bytes(), "\n".as_bytes(), private_key.as_bytes()].concat();
|
||||
reqwest::Identity::from_pem(pem)?
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.use_rustls_tls()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.tls_info(true)
|
||||
.identity(identity)
|
||||
.build()?;
|
||||
|
||||
pub fn try_new(private_key: &str, cert: &str) -> Result<Self, ClientError> {
|
||||
Ok(Self {
|
||||
client,
|
||||
client: super::create_reqwest_client(private_key, cert)?,
|
||||
received_nonce_map: Arc::new(Mutex::new(LruCache::new(
|
||||
NonZeroUsize::new(200).unwrap(),
|
||||
))),
|
||||
@@ -64,7 +49,7 @@ impl LsHttpClientV3 {
|
||||
protocol: &ProtocolType,
|
||||
ip: &str,
|
||||
port: u16,
|
||||
) -> anyhow::Result<String> {
|
||||
) -> Result<String, ClientError> {
|
||||
// Generate nonce to send to server
|
||||
let generated_nonce = crypto::nonce::generate_nonce();
|
||||
let generated_nonce_base64 = util::base64::encode(&generated_nonce);
|
||||
@@ -91,14 +76,14 @@ impl LsHttpClientV3 {
|
||||
.await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
let remote_key = to_identifier(&res, protocol == &ProtocolType::Https, None)?;
|
||||
let body = res.json::<http::dto::NonceResponse>().await?;
|
||||
|
||||
// Save the response nonce and our generated nonce
|
||||
let response_nonce = util::base64::decode(&body.nonce)?;
|
||||
let response_nonce = util::base64::decode(&body.nonce).map_err(|e| anyhow::anyhow!(e))?;
|
||||
|
||||
let mut received_nonce_map = self.received_nonce_map.lock().await;
|
||||
received_nonce_map.put(remote_key.clone(), response_nonce);
|
||||
@@ -126,17 +111,20 @@ impl LsHttpClientV3 {
|
||||
ip: &str,
|
||||
port: u16,
|
||||
payload: http::dto::RegisterDto,
|
||||
) -> anyhow::Result<RegisterResult> {
|
||||
) -> Result<RegisterResult, ClientError> {
|
||||
let res = self
|
||||
.client
|
||||
.post(TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/register",
|
||||
params: &[],
|
||||
}.to_string())
|
||||
.post(
|
||||
TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/register",
|
||||
params: &[],
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
.body(serde_json::to_string(&payload)?)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -158,17 +146,20 @@ impl LsHttpClientV3 {
|
||||
port: u16,
|
||||
public_key: Option<String>,
|
||||
payload: http::dto::PrepareUploadRequestDto,
|
||||
) -> anyhow::Result<http::dto::PrepareUploadResponseDto> {
|
||||
) -> Result<http::dto::PrepareUploadResponseDto, ClientError> {
|
||||
let res = self
|
||||
.client
|
||||
.post(TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/prepare-upload",
|
||||
params: &[],
|
||||
}.to_string())
|
||||
.post(
|
||||
TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/prepare-upload",
|
||||
params: &[],
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
.body(serde_json::to_string(&payload)?)
|
||||
.send()
|
||||
.await?;
|
||||
@@ -178,7 +169,7 @@ impl LsHttpClientV3 {
|
||||
}
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
let body = res.json::<http::dto::PrepareUploadResponseDto>().await?;
|
||||
@@ -196,17 +187,24 @@ impl LsHttpClientV3 {
|
||||
file_id: String,
|
||||
token: String,
|
||||
binary: mpsc::Receiver<Vec<u8>>,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<(), ClientError> {
|
||||
let res = self
|
||||
.client
|
||||
.post(TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/upload",
|
||||
params: &[("sessionId", &session_id), ("fileId", &file_id), ("token", &token)],
|
||||
}.to_string())
|
||||
.post(
|
||||
TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/upload",
|
||||
params: &[
|
||||
("sessionId", &session_id),
|
||||
("fileId", &file_id),
|
||||
("token", &token),
|
||||
],
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
.body({
|
||||
let stream = ReceiverStream::new(binary).map(Ok::<Vec<u8>, anyhow::Error>);
|
||||
reqwest::Body::wrap_stream(stream)
|
||||
@@ -215,7 +213,7 @@ impl LsHttpClientV3 {
|
||||
.await?;
|
||||
|
||||
if res.status() != StatusCode::OK {
|
||||
return Err(status_code_error_from_res(res).await?);
|
||||
return res.into_error().await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -227,16 +225,19 @@ impl LsHttpClientV3 {
|
||||
ip: &str,
|
||||
port: u16,
|
||||
session_id: String,
|
||||
) -> anyhow::Result<()> {
|
||||
) -> Result<(), ClientError> {
|
||||
self.client
|
||||
.post(TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/cancel",
|
||||
params: &[("sessionId", &session_id)],
|
||||
}.to_string())
|
||||
.post(
|
||||
TargetUrl {
|
||||
version: ApiVersion::V3,
|
||||
protocol: protocol.as_str(),
|
||||
host: ip.to_string(),
|
||||
port,
|
||||
path: "/cancel",
|
||||
params: &[("sessionId", &session_id)],
|
||||
}
|
||||
.to_string(),
|
||||
)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
@@ -244,39 +245,17 @@ impl LsHttpClientV3 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ErrorResponse {
|
||||
message: String,
|
||||
}
|
||||
|
||||
async fn status_code_error_from_res(response: Response) -> anyhow::Result<anyhow::Error> {
|
||||
let status = response.status().as_u16();
|
||||
let body = response.text().await?;
|
||||
let body = match serde_json::from_str::<ErrorResponse>(&body) {
|
||||
Ok(error) => error.message,
|
||||
Err(_) => body,
|
||||
};
|
||||
|
||||
Ok(anyhow::Error::new(StatusCodeError {
|
||||
status,
|
||||
message: match body {
|
||||
_ if body.is_empty() => None,
|
||||
_ => Some(body),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn to_identifier(
|
||||
response: &Response,
|
||||
require_cert: bool,
|
||||
public_key: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
) -> Result<String, ClientError> {
|
||||
match require_cert {
|
||||
true => super::verify_cert_from_res(response, public_key),
|
||||
true => Ok(super::verify_cert_from_res(response, public_key)?),
|
||||
false => response
|
||||
.remote_addr()
|
||||
.map(|addr| addr.ip().to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("Remote address not found in response")),
|
||||
.ok_or_else(|| anyhow::anyhow!("Remote address not found in response"))
|
||||
.map_err(ClientError::Other),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,11 @@ pub struct PrepareUploadResponseDtoV2 {
|
||||
pub files: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub struct PrepareUploadResultV2 {
|
||||
pub status_code: u16,
|
||||
pub response: PrepareUploadResponseDtoV2,
|
||||
}
|
||||
|
||||
/// Prepare download response DTO for v2.1 protocol (Download API).
|
||||
///
|
||||
/// Response from POST /api/localsend/v2/prepare-download.
|
||||
|
||||
@@ -8,7 +8,7 @@ pub mod state;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{status};{message:?}")]
|
||||
pub(crate) struct StatusCodeError {
|
||||
pub struct StatusCodeError {
|
||||
status: u16,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user