[wip] impl Client for ureq::Agent
- impl `sharry_file_patch` - completely rework chunking logic
This commit is contained in:
parent
c9528a9ac1
commit
69bef4e994
6 changed files with 112 additions and 130 deletions
|
|
@ -13,9 +13,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
sharry::{
|
sharry::{Client, ClientError, FileChecked, FileUploading, SharryFile, Uri},
|
||||||
ChunkState, Client, ClientError, FileChecked, FileUploading, SharryFile, UploadError, Uri,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,17 @@ pub trait Client {
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
alias_id: &str,
|
alias_id: &str,
|
||||||
share_id: &str,
|
share_id: &str,
|
||||||
file: FileChecked,
|
file_name: &str,
|
||||||
) -> Result<FileUploading, ClientError>;
|
file_size: u64,
|
||||||
|
) -> Result<String, ClientError>;
|
||||||
|
|
||||||
// fn sharry_file_patch(&self);
|
fn sharry_file_patch(
|
||||||
|
&self,
|
||||||
|
patch_uri: &str,
|
||||||
|
alias_id: &str,
|
||||||
|
offset: u64,
|
||||||
|
chunk: &[u8],
|
||||||
|
) -> Result<u64, ClientError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
@ -54,15 +61,29 @@ pub enum ClientError {
|
||||||
|
|
||||||
impl ClientError {
|
impl ClientError {
|
||||||
fn req_err(msg: impl Display) -> Self {
|
fn req_err(msg: impl Display) -> Self {
|
||||||
ClientError::Request(msg.to_string())
|
Self::Request(msg.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn res_parse_err(msg: impl Display) -> Self {
|
fn res_parse_err(msg: impl Display) -> Self {
|
||||||
ClientError::ResponseParsing(msg.to_string())
|
Self::ResponseParsing(msg.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn res_content_err(msg: impl Display) -> Self {
|
fn res_content_err(msg: impl Display) -> Self {
|
||||||
ClientError::ResponseContent(msg.to_string())
|
Self::ResponseContent(msg.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn res_check_status<T>(actual: T, expected: T) -> Result<(), Self>
|
||||||
|
where
|
||||||
|
T: Into<u16> + Eq,
|
||||||
|
{
|
||||||
|
if actual == expected {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Self::ResponseStatus {
|
||||||
|
actual: actual.into(),
|
||||||
|
expected: expected.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,36 +143,67 @@ impl Client for ureq::Agent {
|
||||||
uri: &Uri,
|
uri: &Uri,
|
||||||
alias_id: &str,
|
alias_id: &str,
|
||||||
share_id: &str,
|
share_id: &str,
|
||||||
file: FileChecked,
|
file_name: &str,
|
||||||
) -> Result<FileUploading, ClientError> {
|
file_size: u64,
|
||||||
let size = file.get_size();
|
) -> Result<String, ClientError> {
|
||||||
|
|
||||||
let res = {
|
let res = {
|
||||||
let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id));
|
let endpoint = uri.get_endpoint(format!("alias/upload/{}/files/tus", share_id));
|
||||||
|
|
||||||
self.post(endpoint)
|
self.post(endpoint)
|
||||||
.header("Sharry-Alias", alias_id)
|
.header("Sharry-Alias", alias_id)
|
||||||
.header("Sharry-File-Name", file.get_name())
|
.header("Sharry-File-Name", file_name)
|
||||||
.header("Upload-Length", size)
|
.header("Upload-Length", file_size)
|
||||||
.send_empty()
|
.send_empty()
|
||||||
.map_err(|e| ClientError::req_err(e))?
|
.map_err(ClientError::req_err)?
|
||||||
};
|
};
|
||||||
|
|
||||||
if res.status() != ureq::http::StatusCode::CREATED {
|
ClientError::res_check_status(res.status(), ureq::http::StatusCode::CREATED)?;
|
||||||
return Err(ClientError::ResponseStatus {
|
|
||||||
actual: res.status().as_u16(),
|
|
||||||
expected: ureq::http::StatusCode::CREATED.as_u16(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let location = (res.headers().get("Location"))
|
let location = (res.headers().get("Location"))
|
||||||
.ok_or_else(|| ClientError::res_parse_err("Location header not found"))?
|
.ok_or_else(|| ClientError::res_parse_err("Location header not found"))?
|
||||||
.to_str()
|
.to_str()
|
||||||
.map_err(|e| ClientError::res_parse_err(e))?
|
.map_err(ClientError::res_parse_err)?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
debug!("patch uri: {location}");
|
debug!("patch uri: {location}");
|
||||||
|
|
||||||
Ok(FileUploading::new(file.into_path(), size, location))
|
Ok(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sharry_file_patch(
|
||||||
|
&self,
|
||||||
|
patch_uri: &str,
|
||||||
|
alias_id: &str,
|
||||||
|
offset: u64,
|
||||||
|
chunk: &[u8],
|
||||||
|
) -> Result<u64, ClientError> {
|
||||||
|
let res = self
|
||||||
|
.patch(patch_uri)
|
||||||
|
.header("Sharry-Alias", alias_id)
|
||||||
|
.header("Upload-Offset", offset)
|
||||||
|
.send(chunk)
|
||||||
|
.map_err(ClientError::req_err)?;
|
||||||
|
|
||||||
|
ClientError::res_check_status(res.status(), ureq::http::StatusCode::NO_CONTENT)?;
|
||||||
|
|
||||||
|
let res_offset = (res.headers().get("Upload-Offset"))
|
||||||
|
.ok_or_else(|| ClientError::res_parse_err("Upload-Offset header not found"))?
|
||||||
|
.to_str()
|
||||||
|
.map_err(ClientError::res_parse_err)?
|
||||||
|
.parse::<u64>()
|
||||||
|
.map_err(ClientError::res_parse_err)?;
|
||||||
|
|
||||||
|
// get chunk length as `u64` (we have checked while reading the chunk!)
|
||||||
|
let chunk_len = u64::try_from(chunk.len()).expect("something's VERY wrong");
|
||||||
|
|
||||||
|
if offset + chunk_len == res_offset {
|
||||||
|
Ok(res_offset)
|
||||||
|
} else {
|
||||||
|
Err(ClientError::ResponseContent(format!(
|
||||||
|
"Unexpected Upload-Offset: {} (expected {})",
|
||||||
|
res_offset,
|
||||||
|
offset + chunk_len
|
||||||
|
)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,16 @@
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
|
||||||
fs, io,
|
fs, io,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ureq::http::{HeaderValue, StatusCode};
|
|
||||||
|
|
||||||
use super::{FileUploading, SharryFile};
|
use super::SharryFile;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct FileChecked {
|
pub struct FileChecked {
|
||||||
path: PathBuf,
|
pub(super) path: PathBuf,
|
||||||
|
pub(super) size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileChecked {
|
impl FileChecked {
|
||||||
|
|
@ -21,6 +19,7 @@ impl FileChecked {
|
||||||
if meta.is_file() {
|
if meta.is_file() {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path: fs::canonicalize(&value)?,
|
path: fs::canonicalize(&value)?,
|
||||||
|
size: meta.len(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(io::Error::new(
|
Err(io::Error::new(
|
||||||
|
|
@ -32,10 +31,6 @@ impl FileChecked {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'t> SharryFile<'t> for FileChecked {
|
impl<'t> SharryFile<'t> for FileChecked {
|
||||||
fn into_path(self) -> PathBuf {
|
|
||||||
self.path
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get a reference to the file's name
|
/// get a reference to the file's name
|
||||||
///
|
///
|
||||||
/// Uses `SharryFile::extract_file_name`, which may **panic**!
|
/// Uses `SharryFile::extract_file_name`, which may **panic**!
|
||||||
|
|
@ -44,6 +39,6 @@ impl<'t> SharryFile<'t> for FileChecked {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_size(&self) -> u64 {
|
fn get_size(&self) -> u64 {
|
||||||
fs::metadata(&self.path).unwrap().len()
|
self.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use checked::FileChecked;
|
pub use checked::FileChecked;
|
||||||
pub use uploading::{ChunkState, FileUploading, UploadError};
|
pub use uploading::FileUploading;
|
||||||
|
|
||||||
pub trait SharryFile<'t> {
|
pub trait SharryFile<'t> {
|
||||||
/// extract the filename part of a `Path` reference
|
/// extract the filename part of a `Path` reference
|
||||||
|
|
@ -21,8 +21,6 @@ pub trait SharryFile<'t> {
|
||||||
.expect("bad file name")
|
.expect("bad file name")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn into_path(self) -> PathBuf;
|
|
||||||
|
|
||||||
fn get_name(&'t self) -> &'t str;
|
fn get_name(&'t self) -> &'t str;
|
||||||
|
|
||||||
fn get_size(&self) -> u64;
|
fn get_size(&self) -> u64;
|
||||||
|
|
|
||||||
|
|
@ -1,50 +1,21 @@
|
||||||
use std::{
|
use std::{
|
||||||
ffi::OsStr,
|
|
||||||
fmt, fs,
|
fmt, fs,
|
||||||
io::{self, Read, Seek, SeekFrom},
|
io::{self, Read, Seek, SeekFrom},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::debug;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use ureq::http::{HeaderValue, StatusCode};
|
|
||||||
|
|
||||||
use super::SharryFile;
|
use super::{FileChecked, SharryFile};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct FileUploading {
|
pub struct FileUploading {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
size: u64,
|
size: u64,
|
||||||
uri: String,
|
patch_uri: String,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum UploadError {
|
|
||||||
#[error("file I/O error: {0}")]
|
|
||||||
FileIO(#[from] io::Error),
|
|
||||||
|
|
||||||
#[error("network request failed")]
|
|
||||||
Request,
|
|
||||||
|
|
||||||
#[error("unexpected response status")]
|
|
||||||
ResponseStatus,
|
|
||||||
|
|
||||||
#[error("could not parse offset header")]
|
|
||||||
ResponseOffset,
|
|
||||||
// #[error("chunk length conversion failed: {0}")]
|
|
||||||
// InvalidChunkLength(String),
|
|
||||||
|
|
||||||
// #[error("offset mismatch")]
|
|
||||||
// ResponseOffsetMismatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ChunkState {
|
|
||||||
Ok(FileUploading),
|
|
||||||
Err(FileUploading, UploadError),
|
|
||||||
Finished(PathBuf),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for FileUploading {
|
impl fmt::Display for FileUploading {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
|
|
@ -58,87 +29,55 @@ impl fmt::Display for FileUploading {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileUploading {
|
impl FileUploading {
|
||||||
pub fn new(path: PathBuf, size: u64, uri: String) -> Self {
|
pub fn new(file: FileChecked, patch_uri: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path,
|
path: file.path,
|
||||||
size,
|
size: file.size,
|
||||||
uri,
|
patch_uri,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_patch_uri(&self) -> &str {
|
||||||
|
&self.patch_uri
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_offset(&self) -> u64 {
|
pub fn get_offset(&self) -> u64 {
|
||||||
self.offset
|
self.offset
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_chunk(&self, chunk_size: usize) -> io::Result<Vec<u8>> {
|
pub fn read(&mut self, buf: &mut [u8]) -> io::Result<u64> {
|
||||||
let mut f = fs::File::open(&self.path)?;
|
let mut f = fs::File::open(&self.path)?;
|
||||||
|
|
||||||
f.seek(SeekFrom::Start(self.offset))?;
|
f.seek(SeekFrom::Start(self.offset))?;
|
||||||
|
let read_len = f.read(buf)?;
|
||||||
|
|
||||||
let mut bytes = vec![0; chunk_size];
|
// convert into `u64`
|
||||||
let read_len = f.read(&mut bytes)?;
|
//
|
||||||
bytes.truncate(read_len);
|
// BOOKMARK this might **panic** on platforms where `usize` has more than 64 bit.
|
||||||
|
// Also, you're reading more than 2 EiB ... in ONE chunk.
|
||||||
|
// Whoa! Maybe just chill?
|
||||||
|
let read_len = u64::try_from(read_len)
|
||||||
|
.unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", read_len, e));
|
||||||
|
|
||||||
Ok(bytes)
|
self.offset += read_len;
|
||||||
|
|
||||||
|
Ok(read_len)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_chunk(
|
pub fn check_eof(self) -> Result<Self, PathBuf> {
|
||||||
mut self,
|
if self.offset < self.size {
|
||||||
http: &ureq::Agent,
|
Ok(self)
|
||||||
alias: &Alias,
|
} else {
|
||||||
chunk_size: usize,
|
Err(self.path)
|
||||||
) -> ChunkState {
|
|
||||||
let chunk = match self.read_chunk(chunk_size) {
|
|
||||||
Err(e) => return ChunkState::Err(self, UploadError::FileIO(e)),
|
|
||||||
Ok(value) => value,
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(res) = (http.patch(&self.uri))
|
|
||||||
.sharry_header(alias)
|
|
||||||
.header("Upload-Offset", self.offset)
|
|
||||||
.send(&chunk)
|
|
||||||
else {
|
|
||||||
return ChunkState::Err(self, UploadError::Request);
|
|
||||||
};
|
|
||||||
|
|
||||||
if res.status() != StatusCode::NO_CONTENT {
|
|
||||||
return ChunkState::Err(self, UploadError::ResponseStatus);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(Ok(Ok(res_offset))) = (res.headers().get("Upload-Offset"))
|
|
||||||
.map(HeaderValue::to_str)
|
|
||||||
.map(|v| v.map(str::parse::<u64>))
|
|
||||||
else {
|
|
||||||
return ChunkState::Err(self, UploadError::ResponseOffset);
|
|
||||||
};
|
|
||||||
|
|
||||||
// convert chunk.len() into an `u64`
|
|
||||||
//
|
|
||||||
// BOOKMARK this might panic on platforms where `usize` > 64 bit.
|
|
||||||
// Also whoa, you've sent more than 2 EiB ... in ONE chunk.
|
|
||||||
// Maybe just chill?
|
|
||||||
let chunk_len = u64::try_from(chunk.len())
|
|
||||||
.unwrap_or_else(|e| panic!("usize={} did not fit into u64: {}", chunk.len(), e));
|
|
||||||
|
|
||||||
if self.offset + chunk_len != res_offset {
|
|
||||||
return ChunkState::Err(self, UploadError::ResponseOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.offset = res_offset;
|
|
||||||
|
|
||||||
if self.offset == self.size {
|
|
||||||
return ChunkState::Finished(self.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
ChunkState::Ok(self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'t> SharryFile<'t> for FileUploading {
|
impl<'t> SharryFile<'t> for FileUploading {
|
||||||
fn into_path(self) -> PathBuf {
|
/// get a reference to the file's name
|
||||||
self.path
|
///
|
||||||
}
|
/// Uses `SharryFile::extract_file_name`, which may **panic**!
|
||||||
|
|
||||||
fn get_name(&'t self) -> &'t str {
|
fn get_name(&'t self) -> &'t str {
|
||||||
<Self as SharryFile>::extract_file_name(&self.path)
|
<Self as SharryFile>::extract_file_name(&self.path)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,4 @@ mod file;
|
||||||
|
|
||||||
pub use api::{NewShareRequest, Uri};
|
pub use api::{NewShareRequest, Uri};
|
||||||
pub use client::{Client, ClientError};
|
pub use client::{Client, ClientError};
|
||||||
pub use file::{ChunkState, FileChecked, FileUploading, SharryFile, UploadError};
|
pub use file::{FileChecked, FileUploading, SharryFile};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue