Compare commits

..

4 commits

9 changed files with 203 additions and 19 deletions

92
Cargo.lock generated
View file

@ -73,6 +73,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.10.1" version = "1.10.1"
@ -187,6 +193,27 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@ -460,6 +487,16 @@ version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags",
"libc",
]
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.0" version = "0.8.0"
@ -565,6 +602,17 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.11.1" version = "1.11.1"
@ -701,9 +749,11 @@ name = "shrupl"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"dirs-next",
"env_logger", "env_logger",
"log", "log",
"serde", "serde",
"serde_json",
"ureq", "ureq",
] ]
@ -753,6 +803,26 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.41" version = "0.3.41"
@ -898,6 +968,28 @@ dependencies = [
"rustls-pki-types", "rustls-pki-types",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"

View file

@ -6,9 +6,11 @@ description = "ShrUpl is a tool to upload files to a Sharry Instance through a p
[dependencies] [dependencies]
clap = { version = "4.5.38", features = ["derive"] } clap = { version = "4.5.38", features = ["derive"] }
dirs-next = "2.0.0"
env_logger = "0.11.8" env_logger = "0.11.8"
log = "0.4.27" log = "0.4.27"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
ureq = { version = "3.0.11", features = ["json"] } ureq = { version = "3.0.11", features = ["json"] }
[profile.release] [profile.release]

49
src/appstate.rs Normal file
View file

@ -0,0 +1,49 @@
use std::{
fs,
io::{self, Write},
path::Path,
};
use log::{debug, trace};
use serde::{Deserialize, Serialize};
use super::{
cli::Cli,
sharry::{Alias, File, Share},
};
#[derive(Serialize, Deserialize, Debug)]
pub struct AppState {
alias: Alias,
share: Share,
files: Vec<File>,
}
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)
}
fn save(&self, file_name: impl AsRef<Path>) -> io::Result<()> {
let json = serde_json::to_string_pretty(self).map_err(io::Error::other)?;
let mut file = fs::File::create(file_name)?;
file.write_all(json.as_bytes())?;
Ok(())
}
pub fn try_resume(args: &Cli) -> Option<Self> {
let file_name = dirs_next::cache_dir()?
.join("shrupl")
.join(format!("{}.json", args.get_hash()));
trace!("loading from {}", file_name.display());
Self::load(&file_name)
.inspect_err(|e| debug!("could not resume from {}: {e}", file_name.display()))
.ok()
}
}

View file

@ -1,15 +1,18 @@
use std::time::Duration; use std::{
hash::{DefaultHasher, Hash, Hasher},
time::Duration,
};
use clap::{Parser, builder::PossibleValuesParser}; use clap::{Parser, builder::PossibleValuesParser};
use super::sharry::{File, Alias, Uri, NewShareRequest}; use super::sharry::{Alias, File, NewShareRequest, Uri};
#[derive(Parser, Debug, Hash)] #[derive(Parser, Debug, Hash)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
pub struct Cli { pub struct Cli {
/// Timeout in seconds for HTTP actions (set 0 or invalid to disable) /// Timeout in seconds for HTTP actions (set 0 or invalid to disable)
#[arg( #[arg(
short, long, short, long,
default_value = "10", value_name = "SECS", default_value = "10", value_name = "SECS",
value_parser = parse_seconds, value_parser = parse_seconds,
)] )]
@ -70,4 +73,18 @@ impl Cli {
pub fn get_share_request(&self) -> NewShareRequest { pub fn get_share_request(&self) -> NewShareRequest {
NewShareRequest::new(&self.name, self.description.as_ref(), self.max_views) NewShareRequest::new(&self.name, self.description.as_ref(), self.max_views)
} }
pub fn get_hash(&self) -> String {
let file_refs = {
let mut refs: Vec<_> = self.files.iter().map(File::get_path).collect();
refs.sort_unstable();
refs
};
let mut hasher = DefaultHasher::new();
(self.get_alias(), file_refs).hash(&mut hasher);
format!("{:x}", hasher.finish())
}
} }

View file

