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 {
|
||||
&self.user
|
||||
}
|
||||
|
||||
pub fn unpack(self) -> (AuthSession, User) {
|
||||
(self.session, self.user)
|
||||
}
|
||||
}
|
||||
|
||||
impl FetchAuthSessionRequest {
|
||||
|
||||
@@ -142,6 +142,10 @@ impl FilePath {
|
||||
self.0.push('/');
|
||||
self.0.push_str(&other.0);
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for FilePath {
|
||||
@@ -172,6 +176,10 @@ impl RelativeFilePath {
|
||||
self.0.push_str(&other.0);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AbsoluteFilePath {
|
||||
@@ -199,6 +207,10 @@ impl AbsoluteFilePath {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Error)]
|
||||
|
||||
@@ -5,6 +5,7 @@ mod mkdir;
|
||||
mod mv;
|
||||
mod rm;
|
||||
mod save;
|
||||
mod stat;
|
||||
mod touch;
|
||||
|
||||
pub use cat::*;
|
||||
@@ -14,4 +15,5 @@ pub use mkdir::*;
|
||||
pub use mv::*;
|
||||
pub use rm::*;
|
||||
pub use save::*;
|
||||
pub use stat::*;
|
||||
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 file;
|
||||
pub mod share;
|
||||
pub mod user;
|
||||
pub mod user_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_modify_files: bool,
|
||||
can_delete_files: bool,
|
||||
can_list_shares: bool,
|
||||
can_create_shares: bool,
|
||||
can_modify_shares: bool,
|
||||
can_delete_shares: bool,
|
||||
}
|
||||
|
||||
impl UserWarren {
|
||||
@@ -20,6 +24,10 @@ impl UserWarren {
|
||||
can_read_files: bool,
|
||||
can_modify_files: bool,
|
||||
can_delete_files: bool,
|
||||
can_list_shares: bool,
|
||||
can_create_shares: bool,
|
||||
can_modify_shares: bool,
|
||||
can_delete_shares: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_id,
|
||||
@@ -28,6 +36,10 @@ impl UserWarren {
|
||||
can_read_files,
|
||||
can_modify_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 {
|
||||
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};
|
||||
|
||||
pub trait HasWarrenId {
|
||||
fn warren_id(&self) -> &Uuid;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct FetchWarrenRequest {
|
||||
id: Uuid,
|
||||
@@ -71,10 +75,6 @@ impl WarrenLsRequest {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &LsRequest {
|
||||
&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 {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
@@ -142,10 +148,6 @@ impl WarrenMkdirRequest {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &MkdirRequest {
|
||||
&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 {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
@@ -207,10 +215,6 @@ impl WarrenRmRequest {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &RmRequest {
|
||||
&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 {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
@@ -272,10 +282,6 @@ impl<'s> WarrenSaveRequest<'s> {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &SaveRequest<'s> {
|
||||
&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<'_> {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
@@ -406,10 +418,6 @@ impl WarrenMvRequest {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &MvRequest {
|
||||
&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 {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
@@ -578,10 +592,6 @@ impl WarrenCatRequest {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &CatRequest {
|
||||
&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 {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
@@ -623,10 +639,6 @@ impl WarrenTouchRequest {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &TouchRequest {
|
||||
&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 {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
FetchWarrenRequest::new(self.warren_id)
|
||||
@@ -688,10 +706,6 @@ impl WarrenCpRequest {
|
||||
Self { warren_id, base }
|
||||
}
|
||||
|
||||
pub fn warren_id(&self) -> &Uuid {
|
||||
&self.warren_id
|
||||
}
|
||||
|
||||
pub fn base(&self) -> &CpRequest {
|
||||
&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 {
|
||||
fn into(self) -> FetchWarrenRequest {
|
||||
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_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 {
|
||||
@@ -68,6 +86,9 @@ pub trait FileSystemMetrics: Clone + Send + Sync + 'static {
|
||||
|
||||
fn record_cp_success(&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 {
|
||||
@@ -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_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::{
|
||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
||||
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::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest, EditUserError,
|
||||
@@ -106,6 +112,31 @@ pub trait WarrenService: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
request: WarrenCpRequest,
|
||||
) -> 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 {
|
||||
@@ -121,6 +152,10 @@ pub trait FileSystemService: Clone + Send + Sync + 'static {
|
||||
) -> impl Future<Output = Result<SaveResponse, SaveError>> + 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 stat(
|
||||
&self,
|
||||
request: StatRequest,
|
||||
) -> impl Future<Output = Result<StatResponse, StatError>> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
@@ -273,4 +308,20 @@ pub trait AuthService: Clone + Send + Sync + 'static {
|
||||
request: AuthRequest<WarrenCpRequest>,
|
||||
warren_service: &WS,
|
||||
) -> 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::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{AbsoluteFilePath, LsResponse},
|
||||
share::{
|
||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||
ShareCatResponse, ShareLsResponse,
|
||||
},
|
||||
user::{ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User},
|
||||
user_warren::UserWarren,
|
||||
warren::{
|
||||
@@ -44,6 +48,22 @@ pub trait WarrenNotifier: Clone + Send + Sync + 'static {
|
||||
path: &AbsoluteFilePath,
|
||||
) -> 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 {
|
||||
@@ -63,6 +83,7 @@ pub trait FileSystemNotifier: Clone + Send + Sync + 'static {
|
||||
path: &AbsoluteFilePath,
|
||||
target_path: &AbsoluteFilePath,
|
||||
) -> impl Future<Output = ()> + Send;
|
||||
fn stat(&self, path: &AbsoluteFilePath) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||
@@ -175,4 +196,22 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
|
||||
user: &User,
|
||||
response: &WarrenCpResponse,
|
||||
) -> 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::{
|
||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
||||
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::{
|
||||
CreateOrUpdateUserOidcError, CreateOrUpdateUserOidcRequest, CreateUserError,
|
||||
@@ -63,6 +69,27 @@ pub trait WarrenRepository: Clone + Send + Sync + 'static {
|
||||
&self,
|
||||
request: FetchWarrenRequest,
|
||||
) -> 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 {
|
||||
@@ -78,6 +105,10 @@ pub trait FileSystemRepository: Clone + Send + Sync + 'static {
|
||||
) -> impl Future<Output = Result<SaveResponse, SaveError>> + 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 stat(
|
||||
&self,
|
||||
request: StatRequest,
|
||||
) -> impl Future<Output = Result<StatResponse, StatError>> + Send;
|
||||
}
|
||||
|
||||
pub trait AuthRepository: Clone + Send + Sync + 'static {
|
||||
|
||||
@@ -12,6 +12,11 @@ use crate::{
|
||||
},
|
||||
},
|
||||
file::FileStream,
|
||||
share::{
|
||||
CreateShareBaseRequest, CreateShareError, CreateShareResponse,
|
||||
DeleteShareError, DeleteShareRequest, DeleteShareResponse, ListSharesError,
|
||||
ListSharesRequest, ListSharesResponse,
|
||||
},
|
||||
user::{
|
||||
CreateUserError, CreateUserRequest, DeleteUserError, DeleteUserRequest,
|
||||
EditUserError, EditUserRequest, GetOidcRedirectError, GetOidcRedirectRequest,
|
||||
@@ -32,12 +37,13 @@ use crate::{
|
||||
warren::{
|
||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
||||
FetchWarrensRequest, Warren, WarrenCatError, WarrenCatRequest, WarrenCpError,
|
||||
WarrenCpRequest, WarrenCpResponse, WarrenLsError, WarrenLsRequest,
|
||||
WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest, WarrenMkdirResponse,
|
||||
WarrenMvError, WarrenMvRequest, WarrenMvResponse, WarrenRmError,
|
||||
WarrenRmRequest, WarrenRmResponse, WarrenSaveError, WarrenSaveRequest,
|
||||
WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest, WarrenTouchResponse,
|
||||
FetchWarrensRequest, HasWarrenId, Warren, WarrenCatError, WarrenCatRequest,
|
||||
WarrenCpError, WarrenCpRequest, WarrenCpResponse, WarrenLsError,
|
||||
WarrenLsRequest, WarrenLsResponse, WarrenMkdirError, WarrenMkdirRequest,
|
||||
WarrenMkdirResponse, WarrenMvError, WarrenMvRequest, WarrenMvResponse,
|
||||
WarrenRmError, WarrenRmRequest, WarrenRmResponse, WarrenSaveError,
|
||||
WarrenSaveRequest, WarrenSaveResponse, WarrenTouchError, WarrenTouchRequest,
|
||||
WarrenTouchResponse,
|
||||
},
|
||||
},
|
||||
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
|
||||
@@ -644,17 +650,7 @@ where
|
||||
request: AuthRequest<WarrenLsRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenLsResponse, AuthError<WarrenLsError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
if !user_warren.can_list_files() {
|
||||
return Err(AuthError::InsufficientPermissions);
|
||||
@@ -667,9 +663,7 @@ where
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_ls_success().await;
|
||||
self.notifier
|
||||
.auth_warren_ls(session_response.user(), response)
|
||||
.await;
|
||||
self.notifier.auth_warren_ls(&user, response).await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_ls_failure().await;
|
||||
}
|
||||
@@ -682,23 +676,14 @@ where
|
||||
request: AuthRequest<WarrenCatRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<FileStream, AuthError<WarrenCatError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
if !user_warren.can_read_files() {
|
||||
return Err(AuthError::InsufficientPermissions);
|
||||
}
|
||||
|
||||
let path = request.base().path().clone();
|
||||
|
||||
let result = warren_service
|
||||
.warren_cat(request)
|
||||
.await
|
||||
@@ -707,7 +692,7 @@ where
|
||||
if let Ok(_stream) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_cat_success().await;
|
||||
self.notifier
|
||||
.auth_warren_cat(session_response.user(), user_warren.warren_id(), &path)
|
||||
.auth_warren_cat(&user, user_warren.warren_id(), &path)
|
||||
.await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_cat_failure().await;
|
||||
@@ -721,17 +706,7 @@ where
|
||||
request: AuthRequest<WarrenMkdirRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenMkdirResponse, AuthError<WarrenMkdirError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
// TODO: Maybe create a separate permission for this
|
||||
if !user_warren.can_modify_files() {
|
||||
@@ -745,9 +720,7 @@ where
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_mkdir_success().await;
|
||||
self.notifier
|
||||
.auth_warren_mkdir(session_response.user(), response)
|
||||
.await;
|
||||
self.notifier.auth_warren_mkdir(&user, response).await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_mkdir_failure().await;
|
||||
}
|
||||
@@ -760,17 +733,7 @@ where
|
||||
request: AuthRequest<WarrenRmRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenRmResponse, AuthError<WarrenRmError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
if !user_warren.can_delete_files() {
|
||||
return Err(AuthError::InsufficientPermissions);
|
||||
@@ -783,9 +746,7 @@ where
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_rm_success().await;
|
||||
self.notifier
|
||||
.auth_warren_rm(session_response.user(), response)
|
||||
.await;
|
||||
self.notifier.auth_warren_rm(&user, response).await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_rm_failure().await;
|
||||
}
|
||||
@@ -798,17 +759,7 @@ where
|
||||
request: AuthRequest<WarrenMvRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenMvResponse, AuthError<WarrenMvError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
if !user_warren.can_modify_files() {
|
||||
return Err(AuthError::InsufficientPermissions);
|
||||
@@ -821,9 +772,7 @@ where
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_mv_success().await;
|
||||
self.notifier
|
||||
.auth_warren_mv(session_response.user(), response)
|
||||
.await;
|
||||
self.notifier.auth_warren_mv(&user, response).await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_mv_failure().await;
|
||||
}
|
||||
@@ -836,17 +785,7 @@ where
|
||||
request: AuthRequest<WarrenSaveRequest<'_>>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenSaveResponse, AuthError<WarrenSaveError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
// TODO: Maybe create a separate permission for this
|
||||
if !user_warren.can_modify_files() {
|
||||
@@ -860,9 +799,7 @@ where
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_save_success().await;
|
||||
self.notifier
|
||||
.auth_warren_save(session_response.user(), response)
|
||||
.await;
|
||||
self.notifier.auth_warren_save(&user, response).await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_save_failure().await;
|
||||
}
|
||||
@@ -875,17 +812,7 @@ where
|
||||
request: AuthRequest<WarrenTouchRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenTouchResponse, AuthError<WarrenTouchError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
// TODO: Maybe create a separate permission for this
|
||||
if !user_warren.can_modify_files() {
|
||||
@@ -899,9 +826,7 @@ where
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_touch_success().await;
|
||||
self.notifier
|
||||
.auth_warren_touch(session_response.user(), response)
|
||||
.await;
|
||||
self.notifier.auth_warren_touch(&user, response).await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_touch_failure().await;
|
||||
}
|
||||
@@ -914,17 +839,7 @@ where
|
||||
request: AuthRequest<WarrenCpRequest>,
|
||||
warren_service: &WS,
|
||||
) -> Result<WarrenCpResponse, AuthError<WarrenCpError>> {
|
||||
let session_response = self.fetch_auth_session((&request).into()).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?;
|
||||
let (request, user, user_warren) = self.get_session_data_and_user_warren(request).await?;
|
||||
|
||||
// TODO: Maybe create a separate permission for this
|
||||
if !user_warren.can_modify_files() {
|
||||
@@ -938,13 +853,128 @@ where
|
||||
|
||||
if let Ok(response) = result.as_ref() {
|
||||
self.metrics.record_auth_warren_cp_success().await;
|
||||
self.notifier
|
||||
.auth_warren_cp(session_response.user(), response)
|
||||
.await;
|
||||
self.notifier.auth_warren_cp(&user, response).await;
|
||||
} else {
|
||||
self.metrics.record_auth_warren_cp_failure().await;
|
||||
}
|
||||
|
||||
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::{
|
||||
CatError, CatRequest, CpError, CpRequest, CpResponse, FileStream, LsError, LsRequest,
|
||||
LsResponse, MkdirError, MkdirRequest, MvError, MvRequest, RmError, RmRequest, SaveError,
|
||||
SaveRequest, SaveResponse, TouchError, TouchRequest,
|
||||
SaveRequest, SaveResponse, StatError, StatRequest, StatResponse, TouchError, TouchRequest,
|
||||
},
|
||||
ports::{FileSystemMetrics, FileSystemNotifier, FileSystemRepository, FileSystemService},
|
||||
};
|
||||
@@ -152,4 +152,18 @@ where
|
||||
|
||||
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::{
|
||||
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::{
|
||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||
EditWarrenError, EditWarrenRequest, FetchWarrensError, FetchWarrensRequest,
|
||||
@@ -335,4 +342,199 @@ where
|
||||
|
||||
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::{
|
||||
domain::warren::models::{
|
||||
auth_session::{AuthError, requests::FetchAuthSessionError},
|
||||
file::{LsError, MkdirError, RmError},
|
||||
file::{CatError, LsError, MkdirError, RmError, StatError},
|
||||
share::{GetShareError, ShareCatError, ShareLsError, VerifySharePasswordError},
|
||||
user::{
|
||||
CreateUserError, GetOidcRedirectError, LoginUserError, LoginUserOidcError,
|
||||
RegisterUserError, VerifyUserPasswordError,
|
||||
},
|
||||
user_warren::requests::FetchUserWarrenError,
|
||||
warren::{
|
||||
FetchWarrenError, FetchWarrensError, WarrenLsError, WarrenMkdirError, WarrenMvError,
|
||||
WarrenRmError, WarrenSaveError,
|
||||
FetchWarrenError, FetchWarrensError, WarrenCatError, WarrenLsError, WarrenMkdirError,
|
||||
WarrenMvError, WarrenRmError, WarrenSaveError,
|
||||
},
|
||||
},
|
||||
inbound::http::responses::ApiError,
|
||||
@@ -191,7 +192,7 @@ impl<T: std::error::Error> From<AuthError<T>> for ApiError {
|
||||
impl From<CreateUserError> for ApiError {
|
||||
fn from(value: CreateUserError) -> Self {
|
||||
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::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_modify_files: bool,
|
||||
can_delete_files: bool,
|
||||
can_list_shares: bool,
|
||||
can_create_shares: bool,
|
||||
can_modify_shares: bool,
|
||||
can_delete_shares: bool,
|
||||
}
|
||||
|
||||
impl CreateUserWarrenHttpRequestBody {
|
||||
@@ -37,6 +41,10 @@ impl CreateUserWarrenHttpRequestBody {
|
||||
self.can_read_files,
|
||||
self.can_modify_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_modify_files: bool,
|
||||
can_delete_files: bool,
|
||||
can_list_shares: bool,
|
||||
can_create_shares: bool,
|
||||
can_modify_shares: bool,
|
||||
can_delete_shares: bool,
|
||||
}
|
||||
|
||||
impl EditUserWarrenHttpRequestBody {
|
||||
@@ -37,6 +41,10 @@ impl EditUserWarrenHttpRequestBody {
|
||||
self.can_read_files,
|
||||
self.can_modify_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 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);
|
||||
@@ -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 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 auth;
|
||||
@@ -39,6 +41,10 @@ pub(super) struct UserWarrenData {
|
||||
can_read_files: bool,
|
||||
can_modify_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 {
|
||||
@@ -50,23 +56,10 @@ impl From<UserWarren> for UserWarrenData {
|
||||
can_read_files: value.can_read_files(),
|
||||
can_modify_files: value.can_modify_files(),
|
||||
can_delete_files: value.can_delete_files(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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(),
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 get_share;
|
||||
mod list_shares;
|
||||
mod list_warrens;
|
||||
mod ls_share;
|
||||
mod upload_warren_files;
|
||||
mod warren_cat;
|
||||
mod warren_cp;
|
||||
@@ -14,23 +20,53 @@ use axum::{
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
domain::warren::ports::{AuthService, WarrenService},
|
||||
inbound::http::AppState,
|
||||
};
|
||||
|
||||
use fetch_warren::fetch_warren;
|
||||
use list_warrens::list_warrens;
|
||||
use serde::Serialize;
|
||||
use warren_ls::list_warren_files;
|
||||
|
||||
use warren_mkdir::create_warren_directory;
|
||||
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 warren_cat::fetch_file;
|
||||
use warren_cp::warren_cp;
|
||||
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>> {
|
||||
Router::new()
|
||||
.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/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,
|
||||
extract::{Query, State},
|
||||
};
|
||||
use base64::Engine as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
use tokio_util::io::ReaderStream;
|
||||
use uuid::Uuid;
|
||||
@@ -20,21 +19,6 @@ use crate::{
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(super) struct WarrenCatHttpRequestBody {
|
||||
|
||||
@@ -7,10 +7,7 @@ use crate::{
|
||||
domain::warren::{
|
||||
models::{
|
||||
auth_session::AuthRequest,
|
||||
file::{
|
||||
AbsoluteFilePathError, File, FileMimeType, FilePath, FilePathError, FileType,
|
||||
LsRequest,
|
||||
},
|
||||
file::{AbsoluteFilePathError, FilePath, FilePathError, LsRequest},
|
||||
warren::{WarrenLsRequest, WarrenLsResponse},
|
||||
},
|
||||
ports::{AuthService, WarrenService},
|
||||
@@ -22,6 +19,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::WarrenFileElement;
|
||||
|
||||
#[derive(Debug, Clone, Error)]
|
||||
pub enum ParseWarrenLsHttpRequestError {
|
||||
#[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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ListWarrenFilesResponseData {
|
||||
@@ -81,17 +71,6 @@ pub struct ListWarrenFilesResponseData {
|
||||
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 {
|
||||
fn from(value: WarrenLsResponse) -> 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 futures_util::TryStreamExt;
|
||||
use rustix::fs::statx;
|
||||
use rustix::fs::{Statx, statx};
|
||||
use tokio::{
|
||||
fs,
|
||||
io::{self, AsyncWriteExt as _},
|
||||
@@ -17,7 +17,8 @@ use crate::{
|
||||
AbsoluteFilePath, CatError, CatRequest, CpError, CpRequest, CpResponse, File,
|
||||
FileMimeType, FileName, FilePath, FileStream, FileType, LsError, LsRequest,
|
||||
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,
|
||||
},
|
||||
@@ -242,7 +243,12 @@ impl FileSystem {
|
||||
async fn touch(&self, path: &AbsoluteFilePath) -> io::Result<()> {
|
||||
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(
|
||||
@@ -257,6 +263,46 @@ impl FileSystem {
|
||||
|
||||
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 {
|
||||
@@ -333,11 +379,25 @@ impl FileSystemRepository for FileSystem {
|
||||
.await
|
||||
.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
|
||||
// https://github.com/rust-lang/rust/pull/142682
|
||||
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
|
||||
P: rustix::path::Arg,
|
||||
{
|
||||
@@ -349,6 +409,4 @@ where
|
||||
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) {
|
||||
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 {
|
||||
@@ -168,6 +209,13 @@ impl FileSystemMetrics for MetricsDebugLogger {
|
||||
async fn record_cp_failure(&self) {
|
||||
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 {
|
||||
@@ -359,6 +407,27 @@ impl AuthMetrics for MetricsDebugLogger {
|
||||
async fn record_auth_warren_cp_failure(&self) {
|
||||
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 {
|
||||
|
||||
@@ -9,6 +9,10 @@ use crate::domain::{
|
||||
models::{
|
||||
auth_session::requests::FetchAuthSessionResponse,
|
||||
file::{AbsoluteFilePath, LsResponse},
|
||||
share::{
|
||||
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
|
||||
ShareCatResponse, ShareLsResponse,
|
||||
},
|
||||
user::{
|
||||
ListAllUsersAndWarrensResponse, LoginUserOidcResponse, LoginUserResponse, User,
|
||||
},
|
||||
@@ -115,6 +119,57 @@ impl WarrenNotifier for NotifierDebugLogger {
|
||||
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 {
|
||||
@@ -149,6 +204,10 @@ impl FileSystemNotifier for NotifierDebugLogger {
|
||||
async fn cp(&self, path: &AbsoluteFilePath, target_path: &AbsoluteFilePath) {
|
||||
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 {
|
||||
@@ -368,6 +427,35 @@ impl AuthNotifier for NotifierDebugLogger {
|
||||
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 {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, anyhow, bail};
|
||||
use argon2::{
|
||||
Argon2, PasswordHash, PasswordVerifier as _,
|
||||
@@ -9,8 +7,7 @@ use argon2::{
|
||||
},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use sqlx::{Acquire as _, PgConnection, PgPool};
|
||||
use tokio::task::JoinHandle;
|
||||
use sqlx::{Acquire as _, PgConnection};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::domain::warren::{
|
||||
@@ -372,27 +369,7 @@ impl AuthRepository for Postgres {
|
||||
}
|
||||
|
||||
impl Postgres {
|
||||
pub(super) fn start_session_cleanup_task(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)");
|
||||
}
|
||||
}
|
||||
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
|
||||
tracing::debug!("Session cleanup task stopped");
|
||||
})
|
||||
}
|
||||
|
||||
async fn delete_expired_auth_sessions(
|
||||
pub(super) async fn delete_expired_auth_sessions(
|
||||
connection: &mut PgConnection,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
let delete_count = sqlx::query(
|
||||
@@ -916,7 +893,11 @@ impl Postgres {
|
||||
can_list_files = $3,
|
||||
can_read_files = $4,
|
||||
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
|
||||
user_id = $1 AND
|
||||
warren_id = $2
|
||||
@@ -930,6 +911,10 @@ impl Postgres {
|
||||
.bind(user_warren.can_read_files())
|
||||
.bind(user_warren.can_modify_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)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ use sqlx::{
|
||||
ConnectOptions as _, Connection as _, PgConnection, PgPool,
|
||||
postgres::{PgConnectOptions, PgPoolOptions},
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
pub mod auth;
|
||||
pub mod share;
|
||||
pub mod warrens;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -58,10 +60,34 @@ impl Postgres {
|
||||
sqlx::migrate!("./migrations").run(&pool).await?;
|
||||
|
||||
// 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 })
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
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::{
|
||||
models::{
|
||||
file::AbsoluteFilePath,
|
||||
share::{
|
||||
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
|
||||
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
|
||||
ListSharesError, ListSharesRequest, ListSharesResponse, Share,
|
||||
VerifySharePasswordError, VerifySharePasswordRequest, VerifySharePasswordResponse,
|
||||
},
|
||||
warren::{
|
||||
CreateWarrenError, CreateWarrenRequest, DeleteWarrenError, DeleteWarrenRequest,
|
||||
EditWarrenError, EditWarrenRequest, FetchWarrenError, FetchWarrenRequest,
|
||||
@@ -132,6 +138,86 @@ impl WarrenRepository for Postgres {
|
||||
|
||||
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 {
|
||||
|
||||
@@ -11,12 +11,14 @@
|
||||
"@nuxt/test-utils": "3.19.2",
|
||||
"@pinia/nuxt": "^0.11.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@vee-validate/yup": "^4.15.1",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/core": "^13.7.0",
|
||||
"byte-size": "^9.0.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs-nuxt": "2.1.11",
|
||||
"eslint": "^9.0.0",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
@@ -813,6 +819,10 @@
|
||||
|
||||
"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=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
import type { DirectoryEntry } from '~/shared/types';
|
||||
|
||||
const { entry } = defineProps<{ entry: DirectoryEntry }>();
|
||||
|
||||
const warrenStore = useWarrenStore();
|
||||
const emit = defineEmits<{
|
||||
back: [];
|
||||
}>();
|
||||
|
||||
const onDrop = onDirectoryEntryDrop(entry, true);
|
||||
</script>
|
||||
@@ -12,7 +13,7 @@ const onDrop = onDirectoryEntryDrop(entry, true);
|
||||
<button
|
||||
class="bg-accent/30 border-border flex w-52 translate-0 flex-row gap-4 overflow-hidden rounded-md border-1 px-4 py-2 select-none"
|
||||
@contextmenu.prevent
|
||||
@click="() => warrenStore.backCurrentPath()"
|
||||
@click="() => emit('back')"
|
||||
@drop="onDrop"
|
||||
>
|
||||
<div class="flex flex-row items-center">
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
ContextMenuItem,
|
||||
ContextMenuSeparator,
|
||||
} from '@/components/ui/context-menu';
|
||||
import {
|
||||
deleteWarrenDirectory,
|
||||
deleteWarrenFile,
|
||||
fetchFile,
|
||||
} from '~/lib/api/warrens';
|
||||
import { deleteWarrenDirectory, deleteWarrenFile } from '~/lib/api/warrens';
|
||||
import type { DirectoryEntry } from '#shared/types';
|
||||
import { toast } from 'vue-sonner';
|
||||
|
||||
@@ -23,6 +19,11 @@ const { entry, disabled } = defineProps<{
|
||||
disabled: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'entry-click': [entry: DirectoryEntry];
|
||||
'entry-download': [entry: DirectoryEntry];
|
||||
}>();
|
||||
|
||||
const deleting = ref(false);
|
||||
const isCopied = computed(
|
||||
() =>
|
||||
@@ -63,30 +64,7 @@ async function openRenameDialog() {
|
||||
}
|
||||
|
||||
async function onClick() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
emit('entry-click', entry);
|
||||
}
|
||||
|
||||
function onDragStart(e: DragEvent) {
|
||||
@@ -112,38 +90,22 @@ function onCopy() {
|
||||
);
|
||||
}
|
||||
|
||||
async function onDownload() {
|
||||
if (warrenStore.current == null) {
|
||||
return;
|
||||
}
|
||||
function onShare() {
|
||||
useShareDialog().openDialog(entry);
|
||||
}
|
||||
|
||||
if (entry.fileType !== 'file') {
|
||||
toast.warning('Download', {
|
||||
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();
|
||||
function onDownload() {
|
||||
emit('entry-download', entry);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<ContextMenuTrigger class="flex sm:w-52">
|
||||
<button
|
||||
:disabled="warrenStore.loading || disabled"
|
||||
: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',
|
||||
]"
|
||||
draggable="true"
|
||||
@@ -151,38 +113,7 @@ async function onDownload() {
|
||||
@drop="onDrop"
|
||||
@click="onClick"
|
||||
>
|
||||
<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
|
||||
: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>
|
||||
<DirectoryEntryIcon :entry />
|
||||
|
||||
<div
|
||||
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>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem @select="openRenameDialog">
|
||||
<ContextMenuItem
|
||||
:class="[warrenStore.current == null && 'hidden']"
|
||||
@select="openRenameDialog"
|
||||
>
|
||||
<Icon name="lucide:pencil" />
|
||||
Rename
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @select="onCopy">
|
||||
<ContextMenuItem
|
||||
:class="[warrenStore.current == null && 'hidden']"
|
||||
@select="onCopy"
|
||||
>
|
||||
<Icon name="lucide:copy" />
|
||||
Copy
|
||||
</ContextMenuItem>
|
||||
@@ -213,15 +150,28 @@ async function onDownload() {
|
||||
<Icon name="lucide:download" />
|
||||
Download
|
||||
</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" />
|
||||
Delete
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
v-if="entry.fileType === 'directory'"
|
||||
:class="[warrenStore.current == null && 'hidden']"
|
||||
@select="() => submitDelete(true)"
|
||||
>
|
||||
<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 type { DirectoryEntry } from '#shared/types';
|
||||
|
||||
const { entries, parent, isOverDropZone } = defineProps<{
|
||||
const {
|
||||
entries,
|
||||
parent,
|
||||
isOverDropZone,
|
||||
disableEntries = false,
|
||||
} = defineProps<{
|
||||
entries: DirectoryEntry[];
|
||||
parent: DirectoryEntry | null;
|
||||
isOverDropZone?: boolean;
|
||||
disableEntries?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'entry-click': [entry: DirectoryEntry];
|
||||
'entry-download': [entry: DirectoryEntry];
|
||||
back: [];
|
||||
}>();
|
||||
|
||||
const { isLoading } = useLoadingIndicator();
|
||||
@@ -13,23 +25,39 @@ const { isLoading } = useLoadingIndicator();
|
||||
const sortedEntries = computed(() =>
|
||||
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>
|
||||
|
||||
<template>
|
||||
<ScrollArea class="h-full w-full">
|
||||
<ScrollArea class="flex h-full w-full flex-col overflow-hidden">
|
||||
<div
|
||||
v-if="isOverDropZone"
|
||||
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" />
|
||||
</div>
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
<DirectoryBackEntry v-if="parent != null" :entry="parent" />
|
||||
<div
|
||||
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
|
||||
v-for="entry in sortedEntries"
|
||||
:key="entry.name"
|
||||
:entry="entry"
|
||||
:disabled="isLoading"
|
||||
:disabled="isLoading || disableEntries"
|
||||
@entry-click="onEntryClicked"
|
||||
@entry-download="onEntryDownload"
|
||||
/>
|
||||
</div>
|
||||
</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<{
|
||||
userWarren: UserWarren;
|
||||
}>();
|
||||
let realUserWarrenState: UserWarren = JSON.parse(
|
||||
JSON.stringify(props.userWarren)
|
||||
);
|
||||
const userWarren = props.userWarren;
|
||||
|
||||
const adminStore = useAdminStore();
|
||||
|
||||
const updatePermissionsDebounced = useDebounceFn(
|
||||
async (userWarren: UserWarren) => {
|
||||
const result = await editUserWarren(userWarren);
|
||||
async (uw: UserWarren) => {
|
||||
const result = await editUserWarren(uw);
|
||||
|
||||
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', {
|
||||
description: `Successfully updated the user's permissions`,
|
||||
});
|
||||
} 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', {
|
||||
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;
|
||||
|
||||
let 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);
|
||||
const cookie = `authorization=WarrenAuth ${value.id}; path=/; SameSite=Lax; Secure;`;
|
||||
|
||||
document.cookie = cookie;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ await useAsyncData('warrens', async () => {
|
||||
|
||||
<template>
|
||||
<SidebarProvider>
|
||||
<ActionsShareDialog />
|
||||
<ImageViewer />
|
||||
<AppSidebar />
|
||||
<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',
|
||||
'@nuxtjs/color-mode',
|
||||
'@pinia/nuxt',
|
||||
'dayjs-nuxt',
|
||||
],
|
||||
|
||||
dayjs: {
|
||||
plugins: ['duration', 'relativeTime'],
|
||||
},
|
||||
|
||||
css: ['~/assets/css/tailwind.css', '~/assets/css/sonner.css'],
|
||||
|
||||
vite: {
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
"@nuxt/test-utils": "3.19.2",
|
||||
"@pinia/nuxt": "^0.11.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@vee-validate/yup": "^4.15.1",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/core": "^13.7.0",
|
||||
"byte-size": "^9.0.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs-nuxt": "2.1.11",
|
||||
"eslint": "^9.0.0",
|
||||
"lucide-vue-next": "^0.525.0",
|
||||
"nuxt": "^3.17.6",
|
||||
|
||||
@@ -33,7 +33,6 @@ if (
|
||||
route.query.state &&
|
||||
typeof route.query.state === 'string'
|
||||
) {
|
||||
console.log('SEND');
|
||||
loggingIn.value = true;
|
||||
const { success } = await oidcLoginUser(
|
||||
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 DirectoryListContextMenu from '~/components/DirectoryListContextMenu.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({
|
||||
middleware: ['authenticated'],
|
||||
@@ -31,7 +32,10 @@ const dirData = useAsyncData(
|
||||
'current-directory',
|
||||
async () => {
|
||||
if (warrenStore.current == null) {
|
||||
return [];
|
||||
return {
|
||||
files: [],
|
||||
parent: null,
|
||||
};
|
||||
}
|
||||
|
||||
loadingIndicator.start();
|
||||
@@ -74,6 +78,57 @@ function onDrop(files: File[] | null, e: DragEvent) {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -87,6 +142,9 @@ function onDrop(files: File[] | null, e: DragEvent) {
|
||||
"
|
||||
:entries="dirData.files"
|
||||
:parent="dirData.parent"
|
||||
@entry-click="onEntryClicked"
|
||||
@entry-download="onEntryDownload"
|
||||
@back="onBack"
|
||||
/>
|
||||
</DirectoryListContextMenu>
|
||||
<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 = {
|
||||
userId: string;
|
||||
warrenId: string;
|
||||
|
||||
canListFiles: boolean;
|
||||
canReadFiles: boolean;
|
||||
canModifyFiles: boolean;
|
||||
canDeleteFiles: boolean;
|
||||
|
||||
canListShares: boolean;
|
||||
canCreateShares: boolean;
|
||||
canModifyShares: boolean;
|
||||
canDeleteShares: boolean;
|
||||
};
|
||||
|
||||
@@ -30,12 +30,14 @@ export const useWarrenStore = defineStore('warrens', {
|
||||
|
||||
this.current.path += path;
|
||||
},
|
||||
backCurrentPath() {
|
||||
backCurrentPath(): boolean {
|
||||
if (this.current == null || this.current.path === '/') {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.current.path = getParentPath(this.current.path);
|
||||
|
||||
return true;
|
||||
},
|
||||
setCurrentWarrenPath(path: string) {
|
||||
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 {
|
||||
const API_BASE_URL = useRuntimeConfig().public.apiBase;
|
||||
return `${API_BASE_URL}/${path}`;
|
||||
@@ -19,3 +21,7 @@ export function routeWithWarrenName(warrenId: string, path: string): string {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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'
|
||||
| 'canReadFiles'
|
||||
| 'canModifyFiles'
|
||||
| 'canDeleteFiles';
|
||||
| 'canDeleteFiles'
|
||||
| 'canListShares'
|
||||
| 'canListShares'
|
||||
| 'canCreateShares'
|
||||
| 'canModifyShares'
|
||||
| 'canDeleteShares';
|
||||
|
||||
export function getUserWarrenPermissions(
|
||||
userWarren: UserWarren
|
||||
@@ -14,6 +19,10 @@ export function getUserWarrenPermissions(
|
||||
['canReadFiles', userWarren.canReadFiles],
|
||||
['canModifyFiles', userWarren.canModifyFiles],
|
||||
['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',
|
||||
canModifyFiles: 'Modify files',
|
||||
canDeleteFiles: 'Delete files',
|
||||
canListShares: 'List shares',
|
||||
canCreateShares: 'Create shares',
|
||||
canModifyShares: 'Modify shares',
|
||||
canDeleteShares: 'Delete shares',
|
||||
};
|
||||
|
||||
export function getUserWarrenPermissionName(
|
||||
|
||||
Reference in New Issue
Block a user