nexus_common/models/file/
details.rs1use 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 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#[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 match exec_single_row(queries::del::delete_file(&self.owner_id, &self.id)).await {
135 Ok(_) => {
136 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}