@ -1,3 +1,4 @@
mod appstate;
mod cli; mod cli;
mod sharry; mod sharry;
@ -5,6 +6,7 @@ use clap::Parser;
use log::{error, info}; use log::{error, info};
use ureq::Agent; use ureq::Agent;
use appstate::AppState;
use cli::Cli; use cli::Cli;
use sharry::Share; use sharry::Share;
@ -20,6 +22,10 @@ fn main() {
.build() .build()
.into(); .into();
if let Some(state) = AppState::try_resume(&args) {
info!("state: {state:?}");
}
let alias = args.get_alias(); let alias = args.get_alias();
let share = Share::create(&agent, &alias, args.get_share_request()).unwrap(); let share = Share::create(&agent, &alias, args.get_share_request()).unwrap();
info!("share: {share:?}"); info!("share: {share:?}");
@ -29,7 +35,7 @@ fn main() {
info!("file: {file:?}"); info!("file: {file:?}");
for chunk in file.chunked(args.chunk_size * 1024 * 1024).seek(0) { for chunk in file.chunked(args.chunk_size * 1024 * 1024).seek(0) {
info!("chunk len: {}", chunk.bytes.len()); info!("chunk: {chunk:?}");
file.upload_chunk(&agent, &alias, &chunk) file.upload_chunk(&agent, &alias, &chunk)
.unwrap_or_else(|e| { .unwrap_or_else(|e| {

View file

@ -1,11 +1,12 @@
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use log::debug; use log::debug;
use serde::{Deserialize, Serialize};
use ureq::RequestBuilder; use ureq::RequestBuilder;
use super::api::Uri; use super::api::Uri;
#[derive(Debug, Hash)] #[derive(Serialize, Deserialize, Debug, Hash)]
pub struct Alias { pub struct Alias {
pub(super) api_uri: String, pub(super) api_uri: String,
pub(super) id: String, pub(super) id: String,

View file

@ -1,4 +1,5 @@
use std::{ use std::{
fmt::Debug,
fs::File, fs::File,
io::{Read, Seek, SeekFrom}, io::{Read, Seek, SeekFrom},
path::Path, path::Path,
@ -8,7 +9,7 @@ use log::error;
pub struct FileChunks<'t> { pub struct FileChunks<'t> {
file_path: &'t Path, file_path: &'t Path,
chunk_index: u64, offset: u64,
chunk_size: usize, chunk_size: usize,
} }
@ -16,15 +17,15 @@ impl<'t> FileChunks<'t> {
pub(super) fn new(path: &'t Path, chunk_size: usize) -> Self { pub(super) fn new(path: &'t Path, chunk_size: usize) -> Self {
Self { Self {
file_path: path, file_path: path,
chunk_index: 0, offset: 0,
chunk_size, chunk_size,
} }
} }
pub fn seek(self, chunk_index: u64) -> Self { pub fn seek(self, offset: u64) -> Self {
Self { Self {
file_path: self.file_path, file_path: self.file_path,
chunk_index, offset,
chunk_size: self.chunk_size, chunk_size: self.chunk_size,
} }
} }
@ -34,10 +35,7 @@ impl Iterator for FileChunks<'_> {
type Item = Chunk; type Item = Chunk;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let offset = self.chunk_index let offset = self.offset;
* u64::try_from(self.chunk_size)
.inspect_err(|e| error!("Error converting to u64: {e}"))
.ok()?;
let mut f = File::open(self.file_path) let mut f = File::open(self.file_path)
.inspect_err(|e| error!("Error opening file: {e}")) .inspect_err(|e| error!("Error opening file: {e}"))
@ -50,7 +48,11 @@ impl Iterator for FileChunks<'_> {
.ok()?; .ok()?;
bytes.truncate(read_len); bytes.truncate(read_len);
self.chunk_index += 1; let read_len: u64 = read_len
.try_into()
.inspect_err(|e| error!("Error converting to u64: {e}"))
.ok()?;
self.offset += read_len;
Some(Self::Item { offset, bytes }).filter(|c| !c.bytes.is_empty()) Some(Self::Item { offset, bytes }).filter(|c| !c.bytes.is_empty())
} }
@ -61,6 +63,15 @@ pub struct Chunk {
pub bytes: Vec<u8>, pub bytes: Vec<u8>,
} }
impl Debug for Chunk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Chunk")
.field("offset", &self.offset)
.field("len", &self.bytes.len())
.finish()
}
}
impl Chunk { impl Chunk {
pub fn after(&self) -> u64 { pub fn after(&self) -> u64 {
let len: u64 = self.bytes.len().try_into().unwrap(); let len: u64 = self.bytes.len().try_into().unwrap();

View file

@ -2,13 +2,14 @@ mod chunks;
use std::{ use std::{
ffi::OsStr, ffi::OsStr,
fs::metadata, fs::{canonicalize, metadata},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
io::{self, ErrorKind}, io::{self, ErrorKind},
path::{Path, PathBuf, absolute}, path::{Path, PathBuf},
}; };
use log::{debug, error}; use log::{debug, error};
use serde::{Deserialize, Serialize};
use ureq::{Error::Other, http::StatusCode}; use ureq::{Error::Other, http::StatusCode};
use super::{ use super::{
@ -17,7 +18,7 @@ use super::{
}; };
pub use chunks::{Chunk, FileChunks}; pub use chunks::{Chunk, FileChunks};
#[derive(Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct File { pub struct File {
abs_path: PathBuf, abs_path: PathBuf,
name: String, name: String,
@ -33,7 +34,7 @@ impl Hash for File {
impl File { impl File {
pub fn new(path: impl AsRef<Path>) -> io::Result<Self> { pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
let abs_path = absolute(path)?; let abs_path = canonicalize(path)?;
let m = metadata(&abs_path)?; let m = metadata(&abs_path)?;
if !m.is_file() { if !m.is_file() {
@ -52,6 +53,10 @@ impl File {
}) })
} }
pub fn get_path(&self) -> &Path {
&self.abs_path
}
pub fn create( pub fn create(
self, self,
http: &ureq::Agent, http: &ureq::Agent,

View file

@ -1,11 +1,12 @@
use log::debug; use log::debug;
use serde::{Deserialize, Serialize};
use super::{ use super::{
alias::{Alias, SharryAlias}, alias::{Alias, SharryAlias},
api::{NewShareRequest, NewShareResponse, NotifyShareResponse}, api::{NewShareRequest, NewShareResponse, NotifyShareResponse},
}; };
#[derive(Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Share { pub struct Share {
pub(super) id: String, pub(super) id: String,
} }