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 {
|
pub enum AbsoluteFilePathListError {
|
||||||
#[error("A list must not be empty")]
|
#[error("A list must not be empty")]
|
||||||
Empty,
|
Empty,
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
function openRenameDialog() {
|
||||||
|
|
||||||
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() {
|
|
||||||
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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
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