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