use std::{ ffi::OsStr, fs, io::{self, Read, Seek, SeekFrom}, path::PathBuf, }; use log::debug; use serde::{Deserialize, Serialize}; use ureq::http::{HeaderValue, StatusCode}; use super::{Alias, SharryAlias, SharryFile}; #[derive(Serialize, Deserialize, Debug)] pub struct FileUploading { path: PathBuf, size: u64, uri: String, offset: u64, } #[derive(Debug)] pub enum UploadError { FileIO(io::Error), Request, ResponseStatus, ResponseOffset, } pub enum ChunkState { Ok(FileUploading), Err(FileUploading, UploadError), Finished(PathBuf), } impl std::fmt::Display for FileUploading { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "Uploading ({:?}, {}, {})", self.path.display(), self.size, self.offset ) } } impl FileUploading { pub(super) fn new(path: PathBuf, size: u64, uri: String) -> Self { Self { path, size, uri, offset: 0, } } pub fn get_offset(&self) -> u64 { self.offset } fn read_chunk(&self, chunk_size: usize) -> io::Result> { let mut f = fs::File::open(&self.path)?; f.seek(SeekFrom::Start(self.offset))?; let mut bytes = vec![0; chunk_size]; let read_len = f.read(&mut bytes)?; bytes.truncate(read_len); Ok(bytes) } pub fn upload_chunk( mut self, http: &ureq::Agent, alias: &Alias, chunk_size: usize, ) -> 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::)) 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 { fn get_name(&'t self) -> &'t str { ::extract_file_name(&self.path) } fn get_size(&self) -> u64 { self.size } }