delete multiple files with selection
This commit is contained in:
@@ -48,7 +48,7 @@ impl AbsoluteFilePathList {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum AbsoluteFilePathListError {
|
||||
#[error("A list must not be empty")]
|
||||
Empty,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
for result in results.iter() {
|
||||
if let Ok(path) = result.as_ref() {
|
||||
self.metrics.record_rm_success().await;
|
||||
self.notifier.rm(&path).await;
|
||||
self.notifier.rm(path).await;
|
||||
} else {
|
||||
self.metrics.record_rm_failure().await;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
results
|
||||
}
|
||||
|
||||
async fn mv(&self, request: MvRequest) -> Result<(), MvError> {
|
||||
|
||||
@@ -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.notifier.warren_rm(&response).await;
|
||||
|
||||
result
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn warren_mv(&self, request: WarrenMvRequest) -> Result<WarrenMvResponse, WarrenMvError> {
|
||||
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 +412,36 @@ impl FileSystemRepository for FileSystem {
|
||||
})
|
||||
}
|
||||
|
||||
async fn rm(&self, request: RmRequest) -> Result<(), RmError> {
|
||||
self.rm(request.path(), request.force())
|
||||
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,
|
||||
std::io::ErrorKind::DirectoryNotEmpty => RmError::NotEmpty,
|
||||
_ => anyhow!("Failed to delete file at {}: {e:?}", request.path()).into(),
|
||||
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> {
|
||||
self.mv(request.path(), request.target_path())
|
||||
.await
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
function onDelete(force: boolean = false) {
|
||||
emit('entry-delete', entry, force);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
17
frontend/utils/selection.ts
Normal file
17
frontend/utils/selection.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user