mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-07 18:42:10 +00:00
initial endpoints + queries
This commit is contained in:
1 parent
62861ecc04
commit
b4f525f5e7
49 files changed
+2521
-2
No files matched your search
@@ -0,0 +1,54 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Canonical wire shape for a folder. Used by single-folder GETs, create
|
||||
/// responses, list rows, and subfolder list rows.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FolderInfo {
|
||||
pub token: FolderToken,
|
||||
pub name: String,
|
||||
pub owner_user_token: UserToken,
|
||||
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||
|
||||
pub maybe_last_media_file_token_1: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_2: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_3: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_4: Option<MediaFileToken>,
|
||||
|
||||
pub maybe_cover_image_custom_media_token: Option<MediaFileToken>,
|
||||
|
||||
pub maybe_color_code: Option<String>,
|
||||
pub has_star: bool,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
/// True when `maybe_parent_folder_token` is set but the referenced
|
||||
/// parent row is missing or soft-deleted.
|
||||
pub is_orphaned: bool,
|
||||
}
|
||||
|
||||
/// Canonical wire shape for a media file inside a folder. Lean enough for
|
||||
/// thumbnail rendering; callers that need richer fields should hit the
|
||||
/// media-file batch-get endpoint.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct FolderMediaFileInfo {
|
||||
pub media_file_token: MediaFileToken,
|
||||
pub added_to_folder_at: DateTime<Utc>,
|
||||
|
||||
pub media_type: String,
|
||||
pub media_class: String,
|
||||
pub maybe_mime_type: Option<String>,
|
||||
pub public_bucket_directory_hash: String,
|
||||
pub maybe_public_bucket_prefix: Option<String>,
|
||||
pub maybe_public_bucket_extension: Option<String>,
|
||||
pub maybe_frame_width: Option<i32>,
|
||||
pub maybe_frame_height: Option<i32>,
|
||||
pub maybe_duration_millis: Option<i32>,
|
||||
pub maybe_title: Option<String>,
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::folders::common::FolderInfo;
|
||||
|
||||
// ── POST /v1/folders/create ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct CreateFolderRequest {
|
||||
pub name: String,
|
||||
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||
pub maybe_color_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct CreateFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
pub folder: FolderInfo,
|
||||
}
|
||||
|
||||
// ── GET /v1/folders/list_all ──
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct ListFoldersQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListFoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
pub folders: Vec<FolderInfo>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
// ── GET /v1/folders/folder/{folder_token} ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct FolderPathInfo {
|
||||
pub folder_token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct GetFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
pub folder: FolderInfo,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/folder/{folder_token}/rename ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct RenameFolderRequest {
|
||||
pub new_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct RenameFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/folder/{folder_token}/star ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetFolderStarRequest {
|
||||
pub has_star: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetFolderStarSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/folder/{folder_token}/color_code ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SetFolderColorCodeRequest {
|
||||
/// `None` clears the color tag.
|
||||
pub maybe_color_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SetFolderColorCodeSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
// ── DELETE /v1/folders/folder/{folder_token} ──
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct DeleteFolderSuccessResponse {
|
||||
pub success: bool,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::folders::common::FolderMediaFileInfo;
|
||||
|
||||
// ── GET /v1/folders/media_files/{folder_token} ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct FolderMediaFilesPathInfo {
|
||||
pub folder_token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct ListFolderMediaFilesQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
pub media_files: Vec<FolderMediaFileInfo>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/media_files/{folder_token}/bulk_add ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkAddFolderMediaFilesRequest {
|
||||
/// Media files to add to the folder. Tokens that don't exist or are
|
||||
/// soft-deleted are silently skipped. Already-added rows are no-ops
|
||||
/// (idempotent via INSERT IGNORE on the composite primary key).
|
||||
pub media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkAddFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
/// Tokens that pre-existed (existed + not deleted). Already-added rows
|
||||
/// are included since the operation is idempotent.
|
||||
pub accepted_media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/media_files/{folder_token}/bulk_remove ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkRemoveFolderMediaFilesRequest {
|
||||
pub media_file_tokens: Vec<MediaFileToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkRemoveFolderMediaFilesSuccessResponse {
|
||||
pub success: bool,
|
||||
/// How many membership rows were actually hard-deleted.
|
||||
pub removed_count: u64,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod common;
|
||||
pub mod folder;
|
||||
pub mod media_files;
|
||||
pub mod subfolder;
|
||||
@@ -0,0 +1,61 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use utoipa::{IntoParams, ToSchema};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::folders::common::FolderInfo;
|
||||
|
||||
// ── GET /v1/folders/subfolders/{folder_token} ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct SubfolderPathInfo {
|
||||
pub folder_token: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
pub struct ListSubfoldersQueryParams {
|
||||
pub cursor: Option<String>,
|
||||
pub limit: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ListSubfoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
pub subfolders: Vec<FolderInfo>,
|
||||
pub maybe_cursor: Option<String>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_add ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkAddSubfoldersRequest {
|
||||
/// The folders to reparent under the URL `folder_token`. Tokens that
|
||||
/// don't exist, aren't owned by the caller, are soft-deleted, or are
|
||||
/// the parent itself are silently skipped (idempotent).
|
||||
pub subfolder_tokens: Vec<FolderToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkAddSubfoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
/// Tokens that were actually accepted (existed + owned + not deleted +
|
||||
/// not the parent itself). Already-parented tokens are included here
|
||||
/// since the operation is idempotent.
|
||||
pub accepted_subfolder_tokens: Vec<FolderToken>,
|
||||
}
|
||||
|
||||
// ── PUT /v1/folders/subfolders/{folder_token}/bulk_remove ──
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
pub struct BulkRemoveSubfoldersRequest {
|
||||
/// Folders to unparent. Only rows currently parented to the URL
|
||||
/// `folder_token` are affected; others are silently skipped.
|
||||
pub subfolder_tokens: Vec<FolderToken>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct BulkRemoveSubfoldersSuccessResponse {
|
||||
pub success: bool,
|
||||
/// How many rows were actually unparented.
|
||||
pub removed_count: u64,
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub mod analytics;
|
||||
pub mod characters;
|
||||
pub mod common;
|
||||
pub mod credits;
|
||||
pub mod folders;
|
||||
pub mod generate;
|
||||
pub mod jobs;
|
||||
pub mod media_file;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Materialized representation of a `folders` row plus the derived
|
||||
/// `is_orphaned` flag (true when `maybe_parent_folder_token` is set but the
|
||||
/// referenced parent row is missing or soft-deleted).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FolderRow {
|
||||
pub id: u64,
|
||||
pub token: FolderToken,
|
||||
pub name: String,
|
||||
pub owner_user_token: UserToken,
|
||||
pub maybe_parent_folder_token: Option<FolderToken>,
|
||||
pub maybe_last_media_file_token_1: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_2: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_3: Option<MediaFileToken>,
|
||||
pub maybe_last_media_file_token_4: Option<MediaFileToken>,
|
||||
pub maybe_cover_image_custom_media_token: Option<MediaFileToken>,
|
||||
pub maybe_color_code: Option<String>,
|
||||
pub has_star: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub is_orphaned: bool,
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::queries::folders::folder::folder_row::FolderRow;
|
||||
|
||||
/// Fetch a single (live) folder by token, scoped to an owner. Returns
|
||||
/// `Ok(None)` if the folder doesn't exist, is soft-deleted, or is owned
|
||||
/// by a different user (don't leak existence of others' folders).
|
||||
pub async fn get_folder_for_owner(
|
||||
folder_token: &FolderToken,
|
||||
owner_user_token: &UserToken,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<Option<FolderRow>, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
f.id as `id: u64`,
|
||||
f.token as `token: FolderToken`,
|
||||
f.name,
|
||||
f.owner_user_token as `owner_user_token: UserToken`,
|
||||
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
|
||||
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
|
||||
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
|
||||
f.maybe_color_code,
|
||||
f.has_star as `has_star: bool`,
|
||||
f.created_at,
|
||||
f.updated_at,
|
||||
(
|
||||
f.maybe_parent_folder_token IS NOT NULL
|
||||
AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)
|
||||
) as `is_orphaned: bool`
|
||||
FROM folders f
|
||||
LEFT JOIN folders p
|
||||
ON p.token = f.maybe_parent_folder_token
|
||||
WHERE f.token = ?
|
||||
AND f.owner_user_token = ?
|
||||
AND f.maybe_deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
folder_token.as_str(),
|
||||
owner_user_token.as_str(),
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.map(|r| FolderRow {
|
||||
id: r.id,
|
||||
token: r.token,
|
||||
name: r.name,
|
||||
owner_user_token: r.owner_user_token,
|
||||
maybe_parent_folder_token: r.maybe_parent_folder_token,
|
||||
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
|
||||
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
|
||||
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
|
||||
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
|
||||
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
|
||||
maybe_color_code: r.maybe_color_code,
|
||||
has_star: r.has_star,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
is_orphaned: r.is_orphaned,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub struct InsertFolderArgs<'a> {
|
||||
pub token: &'a FolderToken,
|
||||
pub name: &'a str,
|
||||
pub owner_user_token: &'a UserToken,
|
||||
pub maybe_parent_folder_token: Option<&'a FolderToken>,
|
||||
pub maybe_color_code: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub async fn insert_folder(
|
||||
args: InsertFolderArgs<'_>,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO folders
|
||||
SET
|
||||
token = ?,
|
||||
name = ?,
|
||||
owner_user_token = ?,
|
||||
maybe_parent_folder_token = ?,
|
||||
maybe_color_code = ?
|
||||
"#,
|
||||
args.token.as_str(),
|
||||
args.name,
|
||||
args.owner_user_token.as_str(),
|
||||
args.maybe_parent_folder_token.map(|t| t.as_str()),
|
||||
args.maybe_color_code,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::queries::folders::folder::folder_row::FolderRow;
|
||||
|
||||
pub struct ListFoldersForUserArgs<'a> {
|
||||
pub owner_user_token: &'a UserToken,
|
||||
pub maybe_cursor_id: Option<u64>,
|
||||
pub limit: u32,
|
||||
pub pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
/// List all live folders owned by a user, newest first. Includes the
|
||||
/// derived `is_orphaned` flag computed by joining the table against itself
|
||||
/// on `maybe_parent_folder_token` — orphaned means the parent row is
|
||||
/// missing or soft-deleted.
|
||||
pub async fn list_folders_for_user(
|
||||
args: ListFoldersForUserArgs<'_>,
|
||||
) -> Result<Vec<FolderRow>, sqlx::Error> {
|
||||
let limit = args.limit as i64;
|
||||
|
||||
let rows = match args.maybe_cursor_id {
|
||||
Some(cursor_id) => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
f.id as `id: u64`,
|
||||
f.token as `token: FolderToken`,
|
||||
f.name,
|
||||
f.owner_user_token as `owner_user_token: UserToken`,
|
||||
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
|
||||
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
|
||||
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
|
||||
f.maybe_color_code,
|
||||
f.has_star as `has_star: bool`,
|
||||
f.created_at,
|
||||
f.updated_at,
|
||||
(
|
||||
f.maybe_parent_folder_token IS NOT NULL
|
||||
AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)
|
||||
) as `is_orphaned: bool`
|
||||
FROM folders f
|
||||
LEFT JOIN folders p
|
||||
ON p.token = f.maybe_parent_folder_token
|
||||
WHERE f.owner_user_token = ?
|
||||
AND f.maybe_deleted_at IS NULL
|
||||
AND f.id < ?
|
||||
ORDER BY f.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.owner_user_token.as_str(),
|
||||
cursor_id,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| FolderRow {
|
||||
id: r.id,
|
||||
token: r.token,
|
||||
name: r.name,
|
||||
owner_user_token: r.owner_user_token,
|
||||
maybe_parent_folder_token: r.maybe_parent_folder_token,
|
||||
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
|
||||
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
|
||||
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
|
||||
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
|
||||
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
|
||||
maybe_color_code: r.maybe_color_code,
|
||||
has_star: r.has_star,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
is_orphaned: r.is_orphaned,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
None => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
f.id as `id: u64`,
|
||||
f.token as `token: FolderToken`,
|
||||
f.name,
|
||||
f.owner_user_token as `owner_user_token: UserToken`,
|
||||
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
|
||||
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
|
||||
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
|
||||
f.maybe_color_code,
|
||||
f.has_star as `has_star: bool`,
|
||||
f.created_at,
|
||||
f.updated_at,
|
||||
(
|
||||
f.maybe_parent_folder_token IS NOT NULL
|
||||
AND (p.token IS NULL OR p.maybe_deleted_at IS NOT NULL)
|
||||
) as `is_orphaned: bool`
|
||||
FROM folders f
|
||||
LEFT JOIN folders p
|
||||
ON p.token = f.maybe_parent_folder_token
|
||||
WHERE f.owner_user_token = ?
|
||||
AND f.maybe_deleted_at IS NULL
|
||||
ORDER BY f.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.owner_user_token.as_str(),
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| FolderRow {
|
||||
id: r.id,
|
||||
token: r.token,
|
||||
name: r.name,
|
||||
owner_user_token: r.owner_user_token,
|
||||
maybe_parent_folder_token: r.maybe_parent_folder_token,
|
||||
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
|
||||
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
|
||||
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
|
||||
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
|
||||
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
|
||||
maybe_color_code: r.maybe_color_code,
|
||||
has_star: r.has_star,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
is_orphaned: r.is_orphaned,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod folder_row;
|
||||
pub mod get_folder_for_owner;
|
||||
pub mod insert_folder;
|
||||
pub mod list_folders_for_user;
|
||||
pub mod soft_delete_folder;
|
||||
pub mod update_folder_color_code;
|
||||
pub mod update_folder_has_star;
|
||||
pub mod update_folder_name;
|
||||
@@ -0,0 +1,29 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Mark a folder as deleted by stamping `maybe_deleted_at`. Children
|
||||
/// retain their `maybe_parent_folder_token` and become "orphaned" — that
|
||||
/// state is surfaced by the list query's `is_orphaned` flag.
|
||||
pub async fn soft_delete_folder(
|
||||
folder_token: &FolderToken,
|
||||
owner_user_token: &UserToken,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE folders
|
||||
SET maybe_deleted_at = NOW()
|
||||
WHERE token = ?
|
||||
AND owner_user_token = ?
|
||||
AND maybe_deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
folder_token.as_str(),
|
||||
owner_user_token.as_str(),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Set or clear (`None`) the color code on a folder.
|
||||
pub async fn update_folder_color_code(
|
||||
folder_token: &FolderToken,
|
||||
owner_user_token: &UserToken,
|
||||
maybe_color_code: Option<&str>,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE folders
|
||||
SET maybe_color_code = ?
|
||||
WHERE token = ?
|
||||
AND owner_user_token = ?
|
||||
AND maybe_deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
maybe_color_code,
|
||||
folder_token.as_str(),
|
||||
owner_user_token.as_str(),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
pub async fn update_folder_has_star(
|
||||
folder_token: &FolderToken,
|
||||
owner_user_token: &UserToken,
|
||||
has_star: bool,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE folders
|
||||
SET has_star = ?
|
||||
WHERE token = ?
|
||||
AND owner_user_token = ?
|
||||
AND maybe_deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
has_star,
|
||||
folder_token.as_str(),
|
||||
owner_user_token.as_str(),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Rename a folder. Scoped to owner so callers can't rename a folder they
|
||||
/// don't own. Returns the number of rows updated (0 if no live folder
|
||||
/// matched).
|
||||
pub async fn update_folder_name(
|
||||
folder_token: &FolderToken,
|
||||
owner_user_token: &UserToken,
|
||||
new_name: &str,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
UPDATE folders
|
||||
SET name = ?
|
||||
WHERE token = ?
|
||||
AND owner_user_token = ?
|
||||
AND maybe_deleted_at IS NULL
|
||||
LIMIT 1
|
||||
"#,
|
||||
new_name,
|
||||
folder_token.as_str(),
|
||||
owner_user_token.as_str(),
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
use sqlx::{MySql, MySqlPool, QueryBuilder};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Hard-delete membership rows for the given folder + media file tokens.
|
||||
/// Idempotent — rows that don't exist are silently skipped.
|
||||
///
|
||||
/// Returns the number of rows actually deleted.
|
||||
pub async fn bulk_delete_folder_media_files(
|
||||
folder_token: &FolderToken,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
if media_file_tokens.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"DELETE FROM folder_media_files WHERE folder_token = ",
|
||||
);
|
||||
builder.push_bind(folder_token.as_str());
|
||||
builder.push(" AND media_file_token IN (");
|
||||
|
||||
let mut separated = builder.separated(", ");
|
||||
for token in media_file_tokens {
|
||||
separated.push_bind(token.as_str());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
|
||||
let result = builder.build().execute(pool).await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
use sqlx::{MySql, MySqlPool, QueryBuilder};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Insert membership rows for every (folder_token, media_file_token) in
|
||||
/// the input. Uses `INSERT IGNORE` so already-present rows don't cause
|
||||
/// duplicate-key errors — idempotent.
|
||||
///
|
||||
/// Returns the number of rows actually inserted.
|
||||
pub async fn bulk_insert_folder_media_files(
|
||||
folder_token: &FolderToken,
|
||||
media_file_tokens: &[MediaFileToken],
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
if media_file_tokens.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"INSERT IGNORE INTO folder_media_files (folder_token, media_file_token) ",
|
||||
);
|
||||
builder.push_values(media_file_tokens, |mut b, media_file_token| {
|
||||
b.push_bind(folder_token.as_str())
|
||||
.push_bind(media_file_token.as_str());
|
||||
});
|
||||
|
||||
let result = builder.build().execute(pool).await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
use sqlx::{MySql, MySqlPool, QueryBuilder, Row};
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Given an input set of candidate media-file tokens, return only those
|
||||
/// that currently exist and aren't soft-deleted (no user_deleted_at or
|
||||
/// mod_deleted_at).
|
||||
pub async fn filter_existing_media_file_tokens(
|
||||
candidate_tokens: &[MediaFileToken],
|
||||
pool: &MySqlPool,
|
||||
) -> Result<Vec<MediaFileToken>, sqlx::Error> {
|
||||
if candidate_tokens.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"SELECT token FROM media_files WHERE user_deleted_at IS NULL \
|
||||
AND mod_deleted_at IS NULL AND token IN (",
|
||||
);
|
||||
|
||||
let mut separated = builder.separated(", ");
|
||||
for token in candidate_tokens {
|
||||
separated.push_bind(token.as_str());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
|
||||
let rows = builder.build().fetch_all(pool).await?;
|
||||
|
||||
Ok(rows.into_iter()
|
||||
.map(|row| MediaFileToken::new(row.get::<String, _>(0)))
|
||||
.collect())
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FolderMediaFileRow {
|
||||
/// The `folder_media_files.id` of the membership row. Use this as the
|
||||
/// cursor for the next page.
|
||||
pub membership_id: u64,
|
||||
|
||||
/// When the media file was added to the folder.
|
||||
pub added_to_folder_at: DateTime<Utc>,
|
||||
|
||||
pub media_file_token: MediaFileToken,
|
||||
pub media_type: String,
|
||||
pub media_class: String,
|
||||
pub maybe_mime_type: Option<String>,
|
||||
pub public_bucket_directory_hash: String,
|
||||
pub maybe_public_bucket_prefix: Option<String>,
|
||||
pub maybe_public_bucket_extension: Option<String>,
|
||||
pub maybe_frame_width: Option<i32>,
|
||||
pub maybe_frame_height: Option<i32>,
|
||||
pub maybe_duration_millis: Option<i32>,
|
||||
pub maybe_title: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ListFolderMediaFilesArgs<'a> {
|
||||
pub folder_token: &'a FolderToken,
|
||||
pub maybe_cursor_id: Option<u64>,
|
||||
pub limit: u32,
|
||||
pub pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
/// Paginated list of media files in the given folder. Joins `media_files`
|
||||
/// and filters out rows that are soft-deleted on the media-file side
|
||||
/// (`user_deleted_at` or `mod_deleted_at` set). Most-recently-added first.
|
||||
///
|
||||
/// The caller is expected to have already authorized the folder access
|
||||
/// (i.e. confirmed the folder is owned by the requesting user).
|
||||
pub async fn list_folder_media_files(
|
||||
args: ListFolderMediaFilesArgs<'_>,
|
||||
) -> Result<Vec<FolderMediaFileRow>, sqlx::Error> {
|
||||
let limit = args.limit as i64;
|
||||
|
||||
let rows = match args.maybe_cursor_id {
|
||||
Some(cursor_id) => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
fmf.id as `membership_id: u64`,
|
||||
fmf.created_at as `added_to_folder_at: DateTime<Utc>`,
|
||||
mf.token as `media_file_token: MediaFileToken`,
|
||||
mf.media_type,
|
||||
mf.media_class,
|
||||
mf.maybe_mime_type,
|
||||
mf.public_bucket_directory_hash,
|
||||
mf.maybe_public_bucket_prefix,
|
||||
mf.maybe_public_bucket_extension,
|
||||
mf.maybe_frame_width,
|
||||
mf.maybe_frame_height,
|
||||
mf.maybe_duration_millis,
|
||||
mf.maybe_title
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
AND fmf.id < ?
|
||||
ORDER BY fmf.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.folder_token.as_str(),
|
||||
cursor_id,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| FolderMediaFileRow {
|
||||
membership_id: r.membership_id,
|
||||
added_to_folder_at: r.added_to_folder_at,
|
||||
media_file_token: r.media_file_token,
|
||||
media_type: r.media_type,
|
||||
media_class: r.media_class,
|
||||
maybe_mime_type: r.maybe_mime_type,
|
||||
public_bucket_directory_hash: r.public_bucket_directory_hash,
|
||||
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
|
||||
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
|
||||
maybe_frame_width: r.maybe_frame_width,
|
||||
maybe_frame_height: r.maybe_frame_height,
|
||||
maybe_duration_millis: r.maybe_duration_millis,
|
||||
maybe_title: r.maybe_title,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
None => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
fmf.id as `membership_id: u64`,
|
||||
fmf.created_at as `added_to_folder_at: DateTime<Utc>`,
|
||||
mf.token as `media_file_token: MediaFileToken`,
|
||||
mf.media_type,
|
||||
mf.media_class,
|
||||
mf.maybe_mime_type,
|
||||
mf.public_bucket_directory_hash,
|
||||
mf.maybe_public_bucket_prefix,
|
||||
mf.maybe_public_bucket_extension,
|
||||
mf.maybe_frame_width,
|
||||
mf.maybe_frame_height,
|
||||
mf.maybe_duration_millis,
|
||||
mf.maybe_title
|
||||
FROM folder_media_files fmf
|
||||
JOIN media_files mf
|
||||
ON mf.token = fmf.media_file_token
|
||||
WHERE fmf.folder_token = ?
|
||||
AND mf.user_deleted_at IS NULL
|
||||
AND mf.mod_deleted_at IS NULL
|
||||
ORDER BY fmf.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.folder_token.as_str(),
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| FolderMediaFileRow {
|
||||
membership_id: r.membership_id,
|
||||
added_to_folder_at: r.added_to_folder_at,
|
||||
media_file_token: r.media_file_token,
|
||||
media_type: r.media_type,
|
||||
media_class: r.media_class,
|
||||
maybe_mime_type: r.maybe_mime_type,
|
||||
public_bucket_directory_hash: r.public_bucket_directory_hash,
|
||||
maybe_public_bucket_prefix: r.maybe_public_bucket_prefix,
|
||||
maybe_public_bucket_extension: r.maybe_public_bucket_extension,
|
||||
maybe_frame_width: r.maybe_frame_width,
|
||||
maybe_frame_height: r.maybe_frame_height,
|
||||
maybe_duration_millis: r.maybe_duration_millis,
|
||||
maybe_title: r.maybe_title,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod bulk_delete_folder_media_files;
|
||||
pub mod bulk_insert_folder_media_files;
|
||||
pub mod filter_existing_media_file_tokens;
|
||||
pub mod list_folder_media_files;
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod folder;
|
||||
pub mod media_files;
|
||||
pub mod subfolder;
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use sqlx::{MySql, MySqlPool, QueryBuilder};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Clear `maybe_parent_folder_token` (set to NULL) on every owned, live
|
||||
/// folder whose token is in `child_tokens` AND whose current parent is
|
||||
/// the given `from_parent_token`. The parent guard makes the operation
|
||||
/// safe to run idempotently — it won't accidentally unparent a folder
|
||||
/// that's actually a child of a different parent.
|
||||
///
|
||||
/// Returns the number of rows affected.
|
||||
pub async fn bulk_clear_parent_folder(
|
||||
child_tokens: &[FolderToken],
|
||||
from_parent_token: &FolderToken,
|
||||
owner_user_token: &UserToken,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
if child_tokens.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"UPDATE folders SET maybe_parent_folder_token = NULL WHERE owner_user_token = ",
|
||||
);
|
||||
builder.push_bind(owner_user_token.as_str());
|
||||
builder.push(" AND maybe_deleted_at IS NULL AND maybe_parent_folder_token = ");
|
||||
builder.push_bind(from_parent_token.as_str());
|
||||
builder.push(" AND token IN (");
|
||||
|
||||
let mut separated = builder.separated(", ");
|
||||
for token in child_tokens {
|
||||
separated.push_bind(token.as_str());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
|
||||
let result = builder.build().execute(pool).await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
use sqlx::{MySql, MySqlPool, QueryBuilder};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Set `maybe_parent_folder_token = new_parent_token` on every owned,
|
||||
/// live folder whose token is in `child_tokens` AND that isn't the parent
|
||||
/// itself (a folder can't be its own parent).
|
||||
///
|
||||
/// Returns the number of rows affected.
|
||||
///
|
||||
/// Idempotent: if a row already has the desired parent, MySQL reports it
|
||||
/// as not-changed but still as matched — `rows_affected()` reflects the
|
||||
/// number of rows that actually changed.
|
||||
pub async fn bulk_set_parent_folder(
|
||||
child_tokens: &[FolderToken],
|
||||
new_parent_token: &FolderToken,
|
||||
owner_user_token: &UserToken,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<u64, sqlx::Error> {
|
||||
if child_tokens.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"UPDATE folders SET maybe_parent_folder_token = ",
|
||||
);
|
||||
builder.push_bind(new_parent_token.as_str());
|
||||
builder.push(" WHERE owner_user_token = ");
|
||||
builder.push_bind(owner_user_token.as_str());
|
||||
builder.push(" AND maybe_deleted_at IS NULL AND token != ");
|
||||
builder.push_bind(new_parent_token.as_str());
|
||||
builder.push(" AND token IN (");
|
||||
|
||||
let mut separated = builder.separated(", ");
|
||||
for token in child_tokens {
|
||||
separated.push_bind(token.as_str());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
|
||||
let result = builder.build().execute(pool).await?;
|
||||
Ok(result.rows_affected())
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
use sqlx::{MySql, MySqlPool, QueryBuilder, Row};
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
/// Given an input set of candidate folder tokens, return only those that
|
||||
/// currently exist, are owned by the given user, and are not soft-deleted.
|
||||
/// Order of the result is unspecified.
|
||||
///
|
||||
/// Uses runtime `QueryBuilder` because the IN-list size is dynamic and the
|
||||
/// macro form can't express that.
|
||||
pub async fn filter_existing_owned_folder_tokens(
|
||||
candidate_tokens: &[FolderToken],
|
||||
owner_user_token: &UserToken,
|
||||
pool: &MySqlPool,
|
||||
) -> Result<Vec<FolderToken>, sqlx::Error> {
|
||||
if candidate_tokens.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut builder = QueryBuilder::<MySql>::new(
|
||||
"SELECT token FROM folders WHERE owner_user_token = ",
|
||||
);
|
||||
builder.push_bind(owner_user_token.as_str());
|
||||
builder.push(" AND maybe_deleted_at IS NULL AND token IN (");
|
||||
|
||||
let mut separated = builder.separated(", ");
|
||||
for token in candidate_tokens {
|
||||
separated.push_bind(token.as_str());
|
||||
}
|
||||
separated.push_unseparated(")");
|
||||
|
||||
let rows = builder.build().fetch_all(pool).await?;
|
||||
|
||||
Ok(rows.into_iter()
|
||||
.map(|row| FolderToken::new(row.get::<String, _>(0)))
|
||||
.collect())
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::queries::folders::folder::folder_row::FolderRow;
|
||||
|
||||
pub struct ListSubfoldersArgs<'a> {
|
||||
pub parent_folder_token: &'a FolderToken,
|
||||
pub owner_user_token: &'a UserToken,
|
||||
pub maybe_cursor_id: Option<u64>,
|
||||
pub limit: u32,
|
||||
pub pool: &'a MySqlPool,
|
||||
}
|
||||
|
||||
/// Paginated list of live folders whose `maybe_parent_folder_token` is
|
||||
/// the given parent. Scoped to the owner to avoid leaking siblings of
|
||||
/// folders owned by another user.
|
||||
///
|
||||
/// The `is_orphaned` flag will be `false` for all results: by definition,
|
||||
/// the parent matched here so it exists. (A leftover, soft-deleted parent
|
||||
/// is still treated as "parent exists" — that case is rare and not the
|
||||
/// caller's concern for this endpoint.)
|
||||
pub async fn list_subfolders(
|
||||
args: ListSubfoldersArgs<'_>,
|
||||
) -> Result<Vec<FolderRow>, sqlx::Error> {
|
||||
let limit = args.limit as i64;
|
||||
|
||||
let rows = match args.maybe_cursor_id {
|
||||
Some(cursor_id) => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
f.id as `id: u64`,
|
||||
f.token as `token: FolderToken`,
|
||||
f.name,
|
||||
f.owner_user_token as `owner_user_token: UserToken`,
|
||||
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
|
||||
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
|
||||
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
|
||||
f.maybe_color_code,
|
||||
f.has_star as `has_star: bool`,
|
||||
f.created_at,
|
||||
f.updated_at
|
||||
FROM folders f
|
||||
WHERE f.maybe_parent_folder_token = ?
|
||||
AND f.owner_user_token = ?
|
||||
AND f.maybe_deleted_at IS NULL
|
||||
AND f.id < ?
|
||||
ORDER BY f.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.parent_folder_token.as_str(),
|
||||
args.owner_user_token.as_str(),
|
||||
cursor_id,
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| FolderRow {
|
||||
id: r.id,
|
||||
token: r.token,
|
||||
name: r.name,
|
||||
owner_user_token: r.owner_user_token,
|
||||
maybe_parent_folder_token: r.maybe_parent_folder_token,
|
||||
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
|
||||
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
|
||||
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
|
||||
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
|
||||
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
|
||||
maybe_color_code: r.maybe_color_code,
|
||||
has_star: r.has_star,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
is_orphaned: false,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
None => {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
SELECT
|
||||
f.id as `id: u64`,
|
||||
f.token as `token: FolderToken`,
|
||||
f.name,
|
||||
f.owner_user_token as `owner_user_token: UserToken`,
|
||||
f.maybe_parent_folder_token as `maybe_parent_folder_token: FolderToken`,
|
||||
f.maybe_last_media_file_token_1 as `maybe_last_media_file_token_1: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_2 as `maybe_last_media_file_token_2: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_3 as `maybe_last_media_file_token_3: MediaFileToken`,
|
||||
f.maybe_last_media_file_token_4 as `maybe_last_media_file_token_4: MediaFileToken`,
|
||||
f.maybe_cover_image_custom_media_token as `maybe_cover_image_custom_media_token: MediaFileToken`,
|
||||
f.maybe_color_code,
|
||||
f.has_star as `has_star: bool`,
|
||||
f.created_at,
|
||||
f.updated_at
|
||||
FROM folders f
|
||||
WHERE f.maybe_parent_folder_token = ?
|
||||
AND f.owner_user_token = ?
|
||||
AND f.maybe_deleted_at IS NULL
|
||||
ORDER BY f.id DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
args.parent_folder_token.as_str(),
|
||||
args.owner_user_token.as_str(),
|
||||
limit,
|
||||
)
|
||||
.fetch_all(args.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| FolderRow {
|
||||
id: r.id,
|
||||
token: r.token,
|
||||
name: r.name,
|
||||
owner_user_token: r.owner_user_token,
|
||||
maybe_parent_folder_token: r.maybe_parent_folder_token,
|
||||
maybe_last_media_file_token_1: r.maybe_last_media_file_token_1,
|
||||
maybe_last_media_file_token_2: r.maybe_last_media_file_token_2,
|
||||
maybe_last_media_file_token_3: r.maybe_last_media_file_token_3,
|
||||
maybe_last_media_file_token_4: r.maybe_last_media_file_token_4,
|
||||
maybe_cover_image_custom_media_token: r.maybe_cover_image_custom_media_token,
|
||||
maybe_color_code: r.maybe_color_code,
|
||||
has_star: r.has_star,
|
||||
created_at: r.created_at,
|
||||
updated_at: r.updated_at,
|
||||
is_orphaned: false,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod bulk_clear_parent_folder;
|
||||
pub mod bulk_set_parent_folder;
|
||||
pub mod filter_existing_owned_folder_tokens;
|
||||
pub mod list_subfolders;
|
||||
@@ -13,6 +13,7 @@ pub mod database_time;
|
||||
pub mod email_sender_jobs;
|
||||
pub mod entity_stats;
|
||||
pub mod featured_items;
|
||||
pub mod folders;
|
||||
pub mod generic_download;
|
||||
pub mod generic_inference;
|
||||
pub mod generic_synthetic_ids;
|
||||
|
||||
@@ -94,6 +94,25 @@ use crate::http_server::endpoints::moderation::staff_audit_logs::moderator_list_
|
||||
use crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_emails::moderator_change_user_email_handler::*;
|
||||
use crate::http_server::endpoints::moderation::user_emails::moderator_list_email_address_changes_for_user_handler::*;
|
||||
|
||||
// Folder endpoints
|
||||
use crate::http_server::endpoints::media_files::folder::color_code_folder_handler::*;
|
||||
use crate::http_server::endpoints::media_files::folder::create_folder_handler::*;
|
||||
use crate::http_server::endpoints::media_files::folder::delete_folder_handler::*;
|
||||
use crate::http_server::endpoints::media_files::folder::get_folder_handler::*;
|
||||
use crate::http_server::endpoints::media_files::folder::list_folders_handler::*;
|
||||
use crate::http_server::endpoints::media_files::folder::rename_folder_handler::*;
|
||||
use crate::http_server::endpoints::media_files::folder::star_folder_handler::*;
|
||||
use crate::http_server::endpoints::media_files::media_files::bulk_add_folder_media_files_handler::*;
|
||||
use crate::http_server::endpoints::media_files::media_files::bulk_remove_folder_media_files_handler::*;
|
||||
use crate::http_server::endpoints::media_files::media_files::list_folder_media_files_handler::*;
|
||||
use crate::http_server::endpoints::media_files::subfolder::bulk_add_subfolders_handler::*;
|
||||
use crate::http_server::endpoints::media_files::subfolder::bulk_remove_subfolders_handler::*;
|
||||
use crate::http_server::endpoints::media_files::subfolder::list_subfolders_handler::*;
|
||||
use artcraft_api_defs::folders::common::*;
|
||||
use artcraft_api_defs::folders::folder::*;
|
||||
use artcraft_api_defs::folders::media_files::*;
|
||||
use artcraft_api_defs::folders::subfolder::*;
|
||||
use artcraft_api_defs::moderation::user_referrals::list_global_user_referrals::*;
|
||||
use artcraft_api_defs::moderation::user_referrals::list_user_referrals_for_user::*;
|
||||
use crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::*;
|
||||
@@ -535,8 +554,23 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
crate::http_server::endpoints::moderation::user_referrals::moderator_list_global_user_referrals_handler::moderator_list_global_user_referrals_handler,
|
||||
crate::http_server::endpoints::moderation::user_referrals::moderator_list_user_referrals_for_user_handler::moderator_list_user_referrals_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_bans::moderation_ban_user_handler::moderation_ban_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_emails::moderator_change_user_email::moderator_change_user_email_handler,
|
||||
crate::http_server::endpoints::moderation::user_emails::moderator_list_email_address_changes_for_user::moderator_list_email_address_changes_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_emails::moderator_change_user_email_handler::moderator_change_user_email_handler,
|
||||
crate::http_server::endpoints::moderation::user_emails::moderator_list_email_address_changes_for_user_handler::moderator_list_email_address_changes_for_user_handler,
|
||||
|
||||
// Folders
|
||||
crate::http_server::endpoints::media_files::folder::create_folder_handler::create_folder_handler,
|
||||
crate::http_server::endpoints::media_files::folder::list_folders_handler::list_folders_handler,
|
||||
crate::http_server::endpoints::media_files::folder::get_folder_handler::get_folder_handler,
|
||||
crate::http_server::endpoints::media_files::folder::rename_folder_handler::rename_folder_handler,
|
||||
crate::http_server::endpoints::media_files::folder::star_folder_handler::star_folder_handler,
|
||||
crate::http_server::endpoints::media_files::folder::color_code_folder_handler::color_code_folder_handler,
|
||||
crate::http_server::endpoints::media_files::folder::delete_folder_handler::delete_folder_handler,
|
||||
crate::http_server::endpoints::media_files::subfolder::list_subfolders_handler::list_subfolders_handler,
|
||||
crate::http_server::endpoints::media_files::subfolder::bulk_add_subfolders_handler::bulk_add_subfolders_handler,
|
||||
crate::http_server::endpoints::media_files::subfolder::bulk_remove_subfolders_handler::bulk_remove_subfolders_handler,
|
||||
crate::http_server::endpoints::media_files::media_files::list_folder_media_files_handler::list_folder_media_files_handler,
|
||||
crate::http_server::endpoints::media_files::media_files::bulk_add_folder_media_files_handler::bulk_add_folder_media_files_handler,
|
||||
crate::http_server::endpoints::media_files::media_files::bulk_remove_folder_media_files_handler::bulk_remove_folder_media_files_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_for_user_handler::moderator_list_user_session_impersonation_requests_for_user_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_list_user_session_impersonation_requests_handler::moderator_list_user_session_impersonation_requests_handler,
|
||||
crate::http_server::endpoints::moderation::user_sessions::moderator_user_session_impersonation_request_handler::moderator_user_session_impersonation_request_handler,
|
||||
@@ -1205,6 +1239,37 @@ use crate::http_server::endpoints::media_files::list::list_batch_generated_redux
|
||||
|
||||
// User Emails (Moderation)
|
||||
ModeratorListUserEmailChangesQueryParams,
|
||||
|
||||
// Folders
|
||||
FolderInfo,
|
||||
FolderMediaFileInfo,
|
||||
FolderPathInfo,
|
||||
FolderMediaFilesPathInfo,
|
||||
SubfolderPathInfo,
|
||||
CreateFolderRequest,
|
||||
CreateFolderSuccessResponse,
|
||||
ListFoldersQueryParams,
|
||||
ListFoldersSuccessResponse,
|
||||
GetFolderSuccessResponse,
|
||||
RenameFolderRequest,
|
||||
RenameFolderSuccessResponse,
|
||||
SetFolderStarRequest,
|
||||
SetFolderStarSuccessResponse,
|
||||
SetFolderColorCodeRequest,
|
||||
SetFolderColorCodeSuccessResponse,
|
||||
DeleteFolderSuccessResponse,
|
||||
ListSubfoldersQueryParams,
|
||||
ListSubfoldersSuccessResponse,
|
||||
BulkAddSubfoldersRequest,
|
||||
BulkAddSubfoldersSuccessResponse,
|
||||
BulkRemoveSubfoldersRequest,
|
||||
BulkRemoveSubfoldersSuccessResponse,
|
||||
ListFolderMediaFilesQueryParams,
|
||||
ListFolderMediaFilesSuccessResponse,
|
||||
BulkAddFolderMediaFilesRequest,
|
||||
BulkAddFolderMediaFilesSuccessResponse,
|
||||
BulkRemoveFolderMediaFilesRequest,
|
||||
BulkRemoveFolderMediaFilesSuccessResponse,
|
||||
))
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::folder::{
|
||||
FolderPathInfo, SetFolderColorCodeRequest, SetFolderColorCodeSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::update_folder_color_code::update_folder_color_code;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_COLOR_CODE_LEN: usize = 16;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/folder/{folder_token}/color_code",
|
||||
params(("folder_token" = String, description = "Folder token")),
|
||||
request_body = SetFolderColorCodeRequest,
|
||||
responses(
|
||||
(status = 200, body = SetFolderColorCodeSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn color_code_folder_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderPathInfo>,
|
||||
request: Json<SetFolderColorCodeRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<SetFolderColorCodeSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
// Normalize: trim, treat empty after trim as `None`. This lets clients
|
||||
// clear by passing `""` or by omitting the field entirely.
|
||||
let trimmed: Option<String> = request.maybe_color_code.as_ref().and_then(|s| {
|
||||
let t = s.trim();
|
||||
if t.is_empty() { None } else { Some(t.to_string()) }
|
||||
});
|
||||
|
||||
if let Some(code) = trimmed.as_deref() {
|
||||
if code.len() > MAX_COLOR_CODE_LEN {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("color_code too long (max {} chars)", MAX_COLOR_CODE_LEN),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
let rows_affected = update_folder_color_code(
|
||||
&folder_token,
|
||||
&user_session.user_token,
|
||||
trimmed.as_deref(),
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("update_folder_color_code failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
Ok(Json(SetFolderColorCodeSuccessResponse { success: true }))
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::Json;
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::common::FolderInfo;
|
||||
use artcraft_api_defs::folders::folder::{CreateFolderRequest, CreateFolderSuccessResponse};
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::get_folder_for_owner;
|
||||
use mysql_queries::queries::folders::folder::insert_folder::{insert_folder, InsertFolderArgs};
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_NAME_LEN: usize = 255;
|
||||
|
||||
/// Create a new folder owned by the logged-in user.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/create",
|
||||
request_body = CreateFolderRequest,
|
||||
responses(
|
||||
(status = 200, body = CreateFolderSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn create_folder_handler(
|
||||
http_request: HttpRequest,
|
||||
request: Json<CreateFolderRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<CreateFolderSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let name = request.name.trim().to_string();
|
||||
if name.is_empty() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("name is empty".to_string()));
|
||||
}
|
||||
if name.len() > MAX_NAME_LEN {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("name too long (max {} chars)", MAX_NAME_LEN),
|
||||
));
|
||||
}
|
||||
|
||||
// If a parent was supplied, require it to exist + be owned by the caller.
|
||||
if let Some(parent_token) = &request.maybe_parent_folder_token {
|
||||
let parent = get_folder_for_owner(parent_token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Parent folder lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
if parent.is_none() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
"parent folder does not exist".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let token = FolderToken::generate();
|
||||
|
||||
insert_folder(InsertFolderArgs {
|
||||
token: &token,
|
||||
name: &name,
|
||||
owner_user_token: &user_session.user_token,
|
||||
maybe_parent_folder_token: request.maybe_parent_folder_token.as_ref(),
|
||||
maybe_color_code: request.maybe_color_code.as_deref(),
|
||||
}, &server_state.mysql_pool).await.map_err(|err| {
|
||||
warn!("insert_folder failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
// Re-fetch so we return the canonical FolderInfo including is_orphaned.
|
||||
let row = get_folder_for_owner(&token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Post-insert folder fetch failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?
|
||||
.ok_or_else(|| CommonWebError::server_error_with_message("folder vanished after insert"))?;
|
||||
|
||||
let folder: FolderInfo = folder_row_to_info(row);
|
||||
|
||||
Ok(Json(CreateFolderSuccessResponse {
|
||||
success: true,
|
||||
folder,
|
||||
}))
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::folder::{DeleteFolderSuccessResponse, FolderPathInfo};
|
||||
use mysql_queries::queries::folders::folder::soft_delete_folder::soft_delete_folder;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Soft-delete a folder. Children retain their parent pointer and become
|
||||
/// "orphaned" — surfaced by the list query's `is_orphaned` flag.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/folder/{folder_token}",
|
||||
params(("folder_token" = String, description = "Folder token")),
|
||||
responses(
|
||||
(status = 200, body = DeleteFolderSuccessResponse),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn delete_folder_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<DeleteFolderSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
let rows_affected = soft_delete_folder(
|
||||
&folder_token,
|
||||
&user_session.user_token,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("soft_delete_folder failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
Ok(Json(DeleteFolderSuccessResponse { success: true }))
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
use artcraft_api_defs::folders::common::{FolderInfo, FolderMediaFileInfo};
|
||||
use mysql_queries::queries::folders::folder::folder_row::FolderRow;
|
||||
use mysql_queries::queries::folders::media_files::list_folder_media_files::FolderMediaFileRow;
|
||||
|
||||
pub fn folder_row_to_info(row: FolderRow) -> FolderInfo {
|
||||
FolderInfo {
|
||||
token: row.token,
|
||||
name: row.name,
|
||||
owner_user_token: row.owner_user_token,
|
||||
maybe_parent_folder_token: row.maybe_parent_folder_token,
|
||||
maybe_last_media_file_token_1: row.maybe_last_media_file_token_1,
|
||||
maybe_last_media_file_token_2: row.maybe_last_media_file_token_2,
|
||||
maybe_last_media_file_token_3: row.maybe_last_media_file_token_3,
|
||||
maybe_last_media_file_token_4: row.maybe_last_media_file_token_4,
|
||||
maybe_cover_image_custom_media_token: row.maybe_cover_image_custom_media_token,
|
||||
maybe_color_code: row.maybe_color_code,
|
||||
has_star: row.has_star,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
is_orphaned: row.is_orphaned,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn folder_media_file_row_to_info(row: FolderMediaFileRow) -> FolderMediaFileInfo {
|
||||
FolderMediaFileInfo {
|
||||
media_file_token: row.media_file_token,
|
||||
added_to_folder_at: row.added_to_folder_at,
|
||||
media_type: row.media_type,
|
||||
media_class: row.media_class,
|
||||
maybe_mime_type: row.maybe_mime_type,
|
||||
public_bucket_directory_hash: row.public_bucket_directory_hash,
|
||||
maybe_public_bucket_prefix: row.maybe_public_bucket_prefix,
|
||||
maybe_public_bucket_extension: row.maybe_public_bucket_extension,
|
||||
maybe_frame_width: row.maybe_frame_width,
|
||||
maybe_frame_height: row.maybe_frame_height,
|
||||
maybe_duration_millis: row.maybe_duration_millis,
|
||||
maybe_title: row.maybe_title,
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::folder::{FolderPathInfo, GetFolderSuccessResponse};
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::get_folder_for_owner;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
/// Fetch a single folder owned by the logged-in user.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/folder/{folder_token}",
|
||||
params(("folder_token" = String, description = "Folder token")),
|
||||
responses(
|
||||
(status = 200, body = GetFolderSuccessResponse),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn get_folder_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderPathInfo>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<GetFolderSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
let row = get_folder_for_owner(&folder_token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("get_folder_for_owner failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?
|
||||
.ok_or(CommonWebError::NotFound)?;
|
||||
|
||||
Ok(Json(GetFolderSuccessResponse {
|
||||
success: true,
|
||||
folder: folder_row_to_info(row),
|
||||
}))
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::folder::{
|
||||
ListFoldersQueryParams, ListFoldersSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::list_folders_for_user::{
|
||||
list_folders_for_user, ListFoldersForUserArgs,
|
||||
};
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "folders_all";
|
||||
const DEFAULT_LIMIT: u32 = 100;
|
||||
const MAX_LIMIT: u32 = 1000;
|
||||
|
||||
/// List all live folders owned by the logged-in user. Newest first.
|
||||
/// Includes the derived `is_orphaned` flag.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/list_all",
|
||||
params(ListFoldersQueryParams),
|
||||
responses(
|
||||
(status = 200, body = ListFoldersSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_folders_handler(
|
||||
http_request: HttpRequest,
|
||||
query: Query<ListFoldersQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListFoldersSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let limit = query.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
|
||||
|
||||
let maybe_cursor_id = match &query.cursor {
|
||||
None => None,
|
||||
Some(cursor_str) => {
|
||||
let decoded = server_state.opaque_cursors
|
||||
.decode_cursor_expecting_name(CURSOR_NAME, cursor_str)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to decode cursor: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("Invalid cursor".to_string())
|
||||
})?;
|
||||
decoded.last_id
|
||||
}
|
||||
};
|
||||
|
||||
let rows = list_folders_for_user(ListFoldersForUserArgs {
|
||||
owner_user_token: &user_session.user_token,
|
||||
maybe_cursor_id,
|
||||
limit,
|
||||
pool: &server_state.mysql_pool,
|
||||
}).await.map_err(|err| {
|
||||
warn!("list_folders_for_user failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let maybe_cursor = rows.last().map(|last| {
|
||||
server_state.opaque_cursors.encode_last_id_cursor(CURSOR_NAME, last.id)
|
||||
}).transpose().map_err(|err| {
|
||||
warn!("Failed to encode cursor: {:?}", err);
|
||||
CommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let folders = rows.into_iter().map(folder_row_to_info).collect();
|
||||
|
||||
Ok(Json(ListFoldersSuccessResponse {
|
||||
success: true,
|
||||
folders,
|
||||
maybe_cursor,
|
||||
}))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod color_code_folder_handler;
|
||||
pub mod create_folder_handler;
|
||||
pub mod delete_folder_handler;
|
||||
pub mod folder_info_conversion;
|
||||
pub mod get_folder_handler;
|
||||
pub mod list_folders_handler;
|
||||
pub mod rename_folder_handler;
|
||||
pub mod star_folder_handler;
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::folder::{
|
||||
FolderPathInfo, RenameFolderRequest, RenameFolderSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::update_folder_name::update_folder_name;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_NAME_LEN: usize = 255;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/folder/{folder_token}/rename",
|
||||
params(("folder_token" = String, description = "Folder token")),
|
||||
request_body = RenameFolderRequest,
|
||||
responses(
|
||||
(status = 200, body = RenameFolderSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn rename_folder_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderPathInfo>,
|
||||
request: Json<RenameFolderRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<RenameFolderSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let new_name = request.new_name.trim().to_string();
|
||||
if new_name.is_empty() {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage("name is empty".to_string()));
|
||||
}
|
||||
if new_name.len() > MAX_NAME_LEN {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("name too long (max {} chars)", MAX_NAME_LEN),
|
||||
));
|
||||
}
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
let rows_affected = update_folder_name(
|
||||
&folder_token,
|
||||
&user_session.user_token,
|
||||
&new_name,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("update_folder_name failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
// No live row matched the (token, owner) pair — either it doesn't
|
||||
// exist, is owned by someone else, or is soft-deleted. Don't leak
|
||||
// which.
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
Ok(Json(RenameFolderSuccessResponse { success: true }))
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::folder::{
|
||||
FolderPathInfo, SetFolderStarRequest, SetFolderStarSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::update_folder_has_star::update_folder_has_star;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
#[utoipa::path(
|
||||
put,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/folder/{folder_token}/star",
|
||||
params(("folder_token" = String, description = "Folder token")),
|
||||
request_body = SetFolderStarRequest,
|
||||
responses(
|
||||
(status = 200, body = SetFolderStarSuccessResponse),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn star_folder_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderPathInfo>,
|
||||
request: Json<SetFolderStarRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<SetFolderStarSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
let rows_affected = update_folder_has_star(
|
||||
&folder_token,
|
||||
&user_session.user_token,
|
||||
request.has_star,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("update_folder_has_star failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
if rows_affected == 0 {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
Ok(Json(SetFolderStarSuccessResponse { success: true }))
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::media_files::{
|
||||
BulkAddFolderMediaFilesRequest, BulkAddFolderMediaFilesSuccessResponse,
|
||||
FolderMediaFilesPathInfo,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::get_folder_for_owner;
|
||||
use mysql_queries::queries::folders::media_files::bulk_insert_folder_media_files::bulk_insert_folder_media_files;
|
||||
use mysql_queries::queries::folders::media_files::filter_existing_media_file_tokens::filter_existing_media_file_tokens;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
|
||||
/// Bulk-add media files to a folder. Idempotent via INSERT IGNORE on the
|
||||
/// composite primary key. Media-file tokens that don't exist or are
|
||||
/// soft-deleted are silently skipped; the response lists the accepted
|
||||
/// tokens.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/media_files/{folder_token}/bulk_add",
|
||||
params(("folder_token" = String, description = "Folder token")),
|
||||
request_body = BulkAddFolderMediaFilesRequest,
|
||||
responses(
|
||||
(status = 200, body = BulkAddFolderMediaFilesSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn bulk_add_folder_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderMediaFilesPathInfo>,
|
||||
request: Json<BulkAddFolderMediaFilesRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<BulkAddFolderMediaFilesSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.media_file_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("too many media files in one request (max {})", MAX_BULK),
|
||||
));
|
||||
}
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
// Confirm the folder exists + is owned.
|
||||
let folder = get_folder_for_owner(&folder_token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Folder lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
if folder.is_none() {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
// Filter to media files that actually exist + aren't deleted.
|
||||
let accepted = filter_existing_media_file_tokens(
|
||||
&request.media_file_tokens,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("filter_existing_media_file_tokens failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
if !accepted.is_empty() {
|
||||
bulk_insert_folder_media_files(
|
||||
&folder_token,
|
||||
&accepted,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("bulk_insert_folder_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Json(BulkAddFolderMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
accepted_media_file_tokens: accepted,
|
||||
}))
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::media_files::{
|
||||
BulkRemoveFolderMediaFilesRequest, BulkRemoveFolderMediaFilesSuccessResponse,
|
||||
FolderMediaFilesPathInfo,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::get_folder_for_owner;
|
||||
use mysql_queries::queries::folders::media_files::bulk_delete_folder_media_files::bulk_delete_folder_media_files;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
|
||||
/// Bulk-remove media files from a folder. Hard-deletes the membership
|
||||
/// rows; idempotent (rows that don't exist are silently skipped).
|
||||
#[utoipa::path(
|
||||
put,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/media_files/{folder_token}/bulk_remove",
|
||||
params(("folder_token" = String, description = "Folder token")),
|
||||
request_body = BulkRemoveFolderMediaFilesRequest,
|
||||
responses(
|
||||
(status = 200, body = BulkRemoveFolderMediaFilesSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn bulk_remove_folder_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderMediaFilesPathInfo>,
|
||||
request: Json<BulkRemoveFolderMediaFilesRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<BulkRemoveFolderMediaFilesSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.media_file_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("too many media files in one request (max {})", MAX_BULK),
|
||||
));
|
||||
}
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
// Confirm the folder exists + is owned. We don't strictly need this for
|
||||
// correctness (the DELETE's folder_token guard suffices), but it keeps
|
||||
// the 404 vs 200 semantics consistent with the rest of the API.
|
||||
let folder = get_folder_for_owner(&folder_token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Folder lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
if folder.is_none() {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
let removed_count = bulk_delete_folder_media_files(
|
||||
&folder_token,
|
||||
&request.media_file_tokens,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("bulk_delete_folder_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(BulkRemoveFolderMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
removed_count,
|
||||
}))
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::media_files::{
|
||||
FolderMediaFilesPathInfo, ListFolderMediaFilesQueryParams, ListFolderMediaFilesSuccessResponse,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::get_folder_for_owner;
|
||||
use mysql_queries::queries::folders::media_files::list_folder_media_files::{
|
||||
list_folder_media_files, ListFolderMediaFilesArgs,
|
||||
};
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::folder::folder_info_conversion::folder_media_file_row_to_info;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "folder_mf";
|
||||
const DEFAULT_LIMIT: u32 = 100;
|
||||
const MAX_LIMIT: u32 = 1000;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/media_files/{folder_token}",
|
||||
params(
|
||||
("folder_token" = String, description = "Folder token"),
|
||||
ListFolderMediaFilesQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = ListFolderMediaFilesSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_folder_media_files_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<FolderMediaFilesPathInfo>,
|
||||
query: Query<ListFolderMediaFilesQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListFolderMediaFilesSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let folder_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
// Confirm the folder exists + is owned.
|
||||
let folder = get_folder_for_owner(&folder_token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Folder lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
if folder.is_none() {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
let limit = query.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
|
||||
|
||||
let maybe_cursor_id = match &query.cursor {
|
||||
None => None,
|
||||
Some(cursor_str) => {
|
||||
let decoded = server_state.opaque_cursors
|
||||
.decode_cursor_expecting_name(CURSOR_NAME, cursor_str)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to decode cursor: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("Invalid cursor".to_string())
|
||||
})?;
|
||||
decoded.last_id
|
||||
}
|
||||
};
|
||||
|
||||
let rows = list_folder_media_files(ListFolderMediaFilesArgs {
|
||||
folder_token: &folder_token,
|
||||
maybe_cursor_id,
|
||||
limit,
|
||||
pool: &server_state.mysql_pool,
|
||||
}).await.map_err(|err| {
|
||||
warn!("list_folder_media_files failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let maybe_cursor = rows.last().map(|last| {
|
||||
server_state.opaque_cursors.encode_last_id_cursor(CURSOR_NAME, last.membership_id)
|
||||
}).transpose().map_err(|err| {
|
||||
warn!("Failed to encode cursor: {:?}", err);
|
||||
CommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let media_files = rows.into_iter().map(folder_media_file_row_to_info).collect();
|
||||
|
||||
Ok(Json(ListFolderMediaFilesSuccessResponse {
|
||||
success: true,
|
||||
media_files,
|
||||
maybe_cursor,
|
||||
}))
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod bulk_add_folder_media_files_handler;
|
||||
pub mod bulk_remove_folder_media_files_handler;
|
||||
pub mod list_folder_media_files_handler;
|
||||
@@ -1,9 +1,12 @@
|
||||
pub mod common_responses;
|
||||
pub mod delete;
|
||||
pub mod edit;
|
||||
pub mod folder;
|
||||
pub mod get;
|
||||
pub mod helpers;
|
||||
pub mod list;
|
||||
pub mod media_files;
|
||||
pub mod search;
|
||||
pub mod subfolder;
|
||||
pub mod upload;
|
||||
pub mod upsert_upload;
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::subfolder::{
|
||||
BulkAddSubfoldersRequest, BulkAddSubfoldersSuccessResponse, SubfolderPathInfo,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::get_folder_for_owner;
|
||||
use mysql_queries::queries::folders::subfolder::bulk_set_parent_folder::bulk_set_parent_folder;
|
||||
use mysql_queries::queries::folders::subfolder::filter_existing_owned_folder_tokens::filter_existing_owned_folder_tokens;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
|
||||
/// Bulk-reparent folders under the URL `folder_token`. Idempotent: tokens
|
||||
/// that don't exist or are already parented to this folder are silently
|
||||
/// accepted; the response lists the tokens that "stick".
|
||||
#[utoipa::path(
|
||||
put,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/subfolders/{folder_token}/bulk_add",
|
||||
params(("folder_token" = String, description = "Parent folder token")),
|
||||
request_body = BulkAddSubfoldersRequest,
|
||||
responses(
|
||||
(status = 200, body = BulkAddSubfoldersSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn bulk_add_subfolders_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<SubfolderPathInfo>,
|
||||
request: Json<BulkAddSubfoldersRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<BulkAddSubfoldersSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.subfolder_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("too many subfolders in one request (max {})", MAX_BULK),
|
||||
));
|
||||
}
|
||||
|
||||
let parent_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
// Confirm the parent exists + is owned.
|
||||
let parent = get_folder_for_owner(&parent_token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Parent folder lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
if parent.is_none() {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
// Filter input to tokens that actually exist, are owned, and aren't deleted.
|
||||
let accepted = filter_existing_owned_folder_tokens(
|
||||
&request.subfolder_tokens,
|
||||
&user_session.user_token,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("filter_existing_owned_folder_tokens failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
if !accepted.is_empty() {
|
||||
bulk_set_parent_folder(
|
||||
&accepted,
|
||||
&parent_token,
|
||||
&user_session.user_token,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("bulk_set_parent_folder failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
}
|
||||
|
||||
// Note: `accepted` may include the parent itself; the SQL guard excludes
|
||||
// it from the UPDATE. We still report it as "accepted" for client
|
||||
// simplicity — no-op tokens are normal in idempotent bulk endpoints.
|
||||
Ok(Json(BulkAddSubfoldersSuccessResponse {
|
||||
success: true,
|
||||
accepted_subfolder_tokens: accepted,
|
||||
}))
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::subfolder::{
|
||||
BulkRemoveSubfoldersRequest, BulkRemoveSubfoldersSuccessResponse, SubfolderPathInfo,
|
||||
};
|
||||
use mysql_queries::queries::folders::subfolder::bulk_clear_parent_folder::bulk_clear_parent_folder;
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const MAX_BULK: usize = 500;
|
||||
|
||||
/// Bulk-unparent folders from the URL `folder_token`. Idempotent: rows
|
||||
/// that aren't currently parented to this folder are silently skipped.
|
||||
#[utoipa::path(
|
||||
put,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/subfolders/{folder_token}/bulk_remove",
|
||||
params(("folder_token" = String, description = "Parent folder token")),
|
||||
request_body = BulkRemoveSubfoldersRequest,
|
||||
responses(
|
||||
(status = 200, body = BulkRemoveSubfoldersSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn bulk_remove_subfolders_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<SubfolderPathInfo>,
|
||||
request: Json<BulkRemoveSubfoldersRequest>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<BulkRemoveSubfoldersSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
if request.subfolder_tokens.len() > MAX_BULK {
|
||||
return Err(CommonWebError::BadInputWithSimpleMessage(
|
||||
format!("too many subfolders in one request (max {})", MAX_BULK),
|
||||
));
|
||||
}
|
||||
|
||||
let from_parent_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
// No parent existence check — the UPDATE's WHERE clause already gates
|
||||
// on owner_user_token AND current parent, so this is naturally a no-op
|
||||
// for rows we shouldn't touch.
|
||||
let removed_count = bulk_clear_parent_folder(
|
||||
&request.subfolder_tokens,
|
||||
&from_parent_token,
|
||||
&user_session.user_token,
|
||||
&server_state.mysql_pool,
|
||||
).await.map_err(|err| {
|
||||
warn!("bulk_clear_parent_folder failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
Ok(Json(BulkRemoveSubfoldersSuccessResponse {
|
||||
success: true,
|
||||
removed_count,
|
||||
}))
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::web::{Json, Path, Query};
|
||||
use actix_web::{web, HttpRequest};
|
||||
use log::warn;
|
||||
|
||||
use artcraft_api_defs::folders::subfolder::{
|
||||
ListSubfoldersQueryParams, ListSubfoldersSuccessResponse, SubfolderPathInfo,
|
||||
};
|
||||
use mysql_queries::queries::folders::folder::get_folder_for_owner::get_folder_for_owner;
|
||||
use mysql_queries::queries::folders::subfolder::list_subfolders::{
|
||||
list_subfolders, ListSubfoldersArgs,
|
||||
};
|
||||
use tokens::tokens::folders::FolderToken;
|
||||
|
||||
use crate::http_server::common_responses::common_web_error::CommonWebError;
|
||||
use crate::http_server::endpoints::media_files::folder::folder_info_conversion::folder_row_to_info;
|
||||
use crate::http_server::web_utils::user_session::require_user_session_using_connection::require_user_session_using_connection;
|
||||
use crate::state::server_state::ServerState;
|
||||
|
||||
const CURSOR_NAME: &str = "subfolders";
|
||||
const DEFAULT_LIMIT: u32 = 100;
|
||||
const MAX_LIMIT: u32 = 1000;
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Folders",
|
||||
path = "/v1/folders/subfolders/{folder_token}",
|
||||
params(
|
||||
("folder_token" = String, description = "Parent folder token"),
|
||||
ListSubfoldersQueryParams,
|
||||
),
|
||||
responses(
|
||||
(status = 200, body = ListSubfoldersSuccessResponse),
|
||||
(status = 400, body = CommonWebError),
|
||||
(status = 401, body = CommonWebError),
|
||||
(status = 404, body = CommonWebError),
|
||||
(status = 500, body = CommonWebError),
|
||||
),
|
||||
)]
|
||||
pub async fn list_subfolders_handler(
|
||||
http_request: HttpRequest,
|
||||
path: Path<SubfolderPathInfo>,
|
||||
query: Query<ListSubfoldersQueryParams>,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<Json<ListSubfoldersSuccessResponse>, CommonWebError> {
|
||||
let mut conn = server_state.mysql_pool.acquire().await.map_err(|err| {
|
||||
warn!("MySQL pool error: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let user_session = require_user_session_using_connection(
|
||||
&http_request, &server_state.session_checker, &mut conn,
|
||||
).await.map_err(|_| CommonWebError::NotAuthorized)?;
|
||||
|
||||
let parent_token = FolderToken::new_from_str(path.folder_token.trim());
|
||||
|
||||
// Confirm the parent exists + is owned by the caller before listing.
|
||||
let parent = get_folder_for_owner(&parent_token, &user_session.user_token, &server_state.mysql_pool)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
warn!("Parent folder lookup failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
if parent.is_none() {
|
||||
return Err(CommonWebError::NotFound);
|
||||
}
|
||||
|
||||
let limit = query.limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
|
||||
|
||||
let maybe_cursor_id = match &query.cursor {
|
||||
None => None,
|
||||
Some(cursor_str) => {
|
||||
let decoded = server_state.opaque_cursors
|
||||
.decode_cursor_expecting_name(CURSOR_NAME, cursor_str)
|
||||
.map_err(|err| {
|
||||
warn!("Failed to decode cursor: {:?}", err);
|
||||
CommonWebError::BadInputWithSimpleMessage("Invalid cursor".to_string())
|
||||
})?;
|
||||
decoded.last_id
|
||||
}
|
||||
};
|
||||
|
||||
let rows = list_subfolders(ListSubfoldersArgs {
|
||||
parent_folder_token: &parent_token,
|
||||
owner_user_token: &user_session.user_token,
|
||||
maybe_cursor_id,
|
||||
limit,
|
||||
pool: &server_state.mysql_pool,
|
||||
}).await.map_err(|err| {
|
||||
warn!("list_subfolders failed: {:?}", err);
|
||||
CommonWebError::from_error(err)
|
||||
})?;
|
||||
|
||||
let maybe_cursor = rows.last().map(|last| {
|
||||
server_state.opaque_cursors.encode_last_id_cursor(CURSOR_NAME, last.id)
|
||||
}).transpose().map_err(|err| {
|
||||
warn!("Failed to encode cursor: {:?}", err);
|
||||
CommonWebError::server_error_with_message("Failed to encode cursor")
|
||||
})?;
|
||||
|
||||
let subfolders = rows.into_iter().map(folder_row_to_info).collect();
|
||||
|
||||
Ok(Json(ListSubfoldersSuccessResponse {
|
||||
success: true,
|
||||
subfolders,
|
||||
maybe_cursor,
|
||||
}))
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod bulk_add_subfolders_handler;
|
||||
pub mod bulk_remove_subfolders_handler;
|
||||
pub mod list_subfolders_handler;
|
||||
+2
@@ -5,6 +5,7 @@ use crate::http_server::routes::application_routes::comments_routes::add_comment
|
||||
use crate::http_server::routes::application_routes::credits_routes::add_credits_routes;
|
||||
use crate::http_server::routes::application_routes::dev_routes::add_dev_routes;
|
||||
use crate::http_server::routes::application_routes::featured_item_routes::add_featured_item_routes;
|
||||
use crate::http_server::routes::application_routes::folders_routes::add_folders_routes;
|
||||
use crate::http_server::routes::application_routes::generate_routes::add_generate_routes;
|
||||
use crate::http_server::routes::application_routes::job_routes::add_job_routes;
|
||||
use crate::http_server::routes::application_routes::media_files_routes::add_media_file_routes;
|
||||
@@ -55,6 +56,7 @@ where
|
||||
|
||||
// Media files routes
|
||||
app = add_media_file_routes(app); // /v1/media_files/...
|
||||
app = add_folders_routes(app); // /v1/folders/...
|
||||
app = add_featured_item_routes(app); // /v1/featured_item/...
|
||||
app = add_prompts_routes(app); // /v1/prompts/...
|
||||
|
||||
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
use actix_http::body::MessageBody;
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{web, App, Error, HttpResponse};
|
||||
|
||||
use crate::http_server::endpoints::media_files::folder::color_code_folder_handler::color_code_folder_handler;
|
||||
use crate::http_server::endpoints::media_files::folder::create_folder_handler::create_folder_handler;
|
||||
use crate::http_server::endpoints::media_files::folder::delete_folder_handler::delete_folder_handler;
|
||||
use crate::http_server::endpoints::media_files::folder::get_folder_handler::get_folder_handler;
|
||||
use crate::http_server::endpoints::media_files::folder::list_folders_handler::list_folders_handler;
|
||||
use crate::http_server::endpoints::media_files::folder::rename_folder_handler::rename_folder_handler;
|
||||
use crate::http_server::endpoints::media_files::folder::star_folder_handler::star_folder_handler;
|
||||
use crate::http_server::endpoints::media_files::media_files::bulk_add_folder_media_files_handler::bulk_add_folder_media_files_handler;
|
||||
use crate::http_server::endpoints::media_files::media_files::bulk_remove_folder_media_files_handler::bulk_remove_folder_media_files_handler;
|
||||
use crate::http_server::endpoints::media_files::media_files::list_folder_media_files_handler::list_folder_media_files_handler;
|
||||
use crate::http_server::endpoints::media_files::subfolder::bulk_add_subfolders_handler::bulk_add_subfolders_handler;
|
||||
use crate::http_server::endpoints::media_files::subfolder::bulk_remove_subfolders_handler::bulk_remove_subfolders_handler;
|
||||
use crate::http_server::endpoints::media_files::subfolder::list_subfolders_handler::list_subfolders_handler;
|
||||
|
||||
pub fn add_folders_routes<T, B>(app: App<T>) -> App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
{
|
||||
app.service(
|
||||
web::scope("/v1/folders")
|
||||
// Folder CRUD
|
||||
.service(
|
||||
web::resource("/create")
|
||||
.route(web::post().to(create_folder_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/list_all")
|
||||
.route(web::get().to(list_folders_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::scope("/folder")
|
||||
.service(
|
||||
web::resource("/{folder_token}")
|
||||
.route(web::get().to(get_folder_handler))
|
||||
.route(web::delete().to(delete_folder_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{folder_token}/rename")
|
||||
.route(web::put().to(rename_folder_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{folder_token}/star")
|
||||
.route(web::put().to(star_folder_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{folder_token}/color_code")
|
||||
.route(web::put().to(color_code_folder_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
),
|
||||
)
|
||||
// Subfolder membership
|
||||
.service(
|
||||
web::scope("/subfolders")
|
||||
.service(
|
||||
web::resource("/{folder_token}")
|
||||
.route(web::get().to(list_subfolders_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{folder_token}/bulk_add")
|
||||
.route(web::put().to(bulk_add_subfolders_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{folder_token}/bulk_remove")
|
||||
.route(web::put().to(bulk_remove_subfolders_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
),
|
||||
)
|
||||
// Media-file membership
|
||||
.service(
|
||||
web::scope("/media_files")
|
||||
.service(
|
||||
web::resource("/{folder_token}")
|
||||
.route(web::get().to(list_folder_media_files_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{folder_token}/bulk_add")
|
||||
.route(web::put().to(bulk_add_folder_media_files_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/{folder_token}/bulk_remove")
|
||||
.route(web::put().to(bulk_remove_folder_media_files_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod comments_routes;
|
||||
mod credits_routes;
|
||||
mod dev_routes;
|
||||
mod featured_item_routes;
|
||||
mod folders_routes;
|
||||
mod generate_routes;
|
||||
mod job_routes;
|
||||
mod media_files_routes;
|
||||
|
||||
Reference in new issue
Block a user