Compare commits

...

3 commits

Author SHA1 Message Date
d1cf5d3038 Implement folder sorting 2025-04-20 08:48:33 -05:00
6eb900b814 Move options up 2025-04-20 08:22:44 -05:00
c56d51707a Fix login again 2025-04-20 08:22:29 -05:00
6 changed files with 58 additions and 15 deletions

View file

@ -1,7 +1,9 @@
use std::cmp::Ordering;
use std::fs::File; use std::fs::File;
use std::io::BufReader; use std::io::BufReader;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::Error; use anyhow::{anyhow, Error};
use log::trace;
use rocket::response::stream::ReaderStream; use rocket::response::stream::ReaderStream;
use rocket::serde::Serialize; use rocket::serde::Serialize;
use tokio::io::BufStream; use tokio::io::BufStream;
@ -17,6 +19,21 @@ pub struct Library {
repo: RepoContainer, repo: RepoContainer,
} }
/// The direction of sort and the field to sort by
#[derive(PartialEq, Debug)]
pub struct ListOptions {
pub sort_field: Option<String>,
pub sort_descending: Option<bool>
}
impl Default for ListOptions {
fn default() -> Self {
ListOptions {
sort_field: Some("name".to_string()),
sort_descending: Some(false),
}
}
}
impl Library { impl Library {
pub fn new(library_model: LibraryModel, repo: RepoContainer) -> Library { pub fn new(library_model: LibraryModel, repo: RepoContainer) -> Library {
Library { Library {
@ -49,9 +66,28 @@ impl Library {
repo.backend.read_file(&self.model.id.to_string(), rel_path) repo.backend.read_file(&self.model.id.to_string(), rel_path)
} }
pub async fn list_files(&self, rel_path: &PathBuf) -> Result<Vec<FileEntry>, anyhow::Error> { pub async fn list_files(&self, rel_path: &PathBuf, options: ListOptions) -> Result<Vec<FileEntry>, anyhow::Error> {
let repo = self.repo.read().await; let repo = self.repo.read().await;
repo.backend.list_files(&self.model.id.to_string(), rel_path) let mut list = repo.backend.list_files(&self.model.id.to_string(), rel_path)?;
let field = options.sort_field.unwrap_or("name".to_string());
let descending = options.sort_descending.unwrap_or(false);
match field.as_str() {
"name" => list.sort_by(|a, b| {
if a._type == FileType::File && b._type != FileType::File { Ordering::Greater }
else if a._type != FileType::File && b._type == FileType::File { Ordering::Less }
else { a.path.cmp(&b.path) }
}),
"size" => list.sort_by(|a, b| {
if a._type == FileType::File && b._type != FileType::File { Ordering::Greater }
else if a._type != FileType::File && b._type == FileType::File { Ordering::Less }
else { a.size.cmp(&b.size) }
}),
_ => return Err(anyhow!("Unsupported field"))
}
if descending {
list.reverse();
}
Ok(list)
} }
pub async fn delete_file(&self, rel_path: &PathBuf) -> Result<(), anyhow::Error> { pub async fn delete_file(&self, rel_path: &PathBuf) -> Result<(), anyhow::Error> {

View file

@ -17,6 +17,7 @@ use crate::managers::libraries::LibraryManager;
use crate::managers::repos::RepoManager; use crate::managers::repos::RepoManager;
use crate::models::library::{LibraryModel, LibraryWithRepoModel}; use crate::models::library::{LibraryModel, LibraryWithRepoModel};
use crate::models::user; use crate::models::user;
use crate::objs::library::ListOptions;
use crate::storage::{FileEntry, FileType}; use crate::storage::{FileEntry, FileType};
use crate::util::{JsonErrorResponse, ResponseError}; use crate::util::{JsonErrorResponse, ResponseError};
#[get("/<library_id>")] #[get("/<library_id>")]
@ -30,7 +31,7 @@ pub(crate) async fn get_file(pool: &State<DB>, library_id: &str) -> Result<Optio
pub(crate) async fn list_files(libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: &str) -> Result<Json<Vec<FileEntry>>, ResponseError> { pub(crate) async fn list_files(libraries: &State<Arc<Mutex<LibraryManager>>>, library_id: &str, path: &str) -> Result<Json<Vec<FileEntry>>, ResponseError> {
let libs = libraries.lock().await; let libs = libraries.lock().await;
let library = libs.get(library_id).await?; let library = libs.get(library_id).await?;
library.list_files(&PathBuf::from(path)).await library.list_files(&PathBuf::from(path), ListOptions::default()).await
.map(|files| Json(files)) .map(|files| Json(files))
.map_err(|e| ResponseError::InternalServerError(JsonErrorResponse { .map_err(|e| ResponseError::InternalServerError(JsonErrorResponse {
code: "STORAGE_ERROR".to_string(), code: "STORAGE_ERROR".to_string(),

View file

@ -90,7 +90,7 @@ pub async fn handler(
let ctx = context! { let ctx = context! {
route: route.uri.path(), route: route.uri.path(),
csrf_token: csrf_token, csrf_token: csrf_token,
form: &Context::default(), form: &form.context,
return_to, return_to,
meta: meta.inner() meta: meta.inner()
}; };

View file

@ -17,6 +17,7 @@ use tokio::sync::Mutex;
use crate::consts::FILE_CONSTANTS; use crate::consts::FILE_CONSTANTS;
use crate::guards::{AuthUser}; use crate::guards::{AuthUser};
use crate::managers::libraries::LibraryManager; use crate::managers::libraries::LibraryManager;
use crate::objs::library::ListOptions;
use crate::routes::ui::auth; use crate::routes::ui::auth;
use crate::util::{JsonErrorResponse, ResponseError}; use crate::util::{JsonErrorResponse, ResponseError};
@ -46,9 +47,20 @@ pub async fn list_library_files(
sort_dir: Option<String>, sort_dir: Option<String>,
display: Option<String>, display: Option<String>,
) -> Result<Template, ResponseError> { ) -> Result<Template, ResponseError> {
let options = FileDisplayOptions {
// TODO: prevent bad values
// TODO: fix login errror msg -------_____------
sort_key: validate_option(sort_key, FILE_CONSTANTS.sort_keys, "name"),
sort_dir: validate_option(sort_dir, &["asc", "desc"], "asc"),
display: validate_option(display, FILE_CONSTANTS.display_options, "list"),
};
let libs = libraries.lock().await; let libs = libraries.lock().await;
let library = libs.get(library_id).await?; let library = libs.get(library_id).await?;
let files = library.list_files(&PathBuf::from(&path)).await let list_options = ListOptions {
sort_field: Some(options.sort_key.clone()),
sort_descending: Some(options.sort_dir == "desc"),
};
let files = library.list_files(&PathBuf::from(&path), list_options).await
.map_err(|e| ResponseError::InternalServerError(JsonErrorResponse { .map_err(|e| ResponseError::InternalServerError(JsonErrorResponse {
code: "STORAGE_ERROR".to_string(), code: "STORAGE_ERROR".to_string(),
message: e.to_string(), message: e.to_string(),
@ -82,13 +94,7 @@ pub async fn list_library_files(
parent, parent,
path_segments: segments, path_segments: segments,
// TODO: have struct? // TODO: have struct?
options: FileDisplayOptions { options,
// TODO: prevent bad values
// TODO: fix login errror msg -------_____------
sort_key: validate_option(sort_key, FILE_CONSTANTS.sort_keys, "name"),
sort_dir: validate_option(sort_dir, &["asc", "desc"], "asc"),
display: validate_option(display, FILE_CONSTANTS.display_options, "list"),
},
DATA: FILE_CONSTANTS DATA: FILE_CONSTANTS
})) }))
} }

View file

@ -19,7 +19,7 @@ pub enum StorageBackendMap {
S3(S3Storage) S3(S3Storage)
} }
#[derive(Debug, Serialize, Deserialize, FromFormField)] #[derive(Debug, Serialize, Deserialize, FromFormField, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum FileType { pub enum FileType {
File, File,

View file

@ -24,7 +24,7 @@
<div class="field"> <div class="field">
<label class="label">Username / Email</label> <label class="label">Username / Email</label>
<div class="control has-icons-left"> <div class="control has-icons-left">
<input required name="username" class="input {{#if errors.username}}is-danger{{/if}}" type="text" placeholder="Username or Email"> <input autofocus required name="username" class="input {{#if errors.username}}is-danger{{/if}}" type="text" placeholder="Username or Email">
<span class="icon is-small is-left"> <span class="icon is-small is-left">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
</span> </span>