delete multiple files with selection

This commit is contained in:
2025-09-04 16:26:23 +02:00
parent 49b4162448
commit 8b2ed0e700
17 changed files with 250 additions and 137 deletions

View File

@@ -48,7 +48,7 @@ impl AbsoluteFilePathList {
} }
} }
#[derive(Debug, Error)] #[derive(Debug, Clone, Error)]
pub enum AbsoluteFilePathListError { pub enum AbsoluteFilePathListError {
#[error("A list must not be empty")] #[error("A list must not be empty")]
Empty, Empty,

View File

@@ -1,24 +1,24 @@
use thiserror::Error; use thiserror::Error;
use crate::domain::warren::models::file::AbsoluteFilePath; use crate::domain::warren::models::file::{AbsoluteFilePath, AbsoluteFilePathList};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RmRequest { pub struct RmRequest {
path: AbsoluteFilePath, paths: AbsoluteFilePathList,
force: bool, force: bool,
} }
impl RmRequest { impl RmRequest {
pub fn new(path: AbsoluteFilePath, force: bool) -> Self { pub fn new(paths: AbsoluteFilePathList, force: bool) -> Self {
Self { path, force } Self { paths, force }
} }
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
} }
pub fn force(&self) -> bool { pub fn force(&self) -> bool {
@@ -28,10 +28,10 @@ impl RmRequest {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum RmError { pub enum RmError {
#[error("The path does not exist")] #[error("At least one file does not exist")]
NotFound, NotFound(AbsoluteFilePath),
#[error("The directory is not empty")] #[error("At least one directory is not empty")]
NotEmpty, NotEmpty(AbsoluteFilePath),
#[error(transparent)] #[error(transparent)]
Unknown(#[from] anyhow::Error), Unknown(#[from] anyhow::Error),
} }

View File

@@ -221,12 +221,15 @@ impl WarrenRmRequest {
pub fn build_fs_request(self, warren: &Warren) -> RmRequest { pub fn build_fs_request(self, warren: &Warren) -> RmRequest {
let force = self.base.force(); let force = self.base.force();
let path = warren
.path()
.clone()
.join(&self.base.into_path().to_relative());
RmRequest::new(path, force) let mut paths = self.base.into_paths();
paths
.paths_mut()
.into_iter()
.for_each(|path| *path = warren.path.clone().join(&path.clone().to_relative()));
RmRequest::new(paths, force)
} }
} }
@@ -242,34 +245,30 @@ impl Into<FetchWarrenRequest> for &WarrenRmRequest {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug)]
pub struct WarrenRmResponse { pub struct WarrenRmResponse {
warren: Warren, warren: Warren,
path: AbsoluteFilePath, results: Vec<Result<AbsoluteFilePath, RmError>>,
} }
impl WarrenRmResponse { impl WarrenRmResponse {
pub fn new(warren: Warren, path: AbsoluteFilePath) -> Self { pub fn new(warren: Warren, results: Vec<Result<AbsoluteFilePath, RmError>>) -> Self {
Self { warren, path } Self { warren, results }
} }
pub fn warren(&self) -> &Warren { pub fn warren(&self) -> &Warren {
&self.warren &self.warren
} }
pub fn path(&self) -> &AbsoluteFilePath { pub fn results(&self) -> &Vec<Result<AbsoluteFilePath, RmError>> {
&self.path &self.results
} }
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum WarrenRmError { pub enum WarrenRmError {
#[error(transparent)]
FileSystem(#[from] RmError),
#[error(transparent)] #[error(transparent)]
FetchWarren(#[from] FetchWarrenError), FetchWarren(#[from] FetchWarrenError),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
} }
pub struct WarrenSaveRequest<'s> { pub struct WarrenSaveRequest<'s> {

View File

@@ -15,9 +15,10 @@ use super::models::{
}, },
}, },
file::{ file::{
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest, AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream,
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError,
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest, RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
TouchError, TouchRequest,
}, },
share::{ share::{
CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse, CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse,
@@ -144,7 +145,10 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
fn cat(&self, request: CatRequest) fn cat(&self, request: CatRequest)
-> impl Future<Output = Result<FileStream, CatError>> + Send; -> impl Future<Output = Result<FileStream, CatError>> + Send;
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send; fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;
fn rm(&self, request: RmRequest) -> impl Future<Output = Result<(), RmError>> + Send; fn rm(
&self,
request: RmRequest,
) -> impl Future<Output = Vec<Result<AbsoluteFilePath, RmError>>> + Send;
fn mv(&self, request: MvRequest) -> impl Future<Output = Result<(), MvError>> + Send; fn mv(&self, request: MvRequest) -> impl Future<Output = Result<(), MvError>> + Send;
fn save( fn save(
&self, &self,

View File

@@ -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: &AbsoluteFilePathList) -> impl Future<Output = ()> + Send; fn cat(&self, paths: &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(

View File

@@ -7,9 +7,10 @@ use crate::domain::warren::models::{
}, },
}, },
file::{ file::{
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest, AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream,
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError,
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest, RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
TouchError, TouchRequest,
}, },
share::{ share::{
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError, CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
@@ -97,7 +98,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
fn cat(&self, request: CatRequest) fn cat(&self, request: CatRequest)
-> impl Future<Output = Result<FileStream, CatError>> + Send; -> impl Future<Output = Result<FileStream, CatError>> + Send;
fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send; fn mkdir(&self, request: MkdirRequest) -> impl Future<Output = Result<(), MkdirError>> + Send;
fn rm(&self, request: RmRequest) -> impl Future<Output = Result<(), RmError>> + Send; fn rm(
&self,
request: RmRequest,
) -> impl Future<Output = Vec<Result<AbsoluteFilePath, RmError>>> + Send;
fn mv(&self, request: MvRequest) -> impl Future<Output = Result<(), MvError>> + Send; fn mv(&self, request: MvRequest) -> impl Future<Output = Result<(), MvError>> + Send;
fn save( fn save(
&self, &self,

View File

@@ -1,8 +1,9 @@
use crate::domain::warren::{ use crate::domain::warren::{
models::file::{ models::file::{
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest, AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream,
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError,
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest, RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
TouchError, TouchRequest,
}, },
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService}, ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
}; };
@@ -81,18 +82,19 @@ where
result result
} }
async fn rm(&self, request: RmRequest) -> Result<(), RmError> { async fn rm(&self, request: RmRequest) -> Vec<Result<AbsoluteFilePath, RmError>> {
let path = request.path().clone(); let results = self.repository.rm(request).await;
let result = self.repository.rm(request).await;
if result.is_ok() { for result in results.iter() {
if let Ok(path) = result.as_ref() {
self.metrics.record_rm_success().await; self.metrics.record_rm_success().await;
self.notifier.rm(&path).await; self.notifier.rm(path).await;
} else { } else {
self.metrics.record_rm_failure().await; self.metrics.record_rm_failure().await;
} }
}
result results
} }
async fn mv(&self, request: MvRequest) -> Result<(), MvError> { async fn mv(&self, request: MvRequest) -> Result<(), MvError> {

View File

@@ -250,26 +250,22 @@ where
} }
async fn warren_rm(&self, request: WarrenRmRequest) -> Result<WarrenRmResponse, WarrenRmError> { async fn warren_rm(&self, request: WarrenRmRequest) -> Result<WarrenRmResponse, WarrenRmError> {
let warren = self.repository.fetch_warren((&request).into()).await?; let warren = match self.repository.fetch_warren((&request).into()).await {
Ok(warren) => warren,
Err(e) => {
self.metrics.record_warren_rm_failure().await;
return Err(e.into());
}
};
let path = request.base().path().clone();
let rm_request = request.build_fs_request(&warren); let rm_request = request.build_fs_request(&warren);
let result = self let response = WarrenRmResponse::new(warren, self.fs_service.rm(rm_request).await);
.fs_service
.rm(rm_request)
.await
.map(|_| WarrenRmResponse::new(warren, path))
.map_err(Into::into);
if let Ok(response) = result.as_ref() {
self.metrics.record_warren_rm_success().await; self.metrics.record_warren_rm_success().await;
self.notifier.warren_rm(response).await; self.notifier.warren_rm(&response).await;
} else {
self.metrics.record_warren_rm_failure().await;
}
result Ok(response)
} }
async fn warren_mv(&self, request: WarrenMvRequest) -> Result<WarrenMvResponse, WarrenMvError> { async fn warren_mv(&self, request: WarrenMvRequest) -> Result<WarrenMvResponse, WarrenMvError> {

View File

@@ -41,8 +41,12 @@ impl From<WarrenMkdirError> for ApiError {
impl From<RmError> for ApiError { impl From<RmError> for ApiError {
fn from(value: RmError) -> Self { fn from(value: RmError) -> Self {
match value { match value {
RmError::NotFound => Self::NotFound("The directory does not exist".to_string()), RmError::NotFound(_) => {
RmError::NotEmpty => Self::BadRequest("The directory is not empty".to_string()), Self::NotFound("At least one of the specified files does not exist".to_string())
}
RmError::NotEmpty(_) => Self::BadRequest(
"At least one of the specified directories does not exist".to_string(),
),
RmError::Unknown(e) => Self::InternalServerError(e.to_string()), RmError::Unknown(e) => Self::InternalServerError(e.to_string()),
} }
} }
@@ -51,9 +55,7 @@ impl From<RmError> for ApiError {
impl From<WarrenRmError> for ApiError { impl From<WarrenRmError> for ApiError {
fn from(value: WarrenRmError) -> Self { fn from(value: WarrenRmError) -> Self {
match value { match value {
WarrenRmError::FileSystem(fs) => fs.into(),
WarrenRmError::FetchWarren(err) => err.into(), WarrenRmError::FetchWarren(err) => err.into(),
WarrenRmError::Unknown(error) => Self::InternalServerError(error.to_string()),
} }
} }
} }

View File

@@ -7,7 +7,10 @@ use crate::{
domain::warren::{ domain::warren::{
models::{ models::{
auth_session::AuthRequest, auth_session::AuthRequest,
file::{AbsoluteFilePathError, FilePath, FilePathError, RmRequest}, file::{
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
AbsoluteFilePathListError, FilePath, FilePathError, RmRequest,
},
warren::WarrenRmRequest, warren::WarrenRmRequest,
}, },
ports::{AuthService, WarrenService}, ports::{AuthService, WarrenService},
@@ -23,7 +26,7 @@ use crate::{
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(super) struct WarrenRmHttpRequestBody { pub(super) struct WarrenRmHttpRequestBody {
warren_id: Uuid, warren_id: Uuid,
path: String, paths: Vec<String>,
force: bool, force: bool,
} }
@@ -33,6 +36,8 @@ pub(super) enum ParseWarrenRmHttpRequestError {
FilePath(#[from] FilePathError), FilePath(#[from] FilePathError),
#[error(transparent)] #[error(transparent)]
AbsoluteFilePath(#[from] AbsoluteFilePathError), AbsoluteFilePath(#[from] AbsoluteFilePathError),
#[error(transparent)]
AbsoluteFilePathList(#[from] AbsoluteFilePathListError),
} }
impl From<ParseWarrenRmHttpRequestError> for ApiError { impl From<ParseWarrenRmHttpRequestError> for ApiError {
@@ -40,12 +45,17 @@ impl From<ParseWarrenRmHttpRequestError> for ApiError {
match value { match value {
ParseWarrenRmHttpRequestError::FilePath(err) => match err { ParseWarrenRmHttpRequestError::FilePath(err) => match err {
FilePathError::InvalidPath => { FilePathError::InvalidPath => {
ApiError::BadRequest("The file path must be valid".to_string()) ApiError::BadRequest("File paths must be valid".to_string())
} }
}, },
ParseWarrenRmHttpRequestError::AbsoluteFilePath(err) => match err { ParseWarrenRmHttpRequestError::AbsoluteFilePath(err) => match err {
AbsoluteFilePathError::NotAbsolute => { AbsoluteFilePathError::NotAbsolute => {
ApiError::BadRequest("The file path must be absolute".to_string()) ApiError::BadRequest("File paths must be absolute".to_string())
}
},
ParseWarrenRmHttpRequestError::AbsoluteFilePathList(err) => match err {
AbsoluteFilePathListError::Empty => {
Self::BadRequest("At least one file path is required".to_string())
} }
}, },
} }
@@ -54,11 +64,17 @@ impl From<ParseWarrenRmHttpRequestError> for ApiError {
impl WarrenRmHttpRequestBody { impl WarrenRmHttpRequestBody {
fn try_into_domain(self) -> Result<WarrenRmRequest, ParseWarrenRmHttpRequestError> { fn try_into_domain(self) -> Result<WarrenRmRequest, ParseWarrenRmHttpRequestError> {
let path = FilePath::new(&self.path)?; let mut paths = Vec::<AbsoluteFilePath>::new();
for path in self.paths.iter() {
paths.push(FilePath::new(path)?.try_into()?);
}
let path_list = AbsoluteFilePathList::new(paths)?;
Ok(WarrenRmRequest::new( Ok(WarrenRmRequest::new(
self.warren_id, self.warren_id,
RmRequest::new(path.try_into()?, self.force), RmRequest::new(path_list, self.force),
)) ))
} }
} }

View File

@@ -1,5 +1,5 @@
use anyhow::{Context as _, anyhow, bail}; use anyhow::{Context as _, anyhow, bail};
use futures_util::TryStreamExt; use futures_util::{TryStreamExt, future::join_all};
use rustix::fs::{Statx, statx}; use rustix::fs::{Statx, statx};
use std::{ use std::{
collections::HashSet, collections::HashSet,
@@ -182,10 +182,10 @@ impl FileSystem {
/// Actually removes a file or directory from the underlying file system /// Actually removes a file or directory from the underlying file system
/// ///
/// * `path`: The directory's absolute path (absolute not in relation to the root file system but `self.base_directory`) /// * `path`: The file's absolute path (absolute not in relation to the root file system but `self.base_directory`)
/// * `force`: Whether to delete directories that are not empty /// * `force`: Whether to delete directories that are not empty
async fn rm(&self, path: &AbsoluteFilePath, force: bool) -> io::Result<()> { async fn rm(&self, path: &AbsoluteFilePath, force: bool) -> io::Result<()> {
let file_path = self.get_target_path(path); let file_path = self.get_target_path(&path);
if fs::metadata(&file_path).await?.is_file() { if fs::metadata(&file_path).await?.is_file() {
return fs::remove_file(&file_path).await; return fs::remove_file(&file_path).await;
@@ -412,16 +412,36 @@ impl FileSystemRepository for FileSystem {
}) })
} }
async fn rm(&self, request: RmRequest) -> Result<(), RmError> { async fn rm(&self, request: RmRequest) -> Vec<Result<AbsoluteFilePath, RmError>> {
self.rm(request.path(), request.force()) let force = request.force();
let paths: Vec<AbsoluteFilePath> = request.into_paths().into();
async fn _rm(
fs: &FileSystem,
path: AbsoluteFilePath,
force: bool,
) -> Result<AbsoluteFilePath, RmError> {
fs.rm(&path, force)
.await .await
.map(|_| path.clone())
.map_err(|e| match e.kind() { .map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => RmError::NotFound, std::io::ErrorKind::NotFound => RmError::NotFound(path),
std::io::ErrorKind::DirectoryNotEmpty => RmError::NotEmpty, std::io::ErrorKind::DirectoryNotEmpty => RmError::NotEmpty(path),
_ => anyhow!("Failed to delete file at {}: {e:?}", request.path()).into(), _ => anyhow!("Failed to delete file at {}: {e:?}", path).into(),
}) })
} }
let results: Vec<Result<AbsoluteFilePath, RmError>> = join_all(
paths
.into_iter()
.map(|path| _rm(&self, path, force))
.collect::<Vec<_>>(),
)
.await;
results
}
async fn mv(&self, request: MvRequest) -> Result<(), MvError> { async fn mv(&self, request: MvRequest) -> Result<(), MvError> {
self.mv(request.path(), request.target_path()) self.mv(request.path(), request.target_path())
.await .await

View File

@@ -87,11 +87,26 @@ impl WarrenNotifier for NotifierDebugLogger {
} }
async fn warren_rm(&self, response: &WarrenRmResponse) { async fn warren_rm(&self, response: &WarrenRmResponse) {
tracing::debug!( let span = tracing::debug_span!("warren_rm", "{}", response.warren().name()).entered();
"[Notifier] Deleted file {} from warren {}",
response.path(), let results = response.results();
response.warren().name(),
); for result in results {
match result.as_ref() {
Ok(path) => tracing::debug!("Deleted file: {path}"),
Err(e) => match e {
crate::domain::warren::models::file::RmError::NotFound(path) => {
tracing::debug!("File not found: {path}")
}
crate::domain::warren::models::file::RmError::NotEmpty(path) => {
tracing::debug!("Directory not empty: {path}")
}
crate::domain::warren::models::file::RmError::Unknown(_) => (),
},
}
}
span.exit();
} }
async fn warren_mv(&self, response: &WarrenMvResponse) { async fn warren_mv(&self, response: &WarrenMvResponse) {
@@ -392,9 +407,11 @@ impl AuthNotifier for NotifierDebugLogger {
} }
async fn auth_warren_rm(&self, user: &User, response: &WarrenRmResponse) { async fn auth_warren_rm(&self, user: &User, response: &WarrenRmResponse) {
let results = response.results();
let successes = results.iter().filter(|r| r.is_ok()).count();
tracing::debug!( tracing::debug!(
"[Notifier] Deleted file {} from warren {} for authenticated user {}", "[Notifier] Deleted {successes} file(s) from warren {} for authenticated user {}",
response.path(),
response.warren().name(), response.warren().name(),
user.id(), user.id(),
); );

View File

@@ -6,7 +6,6 @@ import {
ContextMenuItem, ContextMenuItem,
ContextMenuSeparator, ContextMenuSeparator,
} from '@/components/ui/context-menu'; } from '@/components/ui/context-menu';
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
import type { DirectoryEntry } from '#shared/types'; import type { DirectoryEntry } from '#shared/types';
const warrenStore = useWarrenStore(); const warrenStore = useWarrenStore();
@@ -26,9 +25,9 @@ const {
const emit = defineEmits<{ const emit = defineEmits<{
'entry-click': [entry: DirectoryEntry, event: MouseEvent]; 'entry-click': [entry: DirectoryEntry, event: MouseEvent];
'entry-download': [entry: DirectoryEntry]; 'entry-download': [entry: DirectoryEntry];
'entry-delete': [entry: DirectoryEntry, force: boolean];
}>(); }>();
const deleting = ref(false);
const isCopied = computed( const isCopied = computed(
() => () =>
warrenStore.current != null && warrenStore.current != null &&
@@ -39,32 +38,11 @@ const isCopied = computed(
); );
const isSelected = computed(() => warrenStore.isSelected(entry)); const isSelected = computed(() => warrenStore.isSelected(entry));
async function submitDelete(force: boolean = false) { function onDelete(force: boolean = false) {
if (warrenStore.current == null) { emit('entry-delete', entry, force);
return;
}
deleting.value = true;
if (entry.fileType === 'directory') {
await deleteWarrenDirectory(
warrenStore.current.warrenId,
warrenStore.current.path,
entry.name,
force
);
} else {
await deleteWarrenFile(
warrenStore.current.warrenId,
warrenStore.current.path,
entry.name
);
}
deleting.value = false;
} }
async function openRenameDialog() { function openRenameDialog() {
renameDialog.openDialog(entry); renameDialog.openDialog(entry);
} }
@@ -183,15 +161,14 @@ function onClearCopy() {
<ContextMenuItem <ContextMenuItem
:class="[warrenStore.current == null && 'hidden']" :class="[warrenStore.current == null && 'hidden']"
@select="() => submitDelete(false)" @select="() => onDelete(false)"
> >
<Icon name="lucide:trash-2" /> <Icon name="lucide:trash-2" />
Delete Delete
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem <ContextMenuItem
v-if="entry.fileType === 'directory'"
:class="[warrenStore.current == null && 'hidden']" :class="[warrenStore.current == null && 'hidden']"
@select="() => submitDelete(true)" @select="() => onDelete(true)"
> >
<Icon <Icon
class="text-destructive-foreground" class="text-destructive-foreground"

View File

@@ -19,6 +19,7 @@ const {
const emit = defineEmits<{ const emit = defineEmits<{
'entry-click': [entry: DirectoryEntry, event: MouseEvent]; 'entry-click': [entry: DirectoryEntry, event: MouseEvent];
'entry-download': [entry: DirectoryEntry]; 'entry-download': [entry: DirectoryEntry];
'entry-delete': [entry: DirectoryEntry, force: boolean];
back: []; back: [];
}>(); }>();
@@ -35,6 +36,10 @@ function onEntryClicked(entry: DirectoryEntry, event: MouseEvent) {
function onEntryDownload(entry: DirectoryEntry) { function onEntryDownload(entry: DirectoryEntry) {
emit('entry-download', entry); emit('entry-download', entry);
} }
function onEntryDelete(entry: DirectoryEntry, force: boolean) {
emit('entry-delete', entry, force);
}
</script> </script>
<template> <template>
@@ -61,6 +66,7 @@ function onEntryDownload(entry: DirectoryEntry) {
:draggable="entriesDraggable" :draggable="entriesDraggable"
@entry-click="onEntryClicked" @entry-click="onEntryClicked"
@entry-download="onEntryDownload" @entry-download="onEntryDownload"
@entry-delete="onEntryDelete"
/> />
</div> </div>
</ScrollArea> </ScrollArea>

View File

@@ -87,6 +87,40 @@ export async function createDirectory(
return { success: true }; return { success: true };
} }
export async function warrenRm(
warrenId: string,
paths: string[],
force: boolean
): Promise<{ success: boolean }> {
const { status } = await useFetch(getApiUrl(`warrens/files/rm`), {
method: 'POST',
headers: getApiHeaders(),
body: JSON.stringify({
warrenId,
paths,
force,
}),
});
const TOAST_TITLE = 'Delete';
if (status.value !== 'success') {
toast.error(TOAST_TITLE, {
id: 'WARREN_RM_TOAST',
description: `Failed to delete directory`,
});
return { success: false };
}
await refreshNuxtData('current-directory');
toast.success(TOAST_TITLE, {
id: 'WARREN_RM_TOAST',
description: `Successfully deleted files`,
});
return { success: true };
}
export async function deleteWarrenDirectory( export async function deleteWarrenDirectory(
warrenId: string, warrenId: string,
path: string, path: string,
@@ -104,7 +138,7 @@ export async function deleteWarrenDirectory(
headers: getApiHeaders(), headers: getApiHeaders(),
body: JSON.stringify({ body: JSON.stringify({
warrenId, warrenId,
path, paths: [path],
force, force,
}), }),
}); });
@@ -113,7 +147,7 @@ export async function deleteWarrenDirectory(
if (status.value !== 'success') { if (status.value !== 'success') {
toast.error(TOAST_TITLE, { toast.error(TOAST_TITLE, {
id: 'DELETE_DIRECTORY_TOAST', id: 'WARREN_RM_TOAST',
description: `Failed to delete directory`, description: `Failed to delete directory`,
}); });
return { success: false }; return { success: false };
@@ -122,7 +156,7 @@ export async function deleteWarrenDirectory(
await refreshNuxtData('current-directory'); await refreshNuxtData('current-directory');
toast.success(TOAST_TITLE, { toast.success(TOAST_TITLE, {
id: 'DELETE_DIRECTORY_TOAST', id: 'WARREN_RM_TOAST',
description: `Successfully deleted ${directoryName}`, description: `Successfully deleted ${directoryName}`,
}); });
return { success: true }; return { success: true };
@@ -144,7 +178,7 @@ export async function deleteWarrenFile(
headers: getApiHeaders(), headers: getApiHeaders(),
body: JSON.stringify({ body: JSON.stringify({
warrenId, warrenId,
path, paths: [path],
force: false, force: false,
}), }),
}); });
@@ -153,7 +187,7 @@ export async function deleteWarrenFile(
if (status.value !== 'success') { if (status.value !== 'success') {
toast.error(TOAST_TITLE, { toast.error(TOAST_TITLE, {
id: 'DELETE_FILE_TOAST', id: 'WARREN_RM_TOAST',
description: `Failed to delete file`, description: `Failed to delete file`,
}); });
return { success: false }; return { success: false };
@@ -162,7 +196,7 @@ export async function deleteWarrenFile(
await refreshNuxtData('current-directory'); await refreshNuxtData('current-directory');
toast.success(TOAST_TITLE, { toast.success(TOAST_TITLE, {
id: 'DELETE_FILE_TOAST', id: 'WARREN_RM_TOAST',
description: `Successfully deleted ${fileName}`, description: `Successfully deleted ${fileName}`,
}); });
return { success: true }; return { success: true };

View File

@@ -3,7 +3,13 @@ import { useDropZone } from '@vueuse/core';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import DirectoryListContextMenu from '~/components/DirectoryListContextMenu.vue'; import DirectoryListContextMenu from '~/components/DirectoryListContextMenu.vue';
import RenameEntryDialog from '~/components/actions/RenameEntryDialog.vue'; import RenameEntryDialog from '~/components/actions/RenameEntryDialog.vue';
import { fetchFile, getWarrenDirectory } from '~/lib/api/warrens'; import {
deleteWarrenDirectory,
deleteWarrenFile,
fetchFile,
getWarrenDirectory,
warrenRm,
} from '~/lib/api/warrens';
import type { DirectoryEntry } from '~/shared/types'; import type { DirectoryEntry } from '~/shared/types';
definePageMeta({ definePageMeta({
@@ -119,27 +125,39 @@ function onEntryDownload(entry: DirectoryEntry) {
let downloadName: string; let downloadName: string;
let downloadApiUrl: string; let downloadApiUrl: string;
const selectionSize = warrenStore.selection.size; const targets = getTargetsFromSelection(entry, warrenStore.selection);
if (targets.length === 1) {
if (selectionSize === 0 || !warrenStore.isSelected(entry)) {
downloadName = downloadName =
entry.fileType === 'directory' ? `${entry.name}.zip` : entry.name; entry.fileType === 'directory'
? `${targets[0].name}.zip`
: targets[0].name;
downloadApiUrl = getApiUrl( downloadApiUrl = getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${joinPaths(warrenStore.current.path, entry.name)}` `warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${joinPaths(warrenStore.current.path, targets[0].name)}`
); );
} else { } else {
downloadName = 'download.zip'; downloadName = 'download.zip';
const paths = Array.from(warrenStore.selection).map((entry) => const paths = targets
joinPaths(warrenStore.current!.path, entry.name) .map((entry) => joinPaths(warrenStore.current!.path, entry.name))
); .join(':');
downloadApiUrl = getApiUrl( downloadApiUrl = getApiUrl(
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${paths.join(':')}` `warrens/files/cat?warrenId=${warrenStore.current.warrenId}&paths=${paths}`
); );
} }
downloadFile(downloadName, downloadApiUrl); downloadFile(downloadName, downloadApiUrl);
} }
async function onEntryDelete(entry: DirectoryEntry, force: boolean) {
if (warrenStore.current == null) {
return;
}
const targets = getTargetsFromSelection(entry, warrenStore.selection).map(
(entry) => joinPaths(warrenStore.current!.path, entry.name)
);
await warrenRm(warrenStore.current.warrenId, targets, force);
}
function onBack() { function onBack() {
warrenStore.backCurrentPath(); warrenStore.backCurrentPath();
@@ -162,6 +180,7 @@ function onBack() {
:parent="warrenStore.current.dir.parent" :parent="warrenStore.current.dir.parent"
@entry-click="onEntryClicked" @entry-click="onEntryClicked"
@entry-download="onEntryDownload" @entry-download="onEntryDownload"
@entry-delete="onEntryDelete"
@back="onBack" @back="onBack"
/> />
</DirectoryListContextMenu> </DirectoryListContextMenu>

View File

@@ -0,0 +1,17 @@
import type { DirectoryEntry } from '~/shared/types';
/** Converts a selection and the entry that triggered an action into the target entries
* @param targetEntry - The entry that triggered an action
* @param selection - The selected entries
* @returns If there are no selected elements or the target was not included only the target is returned. Otherwise the selection is returned
*/
export function getTargetsFromSelection(
targetEntry: DirectoryEntry,
selection: Set<DirectoryEntry>
): DirectoryEntry[] {
if (selection.size === 0 || !selection.has(targetEntry)) {
return [targetEntry];
}
return Array.from(selection);
}