basic selection + download multiple files with selection
This commit is contained in:
@@ -1,29 +1,29 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::domain::warren::models::file::AbsoluteFilePath;
|
||||
use super::AbsoluteFilePathList;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct CatRequest {
|
||||
path: AbsoluteFilePath,
|
||||
paths: AbsoluteFilePathList,
|
||||
}
|
||||
|
||||
impl CatRequest {
|
||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
||||
Self { path }
|
||||
pub fn new(paths: AbsoluteFilePathList) -> Self {
|
||||
Self { paths }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
pub fn paths(&self) -> &AbsoluteFilePathList {
|
||||
&self.paths
|
||||
}
|
||||
|
||||
pub fn into_path(self) -> AbsoluteFilePath {
|
||||
self.path
|
||||
pub fn into_paths(self) -> AbsoluteFilePathList {
|
||||
self.paths
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CatError {
|
||||
#[error("The file does not exist")]
|
||||
#[error("A file does not exist")]
|
||||
NotFound,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
|
||||
@@ -16,4 +16,40 @@ pub use mv::*;
|
||||
pub use rm::*;
|
||||
pub use save::*;
|
||||
pub use stat::*;
|
||||
use thiserror::Error;
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::models::{
|
||||
file::{AbsoluteFilePath, CatRequest, FileStream},
|
||||
file::{AbsoluteFilePathList, CatRequest, FileStream},
|
||||
share::{Share, SharePassword},
|
||||
warren::{FetchWarrenError, Warren, WarrenCatError, WarrenCatRequest},
|
||||
};
|
||||
@@ -12,16 +12,16 @@ use super::{VerifySharePasswordError, VerifySharePasswordRequest};
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct ShareCatRequest {
|
||||
share_id: Uuid,
|
||||
path: AbsoluteFilePath,
|
||||
base: CatRequest,
|
||||
password: Option<SharePassword>,
|
||||
}
|
||||
|
||||
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 {
|
||||
share_id,
|
||||
path,
|
||||
password,
|
||||
base,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,18 +29,23 @@ impl ShareCatRequest {
|
||||
&self.share_id
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn password(&self) -> Option<&SharePassword> {
|
||||
self.password.as_ref()
|
||||
}
|
||||
|
||||
pub fn build_warren_cat_request(self, share: &Share, warren: &Warren) -> WarrenCatRequest {
|
||||
let path = share.path().clone().join(&self.path.to_relative());
|
||||
pub fn base(&self) -> &CatRequest {
|
||||
&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 {
|
||||
share: Share,
|
||||
warren: Warren,
|
||||
path: AbsoluteFilePath,
|
||||
paths: AbsoluteFilePathList,
|
||||
stream: FileStream,
|
||||
}
|
||||
|
||||
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 {
|
||||
share,
|
||||
warren,
|
||||
path,
|
||||
paths,
|
||||
stream,
|
||||
}
|
||||
}
|
||||
@@ -75,8 +85,8 @@ impl ShareCatResponse {
|
||||
&self.warren
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &AbsoluteFilePath {
|
||||
&self.path
|
||||
pub fn paths(&self) -> &AbsoluteFilePathList {
|
||||
&self.paths
|
||||
}
|
||||
|
||||
pub fn stream(&self) -> &FileStream {
|
||||
|
||||
@@ -597,12 +597,14 @@ impl WarrenCatRequest {
|
||||
}
|
||||
|
||||
pub fn build_fs_request(self, warren: &Warren) -> CatRequest {
|
||||
let path = warren
|
||||
.path()
|
||||
.clone()
|
||||
.join(&self.base.into_path().to_relative());
|
||||
let mut paths = self.base.into_paths();
|
||||
|
||||
CatRequest::new(path)
|
||||
paths
|
||||
.paths_mut()
|
||||
.into_iter()
|
||||
.for_each(|path| *path = warren.path.clone().join(&path.clone().to_relative()));
|
||||
|
||||
CatRequest::new(paths)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{AbsoluteFilePath, LsResponse},
|
||||
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
||||
share::{
|
||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||
ShareCatResponse, ShareLsResponse,
|
||||
@@ -28,7 +28,7 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
||||
fn warren_cat(
|
||||
&self,
|
||||
warren: &Warren,
|
||||
path: &AbsoluteFilePath,
|
||||
path: &AbsoluteFilePathList,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn warren_mkdir(&self, response: &WarrenMkdirResponse) -> 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 {
|
||||
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 rm(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||
fn mv(
|
||||
@@ -163,7 +163,7 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
user: &User,
|
||||
warren_id: &Uuid,
|
||||
path: &AbsoluteFilePath,
|
||||
paths: &AbsoluteFilePathList,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn auth_warren_mkdir(
|
||||
&self,
|
||||
|
||||
@@ -704,7 +704,7 @@ where
|
||||
return Err(AuthError::InsufficientPermissions);
|
||||
}
|
||||
|
||||
let path = request.base().path().clone();
|
||||
let paths = request.base().paths().clone();
|
||||
|
||||
let result = warren_service
|
||||
.warren_cat(request)
|
||||
@@ -714,7 +714,7 @@ where
|
||||
if let Ok(_stream) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_cat_success().await;
|
||||
self.notifier
|
||||
.auth_warren_cat(&user, user_warren.warren_id(), &path)
|
||||
.auth_warren_cat(&user, user_warren.warren_id(), &paths)
|
||||
.await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_cat_failure().await;
|
||||
|
||||
@@ -54,12 +54,12 @@ where
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if result.is_ok() {
|
||||
self.metrics.record_cat_success().await;
|
||||
self.notifier.cat(&path).await;
|
||||
self.notifier.cat(&paths).await;
|
||||
} else {
|
||||
self.metrics.record_cat_failure().await;
|
||||
}
|
||||
|
||||
@@ -157,14 +157,14 @@ where
|
||||
async fn warren_cat(&self, request: WarrenCatRequest) -> Result<FileStream, WarrenCatError> {
|
||||
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 result = self.fs_service.cat(cat_request).await.map_err(Into::into);
|
||||
|
||||
if result.is_ok() {
|
||||
self.metrics.record_warren_cat_success().await;
|
||||
self.notifier.warren_cat(&warren, &path).await;
|
||||
self.notifier.warren_cat(&warren, &paths).await;
|
||||
} else {
|
||||
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
|
||||
.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.notifier.warren_share_cat(&response).await;
|
||||
|
||||
@@ -9,7 +9,10 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
domain::warren::{
|
||||
models::{
|
||||
file::{AbsoluteFilePathError, FilePath, FilePathError, FileStream},
|
||||
file::{
|
||||
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
|
||||
AbsoluteFilePathListError, CatRequest, FilePath, FilePathError, FileStream,
|
||||
},
|
||||
share::{ShareCatRequest, SharePassword, SharePasswordError},
|
||||
},
|
||||
ports::{AuthService, WarrenService},
|
||||
@@ -24,6 +27,8 @@ enum ParseShareCatHttpRequestError {
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
#[error(transparent)]
|
||||
PathList(#[from] AbsoluteFilePathListError),
|
||||
#[error(transparent)]
|
||||
Password(#[from] SharePasswordError),
|
||||
}
|
||||
|
||||
@@ -38,6 +43,11 @@ impl From<ParseShareCatHttpRequestError> for ApiError {
|
||||
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(
|
||||
match err {
|
||||
SharePasswordError::Empty => "The provided password is empty",
|
||||
@@ -56,7 +66,7 @@ impl From<ParseShareCatHttpRequestError> for ApiError {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct ShareCatHttpRequestBody {
|
||||
share_id: Uuid,
|
||||
path: String,
|
||||
paths: String,
|
||||
}
|
||||
|
||||
impl ShareCatHttpRequestBody {
|
||||
@@ -64,9 +74,19 @@ impl ShareCatHttpRequestBody {
|
||||
self,
|
||||
password: Option<SharePassword>,
|
||||
) -> 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),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ use crate::{
|
||||
domain::warren::{
|
||||
models::{
|
||||
auth_session::AuthRequest,
|
||||
file::{AbsoluteFilePathError, CatRequest, FilePath, FilePathError},
|
||||
file::{
|
||||
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
|
||||
AbsoluteFilePathListError, CatRequest, FilePath, FilePathError,
|
||||
},
|
||||
warren::WarrenCatRequest,
|
||||
},
|
||||
ports::{AuthService, WarrenService},
|
||||
@@ -22,15 +25,17 @@ use crate::{
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct WarrenCatHttpRequestBody {
|
||||
warren_id: Uuid,
|
||||
path: String,
|
||||
paths: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseWarrenCatHttpRequestError {
|
||||
#[error(transparent)]
|
||||
FilePath(#[from] FilePathError),
|
||||
#[error(transparent)]
|
||||
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||
#[error(transparent)]
|
||||
PathList(#[from] AbsoluteFilePathListError),
|
||||
}
|
||||
|
||||
impl From<ParseWarrenCatHttpRequestError> for ApiError {
|
||||
@@ -46,15 +51,29 @@ impl From<ParseWarrenCatHttpRequestError> for ApiError {
|
||||
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 {
|
||||
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),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::{Context as _, anyhow, bail};
|
||||
use futures_util::TryStreamExt;
|
||||
use rustix::fs::{Statx, statx};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
io::Write,
|
||||
os::unix::fs::MetadataExt,
|
||||
path::{Path, PathBuf},
|
||||
@@ -226,46 +227,79 @@ impl FileSystem {
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
async fn cat(&self, path: &AbsoluteFilePath) -> anyhow::Result<FileStream> {
|
||||
let path = self.get_target_path(path);
|
||||
async fn cat(&self, paths: &Vec<AbsoluteFilePath>) -> anyhow::Result<FileStream> {
|
||||
let paths: Vec<FilePath> = paths
|
||||
.into_iter()
|
||||
.map(|path| self.get_target_path(path))
|
||||
.collect();
|
||||
|
||||
let file = fs::OpenOptions::new()
|
||||
.create(false)
|
||||
.write(false)
|
||||
.read(true)
|
||||
.open(&path)
|
||||
.await?;
|
||||
|
||||
let metadata = file.metadata().await?;
|
||||
|
||||
if metadata.is_dir() {
|
||||
drop(file);
|
||||
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(path, sync_tx, self.zip_read_buffer_bytes));
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
let stream = FileStream::new(FileType::Directory, ReceiverStream::new(rx));
|
||||
|
||||
return Ok(stream);
|
||||
FileStream::new(FileType::Directory, ReceiverStream::new(rx))
|
||||
}
|
||||
|
||||
let file_size = metadata.size();
|
||||
match path_request {
|
||||
PathRequest::Single(file_path) => {
|
||||
let file = fs::OpenOptions::new()
|
||||
.create(false)
|
||||
.write(false)
|
||||
.read(true)
|
||||
.open(&file_path)
|
||||
.await?;
|
||||
|
||||
if file_size > self.max_file_fetch_bytes {
|
||||
bail!("File size exceeds configured limit");
|
||||
let metadata = file.metadata().await?;
|
||||
|
||||
if metadata.is_dir() {
|
||||
drop(file);
|
||||
|
||||
return Ok(build_zip_stream(
|
||||
file_path.to_string(),
|
||||
vec![file_path],
|
||||
self.zip_read_buffer_bytes,
|
||||
));
|
||||
}
|
||||
|
||||
if metadata.size() > self.max_file_fetch_bytes {
|
||||
bail!("File size exceeds configured limit");
|
||||
}
|
||||
|
||||
let stream = FileStream::new(FileType::File, ReaderStream::new(file));
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
PathRequest::Multiple {
|
||||
lowest_common_prefix,
|
||||
paths,
|
||||
} => Ok(build_zip_stream(
|
||||
lowest_common_prefix,
|
||||
paths,
|
||||
self.zip_read_buffer_bytes,
|
||||
)),
|
||||
}
|
||||
|
||||
let stream = FileStream::new(FileType::File, ReaderStream::new(file));
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
async fn mv(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) -> io::Result<()> {
|
||||
@@ -364,9 +398,9 @@ impl FileSystemRepository for FileSystem {
|
||||
}
|
||||
|
||||
async fn cat(&self, request: CatRequest) -> Result<FileStream, CatError> {
|
||||
self.cat(request.path())
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to fetch file {}: {e:?}", request.path()).into())
|
||||
self.cat(request.paths().paths()).await.map_err(|e| {
|
||||
anyhow!("Failed to fetch files {:?}: {e:?}", request.paths().paths()).into()
|
||||
})
|
||||
}
|
||||
|
||||
async fn mkdir(&self, request: MkdirRequest) -> Result<(), MkdirError> {
|
||||
@@ -462,16 +496,23 @@ where
|
||||
let mut files = vec![];
|
||||
|
||||
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? {
|
||||
let entry_path_buf = entry.path();
|
||||
|
||||
while let Some(entry) = dir_iter.next_entry().await? {
|
||||
let entry_path_buf = entry.path();
|
||||
|
||||
if entry_path_buf.is_dir() {
|
||||
dirs.push(entry_path_buf);
|
||||
} else {
|
||||
files.push(entry_path_buf);
|
||||
if entry_path_buf.is_dir() {
|
||||
dirs.push(entry_path_buf);
|
||||
} else {
|
||||
files.push(entry_path_buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotADirectory => files.push(path),
|
||||
_ => return Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,14 +524,12 @@ where
|
||||
/// * `path`: The directory's path
|
||||
/// * `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
|
||||
async fn create_zip<P>(
|
||||
path: P,
|
||||
async fn create_zip(
|
||||
prefix: String,
|
||||
paths: Vec<PathBuf>,
|
||||
tx: std::sync::mpsc::Sender<Result<bytes::Bytes, std::io::Error>>,
|
||||
buffer_size: usize,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
) -> anyhow::Result<()> {
|
||||
let options = zip::write::SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Stored)
|
||||
.compression_level(None)
|
||||
@@ -500,12 +539,15 @@ where
|
||||
let mut file_buf = vec![0; buffer_size];
|
||||
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 {
|
||||
let entry_path = entry_path_buf.as_path();
|
||||
let entry_str = entry_path
|
||||
.strip_prefix(&path)?
|
||||
.strip_prefix(&prefix)?
|
||||
.to_str()
|
||||
.context("Failed to get directory entry name")?;
|
||||
|
||||
@@ -546,3 +588,47 @@ impl Write for ChannelWriter {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::domain::{
|
||||
warren::{
|
||||
models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{AbsoluteFilePath, LsResponse},
|
||||
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
|
||||
share::{
|
||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||
ShareCatResponse, ShareLsResponse,
|
||||
@@ -58,10 +58,10 @@ impl WarrenNotifier for NotifierDebugLogger {
|
||||
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!(
|
||||
"[Notifier] Fetched file {} in warren {}",
|
||||
path,
|
||||
"[Notifier] Fetched {} file(s) in warren {}",
|
||||
paths.paths().len(),
|
||||
warren.name(),
|
||||
);
|
||||
}
|
||||
@@ -165,8 +165,8 @@ impl WarrenNotifier for NotifierDebugLogger {
|
||||
|
||||
async fn warren_share_cat(&self, response: &ShareCatResponse) {
|
||||
tracing::debug!(
|
||||
"[Notifier] Fetched file {} from share {}",
|
||||
response.path(),
|
||||
"[Notifier] Fetched {} file(s) from share {}",
|
||||
response.paths().paths().len(),
|
||||
response.share().id(),
|
||||
);
|
||||
}
|
||||
@@ -177,8 +177,8 @@ impl FileSystemNotifier for NotifierDebugLogger {
|
||||
tracing::debug!("[Notifier] Listed {} file(s)", response.files().len());
|
||||
}
|
||||
|
||||
async fn cat(&self, path: &AbsoluteFilePath) {
|
||||
tracing::debug!("[Notifier] Fetched file {path}");
|
||||
async fn cat(&self, paths: &AbsoluteFilePathList) {
|
||||
tracing::debug!("[Notifier] Fetched {} file(s)", paths.paths().len());
|
||||
}
|
||||
|
||||
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!(
|
||||
"[Notifier] User {} fetched file {path} in warren {warren_id}",
|
||||
"[Notifier] User {} fetched {} file(s) in warren {warren_id}",
|
||||
user.id(),
|
||||
paths.paths().len(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,19 +8,23 @@ import {
|
||||
} from '@/components/ui/context-menu';
|
||||
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
|
||||
import type { DirectoryEntry } from '#shared/types';
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
const warrenStore = useWarrenStore();
|
||||
const copyStore = useCopyStore();
|
||||
const renameDialog = useRenameDirectoryDialog();
|
||||
|
||||
const { entry, disabled } = defineProps<{
|
||||
const {
|
||||
entry,
|
||||
disabled,
|
||||
draggable = true,
|
||||
} = defineProps<{
|
||||
entry: DirectoryEntry;
|
||||
disabled: boolean;
|
||||
draggable?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'entry-click': [entry: DirectoryEntry];
|
||||
'entry-click': [entry: DirectoryEntry, event: MouseEvent];
|
||||
'entry-download': [entry: DirectoryEntry];
|
||||
}>();
|
||||
|
||||
@@ -33,6 +37,7 @@ const isCopied = computed(
|
||||
warrenStore.current.path === copyStore.file.path &&
|
||||
entry.name === copyStore.file.name
|
||||
);
|
||||
const isSelected = computed(() => warrenStore.isSelected(entry));
|
||||
|
||||
async function submitDelete(force: boolean = false) {
|
||||
if (warrenStore.current == null) {
|
||||
@@ -63,8 +68,8 @@ async function openRenameDialog() {
|
||||
renameDialog.openDialog(entry);
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
emit('entry-click', entry);
|
||||
function onClick(event: MouseEvent) {
|
||||
emit('entry-click', entry, event);
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent) {
|
||||
@@ -97,6 +102,10 @@ function onShare() {
|
||||
function onDownload() {
|
||||
emit('entry-download', entry);
|
||||
}
|
||||
|
||||
function onClearCopy() {
|
||||
copyStore.clearFile();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -105,10 +114,11 @@ function onDownload() {
|
||||
<button
|
||||
:disabled="warrenStore.loading || disabled"
|
||||
: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',
|
||||
isSelected && 'bg-primary/20',
|
||||
]"
|
||||
draggable="true"
|
||||
:draggable
|
||||
@dragstart="onDragStart"
|
||||
@drop="onDrop"
|
||||
@click="onClick"
|
||||
@@ -136,13 +146,25 @@ function onDownload() {
|
||||
<Icon name="lucide:pencil" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
:class="[warrenStore.current == null && 'hidden']"
|
||||
@select="onCopy"
|
||||
>
|
||||
<Icon name="lucide:copy" />
|
||||
Copy
|
||||
</ContextMenuItem>
|
||||
<template v-if="warrenStore.current != null">
|
||||
<ContextMenuItem
|
||||
v-if="
|
||||
copyStore.file == null ||
|
||||
copyStore.file.warrenId !==
|
||||
warrenStore.current.warrenId ||
|
||||
copyStore.file.path !== warrenStore.current.path ||
|
||||
copyStore.file.name !== entry.name
|
||||
"
|
||||
@select="onCopy"
|
||||
>
|
||||
<Icon name="lucide:copy" />
|
||||
Copy
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem v-else @select="onClearCopy">
|
||||
<Icon name="lucide:copy-x" />
|
||||
Clear clipboard
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
<ContextMenuItem @select="onDownload">
|
||||
<Icon name="lucide:download" />
|
||||
Download
|
||||
|
||||
@@ -33,10 +33,10 @@ const warrenStore = useWarrenStore();
|
||||
:data="
|
||||
route.meta.layout === 'share'
|
||||
? 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(
|
||||
`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)}`
|
||||
)
|
||||
"
|
||||
>
|
||||
|
||||
@@ -7,15 +7,17 @@ const {
|
||||
parent,
|
||||
isOverDropZone,
|
||||
disableEntries = false,
|
||||
entriesDraggable = true,
|
||||
} = defineProps<{
|
||||
entries: DirectoryEntry[];
|
||||
parent: DirectoryEntry | null;
|
||||
isOverDropZone?: boolean;
|
||||
disableEntries?: boolean;
|
||||
entriesDraggable?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'entry-click': [entry: DirectoryEntry];
|
||||
'entry-click': [entry: DirectoryEntry, event: MouseEvent];
|
||||
'entry-download': [entry: DirectoryEntry];
|
||||
back: [];
|
||||
}>();
|
||||
@@ -26,8 +28,8 @@ const sortedEntries = computed(() =>
|
||||
entries.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
function onEntryClicked(entry: DirectoryEntry) {
|
||||
emit('entry-click', entry);
|
||||
function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
|
||||
emit('entry-click', entry, event);
|
||||
}
|
||||
|
||||
function onEntryDownload(entry: DirectoryEntry) {
|
||||
@@ -56,6 +58,7 @@ function onEntryDownload(entry: DirectoryEntry) {
|
||||
:key="entry.name"
|
||||
:entry="entry"
|
||||
:disabled="isLoading || disableEntries"
|
||||
:draggable="entriesDraggable"
|
||||
@entry-click="onEntryClicked"
|
||||
@entry-download="onEntryDownload"
|
||||
/>
|
||||
|
||||
@@ -27,12 +27,16 @@ async function onPaste() {
|
||||
|
||||
pasting.value = true;
|
||||
|
||||
await pasteFile(copyStore.file!, {
|
||||
const success = await pasteFile(copyStore.file!, {
|
||||
warrenId: warrenStore.current!.warrenId,
|
||||
name: copyStore.file!.name,
|
||||
path: warrenStore.current!.path,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
copyStore.clearFile();
|
||||
}
|
||||
|
||||
pasting.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -31,6 +31,34 @@ await useAsyncData('warrens', async () => {
|
||||
</div>
|
||||
|
||||
<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
|
||||
orientation="vertical"
|
||||
class="mr-2 hidden !h-4 md:block"
|
||||
|
||||
@@ -149,7 +149,7 @@ export async function fetchShareFile(
|
||||
password: string | null
|
||||
): Promise<{ success: true; data: Blob } | { success: false }> {
|
||||
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',
|
||||
headers:
|
||||
|
||||
@@ -273,14 +273,27 @@ export async function fetchFile(
|
||||
warrenId: string,
|
||||
path: 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 }> {
|
||||
if (!path.endsWith('/')) {
|
||||
path += '/';
|
||||
}
|
||||
path += fileName;
|
||||
const paths = [];
|
||||
for (const fileName of fileNames) {
|
||||
paths.push(path + fileName);
|
||||
}
|
||||
|
||||
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',
|
||||
headers: getApiHeaders(),
|
||||
@@ -314,7 +327,7 @@ export async function fetchFileStream(
|
||||
path += fileName;
|
||||
|
||||
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',
|
||||
headers: getApiHeaders(),
|
||||
|
||||
@@ -10,11 +10,23 @@ definePageMeta({
|
||||
const warrenStore = useWarrenStore();
|
||||
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 entries = ref<DirectoryEntry[] | null>(null);
|
||||
const parent = ref<DirectoryEntry | null>(null);
|
||||
const password = ref<string>('');
|
||||
const loading = ref<boolean>(false);
|
||||
const passwordValid = ref<boolean>(
|
||||
share == null ? false : !share.data.password
|
||||
);
|
||||
|
||||
if (share != null) {
|
||||
warrenStore.setCurrentWarren(share.data.warrenId, '/');
|
||||
@@ -65,6 +77,7 @@ async function loadFiles() {
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
passwordValid.value = true;
|
||||
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
|
||||
|
||||
if (share.data.expiresAt != null) {
|
||||
@@ -76,18 +89,22 @@ async function loadFiles() {
|
||||
|
||||
document.cookie = cookie;
|
||||
|
||||
entries.value = result.files;
|
||||
parent.value = result.parent;
|
||||
warrenStore.setCurrentWarrenEntries(result.files, result.parent);
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function onEntryClicked(entry: DirectoryEntry) {
|
||||
async function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
|
||||
if (warrenStore.current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey) {
|
||||
warrenStore.toggleSelection(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPath = joinPaths(warrenStore.current.path, entry.name);
|
||||
|
||||
if (entry.fileType === 'directory') {
|
||||
@@ -130,7 +147,7 @@ function onDowloadClicked() {
|
||||
? `${share.file.name}.zip`
|
||||
: share.file.name;
|
||||
const downloadApiUrl = getApiUrl(
|
||||
`warrens/files/cat_share?shareId=${share.data.id}&path=/`
|
||||
`warrens/files/cat_share?shareId=${share.data.id}&paths=/`
|
||||
);
|
||||
|
||||
downloadFile(downloadName, downloadApiUrl);
|
||||
@@ -141,11 +158,27 @@ function onEntryDownload(entry: DirectoryEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadName =
|
||||
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name;
|
||||
const downloadApiUrl = getApiUrl(
|
||||
`warrens/files/cat_share?shareId=${share.data.id}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||
);
|
||||
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;
|
||||
downloadApiUrl = getApiUrl(
|
||||
`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);
|
||||
}
|
||||
@@ -158,8 +191,8 @@ function onEntryDownload(entry: DirectoryEntry) {
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'w-full rounded-lg border transition-all',
|
||||
entries == null ? 'max-w-lg' : 'max-w-screen-xl',
|
||||
'h-[min(98vh,600px)] w-full max-w-screen-xl rounded-lg border transition-all',
|
||||
passwordValid ? 'max-w-screen-xl' : 'max-w-lg',
|
||||
]"
|
||||
>
|
||||
<div
|
||||
@@ -205,6 +238,7 @@ function onEntryDownload(entry: DirectoryEntry) {
|
||||
:entries
|
||||
:parent
|
||||
:disable-entries="loading"
|
||||
:entries-draggable="false"
|
||||
@entry-click="onEntryClicked"
|
||||
@entry-download="onEntryDownload"
|
||||
@back="onBack"
|
||||
|
||||
@@ -28,7 +28,7 @@ if (warrenStore.current == null) {
|
||||
});
|
||||
}
|
||||
|
||||
const dirData = useAsyncData(
|
||||
useAsyncData(
|
||||
'current-directory',
|
||||
async () => {
|
||||
if (warrenStore.current == null) {
|
||||
@@ -49,10 +49,10 @@ const dirData = useAsyncData(
|
||||
warrenStore.loading = false;
|
||||
loadingIndicator.finish();
|
||||
|
||||
return { files, parent };
|
||||
warrenStore.setCurrentWarrenEntries(files, parent);
|
||||
},
|
||||
{ watch: [warrenPath] }
|
||||
).data;
|
||||
);
|
||||
|
||||
function onDrop(files: File[] | null, e: DragEvent) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey) {
|
||||
warrenStore.toggleSelection(entry);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.fileType === 'directory') {
|
||||
warrenStore.addToCurrentWarrenPath(entry.name);
|
||||
return;
|
||||
@@ -111,11 +116,27 @@ function onEntryDownload(entry: DirectoryEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const downloadName =
|
||||
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name;
|
||||
const downloadApiUrl = getApiUrl(
|
||||
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||
);
|
||||
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;
|
||||
downloadApiUrl = getApiUrl(
|
||||
`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);
|
||||
}
|
||||
@@ -129,13 +150,16 @@ function onBack() {
|
||||
<div ref="dropZoneRef" class="grow">
|
||||
<DirectoryListContextMenu class="w-full grow">
|
||||
<DirectoryList
|
||||
v-if="dirData != null"
|
||||
v-if="
|
||||
warrenStore.current != null &&
|
||||
warrenStore.current.dir != null
|
||||
"
|
||||
:is-over-drop-zone="
|
||||
dropZone.isOverDropZone.value &&
|
||||
dropZone.files.value != null
|
||||
"
|
||||
:entries="dirData.files"
|
||||
:parent="dirData.parent"
|
||||
:entries="warrenStore.current.dir.entries"
|
||||
:parent="warrenStore.current.dir.parent"
|
||||
@entry-click="onEntryClicked"
|
||||
@entry-download="onEntryDownload"
|
||||
@back="onBack"
|
||||
|
||||
@@ -9,7 +9,15 @@ export const useWarrenStore = defineStore('warrens', {
|
||||
imageViewer: {
|
||||
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,
|
||||
}),
|
||||
actions: {
|
||||
@@ -17,6 +25,21 @@ export const useWarrenStore = defineStore('warrens', {
|
||||
this.current = {
|
||||
warrenId,
|
||||
path,
|
||||
dir: null,
|
||||
};
|
||||
this.clearSelection();
|
||||
},
|
||||
setCurrentWarrenEntries(
|
||||
entries: DirectoryEntry[],
|
||||
parent: DirectoryEntry | null
|
||||
) {
|
||||
if (this.current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.current.dir = {
|
||||
entries,
|
||||
parent,
|
||||
};
|
||||
},
|
||||
addToCurrentWarrenPath(path: string) {
|
||||
@@ -28,14 +51,14 @@ export const useWarrenStore = defineStore('warrens', {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
this.current.path += path;
|
||||
this.setCurrentWarrenPath(this.current.path + path);
|
||||
},
|
||||
backCurrentPath(): boolean {
|
||||
if (this.current == null || this.current.path === '/') {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.current.path = getParentPath(this.current.path);
|
||||
this.setCurrentWarrenPath(getParentPath(this.current.path));
|
||||
|
||||
return true;
|
||||
},
|
||||
@@ -44,15 +67,50 @@ export const useWarrenStore = defineStore('warrens', {
|
||||
return;
|
||||
}
|
||||
|
||||
const previous = this.current.path;
|
||||
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
||||
this.current.path = path;
|
||||
|
||||
if (previous !== path) {
|
||||
this.clearSelection();
|
||||
this.current.dir = null;
|
||||
}
|
||||
},
|
||||
clearCurrentWarren() {
|
||||
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();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user