diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 146645a..860ac7e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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
diff --git a/Cargo.toml b/Cargo.toml
index bad44e4..0bcbca2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,3 +22,6 @@ version = "0.2.1"
[profile.dist]
inherits = "release"
lto = "thin"
+
+[profile.release]
+debug = true
\ No newline at end of file
diff --git a/README.md b/README.md
index 55fbb64..6b90130 100644
--- a/README.md
+++ b/README.md
@@ -4,53 +4,62 @@
Command line tool to securely send files (without a relay or port forwarding).
-peer_1: gday send image.jpg folder
-<Asks for confirmation>
-Tell your mate to run "gday get 1.1C30.C71E.A".
-Transfer complete.
+peer_1: gday send file.mp4 folder
+Tell your mate to run "gday get 1.n5xn8.wvqsf".
-peer_2: gday get 1.1C30.C71E.A
-<Asks for confirmation>
+peer_2: gday get 1.n5xn8.wvqsfTransfer complete.
-[![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 `).
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.
@@ -66,7 +75,7 @@ Commands:
Options:
-s, --server Use a custom gday server with this domain name
-p, --port Connect to a custom server port
- -u, --unencrypted Use raw TCP without TLS
+ -u, --unencrypted Use TCP without TLS
-v, --verbosity Verbosity. (trace, debug, info, warn, error) [default: warn]
-h, --help Print help
-V, --version Print version
diff --git a/gday/Cargo.toml b/gday/Cargo.toml
index 8416e9d..5fb8b7e 100644
--- a/gday/Cargo.toml
+++ b/gday/Cargo.toml
@@ -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"] }
diff --git a/gday/src/dialog.rs b/gday/src/dialog.rs
index 49b4f7c..1615f4a 100644
--- a/gday/src/dialog.rs
+++ b/gday/src/dialog.rs
@@ -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 {
+pub fn confirm_send(files: &FileOfferMsg) -> std::io::Result {
// print all the file names and sizes
println!("{}", "Files to send:".bold());
for file in &files.files {
@@ -25,13 +27,12 @@ pub async fn confirm_send(files: &FileOfferMsg) -> std::io::Result {
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)
}
}
@@ -39,7 +40,7 @@ pub async fn confirm_send(files: &FileOfferMsg) -> std::io::Result {
/// 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 {
@@ -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!(
@@ -62,7 +63,7 @@ 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!();
@@ -70,8 +71,9 @@ pub async fn ask_receive(
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.
@@ -79,16 +81,15 @@ pub async fn ask_receive(
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);
}
}
@@ -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
@@ -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 {
- let Some(response) = BufReader::new(tokio::io::stdin())
- .lines()
- .next_line()
- .await?
- else {
+fn get_lowercase_input() -> std::io::Result {
+ 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)
}
diff --git a/gday/src/main.rs b/gday/src/main.rs
index 4dc73f7..06ab06e 100644
--- a/gday/src/main.rs
+++ b/gday/src/main.rs
@@ -56,8 +56,8 @@ enum Command {
#[arg(short, long, conflicts_with = "length")]
code: Option,
- /// 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.
@@ -124,8 +124,8 @@ async fn run(args: crate::Args) -> Result<(), Box> {
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 {
@@ -158,12 +158,12 @@ async fn run(args: crate::Args) -> Result<(), Box> {
};
// 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(());
}
@@ -204,7 +204,7 @@ async fn run(args: crate::Args) -> Result<(), Box> {
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
@@ -226,12 +226,15 @@ async fn run(args: crate::Args) -> Result<(), Box> {
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,
@@ -268,7 +271,7 @@ async fn run(args: crate::Args) -> Result<(), Box> {
// 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?;
@@ -278,6 +281,9 @@ async fn run(args: crate::Args) -> Result<(), Box> {
} else {
transfer::receive_files(offer, response, &path, &mut stream).await?;
}
+
+ // Gracefully close the server connection
+ server_connection.shutdown().await?;
}
}
diff --git a/gday/src/transfer.rs b/gday/src/transfer.rs
index b1030ff..fdbc558 100644
--- a/gday/src/transfer.rs
+++ b/gday/src/transfer.rs
@@ -8,15 +8,16 @@ pub async fn send_files(
response: FileResponseMsg,
writer: &mut EncryptedStream,
) -> Result<(), Box> {
- let progress_bar = create_progress_bar();
+ let len = FileOfferMsg::from(offer.clone()).get_transfer_size(&response)?;
+ let progress_bar = create_progress_bar(len);
let mut current_file = String::from("Starting...");
let update_progress = |report: &TransferReport| {
progress_bar.set_position(report.processed_bytes);
- progress_bar.set_length(report.total_bytes);
if current_file.as_str() != report.current_file.to_string_lossy() {
- current_file = report.current_file.to_string_lossy().to_string();
- progress_bar.set_message(format!("Receiving {}", current_file));
+ current_file.clear();
+ current_file.push_str(&report.current_file.to_string_lossy());
+ progress_bar.set_message(format!("Sending {}", current_file));
}
};
@@ -26,7 +27,7 @@ pub async fn send_files(
Ok(())
}
Err(err) => {
- progress_bar.abandon_with_message("Transfer failed.");
+ progress_bar.abandon_with_message("Send failed.");
Err(err.into())
}
}
@@ -42,14 +43,15 @@ pub async fn receive_files(
save_dir: &std::path::Path,
reader: &mut EncryptedStream,
) -> Result<(), Box> {
- let progress_bar = create_progress_bar();
- let mut current_file = String::from("Starting...");
+ let len = offer.get_transfer_size(&response)?;
+ let progress_bar = create_progress_bar(len);
+ let mut current_file = String::new();
let update_progress = |report: &TransferReport| {
progress_bar.set_position(report.processed_bytes);
- progress_bar.set_length(report.total_bytes);
if current_file.as_str() != report.current_file.to_string_lossy() {
- current_file = report.current_file.to_string_lossy().to_string();
+ current_file.clear();
+ current_file.push_str(&report.current_file.to_string_lossy());
progress_bar.set_message(format!("Receiving {}", current_file));
}
};
@@ -64,20 +66,20 @@ pub async fn receive_files(
Ok(())
}
Err(err) => {
- progress_bar.abandon_with_message("Transfer failed.");
+ progress_bar.abandon_with_message("Receive failed.");
Err(err.into())
}
}
}
/// Create a stylded [`ProgressBar`].
-fn create_progress_bar() -> ProgressBar {
+fn create_progress_bar(len: u64) -> ProgressBar {
let style = ProgressStyle::with_template(
"{msg} [{wide_bar}] {bytes}/{total_bytes} | {bytes_per_sec} | eta: {eta}",
)
.expect("Progress bar style string was invalid.");
let draw = ProgressDrawTarget::stderr_with_hz(2);
- ProgressBar::with_draw_target(None, draw)
+ ProgressBar::with_draw_target(Some(len), draw)
.with_style(style)
.with_message("starting...")
}
diff --git a/gday_contact_exchange_protocol/src/lib.rs b/gday_contact_exchange_protocol/src/lib.rs
index e544153..4bd5f30 100644
--- a/gday_contact_exchange_protocol/src/lib.rs
+++ b/gday_contact_exchange_protocol/src/lib.rs
@@ -207,7 +207,7 @@ impl Display for ServerMsg {
Self::PeerContact(c) => write!(f, "The server says your peer's contact is {c}."),
Self::ErrorRoomTaken => write!(
f,
- "Can't create room with this code, because it's already taken."
+ "Can't create a room with this room code, because it's already taken."
),
Self::ErrorPeerTimedOut => write!(
f,
diff --git a/gday_encryption/Cargo.toml b/gday_encryption/Cargo.toml
index 7d84eee..a64de73 100644
--- a/gday_encryption/Cargo.toml
+++ b/gday_encryption/Cargo.toml
@@ -20,7 +20,7 @@ rand = "0.8.5"
tokio = { version = "1.41.1", features = ["io-util"] }
[dev-dependencies]
-criterion = { version = "0.5.1", features = ["async_tokio", "tokio"] }
+criterion = { version = "0.5.1", features = ["async_tokio"] }
tokio = { version = "1.41.1", features = ["net", "rt", "macros"] }
[[bench]]
diff --git a/gday_encryption/src/helper_buf.rs b/gday_encryption/src/helper_buf.rs
index ba06185..a304058 100644
--- a/gday_encryption/src/helper_buf.rs
+++ b/gday_encryption/src/helper_buf.rs
@@ -53,9 +53,11 @@ impl HelperBuf {
/// Shifts the stored data to the beginning of the internal buffer.
/// Maximizes `spare_capacity_len()` without changing anything else.
pub fn left_align(&mut self) {
- self.inner.copy_within(self.l_cursor..self.r_cursor, 0);
- self.r_cursor -= self.l_cursor;
- self.l_cursor = 0;
+ if self.l_cursor != 0 {
+ self.inner.copy_within(self.l_cursor..self.r_cursor, 0);
+ self.r_cursor -= self.l_cursor;
+ self.l_cursor = 0;
+ }
}
/// Returns a mutable [`aead::Buffer`] view into the part of this
diff --git a/gday_encryption/src/lib.rs b/gday_encryption/src/lib.rs
index 5d3c1e6..d684507 100644
--- a/gday_encryption/src/lib.rs
+++ b/gday_encryption/src/lib.rs
@@ -99,7 +99,8 @@ pub struct EncryptedStream {
/// Data to be sent. Encrypted only when [`Self::flushing`].
/// - Invariant: the first 2 bytes are always
- /// reserved for the length header
+ /// reserved for the length
+ /// - Invariant: Data can only be appended when `flushing` is false.
to_send: HelperBuf,
/// Is the content of `to_send` encrypted and ready to write?
@@ -113,7 +114,7 @@ impl EncryptedStream {
/// - The `key` must be a cryptographically random secret.
/// - The `nonce` shouldn't be reused, but doesn't need to be secret.
///
- /// - See [`Self::encrypt_connection()`] if you'd like an auto-generated nonce.
+ /// - See [`Self::encrypt_connection()`] if you'd like an auto-generatcan't createed nonce.
pub fn new(io_stream: T, key: &[u8; 32], nonce: &[u8; 7]) -> Self {
let mut to_send = HelperBuf::with_capacity(u16::MAX as usize + 2);
// add 2 bytes for length header to uphold invariant
@@ -170,7 +171,7 @@ impl AsyncRead for EncryptedStream {
buf: &mut tokio::io::ReadBuf<'_>,
) -> Poll> {
// if we're out of decrypted data, read more
- if self.as_mut().decrypted.is_empty() {
+ if self.decrypted.is_empty() {
ready!(self.as_mut().inner_read(cx))?;
}
@@ -183,7 +184,7 @@ impl AsyncRead for EncryptedStream {
}
}
-impl AsyncBufRead for EncryptedStream {
+impl AsyncBufRead for EncryptedStream {
fn consume(self: std::pin::Pin<&mut EncryptedStream>, amt: usize) {
self.project().decrypted.consume(amt);
}
@@ -219,9 +220,10 @@ impl AsyncWrite for EncryptedStream {
.extend_from_slice(&buf[0..bytes_taken])
.expect("unreachable");
- // if `to_send` is full, flush it
+ // if `to_send` is full, start the process
+ // of flushing it
if me.to_send.spare_capacity().len() - TAG_SIZE == 0 {
- ready!(self.flush_write_buf(cx))?;
+ let _ = self.flush_write_buf(cx)?;
}
Poll::Ready(Ok(bytes_taken))
}
@@ -310,6 +312,7 @@ impl EncryptedStream {
// If we're just starting a flush,
// encrypt the data.
if !*me.flushing {
+ *me.flushing = true;
// encrypt in place
let mut msg = me.to_send.split_off_aead_buf(2);
me.encryptor
diff --git a/gday_encryption/tests/test_integration.rs b/gday_encryption/tests/test_integration.rs
index 0590160..1719bdf 100644
--- a/gday_encryption/tests/test_integration.rs
+++ b/gday_encryption/tests/test_integration.rs
@@ -38,6 +38,9 @@ async fn test_transfers() {
stream_a.write_all(chunk).await.unwrap();
stream_a.flush().await.unwrap();
}
+ // Ensure calling shutdown multiple times works
+ stream_a.shutdown().await.unwrap();
+ stream_a.shutdown().await.unwrap();
});
// Stream that will receive the test data sent to the loopback address.
@@ -52,6 +55,10 @@ async fn test_transfers() {
stream_b.read_exact(&mut received).await.unwrap();
assert_eq!(*chunk, received);
}
+
+ // EOF should return 0
+ assert_eq!(stream_b.read(&mut [0, 0, 0]).await.unwrap(), 0);
+ assert_eq!(stream_b.read(&mut [0, 0, 0]).await.unwrap(), 0);
}
/// Test bufread
@@ -88,6 +95,9 @@ async fn test_bufread() {
stream_a.write_all(chunk).await.unwrap();
stream_a.flush().await.unwrap();
}
+
+ stream_a.shutdown().await.unwrap();
+ stream_a.shutdown().await.unwrap();
});
// Stream that will receive the test data sent to the loopback address.
@@ -105,6 +115,10 @@ async fn test_bufread() {
assert_ne!(bytes_read, 0);
}
assert_eq!(received, bytes);
+
+ // EOF should return 0
+ assert_eq!(stream_b.read(&mut [0, 0, 0]).await.unwrap(), 0);
+ assert_eq!(stream_b.read(&mut [0, 0, 0]).await.unwrap(), 0);
}
/// Confirm there are no infinite loops on EOF
diff --git a/gday_file_transfer/Cargo.toml b/gday_file_transfer/Cargo.toml
index d580524..f9152c8 100644
--- a/gday_file_transfer/Cargo.toml
+++ b/gday_file_transfer/Cargo.toml
@@ -14,14 +14,13 @@ version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-futures = "0.3.31"
os_str_bytes = "7.0.0"
pin-project = "1.1.7"
-rand = "0.8.5"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
thiserror = "2.0.3"
-tokio = { version = "1.41.1", features = ["fs", "net", "io-util", "rt", "macros"] }
+tokio = { version = "1.41.1", features = ["io-util"] }
[dev-dependencies]
tempfile = "3.14.0"
+tokio = { version = "1.41.1", features = ["macros"] }
diff --git a/gday_file_transfer/src/file_meta.rs b/gday_file_transfer/src/file_meta.rs
index ea6fa6b..bb7fe58 100644
--- a/gday_file_transfer/src/file_meta.rs
+++ b/gday_file_transfer/src/file_meta.rs
@@ -1,5 +1,4 @@
use crate::Error;
-use futures::{future::BoxFuture, FutureExt};
use os_str_bytes::OsStrBytesExt;
use serde::{Deserialize, Serialize};
use std::{
@@ -48,9 +47,9 @@ impl FileMeta {
///
/// If all of these (up to `" (99)"`) are occupied,
/// returns [`Error::FilenameOccupied`].
- pub async fn get_unoccupied_save_path(&self, save_dir: &Path) -> Result {
+ pub fn get_unoccupied_save_path(&self, save_dir: &Path) -> Result {
let mut path = self.get_save_path(save_dir);
- let number = get_first_unoccupied_number(&path).await?;
+ let number = get_first_unoccupied_number(&path)?;
if number != 0 {
suffix_with_number(&mut path, number);
@@ -68,12 +67,9 @@ impl FileMeta {
/// will be one less than that of
/// [`Self::get_unoccupied_save_path()`] (or no suffix
/// if [`Self::get_unoccupied_save_path()`] has suffix of 1).
- pub async fn get_last_occupied_save_path(
- &self,
- save_dir: &Path,
- ) -> Result