reimplementation of sharry::file submodule

- compileable
This commit is contained in:
Jörn-Michael Miehe 2025-06-04 13:25:00 +00:00
parent 7edadd7ca1
commit 88c6ce94de
10 changed files with 372 additions and 168 deletions

View file

@ -11,6 +11,7 @@
"source.organizeImports": "explicit"
},
},
"rust-analyzer.imports.prefix": "plain",
// // override the default setting (`cargo check --all-targets`) which produces the following error
// // "can't find crate for `test`" when the default compilation target is a no_std target
// "rust-analyzer.checkOnSave.allTargets": false,

View file

@ -1,7 +1,7 @@
use std::{
fs,
io::{self, Write},
path::Path,
path::{Path, PathBuf},
};
use log::{debug, trace};
@ -9,22 +9,31 @@ use serde::{Deserialize, Serialize};
use super::{
cli::Cli,
sharry::{Alias, File, Share},
sharry::{Alias, FileChecked, FileUploading, Share},
};
#[derive(Serialize, Deserialize, Debug)]
pub struct AppState {
alias: Alias,
share: Share,
files: Vec<File>,
checked: Vec<FileChecked>,
uploading: Vec<FileUploading>,
}
fn get_cachefile(args: &Cli) -> Option<PathBuf> {
let file_name: PathBuf = dirs_next::cache_dir()?
.join("shrupl")
.join(format!("{}.json", args.get_hash()));
trace!("cachefile: {}", file_name.display());
Some(file_name)
}
impl AppState {
fn load(file_name: impl AsRef<Path>) -> io::Result<Self> {
let content = fs::read_to_string(file_name)?;
let state = serde_json::from_str(&content).map_err(io::Error::other)?;
Ok(state)
serde_json::from_str(&content).map_err(io::Error::other)
}
fn save(&self, file_name: impl AsRef<Path>) -> io::Result<()> {
@ -36,11 +45,12 @@ impl AppState {
}
pub fn try_resume(args: &Cli) -> Option<Self> {
let file_name = dirs_next::cache_dir()?
.join("shrupl")
.join(format!("{}.json", args.get_hash()));
let file_name = get_cachefile(args)?;
trace!("loading from {}", file_name.display());
// let content = fs::read_to_string(&file_name).ok()?;
// serde_json::from_str(&content)
// .inspect_err(|e| debug!("could not resume from {}: {e}", &file_name.display()))
// .ok()
Self::load(&file_name)
.inspect_err(|e| debug!("could not resume from {}: {e}", file_name.display()))

View file

@ -5,7 +5,7 @@ use std::{
use clap::{Parser, builder::PossibleValuesParser};
use super::sharry::{Alias, File, NewShareRequest, Uri};
use super::sharry::{Alias, FileChecked, NewShareRequest, Uri};
#[derive(Parser, Debug, Hash)]
#[command(version, about, long_about = None)]
@ -50,15 +50,15 @@ pub struct Cli {
/// Files to upload to the new share
#[arg(value_name = "FILE", required = true, value_parser = parse_sharry_file)]
pub files: Vec<File>,
pub files: Vec<FileChecked>,
}
fn parse_seconds(data: &str) -> Result<Duration, String> {
data.parse().or(Ok(0)).map(Duration::from_secs)
}
fn parse_sharry_file(data: &str) -> Result<File, String> {
File::new(data).map_err(|e| e.to_string())
fn parse_sharry_file(data: &str) -> Result<FileChecked, String> {
FileChecked::new(data).map_err(|e| e.to_string())
}
impl Cli {
@ -76,7 +76,7 @@ impl Cli {
pub fn get_hash(&self) -> String {
let file_refs = {
let mut refs: Vec<_> = self.files.iter().map(File::get_path).collect();
let mut refs: Vec<_> = self.files.iter().map(FileChecked::get_path).collect();
refs.sort_unstable();
refs

View file

@ -31,18 +31,18 @@ fn main() {
info!("share: {share:?}");
for file in args.files {
let file = file.create(&agent, &alias, &share).unwrap();
let file = file.start_upload(&agent, &alias, &share).unwrap();
info!("file: {file:?}");
for chunk in file.chunked(args.chunk_size * 1024 * 1024).seek(0) {
info!("chunk: {chunk:?}");
// for chunk in file.chunked(args.chunk_size * 1024 * 1024).seek(0) {
// info!("chunk: {chunk:?}");
file.upload_chunk(&agent, &alias, &chunk)
.unwrap_or_else(|e| {
error!("error: {e}");
panic!("{e}");
});
}
// file.upload_chunk(&agent, &alias, &chunk)
// .unwrap_or_else(|e| {
// error!("error: {e}");
// panic!("{e}");
// });
// }
}
share.notify(&agent, &alias).unwrap();

View file

@ -6,7 +6,9 @@ use std::{
};
use log::error;
// use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub struct FileChunks<'t> {
file_path: &'t Path,
offset: u64,
@ -37,19 +39,22 @@ impl Iterator for FileChunks<'_> {
fn next(&mut self) -> Option<Self::Item> {
let offset = self.offset;
let mut f = File::open(self.file_path)
.inspect_err(|e| error!("Error opening file: {e}"))
.ok()?;
f.seek(SeekFrom::Start(offset)).ok()?;
let bytes = {
let mut f = File::open(self.file_path)
.inspect_err(|e| error!("Error opening file: {e}"))
.ok()?;
f.seek(SeekFrom::Start(offset)).ok()?;
let mut bytes = vec![0; self.chunk_size];
let read_len = (f.read(&mut bytes))
.inspect_err(|e| error!("Error reading file: {e}"))
.ok()?;
bytes.truncate(read_len);
let mut bytes = vec![0; self.chunk_size];
let read_len = (f.read(&mut bytes))
.inspect_err(|e| error!("Error reading file: {e}"))
.ok()?;
bytes.truncate(read_len);
let read_len: u64 = read_len
.try_into()
bytes
};
let read_len: u64 = (bytes.len().try_into())
.inspect_err(|e| error!("Error converting to u64: {e}"))
.ok()?;
self.offset += read_len;

142
src/sharry/file/_mod.rs Normal file
View file

@ -0,0 +1,142 @@
mod chunks;
use std::{
ffi::OsStr,
fs::{canonicalize, metadata},
hash::{Hash, Hasher},
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use log::{debug, error};
use serde::{Deserialize, Serialize};
use ureq::{Error::Other, http::StatusCode};
use super::{
alias::{Alias, SharryAlias},
share::Share,
};
pub use chunks::{Chunk, FileChunks};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File {
abs_path: PathBuf,
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);
}
}
impl PartialEq for File {
fn eq(&self, other: &Self) -> bool {
self.abs_path == other.abs_path
}
}
impl Eq for File {}
impl File {
pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
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 get_path(&self) -> &Path {
&self.abs_path
}
pub fn create(
self,
http: &ureq::Agent,
alias: &Alias,
share: &Share,
) -> Result<Self, ureq::Error> {
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::<u64>()
.map_err(|e| Other(e.into()))?;
if chunk.after() != offset {
return Err(Other("unexpected offset response".into()));
}
Ok(())
}
}

View file

@ -0,0 +1,80 @@
use std::{
ffi::OsStr,
fs,
io::{self, ErrorKind},
path::{Path, PathBuf},
};
use log::debug;
use serde::{Deserialize, Serialize};
use ureq::http::StatusCode;
use super::{Alias, FileUploading, Share, SharryAlias};
#[derive(Serialize, Deserialize, Debug, Clone, Hash)]
pub struct FileChecked {
path: PathBuf,
}
impl FileChecked {
pub fn new(value: impl AsRef<Path>) -> io::Result<Self> {
let meta = fs::metadata(&value)?;
if meta.is_file() {
Ok(Self {
path: fs::canonicalize(&value)?,
})
} else {
Err(io::Error::new(
ErrorKind::InvalidInput,
"Not a regular file",
))
}
}
pub fn get_path(&self) -> &Path {
&self.path
}
pub fn start_upload(
self,
http: &ureq::Agent,
alias: &Alias,
share: &Share,
) -> io::Result<FileUploading> {
let size = usize::try_from(fs::metadata(&self.path)?.len()).map_err(io::Error::other)?;
let res = {
let endpoint = alias.get_endpoint(format!("alias/upload/{}/files/tus", share.id));
let name = (self.path.file_name().and_then(OsStr::to_str))
.ok_or_else(|| io::Error::new(ErrorKind::NotFound, "bad file name"))?
.to_string();
(http.post(endpoint))
.sharry_header(alias)
.header("Sharry-File-Name", &name)
.header("Upload-Length", size)
.send_empty()
.map_err(io::Error::other)?
};
if res.status() != StatusCode::CREATED {
return Err(io::Error::other("unexpected response status"));
}
let location = (res.headers().get("Location"))
.ok_or_else(|| io::Error::other("Location header not found"))?
.to_str()
.map_err(|_| io::Error::other("Location header invalid"))?
.to_string();
debug!("patch uri: {location}");
Ok(FileUploading {
path: self.path,
size,
uri: location,
offset: 0,
})
}
}

View file

@ -1,134 +1,7 @@
mod chunks;
mod checked;
mod uploading;
use std::{
ffi::OsStr,
fs::{canonicalize, metadata},
hash::{Hash, Hasher},
io::{self, ErrorKind},
path::{Path, PathBuf},
};
pub use checked::FileChecked;
pub use uploading::FileUploading;
use log::{debug, error};
use serde::{Deserialize, Serialize};
use ureq::{Error::Other, http::StatusCode};
use super::{
alias::{Alias, SharryAlias},
share::Share,
};
pub use chunks::{Chunk, FileChunks};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File {
abs_path: PathBuf,
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);
}
}
impl File {
pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
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 get_path(&self) -> &Path {
&self.abs_path
}
pub fn create(
self,
http: &ureq::Agent,
alias: &Alias,
share: &Share,
) -> Result<Self, ureq::Error> {
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::<u64>()
.map_err(|e| Other(e.into()))?;
if chunk.after() != offset {
return Err(Other("unexpected offset response".into()));
}
Ok(())
}
}
use super::{Alias, Share, alias::SharryAlias};

View file

@ -0,0 +1,93 @@
use std::{
fs::File,
io::{self, Read, Seek, SeekFrom},
path::PathBuf,
};
use log::debug;
use serde::{Deserialize, Serialize};
use ureq::{
Error::Other,
http::{HeaderValue, StatusCode},
};
use super::{Alias, SharryAlias};
#[derive(Serialize, Deserialize, Debug)]
pub struct FileUploading {
pub(super) path: PathBuf,
pub(super) size: usize,
pub(super) uri: String,
pub(super) offset: usize,
}
pub enum UploadError {
FileIO(io::Error),
Request,
ResponseStatus,
ResponseOffset,
}
pub enum ChunkState {
Ok(FileUploading),
Err(FileUploading, UploadError),
Finished,
}
impl FileUploading {
fn read_chunk(&self, chunk_size: usize) -> io::Result<Vec<u8>> {
let offset = u64::try_from(self.offset).map_err(io::Error::other)?;
let mut f = File::open(&self.path)?;
f.seek(SeekFrom::Start(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::<usize>))
else {
return ChunkState::Err(self, UploadError::ResponseOffset);
};
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;
}
ChunkState::Ok(self)
}
}

View file

@ -7,5 +7,5 @@ mod share;
pub use alias::Alias;
pub use api::{NewShareRequest, Uri};
pub use file::File;
pub use file::{FileChecked, FileUploading};
pub use share::Share;