Ability to change the list of files at any time, including through UI (#115)

* Now can update the list of files without pausing/unpausing

* Shrink a few functions

* Reopen write when updating files

* Todos

* opened_file abstraction

* iter_pieces_within iterator

* Simplify iter_pieces_within

* Simplify iter_pieces_within

* Add "iter_file_details"

* temporarily broken: readonly by default

* Live torrent - reopen files

* Reopen files after changing the list

* Now reopening files read only when they are completed

* Fix a bug in opened_file.rs

* update todos

* update help

* Reconnect all peers that are idling

* Add a couple fields to OpenedFile

* Add a couple fields to OpenedFile

* Small cleanups - use the new iterator where possible

* size_of_piece_in_file function

* Updating have

* Include file progress

* Almost nothing

* ugly progress bars

* bad UI, saving

* its not so bad

* Works now

* update progress bar a bit

* Reopen read-only on pause

* Zero bytes isnt too bad! Doesnt break anything

* fix per file progress bars

* progress bar not as ugly anymore?

* ui tweaks

* fix a react bug

* TODO.md update

* Fix js + TODOs

* Compute per-file progress on init

* Fix stats updating live

* Nothing

* Nothing

* cleanup ui a bit

* Nothing

* Final fixes

* Trying to fix rust 1.73

* Sorting filenames

* remove unnecessary indentation

* Remove unnecessary comment
This commit is contained in:
Igor Katson 2024-04-06 09:20:03 +01:00 committed by GitHub
parent d7380217f6
commit 5eb01ac226
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 865 additions and 512 deletions

View file

@ -6,14 +6,12 @@ use std::{
use anyhow::Context;
use parking_lot::Mutex;
use size_format::SizeFormatterBinary as SF;
use tracing::{debug, info, warn};
use crate::{
chunk_tracker::{ChunkTracker, HaveNeededSelected},
file_ops::FileOps,
chunk_tracker::ChunkTracker, file_ops::FileOps, opened_file::OpenedFile,
type_aliases::OpenedFiles,
};
use super::{paused::TorrentStatePaused, ManagedTorrentInfo};
@ -43,48 +41,52 @@ impl TorrentStateInitializing {
}
pub async fn check(&self) -> anyhow::Result<TorrentStatePaused> {
let (files, filenames) = {
let mut files =
Vec::<Arc<Mutex<File>>>::with_capacity(self.meta.info.iter_file_lengths()?.count());
let mut filenames = Vec::new();
for (path_bits, _) in self.meta.info.iter_filenames_and_lengths()? {
let mut full_path = self.meta.out_dir.clone();
let relative_path = path_bits
.to_pathbuf()
.context("error converting file to path")?;
full_path.push(relative_path);
let mut files = OpenedFiles::new();
for file_details in self.meta.info.iter_file_details(&self.meta.lengths)? {
let mut full_path = self.meta.out_dir.clone();
let relative_path = file_details
.filename
.to_pathbuf()
.context("error converting file to path")?;
full_path.push(relative_path);
std::fs::create_dir_all(full_path.parent().unwrap())?;
let file = if self.meta.options.overwrite {
OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&full_path)
.with_context(|| {
format!("error opening {full_path:?} in read/write mode")
})?
} else {
// TODO: create_new does not seem to work with read(true), so calling this twice.
OpenOptions::new()
.create_new(true)
.write(true)
.open(&full_path)
.with_context(|| format!("error creating {:?}", &full_path))?;
OpenOptions::new().read(true).write(true).open(&full_path)?
};
filenames.push(full_path);
files.push(Arc::new(Mutex::new(file)))
}
(files, filenames)
};
std::fs::create_dir_all(full_path.parent().context("bug: no parent")?)?;
let file = if self.meta.options.overwrite {
OpenOptions::new()
.create(true)
.read(true)
.write(true)
.open(&full_path)
.with_context(|| format!("error opening {full_path:?} in read/write mode"))?
} else {
// TODO: create_new does not seem to work with read(true), so calling this twice.
OpenOptions::new()
.create_new(true)
.write(true)
.open(&full_path)
.with_context(|| format!("error creating {:?}", &full_path))?;
OpenOptions::new().read(true).write(true).open(&full_path)?
};
files.push(OpenedFile::new(
file,
full_path,
0,
file_details.len,
file_details.offset,
file_details.pieces,
));
}
debug!("computed lengths: {:?}", &self.meta.lengths);
info!("Doing initial checksum validation, this might take a while...");
let initial_check_results = self.meta.spawner.spawn_block_in_place(|| {
FileOps::new(&self.meta.info, &files, &self.meta.lengths)
.initial_check(self.only_files.as_deref(), &self.checked_bytes)
FileOps::new(&self.meta.info, &files, &self.meta.lengths).initial_check(
self.only_files.as_deref(),
&files,
&self.meta.lengths,
&self.checked_bytes,
)
})?;
info!(
@ -94,36 +96,35 @@ impl TorrentStateInitializing {
SF::new(initial_check_results.selected_bytes)
);
// Ensure file lenghts are correct, and reopen read-only.
self.meta.spawner.spawn_block_in_place(|| {
for (idx, (file, (name, length))) in files
.iter()
.zip(self.meta.info.iter_filenames_and_lengths().unwrap())
.enumerate()
{
for (idx, file) in files.iter().enumerate() {
if self
.only_files
.as_ref()
.map(|v| !v.contains(&idx))
.unwrap_or(false)
.map(|v| v.contains(&idx))
.unwrap_or(true)
{
continue;
}
let now = Instant::now();
if let Err(err) = ensure_file_length(&file.lock(), length) {
warn!(
"Error setting length for file {:?} to {}: {:#?}",
name, length, err
);
} else {
debug!(
"Set length for file {:?} to {} in {:?}",
name,
SF::new(length),
now.elapsed()
);
let now = Instant::now();
if let Err(err) = ensure_file_length(&file.file.lock(), file.len) {
warn!(
"Error setting length for file {:?} to {}: {:#?}",
file.filename, file.len, err
);
} else {
debug!(
"Set length for file {:?} to {} in {:?}",
file.filename,
SF::new(file.len),
now.elapsed()
);
}
}
file.reopen(true)?;
}
});
Ok::<_, anyhow::Error>(())
})?;
let chunk_tracker = ChunkTracker::new(
initial_check_results.have_pieces,
@ -135,13 +136,7 @@ impl TorrentStateInitializing {
let paused = TorrentStatePaused {
info: self.meta.clone(),
files,
filenames,
chunk_tracker,
hns: HaveNeededSelected {
have_bytes: initial_check_results.have_bytes,
needed_bytes: initial_check_results.needed_bytes,
selected_bytes: initial_check_results.selected_bytes,
},
};
Ok(paused)
}

View file

@ -44,10 +44,8 @@ pub mod peers;
pub mod stats;
use std::{
collections::HashMap,
fs::File,
collections::{HashMap, HashSet},
net::SocketAddr,
path::PathBuf,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
@ -60,7 +58,6 @@ use backoff::backoff::Backoff;
use buffers::{ByteBuf, ByteBufOwned};
use clone_to_owned::CloneToOwned;
use futures::{stream::FuturesUnordered, StreamExt};
use itertools::Itertools;
use librqbit_core::{
hash_id::Id20,
lengths::{ChunkInfo, Lengths, ValidPieceIndex},
@ -68,7 +65,7 @@ use librqbit_core::{
speed_estimator::SpeedEstimator,
torrent_metainfo::TorrentMetaV1Info,
};
use parking_lot::{Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard};
use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use peer_binary_protocol::{
extended::handshake::ExtendedHandshake, Handshake, Message, MessageOwned, Piece, Request,
};
@ -90,7 +87,7 @@ use crate::{
},
session::CheckedIncomingConnection,
torrent_state::{peer::Peer, utils::atomic_inc},
type_aliases::{PeerHandle, BF},
type_aliases::{OpenedFiles, PeerHandle, BF},
};
use self::{
@ -116,18 +113,6 @@ struct InflightPiece {
started: Instant,
}
fn dummy_file() -> anyhow::Result<std::fs::File> {
#[cfg(target_os = "windows")]
const DEVNULL: &str = "NUL";
#[cfg(not(target_os = "windows"))]
const DEVNULL: &str = "/dev/null";
std::fs::OpenOptions::new()
.read(true)
.open(DEVNULL)
.with_context(|| format!("error opening {}", DEVNULL))
}
fn make_piece_bitfield(lengths: &Lengths) -> BF {
BF::from_boxed_slice(vec![0; lengths.piece_bitfield_bytes()].into_boxed_slice())
}
@ -170,11 +155,7 @@ pub struct TorrentStateLive {
meta: Arc<ManagedTorrentInfo>,
locked: RwLock<TorrentStateLocked>,
files: Vec<Arc<Mutex<File>>>,
filenames: Vec<PathBuf>,
initially_needed_bytes: u64,
total_selected_bytes: u64,
files: OpenedFiles,
stats: AtomicStats,
lengths: Lengths,
@ -192,22 +173,48 @@ pub struct TorrentStateLive {
cancellation_token: CancellationToken,
}
fn reopen_necessary_files_for_write(ct: &ChunkTracker, files: &OpenedFiles) -> anyhow::Result<()> {
// Reopen files that we don't have, but have selected in write-only mode.
for opened_file in files.iter() {
let prange = opened_file.piece_range_usize();
if prange.is_empty() {
continue;
}
let selected = ct
.get_selected_pieces()
.get(prange.clone())
.with_context(|| format!("bug: bad range get_selected_pieces(), {prange:?}"))?;
let have = ct
.get_have_pieces()
.get(prange.clone())
.with_context(|| format!("bug: bad range get_have_pieces(), {prange:?}"))?;
let need_write = selected
.iter()
.zip(have.iter())
.any(|(selected, have)| *selected && !*have);
if need_write {
opened_file.reopen(false)?;
}
}
Ok(())
}
impl TorrentStateLive {
pub(crate) fn new(
paused: TorrentStatePaused,
fatal_errors_tx: tokio::sync::oneshot::Sender<anyhow::Error>,
cancellation_token: CancellationToken,
) -> Arc<Self> {
) -> anyhow::Result<Arc<Self>> {
let (peer_queue_tx, peer_queue_rx) = unbounded_channel();
let down_speed_estimator = SpeedEstimator::new(5);
let up_speed_estimator = SpeedEstimator::new(5);
let have_bytes = paused.hns.have_bytes;
let needed_bytes = paused.hns.needed_bytes;
let total_selected_bytes = paused.hns.selected_bytes;
let have_bytes = paused.chunk_tracker.get_hns().have_bytes;
let lengths = *paused.chunk_tracker.get_lengths();
reopen_necessary_files_for_write(&paused.chunk_tracker, &paused.files)?;
let state = Arc::new(TorrentStateLive {
meta: paused.info.clone(),
peers: Default::default(),
@ -217,14 +224,11 @@ impl TorrentStateLive {
fatal_errors_tx: Some(fatal_errors_tx),
}),
files: paused.files,
filenames: paused.filenames,
stats: AtomicStats {
have_bytes: AtomicU64::new(have_bytes),
..Default::default()
},
initially_needed_bytes: needed_bytes,
lengths,
total_selected_bytes,
peer_semaphore: Arc::new(Semaphore::new(128)),
peer_queue_tx,
finished_notify: Notify::new(),
@ -246,9 +250,7 @@ impl TorrentStateLive {
let now = Instant::now();
let stats = state.stats_snapshot();
let fetched = stats.fetched_bytes;
let needed = state.initially_needed();
// TODO: this is too coarse.
let remaining = needed - stats.downloaded_and_checked_bytes;
let remaining = state.locked.read().get_chunks()?.get_remaining_bytes();
state
.down_speed_estimator
.add_snapshot(fetched, Some(remaining), now);
@ -265,7 +267,7 @@ impl TorrentStateLive {
error_span!(parent: state.meta.span.clone(), "peer_adder"),
state.clone().task_peer_adder(peer_queue_rx),
);
state
Ok(state)
}
pub(crate) fn spawn(
@ -494,9 +496,6 @@ impl TorrentStateLive {
pub(crate) fn file_ops(&self) -> FileOps<'_> {
FileOps::new(&self.meta.info, &self.files, &self.lengths)
}
pub fn initially_needed(&self) -> u64 {
self.initially_needed_bytes
}
pub(crate) fn lock_read(
&self,
@ -518,10 +517,6 @@ impl TorrentStateLive {
});
}
pub fn get_total_selected_bytes(&self) -> u64 {
self.total_selected_bytes
}
pub fn get_uploaded_bytes(&self) -> u64 {
self.stats.uploaded_bytes.load(Ordering::Relaxed)
}
@ -535,12 +530,11 @@ impl TorrentStateLive {
self.stats.have_bytes.load(Ordering::Relaxed)
}
pub fn is_finished(&self) -> bool {
self.get_left_to_download_bytes() == 0
}
pub fn get_left_to_download_bytes(&self) -> u64 {
self.initially_needed_bytes - self.get_downloaded_bytes()
pub fn get_hns(&self) -> Option<HaveNeededSelected> {
self.lock_read("get_hns")
.get_chunks()
.ok()
.map(|c| *c.get_hns())
}
fn maybe_transmit_haves(&self, index: ValidPieceIndex) {
@ -653,16 +647,11 @@ impl TorrentStateLive {
let files = self
.files
.iter()
.map(|f| {
let mut f = f.lock();
let dummy = dummy_file()?;
let f = std::mem::replace(&mut *f, dummy);
Ok::<_, anyhow::Error>(Arc::new(Mutex::new(f)))
})
.try_collect()?;
let filenames = self.filenames.clone();
.map(|f| f.take_clone())
.collect::<anyhow::Result<Vec<_>>>()?;
for file in files.iter() {
file.reopen(true)?;
}
let mut chunk_tracker = g
.chunks
.take()
@ -670,20 +659,12 @@ impl TorrentStateLive {
for piece_id in g.inflight_pieces.keys().copied() {
chunk_tracker.mark_piece_broken_if_not_have(piece_id);
}
let have_bytes = chunk_tracker.calc_have_bytes();
let needed_bytes = chunk_tracker.calc_needed_bytes();
// g.chunks;
Ok(TorrentStatePaused {
info: self.meta.clone(),
files,
filenames,
chunk_tracker,
hns: HaveNeededSelected {
have_bytes,
needed_bytes,
selected_bytes: self.total_selected_bytes,
},
})
}
@ -699,6 +680,92 @@ impl TorrentStateLive {
}
Err(res)
}
pub(crate) fn update_only_files(&self, only_files: &HashSet<usize>) -> anyhow::Result<()> {
let mut g = self.lock_write("update_only_files");
let ct = g.get_chunks_mut()?;
let hns = ct.update_only_files(self.files.iter().map(|f| f.len), only_files)?;
reopen_necessary_files_for_write(ct, &self.files)?;
if !hns.finished() {
self.reconnect_all_not_needed_peers();
}
Ok(())
}
pub(crate) fn is_finished(&self) -> bool {
self.get_hns().map(|h| h.finished()).unwrap_or_default()
}
fn on_piece_completed(&self, id: ValidPieceIndex) -> anyhow::Result<()> {
// if we have all the pieces of the file, reopen it read only
for (idx, opened_file) in self
.files
.iter()
.enumerate()
.skip_while(|fd| !fd.1.piece_range.contains(&id.get()))
.take_while(|fd| fd.1.piece_range.contains(&id.get()))
{
let bytes = opened_file.update_have_on_piece_completed(id.get(), &self.lengths);
if bytes == 0 {
warn!(file_id=idx, piece_id=id.get(), "bug: update_have_on_piece_completed() returned 0, although this piece is present in the file");
}
let have_all = self
.lock_read("on_piece_completed_reopen")
.get_chunks()?
.get_have_pieces()
.get(opened_file.piece_range_usize())
.with_context(|| {
format!("bug: invalid range {:?}", opened_file.piece_range_usize())
})?
.all();
if have_all {
opened_file.reopen(true)?;
}
}
if self.is_finished() {
info!("torrent finished downloading");
self.finished_notify.notify_waiters();
// There is not poing being connected to peers that have all the torrent, when
// we don't need anything from them, and they don't need anything from us.
self.disconnect_all_peers_that_have_full_torrent();
}
Ok(())
}
fn disconnect_all_peers_that_have_full_torrent(&self) {
for mut pe in self.peers.states.iter_mut() {
if let PeerState::Live(l) = pe.value().state.get() {
if l.has_full_torrent(self.lengths.total_pieces() as usize) {
let prev = pe.value_mut().state.set_not_needed(&self.peers.stats);
let _ = prev
.take_live_no_counters()
.unwrap()
.tx
.send(WriterRequest::Disconnect);
}
}
}
}
fn reconnect_all_not_needed_peers(&self) {
for pe in self.peers.states.iter() {
if let PeerState::NotNeeded = pe.value().state.get() {
if self.peer_queue_tx.send(*pe.key()).is_err() {
return;
}
}
}
}
pub(crate) fn get_file_progress(&self) -> Vec<u64> {
self.files
.iter()
.map(|fd| fd.have.load(Ordering::Relaxed))
.collect()
}
}
struct PeerHandlerLocked {
@ -1202,27 +1269,6 @@ impl PeerHandler {
self.state.peers.mark_peer_interested(self.addr, true);
}
fn reopen_read_only(&self) -> anyhow::Result<()> {
// Lock exclusive just in case to ensure in-flight operations finish.??
let _guard = self.state.lock_write("reopen_read_only");
for (file, filename) in self.state.files.iter().zip(self.state.filenames.iter()) {
let mut g = file.lock();
// this should close the original file
// putting in a block just in case to guarantee drop.
{
*g = dummy_file()?;
}
*g = std::fs::OpenOptions::new()
.read(true)
.open(filename)
.with_context(|| format!("error re-opening {:?} readonly", filename))?;
debug!("reopened {:?} read-only", filename);
}
info!("reopened all torrent files in read-only mode");
Ok(())
}
fn on_i_am_unchoked(&self) {
trace!("we are unchoked");
self.locked.write().i_am_choked = false;
@ -1398,12 +1444,7 @@ impl PeerHandler {
debug!("piece={} successfully downloaded and verified", index);
if self.state.is_finished() {
info!("torrent finished downloading");
self.state.finished_notify.notify_waiters();
self.disconnect_all_peers_that_have_full_torrent();
self.reopen_read_only()?;
}
self.state.on_piece_completed(chunk_info.piece_index)?;
self.state.maybe_transmit_haves(chunk_info.piece_index);
}
@ -1424,19 +1465,4 @@ impl PeerHandler {
.with_context(|| format!("error processing received chunk {chunk_info:?}"))?;
Ok(())
}
fn disconnect_all_peers_that_have_full_torrent(&self) {
for mut pe in self.state.peers.states.iter_mut() {
if let PeerState::Live(l) = pe.value().state.get() {
if l.has_full_torrent(self.state.lengths.total_pieces() as usize) {
let prev = pe.value_mut().state.set_not_needed(&self.state.peers.stats);
let _ = prev
.take_live_no_counters()
.unwrap()
.tx
.send(WriterRequest::Disconnect);
}
}
}
}
}

View file

@ -122,17 +122,18 @@ impl PeerStateNoMut {
}
}
pub fn queued_to_connecting(
pub fn idle_to_connecting(
&mut self,
counters: &AggregatePeerStatsAtomic,
) -> Option<(PeerRx, PeerTx)> {
if let PeerState::Queued = &self.0 {
let (tx, rx) = unbounded_channel();
let tx_2 = tx.clone();
self.set(PeerState::Connecting(tx), counters);
Some((rx, tx_2))
} else {
None
match &self.0 {
PeerState::Queued | PeerState::NotNeeded => {
let (tx, rx) = unbounded_channel();
let tx_2 = tx.clone();
self.set(PeerState::Connecting(tx), counters);
Some((rx, tx_2))
}
_ => None,
}
}

View file

@ -90,7 +90,7 @@ impl PeerStates {
let rx = self
.with_peer_mut(h, "mark_peer_connecting", |peer| {
peer.state
.queued_to_connecting(&self.stats)
.idle_to_connecting(&self.stats)
.context("invalid peer state")
})
.context("peer not found in states")??;

View file

@ -268,7 +268,7 @@ impl ManagedTorrent {
let (tx, rx) = tokio::sync::oneshot::channel();
let live =
TorrentStateLive::new(paused, tx, live_cancellation_token);
TorrentStateLive::new(paused, tx, live_cancellation_token)?;
g.state = ManagedTorrentState::Live(live.clone());
spawn_fatal_errors_receiver(&t, rx, token);
@ -289,7 +289,7 @@ impl ManagedTorrent {
ManagedTorrentState::Paused(_) => {
let paused = g.state.take().assert_paused();
let (tx, rx) = tokio::sync::oneshot::channel();
let live = TorrentStateLive::new(paused, tx, live_cancellation_token.clone());
let live = TorrentStateLive::new(paused, tx, live_cancellation_token.clone())?;
g.state = ManagedTorrentState::Live(live.clone());
spawn_fatal_errors_receiver(self, rx, live_cancellation_token);
spawn_peer_adder(&live, peer_rx);
@ -337,6 +337,7 @@ impl ManagedTorrent {
use stats::TorrentStatsState as S;
let mut resp = TorrentStats {
total_bytes: self.info().lengths.total_length(),
file_progress: Vec::new(),
state: S::Error,
error: None,
progress_bytes: 0,
@ -353,21 +354,25 @@ impl ManagedTorrent {
}
ManagedTorrentState::Paused(p) => {
resp.state = S::Paused;
resp.total_bytes = p.hns.total();
resp.progress_bytes = p.hns.progress();
resp.finished = p.hns.finished();
let hns = p.hns();
resp.total_bytes = hns.total();
resp.progress_bytes = hns.progress();
resp.finished = hns.finished();
resp.file_progress = p
.files
.iter()
.map(|f| f.have.load(Ordering::Relaxed))
.collect();
}
ManagedTorrentState::Live(l) => {
resp.state = S::Live;
let live_stats = LiveStats::from(l.as_ref());
let total = l.get_total_selected_bytes();
let remaining = l.get_left_to_download_bytes();
let progress = total - remaining;
resp.progress_bytes = progress;
resp.total_bytes = total;
resp.finished = remaining == 0;
let hns = l.get_hns().unwrap_or_default();
resp.total_bytes = hns.total();
resp.progress_bytes = hns.progress();
resp.finished = hns.finished();
resp.uploaded_bytes = l.get_uploaded_bytes();
resp.file_progress = l.get_file_progress();
resp.live = Some(live_stats);
}
ManagedTorrentState::Error(e) => {
@ -410,10 +415,7 @@ impl ManagedTorrent {
// Returns true if needed to unpause torrent.
// This is just implementation detail - it's easier to pause/unpause than to tinker with internals.
pub(crate) fn update_only_files(&self, only_files: &HashSet<usize>) -> anyhow::Result<bool> {
if only_files.is_empty() {
anyhow::bail!("you need to select at least one file");
}
pub(crate) fn update_only_files(&self, only_files: &HashSet<usize>) -> anyhow::Result<()> {
let file_count = self.info().info.iter_file_lengths()?.count();
for f in only_files.iter().copied() {
if f >= file_count {
@ -426,25 +428,20 @@ impl ManagedTorrent {
// if paused, need to update chunk tracker
let mut g = self.locked.write();
let need_to_unpause = match &mut g.state {
match &mut g.state {
ManagedTorrentState::Initializing(_) => bail!("can't update initializing torrent"),
ManagedTorrentState::Error(_) => false,
ManagedTorrentState::None => false,
ManagedTorrentState::Error(_) => {}
ManagedTorrentState::None => {}
ManagedTorrentState::Paused(p) => {
p.update_only_files(only_files)?;
false
}
ManagedTorrentState::Live(l) => {
let mut p = l.pause()?;
let e = p.update_only_files(only_files);
g.state = ManagedTorrentState::Paused(p);
e?;
true
l.update_only_files(only_files)?;
}
};
g.only_files = Some(only_files.iter().copied().collect());
Ok(need_to_unpause)
Ok(())
}
}

View file

@ -1,25 +1,26 @@
use std::{collections::HashSet, fs::File, path::PathBuf, sync::Arc};
use std::{collections::HashSet, sync::Arc};
use parking_lot::Mutex;
use crate::chunk_tracker::{ChunkTracker, HaveNeededSelected};
use crate::{
chunk_tracker::{ChunkTracker, HaveNeededSelected},
type_aliases::OpenedFiles,
};
use super::ManagedTorrentInfo;
pub struct TorrentStatePaused {
pub(crate) info: Arc<ManagedTorrentInfo>,
pub(crate) files: Vec<Arc<Mutex<File>>>,
pub(crate) filenames: Vec<PathBuf>,
pub(crate) files: OpenedFiles,
pub(crate) chunk_tracker: ChunkTracker,
pub(crate) hns: HaveNeededSelected,
}
impl TorrentStatePaused {
pub(crate) fn update_only_files(&mut self, only_files: &HashSet<usize>) -> anyhow::Result<()> {
let hns = self
.chunk_tracker
self.chunk_tracker
.update_only_files(self.info.info.iter_file_lengths()?, only_files)?;
self.hns = hns;
Ok(())
}
pub(crate) fn hns(&self) -> &HaveNeededSelected {
self.chunk_tracker.get_hns()
}
}

View file

@ -69,6 +69,7 @@ impl std::fmt::Display for TorrentStatsState {
#[derive(Serialize, Debug)]
pub struct TorrentStats {
pub state: TorrentStatsState,
pub file_progress: Vec<u64>,
pub error: Option<String>,
pub progress_bytes: u64,
pub uploaded_bytes: u64,