shrupl/src/sharry/file/mod.rs

135 lines
3.5 KiB
Rust
Raw Normal View History

2025-05-27 10:44:06 +00:00
mod chunks;
2025-05-27 00:42:43 +00:00
2025-05-27 20:00:21 +00:00
use std::{
ffi::OsStr,
2025-06-02 13:32:34 +00:00
fs::{canonicalize, metadata},
hash::{Hash, Hasher},
2025-05-27 20:00:21 +00:00
io::{self, ErrorKind},
2025-06-02 13:32:34 +00:00
path::{Path, PathBuf},
2025-05-27 20:00:21 +00:00
};
2025-05-27 00:42:43 +00:00
2025-05-27 10:44:06 +00:00
use log::{debug, error};
2025-06-02 23:57:17 +00:00
use serde::{Deserialize, Serialize};
2025-05-27 10:44:06 +00:00
use ureq::{Error::Other, http::StatusCode};
use super::{
alias::{Alias, SharryAlias},
share::Share,
};
pub use chunks::{Chunk, FileChunks};
2025-06-02 23:57:17 +00:00
#[derive(Serialize, Deserialize, Debug, Clone)]
2025-05-27 10:44:06 +00:00
pub struct File {
abs_path: PathBuf,
2025-05-27 10:44:06 +00:00
name: String,
size: u64,
patch_uri: Option<String>,
}
impl Hash for File {
fn hash<H: Hasher>(&self, state: &mut H) {
self.abs_path.hash(state);
}
}
2025-05-27 10:44:06 +00:00
impl File {
2025-05-28 14:07:29 +00:00
pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
2025-06-02 13:32:34 +00:00
let abs_path = canonicalize(path)?;
2025-05-27 10:44:06 +00:00
let m = metadata(&abs_path)?;
2025-05-27 20:00:21 +00:00
if !m.is_file() {
return Err(io::Error::new(ErrorKind::NotFound, "not a file"));
}
2025-05-27 10:44:06 +00:00
let name = (abs_path.file_name().and_then(OsStr::to_str))
2025-05-27 20:00:21 +00:00
.ok_or_else(|| io::Error::new(ErrorKind::NotFound, "bad file name"))?
.to_string();
2025-05-27 10:44:06 +00:00
Ok(Self {
abs_path,
2025-05-27 10:44:06 +00:00
name,
2025-05-27 20:00:21 +00:00
size: m.len(),
2025-05-27 10:44:06 +00:00
patch_uri: None,
})
}
2025-06-02 23:57:17 +00:00
pub fn get_path(&self) -> &Path {
&self.abs_path
}
2025-05-28 14:07:29 +00:00
pub fn create(
self,
http: &ureq::Agent,
alias: &Alias,
share: &Share,
) -> Result<Self, ureq::Error> {
2025-05-27 10:44:06 +00:00
if self.patch_uri.is_some() {
return Err(Other("patch_uri already set".into()));
}
2025-05-28 14:07:29 +00:00
let endpoint = alias.get_endpoint(format!("alias/upload/{}/files/tus", share.id));
2025-05-27 10:44:06 +00:00
2025-05-27 20:00:21 +00:00
let res = (http.post(endpoint))
2025-05-28 14:07:29 +00:00
.sharry_header(alias)
2025-05-27 10:44:06 +00:00
.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()));
}
2025-05-27 20:00:21 +00:00
let location = (res.headers().get("Location"))
2025-05-27 10:44:06 +00:00
.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,
2025-05-27 10:44:06 +00:00
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)
2025-05-27 10:44:06 +00:00
}
pub fn upload_chunk(
&self,
http: &ureq::Agent,
alias: &Alias,
chunk: &Chunk,
) -> Result<(), ureq::Error> {
2025-05-27 20:00:21 +00:00
let patch_uri = (self.patch_uri.as_ref()).ok_or_else(|| Other("unset patch_uri".into()))?;
2025-05-27 10:44:06 +00:00
debug!("upload uri: {patch_uri:?}");
2025-05-27 20:00:21 +00:00
let res = (http.patch(patch_uri))
2025-05-27 10:44:06 +00:00
.sharry_header(alias)
.header("Upload-Offset", chunk.offset)
.send(&chunk.bytes)?;
if res.status() != StatusCode::NO_CONTENT {
return Err(Other("unexpected response status".into()));
}
2025-05-27 20:00:21 +00:00
let offset = (res.headers().get("Upload-Offset"))
2025-05-27 10:44:06 +00:00
.ok_or_else(|| Other("Upload-Offset header not found".into()))?
.to_str()
2025-05-27 20:00:21 +00:00
.map_err(|e| Other(e.into()))?
2025-05-27 10:44:06 +00:00
.parse::<u64>()
2025-05-27 20:00:21 +00:00
.map_err(|e| Other(e.into()))?;
2025-05-27 10:44:06 +00:00
if chunk.after() != offset {
return Err(Other("unexpected offset response".into()));
}
Ok(())
}
}