1use chrono::{DateTime, Utc};
7use librqbit_core::Id20;
8use std::{
9 fs,
10 future::Future,
11 io::Error,
12 path::{Path, PathBuf},
13 str::FromStr,
14 time::SystemTime,
15};
16
17#[derive(Clone, Debug, Default)]
18pub enum Sort {
19 #[default]
20 Modified,
21}
22
23#[derive(Clone, Debug, Default)]
24pub enum Order {
25 #[default]
26 Asc,
27 Desc,
28}
29
30pub struct Torrent {
31 pub bytes: Vec<u8>,
32 pub time: DateTime<Utc>,
33}
34
35pub struct Storage {
36 default_capacity: usize,
37 pub default_limit: usize,
38 root: PathBuf,
39}
40
41impl Storage {
42 pub fn init(
45 root: &Path,
46 default_limit: usize,
47 default_capacity: usize,
48 ) -> Result<Self, String> {
49 if !root.is_dir() {
50 return Err("Public root is not directory".into());
51 }
52 Ok(Self {
53 default_capacity,
54 default_limit,
55 root: root.canonicalize().map_err(|e| e.to_string())?,
56 })
57 }
58
59 pub fn torrent(&self, info_hash: Id20) -> Option<Torrent> {
62 let mut p = PathBuf::from(&self.root);
63 p.push(format!("{}.{E}", info_hash.as_string()));
64 Some(Torrent {
65 bytes: fs::read(&p).ok()?,
66 time: p.metadata().ok()?.modified().ok()?.into(),
67 })
68 }
69
70 pub async fn torrents<F, Fut>(
71 &self,
72 keyword: Option<&str>,
73 sort_order: Option<(Sort, Order)>,
74 start: Option<usize>,
75 limit: Option<usize>,
76 visibility_filter: F,
77 ) -> Result<Torrents, Error>
78 where
79 F: Fn(Id20) -> Fut,
80 Fut: Future<Output = bool>,
81 {
82 let f = self.files(keyword, sort_order)?;
83 let t = f.len(); let l = limit.unwrap_or(t);
85 let s = start.unwrap_or_default();
86 let mut b = Vec::with_capacity(l);
87 let mut i = 0; for file in f.iter() {
89 if let Some(n) = file.path.file_stem()
90 && let Ok(id20) = Id20::from_str(&n.to_string_lossy())
91 && visibility_filter(id20).await
92 {
93 if i >= s && b.len() < l {
94 b.push(Torrent {
95 bytes: fs::read(&file.path)?,
96 time: file.modified.into(),
97 });
98 }
99 i += 1;
100 }
101 }
102 Ok(Torrents {
103 total: t,
104 visible: i,
105 list: b,
106 })
107 }
108
109 pub fn href(&self, info_hash: &str, path: &str) -> Option<String> {
113 let mut relative = PathBuf::from(info_hash);
114 relative.push(path);
115
116 let mut absolute = PathBuf::from(&self.root);
117 absolute.push(&relative);
118
119 let c = absolute.canonicalize().ok()?;
120 if c.starts_with(&self.root) && c.exists() {
121 Some(relative.to_string_lossy().into())
122 } else {
123 None
124 }
125 }
126
127 pub fn filepath(&self, relative: &str) -> Option<PathBuf> {
131 let mut p = PathBuf::from(&self.root);
132 p.push(relative);
133
134 let c = p.canonicalize().ok()?;
135 if c.starts_with(&self.root) && c.is_file() {
136 Some(c)
137 } else {
138 None
139 }
140 }
141
142 fn files(
145 &self,
146 keyword: Option<&str>,
147 sort_order: Option<(Sort, Order)>,
148 ) -> Result<Vec<File>, Error> {
149 let mut files = Vec::with_capacity(self.default_capacity);
150 for dir_entry in fs::read_dir(&self.root)? {
151 let entry = dir_entry?;
152 let path = entry.path();
153 if !path.is_file() || path.extension().is_none_or(|e| e != E) {
154 continue;
155 }
156 if let Some(k) = keyword
157 && !k.trim_matches(S).is_empty()
158 && !librqbit_core::torrent_metainfo::torrent_from_bytes(&fs::read(&path)?)
159 .is_ok_and(|m: librqbit_core::torrent_metainfo::TorrentMetaV1Owned| {
160 k.split(S)
161 .filter(|s| !s.is_empty())
162 .map(|s| s.trim().to_lowercase())
163 .all(|q| {
164 m.info_hash.as_string().to_lowercase().contains(&q)
165 || m.info
166 .name
167 .as_ref()
168 .is_some_and(|n| n.to_string().to_lowercase().contains(&q))
169 || m.comment
170 .as_ref()
171 .is_some_and(|c| c.to_string().to_lowercase().contains(&q))
172 || m.created_by
173 .as_ref()
174 .is_some_and(|c| c.to_string().to_lowercase().contains(&q))
175 || m.publisher
176 .as_ref()
177 .is_some_and(|p| p.to_string().to_lowercase().contains(&q))
178 || m.publisher_url
179 .as_ref()
180 .is_some_and(|u| u.to_string().to_lowercase().contains(&q))
181 || m.announce
182 .as_ref()
183 .is_some_and(|a| a.to_string().to_lowercase().contains(&q))
184 || m.announce_list.iter().any(|l| {
185 l.iter().any(|a| a.to_string().to_lowercase().contains(&q))
186 })
187 || m.info.files.as_ref().is_some_and(|f| {
188 f.iter().any(|f| {
189 let mut p = PathBuf::new();
190 f.full_path(&mut p).is_ok_and(|_| {
191 p.to_string_lossy().to_lowercase().contains(&q)
192 })
193 })
194 })
195 })
196 })
197 {
198 continue;
199 }
200 files.push(File {
201 modified: entry.metadata()?.modified()?,
202 path,
203 })
204 }
205 if let Some((sort, order)) = sort_order {
206 match sort {
207 Sort::Modified => match order {
208 Order::Asc => files.sort_by_key(|a| a.modified),
209 Order::Desc => files.sort_by_key(|b| std::cmp::Reverse(b.modified)),
210 },
211 }
212 }
213 Ok(files)
214 }
215}
216
217const E: &str = "torrent";
221
222const S: &[char] = &[
224 '_', '-', ':', ';', ',', '(', ')', '[', ']', '/', '!', '?', ' ', ];
226
227struct File {
228 modified: SystemTime,
229 path: PathBuf,
230}
231
232pub struct Torrents {
233 pub total: usize,
234 pub visible: usize,
235 pub list: Vec<Torrent>,
236}