Fixing up initialization to allow passing in custom storages

This commit is contained in:
Igor Katson 2024-04-30 08:55:00 +01:00
parent 1b49257019
commit 42bbf84ea5
8 changed files with 128 additions and 90 deletions

View file

@ -173,10 +173,9 @@ impl Api {
{ {
AddTorrentResponse::AlreadyManaged(id, managed) => { AddTorrentResponse::AlreadyManaged(id, managed) => {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"{:?} is already managed, id={}, downloaded to {:?}", "{:?} is already managed, id={}",
managed.info_hash(), managed.info_hash(),
id, id,
&managed.info().out_dir
)) ))
.with_error_status_code(StatusCode::CONFLICT); .with_error_status_code(StatusCode::CONFLICT);
} }
@ -203,8 +202,8 @@ impl Api {
ApiAddTorrentResponse { ApiAddTorrentResponse {
id: Some(id), id: Some(id),
details, details,
output_folder: handle.info().out_dir.to_string_lossy().into_owned(),
seen_peers: None, seen_peers: None,
output_folder: "".to_owned(),
} }
} }
}; };

View file

@ -2,7 +2,7 @@ use std::path::PathBuf;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FileInfo { pub struct FileInfo {
pub 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 len: u64, pub len: u64,

View file

@ -186,7 +186,7 @@ impl<'a> FileOps<'a> {
) { ) {
debug!( debug!(
"error reading from file {} ({:?}) at {}: {:#}", "error reading from file {} ({:?}) at {}: {:#}",
current_file.index, current_file.fi.filename, pos, &err current_file.index, current_file.fi.relative_filename, pos, &err
); );
current_file.is_broken = true; current_file.is_broken = true;
some_files_broken = true; some_files_broken = true;

View file

@ -1093,8 +1093,8 @@ impl Session {
} }
(Ok(Some(paused)), true) => { (Ok(Some(paused)), true) => {
for (id, fi) in removed.info().file_infos.iter().enumerate() { for (id, fi) in removed.info().file_infos.iter().enumerate() {
if let Err(e) = paused.files.remove_file(id, &fi.filename) { if let Err(e) = paused.files.remove_file(id, &fi.relative_filename) {
warn!(?fi.filename, error=?e, "could not delete file"); warn!(?fi.relative_filename, error=?e, "could not delete file");
} }
} }
} }

View file

@ -1,14 +1,19 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::OpenOptions,
io::{Read, Seek, SeekFrom, Write}, io::{Read, Seek, SeekFrom, Write},
path::Path, path::{Path, PathBuf},
}; };
use anyhow::Context; use anyhow::Context;
use librqbit_core::lengths::{Lengths, ValidPieceIndex}; use librqbit_core::lengths::{Lengths, ValidPieceIndex};
use parking_lot::RwLock; use parking_lot::RwLock;
use crate::{opened_file::OpenedFile, type_aliases::FileInfos}; use crate::{opened_file::OpenedFile, torrent_state::ManagedTorrentInfo, type_aliases::FileInfos};
pub trait StorageFactory: Send + Sync {
fn init_storage(&self, info: &ManagedTorrentInfo) -> anyhow::Result<Box<dyn TorrentStorage>>;
}
pub trait TorrentStorage: Send + Sync { pub trait TorrentStorage: Send + Sync {
fn pread_exact(&self, file_id: usize, offset: u64, buf: &mut [u8]) -> anyhow::Result<()>; fn pread_exact(&self, file_id: usize, offset: u64, buf: &mut [u8]) -> anyhow::Result<()>;
@ -22,16 +27,54 @@ pub trait TorrentStorage: Send + Sync {
fn take(&self) -> anyhow::Result<Box<dyn TorrentStorage>>; fn take(&self) -> anyhow::Result<Box<dyn TorrentStorage>>;
} }
pub struct FilesystemStorage { pub struct FilesystemStorageFactory {
opened_files: Vec<OpenedFile>, pub output_folder: PathBuf,
pub allow_overwrite: bool,
} }
impl FilesystemStorage { impl StorageFactory for FilesystemStorageFactory {
pub fn new(opened_files: Vec<OpenedFile>) -> Self { fn init_storage(&self, meta: &ManagedTorrentInfo) -> anyhow::Result<Box<dyn TorrentStorage>> {
Self { opened_files } let mut files = Vec::<OpenedFile>::new();
for file_details in meta.info.iter_file_details(&meta.lengths)? {
let mut full_path = self.output_folder.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().context("bug: no parent")?)?;
let file = if self.allow_overwrite {
OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&full_path)
.with_context(|| format!("error opening {full_path:?} in read/write mode"))?
} else {
// 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));
}
Ok(Box::new(FilesystemStorage {
output_folder: self.output_folder.clone(),
opened_files: files,
}))
} }
} }
pub struct FilesystemStorage {
output_folder: PathBuf,
opened_files: Vec<OpenedFile>,
}
impl TorrentStorage for FilesystemStorage { impl TorrentStorage for FilesystemStorage {
fn pread_exact(&self, file_id: usize, offset: u64, buf: &mut [u8]) -> anyhow::Result<()> { fn pread_exact(&self, file_id: usize, offset: u64, buf: &mut [u8]) -> anyhow::Result<()> {
let mut g = self let mut g = self
@ -56,7 +99,7 @@ impl TorrentStorage for FilesystemStorage {
} }
fn remove_file(&self, _file_id: usize, filename: &Path) -> anyhow::Result<()> { fn remove_file(&self, _file_id: usize, filename: &Path) -> anyhow::Result<()> {
Ok(std::fs::remove_file(filename)?) Ok(std::fs::remove_file(self.output_folder.join(filename))?)
} }
fn ensure_file_length(&self, file_id: usize, len: u64) -> anyhow::Result<()> { fn ensure_file_length(&self, file_id: usize, len: u64) -> anyhow::Result<()> {
@ -64,12 +107,14 @@ impl TorrentStorage for FilesystemStorage {
} }
fn take(&self) -> anyhow::Result<Box<dyn TorrentStorage>> { fn take(&self) -> anyhow::Result<Box<dyn TorrentStorage>> {
Ok(Box::new(Self::new( Ok(Box::new(Self {
self.opened_files opened_files: self
.opened_files
.iter() .iter()
.map(|f| f.take_clone()) .map(|f| f.take_clone())
.collect::<anyhow::Result<Vec<_>>>()?, .collect::<anyhow::Result<Vec<_>>>()?,
))) output_folder: self.output_folder.clone(),
}))
} }
} }

View file

@ -1,5 +1,4 @@
use std::{ use std::{
fs::OpenOptions,
sync::{atomic::AtomicU64, Arc}, sync::{atomic::AtomicU64, Arc},
time::Instant, time::Instant,
}; };
@ -9,12 +8,7 @@ use anyhow::Context;
use size_format::SizeFormatterBinary as SF; use size_format::SizeFormatterBinary as SF;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::{ use crate::{chunk_tracker::ChunkTracker, file_ops::FileOps, storage::StorageFactory};
chunk_tracker::ChunkTracker,
file_ops::FileOps,
opened_file::OpenedFile,
storage::{FilesystemStorage, InMemoryGarbageCollectingStorage, TorrentStorage},
};
use super::{paused::TorrentStatePaused, ManagedTorrentInfo}; use super::{paused::TorrentStatePaused, ManagedTorrentInfo};
@ -38,56 +32,11 @@ impl TorrentStateInitializing {
.load(std::sync::atomic::Ordering::Relaxed) .load(std::sync::atomic::Ordering::Relaxed)
} }
pub async fn check(&self) -> anyhow::Result<TorrentStatePaused> { pub async fn check(
// Return in-memory store &self,
let store = storage_factory: &dyn StorageFactory,
InMemoryGarbageCollectingStorage::new(self.meta.lengths, self.meta.file_infos.clone())?; ) -> anyhow::Result<TorrentStatePaused> {
let ct = ChunkTracker::new_empty(self.meta.lengths, &self.meta.file_infos)?; let files = storage_factory.init_storage(&self.meta)?;
Ok(TorrentStatePaused {
info: self.meta.clone(),
files: Box::new(store),
chunk_tracker: ct,
streams: Arc::new(Default::default()),
})
// self.check_disk().await
}
pub async fn check_disk(&self) -> anyhow::Result<TorrentStatePaused> {
let mut files = Vec::<OpenedFile>::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().context("bug: no parent")?)?;
let file = if self.meta.options.overwrite {
OpenOptions::new()
.create(true)
.truncate(false)
.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));
}
let files: Box<dyn TorrentStorage> = Box::new(FilesystemStorage::new(files));
debug!("computed lengths: {:?}", &self.meta.lengths);
info!("Doing initial checksum validation, this might take a while..."); info!("Doing initial checksum validation, this might take a while...");
let initial_check_results = self.meta.spawner.spawn_block_in_place(|| { let initial_check_results = self.meta.spawner.spawn_block_in_place(|| {
FileOps::new( FileOps::new(
@ -119,12 +68,12 @@ impl TorrentStateInitializing {
if let Err(err) = files.ensure_file_length(idx, fi.len) { if let Err(err) = files.ensure_file_length(idx, fi.len) {
warn!( warn!(
"Error setting length for file {:?} to {}: {:#?}", "Error setting length for file {:?} to {}: {:#?}",
fi.filename, fi.len, err fi.relative_filename, fi.len, err
); );
} else { } else {
debug!( debug!(
"Set length for file {:?} to {} in {:?}", "Set length for file {:?} to {} in {:?}",
fi.filename, fi.relative_filename,
SF::new(fi.len), SF::new(fi.len),
now.elapsed() now.elapsed()
); );

View file

@ -203,7 +203,7 @@ impl TorrentStateLive {
.info .info
.file_infos .file_infos
.get(*id) .get(*id)
.map(|fi| fi.filename.as_path()) .map(|fi| fi.relative_filename.as_path())
}); });
pri pri
}; };

View file

@ -36,6 +36,8 @@ use tracing::warn;
use crate::chunk_tracker::ChunkTracker; use crate::chunk_tracker::ChunkTracker;
use crate::file_info::FileInfo; use crate::file_info::FileInfo;
use crate::spawn_utils::BlockingSpawner; use crate::spawn_utils::BlockingSpawner;
use crate::storage::FilesystemStorageFactory;
use crate::storage::StorageFactory;
use crate::torrent_state::stats::LiveStats; use crate::torrent_state::stats::LiveStats;
use crate::type_aliases::FileInfos; use crate::type_aliases::FileInfos;
use crate::type_aliases::PeerStream; use crate::type_aliases::PeerStream;
@ -89,13 +91,11 @@ pub(crate) struct ManagedTorrentOptions {
pub force_tracker_interval: Option<Duration>, pub force_tracker_interval: Option<Duration>,
pub peer_connect_timeout: Option<Duration>, pub peer_connect_timeout: Option<Duration>,
pub peer_read_write_timeout: Option<Duration>, pub peer_read_write_timeout: Option<Duration>,
pub overwrite: bool,
} }
pub struct ManagedTorrentInfo { pub struct ManagedTorrentInfo {
pub info: TorrentMetaV1Info<ByteBufOwned>, pub info: TorrentMetaV1Info<ByteBufOwned>,
pub info_hash: Id20, pub info_hash: Id20,
pub out_dir: PathBuf,
pub(crate) spawner: BlockingSpawner, pub(crate) spawner: BlockingSpawner,
pub trackers: HashSet<String>, pub trackers: HashSet<String>,
pub peer_id: Id20, pub peer_id: Id20,
@ -107,6 +107,7 @@ pub struct ManagedTorrentInfo {
pub struct ManagedTorrent { pub struct ManagedTorrent {
pub info: Arc<ManagedTorrentInfo>, pub info: Arc<ManagedTorrentInfo>,
storage_factory: Box<dyn StorageFactory>,
locked: RwLock<ManagedTorrentLocked>, locked: RwLock<ManagedTorrentLocked>,
} }
@ -267,7 +268,7 @@ impl ManagedTorrent {
error_span!(parent: span.clone(), "initialize_and_start"), error_span!(parent: span.clone(), "initialize_and_start"),
token.clone(), token.clone(),
async move { async move {
match init.check().await { match init.check(&*self.storage_factory).await {
Ok(paused) => { Ok(paused) => {
let mut g = t.locked.write(); let mut g = t.locked.write();
if let ManagedTorrentState::Initializing(_) = &g.state { if let ManagedTorrentState::Initializing(_) = &g.state {
@ -461,18 +462,42 @@ impl ManagedTorrent {
} }
} }
enum ManagedTorrentBuilderStorage {
Filesystem {
overwrite: bool,
output_folder: PathBuf,
},
Custom(Box<dyn StorageFactory>),
}
impl ManagedTorrentBuilderStorage {
fn build(self) -> anyhow::Result<Box<dyn StorageFactory>> {
let s = match self {
ManagedTorrentBuilderStorage::Filesystem {
overwrite,
output_folder,
} => Box::new(FilesystemStorageFactory {
output_folder,
allow_overwrite: overwrite,
}),
ManagedTorrentBuilderStorage::Custom(s) => s,
};
Ok(s)
}
}
pub struct ManagedTorrentBuilder { pub struct ManagedTorrentBuilder {
info: TorrentMetaV1Info<ByteBufOwned>, info: TorrentMetaV1Info<ByteBufOwned>,
info_hash: Id20, info_hash: Id20,
output_folder: PathBuf,
force_tracker_interval: Option<Duration>, force_tracker_interval: Option<Duration>,
peer_connect_timeout: Option<Duration>, peer_connect_timeout: Option<Duration>,
peer_read_write_timeout: Option<Duration>, peer_read_write_timeout: Option<Duration>,
only_files: Option<Vec<usize>>, only_files: Option<Vec<usize>>,
trackers: Vec<String>, trackers: Vec<String>,
peer_id: Option<Id20>, peer_id: Option<Id20>,
overwrite: bool,
spawner: Option<BlockingSpawner>, spawner: Option<BlockingSpawner>,
deferred_build_errors: Vec<String>,
storage: Option<ManagedTorrentBuilderStorage>,
} }
impl ManagedTorrentBuilder { impl ManagedTorrentBuilder {
@ -484,15 +509,19 @@ impl ManagedTorrentBuilder {
Self { Self {
info, info,
info_hash, info_hash,
output_folder: output_folder.as_ref().into(),
spawner: None, spawner: None,
force_tracker_interval: None, force_tracker_interval: None,
peer_connect_timeout: None, peer_connect_timeout: None,
peer_read_write_timeout: None, peer_read_write_timeout: None,
only_files: None, only_files: None,
deferred_build_errors: Default::default(),
trackers: Default::default(), trackers: Default::default(),
peer_id: None, peer_id: None,
overwrite: false, // default is filesystem to keep the old API unchanged for now
storage: Some(ManagedTorrentBuilderStorage::Filesystem {
overwrite: false,
output_folder: output_folder.as_ref().to_owned(),
}),
} }
} }
@ -506,8 +535,15 @@ impl ManagedTorrentBuilder {
self self
} }
pub fn overwrite(&mut self, overwrite: bool) -> &mut Self { pub fn overwrite(&mut self, new_overwrite: bool) -> &mut Self {
self.overwrite = overwrite; match self.storage.as_mut() {
Some(ManagedTorrentBuilderStorage::Filesystem { overwrite, .. }) => {
*overwrite = new_overwrite
}
_ => self
.deferred_build_errors
.push("overwrite() called when storage factory was not filesystem".to_owned()),
}
self self
} }
@ -537,25 +573,33 @@ impl ManagedTorrentBuilder {
} }
pub(crate) fn build(self, span: tracing::Span) -> anyhow::Result<ManagedTorrentHandle> { pub(crate) fn build(self, span: tracing::Span) -> anyhow::Result<ManagedTorrentHandle> {
if !self.deferred_build_errors.is_empty() {
anyhow::bail!("Errors: {}", self.deferred_build_errors.join(";"))
}
let lengths = Lengths::from_torrent(&self.info)?; let lengths = Lengths::from_torrent(&self.info)?;
let file_infos = self let file_infos = self
.info .info
.iter_file_details(&lengths)? .iter_file_details(&lengths)?
.map(|fd| { .map(|fd| {
Ok::<_, anyhow::Error>(FileInfo { Ok::<_, anyhow::Error>(FileInfo {
filename: self.output_folder.join(fd.filename.to_pathbuf()?), relative_filename: fd.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.len,
}) })
}) })
.collect::<anyhow::Result<Vec<FileInfo>>>()?; .collect::<anyhow::Result<Vec<FileInfo>>>()?;
let storage_factory = self
.storage
.context("by the time build() is called you must set storage factory")?
.build()?;
let info = Arc::new(ManagedTorrentInfo { let info = Arc::new(ManagedTorrentInfo {
span, span,
file_infos, file_infos,
info: self.info, info: self.info,
info_hash: self.info_hash, info_hash: self.info_hash,
out_dir: self.output_folder,
trackers: self.trackers.into_iter().collect(), trackers: self.trackers.into_iter().collect(),
spawner: self.spawner.unwrap_or_default(), spawner: self.spawner.unwrap_or_default(),
peer_id: self.peer_id.unwrap_or_else(generate_peer_id), peer_id: self.peer_id.unwrap_or_else(generate_peer_id),
@ -564,9 +608,9 @@ impl ManagedTorrentBuilder {
force_tracker_interval: self.force_tracker_interval, force_tracker_interval: self.force_tracker_interval,
peer_connect_timeout: self.peer_connect_timeout, peer_connect_timeout: self.peer_connect_timeout,
peer_read_write_timeout: self.peer_read_write_timeout, peer_read_write_timeout: self.peer_read_write_timeout,
overwrite: self.overwrite,
}, },
}); });
let initializing = Arc::new(TorrentStateInitializing::new( let initializing = Arc::new(TorrentStateInitializing::new(
info.clone(), info.clone(),
self.only_files.clone(), self.only_files.clone(),
@ -576,6 +620,7 @@ impl ManagedTorrentBuilder {
state: ManagedTorrentState::Initializing(initializing), state: ManagedTorrentState::Initializing(initializing),
only_files: self.only_files, only_files: self.only_files,
}), }),
storage_factory,
info, info,
})) }))
} }