1use 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 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 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}