Skip to content

Commit 3e4052f

Browse files
authored
feat: support multiples files (#28)
1 parent 19b7a10 commit 3e4052f

File tree

6 files changed

+186
-38
lines changed

6 files changed

+186
-38
lines changed

crates/bit_rev/src/file.rs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use serde_bencode::de;
44
use serde_bencode::ser;
55
use serde_bytes::ByteBuf;
66
use std::fmt::Write;
7-
use std::{error::Error, io::Read};
7+
use std::io::Read;
8+
9+
use anyhow::Result;
810

911
#[derive(Debug, Serialize, Deserialize, Clone)]
1012
pub struct Node(String, i64);
@@ -95,15 +97,15 @@ impl TorrentMeta {
9597
}
9698
}
9799

98-
pub fn from_filename(filename: &str) -> Result<TorrentMeta, Box<dyn Error>> {
100+
pub fn from_filename(filename: &str) -> Result<TorrentMeta> {
99101
let mut file = std::fs::File::open(filename)?;
100102
let mut content = Vec::new();
101103
file.read_to_end(&mut content)?;
102104
let torrent = de::from_bytes::<TorrentFile>(&content)?;
103105
Ok(TorrentMeta::new(torrent))
104106
}
105107

106-
pub fn url_encode_bytes(content: &[u8]) -> Result<String, Box<dyn Error>> {
108+
pub fn url_encode_bytes(content: &[u8]) -> Result<String> {
107109
let mut out: String = String::new();
108110

109111
for byte in content.iter() {
@@ -121,21 +123,27 @@ pub fn build_tracker_url(
121123
peer_id: &[u8],
122124
port: u16,
123125
tracker_url: &str,
124-
) -> String {
126+
) -> Result<String> {
125127
// let announce_url = torrent_meta.torrent_file.announce.as_ref().unwrap();
126-
let info_hash_encoded = url_encode_bytes(torrent_meta.info_hash.as_ref()).unwrap();
127-
let peer_id_encoded = url_encode_bytes(peer_id).unwrap();
128+
let info_hash_encoded = url_encode_bytes(torrent_meta.info_hash.as_ref())?;
129+
let peer_id_encoded = url_encode_bytes(peer_id)?;
128130
// let info_hash_encoded = urlencoding::encode_binary(&torrent_meta.info_hash);
129131
// let peer_id_encoded = urlencoding::encode_binary(&peer_id);
130132

131-
format!(
133+
let total_length = if let Some(length) = torrent_meta.torrent_file.info.length {
134+
length
135+
} else if let Some(files) = &torrent_meta.torrent_file.info.files {
136+
files.iter().map(|f| f.length).sum()
137+
} else {
138+
return Err(anyhow::anyhow!(
139+
"Invalid torrent file: missing length information"
140+
));
141+
};
142+
143+
Ok(format!(
132144
// "{}?info_hash={}&peer_id={}&port={}&uploaded=0&downloaded=0&compact=1&left={}&event=started?supportcrypto=1&numwant=80&key=DF45C574",
133145
"{}?info_hash={}&peer_id={}&port={}&uploaded=0&downloaded=0&compact=1&left={}",
134-
tracker_url,
135-
info_hash_encoded,
136-
peer_id_encoded,
137-
port,
138-
torrent_meta.torrent_file.info.length.as_ref().unwrap()
146+
tracker_url, info_hash_encoded, peer_id_encoded, port, total_length
139147
)
140-
.to_string()
148+
.to_string())
141149
}

crates/bit_rev/src/session.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ impl Session {
7777
&self,
7878
add_torrent: AddTorrentOptions,
7979
) -> anyhow::Result<AddTorrentResult> {
80-
let torrent = Torrent::new(&add_torrent.torrent_meta.clone());
80+
let torrent = Torrent::new(&add_torrent.torrent_meta.clone())?;
8181
let torrent_meta = add_torrent.torrent_meta.clone();
8282
let (pr_tx, pr_rx) = flume::bounded::<PieceResult>(torrent.piece_hashes.len());
8383
let have_broadcast = Arc::new(tokio::sync::broadcast::channel(128).0);

crates/bit_rev/src/torrent.rs

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,65 @@
11
use crate::file::TorrentMeta;
22

3+
use anyhow::Result;
4+
35
#[derive(Debug, Clone, PartialEq)]
46
pub struct Torrent {
57
pub info_hash: [u8; 20],
68
pub piece_hashes: Vec<[u8; 20]>,
79
pub piece_length: i64,
810
pub length: i64,
11+
pub files: Vec<TorrentFileInfo>,
12+
pub name: String,
13+
}
14+
15+
#[derive(Debug, Clone, PartialEq)]
16+
pub struct TorrentFileInfo {
17+
pub path: Vec<String>,
18+
pub length: i64,
19+
pub offset: i64,
920
}
1021

1122
impl Torrent {
12-
pub fn new(torrent_meta: &TorrentMeta) -> Torrent {
13-
Torrent {
23+
pub fn new(torrent_meta: &TorrentMeta) -> Result<Torrent> {
24+
let info = &torrent_meta.torrent_file.info;
25+
26+
let (total_length, files) = if let Some(file_list) = &info.files {
27+
// Multi-file torrent
28+
let mut total = 0i64;
29+
let mut torrent_files = Vec::new();
30+
31+
for file in file_list {
32+
torrent_files.push(TorrentFileInfo {
33+
path: file.path.clone(),
34+
length: file.length,
35+
offset: total,
36+
});
37+
total += file.length;
38+
}
39+
40+
(total, torrent_files)
41+
} else if let Some(length) = info.length {
42+
// Single-file torrent
43+
let single_file = TorrentFileInfo {
44+
path: vec![info.name.clone()],
45+
length,
46+
offset: 0,
47+
};
48+
49+
(length, vec![single_file])
50+
} else {
51+
return Err(anyhow::anyhow!(
52+
"Invalid torrent file: missing length information"
53+
));
54+
};
55+
56+
Ok(Torrent {
1457
info_hash: torrent_meta.info_hash,
1558
piece_hashes: torrent_meta.piece_hashes.clone(),
16-
piece_length: torrent_meta.torrent_file.info.piece_length,
17-
length: torrent_meta.torrent_file.info.length.unwrap(),
18-
}
59+
piece_length: info.piece_length,
60+
length: total_length,
61+
files,
62+
name: info.name.clone(),
63+
})
1964
}
2065
}

crates/bit_rev/src/tracker_peers.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,12 @@ impl TrackerPeers {
9292
let have_broadcast = have_broadcast.clone();
9393
let torrent_downloaded_state = torrent_downloaded_state.clone();
9494
tokio::spawn(async move {
95-
let url = file::build_tracker_url(&torrent_meta, &peer_id, 6881, &tracker);
95+
let url = file::build_tracker_url(&torrent_meta, &peer_id, 6881, &tracker)
96+
.map_err(|e| {
97+
error!("Failed to build tracker URL for {}: {}", tracker, e);
98+
e
99+
})
100+
.unwrap();
96101

97102
match request_peers(&url).await {
98103
Ok(request_peers_res) => {

crates/bit_rev/src/utils.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::torrent::Torrent;
1+
use crate::torrent::{Torrent, TorrentFileInfo};
22
use rand::Rng;
33

44
const BLOCK_SIZE: u32 = 16384;
@@ -42,3 +42,44 @@ pub fn generate_peer_id() -> [u8; 20] {
4242
.try_into()
4343
.unwrap()
4444
}
45+
46+
#[derive(Debug, Clone)]
47+
pub struct PieceFileMapping {
48+
pub file_index: usize,
49+
pub file_offset: usize,
50+
pub length: usize,
51+
}
52+
53+
pub fn map_piece_to_files(torrent: &Torrent, piece_index: usize) -> Vec<PieceFileMapping> {
54+
let (piece_start, piece_end) = calculate_bounds_for_piece(torrent, piece_index);
55+
let mut mappings = Vec::new();
56+
57+
for (file_index, file) in torrent.files.iter().enumerate() {
58+
let file_start = file.offset as usize;
59+
let file_end = file_start + file.length as usize;
60+
61+
// Check if piece overlaps with this file
62+
if piece_start < file_end && piece_end > file_start {
63+
let overlap_start = piece_start.max(file_start);
64+
let overlap_end = piece_end.min(file_end);
65+
let file_offset = overlap_start - file_start;
66+
let length = overlap_end - overlap_start;
67+
68+
mappings.push(PieceFileMapping {
69+
file_index,
70+
file_offset,
71+
length,
72+
});
73+
}
74+
}
75+
76+
mappings
77+
}
78+
79+
pub fn get_full_file_path(torrent: &Torrent, file_info: &TorrentFileInfo) -> std::path::PathBuf {
80+
let mut path = std::path::PathBuf::from(&torrent.name);
81+
for component in &file_info.path {
82+
path.push(component);
83+
}
84+
path
85+
}

crates/cli/src/main.rs

Lines changed: 66 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
22
use std::{
3+
collections::HashMap,
34
fmt::Write,
45
io::SeekFrom,
56
sync::{atomic::AtomicU64, Arc},
67
};
78
use tokio::{
8-
fs::File,
9+
fs::{create_dir_all, File},
910
io::{AsyncSeekExt, AsyncWriteExt},
1011
};
1112
use tracing::trace;
@@ -33,7 +34,7 @@ pub async fn download_file(filename: &str, out_file: Option<String>) -> anyhow::
3334

3435
let add_torrent_result = session.add_torrent(filename.into()).await?;
3536
let torrent = add_torrent_result.torrent.clone();
36-
let torrent_meta = add_torrent_result.torrent_meta;
37+
let _torrent_meta = add_torrent_result.torrent_meta;
3738

3839
let total_size = torrent.length as u64;
3940
let pb = ProgressBar::new(total_size);
@@ -47,13 +48,41 @@ pub async fn download_file(filename: &str, out_file: Option<String>) -> anyhow::
4748
).progress_chars("#>-")
4849
);
4950

50-
let out_filename = match out_file {
51+
// Determine the output directory
52+
let output_dir = match out_file {
5153
Some(name) => name,
52-
None => torrent_meta.clone().torrent_file.info.name.clone(),
54+
None => torrent.name.clone(),
5355
};
54-
let mut file = File::create(out_filename).await?;
5556

56-
// File
57+
// Create output directory and prepare file handles
58+
let mut file_handles: HashMap<usize, File> = HashMap::new();
59+
60+
// Create directories and prepare files for multi-file torrents
61+
for (file_index, file_info) in torrent.files.iter().enumerate() {
62+
let file_path = if torrent.files.len() == 1 {
63+
// Single file torrent - use output_dir as filename
64+
std::path::PathBuf::from(&output_dir)
65+
} else {
66+
// Multi-file torrent - create subdirectory structure
67+
let mut path = std::path::PathBuf::from(&output_dir);
68+
for component in &file_info.path {
69+
path.push(component);
70+
}
71+
path
72+
};
73+
74+
// Create parent directories if needed
75+
if let Some(parent) = file_path.parent() {
76+
create_dir_all(parent).await?;
77+
}
78+
79+
// Create the file
80+
let file = File::create(&file_path).await?;
81+
file_handles.insert(file_index, file);
82+
83+
trace!("Created file: {:?}", file_path);
84+
}
85+
5786
let total_downloaded = Arc::new(AtomicU64::new(0));
5887
let total_downloaded_clone = total_downloaded.clone();
5988

@@ -71,21 +100,41 @@ pub async fn download_file(filename: &str, out_file: Option<String>) -> anyhow::
71100
let pr = add_torrent_result.pr_rx.recv_async().await?;
72101

73102
hashset.insert(pr.index);
74-
let (start, end) = utils::calculate_bounds_for_piece(&torrent, pr.index as usize);
75-
trace!(
76-
"index: {}, start: {}, end: {} len {}",
77-
pr.index,
78-
start,
79-
end,
80-
pr.length
81-
);
82-
file.seek(SeekFrom::Start(start as u64)).await?;
83-
file.write_all(pr.buf.as_slice()).await?;
103+
104+
// Map piece to files and write data accordingly
105+
let file_mappings = utils::map_piece_to_files(&torrent, pr.index as usize);
106+
let mut piece_offset = 0;
107+
108+
for mapping in file_mappings {
109+
let file = file_handles.get_mut(&mapping.file_index).ok_or_else(|| {
110+
anyhow::anyhow!("File handle not found for index {}", mapping.file_index)
111+
})?;
112+
113+
// Seek to correct position in file
114+
file.seek(SeekFrom::Start(mapping.file_offset as u64))
115+
.await?;
116+
117+
// Write the portion of the piece that belongs to this file
118+
let piece_data = &pr.buf[piece_offset..piece_offset + mapping.length];
119+
file.write_all(piece_data).await?;
120+
121+
piece_offset += mapping.length;
122+
123+
trace!(
124+
"Wrote {} bytes to file {} at offset {}",
125+
mapping.length,
126+
mapping.file_index,
127+
mapping.file_offset
128+
);
129+
}
84130

85131
total_downloaded.fetch_add(pr.length as u64, std::sync::atomic::Ordering::Relaxed);
86132
}
87133

88-
file.sync_all().await?;
134+
// Sync all files
135+
for (_, file) in file_handles {
136+
file.sync_all().await?;
137+
}
89138

90139
Ok(())
91140
}

0 commit comments

Comments
 (0)