Skip to content

Commit

Permalink
Added some checks to file transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
manforowicz committed Apr 8, 2024
1 parent 5c4f08c commit 67c252a
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 174 deletions.
32 changes: 21 additions & 11 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@ Quick notes on items to consider implementing.
Not all of them are desirable or necessary.

- Deduplicate errors in the error vec the hole puncher can return.
This might give a more helpful error message to the user.

- Give the client the option to select a server to use.

- Restructure the hole puncher to force keeping connection to server open
during hole-punching. That might please some NATs that lose state when TCP connection is closed.
This might give a more helpful error message to the user.

- Make peer authentication not "block" hole punching.
"Blocking" might be an issue when the peer is receiving other
incoming connections. But this probably won't happen unless
the peer's device is acting as some sort of server.
"Blocking" might be an issue when the peer is receiving other
incoming connections. But this probably won't happen unless
the peer's device is acting as some sort of server.

- Improve error message everywhere in general. Have helpful tips to the user.

Expand All @@ -23,8 +18,23 @@ the peer's device is acting as some sort of server.

- Maybe add some versioning to the protocols?

- Add error handling for if the peer's file accept response is the wrong length.

- Make sure reader returns an EOF error if interrupted?

- Think: What other functionality can I pull out into gday_file_offer_protocol.

## Low-priority ideas

- Allow sending a simple text string instead of only files.
Though, I don't think this is a common use case, so will only
add if I get requests.
Though, I don't think this is a common use case, so will only
add if I get requests.

- Let the client select a source port, to utilize port forwarding.
However, turns out port forwarding works for inbound connections,
and not outbound ones, so this wouldn't help.

