Skip to main content

nexus_common/models/file/
details.rs

1use crate::db::DbError;
2use crate::db::{exec_single_row, queries, RedisOps};
3use crate::media::FileVariant;
4use crate::models::traits::Collection;
5use crate::types::DynError;
6use async_trait::async_trait;
7use chrono::Utc;
8use neo4rs::Query;
9use pubky_app_specs::{ParsedUri, PubkyAppFile, Resource};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::Path;
13use tracing::error;
14use utoipa::ToSchema;
15
16#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)]
17pub struct FileUrls {
18    pub main: String,
19    pub feed: Option<String>,
20    pub small: Option<String>,
21}
22
23impl FileUrls {
24    /// Creates a new instance by constructing URLs for file variants
25    ///
26    /// # Arguments
27    /// * `base_path` - A reference to a `PathBuf` representing the base directory where files are stored
28    /// * `variants` - A slice of `FileVariant` values representing the available file variants
29    pub fn new(base_path: &Path, variants: &[FileVariant]) -> Self {
30        let build_url = |variant: &FileVariant| {
31            base_path
32                .join(variant.to_string())
33                .to_string_lossy()
34                .into_owned()
35        };
36
37        Self {
38            main: build_url(&FileVariant::Main),
39            feed: variants
40                .contains(&FileVariant::Feed)
41                .then(|| build_url(&FileVariant::Feed)),
42            small: variants
43                .contains(&FileVariant::Small)
44                .then(|| build_url(&FileVariant::Small)),
45        }
46    }
47}
48
49mod json_string {
50    use serde::{self, Deserialize, Deserializer, Serializer};
51
52    pub fn serialize<S, T>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
53    where
54        S: Serializer,
55        T: serde::Serialize,
56    {
57        let json_string = serde_json::to_string(value).map_err(serde::ser::Error::custom)?;
58        serializer.serialize_str(&json_string)
59    }
60
61    pub fn deserialize<'de, D, T>(deserializer: D) -> Result<T, D::Error>
62    where
63        D: Deserializer<'de>,
64        T: serde::de::DeserializeOwned,
65    {
66        let json_string = String::deserialize(deserializer)?;
67        serde_json::from_str(&json_string).map_err(serde::de::Error::custom)
68    }
69}
70
71/// Represents a file and its metadata, including links to the actual binary of the file.
72#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Default)]
73pub struct FileDetails {
74    pub id: String,
75    pub uri: String,
76    pub owner_id: String,
77    pub indexed_at: i64,
78    pub created_at: i64,
79    pub src: String,
80    pub name: String,
81    pub size: i64,
82    pub content_type: String,
83    #[serde(with = "json_string")]
84    pub urls: FileUrls,
85    pub metadata: Option<HashMap<String, String>>,
86}
87
88pub struct FileMeta {
89    pub urls: FileUrls,
90}
91
92impl RedisOps for FileDetails {}
93
94#[async_trait]
95impl Collection<&[&str]> for FileDetails {
96    fn collection_details_graph_query(id_list: &[&[&str]]) -> Query {
97        queries::get::get_files_by_ids(id_list)
98    }
99
100    fn put_graph_query(&self) -> Result<Query, DynError> {
101        queries::put::create_file(self)
102    }
103
104    async fn extend_on_index_miss(_: &[std::option::Option<Self>]) -> Result<(), DynError> {
105        Ok(())
106    }
107}
108
109impl FileDetails {
110    pub fn from_homeserver(
111        pubkyapp_file: &PubkyAppFile,
112        uri: String,
113        user_id: String,
114        file_id: String,
115        meta: FileMeta,
116    ) -> Self {
117        Self {
118            name: pubkyapp_file.name.clone(),
119            src: pubkyapp_file.src.clone(),
120            content_type: pubkyapp_file.content_type.clone(),
121            uri,
122            id: file_id,
123            created_at: pubkyapp_file.created_at,
124            indexed_at: Utc::now().timestamp_millis(),
125            owner_id: user_id.to_string(),
126            size: pubkyapp_file.size as i64,
127            urls: meta.urls,
128            metadata: None,
129        }
130    }
131
132    pub async fn delete(&self) -> Result<(), DbError> {
133        // Delete graph node
134        match exec_single_row(queries::del::delete_file(&self.owner_id, &self.id)).await {
135            Ok(_) => {
136                // Delete on Redis
137                match Self::remove_from_index_multiple_json(&[&[&self.owner_id, &self.id]]).await {
138                    Ok(()) => (),
139                    Err(e) => {
140                        error!("Index file deletion, {}: {:?}", self.id, e);
141                        return Err(DbError::IndexOperationFailed {
142                            message: format!("Could not delete the index, {e:?}"),
143                        });
144                    }
145                }
146            }
147            Err(e) => {
148                error!("Graph file deletion, {}: {:?}", self.id, e);
149                return Err(DbError::GraphQueryFailed {
150                    message: format!("Could not delete the file, {e:?}"),
151                });
152            }
153        };
154        Ok(())
155    }
156
157    pub fn file_key_from_uri(uri: &str) -> Vec<String> {
158        let parsed_uri = ParsedUri::try_from(uri).unwrap_or_default();
159        if let Resource::File(file_id) = parsed_uri.resource {
160            vec![parsed_uri.user_id.to_string(), file_id]
161        } else {
162            vec![parsed_uri.user_id.to_string(), String::default()]
163        }
164    }
165}