basic file sharing

This commit is contained in:
2025-08-29 15:32:23 +02:00
parent c8b52a5b3b
commit 284d805590
84 changed files with 3969 additions and 375 deletions

View File

@@ -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
);

View 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;

View File

@@ -0,0 +1 @@
ALTER TABLE shares ALTER COLUMN password_hash DROP NOT NULL;

View File

@@ -0,0 +1 @@
CREATE INDEX idx_shares_path ON shares(path);

View File

@@ -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 {

View File

@@ -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)]

View File

@@ -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::*;

View 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
}
}

View 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;

View 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
}
}

View 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),
}

View 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),
}

View File

@@ -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),
}

View 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),
}

View 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),
}

View 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),
}

View 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::*;

View File

@@ -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),
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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))
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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(),
}
}
}

View File

@@ -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,
))
}
}

View File

@@ -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,
))
}
}

View File

@@ -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()))
}
}
}

View File

@@ -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(),
}
}
}

View 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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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)
}

View 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)
}

View 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)
}

View File

@@ -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))
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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?;

View File

@@ -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 {

View 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)
}
}

View File

@@ -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 {