Merge pull request #269 from ikatson/bep-47
BEP-47 padding files + refactor related code
This commit is contained in:
commit
c2b2e8e8e7
12 changed files with 227 additions and 93 deletions
|
|
@ -533,23 +533,23 @@ fn make_torrent_details(
|
||||||
output_folder: String,
|
output_folder: String,
|
||||||
) -> Result<TorrentDetailsResponse> {
|
) -> Result<TorrentDetailsResponse> {
|
||||||
let files = info
|
let files = info
|
||||||
.iter_filenames_and_lengths()
|
.iter_file_details()
|
||||||
.context("error iterating filenames and lengths")?
|
.context("error iterating filenames and lengths")?
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(idx, (filename_it, length))| {
|
.map(|(idx, d)| {
|
||||||
let name = match filename_it.to_string() {
|
let name = match d.filename.to_string() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("error reading filename: {:?}", err);
|
warn!("error reading filename: {:?}", err);
|
||||||
"<INVALID NAME>".to_string()
|
"<INVALID NAME>".to_string()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let components = filename_it.to_vec().unwrap_or_default();
|
let components = d.filename.to_vec().unwrap_or_default();
|
||||||
let included = only_files.map(|o| o.contains(&idx)).unwrap_or(true);
|
let included = only_files.map(|o| o.contains(&idx)).unwrap_or(true);
|
||||||
TorrentDetailsResponseFile {
|
TorrentDetailsResponseFile {
|
||||||
name,
|
name,
|
||||||
components,
|
components,
|
||||||
length,
|
length: d.len,
|
||||||
included,
|
included,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -568,10 +568,11 @@ fn torrent_file_mime_type(
|
||||||
info: &TorrentMetaV1Info<ByteBufOwned>,
|
info: &TorrentMetaV1Info<ByteBufOwned>,
|
||||||
file_idx: usize,
|
file_idx: usize,
|
||||||
) -> Result<&'static str> {
|
) -> Result<&'static str> {
|
||||||
info.iter_filenames_and_lengths()?
|
info.iter_file_details()?
|
||||||
.nth(file_idx)
|
.nth(file_idx)
|
||||||
.and_then(|(f, _)| {
|
.and_then(|d| {
|
||||||
f.iter_components()
|
d.filename
|
||||||
|
.iter_components()
|
||||||
.last()
|
.last()
|
||||||
.and_then(|r| r.ok())
|
.and_then(|r| r.ok())
|
||||||
.and_then(|s| mime_guess::from_path(s).first_raw())
|
.and_then(|s| mime_guess::from_path(s).first_raw())
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,13 @@ async fn create_torrent_raw<'a>(
|
||||||
.components()
|
.components()
|
||||||
.map(|c| osstr_to_bytes(c.as_os_str()).into())
|
.map(|c| osstr_to_bytes(c.as_os_str()).into())
|
||||||
.collect();
|
.collect();
|
||||||
output_files.push(TorrentMetaV1File { length, path });
|
output_files.push(TorrentMetaV1File {
|
||||||
|
length,
|
||||||
|
path,
|
||||||
|
attr: None,
|
||||||
|
sha1: None,
|
||||||
|
symlink_path: None,
|
||||||
|
});
|
||||||
continue 'outer;
|
continue 'outer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,6 +160,9 @@ async fn create_torrent_raw<'a>(
|
||||||
} else {
|
} else {
|
||||||
Some(output_files)
|
Some(output_files)
|
||||||
},
|
},
|
||||||
|
attr: None,
|
||||||
|
sha1: None,
|
||||||
|
symlink_path: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use librqbit_core::torrent_metainfo::FileDetailsAttrs;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FileInfo {
|
pub struct FileInfo {
|
||||||
pub relative_filename: PathBuf,
|
pub relative_filename: PathBuf,
|
||||||
pub offset_in_torrent: u64,
|
pub offset_in_torrent: u64,
|
||||||
pub piece_range: std::ops::Range<u32>,
|
pub piece_range: std::ops::Range<u32>,
|
||||||
|
pub attrs: FileDetailsAttrs,
|
||||||
pub len: u64,
|
pub len: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ use crate::{
|
||||||
|
|
||||||
pub fn update_hash_from_file<Sha1: ISha1>(
|
pub fn update_hash_from_file<Sha1: ISha1>(
|
||||||
file_id: usize,
|
file_id: usize,
|
||||||
|
file_info: &FileInfo,
|
||||||
mut pos: u64,
|
mut pos: u64,
|
||||||
files: &dyn TorrentStorage,
|
files: &dyn TorrentStorage,
|
||||||
hash: &mut Sha1,
|
hash: &mut Sha1,
|
||||||
|
|
@ -30,9 +31,15 @@ pub fn update_hash_from_file<Sha1: ISha1>(
|
||||||
let mut read = 0;
|
let mut read = 0;
|
||||||
while bytes_to_read > 0 {
|
while bytes_to_read > 0 {
|
||||||
let chunk = std::cmp::min(buf.len(), bytes_to_read);
|
let chunk = std::cmp::min(buf.len(), bytes_to_read);
|
||||||
files
|
if file_info.attrs.padding {
|
||||||
.pread_exact(file_id, pos, &mut buf[..chunk])
|
buf[..chunk].fill(0);
|
||||||
.with_context(|| format!("failed reading chunk of size {chunk}, read so far {read}"))?;
|
} else {
|
||||||
|
files
|
||||||
|
.pread_exact(file_id, pos, &mut buf[..chunk])
|
||||||
|
.with_context(|| {
|
||||||
|
format!("failed reading chunk of size {chunk}, read so far {read}")
|
||||||
|
})?;
|
||||||
|
}
|
||||||
bytes_to_read -= chunk;
|
bytes_to_read -= chunk;
|
||||||
read += chunk;
|
read += chunk;
|
||||||
pos += chunk as u64;
|
pos += chunk as u64;
|
||||||
|
|
@ -138,6 +145,7 @@ impl<'a> FileOps<'a> {
|
||||||
|
|
||||||
if let Err(err) = update_hash_from_file(
|
if let Err(err) = update_hash_from_file(
|
||||||
current_file.index,
|
current_file.index,
|
||||||
|
current_file.fi,
|
||||||
pos,
|
pos,
|
||||||
self.files,
|
self.files,
|
||||||
&mut computed_hash,
|
&mut computed_hash,
|
||||||
|
|
@ -181,7 +189,8 @@ impl<'a> FileOps<'a> {
|
||||||
|
|
||||||
let mut piece_remaining_bytes = piece_length as usize;
|
let mut piece_remaining_bytes = piece_length as usize;
|
||||||
|
|
||||||
for (file_idx, (name, file_len)) in self.torrent.iter_filenames_and_lengths()?.enumerate() {
|
for (file_idx, fi) in self.file_infos.iter().enumerate() {
|
||||||
|
let file_len = fi.len;
|
||||||
if absolute_offset > file_len {
|
if absolute_offset > file_len {
|
||||||
absolute_offset -= file_len;
|
absolute_offset -= file_len;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -198,6 +207,7 @@ impl<'a> FileOps<'a> {
|
||||||
);
|
);
|
||||||
update_hash_from_file(
|
update_hash_from_file(
|
||||||
file_idx,
|
file_idx,
|
||||||
|
fi,
|
||||||
absolute_offset,
|
absolute_offset,
|
||||||
self.files,
|
self.files,
|
||||||
&mut h,
|
&mut h,
|
||||||
|
|
@ -205,7 +215,10 @@ impl<'a> FileOps<'a> {
|
||||||
to_read_in_file,
|
to_read_in_file,
|
||||||
)
|
)
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
format!("error reading {to_read_in_file} bytes, file_id: {file_idx} (\"{name:?}\")")
|
format!(
|
||||||
|
"error reading {to_read_in_file} bytes, file_id: {file_idx} (\"{:?}\")",
|
||||||
|
fi.relative_filename
|
||||||
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
piece_remaining_bytes -= to_read_in_file;
|
piece_remaining_bytes -= to_read_in_file;
|
||||||
|
|
@ -246,7 +259,8 @@ impl<'a> FileOps<'a> {
|
||||||
let mut absolute_offset = self.lengths.chunk_absolute_offset(chunk_info);
|
let mut absolute_offset = self.lengths.chunk_absolute_offset(chunk_info);
|
||||||
let mut buf = result_buf;
|
let mut buf = result_buf;
|
||||||
|
|
||||||
for (file_idx, file_len) in self.torrent.iter_file_lengths()?.enumerate() {
|
for (file_idx, file_info) in self.file_infos.iter().enumerate() {
|
||||||
|
let file_len = file_info.len;
|
||||||
if absolute_offset > file_len {
|
if absolute_offset > file_len {
|
||||||
absolute_offset -= file_len;
|
absolute_offset -= file_len;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -262,11 +276,15 @@ impl<'a> FileOps<'a> {
|
||||||
absolute_offset,
|
absolute_offset,
|
||||||
&chunk_info
|
&chunk_info
|
||||||
);
|
);
|
||||||
self.files
|
if file_info.attrs.padding {
|
||||||
.pread_exact(file_idx, absolute_offset, &mut buf[..to_read_in_file])
|
buf[..to_read_in_file].fill(0);
|
||||||
.with_context(|| {
|
} else {
|
||||||
format!("error reading {file_idx} bytes, file_id: {to_read_in_file}")
|
self.files
|
||||||
})?;
|
.pread_exact(file_idx, absolute_offset, &mut buf[..to_read_in_file])
|
||||||
|
.with_context(|| {
|
||||||
|
format!("error reading {file_idx} bytes, file_id: {to_read_in_file}")
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
buf = &mut buf[to_read_in_file..];
|
buf = &mut buf[to_read_in_file..];
|
||||||
|
|
||||||
|
|
@ -292,7 +310,8 @@ impl<'a> FileOps<'a> {
|
||||||
let mut buf = data.block.as_ref();
|
let mut buf = data.block.as_ref();
|
||||||
let mut absolute_offset = self.lengths.chunk_absolute_offset(chunk_info);
|
let mut absolute_offset = self.lengths.chunk_absolute_offset(chunk_info);
|
||||||
|
|
||||||
for (file_idx, (name, file_len)) in self.torrent.iter_filenames_and_lengths()?.enumerate() {
|
for (file_idx, file_info) in self.file_infos.iter().enumerate() {
|
||||||
|
let file_len = file_info.len;
|
||||||
if absolute_offset > file_len {
|
if absolute_offset > file_len {
|
||||||
absolute_offset -= file_len;
|
absolute_offset -= file_len;
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -311,9 +330,16 @@ impl<'a> FileOps<'a> {
|
||||||
to_write,
|
to_write,
|
||||||
absolute_offset
|
absolute_offset
|
||||||
);
|
);
|
||||||
self.files
|
if !file_info.attrs.padding {
|
||||||
.pwrite_all(file_idx, absolute_offset, &buf[..to_write])
|
self.files
|
||||||
.with_context(|| format!("error writing to file {file_idx} (\"{name:?}\")"))?;
|
.pwrite_all(file_idx, absolute_offset, &buf[..to_write])
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"error writing to file {file_idx} (\"{:?}\")",
|
||||||
|
file_info.relative_filename
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
}
|
||||||
buf = &buf[to_write..];
|
buf = &buf[to_write..];
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,10 @@ impl HttpApi {
|
||||||
let mut playlist_items = handle
|
let mut playlist_items = handle
|
||||||
.shared()
|
.shared()
|
||||||
.info
|
.info
|
||||||
.iter_filenames_and_lengths()?
|
.iter_file_details()?
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(file_idx, (filename, _))| {
|
.filter_map(|(file_idx, file_details)| {
|
||||||
let filename = filename.to_vec().ok()?.join("/");
|
let filename = file_details.filename.to_vec().ok()?.join("/");
|
||||||
let is_playable = mime_guess::from_path(&filename)
|
let is_playable = mime_guess::from_path(&filename)
|
||||||
.first()
|
.first()
|
||||||
.map(|mime| {
|
.map(|mime| {
|
||||||
|
|
|
||||||
|
|
@ -156,8 +156,9 @@ fn compute_only_files_regex<ByteBuf: AsRef<[u8]>>(
|
||||||
) -> anyhow::Result<Vec<usize>> {
|
) -> anyhow::Result<Vec<usize>> {
|
||||||
let filename_re = regex::Regex::new(filename_re).context("filename regex is incorrect")?;
|
let filename_re = regex::Regex::new(filename_re).context("filename regex is incorrect")?;
|
||||||
let mut only_files = Vec::new();
|
let mut only_files = Vec::new();
|
||||||
for (idx, (filename, _)) in torrent.iter_filenames_and_lengths()?.enumerate() {
|
for (idx, fd) in torrent.iter_file_details()?.enumerate() {
|
||||||
let full_path = filename
|
let full_path = fd
|
||||||
|
.filename
|
||||||
.to_pathbuf()
|
.to_pathbuf()
|
||||||
.with_context(|| format!("filename of file {idx} is not valid utf8"))?;
|
.with_context(|| format!("filename of file {idx} is not valid utf8"))?;
|
||||||
if filename_re.is_match(full_path.to_str().unwrap()) {
|
if filename_re.is_match(full_path.to_str().unwrap()) {
|
||||||
|
|
@ -191,12 +192,12 @@ fn compute_only_files(
|
||||||
}
|
}
|
||||||
(None, Some(filename_re)) => {
|
(None, Some(filename_re)) => {
|
||||||
let only_files = compute_only_files_regex(info, &filename_re)?;
|
let only_files = compute_only_files_regex(info, &filename_re)?;
|
||||||
for (idx, (filename, _)) in info.iter_filenames_and_lengths()?.enumerate() {
|
for (idx, fd) in info.iter_file_details()?.enumerate() {
|
||||||
if !only_files.contains(&idx) {
|
if !only_files.contains(&idx) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !list_only {
|
if !list_only {
|
||||||
info!(?filename, "will download");
|
info!(filename=?fd.filename, "will download");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(Some(only_files))
|
Ok(Some(only_files))
|
||||||
|
|
@ -1043,8 +1044,8 @@ impl Session {
|
||||||
info: &TorrentMetaV1Info<ByteBufOwned>,
|
info: &TorrentMetaV1Info<ByteBufOwned>,
|
||||||
) -> anyhow::Result<Option<PathBuf>> {
|
) -> anyhow::Result<Option<PathBuf>> {
|
||||||
let files = info
|
let files = info
|
||||||
.iter_filenames_and_lengths()?
|
.iter_file_details()?
|
||||||
.map(|(f, l)| Ok((f.to_pathbuf()?, l)))
|
.map(|fd| Ok((fd.filename.to_pathbuf()?, fd.len)))
|
||||||
.collect::<anyhow::Result<Vec<(PathBuf, u64)>>>()?;
|
.collect::<anyhow::Result<Vec<(PathBuf, u64)>>>()?;
|
||||||
if files.len() < 2 {
|
if files.len() < 2 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|
@ -1141,13 +1142,14 @@ impl Session {
|
||||||
|
|
||||||
let lengths = Lengths::from_torrent(&info)?;
|
let lengths = Lengths::from_torrent(&info)?;
|
||||||
let file_infos = info
|
let file_infos = info
|
||||||
.iter_file_details(&lengths)?
|
.iter_file_details_ext(&lengths)?
|
||||||
.map(|fd| {
|
.map(|fd| {
|
||||||
Ok::<_, anyhow::Error>(FileInfo {
|
Ok::<_, anyhow::Error>(FileInfo {
|
||||||
relative_filename: fd.filename.to_pathbuf()?,
|
relative_filename: fd.details.filename.to_pathbuf()?,
|
||||||
offset_in_torrent: fd.offset,
|
offset_in_torrent: fd.offset,
|
||||||
piece_range: fd.pieces,
|
piece_range: fd.pieces,
|
||||||
len: fd.len,
|
len: fd.details.len,
|
||||||
|
attrs: fd.details.attrs(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<anyhow::Result<Vec<FileInfo>>>()?;
|
.collect::<anyhow::Result<Vec<FileInfo>>>()?;
|
||||||
|
|
|
||||||
|
|
@ -151,23 +151,26 @@ impl TorrentStorage for FilesystemStorage {
|
||||||
|
|
||||||
fn init(&mut self, meta: &ManagedTorrentShared) -> anyhow::Result<()> {
|
fn init(&mut self, meta: &ManagedTorrentShared) -> anyhow::Result<()> {
|
||||||
let mut files = Vec::<OpenedFile>::new();
|
let mut files = Vec::<OpenedFile>::new();
|
||||||
for file_details in meta.info.iter_file_details(&meta.lengths)? {
|
for file_details in meta.file_infos.iter() {
|
||||||
let mut full_path = self.output_folder.clone();
|
let mut full_path = self.output_folder.clone();
|
||||||
let relative_path = file_details
|
let relative_path = &file_details.relative_filename;
|
||||||
.filename
|
|
||||||
.to_pathbuf()
|
|
||||||
.context("error converting file to path")?;
|
|
||||||
full_path.push(relative_path);
|
full_path.push(relative_path);
|
||||||
|
|
||||||
std::fs::create_dir_all(full_path.parent().context("bug: no parent")?)?;
|
std::fs::create_dir_all(full_path.parent().context("bug: no parent")?)?;
|
||||||
let file = if meta.options.allow_overwrite {
|
let file = if file_details.attrs.padding {
|
||||||
OpenOptions::new()
|
OpenedFile::new_dummy()
|
||||||
.create(true)
|
} else if meta.options.allow_overwrite {
|
||||||
.truncate(false)
|
OpenedFile::new(
|
||||||
.read(true)
|
OpenOptions::new()
|
||||||
.write(true)
|
.create(true)
|
||||||
.open(&full_path)
|
.truncate(false)
|
||||||
.with_context(|| format!("error opening {full_path:?} in read/write mode"))?
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&full_path)
|
||||||
|
.with_context(|| {
|
||||||
|
format!("error opening {full_path:?} in read/write mode")
|
||||||
|
})?,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
// create_new does not seem to work with read(true), so calling this twice.
|
// create_new does not seem to work with read(true), so calling this twice.
|
||||||
OpenOptions::new()
|
OpenOptions::new()
|
||||||
|
|
@ -180,9 +183,9 @@ impl TorrentStorage for FilesystemStorage {
|
||||||
&full_path
|
&full_path
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
OpenOptions::new().read(true).write(true).open(&full_path)?
|
OpenedFile::new(OpenOptions::new().read(true).write(true).open(&full_path)?)
|
||||||
};
|
};
|
||||||
files.push(OpenedFile::new(file));
|
files.push(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.opened_files = files;
|
self.opened_files = files;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@ impl OpenedFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_dummy() -> Self {
|
||||||
|
Self {
|
||||||
|
file: RwLock::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn take(&self) -> anyhow::Result<Option<File>> {
|
pub fn take(&self) -> anyhow::Result<Option<File>> {
|
||||||
let mut f = self.file.write();
|
let mut f = self.file.write();
|
||||||
Ok(f.take())
|
Ok(f.take())
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,9 @@ impl TorrentStateInitializing {
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
if fi.attrs.padding {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Err(err) = self.files.ensure_file_length(idx, fi.len) {
|
if let Err(err) = self.files.ensure_file_length(idx, fi.len) {
|
||||||
warn!(
|
warn!(
|
||||||
"Error setting length for file {:?} to {}: {:#?}",
|
"Error setting length for file {:?} to {}: {:#?}",
|
||||||
|
|
|
||||||
|
|
@ -68,10 +68,10 @@ impl TorrentFileTreeNode {
|
||||||
let last_url_bit = torrent
|
let last_url_bit = torrent
|
||||||
.shared()
|
.shared()
|
||||||
.info
|
.info
|
||||||
.iter_filenames_and_lengths()
|
.iter_file_details()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|mut it| it.nth(fid))
|
.and_then(|mut it| it.nth(fid))
|
||||||
.and_then(|(fi, _)| fi.to_vec().ok())
|
.and_then(|fd| fd.filename.to_vec().ok())
|
||||||
.map(|components| {
|
.map(|components| {
|
||||||
components
|
components
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -111,10 +111,10 @@ struct TorrentFileTree {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_single_file_at_root(info: &TorrentMetaV1Info<ByteBufOwned>) -> bool {
|
fn is_single_file_at_root(info: &TorrentMetaV1Info<ByteBufOwned>) -> bool {
|
||||||
info.iter_filenames_and_lengths()
|
info.iter_file_details()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.flat_map(|(f, _)| f.iter_components())
|
.flat_map(|fd| fd.filename.iter_components())
|
||||||
.nth(1)
|
.nth(1)
|
||||||
.is_none()
|
.is_none()
|
||||||
}
|
}
|
||||||
|
|
@ -123,10 +123,10 @@ impl TorrentFileTree {
|
||||||
fn build(torent_id: TorrentId, info: &TorrentMetaV1Info<ByteBufOwned>) -> anyhow::Result<Self> {
|
fn build(torent_id: TorrentId, info: &TorrentMetaV1Info<ByteBufOwned>) -> anyhow::Result<Self> {
|
||||||
if is_single_file_at_root(info) {
|
if is_single_file_at_root(info) {
|
||||||
let filename = info
|
let filename = info
|
||||||
.iter_filenames_and_lengths()?
|
.iter_file_details()?
|
||||||
.next()
|
.next()
|
||||||
.context("bug")?
|
.context("bug")?
|
||||||
.0
|
.filename
|
||||||
.iter_components()
|
.iter_components()
|
||||||
.last()
|
.last()
|
||||||
.context("bug")??;
|
.context("bug")??;
|
||||||
|
|
@ -159,8 +159,8 @@ impl TorrentFileTree {
|
||||||
|
|
||||||
let mut name_cache = HashMap::new();
|
let mut name_cache = HashMap::new();
|
||||||
|
|
||||||
for (fid, (fi, _)) in info.iter_filenames_and_lengths()?.enumerate() {
|
for (fid, fd) in info.iter_file_details()?.enumerate() {
|
||||||
let components = match fi.to_vec() {
|
let components = match fd.filename.to_vec() {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(_) => continue,
|
Err(_) => continue,
|
||||||
};
|
};
|
||||||
|
|
@ -402,9 +402,15 @@ mod tests {
|
||||||
.map(|f| TorrentMetaV1File {
|
.map(|f| TorrentMetaV1File {
|
||||||
length: 1,
|
length: 1,
|
||||||
path: f.split("/").map(|f| f.as_bytes().into()).collect(),
|
path: f.split("/").map(|f| f.as_bytes().into()).collect(),
|
||||||
|
attr: None,
|
||||||
|
sha1: None,
|
||||||
|
symlink_path: None,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
|
attr: None,
|
||||||
|
sha1: None,
|
||||||
|
symlink_path: None,
|
||||||
},
|
},
|
||||||
comment: None,
|
comment: None,
|
||||||
created_by: None,
|
created_by: None,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use clone_to_owned::CloneToOwned;
|
||||||
use itertools::Either;
|
use itertools::Either;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{iter::once, path::PathBuf};
|
use std::{iter::once, path::PathBuf};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{hash_id::Id20, lengths::Lengths};
|
use crate::{hash_id::Id20, lengths::Lengths};
|
||||||
|
|
||||||
|
|
@ -99,6 +100,16 @@ pub struct TorrentMetaV1Info<BufType> {
|
||||||
// Single-file mode
|
// Single-file mode
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub length: Option<u64>,
|
pub length: Option<u64>,
|
||||||
|
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub attr: Option<BufType>,
|
||||||
|
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sha1: Option<BufType>,
|
||||||
|
#[serde(
|
||||||
|
default = "none",
|
||||||
|
rename = "symlink path",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub symlink_path: Option<Vec<BufType>>,
|
||||||
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub md5sum: Option<BufType>,
|
pub md5sum: Option<BufType>,
|
||||||
|
|
@ -174,14 +185,57 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy)]
|
||||||
|
pub struct FileDetailsAttrs {
|
||||||
|
pub symlink: bool,
|
||||||
|
pub hidden: bool,
|
||||||
|
pub padding: bool,
|
||||||
|
pub executable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FileDetails<'a, BufType> {
|
pub struct FileDetails<'a, BufType> {
|
||||||
pub filename: FileIteratorName<'a, BufType>,
|
pub filename: FileIteratorName<'a, BufType>,
|
||||||
pub offset: u64,
|
|
||||||
pub len: u64,
|
pub len: u64,
|
||||||
|
|
||||||
|
// bep-47
|
||||||
|
attr: Option<&'a BufType>,
|
||||||
|
pub sha1: Option<&'a BufType>,
|
||||||
|
pub symlink_path: Option<&'a [BufType]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, BufType> FileDetails<'a, BufType>
|
||||||
|
where
|
||||||
|
BufType: AsRef<[u8]>,
|
||||||
|
{
|
||||||
|
pub fn attrs(&self) -> FileDetailsAttrs {
|
||||||
|
let attrs = match self.attr {
|
||||||
|
Some(attrs) => attrs,
|
||||||
|
None => return FileDetailsAttrs::default(),
|
||||||
|
};
|
||||||
|
let mut result = FileDetailsAttrs::default();
|
||||||
|
for byte in attrs.as_ref().iter().copied() {
|
||||||
|
match byte {
|
||||||
|
b'l' => result.symlink = true,
|
||||||
|
b'h' => result.hidden = true,
|
||||||
|
b'p' => result.padding = true,
|
||||||
|
b'x' => result.executable = true,
|
||||||
|
other => debug!(attr = other, "unknown file attribute"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FileDetailsExt<'a, BufType> {
|
||||||
|
pub details: FileDetails<'a, BufType>,
|
||||||
|
// absolute offset in torrent if it was a flat blob of bytes
|
||||||
|
pub offset: u64,
|
||||||
|
|
||||||
|
// the pieces that contain this file
|
||||||
pub pieces: std::ops::Range<u32>,
|
pub pieces: std::ops::Range<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, BufType> FileDetails<'a, BufType> {
|
impl<'a, BufType> FileDetailsExt<'a, BufType> {
|
||||||
pub fn pieces_usize(&self) -> std::ops::Range<usize> {
|
pub fn pieces_usize(&self) -> std::ops::Range<usize> {
|
||||||
self.pieces.start as usize..self.pieces.end as usize
|
self.pieces.start as usize..self.pieces.end as usize
|
||||||
}
|
}
|
||||||
|
|
@ -203,60 +257,77 @@ impl<BufType: AsRef<[u8]>> TorrentMetaV1Info<BufType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
pub fn iter_filenames_and_lengths(
|
pub fn iter_file_details(
|
||||||
&self,
|
&self,
|
||||||
) -> anyhow::Result<impl Iterator<Item = (FileIteratorName<'_, BufType>, u64)>> {
|
) -> anyhow::Result<impl Iterator<Item = FileDetails<'_, BufType>>> {
|
||||||
match (self.length, self.files.as_ref()) {
|
match (self.length, self.files.as_ref()) {
|
||||||
// Single-file
|
// Single-file
|
||||||
(Some(length), None) => Ok(Either::Left(once((
|
(Some(length), None) => Ok(Either::Left(once(FileDetails {
|
||||||
FileIteratorName::Single(self.name.as_ref()),
|
filename: FileIteratorName::Single(self.name.as_ref()),
|
||||||
length,
|
len: length,
|
||||||
)))),
|
attr: self.attr.as_ref(),
|
||||||
|
sha1: self.sha1.as_ref(),
|
||||||
|
symlink_path: self.symlink_path.as_deref(),
|
||||||
|
}))),
|
||||||
|
|
||||||
// Multi-file
|
// Multi-file
|
||||||
(None, Some(files)) => {
|
(None, Some(files)) => {
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
anyhow::bail!("expected multi-file torrent to have at least one file")
|
anyhow::bail!("expected multi-file torrent to have at least one file")
|
||||||
}
|
}
|
||||||
Ok(Either::Right(
|
Ok(Either::Right(files.iter().map(|f| FileDetails {
|
||||||
files
|
filename: FileIteratorName::Tree(&f.path),
|
||||||
.iter()
|
len: f.length,
|
||||||
.map(|f| (FileIteratorName::Tree(&f.path), f.length)),
|
attr: f.attr.as_ref(),
|
||||||
))
|
sha1: f.sha1.as_ref(),
|
||||||
|
symlink_path: f.symlink_path.as_deref(),
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
_ => anyhow::bail!("torrent can't be both in single and multi-file mode"),
|
_ => anyhow::bail!("torrent can't be both in single and multi-file mode"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_file_lengths(&self) -> anyhow::Result<impl Iterator<Item = u64> + '_> {
|
pub fn iter_file_lengths(&self) -> anyhow::Result<impl Iterator<Item = u64> + '_> {
|
||||||
Ok(self.iter_filenames_and_lengths()?.map(|(_, l)| l))
|
Ok(self.iter_file_details()?.map(|d| d.len))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: lenghts MUST be construced with Lenghts::from_torrent, otherwise
|
// NOTE: lenghts MUST be construced with Lenghts::from_torrent, otherwise
|
||||||
// the yielded results will be garbage.
|
// the yielded results will be garbage.
|
||||||
pub fn iter_file_details<'a>(
|
pub fn iter_file_details_ext<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
lengths: &'a Lengths,
|
lengths: &'a Lengths,
|
||||||
) -> anyhow::Result<impl Iterator<Item = FileDetails<'a, BufType>> + 'a> {
|
) -> anyhow::Result<impl Iterator<Item = FileDetailsExt<'a, BufType>> + 'a> {
|
||||||
Ok(self
|
Ok(self.iter_file_details()?.scan(0u64, |acc_offset, details| {
|
||||||
.iter_filenames_and_lengths()?
|
let offset = *acc_offset;
|
||||||
.scan(0u64, |acc_offset, (filename, len)| {
|
*acc_offset += details.len;
|
||||||
let offset = *acc_offset;
|
Some(FileDetailsExt {
|
||||||
*acc_offset += len;
|
pieces: lengths.iter_pieces_within_offset(offset, details.len),
|
||||||
Some(FileDetails {
|
details,
|
||||||
filename,
|
offset,
|
||||||
pieces: lengths.iter_pieces_within_offset(offset, len),
|
})
|
||||||
offset,
|
}))
|
||||||
len,
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fn none<T>() -> Option<T> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct TorrentMetaV1File<BufType> {
|
pub struct TorrentMetaV1File<BufType> {
|
||||||
pub length: u64,
|
pub length: u64,
|
||||||
pub path: Vec<BufType>,
|
pub path: Vec<BufType>,
|
||||||
|
|
||||||
|
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub attr: Option<BufType>,
|
||||||
|
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||||
|
pub sha1: Option<BufType>,
|
||||||
|
#[serde(
|
||||||
|
default = "none",
|
||||||
|
rename = "symlink path",
|
||||||
|
skip_serializing_if = "Option::is_none"
|
||||||
|
)]
|
||||||
|
pub symlink_path: Option<Vec<BufType>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<BufType> TorrentMetaV1File<BufType>
|
impl<BufType> TorrentMetaV1File<BufType>
|
||||||
|
|
@ -282,6 +353,9 @@ where
|
||||||
TorrentMetaV1File {
|
TorrentMetaV1File {
|
||||||
length: self.length,
|
length: self.length,
|
||||||
path: self.path.clone_to_owned(within_buffer),
|
path: self.path.clone_to_owned(within_buffer),
|
||||||
|
attr: self.attr.clone_to_owned(within_buffer),
|
||||||
|
sha1: self.sha1.clone_to_owned(within_buffer),
|
||||||
|
symlink_path: self.symlink_path.clone_to_owned(within_buffer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -300,6 +374,9 @@ where
|
||||||
length: self.length,
|
length: self.length,
|
||||||
md5sum: self.md5sum.clone_to_owned(within_buffer),
|
md5sum: self.md5sum.clone_to_owned(within_buffer),
|
||||||
files: self.files.clone_to_owned(within_buffer),
|
files: self.files.clone_to_owned(within_buffer),
|
||||||
|
attr: self.attr.clone_to_owned(within_buffer),
|
||||||
|
sha1: self.sha1.clone_to_owned(within_buffer),
|
||||||
|
symlink_path: self.symlink_path.clone_to_owned(within_buffer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -762,17 +762,15 @@ async fn async_main(opts: Opts, cancel: CancellationToken) -> anyhow::Result<()>
|
||||||
only_files,
|
only_files,
|
||||||
..
|
..
|
||||||
}) => {
|
}) => {
|
||||||
for (idx, (filename, len)) in
|
for (idx, fd) in info.iter_file_details()?.enumerate() {
|
||||||
info.iter_filenames_and_lengths()?.enumerate()
|
|
||||||
{
|
|
||||||
let included = match &only_files {
|
let included = match &only_files {
|
||||||
Some(files) => files.contains(&idx),
|
Some(files) => files.contains(&idx),
|
||||||
None => true,
|
None => true,
|
||||||
};
|
};
|
||||||
info!(
|
info!(
|
||||||
"File {}, size {}{}",
|
"File {:?}, size {}{}",
|
||||||
filename.to_string()?,
|
fd.filename,
|
||||||
SF::new(len),
|
SF::new(fd.len),
|
||||||
if included { "" } else { ", will skip" }
|
if included { "" } else { ", will skip" }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue