tauri_plugin_mongoose/db/
files.rs1use std::{env, path::Path};
2
3use futures::io::{AsyncReadExt, Cursor};
4use mongodb::{
5 bson::{doc, oid::ObjectId, Bson, DateTime, Document},
6 gridfs::FilesCollectionDocument,
7 options::{GridFsBucketOptions, GridFsUploadOptions},
8};
9use serde::Deserialize;
10use serde_json::Value;
11
12use crate::db::state::{get_client, get_db_name};
13
14#[derive(Debug, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct SaveFileRequest {
18 pub collection: String,
20 pub path: Option<Value>,
22 pub data: Option<Vec<u8>>,
24 pub filename: Option<String>,
26 pub metadata: Option<Value>,
28 pub chunk_size_bytes: Option<u32>,
30}
31
32pub async fn save_file(args: SaveFileRequest) -> Result<Value, String> {
35 let SaveFileRequest {
36 collection,
37 path,
38 data,
39 filename,
40 metadata,
41 chunk_size_bytes,
42 } = args;
43
44 if path.is_none() && data.is_none() {
45 return Err("Either 'path' or 'data' must be provided".to_string());
46 }
47
48 let client = get_client().await?;
49 let db_name = get_db_name().await;
50 let db = client.database(&db_name);
51
52 let bucket_options = GridFsBucketOptions::builder()
53 .bucket_name(collection.clone())
54 .chunk_size_bytes(chunk_size_bytes)
55 .build();
56 let bucket = db.gridfs_bucket(bucket_options);
57
58 let (bytes, final_filename) = if let Some(path_value) = path {
60 let path_str_opt = match path_value {
61 Value::String(s) => Some(s),
62 Value::Object(map) => map
63 .get("path")
64 .and_then(|v| v.as_str())
65 .map(|s| s.to_string())
66 .or_else(|| {
67 map.get("filePath")
68 .and_then(|v| v.as_str())
69 .map(|s| s.to_string())
70 }),
71 _ => None,
72 };
73
74 let file_path = path_str_opt.ok_or_else(|| {
75 "Invalid 'path': expected a string or object with a 'path' field".to_string()
76 })?;
77
78 let bytes = tokio::fs::read(&file_path)
79 .await
80 .map_err(|e| format!("Failed to read file at {}: {}", file_path, e))?;
81 let derived_name = Path::new(&file_path)
82 .file_name()
83 .and_then(|n| n.to_str())
84 .map(|s| s.to_string());
85 (
86 bytes,
87 filename
88 .or(derived_name)
89 .unwrap_or_else(|| "untitled".to_string()),
90 )
91 } else if let Some(bytes) = data {
92 (bytes, filename.unwrap_or_else(|| "untitled".to_string()))
93 } else {
94 return Err("No data supplied".to_string());
96 };
97
98 let length = bytes.len() as i64;
99 let chunk_size = chunk_size_bytes.unwrap_or(255 * 1024);
100
101 let metadata_doc: Option<Document> = match metadata {
102 Some(value) => Some(
103 mongodb::bson::to_document(&value).map_err(|e| format!("Invalid metadata: {}", e))?,
104 ),
105 None => None,
106 };
107
108 let upload_options = GridFsUploadOptions::builder()
109 .chunk_size_bytes(chunk_size_bytes)
110 .metadata(metadata_doc.clone())
111 .build();
112
113 let cursor = Cursor::new(bytes);
114 let file_id = bucket
115 .upload_from_futures_0_3_reader(final_filename.clone(), cursor, upload_options)
116 .await
117 .map_err(|e| e.to_string())?;
118
119 let files_collection =
121 db.collection::<FilesCollectionDocument>(&format!("{}.files", collection));
122 let stored = files_collection
123 .find_one(doc! {"_id": file_id.clone()}, None)
124 .await
125 .map_err(|e| e.to_string())?;
126
127 let mut response_doc = if let Some(file) = stored {
128 mongodb::bson::to_document(&file).map_err(|e| e.to_string())?
129 } else {
130 let mut fallback = doc! {
132 "_id": file_id.clone(),
133 "length": length,
134 "chunkSizeBytes": chunk_size as i32,
135 "uploadDate": DateTime::now(),
136 "filename": final_filename.clone(),
137 };
138 if let Some(meta) = metadata_doc.clone() {
139 fallback.insert("metadata", meta);
140 }
141 fallback
142 };
143
144 response_doc.insert("_id", file_id.to_hex());
145 response_doc.insert("bucket", collection.clone());
146
147 mongodb::bson::from_document::<Value>(response_doc).map_err(|e| e.to_string())
148}
149
150pub async fn get_file_url(
152 collection: String,
153 id: String,
154 mime_type: Option<String>,
155) -> Result<String, String> {
156 let client = get_client().await?;
157 let db_name = get_db_name().await;
158 let db = client.database(&db_name);
159
160 let files_collection = db.collection::<Document>(&format!("{}.files", collection));
161 let file_filter = match ObjectId::parse_str(&id) {
162 Ok(oid) => doc! {
163 "$or": [
164 { "_id": oid },
165 { "_id": id.clone() }
166 ]
167 },
168 Err(_) => doc! { "_id": id.clone() },
169 };
170
171 let file_doc = files_collection
172 .find_one(file_filter, None)
173 .await
174 .map_err(|e| e.to_string())?
175 .ok_or_else(|| format!("File not found for id '{}'", id))?;
176
177 let file_id = file_doc
178 .get("_id")
179 .cloned()
180 .ok_or_else(|| "Stored file is missing _id".to_string())?;
181 let filename = file_doc
182 .get("filename")
183 .and_then(Bson::as_str)
184 .map(|name| name.to_string());
185 let metadata = match file_doc.get("metadata") {
186 Some(Bson::Document(doc)) => Some(doc),
187 _ => None,
188 };
189
190 let bucket_options = GridFsBucketOptions::builder()
191 .bucket_name(collection.clone())
192 .build();
193 let bucket = db.gridfs_bucket(bucket_options);
194 let mut stream = bucket
195 .open_download_stream(file_id)
196 .await
197 .map_err(|e| e.to_string())?;
198
199 let mut bytes = Vec::new();
200 stream
201 .read_to_end(&mut bytes)
202 .await
203 .map_err(|e| e.to_string())?;
204
205 let resolved_mime = resolve_mime_type(filename.as_deref(), metadata, mime_type);
206 let cache_dir = env::temp_dir()
207 .join("tauri-plugin-mongoose")
208 .join("gridfs-cache")
209 .join(db_name)
210 .join(collection);
211 tokio::fs::create_dir_all(&cache_dir)
212 .await
213 .map_err(|e| format!("Unable to create cache dir: {}", e))?;
214
215 let mut base_name = filename.unwrap_or_else(|| id.clone());
216 let ext = extension_for_mime(&resolved_mime).or_else(|| {
217 Path::new(&base_name)
218 .extension()
219 .and_then(|e| e.to_str())
220 .map(|s| s.to_string())
221 });
222 if let Some(ref extension) = ext {
223 if !base_name
224 .to_ascii_lowercase()
225 .ends_with(&format!(".{}", extension))
226 {
227 base_name = format!("{}.{}", base_name, extension);
228 }
229 }
230
231 let file_path = cache_dir.join(base_name);
232 tokio::fs::write(&file_path, &bytes)
233 .await
234 .map_err(|e| format!("Unable to write cached file: {}", e))?;
235
236 Ok(format!("file://{}", file_path.to_string_lossy()))
237}
238
239fn resolve_mime_type(
240 filename: Option<&str>,
241 metadata: Option<&Document>,
242 explicit_mime_type: Option<String>,
243) -> String {
244 if let Some(explicit) = explicit_mime_type {
245 if !explicit.trim().is_empty() {
246 return explicit;
247 }
248 }
249
250 if let Some(meta) = metadata {
251 if let Some(Bson::String(content_type)) = meta.get("contentType") {
252 return content_type.clone();
253 }
254 if let Some(Bson::String(content_type)) = meta.get("mimeType") {
255 return content_type.clone();
256 }
257 }
258
259 if let Some(name) = filename {
260 if let Some(ext) = Path::new(name).extension().and_then(|value| value.to_str()) {
261 match ext.to_ascii_lowercase().as_str() {
262 "png" => return "image/png".to_string(),
263 "jpg" | "jpeg" => return "image/jpeg".to_string(),
264 "gif" => return "image/gif".to_string(),
265 "webp" => return "image/webp".to_string(),
266 "svg" => return "image/svg+xml".to_string(),
267 "bmp" => return "image/bmp".to_string(),
268 "tif" | "tiff" => return "image/tiff".to_string(),
269 "pdf" => return "application/pdf".to_string(),
270 "txt" => return "text/plain".to_string(),
271 "json" => return "application/json".to_string(),
272 _ => {}
273 }
274 }
275 }
276
277 "application/octet-stream".to_string()
278}
279
280fn extension_for_mime(mime: &str) -> Option<String> {
281 match mime {
282 "image/png" => Some("png".to_string()),
283 "image/jpeg" => Some("jpg".to_string()),
284 "image/gif" => Some("gif".to_string()),
285 "image/webp" => Some("webp".to_string()),
286 "image/svg+xml" => Some("svg".to_string()),
287 "image/bmp" => Some("bmp".to_string()),
288 "image/tiff" => Some("tiff".to_string()),
289 "application/pdf" => Some("pdf".to_string()),
290 "text/plain" => Some("txt".to_string()),
291 "application/json" => Some("json".to_string()),
292 _ => None,
293 }
294}