directory back up (parent) button + drag entry into parent to move
This commit is contained in:
@@ -1,21 +1,29 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::domain::warren::models::file::AbsoluteFilePath;
|
use crate::domain::warren::models::file::{AbsoluteFilePath, File};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct LsRequest {
|
pub struct LsRequest {
|
||||||
path: AbsoluteFilePath,
|
path: AbsoluteFilePath,
|
||||||
|
include_parent: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LsRequest {
|
impl LsRequest {
|
||||||
pub fn new(path: AbsoluteFilePath) -> Self {
|
pub fn new(path: AbsoluteFilePath, include_parent: bool) -> Self {
|
||||||
Self { path }
|
Self {
|
||||||
|
path,
|
||||||
|
include_parent,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path(&self) -> &AbsoluteFilePath {
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn include_parent(&self) -> bool {
|
||||||
|
self.include_parent
|
||||||
|
}
|
||||||
|
|
||||||
pub fn into_path(self) -> AbsoluteFilePath {
|
pub fn into_path(self) -> AbsoluteFilePath {
|
||||||
self.path
|
self.path
|
||||||
}
|
}
|
||||||
@@ -28,3 +36,23 @@ pub enum LsError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct LsResponse {
|
||||||
|
files: Vec<File>,
|
||||||
|
parent: Option<File>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LsResponse {
|
||||||
|
pub fn new(files: Vec<File>, parent: Option<File>) -> Self {
|
||||||
|
Self { files, parent }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn files(&self) -> &Vec<File> {
|
||||||
|
&self.files
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent(&self) -> Option<&File> {
|
||||||
|
self.parent.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ use futures_util::StreamExt;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::file::LsResponse;
|
||||||
use crate::domain::warren::models::file::SaveResponse;
|
use crate::domain::warren::models::file::SaveResponse;
|
||||||
use crate::domain::warren::models::file::{
|
use crate::domain::warren::models::file::{
|
||||||
AbsoluteFilePath, CatError, CatRequest, File, FileName, LsError, LsRequest, MkdirError,
|
AbsoluteFilePath, CatError, CatRequest, FileName, LsError, LsRequest, MkdirError, MkdirRequest,
|
||||||
MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, TouchError,
|
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, TouchError, TouchRequest,
|
||||||
TouchRequest,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{Warren, WarrenName};
|
use super::{Warren, WarrenName};
|
||||||
@@ -77,12 +77,13 @@ impl WarrenLsRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build_fs_request(self, warren: &Warren) -> LsRequest {
|
pub fn build_fs_request(self, warren: &Warren) -> LsRequest {
|
||||||
|
let include_parent = self.base.include_parent();
|
||||||
let path = warren
|
let path = warren
|
||||||
.path()
|
.path()
|
||||||
.clone()
|
.clone()
|
||||||
.join(&self.base.into_path().to_relative());
|
.join(&self.base.into_path().to_relative());
|
||||||
|
|
||||||
LsRequest::new(path)
|
LsRequest::new(path, include_parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,16 +97,12 @@ impl Into<FetchWarrenRequest> for WarrenLsRequest {
|
|||||||
pub struct WarrenLsResponse {
|
pub struct WarrenLsResponse {
|
||||||
warren: Warren,
|
warren: Warren,
|
||||||
path: AbsoluteFilePath,
|
path: AbsoluteFilePath,
|
||||||
files: Vec<File>,
|
base: LsResponse,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WarrenLsResponse {
|
impl WarrenLsResponse {
|
||||||
pub fn new(warren: Warren, path: AbsoluteFilePath, files: Vec<File>) -> Self {
|
pub fn new(warren: Warren, path: AbsoluteFilePath, base: LsResponse) -> Self {
|
||||||
Self {
|
Self { warren, path, base }
|
||||||
warren,
|
|
||||||
path,
|
|
||||||
files,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren(&self) -> &Warren {
|
pub fn warren(&self) -> &Warren {
|
||||||
@@ -116,8 +113,8 @@ impl WarrenLsResponse {
|
|||||||
&self.path
|
&self.path
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn files(&self) -> &Vec<File> {
|
pub fn base(&self) -> &LsResponse {
|
||||||
&self.files
|
&self.base
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use super::models::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
file::{
|
file::{
|
||||||
CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest,
|
CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
|
||||||
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
|
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
|
||||||
TouchRequest,
|
TouchRequest,
|
||||||
},
|
},
|
||||||
@@ -103,7 +103,7 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||||
fn ls(&self, request: LsRequest) -> impl Future<Output = Result<Vec<File>, LsError>> + Send;
|
fn ls(&self, request: LsRequest) -> impl Future<Output = Result<LsResponse, LsError>> + Send;
|
||||||
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;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, File},
|
file::{AbsoluteFilePath, LsResponse},
|
||||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
warren::{
|
warren::{
|
||||||
@@ -46,7 +46,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, files: &Vec<File>) -> impl Future<Output = ()> + Send;
|
fn ls(&self, response: &LsResponse) -> impl Future<Output = ()> + Send;
|
||||||
fn cat(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
fn cat(&self, path: &AbsoluteFilePath) -> 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;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use crate::domain::warren::models::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
file::{
|
file::{
|
||||||
CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest,
|
CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
|
||||||
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
|
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
|
||||||
TouchRequest,
|
TouchRequest,
|
||||||
},
|
},
|
||||||
@@ -66,7 +66,7 @@ pub trait WarrenRepository: Clone + Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
||||||
fn ls(&self, request: LsRequest) -> impl Future<Output = Result<Vec<File>, LsError>> + Send;
|
fn ls(&self, request: LsRequest) -> impl Future<Output = Result<LsResponse, LsError>> + Send;
|
||||||
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;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::file::{
|
models::file::{
|
||||||
CatError, CatRequest, File, FileStream, LsError, LsRequest, MkdirError, MkdirRequest,
|
CatError, CatRequest, FileStream, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
|
||||||
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
|
MvError, MvRequest, RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError,
|
||||||
TouchRequest,
|
TouchRequest,
|
||||||
},
|
},
|
||||||
@@ -40,7 +40,7 @@ where
|
|||||||
M: FileSystemMetrics,
|
M: FileSystemMetrics,
|
||||||
N: FileSystemNotifier,
|
N: FileSystemNotifier,
|
||||||
{
|
{
|
||||||
async fn ls(&self, request: LsRequest) -> Result<Vec<File>, LsError> {
|
async fn ls(&self, request: LsRequest) -> Result<LsResponse, LsError> {
|
||||||
let result = self.repository.ls(request).await;
|
let result = self.repository.ls(request).await;
|
||||||
|
|
||||||
if let Ok(files) = result.as_ref() {
|
if let Ok(files) = result.as_ref() {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType,
|
AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType,
|
||||||
LsRequest,
|
LsRequest,
|
||||||
},
|
},
|
||||||
warren::WarrenLsRequest,
|
warren::{WarrenLsRequest, WarrenLsResponse},
|
||||||
},
|
},
|
||||||
ports::{AuthService, WarrenService},
|
ports::{AuthService, WarrenService},
|
||||||
},
|
},
|
||||||
@@ -41,7 +41,10 @@ impl WarrenLsHttpRequestBody {
|
|||||||
fn try_into_domain(self) -> Result<WarrenLsRequest, ParseWarrenLsHttpRequestError> {
|
fn try_into_domain(self) -> Result<WarrenLsRequest, ParseWarrenLsHttpRequestError> {
|
||||||
let path = FilePath::new(&self.path)?.try_into()?;
|
let path = FilePath::new(&self.path)?.try_into()?;
|
||||||
|
|
||||||
Ok(WarrenLsRequest::new(self.warren_id, LsRequest::new(path)))
|
Ok(WarrenLsRequest::new(
|
||||||
|
self.warren_id,
|
||||||
|
LsRequest::new(path, &self.path != "/"),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +78,7 @@ pub struct WarrenFileElement {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ListWarrenFilesResponseData {
|
pub struct ListWarrenFilesResponseData {
|
||||||
files: Vec<WarrenFileElement>,
|
files: Vec<WarrenFileElement>,
|
||||||
|
parent: Option<WarrenFileElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&File> for WarrenFileElement {
|
impl From<&File> for WarrenFileElement {
|
||||||
@@ -88,10 +92,16 @@ impl From<&File> for WarrenFileElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&Vec<File>> for ListWarrenFilesResponseData {
|
impl From<WarrenLsResponse> for ListWarrenFilesResponseData {
|
||||||
fn from(value: &Vec<File>) -> Self {
|
fn from(value: WarrenLsResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
files: value.iter().map(WarrenFileElement::from).collect(),
|
files: value
|
||||||
|
.base()
|
||||||
|
.files()
|
||||||
|
.iter()
|
||||||
|
.map(WarrenFileElement::from)
|
||||||
|
.collect(),
|
||||||
|
parent: value.base().parent().map(WarrenFileElement::from),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,6 +117,6 @@ pub async fn list_warren_files<WS: WarrenService, AS: AuthService>(
|
|||||||
.auth_service
|
.auth_service
|
||||||
.auth_warren_ls(domain_request, state.warren_service.as_ref())
|
.auth_warren_ls(domain_request, state.warren_service.as_ref())
|
||||||
.await
|
.await
|
||||||
.map(|response| ApiSuccess::new(StatusCode::OK, response.files().into()))
|
.map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ use crate::{
|
|||||||
models::{
|
models::{
|
||||||
file::{
|
file::{
|
||||||
AbsoluteFilePath, CatError, CatRequest, File, FileMimeType, FileName, FilePath,
|
AbsoluteFilePath, CatError, CatRequest, File, FileMimeType, FileName, FilePath,
|
||||||
FileStream, FileType, LsError, LsRequest, MkdirError, MkdirRequest, MvError,
|
FileStream, FileType, LsError, LsRequest, LsResponse, MkdirError, MkdirRequest,
|
||||||
MvRequest, RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest,
|
MvError, MvRequest, RelativeFilePath, RmError, RmRequest, SaveError, SaveRequest,
|
||||||
SaveResponse, TouchError, TouchRequest,
|
SaveResponse, TouchError, TouchRequest,
|
||||||
},
|
},
|
||||||
warren::UploadFileStream,
|
warren::UploadFileStream,
|
||||||
@@ -75,13 +75,38 @@ impl FileSystem {
|
|||||||
self.base_directory.join(&path.as_relative())
|
self.base_directory.join(&path.as_relative())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_all_files(&self, absolute_path: &AbsoluteFilePath) -> anyhow::Result<Vec<File>> {
|
async fn get_all_files(
|
||||||
let directory_path = self.get_target_path(absolute_path);
|
&self,
|
||||||
|
absolute_path: &AbsoluteFilePath,
|
||||||
let mut dir = fs::read_dir(&directory_path).await?;
|
include_parent: bool,
|
||||||
|
) -> anyhow::Result<LsResponse> {
|
||||||
|
let dir_path = self.get_target_path(absolute_path).as_ref().to_path_buf();
|
||||||
|
|
||||||
let mut files = Vec::new();
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
let parent = if include_parent {
|
||||||
|
let dir_name = FileName::new(
|
||||||
|
&dir_path
|
||||||
|
.file_name()
|
||||||
|
.context("Failed to get directory name")?
|
||||||
|
.to_owned()
|
||||||
|
.into_string()
|
||||||
|
.ok()
|
||||||
|
.context("Failed to get directory name")?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Some(File::new(
|
||||||
|
dir_name,
|
||||||
|
FileType::Directory,
|
||||||
|
None,
|
||||||
|
get_btime(&dir_path),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dir = fs::read_dir(&dir_path).await?;
|
||||||
|
|
||||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||||
let name = entry
|
let name = entry
|
||||||
.file_name()
|
.file_name()
|
||||||
@@ -100,18 +125,7 @@ impl FileSystem {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5
|
let created_at = get_btime(entry.path());
|
||||||
// https://github.com/rust-lang/rust/pull/142682
|
|
||||||
let created_at = unsafe {
|
|
||||||
statx(
|
|
||||||
std::os::fd::BorrowedFd::borrow_raw(-100),
|
|
||||||
entry.path(),
|
|
||||||
rustix::fs::AtFlags::empty(),
|
|
||||||
rustix::fs::StatxFlags::BTIME,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.ok()
|
|
||||||
.map(|statx| statx.stx_btime.tv_sec as u64);
|
|
||||||
|
|
||||||
let mime_type = match file_type {
|
let mime_type = match file_type {
|
||||||
FileType::File => FileMimeType::from_name(&name),
|
FileType::File => FileMimeType::from_name(&name),
|
||||||
@@ -126,7 +140,7 @@ impl FileSystem {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(files)
|
Ok(LsResponse::new(files, parent))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actually created a directory in the underlying file system
|
/// Actually created a directory in the underlying file system
|
||||||
@@ -233,13 +247,16 @@ impl FileSystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemRepository for FileSystem {
|
impl FileSystemRepository for FileSystem {
|
||||||
async fn ls(&self, request: LsRequest) -> Result<Vec<File>, LsError> {
|
async fn ls(&self, request: LsRequest) -> Result<LsResponse, LsError> {
|
||||||
let files = self.get_all_files(request.path()).await.map_err(|err| {
|
let files = self
|
||||||
anyhow!(err).context(format!(
|
.get_all_files(request.path(), request.include_parent())
|
||||||
"Failed to get the files at path: {}",
|
.await
|
||||||
request.path()
|
.map_err(|err| {
|
||||||
))
|
anyhow!(err).context(format!(
|
||||||
})?;
|
"Failed to get the files at path: {}",
|
||||||
|
request.path()
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(files)
|
Ok(files)
|
||||||
}
|
}
|
||||||
@@ -297,3 +314,21 @@ impl FileSystemRepository for FileSystem {
|
|||||||
Ok(self.save(&path, &mut stream).await.map(SaveResponse::new)?)
|
Ok(self.save(&path, &mut stream).await.map(SaveResponse::new)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5
|
||||||
|
// https://github.com/rust-lang/rust/pull/142682
|
||||||
|
fn get_btime<P>(path: P) -> Option<u64>
|
||||||
|
where
|
||||||
|
P: rustix::path::Arg,
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
statx(
|
||||||
|
std::os::fd::BorrowedFd::borrow_raw(-100),
|
||||||
|
path,
|
||||||
|
rustix::fs::AtFlags::empty(),
|
||||||
|
rustix::fs::StatxFlags::BTIME,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.ok()
|
||||||
|
.map(|statx| statx.stx_btime.tv_sec as u64)
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use uuid::Uuid;
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, File},
|
file::{AbsoluteFilePath, LsResponse},
|
||||||
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
warren::{
|
warren::{
|
||||||
@@ -57,7 +57,7 @@ impl WarrenNotifier for NotifierDebugLogger {
|
|||||||
async fn warren_ls(&self, response: &WarrenLsResponse) {
|
async fn warren_ls(&self, response: &WarrenLsResponse) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Listed {} file(s) in warren {}",
|
"[Notifier] Listed {} file(s) in warren {}",
|
||||||
response.files().len(),
|
response.base().files().len(),
|
||||||
response.warren().name()
|
response.warren().name()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -101,8 +101,8 @@ impl WarrenNotifier for NotifierDebugLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemNotifier for NotifierDebugLogger {
|
impl FileSystemNotifier for NotifierDebugLogger {
|
||||||
async fn ls(&self, files: &Vec<File>) {
|
async fn ls(&self, response: &LsResponse) {
|
||||||
tracing::debug!("[Notifier] Listed {} file(s)", files.len());
|
tracing::debug!("[Notifier] Listed {} file(s)", response.files().len());
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cat(&self, path: &AbsoluteFilePath) {
|
async fn cat(&self, path: &AbsoluteFilePath) {
|
||||||
@@ -279,7 +279,7 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
async fn auth_warren_ls(&self, user: &User, response: &WarrenLsResponse) {
|
async fn auth_warren_ls(&self, user: &User, response: &WarrenLsResponse) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"[Notifier] Listed {} file(s) in warren {} for authenticated user {}",
|
"[Notifier] Listed {} file(s) in warren {} for authenticated user {}",
|
||||||
response.files().len(),
|
response.base().files().len(),
|
||||||
response.warren().name(),
|
response.warren().name(),
|
||||||
user.id()
|
user.id()
|
||||||
);
|
);
|
||||||
|
|||||||
38
frontend/components/DirectoryBackEntry.vue
Normal file
38
frontend/components/DirectoryBackEntry.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
|
const { entry } = defineProps<{ entry: DirectoryEntry }>();
|
||||||
|
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
|
||||||
|
const onDrop = onDirectoryEntryDrop(entry, true);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="bg-accent/30 border-border flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none"
|
||||||
|
@contextmenu.prevent
|
||||||
|
@click="() => warrenStore.backCurrentPath()"
|
||||||
|
@drop="onDrop"
|
||||||
|
>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<Icon class="size-6" name="lucide:folder-up" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-col items-start justify-stretch gap-0 overflow-hidden text-left leading-6"
|
||||||
|
>
|
||||||
|
<span class="w-full truncate"
|
||||||
|
>..
|
||||||
|
<span class="text-muted-foreground truncate text-sm"
|
||||||
|
>({{ entry.name }})</span
|
||||||
|
></span
|
||||||
|
>
|
||||||
|
<NuxtTime
|
||||||
|
v-if="entry.createdAt != null"
|
||||||
|
:datetime="entry.createdAt * 1000"
|
||||||
|
class="text-muted-foreground w-full truncate text-sm"
|
||||||
|
relative
|
||||||
|
></NuxtTime>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
@@ -89,28 +89,7 @@ function onDragStart(e: DragEvent) {
|
|||||||
e.dataTransfer.dropEffect = 'move';
|
e.dataTransfer.dropEffect = 'move';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDrop(e: DragEvent) {
|
const onDrop = onDirectoryEntryDrop(entry);
|
||||||
if (e.dataTransfer == null || warrenStore.current == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.fileType !== 'directory') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileName = e.dataTransfer.getData('application/warren');
|
|
||||||
|
|
||||||
if (entry.name === fileName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await moveFile(
|
|
||||||
warrenStore.current.warrenId,
|
|
||||||
warrenStore.current.path,
|
|
||||||
fileName,
|
|
||||||
`${warrenStore.current.path}/${entry.name}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -118,7 +97,7 @@ async function onDrop(e: DragEvent) {
|
|||||||
<ContextMenuTrigger>
|
<ContextMenuTrigger>
|
||||||
<button
|
<button
|
||||||
:disabled="warrenStore.loading || disabled"
|
:disabled="warrenStore.loading || disabled"
|
||||||
class="bg-accent/30 border-border select-none, flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2"
|
class="bg-accent/30 border-border flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="onDragStart"
|
@dragstart="onDragStart"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import type { DirectoryEntry } from '#shared/types';
|
import type { DirectoryEntry } from '#shared/types';
|
||||||
|
|
||||||
const { entries, isOverDropZone } = defineProps<{
|
const { entries, parent, isOverDropZone } = defineProps<{
|
||||||
entries: DirectoryEntry[];
|
entries: DirectoryEntry[];
|
||||||
|
parent: DirectoryEntry | null;
|
||||||
isOverDropZone?: boolean;
|
isOverDropZone?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ const sortedEntries = computed(() =>
|
|||||||
<Icon class="size-16 animate-pulse" name="lucide:upload" />
|
<Icon class="size-16 animate-pulse" name="lucide:upload" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row flex-wrap gap-2">
|
<div class="flex flex-row flex-wrap gap-2">
|
||||||
|
<DirectoryBackEntry v-if="parent != null" :entry="parent" />
|
||||||
<DirectoryEntry
|
<DirectoryEntry
|
||||||
v-for="entry in sortedEntries"
|
v-for="entry in sortedEntries"
|
||||||
:key="entry.name"
|
:key="entry.name"
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ export function useWarrenPath() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${store.current.warrenId}/${store.current.path}`;
|
return `${store.current.warrenId}${store.current.path}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ export async function getWarrens(): Promise<Record<string, WarrenData>> {
|
|||||||
export async function getWarrenDirectory(
|
export async function getWarrenDirectory(
|
||||||
warrenId: string,
|
warrenId: string,
|
||||||
path: string
|
path: string
|
||||||
): Promise<DirectoryEntry[]> {
|
): Promise<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }> {
|
||||||
const { data, error } = await useFetch<
|
const { data, error } = await useFetch<
|
||||||
ApiResponse<{ files: DirectoryEntry[] }>
|
ApiResponse<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }>
|
||||||
>(getApiUrl(`warrens/files/ls`), {
|
>(getApiUrl(`warrens/files/ls`), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getApiHeaders(),
|
headers: getApiHeaders(),
|
||||||
@@ -44,9 +44,9 @@ export async function getWarrenDirectory(
|
|||||||
throw error.value?.name;
|
throw error.value?.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { files } = data.value.data;
|
const { files, parent } = data.value.data;
|
||||||
|
|
||||||
return files;
|
return { files, parent };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createDirectory(
|
export async function createDirectory(
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ if (warrenStore.current == null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = useAsyncData(
|
const dirData = useAsyncData(
|
||||||
'current-directory',
|
'current-directory',
|
||||||
async () => {
|
async () => {
|
||||||
if (warrenStore.current == null) {
|
if (warrenStore.current == null) {
|
||||||
@@ -37,7 +37,7 @@ const entries = useAsyncData(
|
|||||||
loadingIndicator.start();
|
loadingIndicator.start();
|
||||||
warrenStore.loading = true;
|
warrenStore.loading = true;
|
||||||
|
|
||||||
const entries = await getWarrenDirectory(
|
const { files, parent } = await getWarrenDirectory(
|
||||||
warrenStore.current.warrenId,
|
warrenStore.current.warrenId,
|
||||||
warrenStore.current.path
|
warrenStore.current.path
|
||||||
);
|
);
|
||||||
@@ -45,7 +45,7 @@ const entries = useAsyncData(
|
|||||||
warrenStore.loading = false;
|
warrenStore.loading = false;
|
||||||
loadingIndicator.finish();
|
loadingIndicator.finish();
|
||||||
|
|
||||||
return entries;
|
return { files, parent };
|
||||||
},
|
},
|
||||||
{ watch: [warrenPath] }
|
{ watch: [warrenPath] }
|
||||||
).data;
|
).data;
|
||||||
@@ -80,12 +80,13 @@ function onDrop(files: File[] | null, e: DragEvent) {
|
|||||||
<div ref="dropZoneRef" class="grow">
|
<div ref="dropZoneRef" class="grow">
|
||||||
<DirectoryListContextMenu class="w-full grow">
|
<DirectoryListContextMenu class="w-full grow">
|
||||||
<DirectoryList
|
<DirectoryList
|
||||||
v-if="entries != null"
|
v-if="dirData != null"
|
||||||
:is-over-drop-zone="
|
:is-over-drop-zone="
|
||||||
dropZone.isOverDropZone.value &&
|
dropZone.isOverDropZone.value &&
|
||||||
dropZone.files.value != null
|
dropZone.files.value != null
|
||||||
"
|
"
|
||||||
:entries="entries"
|
:entries="dirData.files"
|
||||||
|
:parent="dirData.parent"
|
||||||
/>
|
/>
|
||||||
</DirectoryListContextMenu>
|
</DirectoryListContextMenu>
|
||||||
<RenameEntryDialog />
|
<RenameEntryDialog />
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export type DirectoryEntry = {
|
|||||||
mimeType: string | null;
|
mimeType: string | null;
|
||||||
/// Timestamp in seconds
|
/// Timestamp in seconds
|
||||||
createdAt: number | null;
|
createdAt: number | null;
|
||||||
|
isParent: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UploadStatus =
|
export type UploadStatus =
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import type { DirectoryEntry } from '#shared/types';
|
import type { DirectoryEntry } from '#shared/types';
|
||||||
import type { WarrenData } from '#shared/types/warrens';
|
import type { WarrenData } from '#shared/types/warrens';
|
||||||
|
import { getParentPath } from '~/utils/files';
|
||||||
|
|
||||||
export const useWarrenStore = defineStore('warrens', {
|
export const useWarrenStore = defineStore('warrens', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
@@ -29,6 +30,13 @@ export const useWarrenStore = defineStore('warrens', {
|
|||||||
|
|
||||||
this.current.path += path;
|
this.current.path += path;
|
||||||
},
|
},
|
||||||
|
backCurrentPath() {
|
||||||
|
if (this.current == null || this.current.path === '/') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.current.path = getParentPath(this.current.path);
|
||||||
|
},
|
||||||
setCurrentWarrenPath(path: string) {
|
setCurrentWarrenPath(path: string) {
|
||||||
if (this.current == null) {
|
if (this.current == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
49
frontend/utils/files.ts
Normal file
49
frontend/utils/files.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { moveFile } from '~/lib/api/warrens';
|
||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
|
export function getParentPath(path: string): string {
|
||||||
|
const sliceEnd = Math.max(1, path.lastIndexOf('/'));
|
||||||
|
return path.slice(0, sliceEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function onDirectoryEntryDrop(
|
||||||
|
entry: DirectoryEntry,
|
||||||
|
isParent: boolean = false
|
||||||
|
): (e: DragEvent) => void {
|
||||||
|
return async (e: DragEvent) => {
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
|
||||||
|
if (e.dataTransfer == null || warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.fileType !== 'directory') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = e.dataTransfer.getData('application/warren');
|
||||||
|
|
||||||
|
if (entry.name === fileName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let targetPath: string;
|
||||||
|
|
||||||
|
if (isParent) {
|
||||||
|
targetPath = getParentPath(warrenStore.current.path);
|
||||||
|
} else {
|
||||||
|
targetPath = warrenStore.current.path;
|
||||||
|
if (!targetPath.endsWith('/')) {
|
||||||
|
targetPath += '/';
|
||||||
|
}
|
||||||
|
targetPath += entry.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
await moveFile(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
warrenStore.current.path,
|
||||||
|
fileName,
|
||||||
|
targetPath
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user