- Restructure the hole puncher to force keeping connection to server open
during hole-punching. That might please some NATs that lose state when TCP connection is closed.
This is not really necessary, since I can just add a comment
telling any library users to not drop ServerConnection when calling connect to peer.
70 changes: 34 additions & 36 deletions gday/src/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ use std::{

use crate::TMP_DOWNLOAD_PREFIX;

/// Asks the user which of these files to accept.
/// Every accepted file in `files` will be represented by a
/// `true` at the same index in the returned `Vec<bool>`.
/// TODO: Update this DOC comment
pub fn confirm_receive(files: &[FileMeta]) -> std::io::Result<Vec<Option<u64>>> {
// size of all new and interrupted files to download
/// Asks the user which of these offered `files` to accept.
/// Returns a `Vec<Option<u64>>`, where each
/// - `None` represents rejecting the file at this index,
/// - `Some(0)` represents fully accepting the file at this index,
/// - `Some(x)` represents resuming with the first `x` bytes skipped.
pub fn ask_receive(files: &[FileMeta]) -> std::io::Result<Vec<Option<u64>>> {
let mut new_files = Vec::<Option<u64>>::with_capacity(files.len());
let mut new_size = 0;
let mut total_size = 0;
Expand All @@ -31,8 +31,8 @@ pub fn confirm_receive(files: &[FileMeta]) -> std::io::Result<Vec<Option<u64>>>
print!("{} ({})", file.short_path.display(), HumanBytes(file.len));

// file was already downloaded
if file_exists(file)? {
print!(" {}", "ALREADY EXISTS".bold());
if file.already_exists()? {
print!(" {}", "ALREADY EXISTS".green().bold());
new_files.push(None);

// an interrupted download exists
Expand All @@ -41,9 +41,9 @@ pub fn confirm_receive(files: &[FileMeta]) -> std::io::Result<Vec<Option<u64>>>

print!(
" {} {} {}",
"PARTIALLY DOWNLOADED.".bold(),
HumanBytes(remaining_len).bold(),
"REMAINING".bold()
"PARTIALLY DOWNLOADED.".red().bold(),
HumanBytes(remaining_len).red().bold(),
"REMAINING".red().bold()
);
new_size += remaining_len;
new_files.push(Some(local_len));
Expand All @@ -60,18 +60,23 @@ pub fn confirm_receive(files: &[FileMeta]) -> std::io::Result<Vec<Option<u64>>>

println!();
println!(
"Total size of offered files: {}",
"Size of all offered files: {}",
HumanBytes(total_size).bold()
);
println!(
"Size of files that have a new/changed path or size or were interrupted: {}",
HumanBytes(new_size).bold()
);
println!("{}", "Options:".bold());
println!("1. Download only files with new path or size. Resume any interrupted downloads.");
println!("2. Fully download all files.");
println!("3. Cancel.");
println!("Note: Gday will create new files, instead of overwriting existing files.");
println!(
"{}",
"1. Download only files with new path or size. Resume any interrupted downloads.".bold()
);
println!("{}", "2. Fully download all files.".bold());
println!("{}", "3. Cancel.".bold());
println!(
"Note: gday won't overwrite existing files (it suffixes any new files with the same name)."
);
print!("{} ", "Choose an option (1, 2, or 3):".bold());
std::io::stdout().flush()?;

Expand All @@ -88,10 +93,10 @@ pub fn confirm_receive(files: &[FileMeta]) -> std::io::Result<Vec<Option<u64>>>
}
}

/// Finds all the files at the provided paths.
/// Asks the user to confirm they want to send them, otherwise exits.
/// Recursively finds all the files at the provided paths and
/// asks the user to confirm they want to send them, otherwise exits.
/// Returns the list of files these paths lead to.
pub fn confirm_send(paths: &[PathBuf]) -> std::io::Result<Vec<FileMetaLocal>> {
pub fn ask_send(paths: &[PathBuf]) -> std::io::Result<Vec<FileMetaLocal>> {
// get the file metadatas for all these paths
let files = get_paths_metadatas(paths)?;

Expand All @@ -108,25 +113,18 @@ pub fn confirm_send(paths: &[PathBuf]) -> std::io::Result<Vec<FileMetaLocal>> {
"Total size: ".bold(),
HumanBytes(total_size).bold()
);
print!("Would you like to send these? (y/n): ");
std::io::stdout().flush()?;

Ok(files)
}

/// Checks if there already exists a file with the same save path
/// and size as `meta`.
fn file_exists(meta: &FileMeta) -> std::io::Result<bool> {
let local_save_path = meta.get_save_path()?;

// check if the file can be opened
if let Ok(file) = File::open(local_save_path) {
// check if its length is the same
if let Ok(local_meta) = file.metadata() {
if local_meta.len() == meta.len {
return Ok(true);
}
}
// act on user choice
let mut response = String::new();
std::io::stdin().read_line(&mut response)?;
if "yes".starts_with(response.trim()) {
Ok(files)
} else {
println!("Cancelled.");
std::process::exit(0);
}
Ok(false)
}

/// Checks if there exists a file like `meta`
Expand Down
50 changes: 30 additions & 20 deletions gday/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
//! `gday` is a command line line tool for direct file transfer between peers.
//! TODO
//! `gday` is a command line line tool for peers to send each other files.
//! Features:
//! - Never uses relays. Instead, uses a gday_contact_exchange_server to share socket
//! addresses with peer, and then performs
//! [Hole Punching](https://en.wikipedia.org/wiki/TCP_hole_punching)
//! to establish a direct connection. Note that this may fail when one of the peers
//! is behind a strict [NAT](https://en.wikipedia.org/wiki/Network_address_translation).
#![forbid(unsafe_code)]
#![warn(clippy::all)]

Expand All @@ -9,9 +14,10 @@ mod transfer;

use crate::transfer::encrypt_connection;
use clap::{Parser, Subcommand};
use dialog::confirm_receive;
use gday_file_offer_protocol::{from_reader, FileResponseMsg};
use gday_file_offer_protocol::{to_writer, FileMeta, FileOfferMsg};
use dialog::ask_receive;
use gday_file_offer_protocol::{
from_reader, to_writer, FileMeta, FileMetaLocal, FileOfferMsg, FileResponseMsg,
};
use gday_hole_punch::{
server_connector::{self, DEFAULT_SERVERS},
ContactSharer,
Expand All @@ -33,15 +39,15 @@ struct Args {
operation: Command,

/// Use a custom gday server with this domain name.
#[arg(long)]
#[arg(short, long)]
server: Option<String>,

/// Use a custom port of the custom server.
#[arg(long, requires("server"))]
/// Which server port to connect to.
#[arg(short, long, requires("server"))]
port: Option<u16>,

/// Use unencrypted TCP instead of TLS to the custom server.
#[arg(long, requires("server"))]
#[arg(short, long, requires("server"))]
unencrypted: bool,

/// Verbosity. (trace, debug, info, warn, error)
Expand All @@ -53,18 +59,18 @@ struct Args {
enum Command {
/// Send files
Send {
/// Custom base-16 peer code of form "server_id.room_code.shared_secret".
///
/// Custom shared code of form "server_id.room_code.shared_secret" (base 16).
///
/// server_id must be valid, or 0 when custom --server set.
///
///
/// Doesn't require a checksum digit.
#[arg(long)]
#[arg(short, long)]
code: Option<String>,

paths: Vec<PathBuf>,
},

/// Receive files
/// Receive files. Input the code your peer told you.
Receive { code: String },
}

Expand Down Expand Up @@ -109,12 +115,12 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
// sending files
Command::Send { paths, code } => {
// confirm the user wants to send these files
let local_files = dialog::confirm_send(&paths)?;
let local_files = dialog::ask_send(&paths)?;

// generate random `room_code` and `shared_secret`
// if the user didn't provide custom ones
let peer_code = if let Some(code) = code {
PeerCode::from_str(&code, false)?
PeerCode::parse(&code, false)?
} else {
let room_code = rand::thread_rng().gen_range(0..u16::MAX as u64);
let shared_secret = rand::thread_rng().gen_range(0..u16::MAX as u64);
Expand Down Expand Up @@ -173,13 +179,15 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
if !response.accepted.iter().filter_map(|x| *x).all(|x| x == 0) {
println!("Resuming transfer of interrupted file(s).");
}
transfer::send_files(&mut stream, &local_files, &response.accepted)?;
let pairs: Vec<(FileMetaLocal, Option<u64>)> =
local_files.into_iter().zip(response.accepted).collect();
transfer::send_files(&mut stream, &pairs)?;
}
}

// receiving files
Command::Receive { code } => {
let code = PeerCode::from_str(&code, true)?;
let code = PeerCode::parse(&code, true)?;
let (contact_sharer, my_contact) =
ContactSharer::join_room(code.room_code, &mut server_connection)?;

Expand All @@ -205,7 +213,7 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
let offer: FileOfferMsg = from_reader(&mut stream)?;

// ask user which files to receive
let accepted = confirm_receive(&offer.files)?;
let accepted = ask_receive(&offer.files)?;

// respond to the file offer
to_writer(
Expand All @@ -218,7 +226,9 @@ fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
if accepted.iter().all(|x| x.is_none()) {
println!("No files will be downloaded.");
} else {
transfer::receive_files(&mut stream, &offer.files, &accepted)?;
let pairs: Vec<(FileMeta, Option<u64>)> =
offer.files.into_iter().zip(accepted).collect();
transfer::receive_files(&mut stream, &pairs)?;
}
}
}
Expand Down
Loading

0 comments on commit 67c252a

Please sign in to comment.