1use axum::{
4 extract::{Path, Query, State},
5 Json,
6};
7use serde::{Deserialize, Serialize};
8
9use crate::prelude::*;
10use cloudillo_core::extract::Auth;
11
12const TAG_FORBIDDEN_CHARS: &[char] = &[' ', ',', '#', '\t', '\n'];
13
14#[derive(Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct ListTagsQuery {
18 pub prefix: Option<String>,
19 pub with_counts: Option<bool>,
20 pub limit: Option<u32>,
21}
22
23#[derive(Serialize)]
25pub struct ListTagsResponse {
26 pub tags: Vec<TagInfo>,
27}
28
29pub async fn list_tags(
30 State(app): State<App>,
31 Auth(auth): Auth,
32 Query(q): Query<ListTagsQuery>,
33) -> ClResult<Json<ListTagsResponse>> {
34 let with_counts = q.with_counts.unwrap_or(false);
35 let tags = app
36 .meta_adapter
37 .list_tags(auth.tn_id, q.prefix.as_deref(), with_counts, q.limit)
38 .await?;
39
40 Ok(Json(ListTagsResponse { tags }))
41}
42
43#[derive(Serialize)]
45pub struct TagResponse {
46 pub tags: Vec<String>,
47}
48
49pub async fn put_file_tag(
50 State(app): State<App>,
51 Auth(auth): Auth,
52 Path((file_id, tag)): Path<(String, String)>,
53) -> ClResult<Json<TagResponse>> {
54 if tag.chars().any(|c| TAG_FORBIDDEN_CHARS.contains(&c)) {
56 return Err(Error::PermissionDenied);
57 }
58
59 let tags = app.meta_adapter.add_tag(auth.tn_id, &file_id, &tag).await?;
60
61 info!("User {} added tag {} to file {}", auth.id_tag, tag, file_id);
62
63 Ok(Json(TagResponse { tags }))
64}
65
66pub async fn delete_file_tag(
68 State(app): State<App>,
69 Auth(auth): Auth,
70 Path((file_id, tag)): Path<(String, String)>,
71) -> ClResult<Json<TagResponse>> {
72 let tags = app.meta_adapter.remove_tag(auth.tn_id, &file_id, &tag).await?;
73
74 info!("User {} removed tag {} from file {}", auth.id_tag, tag, file_id);
75
76 Ok(Json(TagResponse { tags }))
77}
78
79