Skip to content

Commit

Permalink
Made file ops sync to reduce thread spawning overhead.
Browse files Browse the repository at this point in the history
  • Loading branch information
manforowicz committed Nov 30, 2024
1 parent 5cd784e commit f9e6837
Show file tree
Hide file tree
Showing 27 changed files with 361 additions and 245 deletions.
9 changes: 4 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# Contributing

I'm eager to hear all your feedback and suggestions!
Just open a [GitHub issue](https://github.com/manforowicz/gday/issues)
and include as many details as you can.
For example, try running with `--verbosity debug` or `--verbosity trace`
and paste the log into your issue.
Open a [GitHub issue](https://github.com/manforowicz/gday/issues)
to report issues and suggest features.
Try running with `--verbosity debug` or `--verbosity trace`
and pasting the log into your issue.

## Contributing code

Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ version = "0.2.1"
[profile.dist]
inherits = "release"
lto = "thin"

[profile.release]
debug = true
47 changes: 28 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,62 @@
Command line tool to securely send files (without a relay or port forwarding).

<pre>
<b style="color:lime;">peer_1:</b> gday send image.jpg folder
<i>&lt;Asks for confirmation&gt;</i>
Tell your mate to run "gday get <b>1.1C30.C71E.A</b>".
<b>Transfer complete.</b>
<b style="color:lime;">peer_1:</b> gday send file.mp4 folder
Tell your mate to run "gday get <b>1.n5xn8.wvqsf</b>".
</pre>

<pre>
<b style="color:lime;">peer_2:</b> gday get <b>1.1C30.C71E.A</b>
<i>&lt;Asks for confirmation&gt;</i>
<b style="color:lime;">peer_2:</b> gday get <b>1.n5xn8.wvqsf</b>
<b>Transfer complete.</b>
</pre>

[![asciicast](https://asciinema.org/a/1jjPVyccHweqgwA5V3un4tCnU.svg)](https://asciinema.org/a/1jjPVyccHweqgwA5V3un4tCnU)
[![asciicast](https://asciinema.org/a/Z8OJJr8xHRAJh6fuqocNcm9Zu.svg)](https://asciinema.org/a/Z8OJJr8xHRAJh6fuqocNcm9Zu)

## Installation

To run the executable directly:

1. Go to [releases](https://github.com/manforowicz/gday/releases)
and download the correct file for your platform.
1. Download an executable from [releases](https://github.com/manforowicz/gday/releases).
2. Extract it (on Linux: `tar xf <file>`).
3. Run it: `./gday`

To install with **cargo**:
```
$ cargo install gday
cargo install gday
```

To install with **brew**:
```
$ brew install manforowicz/tap/gday
brew install manforowicz/tap/gday
```

## Features
- File transfer is always direct, without relay servers.
A server is only used to exchange socket addresses at the beginning.

- No limit on the size of files and folders sent.

- Files are sent directly, without relay servers.
A server is only used to exchange socket addresses at the beginning.

- Automatically resumes interrupted transfers. Just `gday send` the same files, and partial downloads will be detected and resumed.

- Doesn't require port forwarding.
Instead, uses [TCP Hole Punching](https://bford.info/pub/net/p2pnat/) to traverse
[NATs](https://en.wikipedia.org/wiki/Network_address_translation).
Note: this may not work on very restrictive NATs.
- Server connection encrypted with [TLS](https://docs.rs/rustls/)
and file transfer encrypted with [ChaCha20Poly1305](https://docs.rs/chacha20poly1305/).
This may not work on very restrictive NATs. If that happens, enable IPv6 or move to a different network.

- If a contact exchange server is down, just uses a different one from the default list. Or specify your own with `--server`.

- Server connection encrypted with
[TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security)
and file transfer end-to-end encrypted with
[ChaCha20Poly1305](https://en.wikipedia.org/wiki/ChaCha20-Poly1305).

- Automatically tries both IPv4 and IPv6.

- Immune to malicious servers impersonating your peer.
Uses [SPAKE2](https://docs.rs/spake2/) password authenticated key exchange
to derive an encryption key from a shared secret.
Uses [SPAKE2](https://datatracker.ietf.org/doc/rfc9382/) to derive an
encryption key from a shared secret.

- No `unsafe` Rust in this repository.


Expand All @@ -66,7 +75,7 @@ Commands:
Options:
-s, --server <SERVER> Use a custom gday server with this domain name
-p, --port <PORT> Connect to a custom server port
-u, --unencrypted Use raw TCP without TLS
-u, --unencrypted Use TCP without TLS
-v, --verbosity <VERBOSITY> Verbosity. (trace, debug, info, warn, error) [default: warn]
-h, --help Print help
-V, --version Print version
Expand Down
2 changes: 1 addition & 1 deletion gday/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ gday_hole_punch = { version = "^0.2.1", path = "../gday_hole_punch" }
indicatif = "0.17.9"
log = "0.4.22"
owo-colors = "4.1.0"
tokio = { version = "1.41.1", features = ["io-std", "rt-multi-thread"] }
tokio = { version = "1.41.1", features = ["rt-multi-thread", "macros"] }
67 changes: 40 additions & 27 deletions gday/src/dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
use gday_file_transfer::{FileOfferMsg, FileResponseMsg};
use indicatif::HumanBytes;
use owo_colors::OwoColorize;
use std::{io::Write, path::Path};
use tokio::io::{AsyncBufReadExt, BufReader};
use std::{
io::{BufRead, Write},
path::Path,
};

/// Confirms that the user wants to send these `files``.
///
/// If not, returns false.
pub async fn confirm_send(files: &FileOfferMsg) -> std::io::Result<bool> {
pub fn confirm_send(files: &FileOfferMsg) -> std::io::Result<bool> {
// print all the file names and sizes
println!("{}", "Files to send:".bold());
for file in &files.files {
Expand All @@ -25,21 +27,20 @@ pub async fn confirm_send(files: &FileOfferMsg) -> std::io::Result<bool> {
HumanBytes(total_size).bold()
);
std::io::stdout().flush()?;
let input = get_lowercase_input().await?;
let input = get_lowercase_input()?;

// act on user choice
if "yes".starts_with(&input) {
Ok(true)
} else {
println!("Cancelled.");
Ok(false)
}
}

/// Asks the user which of the files in `offer` to accept.
///
/// `save_dir` is the directory where the files will later be saved.
pub async fn ask_receive(
pub fn ask_receive(
offer: &FileOfferMsg,
save_dir: &Path,
) -> Result<FileResponseMsg, gday_file_transfer::Error> {
Expand All @@ -51,7 +52,7 @@ pub async fn ask_receive(
print!("{} ({})", file.short_path.display(), HumanBytes(file.len));

// an interrupted download exists
if let Some(local_len) = file.partial_download_exists(save_dir).await? {
if let Some(local_len) = file.partial_download_exists(save_dir)? {
let remaining_len = file.len - local_len;

print!(
Expand All @@ -62,33 +63,33 @@ pub async fn ask_receive(
);

// file was already downloaded
} else if file.already_exists(save_dir).await? {
} else if file.already_exists(save_dir)? {
print!(" {}", "ALREADY EXISTS".green().bold());
}
println!();
}

println!();

let new_files = FileResponseMsg::accept_only_new_and_interrupted(offer, save_dir).await?;
let new_files = FileResponseMsg::accept_only_new_and_interrupted(offer, save_dir)?;
let all_files = FileResponseMsg::accept_all_files(offer);
let no_files = FileResponseMsg::reject_all_files(offer);

// If there are no existing/interrupted files,
// send or quit.
if new_files == all_files {
print!(
"Download all {} files ({})? (y/n): ",
all_files.get_num_fully_accepted(),
HumanBytes(offer.get_transfer_size(&new_files)?).bold()
HumanBytes(offer.get_transfer_size(&all_files)?).bold()
);
std::io::stdout().flush()?;
let input = get_lowercase_input().await?;
let input = get_lowercase_input()?;

if "yes".starts_with(&input) {
return Ok(all_files);
} else {
println!("Cancelled.");
std::process::exit(0);
return Ok(no_files);
}
}

Expand All @@ -97,17 +98,33 @@ pub async fn ask_receive(
all_files.response.len(),
HumanBytes(offer.get_transfer_size(&all_files)?).bold()
);
println!(
"2. Download only the {} new files, and resume {} interrupted downloads ({}).",
new_files.get_num_fully_accepted(),
new_files.get_num_partially_accepted(),
HumanBytes(offer.get_transfer_size(&new_files)?).bold()
);

if new_files.get_num_partially_accepted() == 0 {
println!(
"2. Only download the {} new files ({}).",
new_files.get_num_fully_accepted(),
HumanBytes(offer.get_transfer_size(&new_files)?).bold()
);
} else if new_files.get_num_fully_accepted() == 0 {
println!(
"2. Only resume the {} interrupted downloads ({}).",
new_files.get_num_partially_accepted(),
HumanBytes(offer.get_transfer_size(&new_files)?).bold()
);
} else {
println!(
"2. Only download the {} new files, and resume {} interrupted downloads ({}).",
new_files.get_num_fully_accepted(),
new_files.get_num_partially_accepted(),
HumanBytes(offer.get_transfer_size(&new_files)?).bold()
);
}

println!("3. Cancel.");
print!("{} ", "Choose an option (1, 2, or 3):".bold());
std::io::stdout().flush()?;

match get_lowercase_input().await?.as_str() {
match get_lowercase_input()?.as_str() {
// all files
"1" => Ok(all_files),
// new/interrupted files
Expand All @@ -118,18 +135,14 @@ pub async fn ask_receive(
}

/// Reads a trimmed ascii-lowercase line of input from the user.
async fn get_lowercase_input() -> std::io::Result<String> {
let Some(response) = BufReader::new(tokio::io::stdin())
.lines()
.next_line()
.await?
else {
fn get_lowercase_input() -> std::io::Result<String> {
let Some(response) = std::io::BufReader::new(std::io::stdin()).lines().next() else {
return Err(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"Couldn't read user input.",
));
};

let response = response.trim().to_ascii_lowercase();
let response = response?.trim().to_ascii_lowercase();
Ok(response)
}
28 changes: 17 additions & 11 deletions gday/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ enum Command {
#[arg(short, long, conflicts_with = "length")]
code: Option<PeerCode>,

/// Length of the last 2 sections of the randomly-generated shareable code.
#[arg(short, long, default_value = "4", conflicts_with = "code")]
/// Length of room_code and shared_secret to generate.
#[arg(short, long, default_value = "5", conflicts_with = "code")]
length: usize,

/// Files and/or directories to send.
Expand Down Expand Up @@ -124,8 +124,8 @@ async fn run(args: crate::Args) -> Result<(), Box<dyn std::error::Error>> {
length,
} => {
// If the user chose a custom server
let (mut server_connection, server_id) = if let Some(forced_server) = custom_server {
(forced_server, 0)
let (mut server_connection, server_id) = if let Some(custom_server) = custom_server {
(custom_server, 0)

// If the user chose a custom code
} else if let Some(code) = &code {
Expand Down Expand Up @@ -158,12 +158,12 @@ async fn run(args: crate::Args) -> Result<(), Box<dyn std::error::Error>> {
};

// get metadata about the files to transfer
let local_files = gday_file_transfer::get_file_metas(&paths).await?;
let local_files = gday_file_transfer::get_file_metas(&paths)?;
let offer_msg = FileOfferMsg::from(local_files.clone());

// confirm the user wants to send these files
if !dialog::confirm_send(&offer_msg).await? {
// Send aborted
if !dialog::confirm_send(&offer_msg)? {
println!("Cancelled.");
return Ok(());
}

Expand Down Expand Up @@ -204,7 +204,7 @@ async fn run(args: crate::Args) -> Result<(), Box<dyn std::error::Error>> {

println!("File offer sent to mate. Waiting on response.");

// receive file offer from peer
// receive response from peer
let response: FileResponseMsg = read_from_async(&mut stream).await?;

// Total number of files accepted
Expand All @@ -226,12 +226,15 @@ async fn run(args: crate::Args) -> Result<(), Box<dyn std::error::Error>> {
if num_accepted != 0 {
transfer::send_files(local_files, response, &mut stream).await?;
}

// Gracefully close the server connection
server_connection.shutdown().await?;
}

// receiving files
crate::Command::Get { path, code } => {
let mut server_connection = if let Some(forced_server) = custom_server {
forced_server
let mut server_connection = if let Some(custom_server) = custom_server {
custom_server
} else {
server_connector::connect_to_server_id(
DEFAULT_SERVERS,
Expand Down Expand Up @@ -268,7 +271,7 @@ async fn run(args: crate::Args) -> Result<(), Box<dyn std::error::Error>> {
// receive file offer from peer
let offer: FileOfferMsg = read_from_async(&mut stream).await?;

let response = ask_receive(&offer, &path).await?;
let response = ask_receive(&offer, &path)?;

// respond to the file offer
write_to_async(&response, &mut stream).await?;
Expand All @@ -278,6 +281,9 @@ async fn run(args: crate::Args) -> Result<(), Box<dyn std::error::Error>> {
} else {
transfer::receive_files(offer, response, &path, &mut stream).await?;
}

// Gracefully close the server connection
server_connection.shutdown().await?;
}
}

Expand Down
Loading

0 comments on commit f9e6837

Please sign in to comment.