Merge pull request #269 from ikatson/bep-47

BEP-47 padding files + refactor related code
This commit is contained in:
Igor Katson 2024-11-07 15:08:20 +00:00 committed by GitHub
commit c2b2e8e8e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 227 additions and 93 deletions

View file

@ -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())

View file

@ -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,
}) })
} }

View file

@ -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,
} }

View file

@ -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;

View file

@ -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| {

View file

@ -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>>>()?;

View file

@ -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;

View file

@ -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())

View file

@ -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 {}: {:#?}",

View file

@ -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,

View file

@ -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),
} }
} }
} }

View file

@ -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" }
) )
} }