Skip to main content

torq_core/
library.rs

1//! Cross-seed library index: `.torrent` files on disk whose data is already
2//! downloaded. Re-adding a matching infohash points librqbit at the existing
3//! files (`output_folder` = torrent file's parent dir), so the piece check
4//! finds them instead of re-downloading.
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use anyhow::{Context, Result};
11use buffers::ByteBufOwned;
12use librqbit_core::torrent_metainfo::torrent_from_bytes;
13use parking_lot::Mutex;
14use tracing::{debug, info};
15
16#[derive(Debug, Clone)]
17pub struct LibraryEntry {
18    pub torrent_path: PathBuf,
19    pub data_dir: PathBuf,
20    pub total_bytes: u64,
21}
22
23pub struct Library {
24    dirs: Vec<PathBuf>,
25    index: Mutex<HashMap<String, LibraryEntry>>,
26}
27
28impl Library {
29    pub fn new(dirs: Vec<PathBuf>) -> Arc<Self> {
30        Arc::new(Self {
31            dirs,
32            index: Mutex::new(HashMap::new()),
33        })
34    }
35
36    /// Rebuild the index from `dirs`; returns the number of torrents found.
37    pub fn scan(&self) -> Result<usize> {
38        let mut index = HashMap::new();
39        for dir in &self.dirs {
40            walk(&mut index, dir)?;
41        }
42        let n = index.len();
43        *self.index.lock() = index;
44        info!(count = n, "library scan complete");
45        Ok(n)
46    }
47
48    pub fn lookup(&self, hash: &str) -> Option<LibraryEntry> {
49        self.index.lock().get(hash).cloned()
50    }
51
52    pub fn count(&self) -> usize {
53        self.index.lock().len()
54    }
55
56    pub fn dirs(&self) -> Vec<PathBuf> {
57        self.dirs.clone()
58    }
59}
60
61fn walk(index: &mut HashMap<String, LibraryEntry>, dir: &Path) -> Result<()> {
62    for entry in std::fs::read_dir(dir).with_context(|| format!("reading {}", dir.display()))? {
63        let entry = entry?;
64        let path = entry.path();
65        if path.is_dir() {
66            walk(index, &path)?;
67        } else if path.extension().and_then(|e| e.to_str()) == Some("torrent")
68            && let Err(e) = index_torrent(index, &path)
69        {
70            debug!(path = %path.display(), "skipping torrent: {e:#}");
71        }
72    }
73    Ok(())
74}
75
76fn index_torrent(index: &mut HashMap<String, LibraryEntry>, path: &Path) -> Result<()> {
77    let bytes = std::fs::read(path)?;
78    let meta = torrent_from_bytes::<ByteBufOwned>(&bytes).context("parsing")?;
79    let hash = meta.info_hash.as_string();
80    let total: u64 = meta.info.iter_file_lengths()?.sum();
81    let data_dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
82    index.insert(
83        hash,
84        LibraryEntry {
85            torrent_path: path.to_path_buf(),
86            data_dir,
87            total_bytes: total,
88        },
89    );
90    Ok(())
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::io::Write;
97
98    /// Minimal single-file torrent: info = {length, name, piece length, pieces}.
99    fn bencode_torrent() -> Vec<u8> {
100        let mut info = b"d6:lengthi1000e4:name8:data.bin12:piece lengthi16384e6:pieces20:".to_vec();
101        info.extend(std::iter::repeat_n(0u8, 20));
102        info.push(b'e');
103        let mut out = b"d8:announce12:https://x.io4:info".to_vec();
104        out.extend_from_slice(&info);
105        out.push(b'e');
106        out
107    }
108
109    #[test]
110    fn scan_indexes_and_lookup_returns_entry() {
111        let dir = std::env::temp_dir().join(format!("torq-lib-test-{}", std::process::id()));
112        std::fs::create_dir_all(&dir).unwrap();
113        let mut f = std::fs::File::create(dir.join("t.torrent")).unwrap();
114        f.write_all(&bencode_torrent()).unwrap();
115
116        let lib = Library::new(vec![dir.clone()]);
117        assert_eq!(lib.scan().unwrap(), 1);
118        let raw = bencode_torrent();
119        let parsed = torrent_from_bytes::<ByteBufOwned>(&raw).unwrap();
120        let entry = lib.lookup(&parsed.info_hash.as_string()).unwrap();
121        assert_eq!(entry.data_dir, dir);
122        assert_eq!(entry.total_bytes, 1000);
123        assert!(entry.torrent_path.ends_with("t.torrent"));
124
125        std::fs::remove_dir_all(&dir).ok();
126    }
127}