basic file sharing
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE shares (
|
||||||
|
id UUID PRIMARY KEY DEFAULT GEN_RANDOM_UUID(),
|
||||||
|
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
warren_id UUID NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
|
||||||
|
path VARCHAR NOT NULL,
|
||||||
|
password_hash VARCHAR NOT NULL,
|
||||||
|
expires_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
21
backend/migrations/20250825115342_share_permissions.sql
Normal file
21
backend/migrations/20250825115342_share_permissions.sql
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
ALTER TABLE
|
||||||
|
user_warrens
|
||||||
|
ADD COLUMN
|
||||||
|
can_list_shares BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN
|
||||||
|
can_create_shares BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN
|
||||||
|
can_modify_shares BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN
|
||||||
|
can_delete_shares BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
ALTER TABLE
|
||||||
|
user_warrens
|
||||||
|
ALTER COLUMN
|
||||||
|
can_list_shares DROP DEFAULT,
|
||||||
|
ALTER COLUMN
|
||||||
|
can_create_shares DROP DEFAULT,
|
||||||
|
ALTER COLUMN
|
||||||
|
can_modify_shares DROP DEFAULT,
|
||||||
|
ALTER COLUMN
|
||||||
|
can_delete_shares DROP DEFAULT;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE shares ALTER COLUMN password_hash DROP NOT NULL;
|
||||||
1
backend/migrations/20250825150026_shares_path_index.sql
Normal file
1
backend/migrations/20250825150026_shares_path_index.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX idx_shares_path ON shares(path);
|
||||||
@@ -69,6 +69,10 @@ impl FetchAuthSessionResponse {
|
|||||||
pub fn user(&self) -> &User {
|
pub fn user(&self) -> &User {
|
||||||
&self.user
|
&self.user
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn unpack(self) -> (AuthSession, User) {
|
||||||
|
(self.session, self.user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FetchAuthSessionRequest {
|
impl FetchAuthSessionRequest {
|
||||||
|
|||||||
@@ -142,6 +142,10 @@ impl FilePath {
|
|||||||
self.0.push('/');
|
self.0.push('/');
|
||||||
self.0.push_str(&other.0);
|
self.0.push_str(&other.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<Path> for FilePath {
|
impl AsRef<Path> for FilePath {
|
||||||
@@ -172,6 +176,10 @@ impl RelativeFilePath {
|
|||||||
self.0.push_str(&other.0);
|
self.0.push_str(&other.0);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AbsoluteFilePath {
|
impl AbsoluteFilePath {
|
||||||
@@ -199,6 +207,10 @@ impl AbsoluteFilePath {
|
|||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Error)]
|
#[derive(Clone, Debug, Error)]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ mod mkdir;
|
|||||||
mod mv;
|
mod mv;
|
||||||
mod rm;
|
mod rm;
|
||||||
mod save;
|
mod save;
|
||||||
|
mod stat;
|
||||||
mod touch;
|
mod touch;
|
||||||
|
|
||||||
pub use cat::*;
|
pub use cat::*;
|
||||||
@@ -14,4 +15,5 @@ pub use mkdir::*;
|
|||||||
pub use mv::*;
|
pub use mv::*;
|
||||||
pub use rm::*;
|
pub use rm::*;
|
||||||
pub use save::*;
|
pub use save::*;
|
||||||
|
pub use stat::*;
|
||||||
pub use touch::*;
|
pub use touch::*;
|
||||||
|
|||||||
51
backend/src/lib/domain/warren/models/file/requests/stat.rs
Normal file
51
backend/src/lib/domain/warren/models/file/requests/stat.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::file::{AbsoluteFilePath, File};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct StatRequest {
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatRequest {
|
||||||
|
pub fn new(path: AbsoluteFilePath) -> Self {
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_path(self) -> AbsoluteFilePath {
|
||||||
|
self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum StatError {
|
||||||
|
#[error("The file does not exist")]
|
||||||
|
NotFound,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct StatResponse {
|
||||||
|
file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatResponse {
|
||||||
|
pub fn new(file: File) -> Self {
|
||||||
|
Self { file }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file(&self) -> &File {
|
||||||
|
&self.file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StatResponse> for File {
|
||||||
|
fn from(value: StatResponse) -> Self {
|
||||||
|
value.file
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod auth_session;
|
pub mod auth_session;
|
||||||
pub mod file;
|
pub mod file;
|
||||||
|
pub mod share;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_warren;
|
pub mod user_warren;
|
||||||
pub mod warren;
|
pub mod warren;
|
||||||
|
|||||||
134
backend/src/lib/domain/warren/models/share/mod.rs
Normal file
134
backend/src/lib/domain/warren/models/share/mod.rs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
mod requests;
|
||||||
|
pub use requests::*;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
file::{AbsoluteFilePath, StatRequest},
|
||||||
|
warren::Warren,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct Share {
|
||||||
|
id: Uuid,
|
||||||
|
creator_id: Uuid,
|
||||||
|
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
|
||||||
|
password_hash: Option<String>,
|
||||||
|
|
||||||
|
expires_at: Option<u64>,
|
||||||
|
|
||||||
|
created_at: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Share {
|
||||||
|
pub fn new(
|
||||||
|
id: Uuid,
|
||||||
|
creator_id: Uuid,
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
password_hash: Option<String>,
|
||||||
|
expires_at: Option<u64>,
|
||||||
|
created_at: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
creator_id,
|
||||||
|
warren_id,
|
||||||
|
path,
|
||||||
|
password_hash,
|
||||||
|
expires_at,
|
||||||
|
created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &Uuid {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn creator_id(&self) -> &Uuid {
|
||||||
|
&self.creator_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password_hash(&self) -> Option<&String> {
|
||||||
|
self.password_hash.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expires_at(&self) -> Option<u64> {
|
||||||
|
self.expires_at
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn created_at(&self) -> u64 {
|
||||||
|
self.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_fs_stat_request(&self, warren: &Warren) -> StatRequest {
|
||||||
|
let base_path = self.path.clone();
|
||||||
|
|
||||||
|
let path = warren.path().clone().join(&base_path.to_relative());
|
||||||
|
|
||||||
|
StatRequest::new(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A valid share password
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct SharePassword(String);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Error)]
|
||||||
|
pub enum SharePasswordError {
|
||||||
|
#[error("A share's password must not be empty")]
|
||||||
|
Empty,
|
||||||
|
#[error("A share's password must not start with a whitespace")]
|
||||||
|
LeadingWhitespace,
|
||||||
|
#[error("A share's password must not end with a whitespace")]
|
||||||
|
TrailingWhitespace,
|
||||||
|
#[error("A share's password must be longer")]
|
||||||
|
TooShort,
|
||||||
|
#[error("A share's password must be shorter")]
|
||||||
|
TooLong,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SharePassword {
|
||||||
|
const MIN_LENGTH: usize = 1;
|
||||||
|
const MAX_LENGTH: usize = 64;
|
||||||
|
|
||||||
|
pub fn new(raw: &str) -> Result<Self, SharePasswordError> {
|
||||||
|
if raw.is_empty() {
|
||||||
|
return Err(SharePasswordError::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.trim_start().len() != raw.len() {
|
||||||
|
return Err(SharePasswordError::LeadingWhitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.trim_end().len() != raw.len() {
|
||||||
|
return Err(SharePasswordError::TrailingWhitespace);
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.len() < Self::MIN_LENGTH {
|
||||||
|
return Err(SharePasswordError::TooShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw.len() > Self::MAX_LENGTH {
|
||||||
|
return Err(SharePasswordError::TooLong);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self(raw.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
104
backend/src/lib/domain/warren/models/share/requests/cat.rs
Normal file
104
backend/src/lib/domain/warren/models/share/requests/cat.rs
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
file::{AbsoluteFilePath, CatRequest, FileStream},
|
||||||
|
share::{Share, SharePassword},
|
||||||
|
warren::{FetchWarrenError, Warren, WarrenCatError, WarrenCatRequest},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{VerifySharePasswordError, VerifySharePasswordRequest};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ShareCatRequest {
|
||||||
|
share_id: Uuid,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
password: Option<SharePassword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareCatRequest {
|
||||||
|
pub fn new(share_id: Uuid, path: AbsoluteFilePath, password: Option<SharePassword>) -> Self {
|
||||||
|
Self {
|
||||||
|
share_id,
|
||||||
|
path,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share_id(&self) -> &Uuid {
|
||||||
|
&self.share_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> Option<&SharePassword> {
|
||||||
|
self.password.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_warren_cat_request(self, share: &Share, warren: &Warren) -> WarrenCatRequest {
|
||||||
|
let path = share.path().clone().join(&self.path.to_relative());
|
||||||
|
|
||||||
|
WarrenCatRequest::new(*warren.id(), CatRequest::new(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ShareCatRequest> for VerifySharePasswordRequest {
|
||||||
|
fn from(value: &ShareCatRequest) -> Self {
|
||||||
|
Self::new(value.share_id, value.password.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ShareCatResponse {
|
||||||
|
share: Share,
|
||||||
|
warren: Warren,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
stream: FileStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareCatResponse {
|
||||||
|
pub fn new(share: Share, warren: Warren, path: AbsoluteFilePath, stream: FileStream) -> Self {
|
||||||
|
Self {
|
||||||
|
share,
|
||||||
|
warren,
|
||||||
|
path,
|
||||||
|
stream,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share(&self) -> &Share {
|
||||||
|
&self.share
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warren(&self) -> &Warren {
|
||||||
|
&self.warren
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stream(&self) -> &FileStream {
|
||||||
|
&self.stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShareCatResponse> for FileStream {
|
||||||
|
fn from(value: ShareCatResponse) -> Self {
|
||||||
|
value.stream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ShareCatError {
|
||||||
|
#[error(transparent)]
|
||||||
|
VerifySharePassword(#[from] VerifySharePasswordError),
|
||||||
|
#[error(transparent)]
|
||||||
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
|
#[error(transparent)]
|
||||||
|
WarrenCat(#[from] WarrenCatError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
121
backend/src/lib/domain/warren/models/share/requests/create.rs
Normal file
121
backend/src/lib/domain/warren/models/share/requests/create.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
file::{AbsoluteFilePath, StatError, StatRequest},
|
||||||
|
share::{Share, SharePassword},
|
||||||
|
warren::{FetchWarrenError, FetchWarrenRequest, HasWarrenId, Warren},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct CreateShareRequest {
|
||||||
|
creator_id: Uuid,
|
||||||
|
base: CreateShareBaseRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateShareRequest {
|
||||||
|
pub fn new(creator_id: Uuid, base: CreateShareBaseRequest) -> Self {
|
||||||
|
Self { creator_id, base }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn creator_id(&self) -> &Uuid {
|
||||||
|
&self.creator_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn base(&self) -> &CreateShareBaseRequest {
|
||||||
|
&self.base
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_fs_stat_request(&self, warren: &Warren) -> StatRequest {
|
||||||
|
let base_path = self.base.path().clone();
|
||||||
|
|
||||||
|
let path = warren.path().clone().join(&base_path.to_relative());
|
||||||
|
|
||||||
|
StatRequest::new(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for CreateShareRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.base.warren_id()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<FetchWarrenRequest> for &CreateShareRequest {
|
||||||
|
fn into(self) -> FetchWarrenRequest {
|
||||||
|
FetchWarrenRequest::new(self.base.warren_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct CreateShareBaseRequest {
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
|
||||||
|
password: Option<SharePassword>,
|
||||||
|
|
||||||
|
lifetime: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateShareBaseRequest {
|
||||||
|
pub fn new(
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
password: Option<SharePassword>,
|
||||||
|
lifetime: Option<u64>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
warren_id,
|
||||||
|
path,
|
||||||
|
password,
|
||||||
|
lifetime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_share_request(self, creator_id: Uuid) -> CreateShareRequest {
|
||||||
|
CreateShareRequest::new(creator_id, self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> Option<&SharePassword> {
|
||||||
|
self.password.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lifetime(&self) -> Option<u64> {
|
||||||
|
self.lifetime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for CreateShareBaseRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct CreateShareResponse {
|
||||||
|
share: Share,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateShareResponse {
|
||||||
|
pub fn new(share: Share) -> Self {
|
||||||
|
Self { share }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share(&self) -> &Share {
|
||||||
|
&self.share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum CreateShareError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Stat(#[from] StatError),
|
||||||
|
#[error(transparent)]
|
||||||
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
share::Share,
|
||||||
|
warren::{FetchWarrenError, HasWarrenId},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct DeleteShareRequest {
|
||||||
|
warren_id: Uuid,
|
||||||
|
share_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteShareRequest {
|
||||||
|
pub fn new(warren_id: Uuid, share_id: Uuid) -> Self {
|
||||||
|
Self {
|
||||||
|
warren_id,
|
||||||
|
share_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share_id(&self) -> &Uuid {
|
||||||
|
&self.share_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for DeleteShareRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct DeleteShareResponse {
|
||||||
|
share: Share,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteShareResponse {
|
||||||
|
pub fn new(share: Share) -> Self {
|
||||||
|
Self { share }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share(&self) -> &Share {
|
||||||
|
&self.share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DeleteShareResponse> for Share {
|
||||||
|
fn from(value: DeleteShareResponse) -> Self {
|
||||||
|
value.share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum DeleteShareError {
|
||||||
|
#[error(transparent)]
|
||||||
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
59
backend/src/lib/domain/warren/models/share/requests/get.rs
Normal file
59
backend/src/lib/domain/warren/models/share/requests/get.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
file::{File, StatError},
|
||||||
|
share::Share,
|
||||||
|
warren::FetchWarrenError,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct GetShareRequest {
|
||||||
|
share_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetShareRequest {
|
||||||
|
pub fn new(share_id: Uuid) -> Self {
|
||||||
|
Self { share_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share_id(&self) -> &Uuid {
|
||||||
|
&self.share_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct GetShareResponse {
|
||||||
|
share: Share,
|
||||||
|
file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetShareResponse {
|
||||||
|
pub fn new(share: Share, file: File) -> Self {
|
||||||
|
Self { share, file }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share(&self) -> &Share {
|
||||||
|
&self.share
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file(&self) -> &File {
|
||||||
|
&self.file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetShareResponse> for Share {
|
||||||
|
fn from(value: GetShareResponse) -> Self {
|
||||||
|
value.share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum GetShareError {
|
||||||
|
#[error(transparent)]
|
||||||
|
FsStat(#[from] StatError),
|
||||||
|
#[error(transparent)]
|
||||||
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
75
backend/src/lib/domain/warren/models/share/requests/list.rs
Normal file
75
backend/src/lib/domain/warren/models/share/requests/list.rs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
file::{AbsoluteFilePath, StatError},
|
||||||
|
share::Share,
|
||||||
|
warren::{FetchWarrenError, FetchWarrenRequest, HasWarrenId, Warren},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ListSharesRequest {
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListSharesRequest {
|
||||||
|
pub fn new(warren_id: Uuid, path: AbsoluteFilePath) -> Self {
|
||||||
|
Self { warren_id, path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for ListSharesRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<FetchWarrenRequest> for &ListSharesRequest {
|
||||||
|
fn into(self) -> FetchWarrenRequest {
|
||||||
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ListSharesResponse {
|
||||||
|
warren: Warren,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
shares: Vec<Share>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListSharesResponse {
|
||||||
|
pub fn new(warren: Warren, path: AbsoluteFilePath, shares: Vec<Share>) -> Self {
|
||||||
|
Self {
|
||||||
|
warren,
|
||||||
|
path,
|
||||||
|
shares,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warren(&self) -> &Warren {
|
||||||
|
&self.warren
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shares(&self) -> &Vec<Share> {
|
||||||
|
&self.shares
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ListSharesError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Stat(#[from] StatError),
|
||||||
|
#[error(transparent)]
|
||||||
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
100
backend/src/lib/domain/warren/models/share/requests/ls.rs
Normal file
100
backend/src/lib/domain/warren/models/share/requests/ls.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
file::{AbsoluteFilePath, LsRequest, LsResponse},
|
||||||
|
share::{Share, SharePassword},
|
||||||
|
warren::{FetchWarrenError, Warren, WarrenLsError, WarrenLsRequest},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{VerifySharePasswordError, VerifySharePasswordRequest};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ShareLsRequest {
|
||||||
|
share_id: Uuid,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
password: Option<SharePassword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareLsRequest {
|
||||||
|
pub fn new(share_id: Uuid, path: AbsoluteFilePath, password: Option<SharePassword>) -> Self {
|
||||||
|
Self {
|
||||||
|
share_id,
|
||||||
|
path,
|
||||||
|
password,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share_id(&self) -> &Uuid {
|
||||||
|
&self.share_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> Option<&SharePassword> {
|
||||||
|
self.password.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_warren_ls_request(self, share: &Share, warren: &Warren) -> WarrenLsRequest {
|
||||||
|
let include_parent = self.path.as_str() != "/";
|
||||||
|
|
||||||
|
let path = share.path().clone().join(&self.path.to_relative());
|
||||||
|
|
||||||
|
WarrenLsRequest::new(*warren.id(), LsRequest::new(path, include_parent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ShareLsRequest> for VerifySharePasswordRequest {
|
||||||
|
fn from(value: &ShareLsRequest) -> Self {
|
||||||
|
Self::new(value.share_id, value.password.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct ShareLsResponse {
|
||||||
|
share: Share,
|
||||||
|
warren: Warren,
|
||||||
|
path: AbsoluteFilePath,
|
||||||
|
base: LsResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareLsResponse {
|
||||||
|
pub fn new(share: Share, warren: Warren, path: AbsoluteFilePath, base: LsResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
share,
|
||||||
|
warren,
|
||||||
|
path,
|
||||||
|
base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share(&self) -> &Share {
|
||||||
|
&self.share
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warren(&self) -> &Warren {
|
||||||
|
&self.warren
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &AbsoluteFilePath {
|
||||||
|
&self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn base(&self) -> &LsResponse {
|
||||||
|
&self.base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ShareLsError {
|
||||||
|
#[error(transparent)]
|
||||||
|
VerifySharePassword(#[from] VerifySharePasswordError),
|
||||||
|
#[error(transparent)]
|
||||||
|
WarrenLs(#[from] WarrenLsError),
|
||||||
|
#[error(transparent)]
|
||||||
|
FetchWarren(#[from] FetchWarrenError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
15
backend/src/lib/domain/warren/models/share/requests/mod.rs
Normal file
15
backend/src/lib/domain/warren/models/share/requests/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
mod cat;
|
||||||
|
mod create;
|
||||||
|
mod delete;
|
||||||
|
mod get;
|
||||||
|
mod list;
|
||||||
|
mod ls;
|
||||||
|
mod verify_password;
|
||||||
|
|
||||||
|
pub use cat::*;
|
||||||
|
pub use create::*;
|
||||||
|
pub use delete::*;
|
||||||
|
pub use get::*;
|
||||||
|
pub use list::*;
|
||||||
|
pub use ls::*;
|
||||||
|
pub use verify_password::*;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::share::{Share, SharePassword};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct VerifySharePasswordRequest {
|
||||||
|
share_id: Uuid,
|
||||||
|
password: Option<SharePassword>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerifySharePasswordRequest {
|
||||||
|
pub fn new(share_id: Uuid, password: Option<SharePassword>) -> Self {
|
||||||
|
Self { share_id, password }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share_id(&self) -> &Uuid {
|
||||||
|
&self.share_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn password(&self) -> Option<&SharePassword> {
|
||||||
|
self.password.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct VerifySharePasswordResponse {
|
||||||
|
share: Share,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerifySharePasswordResponse {
|
||||||
|
pub fn new(share: Share) -> Self {
|
||||||
|
Self { share }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn share(&self) -> &Share {
|
||||||
|
&self.share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VerifySharePasswordResponse> for Share {
|
||||||
|
fn from(value: VerifySharePasswordResponse) -> Self {
|
||||||
|
value.share
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum VerifySharePasswordError {
|
||||||
|
#[error("The specified share does not exist")]
|
||||||
|
NotFound,
|
||||||
|
#[error("The specified password is not correct")]
|
||||||
|
IncorrectPassword,
|
||||||
|
#[error(transparent)]
|
||||||
|
Unknown(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
@@ -10,6 +10,10 @@ pub struct UserWarren {
|
|||||||
can_read_files: bool,
|
can_read_files: bool,
|
||||||
can_modify_files: bool,
|
can_modify_files: bool,
|
||||||
can_delete_files: bool,
|
can_delete_files: bool,
|
||||||
|
can_list_shares: bool,
|
||||||
|
can_create_shares: bool,
|
||||||
|
can_modify_shares: bool,
|
||||||
|
can_delete_shares: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserWarren {
|
impl UserWarren {
|
||||||
@@ -20,6 +24,10 @@ impl UserWarren {
|
|||||||
can_read_files: bool,
|
can_read_files: bool,
|
||||||
can_modify_files: bool,
|
can_modify_files: bool,
|
||||||
can_delete_files: bool,
|
can_delete_files: bool,
|
||||||
|
can_list_shares: bool,
|
||||||
|
can_create_shares: bool,
|
||||||
|
can_modify_shares: bool,
|
||||||
|
can_delete_shares: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
user_id,
|
user_id,
|
||||||
@@ -28,6 +36,10 @@ impl UserWarren {
|
|||||||
can_read_files,
|
can_read_files,
|
||||||
can_modify_files,
|
can_modify_files,
|
||||||
can_delete_files,
|
can_delete_files,
|
||||||
|
can_list_shares,
|
||||||
|
can_create_shares,
|
||||||
|
can_modify_shares,
|
||||||
|
can_delete_shares,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,4 +69,20 @@ impl UserWarren {
|
|||||||
pub fn can_delete_files(&self) -> bool {
|
pub fn can_delete_files(&self) -> bool {
|
||||||
self.can_delete_files
|
self.can_delete_files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_list_shares(&self) -> bool {
|
||||||
|
self.can_list_shares
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_create_shares(&self) -> bool {
|
||||||
|
self.can_create_shares
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_modify_shares(&self) -> bool {
|
||||||
|
self.can_modify_shares
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_delete_shares(&self) -> bool {
|
||||||
|
self.can_delete_shares
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ use crate::domain::warren::models::file::{
|
|||||||
|
|
||||||
use super::{Warren, WarrenName};
|
use super::{Warren, WarrenName};
|
||||||
|
|
||||||
|
pub trait HasWarrenId {
|
||||||
|
fn warren_id(&self) -> &Uuid;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct FetchWarrenRequest {
|
pub struct FetchWarrenRequest {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
@@ -71,10 +75,6 @@ impl WarrenLsRequest {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &LsRequest {
|
pub fn base(&self) -> &LsRequest {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -90,6 +90,12 @@ impl WarrenLsRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenLsRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for WarrenLsRequest {
|
impl Into<FetchWarrenRequest> for WarrenLsRequest {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
@@ -142,10 +148,6 @@ impl WarrenMkdirRequest {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &MkdirRequest {
|
pub fn base(&self) -> &MkdirRequest {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -160,6 +162,12 @@ impl WarrenMkdirRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenMkdirRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for WarrenMkdirRequest {
|
impl Into<FetchWarrenRequest> for WarrenMkdirRequest {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
@@ -207,10 +215,6 @@ impl WarrenRmRequest {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &RmRequest {
|
pub fn base(&self) -> &RmRequest {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -226,6 +230,12 @@ impl WarrenRmRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenRmRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for &WarrenRmRequest {
|
impl Into<FetchWarrenRequest> for &WarrenRmRequest {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
@@ -272,10 +282,6 @@ impl<'s> WarrenSaveRequest<'s> {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &SaveRequest<'s> {
|
pub fn base(&self) -> &SaveRequest<'s> {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -292,6 +298,12 @@ impl<'s> WarrenSaveRequest<'s> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenSaveRequest<'_> {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for &WarrenSaveRequest<'_> {
|
impl Into<FetchWarrenRequest> for &WarrenSaveRequest<'_> {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
@@ -406,10 +418,6 @@ impl WarrenMvRequest {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &MvRequest {
|
pub fn base(&self) -> &MvRequest {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -423,6 +431,12 @@ impl WarrenMvRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenMvRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for &WarrenMvRequest {
|
impl Into<FetchWarrenRequest> for &WarrenMvRequest {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
@@ -578,10 +592,6 @@ impl WarrenCatRequest {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &CatRequest {
|
pub fn base(&self) -> &CatRequest {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -596,6 +606,12 @@ impl WarrenCatRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenCatRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for &WarrenCatRequest {
|
impl Into<FetchWarrenRequest> for &WarrenCatRequest {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
@@ -623,10 +639,6 @@ impl WarrenTouchRequest {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &TouchRequest {
|
pub fn base(&self) -> &TouchRequest {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -641,6 +653,12 @@ impl WarrenTouchRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenTouchRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for &WarrenTouchRequest {
|
impl Into<FetchWarrenRequest> for &WarrenTouchRequest {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
@@ -688,10 +706,6 @@ impl WarrenCpRequest {
|
|||||||
Self { warren_id, base }
|
Self { warren_id, base }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn warren_id(&self) -> &Uuid {
|
|
||||||
&self.warren_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn base(&self) -> &CpRequest {
|
pub fn base(&self) -> &CpRequest {
|
||||||
&self.base
|
&self.base
|
||||||
}
|
}
|
||||||
@@ -706,6 +720,12 @@ impl WarrenCpRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasWarrenId for WarrenCpRequest {
|
||||||
|
fn warren_id(&self) -> &Uuid {
|
||||||
|
&self.warren_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Into<FetchWarrenRequest> for &WarrenCpRequest {
|
impl Into<FetchWarrenRequest> for &WarrenCpRequest {
|
||||||
fn into(self) -> FetchWarrenRequest {
|
fn into(self) -> FetchWarrenRequest {
|
||||||
FetchWarrenRequest::new(self.warren_id)
|
FetchWarrenRequest::new(self.warren_id)
|
||||||
|
|||||||
@@ -42,6 +42,24 @@ pub trait WarrenMetrics: Clone + Send + Sync + 'static {
|
|||||||
|
|
||||||
fn record_warren_cp_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_warren_cp_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_warren_cp_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_warren_cp_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_warren_get_share_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_warren_get_share_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_warren_share_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_warren_share_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_warren_share_list_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_warren_share_list_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_warren_share_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_warren_share_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_warren_share_ls_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_warren_share_ls_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_warren_share_cat_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_warren_share_cat_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
||||||
@@ -68,6 +86,9 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
|||||||
|
|
||||||
fn record_cp_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_cp_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_cp_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_cp_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_stat_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_stat_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
||||||
@@ -151,4 +172,13 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
|
|||||||
|
|
||||||
fn record_auth_warren_cp_success(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_warren_cp_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
fn record_auth_warren_cp_failure(&self) -> impl Future<Output = ()> + Send;
|
fn record_auth_warren_cp_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_auth_share_creation_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_auth_share_creation_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_auth_share_list_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_auth_share_list_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn record_auth_share_deletion_success(&self) -> impl Future<Output = ()> + Send;
|
||||||
|
fn record_auth_share_deletion_failure(&self) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,13 @@ use super::models::{
|
|||||||
file::{
|
file::{
|
||||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
||||||
SaveRequest, SaveResponse, TouchError, TouchRequest,
|
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
||||||
|
},
|
||||||
|
share::{
|
||||||
|
CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse,
|
||||||
|
DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
||||||
|
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, ShareCatError,
|
||||||
|
ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest, ShareLsResponse,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||||
@@ -106,6 +112,31 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: WarrenCpRequest,
|
request: WarrenCpRequest,
|
||||||
) -> impl Future<Output = Result<WarrenCpResponse, WarrenCpError>> + Send;
|
) -> impl Future<Output = Result<WarrenCpResponse, WarrenCpError>> + Send;
|
||||||
|
|
||||||
|
fn warren_get_share(
|
||||||
|
&self,
|
||||||
|
request: GetShareRequest,
|
||||||
|
) -> impl Future<Output = Result<GetShareResponse, GetShareError>> + Send;
|
||||||
|
fn warren_create_share(
|
||||||
|
&self,
|
||||||
|
request: CreateShareRequest,
|
||||||
|
) -> impl Future<Output = Result<CreateShareResponse, CreateShareError>> + Send;
|
||||||
|
fn warren_list_shares(
|
||||||
|
&self,
|
||||||
|
request: ListSharesRequest,
|
||||||
|
) -> impl Future<Output = Result<ListSharesResponse, ListSharesError>> + Send;
|
||||||
|
fn warren_delete_share(
|
||||||
|
&self,
|
||||||
|
request: DeleteShareRequest,
|
||||||
|
) -> impl Future<Output = Result<DeleteShareResponse, DeleteShareError>> + Send;
|
||||||
|
fn warren_share_ls(
|
||||||
|
&self,
|
||||||
|
request: ShareLsRequest,
|
||||||
|
) -> impl Future<Output = Result<ShareLsResponse, ShareLsError>> + Send;
|
||||||
|
fn warren_share_cat(
|
||||||
|
&self,
|
||||||
|
request: ShareCatRequest,
|
||||||
|
) -> impl Future<Output = Result<ShareCatResponse, ShareCatError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||||
@@ -121,6 +152,10 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
|
|||||||
) -> impl Future<Output = Result<SaveResponse, SaveError>> + Send;
|
) -> impl Future<Output = Result<SaveResponse, SaveError>> + Send;
|
||||||
fn touch(&self, request: TouchRequest) -> impl Future<Output = Result<(), TouchError>> + Send;
|
fn touch(&self, request: TouchRequest) -> impl Future<Output = Result<(), TouchError>> + Send;
|
||||||
fn cp(&self, request: CpRequest) -> impl Future<Output = Result<CpResponse, CpError>> + Send;
|
fn cp(&self, request: CpRequest) -> impl Future<Output = Result<CpResponse, CpError>> + Send;
|
||||||
|
fn stat(
|
||||||
|
&self,
|
||||||
|
request: StatRequest,
|
||||||
|
) -> impl Future<Output = Result<StatResponse, StatError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AuthService: Clone + Send + Sync + 'static {
|
pub trait AuthService: Clone + Send + Sync + 'static {
|
||||||
@@ -273,4 +308,20 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
|||||||
request: AuthRequest<WarrenCpRequest>,
|
request: AuthRequest<WarrenCpRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> impl Future<Output = Result<WarrenCpResponse, AuthError<WarrenCpError>>> + Send;
|
) -> impl Future<Output = Result<WarrenCpResponse, AuthError<WarrenCpError>>> + Send;
|
||||||
|
|
||||||
|
fn auth_warren_create_share<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<CreateShareBaseRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> impl Future<Output = Result<CreateShareResponse, AuthError<CreateShareError>>> + Send;
|
||||||
|
fn auth_warren_list_shares<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<ListSharesRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> impl Future<Output = Result<ListSharesResponse, AuthError<ListSharesError>>> + Send;
|
||||||
|
fn auth_warren_delete_share<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<DeleteShareRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> impl Future<Output = Result<DeleteShareResponse, AuthError<DeleteShareError>>> + Send;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ use uuid::Uuid;
|
|||||||
use crate::domain::warren::models::{
|
use crate::domain::warren::models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, LsResponse},
|
file::{AbsoluteFilePath, LsResponse},
|
||||||
|
share::{
|
||||||
|
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||||
|
ShareCatResponse, ShareLsResponse,
|
||||||
|
},
|
||||||
user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
|
user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
|
||||||
user_warren::UserWarren,
|
user_warren::UserWarren,
|
||||||
warren::{
|
warren::{
|
||||||
@@ -44,6 +48,22 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
|||||||
path: &AbsoluteFilePath,
|
path: &AbsoluteFilePath,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
fn warren_cp(&self, response: &WarrenCpResponse) -> impl Future<Output = ()> + Send;
|
fn warren_cp(&self, response: &WarrenCpResponse) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn got_warren_share(&self, response: &GetShareResponse) -> impl Future<Output = ()> + Send;
|
||||||
|
fn warren_share_created(
|
||||||
|
&self,
|
||||||
|
response: &CreateShareResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn warren_shares_listed(
|
||||||
|
&self,
|
||||||
|
response: &ListSharesResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn warren_share_deleted(
|
||||||
|
&self,
|
||||||
|
response: &DeleteShareResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn warren_share_ls(&self, response: &ShareLsResponse) -> impl Future<Output = ()> + Send;
|
||||||
|
fn warren_share_cat(&self, response: &ShareCatResponse) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||||
@@ -63,6 +83,7 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
|||||||
path: &AbsoluteFilePath,
|
path: &AbsoluteFilePath,
|
||||||
target_path: &AbsoluteFilePath,
|
target_path: &AbsoluteFilePath,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
fn stat(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||||
@@ -175,4 +196,22 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
|||||||
user: &User,
|
user: &User,
|
||||||
response: &WarrenCpResponse,
|
response: &WarrenCpResponse,
|
||||||
) -> impl Future<Output = ()> + Send;
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn auth_warren_share_created(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
response: &CreateShareResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn auth_warren_shares_listed(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
response: &ListSharesResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
|
|
||||||
|
fn auth_warren_share_deleted(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
response: &DeleteShareResponse,
|
||||||
|
) -> impl Future<Output = ()> + Send;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ use crate::domain::warren::models::{
|
|||||||
file::{
|
file::{
|
||||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
||||||
SaveRequest, SaveResponse, TouchError, TouchRequest,
|
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
||||||
|
},
|
||||||
|
share::{
|
||||||
|
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
||||||
|
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest, ListSharesError,
|
||||||
|
ListSharesRequest, ListSharesResponse, Share, VerifySharePasswordError,
|
||||||
|
VerifySharePasswordRequest, VerifySharePasswordResponse,
|
||||||
},
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateOrUpdateUserOidcError, CreateOrUpdateUserOidcRequest, CreateUserError,
|
CreateOrUpdateUserOidcError, CreateOrUpdateUserOidcRequest, CreateUserError,
|
||||||
@@ -63,6 +69,27 @@ pub trait WarrenRepository: Clone + Send + Sync + 'static {
|
|||||||
&self,
|
&self,
|
||||||
request: FetchWarrenRequest,
|
request: FetchWarrenRequest,
|
||||||
) -> impl Future<Output = Result<Warren, FetchWarrenError>> + Send;
|
) -> impl Future<Output = Result<Warren, FetchWarrenError>> + Send;
|
||||||
|
|
||||||
|
fn get_warren_share(
|
||||||
|
&self,
|
||||||
|
request: GetShareRequest,
|
||||||
|
) -> impl Future<Output = Result<Share, GetShareError>> + Send;
|
||||||
|
fn create_warren_share(
|
||||||
|
&self,
|
||||||
|
request: CreateShareRequest,
|
||||||
|
) -> impl Future<Output = Result<CreateShareResponse, CreateShareError>> + Send;
|
||||||
|
fn list_warren_shares(
|
||||||
|
&self,
|
||||||
|
request: ListSharesRequest,
|
||||||
|
) -> impl Future<Output = Result<ListSharesResponse, ListSharesError>> + Send;
|
||||||
|
fn delete_warren_share(
|
||||||
|
&self,
|
||||||
|
request: DeleteShareRequest,
|
||||||
|
) -> impl Future<Output = Result<DeleteShareResponse, DeleteShareError>> + Send;
|
||||||
|
fn verify_warren_share_password(
|
||||||
|
&self,
|
||||||
|
request: VerifySharePasswordRequest,
|
||||||
|
) -> impl Future<Output = Result<VerifySharePasswordResponse, VerifySharePasswordError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
||||||
@@ -78,6 +105,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
|||||||
) -> impl Future<Output = Result<SaveResponse, SaveError>> + Send;
|
) -> impl Future<Output = Result<SaveResponse, SaveError>> + Send;
|
||||||
fn touch(&self, request: TouchRequest) -> impl Future<Output = Result<(), TouchError>> + Send;
|
fn touch(&self, request: TouchRequest) -> impl Future<Output = Result<(), TouchError>> + Send;
|
||||||
fn cp(&self, request: CpRequest) -> impl Future<Output = Result<CpResponse, CpError>> + Send;
|
fn cp(&self, request: CpRequest) -> impl Future<Output = Result<CpResponse, CpError>> + Send;
|
||||||
|
fn stat(
|
||||||
|
&self,
|
||||||
|
request: StatRequest,
|
||||||
|
) -> impl Future<Output = Result<StatResponse, StatError>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AuthRepository: Clone + Send + Sync + 'static {
|
pub trait AuthRepository: Clone + Send + Sync + 'static {
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ use crate::{
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
file::FileStream,
|
file::FileStream,
|
||||||
|
share::{
|
||||||
|
CreateShareBaseRequest, CreateShareError, CreateShareResponse,
|
||||||
|
DeleteShareError, DeleteShareRequest, DeleteShareResponse, ListSharesError,
|
||||||
|
ListSharesRequest, ListSharesResponse,
|
||||||
|
},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||||
EditUserError, EditUserRequest, GetOidcRedirectError, GetOidcRedirectRequest,
|
EditUserError, EditUserRequest, GetOidcRedirectError, GetOidcRedirectRequest,
|
||||||
@@ -32,12 +37,13 @@ use crate::{
|
|||||||
warren::{
|
warren::{
|
||||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
||||||
FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenCpError,
|
FetchWarrensRequest, HasWarrenId, Warren, WarrenCatError, WarrenCatRequest,
|
||||||
WarrenCpRequest, WarrenCpResponse, WarrenLsError, WarrenLsRequest,
|
WarrenCpError, WarrenCpRequest, WarrenCpResponse, WarrenLsError,
|
||||||
WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse,
|
WarrenLsRequest, WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest,
|
||||||
WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError,
|
WarrenMkdirResponse, WarrenMvError, WarrenMvRequest, WarrenMvResponse,
|
||||||
WarrenRmRequest, WarrenRmResponse, WarrenSaveError, WarrenSaveRequest,
|
WarrenRmError, WarrenRmRequest, WarrenRmResponse, WarrenSaveError,
|
||||||
WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse,
|
WarrenSaveRequest, WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest,
|
||||||
|
WarrenTouchResponse,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
||||||
@@ -644,17 +650,7 @@ where
|
|||||||
request: AuthRequest<WarrenLsRequest>,
|
request: AuthRequest<WarrenLsRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<WarrenLsResponse, AuthError<WarrenLsError>> {
|
) -> Result<WarrenLsResponse, AuthError<WarrenLsError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !user_warren.can_list_files() {
|
if !user_warren.can_list_files() {
|
||||||
return Err(AuthError::InsufficientPermissions);
|
return Err(AuthError::InsufficientPermissions);
|
||||||
@@ -667,9 +663,7 @@ where
|
|||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
if let Ok(response) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_ls_success().await;
|
self.metrics.record_auth_warren_ls_success().await;
|
||||||
self.notifier
|
self.notifier.auth_warren_ls(&user, response).await;
|
||||||
.auth_warren_ls(session_response.user(), response)
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_ls_failure().await;
|
self.metrics.record_auth_warren_ls_failure().await;
|
||||||
}
|
}
|
||||||
@@ -682,23 +676,14 @@ where
|
|||||||
request: AuthRequest<WarrenCatRequest>,
|
request: AuthRequest<WarrenCatRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<FileStream, AuthError<WarrenCatError>> {
|
) -> Result<FileStream, AuthError<WarrenCatError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
let path = request.base().path().clone();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !user_warren.can_read_files() {
|
if !user_warren.can_read_files() {
|
||||||
return Err(AuthError::InsufficientPermissions);
|
return Err(AuthError::InsufficientPermissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let path = request.base().path().clone();
|
||||||
|
|
||||||
let result = warren_service
|
let result = warren_service
|
||||||
.warren_cat(request)
|
.warren_cat(request)
|
||||||
.await
|
.await
|
||||||
@@ -707,7 +692,7 @@ where
|
|||||||
if let Ok(_stream) = result.as_ref() {
|
if let Ok(_stream) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_cat_success().await;
|
self.metrics.record_auth_warren_cat_success().await;
|
||||||
self.notifier
|
self.notifier
|
||||||
.auth_warren_cat(session_response.user(), user_warren.warren_id(), &path)
|
.auth_warren_cat(&user, user_warren.warren_id(), &path)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_cat_failure().await;
|
self.metrics.record_auth_warren_cat_failure().await;
|
||||||
@@ -721,17 +706,7 @@ where
|
|||||||
request: AuthRequest<WarrenMkdirRequest>,
|
request: AuthRequest<WarrenMkdirRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<WarrenMkdirResponse, AuthError<WarrenMkdirError>> {
|
) -> Result<WarrenMkdirResponse, AuthError<WarrenMkdirError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: Maybe create a separate permission for this
|
// TODO: Maybe create a separate permission for this
|
||||||
if !user_warren.can_modify_files() {
|
if !user_warren.can_modify_files() {
|
||||||
@@ -745,9 +720,7 @@ where
|
|||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
if let Ok(response) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_mkdir_success().await;
|
self.metrics.record_auth_warren_mkdir_success().await;
|
||||||
self.notifier
|
self.notifier.auth_warren_mkdir(&user, response).await;
|
||||||
.auth_warren_mkdir(session_response.user(), response)
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_mkdir_failure().await;
|
self.metrics.record_auth_warren_mkdir_failure().await;
|
||||||
}
|
}
|
||||||
@@ -760,17 +733,7 @@ where
|
|||||||
request: AuthRequest<WarrenRmRequest>,
|
request: AuthRequest<WarrenRmRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<WarrenRmResponse, AuthError<WarrenRmError>> {
|
) -> Result<WarrenRmResponse, AuthError<WarrenRmError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !user_warren.can_delete_files() {
|
if !user_warren.can_delete_files() {
|
||||||
return Err(AuthError::InsufficientPermissions);
|
return Err(AuthError::InsufficientPermissions);
|
||||||
@@ -783,9 +746,7 @@ where
|
|||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
if let Ok(response) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_rm_success().await;
|
self.metrics.record_auth_warren_rm_success().await;
|
||||||
self.notifier
|
self.notifier.auth_warren_rm(&user, response).await;
|
||||||
.auth_warren_rm(session_response.user(), response)
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_rm_failure().await;
|
self.metrics.record_auth_warren_rm_failure().await;
|
||||||
}
|
}
|
||||||
@@ -798,17 +759,7 @@ where
|
|||||||
request: AuthRequest<WarrenMvRequest>,
|
request: AuthRequest<WarrenMvRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<WarrenMvResponse, AuthError<WarrenMvError>> {
|
) -> Result<WarrenMvResponse, AuthError<WarrenMvError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !user_warren.can_modify_files() {
|
if !user_warren.can_modify_files() {
|
||||||
return Err(AuthError::InsufficientPermissions);
|
return Err(AuthError::InsufficientPermissions);
|
||||||
@@ -821,9 +772,7 @@ where
|
|||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
if let Ok(response) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_mv_success().await;
|
self.metrics.record_auth_warren_mv_success().await;
|
||||||
self.notifier
|
self.notifier.auth_warren_mv(&user, response).await;
|
||||||
.auth_warren_mv(session_response.user(), response)
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_mv_failure().await;
|
self.metrics.record_auth_warren_mv_failure().await;
|
||||||
}
|
}
|
||||||
@@ -836,17 +785,7 @@ where
|
|||||||
request: AuthRequest<WarrenSaveRequest<'_>>,
|
request: AuthRequest<WarrenSaveRequest<'_>>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<WarrenSaveResponse, AuthError<WarrenSaveError>> {
|
) -> Result<WarrenSaveResponse, AuthError<WarrenSaveError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: Maybe create a separate permission for this
|
// TODO: Maybe create a separate permission for this
|
||||||
if !user_warren.can_modify_files() {
|
if !user_warren.can_modify_files() {
|
||||||
@@ -860,9 +799,7 @@ where
|
|||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
if let Ok(response) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_save_success().await;
|
self.metrics.record_auth_warren_save_success().await;
|
||||||
self.notifier
|
self.notifier.auth_warren_save(&user, response).await;
|
||||||
.auth_warren_save(session_response.user(), response)
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_save_failure().await;
|
self.metrics.record_auth_warren_save_failure().await;
|
||||||
}
|
}
|
||||||
@@ -875,17 +812,7 @@ where
|
|||||||
request: AuthRequest<WarrenTouchRequest>,
|
request: AuthRequest<WarrenTouchRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<WarrenTouchResponse, AuthError<WarrenTouchError>> {
|
) -> Result<WarrenTouchResponse, AuthError<WarrenTouchError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: Maybe create a separate permission for this
|
// TODO: Maybe create a separate permission for this
|
||||||
if !user_warren.can_modify_files() {
|
if !user_warren.can_modify_files() {
|
||||||
@@ -899,9 +826,7 @@ where
|
|||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
if let Ok(response) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_touch_success().await;
|
self.metrics.record_auth_warren_touch_success().await;
|
||||||
self.notifier
|
self.notifier.auth_warren_touch(&user, response).await;
|
||||||
.auth_warren_touch(session_response.user(), response)
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_touch_failure().await;
|
self.metrics.record_auth_warren_touch_failure().await;
|
||||||
}
|
}
|
||||||
@@ -914,17 +839,7 @@ where
|
|||||||
request: AuthRequest<WarrenCpRequest>,
|
request: AuthRequest<WarrenCpRequest>,
|
||||||
warren_service: &WS,
|
warren_service: &WS,
|
||||||
) -> Result<WarrenCpResponse, AuthError<WarrenCpError>> {
|
) -> Result<WarrenCpResponse, AuthError<WarrenCpError>> {
|
||||||
let session_response = self.fetch_auth_session((&request).into()).await?;
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
let request = request.into_value();
|
|
||||||
|
|
||||||
let user_warren = self
|
|
||||||
.repository
|
|
||||||
.fetch_user_warren(FetchUserWarrenRequest::new(
|
|
||||||
session_response.user().id().clone(),
|
|
||||||
request.warren_id().clone(),
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// TODO: Maybe create a separate permission for this
|
// TODO: Maybe create a separate permission for this
|
||||||
if !user_warren.can_modify_files() {
|
if !user_warren.can_modify_files() {
|
||||||
@@ -938,13 +853,128 @@ where
|
|||||||
|
|
||||||
if let Ok(response) = result.as_ref() {
|
if let Ok(response) = result.as_ref() {
|
||||||
self.metrics.record_auth_warren_cp_success().await;
|
self.metrics.record_auth_warren_cp_success().await;
|
||||||
self.notifier
|
self.notifier.auth_warren_cp(&user, response).await;
|
||||||
.auth_warren_cp(session_response.user(), response)
|
|
||||||
.await;
|
|
||||||
} else {
|
} else {
|
||||||
self.metrics.record_auth_warren_cp_failure().await;
|
self.metrics.record_auth_warren_cp_failure().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn auth_warren_create_share<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<CreateShareBaseRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> Result<CreateShareResponse, AuthError<CreateShareError>> {
|
||||||
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
|
if !user_warren.can_create_shares() {
|
||||||
|
return Err(AuthError::InsufficientPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = warren_service
|
||||||
|
.warren_create_share(request.build_share_request(user.id().clone()))
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::Custom);
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_auth_share_creation_success().await;
|
||||||
|
self.notifier
|
||||||
|
.auth_warren_share_created(&user, response)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_auth_share_creation_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_warren_list_shares<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<ListSharesRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> Result<ListSharesResponse, AuthError<ListSharesError>> {
|
||||||
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
|
if !user_warren.can_list_shares() {
|
||||||
|
return Err(AuthError::InsufficientPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = warren_service
|
||||||
|
.warren_list_shares(request)
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::Custom);
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_auth_share_list_success().await;
|
||||||
|
self.notifier
|
||||||
|
.auth_warren_shares_listed(&user, response)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_auth_share_list_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_warren_delete_share<WS: WarrenService>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<DeleteShareRequest>,
|
||||||
|
warren_service: &WS,
|
||||||
|
) -> Result<DeleteShareResponse, AuthError<DeleteShareError>> {
|
||||||
|
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||||
|
|
||||||
|
if !user_warren.can_delete_shares() {
|
||||||
|
return Err(AuthError::InsufficientPermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = warren_service
|
||||||
|
.warren_delete_share(request)
|
||||||
|
.await
|
||||||
|
.map_err(AuthError::Custom);
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_auth_share_deletion_success().await;
|
||||||
|
self.notifier
|
||||||
|
.auth_warren_share_deleted(&user, response)
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_auth_share_deletion_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
|
||||||
|
where
|
||||||
|
R: AuthRepository,
|
||||||
|
M: AuthMetrics,
|
||||||
|
N: AuthNotifier,
|
||||||
|
OIDC: OidcService,
|
||||||
|
{
|
||||||
|
/// A helper to get a [UserWarren], [User] and the underlying request from an [AuthRequest]
|
||||||
|
async fn get_session_data_and_user_warren<T, E>(
|
||||||
|
&self,
|
||||||
|
request: AuthRequest<T>,
|
||||||
|
) -> Result<(T, User, UserWarren), AuthError<E>>
|
||||||
|
where
|
||||||
|
T: HasWarrenId,
|
||||||
|
E: std::error::Error,
|
||||||
|
{
|
||||||
|
let session_response = self.fetch_auth_session((&request).into()).await?;
|
||||||
|
let user = session_response.unpack().1;
|
||||||
|
|
||||||
|
let request = request.into_value();
|
||||||
|
|
||||||
|
let user_warren = self
|
||||||
|
.repository
|
||||||
|
.fetch_user_warren(FetchUserWarrenRequest::new(
|
||||||
|
user.id().clone(),
|
||||||
|
request.warren_id().clone(),
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((request, user, user_warren))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::domain::warren::{
|
|||||||
models::file::{
|
models::file::{
|
||||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
||||||
SaveRequest, SaveResponse, TouchError, TouchRequest,
|
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
||||||
},
|
},
|
||||||
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
||||||
};
|
};
|
||||||
@@ -152,4 +152,18 @@ where
|
|||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn stat(&self, request: StatRequest) -> Result<StatResponse, StatError> {
|
||||||
|
let path = request.path().clone();
|
||||||
|
let result = self.repository.stat(request).await;
|
||||||
|
|
||||||
|
if result.is_ok() {
|
||||||
|
self.metrics.record_stat_success().await;
|
||||||
|
self.notifier.stat(&path).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_stat_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
file::FileStream,
|
file::{File, FileStream},
|
||||||
|
share::{
|
||||||
|
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
||||||
|
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
||||||
|
GetShareResponse, ListSharesError, ListSharesRequest, ListSharesResponse, Share,
|
||||||
|
ShareCatError, ShareCatRequest, ShareCatResponse, ShareLsError, ShareLsRequest,
|
||||||
|
ShareLsResponse,
|
||||||
|
},
|
||||||
warren::{
|
warren::{
|
||||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||||
EditWarrenError, EditWarrenRequest, FetchWarrensError, FetchWarrensRequest,
|
EditWarrenError, EditWarrenRequest, FetchWarrensError, FetchWarrensRequest,
|
||||||
@@ -335,4 +342,199 @@ where
|
|||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn warren_get_share(
|
||||||
|
&self,
|
||||||
|
request: GetShareRequest,
|
||||||
|
) -> Result<GetShareResponse, GetShareError> {
|
||||||
|
let share = match self.repository.get_warren_share(request).await {
|
||||||
|
Ok(share) => share,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_get_share_failure().await;
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let warren = match self
|
||||||
|
.repository
|
||||||
|
.fetch_warren(FetchWarrenRequest::new(*share.warren_id()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(warren) => warren,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_get_share_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let file: File = match self
|
||||||
|
.fs_service
|
||||||
|
.stat(share.build_fs_stat_request(&warren))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(stat) => stat.into(),
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_get_share_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = GetShareResponse::new(share, file);
|
||||||
|
|
||||||
|
self.metrics.record_warren_get_share_success().await;
|
||||||
|
self.notifier.got_warren_share(&response).await;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_create_share(
|
||||||
|
&self,
|
||||||
|
request: CreateShareRequest,
|
||||||
|
) -> Result<CreateShareResponse, CreateShareError> {
|
||||||
|
let warren = self.repository.fetch_warren((&request).into()).await?;
|
||||||
|
let stat_request = request.build_fs_stat_request(&warren);
|
||||||
|
|
||||||
|
self.fs_service.stat(stat_request).await?;
|
||||||
|
|
||||||
|
let result = self.repository.create_warren_share(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_warren_share_creation_success().await;
|
||||||
|
self.notifier.warren_share_created(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_warren_share_creation_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_list_shares(
|
||||||
|
&self,
|
||||||
|
request: ListSharesRequest,
|
||||||
|
) -> Result<ListSharesResponse, ListSharesError> {
|
||||||
|
let result = self.repository.list_warren_shares(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_warren_share_list_success().await;
|
||||||
|
self.notifier.warren_shares_listed(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_warren_share_list_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_delete_share(
|
||||||
|
&self,
|
||||||
|
request: DeleteShareRequest,
|
||||||
|
) -> Result<DeleteShareResponse, DeleteShareError> {
|
||||||
|
let result = self.repository.delete_warren_share(request).await;
|
||||||
|
|
||||||
|
if let Ok(response) = result.as_ref() {
|
||||||
|
self.metrics.record_warren_share_deletion_success().await;
|
||||||
|
self.notifier.warren_share_deleted(response).await;
|
||||||
|
} else {
|
||||||
|
self.metrics.record_warren_share_deletion_failure().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_share_ls(
|
||||||
|
&self,
|
||||||
|
request: ShareLsRequest,
|
||||||
|
) -> Result<ShareLsResponse, ShareLsError> {
|
||||||
|
let share: Share = match self
|
||||||
|
.repository
|
||||||
|
.verify_warren_share_password((&request).into())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response.into(),
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_share_ls_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let warren = match self
|
||||||
|
.repository
|
||||||
|
.fetch_warren(FetchWarrenRequest::new(*share.warren_id()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(warren) => warren,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_share_ls_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = request.path().clone();
|
||||||
|
|
||||||
|
let ls_response = match self
|
||||||
|
.warren_ls(request.build_warren_ls_request(&share, &warren))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_share_ls_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = ShareLsResponse::new(share, warren, path, ls_response.base().clone());
|
||||||
|
|
||||||
|
self.metrics.record_warren_share_ls_success().await;
|
||||||
|
self.notifier.warren_share_ls(&response).await;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_share_cat(
|
||||||
|
&self,
|
||||||
|
request: ShareCatRequest,
|
||||||
|
) -> Result<ShareCatResponse, ShareCatError> {
|
||||||
|
let share: Share = match self
|
||||||
|
.repository
|
||||||
|
.verify_warren_share_password((&request).into())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response.into(),
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_share_cat_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let warren = match self
|
||||||
|
.repository
|
||||||
|
.fetch_warren(FetchWarrenRequest::new(*share.warren_id()))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(warren) => warren,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_share_cat_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = request.path().clone();
|
||||||
|
|
||||||
|
let stream = match self
|
||||||
|
.warren_cat(request.build_warren_cat_request(&share, &warren))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
self.metrics.record_warren_share_cat_failure().await;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = ShareCatResponse::new(share, warren, path, stream);
|
||||||
|
|
||||||
|
self.metrics.record_warren_share_cat_success().await;
|
||||||
|
self.notifier.warren_share_cat(&response).await;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
domain::warren::models::{
|
domain::warren::models::{
|
||||||
auth_session::{AuthError, requests::FetchAuthSessionError},
|
auth_session::{AuthError, requests::FetchAuthSessionError},
|
||||||
file::{LsError, MkdirError, RmError},
|
file::{CatError, LsError, MkdirError, RmError, StatError},
|
||||||
|
share::{GetShareError, ShareCatError, ShareLsError, VerifySharePasswordError},
|
||||||
user::{
|
user::{
|
||||||
CreateUserError, GetOidcRedirectError, LoginUserError, LoginUserOidcError,
|
CreateUserError, GetOidcRedirectError, LoginUserError, LoginUserOidcError,
|
||||||
RegisterUserError, VerifyUserPasswordError,
|
RegisterUserError, VerifyUserPasswordError,
|
||||||
},
|
},
|
||||||
user_warren::requests::FetchUserWarrenError,
|
user_warren::requests::FetchUserWarrenError,
|
||||||
warren::{
|
warren::{
|
||||||
FetchWarrenError, FetchWarrensError, WarrenLsError, WarrenMkdirError, WarrenMvError,
|
FetchWarrenError, FetchWarrensError, WarrenCatError, WarrenLsError, WarrenMkdirError,
|
||||||
WarrenRmError, WarrenSaveError,
|
WarrenMvError, WarrenRmError, WarrenSaveError,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
inbound::http::responses::ApiError,
|
inbound::http::responses::ApiError,
|
||||||
@@ -191,7 +192,7 @@ impl<T: std::error::Error> From<AuthError<T>> for ApiError {
|
|||||||
impl From<CreateUserError> for ApiError {
|
impl From<CreateUserError> for ApiError {
|
||||||
fn from(value: CreateUserError) -> Self {
|
fn from(value: CreateUserError) -> Self {
|
||||||
match value {
|
match value {
|
||||||
CreateUserError::Unknown(err) => Self::InternalServerError(err.to_string()),
|
CreateUserError::Unknown(err) => err.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,7 +206,63 @@ impl From<GetOidcRedirectError> for ApiError {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
GetOidcRedirectError::Disabled => Self::BadRequest("OIDC is disabled".to_string()),
|
GetOidcRedirectError::Disabled => Self::BadRequest("OIDC is disabled".to_string()),
|
||||||
GetOidcRedirectError::Unknown(e) => Self::InternalServerError(e.to_string()),
|
GetOidcRedirectError::Unknown(e) => e.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetShareError> for ApiError {
|
||||||
|
fn from(value: GetShareError) -> Self {
|
||||||
|
match value {
|
||||||
|
GetShareError::FetchWarren(err) => err.into(),
|
||||||
|
GetShareError::FsStat(err) => match err {
|
||||||
|
StatError::NotFound => Self::NotFound("The share no longer exists".to_string()),
|
||||||
|
StatError::Unknown(err) => err.into(),
|
||||||
|
},
|
||||||
|
GetShareError::Unknown(err) => err.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<VerifySharePasswordError> for ApiError {
|
||||||
|
fn from(value: VerifySharePasswordError) -> Self {
|
||||||
|
match value {
|
||||||
|
VerifySharePasswordError::NotFound => {
|
||||||
|
Self::NotFound("Could not find the specified share".to_string())
|
||||||
|
}
|
||||||
|
VerifySharePasswordError::IncorrectPassword => {
|
||||||
|
Self::BadRequest("Incorrect password".to_string())
|
||||||
|
}
|
||||||
|
VerifySharePasswordError::Unknown(error) => error.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShareLsError> for ApiError {
|
||||||
|
fn from(value: ShareLsError) -> Self {
|
||||||
|
match value {
|
||||||
|
ShareLsError::VerifySharePassword(err) => err.into(),
|
||||||
|
ShareLsError::WarrenLs(err) => err.into(),
|
||||||
|
ShareLsError::FetchWarren(err) => err.into(),
|
||||||
|
ShareLsError::Unknown(err) => err.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShareCatError> for ApiError {
|
||||||
|
fn from(value: ShareCatError) -> Self {
|
||||||
|
match value {
|
||||||
|
ShareCatError::VerifySharePassword(err) => err.into(),
|
||||||
|
ShareCatError::FetchWarren(err) => err.into(),
|
||||||
|
ShareCatError::WarrenCat(err) => match err {
|
||||||
|
WarrenCatError::FetchWarren(err) => err.into(),
|
||||||
|
WarrenCatError::FileSystem(err) => match err {
|
||||||
|
CatError::NotFound => Self::NotFound("This file does not exist".to_string()),
|
||||||
|
CatError::Unknown(err) => err.into(),
|
||||||
|
},
|
||||||
|
WarrenCatError::Unknown(err) => err.into(),
|
||||||
|
},
|
||||||
|
ShareCatError::Unknown(err) => err.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ pub(super) struct CreateUserWarrenHttpRequestBody {
|
|||||||
can_read_files: bool,
|
can_read_files: bool,
|
||||||
can_modify_files: bool,
|
can_modify_files: bool,
|
||||||
can_delete_files: bool,
|
can_delete_files: bool,
|
||||||
|
can_list_shares: bool,
|
||||||
|
can_create_shares: bool,
|
||||||
|
can_modify_shares: bool,
|
||||||
|
can_delete_shares: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreateUserWarrenHttpRequestBody {
|
impl CreateUserWarrenHttpRequestBody {
|
||||||
@@ -37,6 +41,10 @@ impl CreateUserWarrenHttpRequestBody {
|
|||||||
self.can_read_files,
|
self.can_read_files,
|
||||||
self.can_modify_files,
|
self.can_modify_files,
|
||||||
self.can_delete_files,
|
self.can_delete_files,
|
||||||
|
self.can_list_shares,
|
||||||
|
self.can_create_shares,
|
||||||
|
self.can_modify_shares,
|
||||||
|
self.can_delete_shares,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ pub(super) struct EditUserWarrenHttpRequestBody {
|
|||||||
can_read_files: bool,
|
can_read_files: bool,
|
||||||
can_modify_files: bool,
|
can_modify_files: bool,
|
||||||
can_delete_files: bool,
|
can_delete_files: bool,
|
||||||
|
can_list_shares: bool,
|
||||||
|
can_create_shares: bool,
|
||||||
|
can_modify_shares: bool,
|
||||||
|
can_delete_shares: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditUserWarrenHttpRequestBody {
|
impl EditUserWarrenHttpRequestBody {
|
||||||
@@ -37,6 +41,10 @@ impl EditUserWarrenHttpRequestBody {
|
|||||||
self.can_read_files,
|
self.can_read_files,
|
||||||
self.can_modify_files,
|
self.can_modify_files,
|
||||||
self.can_delete_files,
|
self.can_delete_files,
|
||||||
|
self.can_list_shares,
|
||||||
|
self.can_create_shares,
|
||||||
|
self.can_modify_shares,
|
||||||
|
self.can_delete_shares,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ use axum::{
|
|||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::warren::models::auth_session::AuthSessionIdWithType, inbound::http::responses::ApiError,
|
domain::warren::models::{auth_session::AuthSessionIdWithType, share::SharePassword},
|
||||||
|
inbound::http::responses::ApiError,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SessionIdHeader(pub AuthSessionIdWithType);
|
pub struct SessionIdHeader(pub AuthSessionIdWithType);
|
||||||
@@ -51,3 +52,48 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct SharePasswordHeader(pub Option<SharePassword>);
|
||||||
|
|
||||||
|
impl SharePasswordHeader {
|
||||||
|
const NAME: &str = "X-Share-Password";
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S> FromRequestParts<S> for SharePasswordHeader
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = ApiError;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
// Release build
|
||||||
|
if !cfg!(debug_assertions) {
|
||||||
|
let jar = CookieJar::from_request_parts(parts, state).await.unwrap();
|
||||||
|
|
||||||
|
let Some(cookie) = jar.get(Self::NAME) else {
|
||||||
|
return Ok(Self(None));
|
||||||
|
};
|
||||||
|
|
||||||
|
SharePassword::new(cookie.value())
|
||||||
|
.map(|v| Self(Some(v)))
|
||||||
|
.map_err(|_| ApiError::BadRequest("Invalid password".to_string()))
|
||||||
|
}
|
||||||
|
// Debug build
|
||||||
|
else {
|
||||||
|
let Some(header) = parts.headers.get(Self::NAME) else {
|
||||||
|
return Ok(Self(None));
|
||||||
|
};
|
||||||
|
|
||||||
|
let header_str = header.to_str().map_err(|_| {
|
||||||
|
ApiError::InternalServerError(format!(
|
||||||
|
"Failed to get {} header as string",
|
||||||
|
Self::NAME
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
SharePassword::new(header_str)
|
||||||
|
.map(|v| Self(Some(v)))
|
||||||
|
.map_err(|_| ApiError::BadRequest("Invalid share password".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::models::{user::User, user_warren::UserWarren, warren::Warren};
|
use crate::domain::warren::models::{
|
||||||
|
share::Share, user::User, user_warren::UserWarren, warren::Warren,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod admin;
|
pub mod admin;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
@@ -39,6 +41,10 @@ pub(super) struct UserWarrenData {
|
|||||||
can_read_files: bool,
|
can_read_files: bool,
|
||||||
can_modify_files: bool,
|
can_modify_files: bool,
|
||||||
can_delete_files: bool,
|
can_delete_files: bool,
|
||||||
|
can_list_shares: bool,
|
||||||
|
can_create_shares: bool,
|
||||||
|
can_modify_shares: bool,
|
||||||
|
can_delete_shares: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<UserWarren> for UserWarrenData {
|
impl From<UserWarren> for UserWarrenData {
|
||||||
@@ -50,23 +56,10 @@ impl From<UserWarren> for UserWarrenData {
|
|||||||
can_read_files: value.can_read_files(),
|
can_read_files: value.can_read_files(),
|
||||||
can_modify_files: value.can_modify_files(),
|
can_modify_files: value.can_modify_files(),
|
||||||
can_delete_files: value.can_delete_files(),
|
can_delete_files: value.can_delete_files(),
|
||||||
}
|
can_list_shares: value.can_list_shares(),
|
||||||
}
|
can_create_shares: value.can_create_shares(),
|
||||||
}
|
can_modify_shares: value.can_modify_shares(),
|
||||||
|
can_delete_shares: value.can_delete_shares(),
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
/// A warren that can be safely sent to the client
|
|
||||||
pub(super) struct WarrenData {
|
|
||||||
id: Uuid,
|
|
||||||
name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Warren> for WarrenData {
|
|
||||||
fn from(value: Warren) -> Self {
|
|
||||||
Self {
|
|
||||||
id: *value.id(),
|
|
||||||
name: value.name().to_string(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,3 +82,37 @@ impl From<Warren> for AdminWarrenData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct ShareData {
|
||||||
|
id: Uuid,
|
||||||
|
creator_id: Uuid,
|
||||||
|
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: String,
|
||||||
|
|
||||||
|
password: bool,
|
||||||
|
|
||||||
|
expires_at: Option<u64>,
|
||||||
|
|
||||||
|
created_at: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for ShareData
|
||||||
|
where
|
||||||
|
T: Into<Share>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
let value: Share = value.into();
|
||||||
|
Self {
|
||||||
|
id: *value.id(),
|
||||||
|
creator_id: *value.creator_id(),
|
||||||
|
warren_id: *value.warren_id(),
|
||||||
|
path: value.path().to_string(),
|
||||||
|
password: value.password_hash().is_some(),
|
||||||
|
expires_at: value.expires_at(),
|
||||||
|
created_at: value.created_at(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
86
backend/src/lib/inbound/http/handlers/warrens/cat_share.rs
Normal file
86
backend/src/lib/inbound/http/handlers/warrens/cat_share.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
extract::{Query, State},
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
file::{AbsoluteFilePathError, FilePath, FilePathError, FileStream},
|
||||||
|
share::{ShareCatRequest, SharePassword, SharePasswordError},
|
||||||
|
},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{AppState, handlers::extractors::SharePasswordHeader, responses::ApiError},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum ParseShareCatHttpRequestError {
|
||||||
|
#[error(transparent)]
|
||||||
|
FilePath(#[from] FilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Password(#[from] SharePasswordError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseShareCatHttpRequestError> for ApiError {
|
||||||
|
fn from(value: ParseShareCatHttpRequestError) -> Self {
|
||||||
|
match value {
|
||||||
|
ParseShareCatHttpRequestError::FilePath(err) => match err {
|
||||||
|
FilePathError::InvalidPath => Self::BadRequest("The path is invalid".to_string()),
|
||||||
|
},
|
||||||
|
ParseShareCatHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||||
|
AbsoluteFilePathError::NotAbsolute => {
|
||||||
|
Self::BadRequest("The path must be absolute".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ParseShareCatHttpRequestError::Password(err) => Self::BadRequest(
|
||||||
|
match err {
|
||||||
|
SharePasswordError::Empty => "The provided password is empty",
|
||||||
|
SharePasswordError::LeadingWhitespace
|
||||||
|
| SharePasswordError::TrailingWhitespace
|
||||||
|
| SharePasswordError::TooShort
|
||||||
|
| SharePasswordError::TooLong => "",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct ShareCatHttpRequestBody {
|
||||||
|
share_id: Uuid,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShareCatHttpRequestBody {
|
||||||
|
fn try_into_domain(
|
||||||
|
self,
|
||||||
|
password: Option<SharePassword>,
|
||||||
|
) -> Result<ShareCatRequest, ParseShareCatHttpRequestError> {
|
||||||
|
let path = FilePath::new(&self.path)?.try_into()?;
|
||||||
|
|
||||||
|
Ok(ShareCatRequest::new(self.share_id, path, password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cat_share<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SharePasswordHeader(password): SharePasswordHeader,
|
||||||
|
Query(request): Query<ShareCatHttpRequestBody>,
|
||||||
|
) -> Result<Body, ApiError> {
|
||||||
|
let domain_request = request.try_into_domain(password)?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.warren_service
|
||||||
|
.warren_share_cat(domain_request)
|
||||||
|
.await
|
||||||
|
.map(|response| FileStream::from(response).into())
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
auth_session::AuthRequest,
|
||||||
|
file::{AbsoluteFilePath, AbsoluteFilePathError, FilePath, FilePathError},
|
||||||
|
share::{CreateShareBaseRequest, SharePassword, SharePasswordError},
|
||||||
|
},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
handlers::{ShareData, extractors::SessionIdHeader},
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct CreateShareHttpRequestBody {
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: String,
|
||||||
|
lifetime: Option<u64>,
|
||||||
|
password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum ParseCreateShareHttpRequestError {
|
||||||
|
#[error(transparent)]
|
||||||
|
FilePath(#[from] FilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
SharePassword(#[from] SharePasswordError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseCreateShareHttpRequestError> for ApiError {
|
||||||
|
fn from(e: ParseCreateShareHttpRequestError) -> Self {
|
||||||
|
ApiError::BadRequest(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateShareHttpRequestBody {
|
||||||
|
fn try_into_domain(self) -> Result<CreateShareBaseRequest, ParseCreateShareHttpRequestError> {
|
||||||
|
let path: AbsoluteFilePath = {
|
||||||
|
let file_path = FilePath::new(&self.path)?;
|
||||||
|
file_path.try_into()?
|
||||||
|
};
|
||||||
|
|
||||||
|
let password = if let Some(password) = self.password {
|
||||||
|
Some(SharePassword::new(&password)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(CreateShareBaseRequest::new(
|
||||||
|
self.warren_id,
|
||||||
|
path,
|
||||||
|
password,
|
||||||
|
self.lifetime,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_share<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SessionIdHeader(session): SessionIdHeader,
|
||||||
|
Json(request): Json<CreateShareHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<ShareData>, ApiError> {
|
||||||
|
let domain_request = AuthRequest::new(session, request.try_into_domain()?);
|
||||||
|
|
||||||
|
state
|
||||||
|
.auth_service
|
||||||
|
.auth_warren_create_share(domain_request, state.warren_service.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|response| {
|
||||||
|
ApiSuccess::new(
|
||||||
|
StatusCode::CREATED,
|
||||||
|
ShareData::from(response.share().clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{auth_session::AuthRequest, share::DeleteShareRequest},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
handlers::{ShareData, extractors::SessionIdHeader},
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct DeleteShareHttpRequestBody {
|
||||||
|
warren_id: Uuid,
|
||||||
|
share_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeleteShareHttpRequestBody {
|
||||||
|
fn into_domain(self) -> DeleteShareRequest {
|
||||||
|
DeleteShareRequest::new(self.warren_id, self.share_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_share<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SessionIdHeader(session): SessionIdHeader,
|
||||||
|
Json(request): Json<DeleteShareHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<ShareData>, ApiError> {
|
||||||
|
let domain_request = AuthRequest::new(session, request.into_domain());
|
||||||
|
|
||||||
|
state
|
||||||
|
.auth_service
|
||||||
|
.auth_warren_delete_share(domain_request, state.warren_service.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|response| ApiSuccess::new(StatusCode::OK, ShareData::from(response)))
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
59
backend/src/lib/inbound/http/handlers/warrens/get_share.rs
Normal file
59
backend/src/lib/inbound/http/handlers/warrens/get_share.rs
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::share::{GetShareRequest, GetShareResponse},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
handlers::ShareData,
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::WarrenFileElement;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct GetShareHttpRequestBody {
|
||||||
|
share_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetShareHttpRequestBody {
|
||||||
|
fn into_domain(self) -> GetShareRequest {
|
||||||
|
GetShareRequest::new(self.share_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct GetShareHttpResponseBody {
|
||||||
|
share: ShareData,
|
||||||
|
file: WarrenFileElement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GetShareResponse> for GetShareHttpResponseBody {
|
||||||
|
fn from(value: GetShareResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
share: value.share().clone().into(),
|
||||||
|
file: value.file().into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_share<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
Json(request): Json<GetShareHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<GetShareHttpResponseBody>, ApiError> {
|
||||||
|
let domain_request = request.into_domain();
|
||||||
|
|
||||||
|
state
|
||||||
|
.warren_service
|
||||||
|
.warren_get_share(domain_request)
|
||||||
|
.await
|
||||||
|
.map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
77
backend/src/lib/inbound/http/handlers/warrens/list_shares.rs
Normal file
77
backend/src/lib/inbound/http/handlers/warrens/list_shares.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
auth_session::AuthRequest,
|
||||||
|
file::{AbsoluteFilePath, AbsoluteFilePathError, FilePath, FilePathError},
|
||||||
|
share::ListSharesRequest,
|
||||||
|
},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
handlers::{ShareData, extractors::SessionIdHeader},
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct ListSharesHttpRequestBody {
|
||||||
|
warren_id: Uuid,
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum ParseListSharesHttpRequestError {
|
||||||
|
#[error(transparent)]
|
||||||
|
FilePath(#[from] FilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseListSharesHttpRequestError> for ApiError {
|
||||||
|
fn from(e: ParseListSharesHttpRequestError) -> Self {
|
||||||
|
ApiError::BadRequest(e.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListSharesHttpRequestBody {
|
||||||
|
fn try_into_domain(self) -> Result<ListSharesRequest, ParseListSharesHttpRequestError> {
|
||||||
|
let path: AbsoluteFilePath = {
|
||||||
|
let file_path = FilePath::new(&self.path)?;
|
||||||
|
file_path.try_into()?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ListSharesRequest::new(self.warren_id, path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_shares<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
SessionIdHeader(session): SessionIdHeader,
|
||||||
|
Json(request): Json<ListSharesHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<Vec<ShareData>>, ApiError> {
|
||||||
|
let domain_request = AuthRequest::new(session, request.try_into_domain()?);
|
||||||
|
|
||||||
|
state
|
||||||
|
.auth_service
|
||||||
|
.auth_warren_list_shares(domain_request, state.warren_service.as_ref())
|
||||||
|
.await
|
||||||
|
.map(|response| {
|
||||||
|
ApiSuccess::new(
|
||||||
|
StatusCode::OK,
|
||||||
|
response
|
||||||
|
.shares()
|
||||||
|
.into_iter()
|
||||||
|
.cloned()
|
||||||
|
.map(ShareData::from)
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
111
backend/src/lib/inbound/http/handlers/warrens/ls_share.rs
Normal file
111
backend/src/lib/inbound/http/handlers/warrens/ls_share.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use axum::{Json, extract::State, http::StatusCode};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::{
|
||||||
|
file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||||
|
share::{ShareLsRequest, ShareLsResponse, SharePassword, SharePasswordError},
|
||||||
|
},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
AppState,
|
||||||
|
responses::{ApiError, ApiSuccess},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::WarrenFileElement;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum ParseShareLsHttpRequestError {
|
||||||
|
#[error(transparent)]
|
||||||
|
FilePath(#[from] FilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Password(#[from] SharePasswordError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseShareLsHttpRequestError> for ApiError {
|
||||||
|
fn from(value: ParseShareLsHttpRequestError) -> Self {
|
||||||
|
match value {
|
||||||
|
ParseShareLsHttpRequestError::FilePath(err) => match err {
|
||||||
|
FilePathError::InvalidPath => Self::BadRequest("The path is invalid".to_string()),
|
||||||
|
},
|
||||||
|
ParseShareLsHttpRequestError::AbsoluteFilePath(err) => match err {
|
||||||
|
AbsoluteFilePathError::NotAbsolute => {
|
||||||
|
Self::BadRequest("The path must be absolute".to_string())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ParseShareLsHttpRequestError::Password(err) => Self::BadRequest(
|
||||||
|
match err {
|
||||||
|
SharePasswordError::Empty => "The provided password is empty",
|
||||||
|
SharePasswordError::LeadingWhitespace
|
||||||
|
| SharePasswordError::TrailingWhitespace
|
||||||
|
| SharePasswordError::TooShort
|
||||||
|
| SharePasswordError::TooLong => "",
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(super) struct LsShareHttpRequestBody {
|
||||||
|
share_id: Uuid,
|
||||||
|
path: String,
|
||||||
|
password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LsShareHttpRequestBody {
|
||||||
|
fn try_into_domain(self) -> Result<ShareLsRequest, ParseShareLsHttpRequestError> {
|
||||||
|
let path = FilePath::new(&self.path)?.try_into()?;
|
||||||
|
let password = if let Some(password) = self.password.as_ref() {
|
||||||
|
Some(SharePassword::new(password)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ShareLsRequest::new(self.share_id, path, password))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ShareLsResponseData {
|
||||||
|
files: Vec<WarrenFileElement>,
|
||||||
|
parent: Option<WarrenFileElement>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ShareLsResponse> for ShareLsResponseData {
|
||||||
|
fn from(value: ShareLsResponse) -> Self {
|
||||||
|
Self {
|
||||||
|
files: value
|
||||||
|
.base()
|
||||||
|
.files()
|
||||||
|
.into_iter()
|
||||||
|
.map(WarrenFileElement::from)
|
||||||
|
.collect(),
|
||||||
|
parent: value.base().parent().map(WarrenFileElement::from),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ls_share<WS: WarrenService, AS: AuthService>(
|
||||||
|
State(state): State<AppState<WS, AS>>,
|
||||||
|
Json(request): Json<LsShareHttpRequestBody>,
|
||||||
|
) -> Result<ApiSuccess<ShareLsResponseData>, ApiError> {
|
||||||
|
let domain_request = request.try_into_domain()?;
|
||||||
|
|
||||||
|
state
|
||||||
|
.warren_service
|
||||||
|
.warren_share_ls(domain_request)
|
||||||
|
.await
|
||||||
|
.map(|response| ApiSuccess::new(StatusCode::OK, response.into()))
|
||||||
|
.map_err(ApiError::from)
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
mod cat_share;
|
||||||
|
mod create_share;
|
||||||
|
mod delete_share;
|
||||||
mod fetch_warren;
|
mod fetch_warren;
|
||||||
|
mod get_share;
|
||||||
|
mod list_shares;
|
||||||
mod list_warrens;
|
mod list_warrens;
|
||||||
|
mod ls_share;
|
||||||
mod upload_warren_files;
|
mod upload_warren_files;
|
||||||
mod warren_cat;
|
mod warren_cat;
|
||||||
mod warren_cp;
|
mod warren_cp;
|
||||||
@@ -14,23 +20,53 @@ use axum::{
|
|||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
domain::warren::ports::{AuthService, WarrenService},
|
|
||||||
inbound::http::AppState,
|
|
||||||
};
|
|
||||||
|
|
||||||
use fetch_warren::fetch_warren;
|
use fetch_warren::fetch_warren;
|
||||||
use list_warrens::list_warrens;
|
use list_warrens::list_warrens;
|
||||||
|
use serde::Serialize;
|
||||||
use warren_ls::list_warren_files;
|
use warren_ls::list_warren_files;
|
||||||
|
|
||||||
use warren_mkdir::create_warren_directory;
|
use warren_mkdir::create_warren_directory;
|
||||||
use warren_rm::warren_rm;
|
use warren_rm::warren_rm;
|
||||||
|
|
||||||
|
use cat_share::cat_share;
|
||||||
|
use create_share::create_share;
|
||||||
|
use delete_share::delete_share;
|
||||||
|
use get_share::get_share;
|
||||||
|
use list_shares::list_shares;
|
||||||
|
use ls_share::ls_share;
|
||||||
use upload_warren_files::warren_save;
|
use upload_warren_files::warren_save;
|
||||||
use warren_cat::fetch_file;
|
use warren_cat::fetch_file;
|
||||||
use warren_cp::warren_cp;
|
use warren_cp::warren_cp;
|
||||||
use warren_mv::warren_mv;
|
use warren_mv::warren_mv;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::warren::{
|
||||||
|
models::file::{File, FileMimeType, FileType},
|
||||||
|
ports::{AuthService, WarrenService},
|
||||||
|
},
|
||||||
|
inbound::http::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct WarrenFileElement {
|
||||||
|
name: String,
|
||||||
|
file_type: FileType,
|
||||||
|
mime_type: Option<String>,
|
||||||
|
created_at: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&File> for WarrenFileElement {
|
||||||
|
fn from(value: &File) -> Self {
|
||||||
|
Self {
|
||||||
|
name: value.name().to_string(),
|
||||||
|
file_type: value.file_type().to_owned(),
|
||||||
|
mime_type: value.mime_type().map(FileMimeType::to_string),
|
||||||
|
created_at: value.created_at(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(list_warrens))
|
.route("/", get(list_warrens))
|
||||||
@@ -46,4 +82,10 @@ pub fn routes<WS: WarrenService, AS: AuthService>() -> Router<AppState<WS, AS>>
|
|||||||
)
|
)
|
||||||
.route("/files/mv", post(warren_mv))
|
.route("/files/mv", post(warren_mv))
|
||||||
.route("/files/cp", post(warren_cp))
|
.route("/files/cp", post(warren_cp))
|
||||||
|
.route("/files/create_share", post(create_share))
|
||||||
|
.route("/files/list_shares", post(list_shares))
|
||||||
|
.route("/files/delete_share", post(delete_share))
|
||||||
|
.route("/files/get_share", post(get_share))
|
||||||
|
.route("/files/ls_share", post(ls_share))
|
||||||
|
.route("/files/cat_share", get(cat_share))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ use axum::{
|
|||||||
body::Body,
|
body::Body,
|
||||||
extract::{Query, State},
|
extract::{Query, State},
|
||||||
};
|
};
|
||||||
use base64::Engine as _;
|
use serde::Deserialize;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio_util::io::ReaderStream;
|
use tokio_util::io::ReaderStream;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -20,21 +19,6 @@ use crate::{
|
|||||||
inbound::http::{AppState, handlers::extractors::SessionIdHeader, responses::ApiError},
|
inbound::http::{AppState, handlers::extractors::SessionIdHeader, responses::ApiError},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Hash)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub(super) struct FetchWarrenFileHttpResponseBody {
|
|
||||||
// base64 encoded file content bytes
|
|
||||||
contents: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<u8>> for FetchWarrenFileHttpResponseBody {
|
|
||||||
fn from(value: Vec<u8>) -> Self {
|
|
||||||
Self {
|
|
||||||
contents: base64::prelude::BASE64_STANDARD.encode(&value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(super) struct WarrenCatHttpRequestBody {
|
pub(super) struct WarrenCatHttpRequestBody {
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ use crate::{
|
|||||||
domain::warren::{
|
domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
auth_session::AuthRequest,
|
auth_session::AuthRequest,
|
||||||
file::{
|
file::{AbsoluteFilePathError, FilePath, FilePathError, LsRequest},
|
||||||
AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType,
|
|
||||||
LsRequest,
|
|
||||||
},
|
|
||||||
warren::{WarrenLsRequest, WarrenLsResponse},
|
warren::{WarrenLsRequest, WarrenLsResponse},
|
||||||
},
|
},
|
||||||
ports::{AuthService, WarrenService},
|
ports::{AuthService, WarrenService},
|
||||||
@@ -22,6 +19,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::WarrenFileElement;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Error)]
|
#[derive(Debug, Clone, Error)]
|
||||||
pub enum ParseWarrenLsHttpRequestError {
|
pub enum ParseWarrenLsHttpRequestError {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
@@ -65,15 +64,6 @@ impl From<ParseWarrenLsHttpRequestError> for ApiError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct WarrenFileElement {
|
|
||||||
name: String,
|
|
||||||
file_type: FileType,
|
|
||||||
mime_type: Option<String>,
|
|
||||||
created_at: Option<u64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ListWarrenFilesResponseData {
|
pub struct ListWarrenFilesResponseData {
|
||||||
@@ -81,17 +71,6 @@ pub struct ListWarrenFilesResponseData {
|
|||||||
parent: Option<WarrenFileElement>,
|
parent: Option<WarrenFileElement>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&File> for WarrenFileElement {
|
|
||||||
fn from(value: &File) -> Self {
|
|
||||||
Self {
|
|
||||||
name: value.name().to_string(),
|
|
||||||
file_type: value.file_type().to_owned(),
|
|
||||||
mime_type: value.mime_type().map(FileMimeType::to_string),
|
|
||||||
created_at: value.created_at(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<WarrenLsResponse> for ListWarrenFilesResponseData {
|
impl From<WarrenLsResponse> for ListWarrenFilesResponseData {
|
||||||
fn from(value: WarrenLsResponse) -> Self {
|
fn from(value: WarrenLsResponse) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::os::unix::fs::MetadataExt;
|
use std::{os::unix::fs::MetadataExt, path::PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context, anyhow, bail};
|
use anyhow::{Context, anyhow, bail};
|
||||||
use futures_util::TryStreamExt;
|
use futures_util::TryStreamExt;
|
||||||
use rustix::fs::statx;
|
use rustix::fs::{Statx, statx};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs,
|
fs,
|
||||||
io::{self, AsyncWriteExt as _},
|
io::{self, AsyncWriteExt as _},
|
||||||
@@ -17,7 +17,8 @@ use crate::{
|
|||||||
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, File,
|
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, File,
|
||||||
FileMimeType, FileName, FilePath, FileStream, FileType, LsError, LsRequest,
|
FileMimeType, FileName, FilePath, FileStream, FileType, LsError, LsRequest,
|
||||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RelativeFilePath,
|
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RelativeFilePath,
|
||||||
RmError, RmRequest, SaveError, SaveRequest, SaveResponse, TouchError, TouchRequest,
|
RmError, RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest,
|
||||||
|
StatResponse, TouchError, TouchRequest,
|
||||||
},
|
},
|
||||||
warren::UploadFileStream,
|
warren::UploadFileStream,
|
||||||
},
|
},
|
||||||
@@ -242,7 +243,12 @@ impl FileSystem {
|
|||||||
async fn touch(&self, path: &AbsoluteFilePath) -> io::Result<()> {
|
async fn touch(&self, path: &AbsoluteFilePath) -> io::Result<()> {
|
||||||
let path = self.get_target_path(path);
|
let path = self.get_target_path(path);
|
||||||
|
|
||||||
fs::File::create(&path).await.map(|_| ())
|
fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.open(&path)
|
||||||
|
.await
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cp(
|
async fn cp(
|
||||||
@@ -257,6 +263,46 @@ impl FileSystem {
|
|||||||
|
|
||||||
Ok(CpResponse::new(path, target_path))
|
Ok(CpResponse::new(path, target_path))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn stat(&self, path: AbsoluteFilePath) -> anyhow::Result<File> {
|
||||||
|
let target_path = self.get_target_path(&path);
|
||||||
|
let fs_path = PathBuf::from(target_path.to_string());
|
||||||
|
|
||||||
|
let metadata = fs::metadata(&fs_path).await?;
|
||||||
|
|
||||||
|
let name = {
|
||||||
|
let file_name = fs_path
|
||||||
|
.clone()
|
||||||
|
.file_name()
|
||||||
|
.context("Failed to get file name")?
|
||||||
|
.to_owned()
|
||||||
|
.into_string()
|
||||||
|
.ok()
|
||||||
|
.context("Failed to get file name")?;
|
||||||
|
FileName::new(&file_name)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let file_type = {
|
||||||
|
let file_type = metadata.file_type();
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
FileType::Directory
|
||||||
|
} else if file_type.is_file() {
|
||||||
|
FileType::File
|
||||||
|
} else {
|
||||||
|
bail!("Invalid file type");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mime_type = match file_type {
|
||||||
|
FileType::File => FileMimeType::from_name(name.as_str()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let created_at = get_btime(&fs_path);
|
||||||
|
|
||||||
|
Ok(File::new(name, file_type, mime_type, created_at))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemRepository for FileSystem {
|
impl FileSystemRepository for FileSystem {
|
||||||
@@ -333,11 +379,25 @@ impl FileSystemRepository for FileSystem {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| CpError::Unknown(e.into()))
|
.map_err(|e| CpError::Unknown(e.into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn stat(&self, request: StatRequest) -> Result<StatResponse, StatError> {
|
||||||
|
let path = request.into_path();
|
||||||
|
Ok(self.stat(path).await.map(StatResponse::new)?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Use `DirEntry::metadata` once `target=x86_64-unknown-linux-musl` updates from musl 1.2.3 to 1.2.5
|
// 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
|
// https://github.com/rust-lang/rust/pull/142682
|
||||||
fn get_btime<P>(path: P) -> Option<u64>
|
fn get_btime<P>(path: P) -> Option<u64>
|
||||||
|
where
|
||||||
|
P: rustix::path::Arg,
|
||||||
|
{
|
||||||
|
get_statx(path)
|
||||||
|
.ok()
|
||||||
|
.map(|statx| statx.stx_btime.tv_sec as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_statx<P>(path: P) -> rustix::io::Result<Statx>
|
||||||
where
|
where
|
||||||
P: rustix::path::Arg,
|
P: rustix::path::Arg,
|
||||||
{
|
{
|
||||||
@@ -349,6 +409,4 @@ where
|
|||||||
rustix::fs::StatxFlags::BTIME,
|
rustix::fs::StatxFlags::BTIME,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.ok()
|
|
||||||
.map(|statx| statx.stx_btime.tv_sec as u64)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,6 +110,47 @@ impl WarrenMetrics for MetricsDebugLogger {
|
|||||||
async fn record_warren_cp_failure(&self) {
|
async fn record_warren_cp_failure(&self) {
|
||||||
tracing::debug!("[Metrics] Warren entry cp failed");
|
tracing::debug!("[Metrics] Warren entry cp failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_warren_get_share_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren get share succeeded");
|
||||||
|
}
|
||||||
|
async fn record_warren_get_share_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren get share failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_warren_share_creation_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share creation succeeded");
|
||||||
|
}
|
||||||
|
async fn record_warren_share_creation_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share creation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_warren_share_list_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share list succeeded");
|
||||||
|
}
|
||||||
|
async fn record_warren_share_list_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share list failed");
|
||||||
|
}
|
||||||
|
async fn record_warren_share_deletion_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share deletion succeeded");
|
||||||
|
}
|
||||||
|
async fn record_warren_share_deletion_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share deletion failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_warren_share_ls_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share ls succeeded");
|
||||||
|
}
|
||||||
|
async fn record_warren_share_ls_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share ls failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_warren_share_cat_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share cat succeeded");
|
||||||
|
}
|
||||||
|
async fn record_warren_share_cat_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Warren share cat failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemMetrics for MetricsDebugLogger {
|
impl FileSystemMetrics for MetricsDebugLogger {
|
||||||
@@ -168,6 +209,13 @@ impl FileSystemMetrics for MetricsDebugLogger {
|
|||||||
async fn record_cp_failure(&self) {
|
async fn record_cp_failure(&self) {
|
||||||
tracing::debug!("[Metrics] Cp failed");
|
tracing::debug!("[Metrics] Cp failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_stat_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Stat succeeded");
|
||||||
|
}
|
||||||
|
async fn record_stat_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Stat failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthMetrics for MetricsDebugLogger {
|
impl AuthMetrics for MetricsDebugLogger {
|
||||||
@@ -359,6 +407,27 @@ impl AuthMetrics for MetricsDebugLogger {
|
|||||||
async fn record_auth_warren_cp_failure(&self) {
|
async fn record_auth_warren_cp_failure(&self) {
|
||||||
tracing::debug!("[Metrics] Auth warren cp failed");
|
tracing::debug!("[Metrics] Auth warren cp failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn record_auth_share_creation_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth warren share creation succeeded");
|
||||||
|
}
|
||||||
|
async fn record_auth_share_creation_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth warren share creation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_auth_share_list_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth warren share list succeeded");
|
||||||
|
}
|
||||||
|
async fn record_auth_share_list_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth warren share list failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn record_auth_share_deletion_success(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth warren share deletion succeeded");
|
||||||
|
}
|
||||||
|
async fn record_auth_share_deletion_failure(&self) {
|
||||||
|
tracing::debug!("[Metrics] Auth warren share deletion failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OidcMetrics for MetricsDebugLogger {
|
impl OidcMetrics for MetricsDebugLogger {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ use crate::domain::{
|
|||||||
models::{
|
models::{
|
||||||
auth_session::requests::FetchAuthSessionResponse,
|
auth_session::requests::FetchAuthSessionResponse,
|
||||||
file::{AbsoluteFilePath, LsResponse},
|
file::{AbsoluteFilePath, LsResponse},
|
||||||
|
share::{
|
||||||
|
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||||
|
ShareCatResponse, ShareLsResponse,
|
||||||
|
},
|
||||||
user::{
|
user::{
|
||||||
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
|
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
|
||||||
},
|
},
|
||||||
@@ -115,6 +119,57 @@ impl WarrenNotifier for NotifierDebugLogger {
|
|||||||
response.warren().name()
|
response.warren().name()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn warren_share_created(&self, response: &CreateShareResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Created share for file {} in warren {}",
|
||||||
|
response.share().path(),
|
||||||
|
response.share().warren_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_shares_listed(&self, response: &ListSharesResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Listed {} share(s) for file {} in warren {}",
|
||||||
|
response.shares().len(),
|
||||||
|
response.path(),
|
||||||
|
response.warren().id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_share_deleted(&self, response: &DeleteShareResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Deleted share {} for file {} in warren {}",
|
||||||
|
response.share().id(),
|
||||||
|
response.share().path(),
|
||||||
|
response.share().warren_id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn got_warren_share(&self, response: &GetShareResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Got share {} from warren {}",
|
||||||
|
response.share().id(),
|
||||||
|
response.share().warren_id()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_share_ls(&self, response: &ShareLsResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Listed {} file(s) in share {} at path {}",
|
||||||
|
response.base().files().len(),
|
||||||
|
response.share().id(),
|
||||||
|
response.path()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn warren_share_cat(&self, response: &ShareCatResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Fetched file {} from share {}",
|
||||||
|
response.path(),
|
||||||
|
response.share().id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileSystemNotifier for NotifierDebugLogger {
|
impl FileSystemNotifier for NotifierDebugLogger {
|
||||||
@@ -149,6 +204,10 @@ impl FileSystemNotifier for NotifierDebugLogger {
|
|||||||
async fn cp(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) {
|
async fn cp(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) {
|
||||||
tracing::debug!("[Notifier] Copied file {} to {}", path, target_path);
|
tracing::debug!("[Notifier] Copied file {} to {}", path, target_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn stat(&self, path: &AbsoluteFilePath) {
|
||||||
|
tracing::debug!("[Notifier] Got stats for file {}", path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthNotifier for NotifierDebugLogger {
|
impl AuthNotifier for NotifierDebugLogger {
|
||||||
@@ -368,6 +427,35 @@ impl AuthNotifier for NotifierDebugLogger {
|
|||||||
user.id()
|
user.id()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn auth_warren_share_created(&self, user: &User, response: &CreateShareResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Created share for file {} in warren {} for authenticated user {}",
|
||||||
|
response.share().path(),
|
||||||
|
response.share().warren_id(),
|
||||||
|
user.id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_warren_shares_listed(&self, user: &User, response: &ListSharesResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Listed {} share(s) for file {} in warren {} for authenticated user {}",
|
||||||
|
response.shares().len(),
|
||||||
|
response.path(),
|
||||||
|
response.warren().id(),
|
||||||
|
user.id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_warren_share_deleted(&self, user: &User, response: &DeleteShareResponse) {
|
||||||
|
tracing::debug!(
|
||||||
|
"[Notifier] Deleted share {} for file {} in warren {} for authenticated user {}",
|
||||||
|
response.share().id(),
|
||||||
|
response.share().path(),
|
||||||
|
response.share().warren_id(),
|
||||||
|
user.id(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OidcNotifier for NotifierDebugLogger {
|
impl OidcNotifier for NotifierDebugLogger {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::{Context as _, anyhow, bail};
|
use anyhow::{Context as _, anyhow, bail};
|
||||||
use argon2::{
|
use argon2::{
|
||||||
Argon2, PasswordHash, PasswordVerifier as _,
|
Argon2, PasswordHash, PasswordVerifier as _,
|
||||||
@@ -9,8 +7,7 @@ use argon2::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use sqlx::{Acquire as _, PgConnection, PgPool};
|
use sqlx::{Acquire as _, PgConnection};
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
@@ -372,27 +369,7 @@ impl AuthRepository for Postgres {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Postgres {
|
||||||
pub(super) fn start_session_cleanup_task(pool: PgPool, interval: Duration) -> JoinHandle<()> {
|
pub(super) async fn delete_expired_auth_sessions(
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
{
|
|
||||||
let Ok(mut connection) = pool.acquire().await else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
|
|
||||||
tracing::debug!("Removed {count} expired auth session(s)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(interval).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracing::debug!("Session cleanup task stopped");
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_expired_auth_sessions(
|
|
||||||
connection: &mut PgConnection,
|
connection: &mut PgConnection,
|
||||||
) -> Result<u64, sqlx::Error> {
|
) -> Result<u64, sqlx::Error> {
|
||||||
let delete_count = sqlx::query(
|
let delete_count = sqlx::query(
|
||||||
@@ -916,7 +893,11 @@ impl Postgres {
|
|||||||
can_list_files = $3,
|
can_list_files = $3,
|
||||||
can_read_files = $4,
|
can_read_files = $4,
|
||||||
can_modify_files = $5,
|
can_modify_files = $5,
|
||||||
can_delete_files = $6
|
can_delete_files = $6,
|
||||||
|
can_list_shares = $7,
|
||||||
|
can_create_shares = $8,
|
||||||
|
can_modify_shares = $9,
|
||||||
|
can_delete_shares = $10
|
||||||
WHERE
|
WHERE
|
||||||
user_id = $1 AND
|
user_id = $1 AND
|
||||||
warren_id = $2
|
warren_id = $2
|
||||||
@@ -930,6 +911,10 @@ impl Postgres {
|
|||||||
.bind(user_warren.can_read_files())
|
.bind(user_warren.can_read_files())
|
||||||
.bind(user_warren.can_modify_files())
|
.bind(user_warren.can_modify_files())
|
||||||
.bind(user_warren.can_delete_files())
|
.bind(user_warren.can_delete_files())
|
||||||
|
.bind(user_warren.can_list_shares())
|
||||||
|
.bind(user_warren.can_create_shares())
|
||||||
|
.bind(user_warren.can_modify_shares())
|
||||||
|
.bind(user_warren.can_delete_shares())
|
||||||
.fetch_one(connection)
|
.fetch_one(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use sqlx::{
|
|||||||
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
||||||
postgres::{PgConnectOptions, PgPoolOptions},
|
postgres::{PgConnectOptions, PgPoolOptions},
|
||||||
};
|
};
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
pub mod share;
|
||||||
pub mod warrens;
|
pub mod warrens;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -58,10 +60,34 @@ impl Postgres {
|
|||||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||||
|
|
||||||
// 3600 seconds = 1 hour
|
// 3600 seconds = 1 hour
|
||||||
Self::start_session_cleanup_task(pool.clone(), Duration::from_secs(3600));
|
Self::start_cleanup_tasks(pool.clone(), Duration::from_secs(3600));
|
||||||
|
|
||||||
Ok(Self { pool })
|
Ok(Self { pool })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn start_cleanup_tasks(pool: PgPool, interval: Duration) -> JoinHandle<()> {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
{
|
||||||
|
let Ok(mut connection) = pool.acquire().await else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(count) = Self::delete_expired_auth_sessions(&mut connection).await {
|
||||||
|
tracing::debug!("Removed {count} expired auth session(s)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(count) = Self::delete_expired_shares(&mut connection).await {
|
||||||
|
tracing::debug!("Deleted {count} expired share(s)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(interval).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::debug!("Session cleanup task stopped");
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
|
pub(super) fn is_not_found_error(err: &sqlx::Error) -> bool {
|
||||||
|
|||||||
285
backend/src/lib/outbound/postgres/share.rs
Normal file
285
backend/src/lib/outbound/postgres/share.rs
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use argon2::{
|
||||||
|
Argon2, PasswordHash, PasswordVerifier as _,
|
||||||
|
password_hash::{PasswordHasher as _, SaltString, rand_core::OsRng},
|
||||||
|
};
|
||||||
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use sqlx::{Acquire as _, PgConnection};
|
||||||
|
use thiserror::Error;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::domain::warren::models::{
|
||||||
|
file::{AbsoluteFilePathError, FilePath, FilePathError},
|
||||||
|
share::{
|
||||||
|
CreateShareRequest, DeleteShareRequest, GetShareRequest, ListSharesRequest, Share,
|
||||||
|
VerifySharePasswordError, VerifySharePasswordRequest,
|
||||||
|
},
|
||||||
|
warren::HasWarrenId as _,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{Postgres, is_not_found_error};
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct ShareRow {
|
||||||
|
id: Uuid,
|
||||||
|
|
||||||
|
creator_id: Uuid,
|
||||||
|
warren_id: Uuid,
|
||||||
|
|
||||||
|
path: String,
|
||||||
|
|
||||||
|
password_hash: Option<String>,
|
||||||
|
|
||||||
|
expires_at: Option<NaiveDateTime>,
|
||||||
|
|
||||||
|
created_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum TryFromShareRowError {
|
||||||
|
#[error(transparent)]
|
||||||
|
FilePath(#[from] FilePathError),
|
||||||
|
#[error(transparent)]
|
||||||
|
AbsoluteFilePath(#[from] AbsoluteFilePathError),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<ShareRow> for Share {
|
||||||
|
type Error = TryFromShareRowError;
|
||||||
|
|
||||||
|
fn try_from(value: ShareRow) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Share::new(
|
||||||
|
value.id,
|
||||||
|
value.creator_id,
|
||||||
|
value.warren_id,
|
||||||
|
FilePath::new(&value.path)?.try_into()?,
|
||||||
|
value.password_hash,
|
||||||
|
value
|
||||||
|
.expires_at
|
||||||
|
.map(|nt| nt.and_utc().timestamp_millis() as u64),
|
||||||
|
value.created_at.and_utc().timestamp_millis() as u64,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn get_share(
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
request: GetShareRequest,
|
||||||
|
) -> anyhow::Result<Share> {
|
||||||
|
let share_row: ShareRow = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
creator_id,
|
||||||
|
warren_id,
|
||||||
|
path,
|
||||||
|
password_hash,
|
||||||
|
expires_at,
|
||||||
|
created_at
|
||||||
|
FROM
|
||||||
|
shares
|
||||||
|
WHERE
|
||||||
|
id = $1 AND
|
||||||
|
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(request.share_id())
|
||||||
|
.fetch_one(connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Share::try_from(share_row)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn list_shares(
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
request: ListSharesRequest,
|
||||||
|
) -> anyhow::Result<Vec<Share>> {
|
||||||
|
let share_rows: Vec<ShareRow> = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
creator_id,
|
||||||
|
warren_id,
|
||||||
|
path,
|
||||||
|
password_hash,
|
||||||
|
expires_at,
|
||||||
|
created_at
|
||||||
|
FROM
|
||||||
|
shares
|
||||||
|
WHERE
|
||||||
|
warren_id = $1 AND
|
||||||
|
path = $2
|
||||||
|
ORDER BY
|
||||||
|
created_at DESC
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(request.warren_id())
|
||||||
|
.bind(request.path())
|
||||||
|
.fetch_all(connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let shares = share_rows
|
||||||
|
.into_iter()
|
||||||
|
.map(Share::try_from)
|
||||||
|
.collect::<Result<Vec<Share>, TryFromShareRowError>>()?;
|
||||||
|
|
||||||
|
Ok(shares)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn create_share(
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
request: CreateShareRequest,
|
||||||
|
) -> anyhow::Result<Share> {
|
||||||
|
let mut tx = connection.begin().await?;
|
||||||
|
|
||||||
|
let password_hash = if let Some(password) = request.base().password() {
|
||||||
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
|
||||||
|
Some(
|
||||||
|
argon2
|
||||||
|
.hash_password(password.as_str().as_bytes(), &salt)
|
||||||
|
.map(|h| h.to_string())
|
||||||
|
.map_err(|e| anyhow!("Failed to hash file password: {e:?}"))?,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let expires_at = if let Some(lifetime) = request.base().lifetime() {
|
||||||
|
Some(Utc::now().timestamp_millis() + i64::try_from(lifetime)?.saturating_mul(1000))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let share: ShareRow = sqlx::query_as(
|
||||||
|
"
|
||||||
|
INSERT INTO shares (
|
||||||
|
creator_id,
|
||||||
|
warren_id,
|
||||||
|
path,
|
||||||
|
password_hash,
|
||||||
|
expires_at
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
TO_TIMESTAMP($5::double precision / 1000)
|
||||||
|
)
|
||||||
|
RETURNING
|
||||||
|
*
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(request.creator_id())
|
||||||
|
.bind(request.warren_id())
|
||||||
|
.bind(request.base().path())
|
||||||
|
.bind(password_hash)
|
||||||
|
.bind(expires_at)
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Share::try_from(share)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn delete_share(
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
request: DeleteShareRequest,
|
||||||
|
) -> anyhow::Result<Share> {
|
||||||
|
let mut tx = connection.begin().await?;
|
||||||
|
|
||||||
|
let share_row: ShareRow = sqlx::query_as(
|
||||||
|
"
|
||||||
|
DELETE FROM
|
||||||
|
shares
|
||||||
|
WHERE
|
||||||
|
id = $1
|
||||||
|
RETURNING
|
||||||
|
*
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(request.share_id())
|
||||||
|
.fetch_one(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(Share::try_from(share_row)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn verify_password(
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
request: VerifySharePasswordRequest,
|
||||||
|
) -> Result<Share, VerifySharePasswordError> {
|
||||||
|
let share_row: ShareRow = sqlx::query_as(
|
||||||
|
"
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
shares
|
||||||
|
WHERE
|
||||||
|
id = $1 AND
|
||||||
|
(expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP)
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.bind(request.share_id())
|
||||||
|
.fetch_one(connection)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
if is_not_found_error(&e) {
|
||||||
|
VerifySharePasswordError::NotFound
|
||||||
|
} else {
|
||||||
|
anyhow!(e).into()
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let share = Share::try_from(share_row).map_err(|e| anyhow!(e))?;
|
||||||
|
|
||||||
|
match (request.password(), share.password_hash()) {
|
||||||
|
// If the share doesn't have a password hash or the request provided a password but the share doesn't have one we do
|
||||||
|
// not care
|
||||||
|
(None, None) | (Some(_), None) => Ok(share),
|
||||||
|
// If there is a password hash and the request provided a password we have to check if they
|
||||||
|
// match
|
||||||
|
(Some(password), Some(hash)) => {
|
||||||
|
let argon = Argon2::default();
|
||||||
|
let hash = PasswordHash::new(hash)
|
||||||
|
.map_err(|e| VerifySharePasswordError::Unknown(anyhow!(e)))?;
|
||||||
|
|
||||||
|
argon
|
||||||
|
.verify_password(password.as_str().as_bytes(), &hash)
|
||||||
|
.map_err(|e| match e {
|
||||||
|
argon2::password_hash::Error::Password => {
|
||||||
|
VerifySharePasswordError::IncorrectPassword
|
||||||
|
}
|
||||||
|
_ => VerifySharePasswordError::Unknown(anyhow!(e)),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(share)
|
||||||
|
}
|
||||||
|
// If the request didn't provide a password but the share has a password hash the access
|
||||||
|
// should be denied
|
||||||
|
(None, Some(_hash)) => Err(VerifySharePasswordError::IncorrectPassword),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Postgres {
|
||||||
|
pub(super) async fn delete_expired_shares(
|
||||||
|
connection: &mut PgConnection,
|
||||||
|
) -> Result<u64, sqlx::Error> {
|
||||||
|
let delete_count = sqlx::query(
|
||||||
|
"
|
||||||
|
DELETE FROM
|
||||||
|
shares
|
||||||
|
WHERE
|
||||||
|
expires_at <= CURRENT_TIMESTAMP
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.execute(connection)
|
||||||
|
.await?
|
||||||
|
.rows_affected();
|
||||||
|
|
||||||
|
Ok(delete_count)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,12 @@ use uuid::Uuid;
|
|||||||
use crate::domain::warren::{
|
use crate::domain::warren::{
|
||||||
models::{
|
models::{
|
||||||
file::AbsoluteFilePath,
|
file::AbsoluteFilePath,
|
||||||
|
share::{
|
||||||
|
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
||||||
|
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
||||||
|
ListSharesError, ListSharesRequest, ListSharesResponse, Share,
|
||||||
|
VerifySharePasswordError, VerifySharePasswordRequest, VerifySharePasswordResponse,
|
||||||
|
},
|
||||||
warren::{
|
warren::{
|
||||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
||||||
@@ -132,6 +138,86 @@ impl WarrenRepository for Postgres {
|
|||||||
|
|
||||||
Ok(warren)
|
Ok(warren)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_warren_share(&self, request: GetShareRequest) -> Result<Share, GetShareError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
super::share::get_share(&mut connection, request)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_warren_share(
|
||||||
|
&self,
|
||||||
|
request: CreateShareRequest,
|
||||||
|
) -> Result<CreateShareResponse, CreateShareError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
super::share::create_share(&mut connection, request)
|
||||||
|
.await
|
||||||
|
.map(CreateShareResponse::new)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_warren_shares(
|
||||||
|
&self,
|
||||||
|
request: ListSharesRequest,
|
||||||
|
) -> Result<ListSharesResponse, ListSharesError> {
|
||||||
|
let warren = self.fetch_warren((&request).into()).await?;
|
||||||
|
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
let path = request.path().clone();
|
||||||
|
|
||||||
|
super::share::list_shares(&mut connection, request)
|
||||||
|
.await
|
||||||
|
.map(|shares| ListSharesResponse::new(warren, path, shares))
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_warren_share(
|
||||||
|
&self,
|
||||||
|
request: DeleteShareRequest,
|
||||||
|
) -> Result<DeleteShareResponse, DeleteShareError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
super::share::delete_share(&mut connection, request)
|
||||||
|
.await
|
||||||
|
.map(DeleteShareResponse::new)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_warren_share_password(
|
||||||
|
&self,
|
||||||
|
request: VerifySharePasswordRequest,
|
||||||
|
) -> Result<VerifySharePasswordResponse, VerifySharePasswordError> {
|
||||||
|
let mut connection = self
|
||||||
|
.pool
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.context("Failed to get a PostgreSQL connection")?;
|
||||||
|
|
||||||
|
super::share::verify_password(&mut connection, request)
|
||||||
|
.await
|
||||||
|
.map(VerifySharePasswordResponse::new)
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Postgres {
|
impl Postgres {
|
||||||
|
|||||||
@@ -11,12 +11,14 @@
|
|||||||
"@nuxt/test-utils": "3.19.2",
|
"@nuxt/test-utils": "3.19.2",
|
||||||
"@pinia/nuxt": "^0.11.1",
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vee-validate/yup": "^4.15.1",
|
"@vee-validate/yup": "^4.15.1",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.7.0",
|
||||||
"byte-size": "^9.0.1",
|
"byte-size": "^9.0.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs-nuxt": "2.1.11",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"lucide-vue-next": "^0.525.0",
|
"lucide-vue-next": "^0.525.0",
|
||||||
"nuxt": "^3.17.6",
|
"nuxt": "^3.17.6",
|
||||||
@@ -489,8 +491,12 @@
|
|||||||
|
|
||||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||||
|
|
||||||
|
"@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="],
|
||||||
|
|
||||||
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
|
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="],
|
||||||
|
|
||||||
|
"@tanstack/vue-table": ["@tanstack/vue-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "vue": ">=3.2" } }, "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw=="],
|
||||||
|
|
||||||
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww=="],
|
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww=="],
|
||||||
|
|
||||||
"@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="],
|
"@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="],
|
||||||
@@ -583,11 +589,11 @@
|
|||||||
|
|
||||||
"@vue/shared": ["@vue/shared@3.5.17", "", {}, "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="],
|
"@vue/shared": ["@vue/shared@3.5.17", "", {}, "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="],
|
||||||
|
|
||||||
"@vueuse/core": ["@vueuse/core@13.5.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.5.0", "@vueuse/shared": "13.5.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-wV7z0eUpifKmvmN78UBZX8T7lMW53Nrk6JP5+6hbzrB9+cJ3jr//hUlhl9TZO/03bUkMK6gGkQpqOPWoabr72g=="],
|
"@vueuse/core": ["@vueuse/core@13.7.0", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "13.7.0", "@vueuse/shared": "13.7.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-myagn09+c6BmS6yHc1gTwwsdZilAovHslMjyykmZH3JNyzI5HoWhv114IIdytXiPipdHJ2gDUx0PB93jRduJYg=="],
|
||||||
|
|
||||||
"@vueuse/metadata": ["@vueuse/metadata@13.5.0", "", {}, "sha512-euhItU3b0SqXxSy8u1XHxUCdQ8M++bsRs+TYhOLDU/OykS7KvJnyIFfep0XM5WjIFry9uAPlVSjmVHiqeshmkw=="],
|
"@vueuse/metadata": ["@vueuse/metadata@13.7.0", "", {}, "sha512-8okFhS/1ite8EwUdZZfvTYowNTfXmVCOrBFlA31O0HD8HKXhY+WtTRyF0LwbpJfoFPc+s9anNJIXMVrvP7UTZg=="],
|
||||||
|
|
||||||
"@vueuse/shared": ["@vueuse/shared@13.5.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-K7GrQIxJ/ANtucxIXbQlUHdB0TPA8c+q5i+zbrjxuhJCnJ9GtBg75sBSnvmLSxHKPg2Yo8w62PWksl9kwH0Q8g=="],
|
"@vueuse/shared": ["@vueuse/shared@13.7.0", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Wi2LpJi4UA9kM0OZ0FCZslACp92HlVNw1KPaDY6RAzvQ+J1s7seOtcOpmkfbD5aBSmMn9NvOakc8ZxMxmDXTIg=="],
|
||||||
|
|
||||||
"@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="],
|
"@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="],
|
||||||
|
|
||||||
@@ -813,6 +819,10 @@
|
|||||||
|
|
||||||
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
|
||||||
|
|
||||||
|
"dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="],
|
||||||
|
|
||||||
|
"dayjs-nuxt": ["dayjs-nuxt@2.1.11", "", { "dependencies": { "@nuxt/kit": "^3.7.4", "dayjs": "^1.11.10" } }, "sha512-KDDNiET7KAKf6yzL3RaPWq5aV7ql9QTt5fIDYv+4eOegDmnEQGjwkKYADDystsKtPjt7QZerpVbhC96o3BIyqQ=="],
|
||||||
|
|
||||||
"db0": ["db0@0.3.2", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw=="],
|
"db0": ["db0@0.3.2", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-xzWNQ6jk/+NtdfLyXEipbX55dmDSeteLFt/ayF+wZUU5bzKgmrDOxmInUTbyVRp46YwnJdkDA1KhB7WIXFofJw=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import type { DirectoryEntry } from '~/shared/types';
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
const { entry } = defineProps<{ entry: DirectoryEntry }>();
|
const { entry } = defineProps<{ entry: DirectoryEntry }>();
|
||||||
|
const emit = defineEmits<{
|
||||||
const warrenStore = useWarrenStore();
|
back: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
const onDrop = onDirectoryEntryDrop(entry, true);
|
const onDrop = onDirectoryEntryDrop(entry, true);
|
||||||
</script>
|
</script>
|
||||||
@@ -12,7 +13,7 @@ const onDrop = onDirectoryEntryDrop(entry, true);
|
|||||||
<button
|
<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"
|
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
|
@contextmenu.prevent
|
||||||
@click="() => warrenStore.backCurrentPath()"
|
@click="() => emit('back')"
|
||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center">
|
<div class="flex flex-row items-center">
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import {
|
|||||||
ContextMenuItem,
|
ContextMenuItem,
|
||||||
ContextMenuSeparator,
|
ContextMenuSeparator,
|
||||||
} from '@/components/ui/context-menu';
|
} from '@/components/ui/context-menu';
|
||||||
import {
|
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
|
||||||
deleteWarrenDirectory,
|
|
||||||
deleteWarrenFile,
|
|
||||||
fetchFile,
|
|
||||||
} from '~/lib/api/warrens';
|
|
||||||
import type { DirectoryEntry } from '#shared/types';
|
import type { DirectoryEntry } from '#shared/types';
|
||||||
import { toast } from 'vue-sonner';
|
import { toast } from 'vue-sonner';
|
||||||
|
|
||||||
@@ -23,6 +19,11 @@ const { entry, disabled } = defineProps<{
|
|||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'entry-click': [entry: DirectoryEntry];
|
||||||
|
'entry-download': [entry: DirectoryEntry];
|
||||||
|
}>();
|
||||||
|
|
||||||
const deleting = ref(false);
|
const deleting = ref(false);
|
||||||
const isCopied = computed(
|
const isCopied = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -63,30 +64,7 @@ async function openRenameDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onClick() {
|
async function onClick() {
|
||||||
if (warrenStore.loading || warrenStore.current == null) {
|
emit('entry-click', entry);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.fileType === 'directory') {
|
|
||||||
warrenStore.addToCurrentWarrenPath(entry.name);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.mimeType == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.mimeType.startsWith('image/')) {
|
|
||||||
const result = await fetchFile(
|
|
||||||
warrenStore.current.warrenId,
|
|
||||||
warrenStore.current.path,
|
|
||||||
entry.name
|
|
||||||
);
|
|
||||||
if (result.success) {
|
|
||||||
const url = URL.createObjectURL(result.data);
|
|
||||||
warrenStore.imageViewer.src = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDragStart(e: DragEvent) {
|
function onDragStart(e: DragEvent) {
|
||||||
@@ -112,38 +90,22 @@ function onCopy() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDownload() {
|
function onShare() {
|
||||||
if (warrenStore.current == null) {
|
useShareDialog().openDialog(entry);
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.fileType !== 'file') {
|
function onDownload() {
|
||||||
toast.warning('Download', {
|
emit('entry-download', entry);
|
||||||
description: 'Directory downloads are not supported yet',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
|
|
||||||
anchor.download = entry.name;
|
|
||||||
anchor.href = getApiUrl(
|
|
||||||
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
|
||||||
);
|
|
||||||
anchor.rel = 'noopener';
|
|
||||||
anchor.target = '_blank';
|
|
||||||
|
|
||||||
anchor.click();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger>
|
<ContextMenuTrigger class="flex sm:w-52">
|
||||||
<button
|
<button
|
||||||
:disabled="warrenStore.loading || disabled"
|
:disabled="warrenStore.loading || disabled"
|
||||||
:class="[
|
: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',
|
'bg-accent/30 border-border flex w-full translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none',
|
||||||
isCopied && 'border-primary/50 border',
|
isCopied && 'border-primary/50 border',
|
||||||
]"
|
]"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@@ -151,38 +113,7 @@ async function onDownload() {
|
|||||||
@drop="onDrop"
|
@drop="onDrop"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row items-center">
|
<DirectoryEntryIcon :entry />
|
||||||
<Icon
|
|
||||||
v-if="
|
|
||||||
entry.fileType !== 'file' ||
|
|
||||||
entry.mimeType == null ||
|
|
||||||
!entry.mimeType.startsWith('image/')
|
|
||||||
"
|
|
||||||
class="size-6"
|
|
||||||
:name="
|
|
||||||
entry.fileType === 'file'
|
|
||||||
? getFileIcon(entry.mimeType)
|
|
||||||
: 'lucide:folder'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<object
|
|
||||||
v-else
|
|
||||||
:type="entry.mimeType"
|
|
||||||
class="size-6 object-cover"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
:data="
|
|
||||||
getApiUrl(
|
|
||||||
`warrens/files/cat?warrenId=${warrenStore.current!.warrenId}&path=${joinPaths(warrenStore.current!.path, entry.name)}`
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
class="size-6"
|
|
||||||
:name="getFileIcon(entry.mimeType)"
|
|
||||||
/>
|
|
||||||
</object>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col items-start justify-stretch gap-0 overflow-hidden text-left leading-6"
|
class="flex w-full flex-col items-start justify-stretch gap-0 overflow-hidden text-left leading-6"
|
||||||
@@ -198,11 +129,17 @@ async function onDownload() {
|
|||||||
</button>
|
</button>
|
||||||
</ContextMenuTrigger>
|
</ContextMenuTrigger>
|
||||||
<ContextMenuContent>
|
<ContextMenuContent>
|
||||||
<ContextMenuItem @select="openRenameDialog">
|
<ContextMenuItem
|
||||||
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@select="openRenameDialog"
|
||||||
|
>
|
||||||
<Icon name="lucide:pencil" />
|
<Icon name="lucide:pencil" />
|
||||||
Rename
|
Rename
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem @select="onCopy">
|
<ContextMenuItem
|
||||||
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@select="onCopy"
|
||||||
|
>
|
||||||
<Icon name="lucide:copy" />
|
<Icon name="lucide:copy" />
|
||||||
Copy
|
Copy
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
@@ -213,15 +150,28 @@ async function onDownload() {
|
|||||||
<Icon name="lucide:download" />
|
<Icon name="lucide:download" />
|
||||||
Download
|
Download
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem
|
||||||
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@select="onShare"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:share" />
|
||||||
|
Share
|
||||||
|
</ContextMenuItem>
|
||||||
|
|
||||||
<ContextMenuSeparator />
|
<ContextMenuSeparator
|
||||||
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
/>
|
||||||
|
|
||||||
<ContextMenuItem @select="() => submitDelete(false)">
|
<ContextMenuItem
|
||||||
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
|
@select="() => submitDelete(false)"
|
||||||
|
>
|
||||||
<Icon name="lucide:trash-2" />
|
<Icon name="lucide:trash-2" />
|
||||||
Delete
|
Delete
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
<ContextMenuItem
|
<ContextMenuItem
|
||||||
v-if="entry.fileType === 'directory'"
|
v-if="entry.fileType === 'directory'"
|
||||||
|
:class="[warrenStore.current == null && 'hidden']"
|
||||||
@select="() => submitDelete(true)"
|
@select="() => submitDelete(true)"
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
|
|||||||
47
frontend/components/DirectoryEntryIcon.vue
Normal file
47
frontend/components/DirectoryEntryIcon.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
|
const { entry } = defineProps<{
|
||||||
|
entry: DirectoryEntry;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<Icon
|
||||||
|
v-if="
|
||||||
|
entry.fileType !== 'file' ||
|
||||||
|
entry.mimeType == null ||
|
||||||
|
!entry.mimeType.startsWith('image/')
|
||||||
|
"
|
||||||
|
class="size-6"
|
||||||
|
:name="
|
||||||
|
entry.fileType === 'file'
|
||||||
|
? getFileIcon(entry.mimeType)
|
||||||
|
: 'lucide:folder'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<object
|
||||||
|
v-else-if="warrenStore.current != null"
|
||||||
|
:type="entry.mimeType"
|
||||||
|
class="size-6 object-cover"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
:data="
|
||||||
|
route.meta.layout === 'share'
|
||||||
|
? getApiUrl(
|
||||||
|
`warrens/files/cat_share?shareId=${route.query.id}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||||
|
)
|
||||||
|
: getApiUrl(
|
||||||
|
`warrens/files/cat?warrenId=${warrenStore.current!.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<Icon class="size-6" :name="getFileIcon(entry.mimeType)" />
|
||||||
|
</object>
|
||||||
|
<Icon v-else class="size-6" :name="getFileIcon(entry.mimeType)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -2,10 +2,22 @@
|
|||||||
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, parent, isOverDropZone } = defineProps<{
|
const {
|
||||||
|
entries,
|
||||||
|
parent,
|
||||||
|
isOverDropZone,
|
||||||
|
disableEntries = false,
|
||||||
|
} = defineProps<{
|
||||||
entries: DirectoryEntry[];
|
entries: DirectoryEntry[];
|
||||||
parent: DirectoryEntry | null;
|
parent: DirectoryEntry | null;
|
||||||
isOverDropZone?: boolean;
|
isOverDropZone?: boolean;
|
||||||
|
disableEntries?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'entry-click': [entry: DirectoryEntry];
|
||||||
|
'entry-download': [entry: DirectoryEntry];
|
||||||
|
back: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { isLoading } = useLoadingIndicator();
|
const { isLoading } = useLoadingIndicator();
|
||||||
@@ -13,23 +25,39 @@ const { isLoading } = useLoadingIndicator();
|
|||||||
const sortedEntries = computed(() =>
|
const sortedEntries = computed(() =>
|
||||||
entries.toSorted((a, b) => a.name.localeCompare(b.name))
|
entries.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function onEntryClicked(entry: DirectoryEntry) {
|
||||||
|
emit('entry-click', entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEntryDownload(entry: DirectoryEntry) {
|
||||||
|
emit('entry-download', entry);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ScrollArea class="h-full w-full">
|
<ScrollArea class="flex h-full w-full flex-col overflow-hidden">
|
||||||
<div
|
<div
|
||||||
v-if="isOverDropZone"
|
v-if="isOverDropZone"
|
||||||
class="bg-background/50 pointer-events-none absolute flex h-full w-full items-center justify-center"
|
class="bg-background/50 pointer-events-none absolute flex h-full w-full items-center justify-center"
|
||||||
>
|
>
|
||||||
<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
|
||||||
<DirectoryBackEntry v-if="parent != null" :entry="parent" />
|
class="flex w-full flex-col gap-2 overflow-hidden sm:flex-row sm:flex-wrap"
|
||||||
|
>
|
||||||
|
<DirectoryBackEntry
|
||||||
|
v-if="parent != null"
|
||||||
|
:entry="parent"
|
||||||
|
@back="() => emit('back')"
|
||||||
|
/>
|
||||||
<DirectoryEntry
|
<DirectoryEntry
|
||||||
v-for="entry in sortedEntries"
|
v-for="entry in sortedEntries"
|
||||||
:key="entry.name"
|
:key="entry.name"
|
||||||
:entry="entry"
|
:entry="entry"
|
||||||
:disabled="isLoading"
|
:disabled="isLoading || disableEntries"
|
||||||
|
@entry-click="onEntryClicked"
|
||||||
|
@entry-download="onEntryDownload"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
95
frontend/components/SharesTable.vue
Normal file
95
frontend/components/SharesTable.vue
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '@/components/ui/table';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { deleteShare } from '~/lib/api/shares';
|
||||||
|
import type { Share } from '~/shared/types/shares';
|
||||||
|
|
||||||
|
const { shares } = defineProps<{
|
||||||
|
shares: Share[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
function onCopyClicked(share: Share) {
|
||||||
|
const link = getShareLink(share);
|
||||||
|
if (copyToClipboard(link)) {
|
||||||
|
toast.success('Share', {
|
||||||
|
description: 'Copied the link to the clipboard',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log(`Here's the link to ${share.path}: ${link}`);
|
||||||
|
toast.error('Share', {
|
||||||
|
description: `Failed to copy the link to the clipboard. Logged it to the console instead.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDeleteClicked(share: Share) {
|
||||||
|
const result = await deleteShare(share.warrenId, share.id);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
refreshNuxtData('current-file-shares');
|
||||||
|
toast.success('Share', {
|
||||||
|
description: `Successfully deleted the share for ${result.share.path}`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.error('Share', {
|
||||||
|
description: 'Failed to delete share',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead class="w-[100px]">ID</TableHead>
|
||||||
|
<TableHead>Password</TableHead>
|
||||||
|
<TableHead>Expiration</TableHead>
|
||||||
|
<TableHead>Created</TableHead>
|
||||||
|
<TableHead class="w-0 text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<TableRow v-for="share in shares" :key="share.id">
|
||||||
|
<TableCell class="font-medium" :title="share.id">
|
||||||
|
<span class="hidden sm:block">
|
||||||
|
{{ share.id }}
|
||||||
|
</span>
|
||||||
|
<span class="block sm:hidden"
|
||||||
|
>{{ share.id.slice(0, 3) }}...{{ share.id.slice(-3) }}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{{ share.password ? 'Yes' : 'No' }}</TableCell>
|
||||||
|
<TableCell>{{
|
||||||
|
share.expiresAt == null
|
||||||
|
? 'Never'
|
||||||
|
: $dayjs(share.expiresAt).format('MMM D, YYYY HH:mm')
|
||||||
|
}}</TableCell>
|
||||||
|
<TableCell>{{
|
||||||
|
$dayjs(share.createdAt).format('MMM D, YYYY HH:mm')
|
||||||
|
}}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
@click="() => onCopyClicked(share)"
|
||||||
|
><Icon name="lucide:copy"
|
||||||
|
/></Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
@click="() => onDeleteClicked(share)"
|
||||||
|
><Icon name="lucide:trash-2"
|
||||||
|
/></Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</template>
|
||||||
203
frontend/components/actions/ShareDialog.vue
Normal file
203
frontend/components/actions/ShareDialog.vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { useShareDialog } from '@/stores';
|
||||||
|
import { toTypedSchema } from '@vee-validate/yup';
|
||||||
|
import { useForm } from 'vee-validate';
|
||||||
|
import { toast } from 'vue-sonner';
|
||||||
|
import { createShare, listShares } from '~/lib/api/shares';
|
||||||
|
import { shareSchema } from '~/lib/schemas/share';
|
||||||
|
import type { Share } from '~/shared/types/shares';
|
||||||
|
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
const dialog = useShareDialog();
|
||||||
|
|
||||||
|
const newScreen = ref(false);
|
||||||
|
|
||||||
|
const existingShares = ref<Share[]>([]);
|
||||||
|
useAsyncData('current-file-shares', async () => {
|
||||||
|
if (warrenStore.current == null || dialog.target == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await listShares(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
joinPaths(warrenStore.current.path, dialog.target.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.shares.length < 1) {
|
||||||
|
newScreen.value = true;
|
||||||
|
} else {
|
||||||
|
newScreen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
existingShares.value = result.shares;
|
||||||
|
});
|
||||||
|
dialog.$subscribe(async (_, value) => {
|
||||||
|
if (value.target !== null) {
|
||||||
|
await refreshNuxtData('current-file-shares');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onOpenChange() {
|
||||||
|
dialog.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancel() {
|
||||||
|
if (newScreen.value && existingShares.value.length > 0) {
|
||||||
|
newScreen.value = false;
|
||||||
|
} else {
|
||||||
|
dialog.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
validationSchema: toTypedSchema(shareSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit(async (values) => {
|
||||||
|
if (dialog.target == null || warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createShare(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
joinPaths(warrenStore.current.path, dialog.target.name),
|
||||||
|
values.password ?? null,
|
||||||
|
values.lifetime ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
toast.success('Share', {
|
||||||
|
description: `Successfully created a share for ${dialog.target.name}`,
|
||||||
|
});
|
||||||
|
await refreshNuxtData('current-file-shares');
|
||||||
|
newScreen.value = false;
|
||||||
|
} else {
|
||||||
|
toast.error('Share', {
|
||||||
|
description: `Failed to create share`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog :open="dialog.target != null" @update:open="onOpenChange">
|
||||||
|
<DialogTrigger as-child>
|
||||||
|
<slot />
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent
|
||||||
|
v-if="dialog.target != null"
|
||||||
|
class="w-full !max-w-[calc(100vw-8px)] sm:!max-w-[min(98vw,1000px)]"
|
||||||
|
>
|
||||||
|
<DialogHeader class="overflow-hidden">
|
||||||
|
<DialogTitle class="truncate"
|
||||||
|
>Share {{ dialog.target.name }}</DialogTitle
|
||||||
|
>
|
||||||
|
<DialogDescription
|
||||||
|
>Create a shareable link to this
|
||||||
|
{{ dialog.target.fileType }}</DialogDescription
|
||||||
|
>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<SharesTable v-if="!newScreen" :shares="existingShares" />
|
||||||
|
<form
|
||||||
|
v-else
|
||||||
|
id="share-form"
|
||||||
|
class="grid gap-4"
|
||||||
|
@submit.prevent="onSubmit"
|
||||||
|
>
|
||||||
|
<FormField v-slot="{ componentField }" name="password">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
autocomplete="off"
|
||||||
|
data-1p-ignore
|
||||||
|
data-protonpass-ignore
|
||||||
|
data-bwignore
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
form.values.password == null ||
|
||||||
|
form.values.password.length < 1
|
||||||
|
"
|
||||||
|
>
|
||||||
|
The share will not require a password
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
The share will require the specified password
|
||||||
|
</span>
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
<FormField v-slot="{ componentField }" name="lifetime">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Lifetime (seconds)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-row justify-between gap-1"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
v-bind="componentField"
|
||||||
|
id="lifetime"
|
||||||
|
type="number"
|
||||||
|
class="w-full max-w-full min-w-0"
|
||||||
|
autocomplete="off"
|
||||||
|
data-1p-ignore
|
||||||
|
data-protonpass-ignore
|
||||||
|
data-bwignore
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
<FormDescription>
|
||||||
|
<span
|
||||||
|
v-if="
|
||||||
|
form.values.lifetime != null &&
|
||||||
|
form.values.lifetime > 0
|
||||||
|
"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
The share will expire in
|
||||||
|
{{
|
||||||
|
$dayjs
|
||||||
|
.duration({
|
||||||
|
seconds: form.values.lifetime,
|
||||||
|
})
|
||||||
|
.humanize()
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else> The share will be permanent </span>
|
||||||
|
</FormDescription>
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" @click="onCancel">Cancel</Button>
|
||||||
|
<Button v-if="newScreen" type="submit" form="share-form"
|
||||||
|
>Share</Button
|
||||||
|
>
|
||||||
|
<Button v-else @click="() => (newScreen = true)">New</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -13,19 +13,37 @@ import type { UserWarren } from '~/shared/types/warrens';
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
userWarren: UserWarren;
|
userWarren: UserWarren;
|
||||||
}>();
|
}>();
|
||||||
|
let realUserWarrenState: UserWarren = JSON.parse(
|
||||||
|
JSON.stringify(props.userWarren)
|
||||||
|
);
|
||||||
const userWarren = props.userWarren;
|
const userWarren = props.userWarren;
|
||||||
|
|
||||||
const adminStore = useAdminStore();
|
const adminStore = useAdminStore();
|
||||||
|
|
||||||
const updatePermissionsDebounced = useDebounceFn(
|
const updatePermissionsDebounced = useDebounceFn(
|
||||||
async (userWarren: UserWarren) => {
|
async (uw: UserWarren) => {
|
||||||
const result = await editUserWarren(userWarren);
|
const result = await editUserWarren(uw);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
for (const [key, value] of Object.entries(result.data)) {
|
||||||
|
if (key in userWarren) {
|
||||||
|
// @ts-expect-error Element implicitly has an 'any' type because expression of type 'string' can't be used to index type
|
||||||
|
userWarren[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
realUserWarrenState = JSON.parse(JSON.stringify(result.data));
|
||||||
toast.success('Permissions', {
|
toast.success('Permissions', {
|
||||||
description: `Successfully updated the user's permissions`,
|
description: `Successfully updated the user's permissions`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(realUserWarrenState)) {
|
||||||
|
if (key in userWarren) {
|
||||||
|
// @ts-expect-error Element implicitly has an 'any' type because expression of type 'string' can't be used to index type
|
||||||
|
userWarren[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userWarren.canCreateShares = realUserWarrenState.canCreateShares;
|
||||||
|
|
||||||
toast.error('Permissions', {
|
toast.error('Permissions', {
|
||||||
description: `Failed to update the user's permissions`,
|
description: `Failed to update the user's permissions`,
|
||||||
});
|
});
|
||||||
|
|||||||
16
frontend/components/ui/table/Table.vue
Normal file
16
frontend/components/ui/table/Table.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-slot="table-container" class="relative w-full overflow-auto">
|
||||||
|
<table data-slot="table" :class="cn('w-full caption-bottom text-sm', props.class)">
|
||||||
|
<slot />
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/table/TableBody.vue
Normal file
17
frontend/components/ui/table/TableBody.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
:class="cn('[&_tr:last-child]:border-0', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</tbody>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/table/TableCaption.vue
Normal file
17
frontend/components/ui/table/TableCaption.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
:class="cn('text-muted-foreground mt-4 text-sm', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</caption>
|
||||||
|
</template>
|
||||||
22
frontend/components/ui/table/TableCell.vue
Normal file
22
frontend/components/ui/table/TableCell.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
34
frontend/components/ui/table/TableEmpty.vue
Normal file
34
frontend/components/ui/table/TableEmpty.vue
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { reactiveOmit } from "@vueuse/core"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import TableCell from "./TableCell.vue"
|
||||||
|
import TableRow from "./TableRow.vue"
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
colspan?: number
|
||||||
|
}>(), {
|
||||||
|
colspan: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, "class")
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'p-4 whitespace-nowrap align-middle text-sm text-foreground',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center py-10">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/table/TableFooter.vue
Normal file
17
frontend/components/ui/table/TableFooter.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
:class="cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</tfoot>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/table/TableHead.vue
Normal file
17
frontend/components/ui/table/TableHead.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
:class="cn('text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/table/TableHeader.vue
Normal file
17
frontend/components/ui/table/TableHeader.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
:class="cn('[&_tr]:border-b', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</thead>
|
||||||
|
</template>
|
||||||
17
frontend/components/ui/table/TableRow.vue
Normal file
17
frontend/components/ui/table/TableRow.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from "vue"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes["class"]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
:class="cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
9
frontend/components/ui/table/index.ts
Normal file
9
frontend/components/ui/table/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export { default as Table } from "./Table.vue"
|
||||||
|
export { default as TableBody } from "./TableBody.vue"
|
||||||
|
export { default as TableCaption } from "./TableCaption.vue"
|
||||||
|
export { default as TableCell } from "./TableCell.vue"
|
||||||
|
export { default as TableEmpty } from "./TableEmpty.vue"
|
||||||
|
export { default as TableFooter } from "./TableFooter.vue"
|
||||||
|
export { default as TableHead } from "./TableHead.vue"
|
||||||
|
export { default as TableHeader } from "./TableHeader.vue"
|
||||||
|
export { default as TableRow } from "./TableRow.vue"
|
||||||
10
frontend/components/ui/table/utils.ts
Normal file
10
frontend/components/ui/table/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Updater } from "@tanstack/vue-table"
|
||||||
|
|
||||||
|
import type { Ref } from "vue"
|
||||||
|
import { isFunction } from "@tanstack/vue-table"
|
||||||
|
|
||||||
|
export function valueUpdater<T>(updaterOrValue: Updater<T>, ref: Ref<T>) {
|
||||||
|
ref.value = isFunction(updaterOrValue)
|
||||||
|
? updaterOrValue(ref.value)
|
||||||
|
: updaterOrValue
|
||||||
|
}
|
||||||
@@ -27,16 +27,7 @@ export function setAuthSession(value: {
|
|||||||
}) {
|
}) {
|
||||||
useAuthSession().value = value;
|
useAuthSession().value = value;
|
||||||
|
|
||||||
let cookie = `authorization=WarrenAuth ${value.id}; path=/; SameSite=Lax; Secure;`;
|
const cookie = `authorization=WarrenAuth ${value.id}; path=/; SameSite=Lax; Secure;`;
|
||||||
|
|
||||||
const config = useRuntimeConfig().public;
|
|
||||||
console.log('config', config);
|
|
||||||
const cookieDomain = config.authCookieDomain;
|
|
||||||
if (cookieDomain != null && cookieDomain.length > 0) {
|
|
||||||
cookie += ` domain=${cookieDomain}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(cookie);
|
|
||||||
|
|
||||||
document.cookie = cookie;
|
document.cookie = cookie;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ await useAsyncData('warrens', async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
|
<ActionsShareDialog />
|
||||||
<ImageViewer />
|
<ImageViewer />
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<SidebarInset class="flex flex-col-reverse md:flex-col">
|
<SidebarInset class="flex flex-col-reverse md:flex-col">
|
||||||
|
|||||||
8
frontend/layouts/share.vue
Normal file
8
frontend/layouts/share.vue
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="flex h-full w-full items-center justify-center">
|
||||||
|
<ImageViewer />
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
176
frontend/lib/api/shares.ts
Normal file
176
frontend/lib/api/shares.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import type { ApiResponse } from '~/shared/types/api';
|
||||||
|
import type { Share } from '~/shared/types/shares';
|
||||||
|
import { getApiHeaders } from '.';
|
||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
|
export async function getShare(
|
||||||
|
shareId: string
|
||||||
|
): Promise<
|
||||||
|
{ success: true; share: Share; file: DirectoryEntry } | { success: false }
|
||||||
|
> {
|
||||||
|
const { data } = await useFetch<
|
||||||
|
ApiResponse<{ share: Share; file: DirectoryEntry }>
|
||||||
|
>(getApiUrl('warrens/files/get_share'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(false),
|
||||||
|
body: JSON.stringify({
|
||||||
|
shareId: shareId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { share, file } = data.value.data;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
share,
|
||||||
|
file,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createShare(
|
||||||
|
warrenId: string,
|
||||||
|
path: string,
|
||||||
|
password: string | null,
|
||||||
|
lifetime: number | null
|
||||||
|
): Promise<{ success: true; share: Share } | { success: false }> {
|
||||||
|
const { data } = await useFetch<ApiResponse<Share>>(
|
||||||
|
getApiUrl('warrens/files/create_share'),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
warrenId: warrenId,
|
||||||
|
path: path,
|
||||||
|
lifetime: lifetime,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, share: data.value.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listShares(
|
||||||
|
warrenId: string,
|
||||||
|
path: string
|
||||||
|
): Promise<{ success: true; shares: Share[] } | { success: false }> {
|
||||||
|
const { data } = await useFetch<ApiResponse<Share[]>>(
|
||||||
|
getApiUrl('warrens/files/list_shares'),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
warrenId: warrenId,
|
||||||
|
path: path,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, shares: data.value.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteShare(
|
||||||
|
warrenId: string,
|
||||||
|
shareId: string
|
||||||
|
): Promise<{ success: true; share: Share } | { success: false }> {
|
||||||
|
const { data } = await useFetch<ApiResponse<Share>>(
|
||||||
|
getApiUrl('warrens/files/delete_share'),
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
warrenId: warrenId,
|
||||||
|
shareId: shareId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, share: data.value.data };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listShareFiles(
|
||||||
|
shareId: string,
|
||||||
|
path: string,
|
||||||
|
password: string | null
|
||||||
|
): Promise<
|
||||||
|
| { success: true; files: DirectoryEntry[]; parent: DirectoryEntry | null }
|
||||||
|
| { success: false }
|
||||||
|
> {
|
||||||
|
const { data } = await useFetch<
|
||||||
|
ApiResponse<{ files: DirectoryEntry[]; parent: DirectoryEntry | null }>
|
||||||
|
>(getApiUrl('warrens/files/ls_share'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
shareId: shareId,
|
||||||
|
path: path,
|
||||||
|
password: password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { files, parent } = data.value.data;
|
||||||
|
|
||||||
|
return { success: true, files, parent };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchShareFile(
|
||||||
|
shareId: string,
|
||||||
|
path: string,
|
||||||
|
password: string | null
|
||||||
|
): Promise<{ success: true; data: Blob } | { success: false }> {
|
||||||
|
const { data } = await useFetch<Blob>(
|
||||||
|
getApiUrl(`warrens/files/cat_share?shareId=${shareId}&path=${path}`),
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers:
|
||||||
|
password != null
|
||||||
|
? {
|
||||||
|
'X-Share-Password': password,
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
responseType: 'blob',
|
||||||
|
cache: 'default',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data.value == null) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: data.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
12
frontend/lib/schemas/share.ts
Normal file
12
frontend/lib/schemas/share.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { number, object, string } from 'yup';
|
||||||
|
|
||||||
|
export const shareSchema = object({
|
||||||
|
password: string()
|
||||||
|
.trim()
|
||||||
|
.transform((s: string) => (s.length > 0 ? s : undefined))
|
||||||
|
.optional(),
|
||||||
|
lifetime: number()
|
||||||
|
.positive()
|
||||||
|
.transform((n) => (isNaN(n) ? undefined : n))
|
||||||
|
.optional(),
|
||||||
|
});
|
||||||
@@ -14,8 +14,13 @@ export default defineNuxtConfig({
|
|||||||
'shadcn-nuxt',
|
'shadcn-nuxt',
|
||||||
'@nuxtjs/color-mode',
|
'@nuxtjs/color-mode',
|
||||||
'@pinia/nuxt',
|
'@pinia/nuxt',
|
||||||
|
'dayjs-nuxt',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
dayjs: {
|
||||||
|
plugins: ['duration', 'relativeTime'],
|
||||||
|
},
|
||||||
|
|
||||||
css: ['~/assets/css/tailwind.css', '~/assets/css/sonner.css'],
|
css: ['~/assets/css/tailwind.css', '~/assets/css/sonner.css'],
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
|
|||||||
@@ -18,12 +18,14 @@
|
|||||||
"@nuxt/test-utils": "3.19.2",
|
"@nuxt/test-utils": "3.19.2",
|
||||||
"@pinia/nuxt": "^0.11.1",
|
"@pinia/nuxt": "^0.11.1",
|
||||||
"@tailwindcss/vite": "^4.1.11",
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@tanstack/vue-table": "^8.21.3",
|
||||||
"@vee-validate/yup": "^4.15.1",
|
"@vee-validate/yup": "^4.15.1",
|
||||||
"@vee-validate/zod": "^4.15.1",
|
"@vee-validate/zod": "^4.15.1",
|
||||||
"@vueuse/core": "^13.5.0",
|
"@vueuse/core": "^13.7.0",
|
||||||
"byte-size": "^9.0.1",
|
"byte-size": "^9.0.1",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs-nuxt": "2.1.11",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"lucide-vue-next": "^0.525.0",
|
"lucide-vue-next": "^0.525.0",
|
||||||
"nuxt": "^3.17.6",
|
"nuxt": "^3.17.6",
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ if (
|
|||||||
route.query.state &&
|
route.query.state &&
|
||||||
typeof route.query.state === 'string'
|
typeof route.query.state === 'string'
|
||||||
) {
|
) {
|
||||||
console.log('SEND');
|
|
||||||
loggingIn.value = true;
|
loggingIn.value = true;
|
||||||
const { success } = await oidcLoginUser(
|
const { success } = await oidcLoginUser(
|
||||||
route.query.code,
|
route.query.code,
|
||||||
|
|||||||
237
frontend/pages/share.vue
Normal file
237
frontend/pages/share.vue
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { fetchShareFile, getShare, listShareFiles } from '~/lib/api/shares';
|
||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
import type { Share } from '~/shared/types/shares';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'share',
|
||||||
|
});
|
||||||
|
|
||||||
|
const warrenStore = useWarrenStore();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const share = await getShareFromQuery();
|
||||||
|
const entries = ref<DirectoryEntry[] | null>(null);
|
||||||
|
const parent = ref<DirectoryEntry | null>(null);
|
||||||
|
const password = ref<string>('');
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
|
if (share != null) {
|
||||||
|
warrenStore.setCurrentWarren(share.data.warrenId, '/');
|
||||||
|
|
||||||
|
if (!share.data.password) {
|
||||||
|
await loadFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getShareFromQuery(): Promise<{
|
||||||
|
data: Share;
|
||||||
|
file: DirectoryEntry;
|
||||||
|
} | null> {
|
||||||
|
const shareId = route.query.id;
|
||||||
|
|
||||||
|
if (shareId == null || typeof shareId !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await getShare(shareId);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { data: result.share, file: result.file };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitPassword() {
|
||||||
|
loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFiles() {
|
||||||
|
if (loading.value || share == null || warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (share.file.fileType !== 'directory') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const result = await listShareFiles(
|
||||||
|
share.data.id,
|
||||||
|
warrenStore.current.path,
|
||||||
|
password.value.length > 0 ? password.value : null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
let cookie = `X-Share-Password=${password.value}; Path=/; SameSite=Lax; Secure;`;
|
||||||
|
|
||||||
|
if (share.data.expiresAt != null) {
|
||||||
|
const dayjs = useDayjs();
|
||||||
|
|
||||||
|
const diff = dayjs(share.data.expiresAt).diff(dayjs()) / 1000;
|
||||||
|
cookie += `Max-Age=${diff};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = cookie;
|
||||||
|
|
||||||
|
entries.value = result.files;
|
||||||
|
parent.value = result.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onEntryClicked(entry: DirectoryEntry) {
|
||||||
|
if (warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryPath = joinPaths(warrenStore.current.path, entry.name);
|
||||||
|
|
||||||
|
if (entry.fileType === 'directory') {
|
||||||
|
warrenStore.setCurrentWarrenPath(entryPath);
|
||||||
|
await loadFiles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType.startsWith('image/')) {
|
||||||
|
const result = await fetchShareFile(
|
||||||
|
share!.data.id,
|
||||||
|
entryPath,
|
||||||
|
password.value.length > 0 ? password.value : null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
const url = URL.createObjectURL(result.data);
|
||||||
|
warrenStore.imageViewer.src = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onBack() {
|
||||||
|
if (warrenStore.backCurrentPath()) {
|
||||||
|
await loadFiles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDowloadClicked() {
|
||||||
|
if (share == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(
|
||||||
|
share.file.name,
|
||||||
|
getApiUrl(`warrens/files/cat_share?shareId=${share.data.id}&path=/`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEntryDownload(entry: DirectoryEntry) {
|
||||||
|
if (share == null || warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(
|
||||||
|
entry.name,
|
||||||
|
getApiUrl(
|
||||||
|
`warrens/files/cat_share?shareId=${share.data.id}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="share != null"
|
||||||
|
class="flex h-full w-full items-center justify-center px-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'w-full rounded-lg border transition-all',
|
||||||
|
entries == null ? 'max-w-lg' : 'max-w-screen-xl',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-row items-center justify-between gap-4 px-6 pt-6"
|
||||||
|
>
|
||||||
|
<div class="flex w-full flex-row">
|
||||||
|
<div class="flex grow flex-col gap-1.5">
|
||||||
|
<h3 class="leading-none font-semibold">Share</h3>
|
||||||
|
<p class="text-muted-foreground text-sm">
|
||||||
|
Created
|
||||||
|
{{
|
||||||
|
$dayjs(share.data.createdAt).format(
|
||||||
|
'MMM D, YYYY HH:mm'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-center justify-end gap-4">
|
||||||
|
<p>{{ share.file.name }}</p>
|
||||||
|
<DirectoryEntryIcon
|
||||||
|
:entry="{ ...share.file, name: '/' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row items-end">
|
||||||
|
<Button
|
||||||
|
:class="
|
||||||
|
share.file.fileType !== 'file' &&
|
||||||
|
entries == null &&
|
||||||
|
'hidden'
|
||||||
|
"
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
@click="onDowloadClicked"
|
||||||
|
><Icon name="lucide:download"
|
||||||
|
/></Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex w-full flex-col p-6">
|
||||||
|
<DirectoryList
|
||||||
|
v-if="entries != null"
|
||||||
|
:entries
|
||||||
|
:parent
|
||||||
|
:disable-entries="loading"
|
||||||
|
@entry-click="onEntryClicked"
|
||||||
|
@entry-download="onEntryDownload"
|
||||||
|
@back="onBack"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else-if="share.data.password"
|
||||||
|
class="flex h-full flex-col justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<Label for="password">Password</Label>
|
||||||
|
<Input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
autocomplete="off"
|
||||||
|
data-1p-ignore
|
||||||
|
data-protonpass-ignore
|
||||||
|
data-bwignore
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row-reverse items-end">
|
||||||
|
<Button
|
||||||
|
:disabled="loading || password.length < 1"
|
||||||
|
@click="submitPassword"
|
||||||
|
>Enter</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="bg-accent/20 rounded-md p-4">
|
||||||
|
<p class="text-destructive-foreground">Failed to get share</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -3,7 +3,8 @@ 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 { getWarrenDirectory } from '~/lib/api/warrens';
|
import { fetchFile, getWarrenDirectory } from '~/lib/api/warrens';
|
||||||
|
import type { DirectoryEntry } from '~/shared/types';
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: ['authenticated'],
|
middleware: ['authenticated'],
|
||||||
@@ -31,7 +32,10 @@ const dirData = useAsyncData(
|
|||||||
'current-directory',
|
'current-directory',
|
||||||
async () => {
|
async () => {
|
||||||
if (warrenStore.current == null) {
|
if (warrenStore.current == null) {
|
||||||
return [];
|
return {
|
||||||
|
files: [],
|
||||||
|
parent: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingIndicator.start();
|
loadingIndicator.start();
|
||||||
@@ -74,6 +78,57 @@ function onDrop(files: File[] | null, e: DragEvent) {
|
|||||||
uploadStore.dialogOpen = true;
|
uploadStore.dialogOpen = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onEntryClicked(entry: DirectoryEntry) {
|
||||||
|
if (warrenStore.loading || warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.fileType === 'directory') {
|
||||||
|
warrenStore.addToCurrentWarrenPath(entry.name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.mimeType.startsWith('image/')) {
|
||||||
|
const result = await fetchFile(
|
||||||
|
warrenStore.current.warrenId,
|
||||||
|
warrenStore.current.path,
|
||||||
|
entry.name
|
||||||
|
);
|
||||||
|
if (result.success) {
|
||||||
|
const url = URL.createObjectURL(result.data);
|
||||||
|
warrenStore.imageViewer.src = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEntryDownload(entry: DirectoryEntry) {
|
||||||
|
if (warrenStore.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.fileType !== 'file') {
|
||||||
|
toast.warning('Download', {
|
||||||
|
description: 'Directory downloads are not supported yet',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(
|
||||||
|
entry.name,
|
||||||
|
getApiUrl(
|
||||||
|
`warrens/files/cat?warrenId=${warrenStore.current.warrenId}&path=${joinPaths(warrenStore.current.path, entry.name)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onBack() {
|
||||||
|
warrenStore.backCurrentPath();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -87,6 +142,9 @@ function onDrop(files: File[] | null, e: DragEvent) {
|
|||||||
"
|
"
|
||||||
:entries="dirData.files"
|
:entries="dirData.files"
|
||||||
:parent="dirData.parent"
|
:parent="dirData.parent"
|
||||||
|
@entry-click="onEntryClicked"
|
||||||
|
@entry-download="onEntryDownload"
|
||||||
|
@back="onBack"
|
||||||
/>
|
/>
|
||||||
</DirectoryListContextMenu>
|
</DirectoryListContextMenu>
|
||||||
<RenameEntryDialog />
|
<RenameEntryDialog />
|
||||||
|
|||||||
13
frontend/shared/types/shares.ts
Normal file
13
frontend/shared/types/shares.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export type Share = {
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
creatorId: string;
|
||||||
|
warrenId: string;
|
||||||
|
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
password: boolean;
|
||||||
|
|
||||||
|
expiresAt: number | null;
|
||||||
|
createdAt: number;
|
||||||
|
};
|
||||||
@@ -10,8 +10,14 @@ export type AdminWarrenData = WarrenData & {
|
|||||||
export type UserWarren = {
|
export type UserWarren = {
|
||||||
userId: string;
|
userId: string;
|
||||||
warrenId: string;
|
warrenId: string;
|
||||||
|
|
||||||
canListFiles: boolean;
|
canListFiles: boolean;
|
||||||
canReadFiles: boolean;
|
canReadFiles: boolean;
|
||||||
canModifyFiles: boolean;
|
canModifyFiles: boolean;
|
||||||
canDeleteFiles: boolean;
|
canDeleteFiles: boolean;
|
||||||
|
|
||||||
|
canListShares: boolean;
|
||||||
|
canCreateShares: boolean;
|
||||||
|
canModifyShares: boolean;
|
||||||
|
canDeleteShares: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,12 +30,14 @@ export const useWarrenStore = defineStore('warrens', {
|
|||||||
|
|
||||||
this.current.path += path;
|
this.current.path += path;
|
||||||
},
|
},
|
||||||
backCurrentPath() {
|
backCurrentPath(): boolean {
|
||||||
if (this.current == null || this.current.path === '/') {
|
if (this.current == null || this.current.path === '/') {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.current.path = getParentPath(this.current.path);
|
this.current.path = getParentPath(this.current.path);
|
||||||
|
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
setCurrentWarrenPath(path: string) {
|
setCurrentWarrenPath(path: string) {
|
||||||
if (this.current == null) {
|
if (this.current == null) {
|
||||||
@@ -89,3 +91,20 @@ export const useRenameDirectoryDialog = defineStore('rename_directory_dialog', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const useShareDialog = defineStore('share_dialog', {
|
||||||
|
state: () => ({
|
||||||
|
target: null as DirectoryEntry | null,
|
||||||
|
password: '',
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
openDialog(target: DirectoryEntry) {
|
||||||
|
this.target = target;
|
||||||
|
this.password = '';
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.target = null;
|
||||||
|
this.password = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { Share } from '~/shared/types/shares';
|
||||||
|
|
||||||
export function getApiUrl(path: string): string {
|
export function getApiUrl(path: string): string {
|
||||||
const API_BASE_URL = useRuntimeConfig().public.apiBase;
|
const API_BASE_URL = useRuntimeConfig().public.apiBase;
|
||||||
return `${API_BASE_URL}/${path}`;
|
return `${API_BASE_URL}/${path}`;
|
||||||
@@ -19,3 +21,7 @@ export function routeWithWarrenName(warrenId: string, path: string): string {
|
|||||||
|
|
||||||
return `${warrenName}${path}`;
|
return `${warrenName}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getShareLink(share: Share): string {
|
||||||
|
return `${window.location.origin}/share?id=${share.id}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,3 +75,14 @@ export async function pasteFile(
|
|||||||
|
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function downloadFile(fileName: string, href: string) {
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
|
||||||
|
anchor.href = href;
|
||||||
|
anchor.download = fileName;
|
||||||
|
anchor.rel = 'noopener';
|
||||||
|
anchor.target = '_blank';
|
||||||
|
|
||||||
|
anchor.click();
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,3 +27,13 @@ export function trim(str: string, char: string) {
|
|||||||
|
|
||||||
return start > 0 || end < str.length ? str.substring(start, end) : str;
|
return start > 0 || end < str.length ? str.substring(start, end) : str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copyToClipboard(content: string): boolean {
|
||||||
|
navigator.clipboard.writeText(content);
|
||||||
|
|
||||||
|
return navigator.clipboard != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function capitalize(s: string): string {
|
||||||
|
return s.slice(0, 1).toUpperCase() + s.slice(1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ export type UserWarrenPermissionKey =
|
|||||||
| 'canListFiles'
|
| 'canListFiles'
|
||||||
| 'canReadFiles'
|
| 'canReadFiles'
|
||||||
| 'canModifyFiles'
|
| 'canModifyFiles'
|
||||||
| 'canDeleteFiles';
|
| 'canDeleteFiles'
|
||||||
|
| 'canListShares'
|
||||||
|
| 'canListShares'
|
||||||
|
| 'canCreateShares'
|
||||||
|
| 'canModifyShares'
|
||||||
|
| 'canDeleteShares';
|
||||||
|
|
||||||
export function getUserWarrenPermissions(
|
export function getUserWarrenPermissions(
|
||||||
userWarren: UserWarren
|
userWarren: UserWarren
|
||||||
@@ -14,6 +19,10 @@ export function getUserWarrenPermissions(
|
|||||||
['canReadFiles', userWarren.canReadFiles],
|
['canReadFiles', userWarren.canReadFiles],
|
||||||
['canModifyFiles', userWarren.canModifyFiles],
|
['canModifyFiles', userWarren.canModifyFiles],
|
||||||
['canDeleteFiles', userWarren.canDeleteFiles],
|
['canDeleteFiles', userWarren.canDeleteFiles],
|
||||||
|
['canListShares', userWarren.canListShares],
|
||||||
|
['canCreateShares', userWarren.canCreateShares],
|
||||||
|
['canModifyShares', userWarren.canModifyShares],
|
||||||
|
['canDeleteShares', userWarren.canDeleteShares],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +31,10 @@ const PERMISSION_NAMES: Record<UserWarrenPermissionKey, string> = {
|
|||||||
canReadFiles: 'Read files',
|
canReadFiles: 'Read files',
|
||||||
canModifyFiles: 'Modify files',
|
canModifyFiles: 'Modify files',
|
||||||
canDeleteFiles: 'Delete files',
|
canDeleteFiles: 'Delete files',
|
||||||
|
canListShares: 'List shares',
|
||||||
|
canCreateShares: 'Create shares',
|
||||||
|
canModifyShares: 'Modify shares',
|
||||||
|
canDeleteShares: 'Delete shares',
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getUserWarrenPermissionName(
|
export function getUserWarrenPermissionName(
|
||||||
|
|||||||
Reference in New Issue
Block a user