diff --git a/.sqlx/query-3b79b1b81fc7b84e8aee12a02138a764f7fade4eac0acbd7082c1272d7e67d8a.json b/.sqlx/query-3b79b1b81fc7b84e8aee12a02138a764f7fade4eac0acbd7082c1272d7e67d8a.json new file mode 100644 index 0000000..243a05a --- /dev/null +++ b/.sqlx/query-3b79b1b81fc7b84e8aee12a02138a764f7fade4eac0acbd7082c1272d7e67d8a.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE folders\n SET parent_name_path = $1 || parent_name_path[array_length($1::text[], 1) + 1:]\n WHERE owner_id = $2 AND parent_name_path >= $3 AND parent_name_path < $3 || NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "TextArray", + "Bytea", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "3b79b1b81fc7b84e8aee12a02138a764f7fade4eac0acbd7082c1272d7e67d8a" +} diff --git a/.sqlx/query-676ec52814ed958e452c86ac8034b9f3d91f1de63000d0eb886546ef02debbef.json b/.sqlx/query-676ec52814ed958e452c86ac8034b9f3d91f1de63000d0eb886546ef02debbef.json new file mode 100644 index 0000000..970913c --- /dev/null +++ b/.sqlx/query-676ec52814ed958e452c86ac8034b9f3d91f1de63000d0eb886546ef02debbef.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE files\n SET parent_name_path = $1 || parent_name_path[array_length($1::text[], 1) + 1:]\n WHERE owner_id = $2 AND parent_name_path >= $3 AND parent_name_path < $3 || NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "TextArray", + "Bytea", + "TextArray" + ] + }, + "nullable": [] + }, + "hash": "676ec52814ed958e452c86ac8034b9f3d91f1de63000d0eb886546ef02debbef" +} diff --git a/.sqlx/query-d846a4ddf323a15d66cc0b9f88bd63a11b02535e49ef12e83f19889bbfa74eed.json b/.sqlx/query-d846a4ddf323a15d66cc0b9f88bd63a11b02535e49ef12e83f19889bbfa74eed.json new file mode 100644 index 0000000..6747c35 --- /dev/null +++ b/.sqlx/query-d846a4ddf323a15d66cc0b9f88bd63a11b02535e49ef12e83f19889bbfa74eed.json @@ -0,0 +1,30 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE folders\n SET name = $1\n WHERE id = $2 AND owner_id = $3\n RETURNING parent_name_path, OLD.name AS old_name", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "parent_name_path", + "type_info": "TextArray" + }, + { + "ordinal": 1, + "name": "old_name", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text", + "Bytea", + "Bytea" + ] + }, + "nullable": [ + false, + false + ] + }, + "hash": "d846a4ddf323a15d66cc0b9f88bd63a11b02535e49ef12e83f19889bbfa74eed" +} diff --git a/backend/api/routes.rs b/backend/api/routes.rs index 3f11086..0e627ef 100644 --- a/backend/api/routes.rs +++ b/backend/api/routes.rs @@ -38,6 +38,10 @@ pub(super) static ROUTER: LazyLock = LazyLock::new(|| { "/files/{file_id}/share", delete(v0::files::file::share::delete).post(v0::files::file::share::post), ) + .route( + "/folders/{folder_id}/name", + put(v0::folders::folder::name::put), + ) .route( "/folders/{folder_id}/share", delete(v0::folders::folder::share::delete).post(v0::folders::folder::share::post), diff --git a/backend/api/routes/v0/folders/folder.rs b/backend/api/routes/v0/folders/folder.rs index 8b2db58..447303b 100644 --- a/backend/api/routes/v0/folders/folder.rs +++ b/backend/api/routes/v0/folders/folder.rs @@ -1,3 +1,4 @@ //! A folder. +pub(crate) mod name; pub(crate) mod share; diff --git a/backend/api/routes/v0/folders/folder/name.rs b/backend/api/routes/v0/folders/folder/name.rs new file mode 100644 index 0000000..c7104ea --- /dev/null +++ b/backend/api/routes/v0/folders/folder/name.rs @@ -0,0 +1,122 @@ +//! A folder's name. + +use axum::http::StatusCode; +use axum_macros::debug_handler; +use serde::{Deserialize, Serialize}; + +use crate::{ + api::{ + self, Json, + extract::{AuthToken, Path}, + response::Response, + validation::FileName, + }, + db::{self, TxError, TxResult}, + id::Id, +}; + +/// A request path for this API route. +type PathParams = Path; + +/// A `PUT` request body for this API route. +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub(crate) struct PutRequest { + /// The folder's new name. + name: FileName, +} + +/// Renames a folder. +/// +/// # Errors +/// +/// See [`crate::api::Error`]. +#[debug_handler] +pub(crate) async fn put( + Path(folder_id): PathParams, + AuthToken(token_hash): AuthToken, + Json(body): Json, +) -> impl Response { + 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 folder = match sqlx::query!( + "UPDATE folders + SET name = $1 + WHERE id = $2 AND owner_id = $3 + RETURNING parent_name_path, OLD.name AS old_name", + body.name.as_str(), + folder_id.as_slice(), + session.user_id, + ) + .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) => { + // The client shouldn't be able to tell whether a non-shared folder exists by its + // ID. + return Err(TxError::Abort(api::Error::AccessDenied)); + } + + Ok(Some(folder)) => folder, + }; + + let mut old_folder_path = folder.parent_name_path.clone(); + old_folder_path.push(folder.old_name); + + let mut new_folder_path = folder.parent_name_path; + new_folder_path.push(body.name.to_string()); + + sqlx::query!( + "UPDATE folders + SET parent_name_path = $1 || parent_name_path[array_length($1::text[], 1) + 1:] + WHERE owner_id = $2 AND parent_name_path >= $3 AND parent_name_path < $3 || NULL", + new_folder_path.as_slice(), + session.user_id, + old_folder_path.as_slice(), + ) + .execute(tx.as_mut()) + .await?; + + sqlx::query!( + "UPDATE files + SET parent_name_path = $1 || parent_name_path[array_length($1::text[], 1) + 1:] + WHERE owner_id = $2 AND parent_name_path >= $3 AND parent_name_path < $3 || NULL", + new_folder_path.as_slice(), + session.user_id, + old_folder_path.as_slice(), + ) + .execute(tx.as_mut()) + .await?; + + Ok(()) + }) + .await?; + + Ok((StatusCode::OK, Json(PutResponse { name: body.name }))) +} + +/// A `PUT` response body for this API route. +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PutResponse { + /// The folder's new name. + name: FileName, +}