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 {
#[error("A list must not be empty")]
Empty,

View File

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

View File

@@ -221,12 +221,15 @@ impl WarrenRmRequest {
pub fn build_fs_request(self, warren: &Warren) -> RmRequest {
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 {
warren: Warren,
path: AbsoluteFilePath,
results: Vec<Result<AbsoluteFilePath, RmError>>,
}
impl WarrenRmResponse {
pub fn new(warren: Warren, path: AbsoluteFilePath) -> Self {
Self { warren, path }
pub fn new(warren: Warren, results: Vec<Result<AbsoluteFilePath, RmError>>) -> Self {
Self { warren, results }
}
pub fn warren(&self) -> &Warren {
&self.warren
}
pub fn path(&self) -> &AbsoluteFilePath {
&self.path
pub fn results(&self) -> &Vec<Result<AbsoluteFilePath, RmError>> {
&self.results
}
}
#[derive(Debug, Error)]
pub enum WarrenRmError {
#[error(transparent)]
FileSystem(#[from] RmError),
#[error(transparent)]
FetchWarren(#[from] FetchWarrenError),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}
pub struct WarrenSaveRequest<'s> {

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
use crate::domain::warren::{
models::file::{
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream,
LsError, LsRequest, LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError,
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
TouchError, TouchRequest,
},
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
};
@@ -81,18 +82,19 @@ where
result
}
async fn rm(&self, request: RmRequest) -> Result<(), RmError> {
let path = request.path().clone();
let result = self.repository.rm(request).await;
async fn rm(&self, request: RmRequest) -> Vec<Result<AbsoluteFilePath, RmError>> {
let results = self.repository.rm(request).await;
if result.is_ok() {
self.metrics.record_rm_success().await;
self.notifier.rm(&path).await;
} else {
self.metrics.record_rm_failure().await;
for result in results.iter() {
if let Ok(path) = result.as_ref() {
self.metrics.record_rm_success().await;
self.notifier.rm(path).await;
} else {
self.metrics.record_rm_failure().await;
}
}
result
results
}
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> {
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 result = self
.fs_service
.rm(rm_request)
.await
.map(|_| WarrenRmResponse::new(warren, path))
.map_err(Into::into);
let response = WarrenRmResponse::new(warren, self.fs_service.rm(rm_request).await);
if let Ok(response) = result.as_ref() {
self.metrics.record_warren_rm_success().await;
self.notifier.warren_rm(response).await;
} else {
self.metrics.record_warren_rm_failure().await;
}
self.metrics.record_warren_rm_success().await;
self.notifier.warren_rm(&response).await;
result
Ok(response)
}
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 {
fn from(value: RmError) -> Self {
match value {
RmError::NotFound => Self::NotFound("The directory does not exist".to_string()),
RmError::NotEmpty => Self::BadRequest("The directory is not empty".to_string()),
RmError::NotFound(_) => {
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()),
}
}
@@ -51,9 +55,7 @@ impl From<RmError> for ApiError {
impl From<WarrenRmError> for ApiError {
fn from(value: WarrenRmError) -> Self {
match value {
WarrenRmError::FileSystem(fs) => fs.into(),
WarrenRmError::FetchWarren(err) => err.into(),
WarrenRmError::Unknown(error) => Self::InternalServerError(error.to_string()),
}
}
}

View File

@@ -7,7 +7,10 @@ use crate::{
domain::warren::{
models::{
auth_session::AuthRequest,
file::{AbsoluteFilePathError, FilePath, FilePathError, RmRequest},
file::{
AbsoluteFilePath, AbsoluteFilePathError, AbsoluteFilePathList,
AbsoluteFilePathListError, FilePath, FilePathError, RmRequest,
},
warren::WarrenRmRequest,
},
ports::{AuthService, WarrenService},
@@ -23,7 +26,7 @@ use crate::{
#[serde(rename_all = "camelCase")]
pub(super) struct WarrenRmHttpRequestBody {
warren_id: Uuid,
path: String,
paths: Vec<String>,
force: bool,
}
@@ -33,6 +36,8 @@ pub(super) enum ParseWarrenRmHttpRequestError {
FilePath(#[from] FilePathError),
#[error(transparent)]
AbsoluteFilePath(#[from] AbsoluteFilePathError),
#[error(transparent)]
AbsoluteFilePathList(#[from] AbsoluteFilePathListError),
}
impl From<ParseWarrenRmHttpRequestError> for ApiError {
@@ -40,12 +45,17 @@ impl From<ParseWarrenRmHttpRequestError> for ApiError {
match value {
ParseWarrenRmHttpRequestError::FilePath(err) => match err {
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 {
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 {
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(
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 futures_util::TryStreamExt;
use futures_util::{TryStreamExt, future::join_all};
use rustix::fs::{Statx, statx};
use std::{
collections::HashSet,
@@ -182,10 +182,10 @@ impl FileSystem {
/// 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
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() {
return fs::remove_file(&file_path).await;
@@ -412,14 +412,34 @@ impl FileSystemRepository for FileSystem {
})
}
async fn rm(&self, request: RmRequest) -> Result<(), RmError> {
self.rm(request.path(), request.force())
.await
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => RmError::NotFound,
std::io::ErrorKind::DirectoryNotEmpty => RmError::NotEmpty,
_ => anyhow!("Failed to delete file at {}: {e:?}", request.path()).into(),
})
async fn rm(&self, request: RmRequest) -> Vec<Result<AbsoluteFilePath, RmError>> {
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
.map(|_| path.clone())
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => RmError::NotFound(path),
std::io::ErrorKind::DirectoryNotEmpty => RmError::NotEmpty(path),
_ => 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> {

View File

@@ -87,11 +87,26 @@ impl WarrenNotifier for NotifierDebugLogger {
}
async fn warren_rm(&self, response: &WarrenRmResponse) {
tracing::debug!(
"[Notifier] Deleted file {} from warren {}",
response.path(),
response.warren().name(),
);
let span = tracing::debug_span!("warren_rm", "{}", response.warren().name()).entered();
let results = response.results();
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) {
@@ -392,9 +407,11 @@ impl AuthNotifier for NotifierDebugLogger {
}
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!(
"[Notifier] Deleted file {} from warren {} for authenticated user {}",
response.path(),
"[Notifier] Deleted {successes} file(s) from warren {} for authenticated user {}",
response.warren().name(),
user.id(),
);

View File

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

View File

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

View File

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

View File

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