basic selection + download multiple files with selection

This commit is contained in:
2025-09-02 18:08:13 +02:00
parent be46f92ddf
commit e2085c1baa
22 changed files with 516 additions and 156 deletions

View File

@@ -1,29 +1,29 @@
use thiserror::Error; use thiserror::Error;
use crate::domain::warren::models::file::AbsoluteFilePath; use super::AbsoluteFilePathList;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CatRequest { pub struct CatRequest {
path: AbsoluteFilePath, paths: AbsoluteFilePathList,
} }
impl CatRequest { impl CatRequest {
pub fn new(path: AbsoluteFilePath) -> Self { pub fn new(paths: AbsoluteFilePathList) -> Self {
Self { path } Self { paths }
} }
pub fn path(&self) -> &AbsoluteFilePath { pub fn paths(&self) -> &AbsoluteFilePathList {
&self.path &self.paths
} }
pub fn into_path(self) -> AbsoluteFilePath { pub fn into_paths(self) -> AbsoluteFilePathList {
self.path self.paths
} }
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum CatError { pub enum CatError {
#[error("The file does not exist")] #[error("A file does not exist")]
NotFound, NotFound,
#[error(transparent)] #[error(transparent)]
Unknown(#[from] anyhow::Error), Unknown(#[from] anyhow::Error),

View File

@@ -16,4 +16,40 @@ pub use mv::*;
pub use rm::*; pub use rm::*;
pub use save::*; pub use save::*;
pub use stat::*; pub use stat::*;
use thiserror::Error;
pub use touch::*; pub use touch::*;
use super::AbsoluteFilePath;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AbsoluteFilePathList(Vec<AbsoluteFilePath>);
impl From<AbsoluteFilePathList> for Vec<AbsoluteFilePath> {
fn from(value: AbsoluteFilePathList) -> Self {
value.0
}
}
impl AbsoluteFilePathList {
pub fn new(paths: Vec<AbsoluteFilePath>) -> Result<Self, AbsoluteFilePathListError> {
if paths.is_empty() {
return Err(AbsoluteFilePathListError::Empty);
}
Ok(Self(paths))
}
pub fn paths(&self) -> &Vec<AbsoluteFilePath> {
&self.0
}
pub fn paths_mut(&mut self) -> &mut Vec<AbsoluteFilePath> {
&mut self.0
}
}
#[derive(Debug, Error)]
pub enum AbsoluteFilePathListError {
#[error("A list must not be empty")]
Empty,
}

View File

@@ -2,7 +2,7 @@ use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::domain::warren::models::{ use crate::domain::warren::models::{
file::{AbsoluteFilePath, CatRequest, FileStream}, file::{AbsoluteFilePathList, CatRequest, FileStream},
share::{Share, SharePassword}, share::{Share, SharePassword},
warren::{FetchWarrenError, Warren, WarrenCatError, WarrenCatRequest}, warren::{FetchWarrenError, Warren, WarrenCatError, WarrenCatRequest},
}; };
@@ -12,16 +12,16 @@ use super::{VerifySharePasswordError, VerifySharePasswordRequest};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ShareCatRequest { pub struct ShareCatRequest {
share_id: Uuid, share_id: Uuid,
path: AbsoluteFilePath, base: CatRequest,
password: Option<SharePassword>, password: Option<SharePassword>,
} }
impl ShareCatRequest { impl ShareCatRequest {
pub fn new(share_id: Uuid, path: AbsoluteFilePath, password: Option<SharePassword>) -> Self { pub fn new(share_id: Uuid, password: Option<SharePassword>, base: CatRequest) -> Self {
Self { Self {
share_id, share_id,
path,
password, password,
base,
} }
} }
@@ -29,18 +29,23 @@ impl ShareCatRequest {
&self.share_id &self.share_id
} }
pub fn path(&self) -> &AbsoluteFilePath {
&self.path
}
pub fn password(&self) -> Option<&SharePassword> { pub fn password(&self) -> Option<&SharePassword> {
self.password.as_ref() self.password.as_ref()
} }
pub fn build_warren_cat_request(self, share: &Share, warren: &Warren) -> WarrenCatRequest { pub fn base(&self) -> &CatRequest {
let path = share.path().clone().join(&self.path.to_relative()); &self.base
}
WarrenCatRequest::new(*warren.id(), CatRequest::new(path)) pub fn build_warren_cat_request(self, share: &Share, warren: &Warren) -> WarrenCatRequest {
let mut paths = self.base.into_paths();
paths
.paths_mut()
.into_iter()
.for_each(|path| *path = share.path.clone().join(&path.clone().to_relative()));
WarrenCatRequest::new(*warren.id(), CatRequest::new(paths))
} }
} }
@@ -53,16 +58,21 @@ impl From<&ShareCatRequest> for VerifySharePasswordRequest {
pub struct ShareCatResponse { pub struct ShareCatResponse {
share: Share, share: Share,
warren: Warren, warren: Warren,
path: AbsoluteFilePath, paths: AbsoluteFilePathList,
stream: FileStream, stream: FileStream,
} }
impl ShareCatResponse { impl ShareCatResponse {
pub fn new(share: Share, warren: Warren, path: AbsoluteFilePath, stream: FileStream) -> Self { pub fn new(
share: Share,
warren: Warren,
paths: AbsoluteFilePathList,
stream: FileStream,
) -> Self {
Self { Self {
share, share,
warren, warren,
path, paths,
stream, stream,
} }
} }
@@ -75,8 +85,8 @@ impl ShareCatResponse {
&self.warren &self.warren
} }
pub fn path(&self) -> &AbsoluteFilePath { pub fn paths(&self) -> &AbsoluteFilePathList {
&self.path &self.paths
} }
pub fn stream(&self) -> &FileStream { pub fn stream(&self) -> &FileStream {

View File

@@ -597,12 +597,14 @@ impl WarrenCatRequest {
} }
pub fn build_fs_request(self, warren: &Warren) -> CatRequest { pub fn build_fs_request(self, warren: &Warren) -> CatRequest {
let path = warren let mut paths = self.base.into_paths();
.path()
.clone()
.join(&self.base.into_path().to_relative());
CatRequest::new(path) paths
.paths_mut()
.into_iter()
.for_each(|path| *path = warren.path.clone().join(&path.clone().to_relative()));
CatRequest::new(paths)
} }
} }

View File

@@ -2,7 +2,7 @@ use uuid::Uuid;
use crate::domain::warren::models::{ use crate::domain::warren::models::{
auth_session::requests::FetchAuthSessionResponse, auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, LsResponse}, file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
share::{ share::{
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse, CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
ShareCatResponse, ShareLsResponse, ShareCatResponse, ShareLsResponse,
@@ -28,7 +28,7 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
fn warren_cat( fn warren_cat(
&self, &self,
warren: &Warren, warren: &Warren,
path: &AbsoluteFilePath, path: &AbsoluteFilePathList,
) -> impl Future<Output = ()> + Send; ) -> impl Future<Output = ()> + Send;
fn warren_mkdir(&self, response: &WarrenMkdirResponse) -> impl Future<Output = ()> + Send; fn warren_mkdir(&self, response: &WarrenMkdirResponse) -> impl Future<Output = ()> + Send;
fn warren_rm(&self, response: &WarrenRmResponse) -> impl Future<Output = ()> + Send; fn warren_rm(&self, response: &WarrenRmResponse) -> impl Future<Output = ()> + Send;
@@ -68,7 +68,7 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
pub trait FileSystemNotifier: Clone + Send + Sync + 'static { pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
fn ls(&self, response: &LsResponse) -> impl Future<Output = ()> + Send; fn ls(&self, response: &LsResponse) -> impl Future<Output = ()> + Send;
fn cat(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send; fn cat(&self, path: &AbsoluteFilePathList) -> impl Future<Output = ()> + Send;
fn mkdir(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send; fn mkdir(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
fn rm(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send; fn rm(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
fn mv( fn mv(
@@ -163,7 +163,7 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
&self, &self,
user: &User, user: &User,
warren_id: &Uuid, warren_id: &Uuid,
path: &AbsoluteFilePath, paths: &AbsoluteFilePathList,
) -> impl Future<Output = ()> + Send; ) -> impl Future<Output = ()> + Send;
fn auth_warren_mkdir( fn auth_warren_mkdir(
&self, &self,

View File

@@ -704,7 +704,7 @@ where
return Err(AuthError::InsufficientPermissions); return Err(AuthError::InsufficientPermissions);
} }
let path = request.base().path().clone(); let paths = request.base().paths().clone();
let result = warren_service let result = warren_service
.warren_cat(request) .warren_cat(request)
@@ -714,7 +714,7 @@ where
if let Ok(_stream) = result.as_ref() { if let Ok(_stream) = result.as_ref() {
self.metrics.record_auth_warren_cat_success().await; self.metrics.record_auth_warren_cat_success().await;
self.notifier self.notifier
.auth_warren_cat(&user, user_warren.warren_id(), &path) .auth_warren_cat(&user, user_warren.warren_id(), &paths)
.await; .await;
} else { } else {
self.metrics.record_auth_warren_cat_failure().await; self.metrics.record_auth_warren_cat_failure().await;

View File

@@ -54,12 +54,12 @@ where
} }
async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> { async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> {
let path = request.path().clone(); let paths = request.paths().clone();
let result = self.repository.cat(request).await; let result = self.repository.cat(request).await;
if result.is_ok() { if result.is_ok() {
self.metrics.record_cat_success().await; self.metrics.record_cat_success().await;
self.notifier.cat(&path).await; self.notifier.cat(&paths).await;
} else { } else {
self.metrics.record_cat_failure().await; self.metrics.record_cat_failure().await;
} }

View File

@@ -157,14 +157,14 @@ where
async fn warren_cat(&self, request: WarrenCatRequest) -> Result<FileStream, WarrenCatError> { async fn warren_cat(&self, request: WarrenCatRequest) -> Result<FileStream, WarrenCatError> {
let warren = self.repository.fetch_warren((&request).into()).await?; let warren = self.repository.fetch_warren((&request).into()).await?;
let path = request.base().path().clone(); let paths = request.base().paths().clone();
let cat_request = request.build_fs_request(&warren); let cat_request = request.build_fs_request(&warren);
let result = self.fs_service.cat(cat_request).await.map_err(Into::into); let result = self.fs_service.cat(cat_request).await.map_err(Into::into);
if result.is_ok() { if result.is_ok() {
self.metrics.record_warren_cat_success().await; self.metrics.record_warren_cat_success().await;
self.notifier.warren_cat(&warren, &path).await; self.notifier.warren_cat(&warren, &paths).await;
} else { } else {
self.metrics.record_warren_cat_failure().await; self.metrics.record_warren_cat_failure().await;
} }
@@ -517,7 +517,7 @@ where
} }
}; };
let path = request.path().clone(); let paths = request.base().paths().clone();
let stream = match self let stream = match self
.warren_cat(request.build_warren_cat_request(&share, &warren)) .warren_cat(request.build_warren_cat_request(&share, &warren))
@@ -530,7 +530,7 @@ where
} }
}; };
let response = ShareCatResponse::new(share, warren, path, stream); let response = ShareCatResponse::new(share, warren, paths, stream);
self.metrics.record_warren_share_cat_success().await; self.metrics.record_warren_share_cat_success().await;
self.notifier.warren_share_cat(&response).await; self.notifier.warren_share_cat(&response).await;

View File

@@ -9,7 +9,10 @@ use uuid::Uuid;
use crate::{ use crate::{
domain::warren::{ domain::warren::{
models::{ models::{
file::{AbsoluteFilePathError, FilePath, FilePathError, FileStream}, file::{
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
AbsoluteFilePathListError, CatRequest, FilePath, FilePathError, FileStream,
},
share::{ShareCatRequest, SharePassword, SharePasswordError}, share::{ShareCatRequest, SharePassword, SharePasswordError},
}, },
ports::{AuthService, WarrenService}, ports::{AuthService, WarrenService},
@@ -24,6 +27,8 @@ enum ParseShareCatHttpRequestError {
#[error(transparent)] #[error(transparent)]
AbsoluteFilePath(#[from] AbsoluteFilePathError), AbsoluteFilePath(#[from] AbsoluteFilePathError),
#[error(transparent)] #[error(transparent)]
PathList(#[from] AbsoluteFilePathListError),
#[error(transparent)]
Password(#[from] SharePasswordError), Password(#[from] SharePasswordError),
} }
@@ -38,6 +43,11 @@ impl From<ParseShareCatHttpRequestError> for ApiError {
Self::BadRequest("The path must be absolute".to_string()) Self::BadRequest("The path must be absolute".to_string())
} }
}, },
ParseShareCatHttpRequestError::PathList(err) => match err {
AbsoluteFilePathListError::Empty => {
Self::BadRequest("You must provide at least 1 path".to_string())
}
},
ParseShareCatHttpRequestError::Password(err) => Self::BadRequest( ParseShareCatHttpRequestError::Password(err) => Self::BadRequest(
match err { match err {
SharePasswordError::Empty => "The provided password is empty", SharePasswordError::Empty => "The provided password is empty",
@@ -56,7 +66,7 @@ impl From<ParseShareCatHttpRequestError> for ApiError {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(super) struct ShareCatHttpRequestBody { pub(super) struct ShareCatHttpRequestBody {
share_id: Uuid, share_id: Uuid,
path: String, paths: String,
} }
impl ShareCatHttpRequestBody { impl ShareCatHttpRequestBody {
@@ -64,9 +74,19 @@ impl ShareCatHttpRequestBody {
self, self,
password: Option<SharePassword>, password: Option<SharePassword>,
) -> Result<ShareCatRequest, ParseShareCatHttpRequestError> { ) -> Result<ShareCatRequest, ParseShareCatHttpRequestError> {
let path = FilePath::new(&self.path)?.try_into()?; let mut paths = Vec::<AbsoluteFilePath>::new();
Ok(ShareCatRequest::new(self.share_id, path, password)) for path in self.paths.split(':') {
paths.push(FilePath::new(path)?.try_into()?);
}
let path_list = AbsoluteFilePathList::new(paths)?;
Ok(ShareCatRequest::new(
self.share_id,
password,
CatRequest::new(path_list),
))
} }
} }

View File

@@ -10,7 +10,10 @@ use crate::{
domain::warren::{ domain::warren::{
models::{ models::{
auth_session::AuthRequest, auth_session::AuthRequest,
file::{AbsoluteFilePathError, CatRequest, FilePath, FilePathError}, file::{
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
AbsoluteFilePathListError, CatRequest, FilePath, FilePathError,
},
warren::WarrenCatRequest, warren::WarrenCatRequest,
}, },
ports::{AuthService, WarrenService}, ports::{AuthService, WarrenService},
@@ -22,15 +25,17 @@ use crate::{
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(super) struct WarrenCatHttpRequestBody { pub(super) struct WarrenCatHttpRequestBody {
warren_id: Uuid, warren_id: Uuid,
path: String, paths: String,
} }
#[derive(Debug, Clone, Error)] #[derive(Debug, Error)]
pub enum ParseWarrenCatHttpRequestError { pub enum ParseWarrenCatHttpRequestError {
#[error(transparent)] #[error(transparent)]
FilePath(#[from] FilePathError), FilePath(#[from] FilePathError),
#[error(transparent)] #[error(transparent)]
AbsoluteFilePath(#[from] AbsoluteFilePathError), AbsoluteFilePath(#[from] AbsoluteFilePathError),
#[error(transparent)]
PathList(#[from] AbsoluteFilePathListError),
} }
impl From<ParseWarrenCatHttpRequestError> for ApiError { impl From<ParseWarrenCatHttpRequestError> for ApiError {
@@ -46,15 +51,29 @@ impl From<ParseWarrenCatHttpRequestError> for ApiError {
ApiError::BadRequest("The file path must be absolute".to_string()) ApiError::BadRequest("The file path must be absolute".to_string())
} }
}, },
ParseWarrenCatHttpRequestError::PathList(err) => match err {
AbsoluteFilePathListError::Empty => {
Self::BadRequest("You must provide at least 1 path".to_string())
}
},
} }
} }
} }
impl WarrenCatHttpRequestBody { impl WarrenCatHttpRequestBody {
fn try_into_domain(self) -> Result<WarrenCatRequest, ParseWarrenCatHttpRequestError> { fn try_into_domain(self) -> Result<WarrenCatRequest, ParseWarrenCatHttpRequestError> {
let path = FilePath::new(&self.path)?.try_into()?; let mut paths = Vec::<AbsoluteFilePath>::new();
Ok(WarrenCatRequest::new(self.warren_id, CatRequest::new(path))) for path in self.paths.split(':') {
paths.push(FilePath::new(path)?.try_into()?);
}
let path_list = AbsoluteFilePathList::new(paths)?;
Ok(WarrenCatRequest::new(
self.warren_id,
CatRequest::new(path_list),
))
} }
} }

View File

@@ -2,6 +2,7 @@ use anyhow::{Context as _, anyhow, bail};
use futures_util::TryStreamExt; use futures_util::TryStreamExt;
use rustix::fs::{Statx, statx}; use rustix::fs::{Statx, statx};
use std::{ use std::{
collections::HashSet,
io::Write, io::Write,
os::unix::fs::MetadataExt, os::unix::fs::MetadataExt,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -226,14 +227,48 @@ impl FileSystem {
Ok(paths) Ok(paths)
} }
async fn cat(&self, path: &AbsoluteFilePath) -> anyhow::Result<FileStream> { async fn cat(&self, paths: &Vec<AbsoluteFilePath>) -> anyhow::Result<FileStream> {
let path = self.get_target_path(path); let paths: Vec<FilePath> = paths
.into_iter()
.map(|path| self.get_target_path(path))
.collect();
let path_request = PathRequest::from_paths(paths)?;
fn build_zip_stream(
prefix: String,
paths: Vec<FilePath>,
buffer_size: usize,
) -> FileStream {
let (sync_tx, sync_rx) =
std::sync::mpsc::channel::<Result<bytes::Bytes, std::io::Error>>();
let (tx, rx) = tokio::sync::mpsc::channel::<Result<bytes::Bytes, std::io::Error>>(1024);
tokio::task::spawn(create_zip(
prefix,
paths
.into_iter()
.map(|p| PathBuf::from(p.as_str()))
.collect(),
sync_tx,
buffer_size,
));
tokio::task::spawn(async move {
while let Ok(v) = sync_rx.recv() {
let _ = tx.send(v).await;
}
});
FileStream::new(FileType::Directory, ReceiverStream::new(rx))
}
match path_request {
PathRequest::Single(file_path) => {
let file = fs::OpenOptions::new() let file = fs::OpenOptions::new()
.create(false) .create(false)
.write(false) .write(false)
.read(true) .read(true)
.open(&path) .open(&file_path)
.await?; .await?;
let metadata = file.metadata().await?; let metadata = file.metadata().await?;
@@ -241,25 +276,14 @@ impl FileSystem {
if metadata.is_dir() { if metadata.is_dir() {
drop(file); drop(file);
let (sync_tx, sync_rx) = return Ok(build_zip_stream(
std::sync::mpsc::channel::<Result<bytes::Bytes, std::io::Error>>(); file_path.to_string(),
let (tx, rx) = tokio::sync::mpsc::channel::<Result<bytes::Bytes, std::io::Error>>(1024); vec![file_path],
self.zip_read_buffer_bytes,
tokio::task::spawn(create_zip(path, sync_tx, self.zip_read_buffer_bytes)); ));
tokio::task::spawn(async move {
while let Ok(v) = sync_rx.recv() {
let _ = tx.send(v).await;
}
});
let stream = FileStream::new(FileType::Directory, ReceiverStream::new(rx));
return Ok(stream);
} }
let file_size = metadata.size(); if metadata.size() > self.max_file_fetch_bytes {
if file_size > self.max_file_fetch_bytes {
bail!("File size exceeds configured limit"); bail!("File size exceeds configured limit");
} }
@@ -267,6 +291,16 @@ impl FileSystem {
Ok(stream) Ok(stream)
} }
PathRequest::Multiple {
lowest_common_prefix,
paths,
} => Ok(build_zip_stream(
lowest_common_prefix,
paths,
self.zip_read_buffer_bytes,
)),
}
}
async fn mv(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) -> io::Result<()> { async fn mv(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) -> io::Result<()> {
let current_path = self.get_target_path(path); let current_path = self.get_target_path(path);
@@ -364,9 +398,9 @@ impl FileSystemRepository for FileSystem {
} }
async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> { async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> {
self.cat(request.path()) self.cat(request.paths().paths()).await.map_err(|e| {
.await anyhow!("Failed to fetch files {:?}: {e:?}", request.paths().paths()).into()
.map_err(|e| anyhow!("Failed to fetch file {}: {e:?}", request.path()).into()) })
} }
async fn mkdir(&self, request: MkdirRequest) -> Result<(), MkdirError> { async fn mkdir(&self, request: MkdirRequest) -> Result<(), MkdirError> {
@@ -462,8 +496,9 @@ where
let mut files = vec![]; let mut files = vec![];
while !dirs.is_empty() { while !dirs.is_empty() {
let mut dir_iter = tokio::fs::read_dir(dirs.remove(0)).await?; let path = dirs.remove(0);
match tokio::fs::read_dir(path.clone()).await {
Ok(mut dir_iter) => {
while let Some(entry) = dir_iter.next_entry().await? { while let Some(entry) = dir_iter.next_entry().await? {
let entry_path_buf = entry.path(); let entry_path_buf = entry.path();
@@ -474,6 +509,12 @@ where
} }
} }
} }
Err(e) => match e.kind() {
std::io::ErrorKind::NotADirectory => files.push(path),
_ => return Err(e),
},
}
}
Ok(files) Ok(files)
} }
@@ -483,14 +524,12 @@ where
/// * `path`: The directory's path /// * `path`: The directory's path
/// * `tx`: The sender for the new ZIP archive's bytes /// * `tx`: The sender for the new ZIP archive's bytes
/// * `buffer_size`: The size of the file read buffer. A large buffer increases both the speed and the memory usage /// * `buffer_size`: The size of the file read buffer. A large buffer increases both the speed and the memory usage
async fn create_zip<P>( async fn create_zip(
path: P, prefix: String,
paths: Vec<PathBuf>,
tx: std::sync::mpsc::Sender<Result<bytes::Bytes, std::io::Error>>, tx: std::sync::mpsc::Sender<Result<bytes::Bytes, std::io::Error>>,
buffer_size: usize, buffer_size: usize,
) -> anyhow::Result<()> ) -> anyhow::Result<()> {
where
P: AsRef<Path>,
{
let options = zip::write::SimpleFileOptions::default() let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Stored) .compression_method(zip::CompressionMethod::Stored)
.compression_level(None) .compression_level(None)
@@ -500,12 +539,15 @@ where
let mut file_buf = vec![0; buffer_size]; let mut file_buf = vec![0; buffer_size];
let mut zip = zip::write::ZipWriter::new_stream(ChannelWriter(tx)); let mut zip = zip::write::ZipWriter::new_stream(ChannelWriter(tx));
let entries = walk_dir(&path).await?; let mut entries = Vec::new();
for path in &paths {
entries.append(&mut walk_dir(&path).await?);
}
for entry_path_buf in entries { for entry_path_buf in entries {
let entry_path = entry_path_buf.as_path(); let entry_path = entry_path_buf.as_path();
let entry_str = entry_path let entry_str = entry_path
.strip_prefix(&path)? .strip_prefix(&prefix)?
.to_str() .to_str()
.context("Failed to get directory entry name")?; .context("Failed to get directory entry name")?;
@@ -546,3 +588,47 @@ impl Write for ChannelWriter {
Ok(()) Ok(())
} }
} }
enum PathRequest {
Single(FilePath),
Multiple {
lowest_common_prefix: String,
paths: Vec<FilePath>,
},
}
impl PathRequest {
fn from_paths(paths: Vec<FilePath>) -> anyhow::Result<Self> {
let mut input_paths: Vec<FilePath> = HashSet::<FilePath>::from_iter(paths.into_iter())
.into_iter()
.collect();
if input_paths.len() == 1 {
return Ok(Self::Single(input_paths.pop().unwrap()));
}
let mut lowest_common_prefix = input_paths
.first()
.expect("paths to contain at least 1 entry")
.to_string();
for path in &input_paths {
let chars = lowest_common_prefix
.chars()
.zip(path.as_str().chars())
.enumerate();
for (index, (a, b)) in chars {
if a != b {
lowest_common_prefix = lowest_common_prefix[..index].to_string();
break;
}
}
}
Ok(Self::Multiple {
lowest_common_prefix,
paths: input_paths,
})
}
}

View File

@@ -8,7 +8,7 @@ use crate::domain::{
warren::{ warren::{
models::{ models::{
auth_session::requests::FetchAuthSessionResponse, auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, LsResponse}, file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
share::{ share::{
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse, CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
ShareCatResponse, ShareLsResponse, ShareCatResponse, ShareLsResponse,
@@ -58,10 +58,10 @@ impl WarrenNotifier for NotifierDebugLogger {
tracing::debug!("[Notifier] Fetched warren {}", warren.name()); tracing::debug!("[Notifier] Fetched warren {}", warren.name());
} }
async fn warren_cat(&self, warren: &Warren, path: &AbsoluteFilePath) { async fn warren_cat(&self, warren: &Warren, paths: &AbsoluteFilePathList) {
tracing::debug!( tracing::debug!(
"[Notifier] Fetched file {} in warren {}", "[Notifier] Fetched {} file(s) in warren {}",
path, paths.paths().len(),
warren.name(), warren.name(),
); );
} }
@@ -165,8 +165,8 @@ impl WarrenNotifier for NotifierDebugLogger {
async fn warren_share_cat(&self, response: &ShareCatResponse) { async fn warren_share_cat(&self, response: &ShareCatResponse) {
tracing::debug!( tracing::debug!(
"[Notifier] Fetched file {} from share {}", "[Notifier] Fetched {} file(s) from share {}",
response.path(), response.paths().paths().len(),
response.share().id(), response.share().id(),
); );
} }
@@ -177,8 +177,8 @@ impl FileSystemNotifier for NotifierDebugLogger {
tracing::debug!("[Notifier] Listed {} file(s)", response.files().len()); tracing::debug!("[Notifier] Listed {} file(s)", response.files().len());
} }
async fn cat(&self, path: &AbsoluteFilePath) { async fn cat(&self, paths: &AbsoluteFilePathList) {
tracing::debug!("[Notifier] Fetched file {path}"); tracing::debug!("[Notifier] Fetched {} file(s)", paths.paths().len());
} }
async fn mkdir(&self, path: &AbsoluteFilePath) { async fn mkdir(&self, path: &AbsoluteFilePath) {
@@ -356,10 +356,11 @@ impl AuthNotifier for NotifierDebugLogger {
); );
} }
async fn auth_warren_cat(&self, user: &User, warren_id: &Uuid, path: &AbsoluteFilePath) { async fn auth_warren_cat(&self, user: &User, warren_id: &Uuid, paths: &AbsoluteFilePathList) {
tracing::debug!( tracing::debug!(
"[Notifier] User {} fetched file {path} in warren {warren_id}", "[Notifier] User {} fetched {} file(s) in warren {warren_id}",
user.id(), user.id(),
paths.paths().len(),
); );
} }

View File

@@ -8,19 +8,23 @@ import {
} from '@/components/ui/context-menu'; } from '@/components/ui/context-menu';
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens'; import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
import type { DirectoryEntry } from '#shared/types'; import type { DirectoryEntry } from '#shared/types';
import { toast } from 'vue-sonner';
const warrenStore = useWarrenStore(); const warrenStore = useWarrenStore();
const copyStore = useCopyStore(); const copyStore = useCopyStore();
const renameDialog = useRenameDirectoryDialog(); const renameDialog = useRenameDirectoryDialog();
const { entry, disabled } = defineProps<{ const {
entry,
disabled,
draggable = true,
} = defineProps<{
entry: DirectoryEntry; entry: DirectoryEntry;
disabled: boolean; disabled: boolean;
draggable?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
'entry-click': [entry: DirectoryEntry]; 'entry-click': [entry: DirectoryEntry, event: MouseEvent];
'entry-download': [entry: DirectoryEntry]; 'entry-download': [entry: DirectoryEntry];
}>(); }>();
@@ -33,6 +37,7 @@ const isCopied = computed(
warrenStore.current.path === copyStore.file.path && warrenStore.current.path === copyStore.file.path &&
entry.name === copyStore.file.name entry.name === copyStore.file.name
); );
const isSelected = computed(() => warrenStore.isSelected(entry));
async function submitDelete(force: boolean = false) { async function submitDelete(force: boolean = false) {
if (warrenStore.current == null) { if (warrenStore.current == null) {
@@ -63,8 +68,8 @@ async function openRenameDialog() {
renameDialog.openDialog(entry); renameDialog.openDialog(entry);
} }
async function onClick() { function onClick(event: MouseEvent) {
emit('entry-click', entry); emit('entry-click', entry, event);
} }
function onDragStart(e: DragEvent) { function onDragStart(e: DragEvent) {
@@ -97,6 +102,10 @@ function onShare() {
function onDownload() { function onDownload() {
emit('entry-download', entry); emit('entry-download', entry);
} }
function onClearCopy() {
copyStore.clearFile();
}
</script> </script>
<template> <template>
@@ -105,10 +114,11 @@ function onDownload() {
<button <button
:disabled="warrenStore.loading || disabled" :disabled="warrenStore.loading || disabled"
:class="[ :class="[
'bg-accent/30 border-border flex w-full translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none', 'bg-accent/30 border-border flex w-full translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 outline-0 select-none',
isCopied && 'border-primary/50 border', isCopied && 'border-primary/50 border',
isSelected && 'bg-primary/20',
]" ]"
draggable="true" :draggable
@dragstart="onDragStart" @dragstart="onDragStart"
@drop="onDrop" @drop="onDrop"
@click="onClick" @click="onClick"
@@ -136,13 +146,25 @@ function onDownload() {
<Icon name="lucide:pencil" /> <Icon name="lucide:pencil" />
Rename Rename
</ContextMenuItem> </ContextMenuItem>
<template v-if="warrenStore.current != null">
<ContextMenuItem <ContextMenuItem
:class="[warrenStore.current == null && 'hidden']" v-if="
copyStore.file == null ||
copyStore.file.warrenId !==
warrenStore.current.warrenId ||
copyStore.file.path !== warrenStore.current.path ||
copyStore.file.name !== entry.name
"
@select="onCopy" @select="onCopy"
> >
<Icon name="lucide:copy" /> <Icon name="lucide:copy" />
Copy Copy
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem v-else @select="onClearCopy">
<Icon name="lucide:copy-x" />
Clear clipboard
</ContextMenuItem>
</template>
<ContextMenuItem @select="onDownload"> <ContextMenuItem @select="onDownload">
<Icon name="lucide:download" /> <Icon name="lucide:download" />
Download Download

View File

@@ -33,10 +33,10 @@ const warrenStore = useWarrenStore();
:data=" :data="
route.meta.layout === 'share' route.meta.layout === 'share'
? getApiUrl( ? getApiUrl(
`warrens/files/cat_share?shareId=${route.query.id}&path=${joinPaths(warrenStore.current.path, entry.name)}` `warrens/files/cat_share?shareId=${route.query.id}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
) )
: getApiUrl( : getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current!.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}` `warrens/files/cat?warrenId=${warrenStore.current!.warrenId}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
) )
" "
> >

View File

@@ -7,15 +7,17 @@ const {
parent, parent,
isOverDropZone, isOverDropZone,
disableEntries = false, disableEntries = false,
entriesDraggable = true,
} = defineProps<{ } = defineProps<{
entries: DirectoryEntry[]; entries: DirectoryEntry[];
parent: DirectoryEntry | null; parent: DirectoryEntry | null;
isOverDropZone?: boolean; isOverDropZone?: boolean;
disableEntries?: boolean; disableEntries?: boolean;
entriesDraggable?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
'entry-click': [entry: DirectoryEntry]; 'entry-click': [entry: DirectoryEntry, event: MouseEvent];
'entry-download': [entry: DirectoryEntry]; 'entry-download': [entry: DirectoryEntry];
back: []; back: [];
}>(); }>();
@@ -26,8 +28,8 @@ const sortedEntries = computed(() =>
entries.toSorted((a, b) => a.name.localeCompare(b.name)) entries.toSorted((a, b) => a.name.localeCompare(b.name))
); );
function onEntryClicked(entry: DirectoryEntry) { function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
emit('entry-click', entry); emit('entry-click', entry, event);
} }
function onEntryDownload(entry: DirectoryEntry) { function onEntryDownload(entry: DirectoryEntry) {
@@ -56,6 +58,7 @@ function onEntryDownload(entry: DirectoryEntry) {
:key="entry.name" :key="entry.name"
:entry="entry" :entry="entry"
:disabled="isLoading || disableEntries" :disabled="isLoading || disableEntries"
:draggable="entriesDraggable"
@entry-click="onEntryClicked" @entry-click="onEntryClicked"
@entry-download="onEntryDownload" @entry-download="onEntryDownload"
/> />

View File

@@ -27,12 +27,16 @@ async function onPaste() {
pasting.value = true; pasting.value = true;
await pasteFile(copyStore.file!, { const success = await pasteFile(copyStore.file!, {
warrenId: warrenStore.current!.warrenId, warrenId: warrenStore.current!.warrenId,
name: copyStore.file!.name, name: copyStore.file!.name,
path: warrenStore.current!.path, path: warrenStore.current!.path,
}); });
if (success) {
copyStore.clearFile();
}
pasting.value = false; pasting.value = false;
} }
</script> </script>

View File

@@ -31,6 +31,34 @@ await useAsyncData('warrens', async () => {
</div> </div>
<div class="ml-auto flex flex-row items-center gap-2"> <div class="ml-auto flex flex-row items-center gap-2">
<Button
variant="outline"
size="icon"
:disabled="
store.current != null &&
store.current.dir != null &&
store.selection.size >=
store.current.dir.entries.length
"
@click="
() =>
store.current != null &&
store.current.dir != null &&
store.addMultipleToSelection(
store.current.dir.entries
)
"
>
<Icon name="lucide:list" />
</Button>
<Button
variant="outline"
size="icon"
:disabled="store.selection.size < 1"
@click="() => store.clearSelection()"
>
<Icon name="lucide:list-x" />
</Button>
<Separator <Separator
orientation="vertical" orientation="vertical"
class="mr-2 hidden !h-4 md:block" class="mr-2 hidden !h-4 md:block"

View File

@@ -149,7 +149,7 @@ export async function fetchShareFile(
password: string | null password: string | null
): Promise<{ success: true; data: Blob } | { success: false }> { ): Promise<{ success: true; data: Blob } | { success: false }> {
const { data } = await useFetch<Blob>( const { data } = await useFetch<Blob>(
getApiUrl(`warrens/files/cat_share?shareId=${shareId}&path=${path}`), getApiUrl(`warrens/files/cat_share?shareId=${shareId}&paths=${path}`),
{ {
method: 'GET', method: 'GET',
headers: headers:

View File

@@ -273,14 +273,27 @@ export async function fetchFile(
warrenId: string, warrenId: string,
path: string, path: string,
fileName: string fileName: string
): Promise<{ success: true; data: Blob } | { success: false }> {
return fetchFiles(warrenId, path, [fileName]);
}
export async function fetchFiles(
warrenId: string,
path: string,
fileNames: string[]
): Promise<{ success: true; data: Blob } | { success: false }> { ): Promise<{ success: true; data: Blob } | { success: false }> {
if (!path.endsWith('/')) { if (!path.endsWith('/')) {
path += '/'; path += '/';
} }
path += fileName; const paths = [];
for (const fileName of fileNames) {
paths.push(path + fileName);
}
const { data, error } = await useFetch<Blob>( const { data, error } = await useFetch<Blob>(
getApiUrl(`warrens/files/cat?warrenId=${warrenId}&path=${path}`), getApiUrl(
`warrens/files/cat?warrenId=${warrenId}&paths=${paths.join(':')}`
),
{ {
method: 'GET', method: 'GET',
headers: getApiHeaders(), headers: getApiHeaders(),
@@ -314,7 +327,7 @@ export async function fetchFileStream(
path += fileName; path += fileName;
const { data, error } = await useFetch<ReadableStream<Uint8Array>>( const { data, error } = await useFetch<ReadableStream<Uint8Array>>(
getApiUrl(`warrens/files/cat?warrenId=${warrenId}&path=${path}`), getApiUrl(`warrens/files/cat?warrenId=${warrenId}&paths=${path}`),
{ {
method: 'GET', method: 'GET',
headers: getApiHeaders(), headers: getApiHeaders(),

View File

@@ -10,11 +10,23 @@ definePageMeta({
const warrenStore = useWarrenStore(); const warrenStore = useWarrenStore();
const route = useRoute(); const route = useRoute();
const entries = computed(() =>
warrenStore.current != null && warrenStore.current.dir != null
? warrenStore.current.dir.entries
: null
);
const parent = computed(() =>
warrenStore.current != null && warrenStore.current.dir != null
? warrenStore.current.dir.parent
: null
);
const share = await getShareFromQuery(); const share = await getShareFromQuery();
const entries = ref<DirectoryEntry[] | null>(null);
const parent = ref<DirectoryEntry | null>(null);
const password = ref<string>(''); const password = ref<string>('');
const loading = ref<boolean>(false); const loading = ref<boolean>(false);
const passwordValid = ref<boolean>(
share == null ? false : !share.data.password
);
if (share != null) { if (share != null) {
warrenStore.setCurrentWarren(share.data.warrenId, '/'); warrenStore.setCurrentWarren(share.data.warrenId, '/');
@@ -65,6 +77,7 @@ async function loadFiles() {
); );
if (result.success) { if (result.success) {
passwordValid.value = true;
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`; let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
if (share.data.expiresAt != null) { if (share.data.expiresAt != null) {
@@ -76,18 +89,22 @@ async function loadFiles() {
document.cookie = cookie; document.cookie = cookie;
entries.value = result.files; warrenStore.setCurrentWarrenEntries(result.files, result.parent);
parent.value = result.parent;
} }
loading.value = false; loading.value = false;
} }
async function onEntryClicked(entry: DirectoryEntry) { async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
if (warrenStore.current == null) { if (warrenStore.current == null) {
return; return;
} }
if (event.ctrlKey) {
warrenStore.toggleSelection(entry);
return;
}
const entryPath = joinPaths(warrenStore.current.path, entry.name); const entryPath = joinPaths(warrenStore.current.path, entry.name);
if (entry.fileType === 'directory') { if (entry.fileType === 'directory') {
@@ -130,7 +147,7 @@ function onDowloadClicked() {
? `${share.file.name}.zip` ? `${share.file.name}.zip`
: share.file.name; : share.file.name;
const downloadApiUrl = getApiUrl( const downloadApiUrl = getApiUrl(
`warrens/files/cat_share?shareId=${share.data.id}&path=/` `warrens/files/cat_share?shareId=${share.data.id}&paths=/`
); );
downloadFile(downloadName, downloadApiUrl); downloadFile(downloadName, downloadApiUrl);
@@ -141,11 +158,27 @@ function onEntryDownload(entry: DirectoryEntry) {
return; return;
} }
const downloadName = let downloadName: string;
let downloadApiUrl: string;
const selectionSize = warrenStore.selection.size;
if (selectionSize === 0 || !warrenStore.isSelected(entry)) {
downloadName =
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name; entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name;
const downloadApiUrl = getApiUrl( downloadApiUrl = getApiUrl(
`warrens/files/cat_share?shareId=${share.data.id}&path=${joinPaths(warrenStore.current.path, entry.name)}` `warrens/files/cat_share?shareId=${share.data.id}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
); );
} else {
downloadName = 'download.zip';
const paths = Array.from(warrenStore.selection).map((entry) =>
joinPaths(warrenStore.current!.path, entry.name)
);
downloadApiUrl = getApiUrl(
`warrens/files/cat_share?shareId=${share.data.id}&paths=${paths.join(':')}`
);
}
downloadFile(downloadName, downloadApiUrl); downloadFile(downloadName, downloadApiUrl);
} }
@@ -158,8 +191,8 @@ function onEntryDownload(entry: DirectoryEntry) {
> >
<div <div
:class="[ :class="[
'w-full rounded-lg border transition-all', 'h-[min(98vh,600px)] w-full max-w-screen-xl rounded-lg border transition-all',
entries == null ? 'max-w-lg' : 'max-w-screen-xl', passwordValid ? 'max-w-screen-xl' : 'max-w-lg',
]" ]"
> >
<div <div
@@ -205,6 +238,7 @@ function onEntryDownload(entry: DirectoryEntry) {
:entries :entries
:parent :parent
:disable-entries="loading" :disable-entries="loading"
:entries-draggable="false"
@entry-click="onEntryClicked" @entry-click="onEntryClicked"
@entry-download="onEntryDownload" @entry-download="onEntryDownload"
@back="onBack" @back="onBack"

View File

@@ -28,7 +28,7 @@ if (warrenStore.current == null) {
}); });
} }
const dirData = useAsyncData( useAsyncData(
'current-directory', 'current-directory',
async () => { async () => {
if (warrenStore.current == null) { if (warrenStore.current == null) {
@@ -49,10 +49,10 @@ const dirData = useAsyncData(
warrenStore.loading = false; warrenStore.loading = false;
loadingIndicator.finish(); loadingIndicator.finish();
return { files, parent }; warrenStore.setCurrentWarrenEntries(files, parent);
}, },
{ watch: [warrenPath] } { watch: [warrenPath] }
).data; );
function onDrop(files: File[] | null, e: DragEvent) { function onDrop(files: File[] | null, e: DragEvent) {
if (files == null) { if (files == null) {
@@ -79,11 +79,16 @@ function onDrop(files: File[] | null, e: DragEvent) {
} }
} }
async function onEntryClicked(entry: DirectoryEntry) { async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
if (warrenStore.loading || warrenStore.current == null) { if (warrenStore.loading || warrenStore.current == null) {
return; return;
} }
if (event.ctrlKey) {
warrenStore.toggleSelection(entry);
return;
}
if (entry.fileType === 'directory') { if (entry.fileType === 'directory') {
warrenStore.addToCurrentWarrenPath(entry.name); warrenStore.addToCurrentWarrenPath(entry.name);
return; return;
@@ -111,11 +116,27 @@ function onEntryDownload(entry: DirectoryEntry) {
return; return;
} }
const downloadName = let downloadName: string;
let downloadApiUrl: string;
const selectionSize = warrenStore.selection.size;
if (selectionSize === 0 || !warrenStore.isSelected(entry)) {
downloadName =
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name; entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name;
const downloadApiUrl = getApiUrl( downloadApiUrl = getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}` `warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${joinPaths(warrenStore.current.path, entry.name)}`
); );
} else {
downloadName = 'download.zip';
const paths = Array.from(warrenStore.selection).map((entry) =>
joinPaths(warrenStore.current!.path, entry.name)
);
downloadApiUrl = getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${paths.join(':')}`
);
}
downloadFile(downloadName, downloadApiUrl); downloadFile(downloadName, downloadApiUrl);
} }
@@ -129,13 +150,16 @@ function onBack() {
<div ref="dropZoneRef" class="grow"> <div ref="dropZoneRef" class="grow">
<DirectoryListContextMenu class="w-full grow"> <DirectoryListContextMenu class="w-full grow">
<DirectoryList <DirectoryList
v-if="dirData != null" v-if="
warrenStore.current != null &&
warrenStore.current.dir != null
"
:is-over-drop-zone=" :is-over-drop-zone="
dropZone.isOverDropZone.value && dropZone.isOverDropZone.value &&
dropZone.files.value != null dropZone.files.value != null
" "
:entries="dirData.files" :entries="warrenStore.current.dir.entries"
:parent="dirData.parent" :parent="warrenStore.current.dir.parent"
@entry-click="onEntryClicked" @entry-click="onEntryClicked"
@entry-download="onEntryDownload" @entry-download="onEntryDownload"
@back="onBack" @back="onBack"

View File

@@ -9,7 +9,15 @@ export const useWarrenStore = defineStore('warrens', {
imageViewer: { imageViewer: {
src: null as string | null, src: null as string | null,
}, },
current: null as { warrenId: string; path: string } | null, current: null as {
warrenId: string;
path: string;
dir: {
parent: DirectoryEntry | null;
entries: DirectoryEntry[];
} | null;
} | null,
selection: new Set() as Set<DirectoryEntry>,
loading: false, loading: false,
}), }),
actions: { actions: {
@@ -17,6 +25,21 @@ export const useWarrenStore = defineStore('warrens', {
this.current = { this.current = {
warrenId, warrenId,
path, path,
dir: null,
};
this.clearSelection();
},
setCurrentWarrenEntries(
entries: DirectoryEntry[],
parent: DirectoryEntry | null
) {
if (this.current == null) {
return;
}
this.current.dir = {
entries,
parent,
}; };
}, },
addToCurrentWarrenPath(path: string) { addToCurrentWarrenPath(path: string) {
@@ -28,14 +51,14 @@ export const useWarrenStore = defineStore('warrens', {
path = '/' + path; path = '/' + path;
} }
this.current.path += path; this.setCurrentWarrenPath(this.current.path + path);
}, },
backCurrentPath(): boolean { backCurrentPath(): boolean {
if (this.current == null || this.current.path === '/') { if (this.current == null || this.current.path === '/') {
return false; return false;
} }
this.current.path = getParentPath(this.current.path); this.setCurrentWarrenPath(getParentPath(this.current.path));
return true; return true;
}, },
@@ -44,15 +67,50 @@ export const useWarrenStore = defineStore('warrens', {
return; return;
} }
const previous = this.current.path;
if (!path.startsWith('/')) { if (!path.startsWith('/')) {
path = '/' + path; path = '/' + path;
} }
this.current.path = path; this.current.path = path;
if (previous !== path) {
this.clearSelection();
this.current.dir = null;
}
}, },
clearCurrentWarren() { clearCurrentWarren() {
this.current = null; this.current = null;
}, },
addToSelection(entry: DirectoryEntry) {
this.selection.add(entry);
},
addMultipleToSelection(entries: DirectoryEntry[]) {
for (const entry of entries) {
this.selection.add(entry);
}
},
removeFromSelection(entry: DirectoryEntry): boolean {
return this.selection.delete(entry);
},
toggleSelection(entry: DirectoryEntry) {
if (this.selection.has(entry)) {
this.removeFromSelection(entry);
} else {
this.addToSelection(entry);
}
},
isSelected(entry: DirectoryEntry): boolean {
if (this.current == null) {
return false;
}
return this.selection.has(entry);
},
clearSelection() {
this.selection.clear();
},
}, },
}); });