mod chunks; use std::{ ffi::OsStr, fs::metadata, io::{self, ErrorKind}, path::PathBuf, }; use log::{debug, error}; use ureq::{Error::Other, http::StatusCode}; use super::{ alias::{Alias, SharryAlias}, share::Share, }; pub use chunks::{Chunk, FileChunks}; #[derive(Debug, Clone, Hash)] pub struct File { path: PathBuf, name: String, size: u64, patch_uri: Option, } impl File { pub fn new(path: impl Into) -> io::Result { let path: PathBuf = path.into(); let m = metadata(&path)?; if !m.is_file() { return Err(io::Error::new(ErrorKind::NotFound, "not a file")); } let name = (path.file_name().and_then(OsStr::to_str)) .ok_or_else(|| io::Error::new(ErrorKind::NotFound, "bad file name"))? .to_string(); Ok(Self { path, name, size: m.len(), patch_uri: None, }) } pub fn create(self, http: &ureq::Agent, share: &Share) -> Result { if self.patch_uri.is_some() { return Err(Other("patch_uri already set".into())); } let endpoint = (share.alias).get_endpoint(format!("alias/upload/{}/files/tus", share.id)); let res = (http.post(endpoint)) .sharry_header(share.alias) .header("Sharry-File-Name", &self.name) .header("Upload-Length", self.size) .send_empty()?; if res.status() != StatusCode::CREATED { return Err(Other("unexpected response status".into())); } let location = (res.headers().get("Location")) .ok_or_else(|| Other("Location header not found".into()))? .to_str() .map_err(|_| Other("Location header invalid".into()))? .to_string(); debug!("received uri: {location}"); Ok(Self { path: self.path, name: self.name, size: self.size, patch_uri: Some(location), }) } pub fn chunked(&self, chunk_size: usize) -> FileChunks { FileChunks::new(&self.path, chunk_size) } pub fn upload_chunk( &self, http: &ureq::Agent, alias: &Alias, chunk: &Chunk, ) -> Result<(), ureq::Error> { let patch_uri = (self.patch_uri.as_ref()).ok_or_else(|| Other("unset patch_uri".into()))?; debug!("upload uri: {patch_uri:?}"); let res = (http.patch(patch_uri)) .sharry_header(alias) .header("Upload-Offset", chunk.offset) .send(&chunk.bytes)?; if res.status() != StatusCode::NO_CONTENT { return Err(Other("unexpected response status".into())); } let offset = (res.headers().get("Upload-Offset")) .ok_or_else(|| Other("Upload-Offset header not found".into()))? .to_str() .map_err(|e| Other(e.into()))? .parse::() .map_err(|e| Other(e.into()))?; if chunk.after() != offset { return Err(Other("unexpected offset response".into())); } Ok(()) } }