mod chunks; use std::{ ffi::OsStr, fs::{canonicalize, metadata}, hash::{Hash, Hasher}, io::{self, ErrorKind}, path::{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)] pub struct File { abs_path: PathBuf, name: String, size: u64, patch_uri: Option, } impl Hash for File { fn hash(&self, state: &mut H) { self.abs_path.hash(state); } } impl File { pub fn new(path: impl AsRef) -> io::Result { let abs_path = canonicalize(path)?; let m = metadata(&abs_path)?; if !m.is_file() { return Err(io::Error::new(ErrorKind::NotFound, "not a file")); } let name = (abs_path.file_name().and_then(OsStr::to_str)) .ok_or_else(|| io::Error::new(ErrorKind::NotFound, "bad file name"))? .to_string(); Ok(Self { abs_path, name, size: m.len(), patch_uri: None, }) } pub fn create( self, http: &ureq::Agent, alias: &Alias, share: &Share, ) -> Result { if self.patch_uri.is_some() { return Err(Other("patch_uri already set".into())); } let endpoint = alias.get_endpoint(format!("alias/upload/{}/files/tus", share.id)); let res = (http.post(endpoint)) .sharry_header(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 { abs_path: self.abs_path, name: self.name, size: self.size, patch_uri: Some(location), }) } pub fn chunked(&self, chunk_size: usize) -> FileChunks { FileChunks::new(&self.abs_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(()) } }