diff --git a/server/service/src/main.rs b/server/service/src/main.rs index 1e36640..8fb7ec1 100644 --- a/server/service/src/main.rs +++ b/server/service/src/main.rs @@ -24,6 +24,33 @@ use serde_json::Value; const DEFAULT_SIZE: usize = 25; +#[derive(Copy, Clone, Debug, Deserialize)] +enum SortKey { + Name, + Size, + Seeds, + Leeches, + Scraped, +} + +impl Default for SortKey { + fn default() -> Self { + Self::Scraped + } +} + +#[derive(Copy, Clone, Debug, Deserialize)] +enum SortDirection { + Asc, + Desc, +} + +impl Default for SortDirection { + fn default() -> Self { + Self::Desc + } +} + #[actix_rt::main] async fn main() -> io::Result<()> { println!("Access me at {}", endpoint()); @@ -69,6 +96,8 @@ struct SearchQuery { q: String, page: Option, size: Option, + sort_key: Option, + sort_dir: Option, type_: Option, } @@ -101,7 +130,6 @@ async fn new_torrents( db: web::Data>, query: web::Query, ) -> Result { - let res = web::block(move || { let conn = db.get().unwrap(); new_query(query, conn) @@ -126,20 +154,22 @@ fn search_query( } let page = query.page.unwrap_or(1); + let sort_key = query.sort_key.unwrap_or_default(); + let sort_dir = query.sort_dir.unwrap_or_default(); let size = cmp::min(100, query.size.unwrap_or(DEFAULT_SIZE)); let type_ = query.type_.as_ref().map_or("torrent", String::deref); let offset = size * (page - 1); println!( - "query = {}, type = {}, page = {}, size = {}", - q, type_, page, size + "query = {}, type = {}, page = {}, size = {}, sort_key = {:?}, sort_dir = {:?}", + q, type_, page, size, sort_key, sort_dir ); let res = if type_ == "file" { - let results = torrent_file_search(conn, q, size, offset)?; + let results = torrent_file_search(conn, q, sort_key, sort_dir, size, offset)?; serde_json::to_value(&results).unwrap() } else { - let results = torrent_search(conn, q, size, offset)?; + let results = torrent_search(conn, q, sort_key, sort_dir, size, offset)?; serde_json::to_value(&results).unwrap() }; @@ -187,10 +217,14 @@ struct Torrent { fn torrent_search( conn: r2d2::PooledConnection, query: &str, + sort_key: SortKey, + sort_dir: SortDirection, size: usize, offset: usize, ) -> Result, Error> { - let stmt_str = "select * from torrents where name like '%' || ?1 || '%' limit ?2, ?3"; + let order_clause = build_order_clause("torrent", sort_key, sort_dir); + // `order_clause` is already sanitized and should not be escaped: + let stmt_str = format!("select * from torrents where name like '%' || ?1 || '%' order by {} limit ?2, ?3", order_clause); let mut stmt = conn.prepare(&stmt_str)?; let torrent_iter = stmt.query_map( params![ @@ -270,10 +304,14 @@ struct File { fn torrent_file_search( conn: r2d2::PooledConnection, query: &str, + sort_key: SortKey, + sort_dir: SortDirection, size: usize, offset: usize, ) -> Result, Error> { - let stmt_str = "select * from files where path like '%' || ?1 || '%' limit ?2, ?3"; + let order_clause = build_order_clause("file", sort_key, sort_dir); + // `order_clause` is already sanitized and should not be escaped: + let stmt_str = format!("select * from files where path like '%' || ?1 || '%' order by {} limit ?2, ?3", order_clause); let mut stmt = conn.prepare(&stmt_str).unwrap(); let file_iter = stmt.query_map( params![ @@ -337,6 +375,29 @@ fn torrent_file_new( Ok(files) } +fn build_order_clause(type_: &str, sort_key: SortKey, sort_dir: SortDirection) -> String { + let dir = match sort_dir { + SortDirection::Asc => "asc", + SortDirection::Desc => "desc", + }; + + let column = match sort_key { + SortKey::Name => { + if type_ == "file" { + "path" + } else { + "name" + } + }, + SortKey::Size => "size_bytes", + SortKey::Seeds => "seeders", + SortKey::Leeches => "leechers", + SortKey::Scraped => "scraped_date", + }; + + format!("{} {}", column, dir) +} + #[cfg(test)] mod tests { use r2d2_sqlite::SqliteConnectionManager; @@ -346,7 +407,7 @@ mod tests { let manager = SqliteConnectionManager::file(super::torrents_db_file()); let pool = r2d2::Pool::builder().max_size(15).build(manager).unwrap(); let conn = pool.get().unwrap(); - let results = super::torrent_search(conn, "sherlock", 10, 0); + let results = super::torrent_search(conn, "sherlock", super::SortKey::Name, super::SortDirection::Desc, 10, 0); assert!(results.unwrap().len() > 2); // println!("Query took {:?} seconds.", end - start); } diff --git a/server/ui/src/Main.css b/server/ui/src/Main.css index a381704..8e0fc7f 100644 --- a/server/ui/src/Main.css +++ b/server/ui/src/Main.css @@ -79,6 +79,13 @@ a::-moz-focus-inner { align-self: center; } +th .icon { + color: #666666; + position: relative; + top: 0.125em; + left: 0.125em; +} + .spinner { animation: spin 2s linear infinite; width: 20vw; diff --git a/server/ui/src/components/navbar.tsx b/server/ui/src/components/navbar.tsx index 3ee9d03..95139ba 100644 --- a/server/ui/src/components/navbar.tsx +++ b/server/ui/src/components/navbar.tsx @@ -8,11 +8,12 @@ interface State { } export class Navbar extends Component { - state: State = { searchParams: { page: 1, q: "", + sort_key: 'Name', + sort_dir: 'Desc', type_: "torrent" } } @@ -52,7 +53,7 @@ export class Navbar extends Component { value={this.state.searchParams.q} onInput={linkEvent(this, this.searchChange)}>
-