Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 39 additions & 1 deletion backend/api/db_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

use sqlx::PgTransaction;

use crate::{crypto::hash_without_salt, db::TxResult, id::Token};
use crate::{
api,
crypto::hash_without_salt,
db::{TxError, TxResult},
id::Token,
};

/// Creates a new user session and returns its token.
///
Expand Down Expand Up @@ -31,3 +36,36 @@ where

Ok(token)
}

/// `SELECT`s a folder's ID and name paths `FOR UPDATE` if the user has permission to modify the
/// folder's contents.
///
/// # Errors
///
/// Returns an error if a database query fails, or if the user doesn't have access to the folder.
pub(crate) async fn query_folder_paths_for_update(
tx: &mut PgTransaction<'static>,
user_id: &[u8],
folder_id: &[u8],
) -> TxResult<(Vec<Vec<u8>>, Vec<String>), api::Error> {
let Some(folder) = sqlx::query!(
"SELECT name, parent_id_path, parent_name_path FROM folders
WHERE owner_id = $1 AND id = $2
FOR UPDATE",
user_id,
folder_id,
)
.fetch_optional(tx.as_mut())
.await?
else {
return Err(TxError::Abort(api::Error::AccessDenied));
};

let mut id_path = folder.parent_id_path;
id_path.push(folder_id.to_vec());

let mut name_path = folder.parent_name_path;
name_path.push(folder.name);

Ok((id_path, name_path))
}
4 changes: 4 additions & 0 deletions backend/api/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ pub(super) static ROUTER: LazyLock<Router> = LazyLock::new(|| {
"/folders/{folder_id}/name",
put(v0::folders::folder::name::put),
)
.route(
"/folders/{folder_id}/move",
post(v0::folders::folder::r#move::post),
)
.route(
"/folders/{folder_id}/share",
delete(v0::folders::folder::share::delete).post(v0::folders::folder::share::post),
Expand Down
35 changes: 2 additions & 33 deletions backend/api/routes/v0/files/file/move.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
use axum_macros::debug_handler;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use sqlx::PgTransaction;

use crate::{
api::{
self, Json,
db_helpers::query_folder_paths_for_update,
extract::{AuthToken, Path},
response::Response,
},
Expand Down Expand Up @@ -51,7 +51,7 @@ pub(crate) async fn post(

let (new_parent_id_path, new_parent_name_path) = match &body.parent_folder_id {
Some(parent_folder_id) => {
query_folder_paths(tx, &session.user_id, parent_folder_id).await?
query_folder_paths_for_update(tx, &session.user_id, parent_folder_id).await?
}
None => (vec![], vec![]),
};
Expand Down Expand Up @@ -118,34 +118,3 @@ pub(crate) async fn post(
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PostResponse {}

/// Obtains a folder's ID and name paths.
///
/// # Errors
///
/// Returns an error if a database query fails, or if the user doesn't have access to the folder.
async fn query_folder_paths(
tx: &mut PgTransaction<'static>,
user_id: &[u8],
folder_id: &[u8],
) -> TxResult<(Vec<Vec<u8>>, Vec<String>), api::Error> {
let Some(folder) = sqlx::query!(
"SELECT parent_id_path, parent_name_path, name FROM folders
WHERE owner_id = $1 AND id = $2",
user_id,
folder_id,
)
.fetch_optional(tx.as_mut())
.await?
else {
return Err(TxError::Abort(api::Error::AccessDenied));
};

let mut id_path = folder.parent_id_path;
id_path.push(folder_id.to_vec());

let mut name_path = folder.parent_name_path;
name_path.push(folder.name);

Ok((id_path, name_path))
}
1 change: 1 addition & 0 deletions backend/api/routes/v0/folders/folder.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! A folder.

pub(crate) mod r#move;
pub(crate) mod name;
pub(crate) mod share;
158 changes: 158 additions & 0 deletions backend/api/routes/v0/folders/folder/move.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
//! See [`post`].

use axum_macros::debug_handler;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};

use crate::{
api::{
self, Json,
db_helpers::query_folder_paths_for_update,
extract::{AuthToken, Path},
response::Response,
},
db::{self, TxError, TxResult},
id::Id,
};

/// A request path for this API route.
type PathParams = Path<Id>;

/// A `POST` request body for this API route.
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub(crate) struct PostRequest {
/// The new parent folder's ID, or [`None`] for the root directory.
pub parent_folder_id: Option<Id>,
}

/// Changes a folder's parent folder.
///
/// # Errors
///
/// See [`crate::api::Error`].
#[debug_handler]
pub(crate) async fn post(
Path(folder_id): PathParams,
AuthToken(token_hash): AuthToken,
Json(body): Json<PostRequest>,
) -> impl Response<PostResponse> {
db::transaction!(async |tx| -> TxResult<_, api::Error> {
let Some(session) = sqlx::query!(
"SELECT user_id FROM sessions
WHERE token_hash = $1",
token_hash.as_ref(),
)
.fetch_optional(tx.as_mut())
.await?
else {
return Err(TxError::Abort(api::Error::AuthFailed));
};

let (new_parent_id_path, new_parent_name_path) = match &body.parent_folder_id {
Some(parent_folder_id) => {
query_folder_paths_for_update(tx, &session.user_id, parent_folder_id).await?
}
None => (vec![], vec![]),
};

let folder = match sqlx::query!(
"UPDATE folders
SET parent_id_path = $1,
parent_name_path = $2
WHERE owner_id = $3 AND id = $4
RETURNING
name,
size,
OLD.parent_id_path AS old_parent_id_path",
new_parent_id_path.as_slice(),
new_parent_name_path.as_slice(),
session.user_id,
folder_id.as_slice(),
)
.fetch_optional(tx.as_mut())
.await
{
Err(sqlx::Error::Database(error))
if error.constraint() == Some("folders_by_name_path") =>
{
return Err(TxError::Abort(api::Error::AlreadyExists));
}

Err(error) => return Err(error.into()),

Ok(None) => return Err(TxError::Abort(api::Error::AccessDenied)),

Ok(Some(folder)) => folder,
};

if !folder.old_parent_id_path.is_empty() {
sqlx::query!(
"UPDATE folders
SET size = size - $1
WHERE id = ANY($2)",
folder.size,
folder.old_parent_id_path.as_slice(),
)
.execute(tx.as_mut())
.await?;
}

if !new_parent_id_path.is_empty() {
sqlx::query!(
"UPDATE folders
SET size = size + $1
WHERE id = ANY($2)",
folder.size,
new_parent_id_path.as_slice(),
)
.execute(tx.as_mut())
.await?;
}

let mut old_folder_id_path = folder.old_parent_id_path;
old_folder_id_path.push(folder_id.to_vec());

let mut new_folder_id_path = new_parent_id_path;
new_folder_id_path.push(folder_id.to_vec());

let mut new_folder_name_path = new_parent_name_path;
new_folder_name_path.push(folder.name);

sqlx::query!(
"UPDATE folders
SET parent_id_path = $1 || parent_id_path[array_length($2::bytea[], 1) + 1:],
parent_name_path = $3 || parent_name_path[array_length($2::text[], 1) + 1:]
WHERE owner_id = $4 AND parent_id_path >= $2 AND parent_id_path < $2 || NULL",
new_folder_id_path.as_slice(),
old_folder_id_path.as_slice(),
new_folder_name_path.as_slice(),
session.user_id,
)
.execute(tx.as_mut())
.await?;

sqlx::query!(
"UPDATE files
SET parent_id_path = $1 || parent_id_path[array_length($2::bytea[], 1) + 1:],
parent_name_path = $3 || parent_name_path[array_length($2::text[], 1) + 1:]
WHERE owner_id = $4 AND parent_id_path >= $2 AND parent_id_path < $2 || NULL",
new_folder_id_path.as_slice(),
old_folder_id_path.as_slice(),
new_folder_name_path.as_slice(),
session.user_id,
)
.execute(tx.as_mut())
.await?;

Ok(())
})
.await?;

Ok((StatusCode::OK, Json(PostResponse {})))
}

/// A `POST` response body for this API route.
#[derive(Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PostResponse {}
Loading