| 1 |
|
| 2 |
|
| 3 |
use chrono::{DateTime, Utc}; |
| 4 |
use serde::{Deserialize, Serialize}; |
| 5 |
use crate::id_types::{AttachmentId, EmailId, ProjectId, TaskId, UserId}; |
| 6 |
|
| 7 |
|
| 8 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 9 |
#[serde(rename_all = "camelCase")] |
| 10 |
pub struct Attachment { |
| 11 |
pub id: AttachmentId, |
| 12 |
pub user_id: UserId, |
| 13 |
pub task_id: Option<TaskId>, |
| 14 |
pub project_id: Option<ProjectId>, |
| 15 |
pub filename: String, |
| 16 |
pub file_size: i64, |
| 17 |
pub mime_type: String, |
| 18 |
pub blob_hash: String, |
| 19 |
pub source_email_id: Option<EmailId>, |
| 20 |
pub created_at: DateTime<Utc>, |
| 21 |
} |
| 22 |
|
| 23 |
|
| 24 |
#[derive(Debug, Clone)] |
| 25 |
pub struct NewAttachment { |
| 26 |
pub task_id: Option<TaskId>, |
| 27 |
pub project_id: Option<ProjectId>, |
| 28 |
pub filename: String, |
| 29 |
pub file_size: i64, |
| 30 |
pub mime_type: String, |
| 31 |
pub blob_hash: String, |
| 32 |
pub source_email_id: Option<EmailId>, |
| 33 |
} |
| 34 |
|
| 35 |
|
| 36 |
#[derive(Debug, Clone, Serialize, Deserialize)] |
| 37 |
pub struct AttachmentMeta { |
| 38 |
pub filename: String, |
| 39 |
pub mime_type: String, |
| 40 |
pub size: usize, |
| 41 |
pub blob_hash: String, |
| 42 |
} |
| 43 |
|
| 44 |
|
| 45 |
pub fn mime_from_extension(filename: &str) -> &'static str { |
| 46 |
let ext = filename.rsplit('.').next().unwrap_or("").to_ascii_lowercase(); |
| 47 |
match ext.as_str() { |
| 48 |
"pdf" => "application/pdf", |
| 49 |
"doc" | "docx" => "application/msword", |
| 50 |
"xls" | "xlsx" => "application/vnd.ms-excel", |
| 51 |
"ppt" | "pptx" => "application/vnd.ms-powerpoint", |
| 52 |
"zip" => "application/zip", |
| 53 |
"gz" | "gzip" => "application/gzip", |
| 54 |
"tar" => "application/x-tar", |
| 55 |
"json" => "application/json", |
| 56 |
"xml" => "application/xml", |
| 57 |
"csv" => "text/csv", |
| 58 |
"txt" | "md" | "markdown" => "text/plain", |
| 59 |
"html" | "htm" => "text/html", |
| 60 |
"css" => "text/css", |
| 61 |
"js" => "text/javascript", |
| 62 |
"png" => "image/png", |
| 63 |
"jpg" | "jpeg" => "image/jpeg", |
| 64 |
"gif" => "image/gif", |
| 65 |
"svg" => "image/svg+xml", |
| 66 |
"webp" => "image/webp", |
| 67 |
"ico" => "image/x-icon", |
| 68 |
"mp3" => "audio/mpeg", |
| 69 |
"wav" => "audio/wav", |
| 70 |
"ogg" => "audio/ogg", |
| 71 |
"flac" => "audio/flac", |
| 72 |
"aac" => "audio/aac", |
| 73 |
"mp4" => "video/mp4", |
| 74 |
"webm" => "video/webm", |
| 75 |
"mov" => "video/quicktime", |
| 76 |
"avi" => "video/x-msvideo", |
| 77 |
"rs" => "text/x-rust", |
| 78 |
"py" => "text/x-python", |
| 79 |
"toml" => "application/toml", |
| 80 |
"yaml" | "yml" => "application/yaml", |
| 81 |
_ => "application/octet-stream", |
| 82 |
} |
| 83 |
} |
| 84 |
|
| 85 |
|
| 86 |
pub fn format_file_size(bytes: i64) -> String { |
| 87 |
if bytes < 1024 { |
| 88 |
format!("{} B", bytes) |
| 89 |
} else if bytes < 1024 * 1024 { |
| 90 |
format!("{:.1} KB", bytes as f64 / 1024.0) |
| 91 |
} else if bytes < 1024 * 1024 * 1024 { |
| 92 |
format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0)) |
| 93 |
} else { |
| 94 |
format!("{:.1} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0)) |
| 95 |
} |
| 96 |
} |
| 97 |
|
| 98 |
#[cfg(test)] |
| 99 |
mod tests { |
| 100 |
use super::*; |
| 101 |
|
| 102 |
#[test] |
| 103 |
fn mime_detection_common_types() { |
| 104 |
assert_eq!(mime_from_extension("report.pdf"), "application/pdf"); |
| 105 |
assert_eq!(mime_from_extension("photo.jpg"), "image/jpeg"); |
| 106 |
assert_eq!(mime_from_extension("photo.JPEG"), "image/jpeg"); |
| 107 |
assert_eq!(mime_from_extension("song.mp3"), "audio/mpeg"); |
| 108 |
assert_eq!(mime_from_extension("video.mp4"), "video/mp4"); |
| 109 |
assert_eq!(mime_from_extension("notes.txt"), "text/plain"); |
| 110 |
assert_eq!(mime_from_extension("data.csv"), "text/csv"); |
| 111 |
} |
| 112 |
|
| 113 |
#[test] |
| 114 |
fn mime_detection_unknown_extension() { |
| 115 |
assert_eq!(mime_from_extension("file.xyz"), "application/octet-stream"); |
| 116 |
assert_eq!(mime_from_extension("noext"), "application/octet-stream"); |
| 117 |
} |
| 118 |
|
| 119 |
#[test] |
| 120 |
fn file_size_formatting() { |
| 121 |
assert_eq!(format_file_size(0), "0 B"); |
| 122 |
assert_eq!(format_file_size(512), "512 B"); |
| 123 |
assert_eq!(format_file_size(1024), "1.0 KB"); |
| 124 |
assert_eq!(format_file_size(1536), "1.5 KB"); |
| 125 |
assert_eq!(format_file_size(1048576), "1.0 MB"); |
| 126 |
assert_eq!(format_file_size(1572864), "1.5 MB"); |
| 127 |
assert_eq!(format_file_size(1073741824), "1.0 GB"); |
| 128 |
} |
| 129 |
|
| 130 |
#[test] |
| 131 |
fn mime_from_dotfile() { |
| 132 |
assert_eq!(mime_from_extension(".gitignore"), "application/octet-stream"); |
| 133 |
} |
| 134 |
|
| 135 |
#[test] |
| 136 |
fn attachment_meta_serialization_roundtrip() { |
| 137 |
let meta = AttachmentMeta { |
| 138 |
filename: "report.pdf".into(), |
| 139 |
mime_type: "application/pdf".into(), |
| 140 |
size: 12345, |
| 141 |
blob_hash: "abc123def456".into(), |
| 142 |
}; |
| 143 |
let json = serde_json::to_string(&meta).unwrap(); |
| 144 |
let deserialized: AttachmentMeta = serde_json::from_str(&json).unwrap(); |
| 145 |
assert_eq!(deserialized.filename, "report.pdf"); |
| 146 |
assert_eq!(deserialized.mime_type, "application/pdf"); |
| 147 |
assert_eq!(deserialized.size, 12345); |
| 148 |
assert_eq!(deserialized.blob_hash, "abc123def456"); |
| 149 |
} |
| 150 |
|
| 151 |
#[test] |
| 152 |
fn attachment_meta_empty_list() { |
| 153 |
let metas: Vec<AttachmentMeta> = vec![]; |
| 154 |
let json = serde_json::to_string(&metas).unwrap(); |
| 155 |
assert_eq!(json, "[]"); |
| 156 |
let deserialized: Vec<AttachmentMeta> = serde_json::from_str(&json).unwrap(); |
| 157 |
assert!(deserialized.is_empty()); |
| 158 |
} |
| 159 |
|
| 160 |
#[test] |
| 161 |
fn attachment_meta_multiple() { |
| 162 |
let metas = vec![ |
| 163 |
AttachmentMeta { |
| 164 |
filename: "doc.pdf".into(), |
| 165 |
mime_type: "application/pdf".into(), |
| 166 |
size: 1000, |
| 167 |
blob_hash: "hash1".into(), |
| 168 |
}, |
| 169 |
AttachmentMeta { |
| 170 |
filename: "image.png".into(), |
| 171 |
mime_type: "image/png".into(), |
| 172 |
size: 2000, |
| 173 |
blob_hash: "hash2".into(), |
| 174 |
}, |
| 175 |
]; |
| 176 |
let json = serde_json::to_string(&metas).unwrap(); |
| 177 |
let deserialized: Vec<AttachmentMeta> = serde_json::from_str(&json).unwrap(); |
| 178 |
assert_eq!(deserialized.len(), 2); |
| 179 |
assert_eq!(deserialized[0].filename, "doc.pdf"); |
| 180 |
assert_eq!(deserialized[1].filename, "image.png"); |
| 181 |
} |
| 182 |
} |
| 183 |
|