figma_mcp/figma/
image_cache.rs1use std::collections::HashMap;
2use std::sync::{Arc, RwLock};
3use std::time::SystemTime;
4
5use crate::{Error, Result};
6
7#[derive(Clone)]
8pub struct ImageCache {
9 entries: Arc<RwLock<HashMap<String, ImageEntry>>>,
10}
11
12#[derive(Clone, Debug)]
13pub struct ImageEntry {
14 pub file_key: String,
15 pub node_id: String,
16 pub format: String,
17 pub scale: f64,
18 pub figma_url: String,
19 pub cached_data: Option<Vec<u8>>,
20 pub export_time: SystemTime,
21}
22
23impl ImageCache {
24 pub fn new() -> Self {
25 Self {
26 entries: Arc::new(RwLock::new(HashMap::new())),
27 }
28 }
29
30 pub fn register_export(
31 &self,
32 file_key: String,
33 node_id: String,
34 format: String,
35 scale: f64,
36 figma_url: String,
37 ) -> Result<String> {
38 let uri = Self::generate_uri(&file_key, &node_id, &format, scale);
39
40 let entry = ImageEntry {
41 file_key,
42 node_id,
43 format,
44 scale,
45 figma_url,
46 cached_data: None,
47 export_time: SystemTime::now(),
48 };
49
50 let mut entries = self.entries.write()
51 .map_err(|_| Error::Internal("Failed to acquire lock".to_string()))?;
52 entries.insert(uri.clone(), entry);
53
54 Ok(uri)
55 }
56
57 pub fn list_all(&self) -> Result<Vec<(String, ImageEntry)>> {
58 let entries = self.entries.read()
59 .map_err(|_| Error::Internal("Failed to acquire lock".to_string()))?;
60
61 Ok(entries.iter()
62 .map(|(uri, entry)| (uri.clone(), entry.clone()))
63 .collect())
64 }
65
66 pub fn get_entry(&self, uri: &str) -> Result<Option<ImageEntry>> {
67 let entries = self.entries.read()
68 .map_err(|_| Error::Internal("Failed to acquire lock".to_string()))?;
69
70 Ok(entries.get(uri).cloned())
71 }
72
73 pub fn update_cached_data(&self, uri: &str, data: Vec<u8>) -> Result<()> {
74 let mut entries = self.entries.write()
75 .map_err(|_| Error::Internal("Failed to acquire lock".to_string()))?;
76
77 if let Some(entry) = entries.get_mut(uri) {
78 entry.cached_data = Some(data);
79 Ok(())
80 } else {
81 Err(Error::NotFound(format!("Resource not found: {}", uri)))
82 }
83 }
84
85 pub fn is_expired(&self, entry: &ImageEntry) -> bool {
86 if let Ok(elapsed) = entry.export_time.elapsed() {
87 elapsed.as_secs() > 3600
89 } else {
90 true
91 }
92 }
93
94 pub fn get_mime_type(format: &str) -> &'static str {
95 match format.to_lowercase().as_str() {
96 "png" => "image/png",
97 "jpg" | "jpeg" => "image/jpeg",
98 "svg" => "image/svg+xml",
99 "pdf" => "application/pdf",
100 _ => "application/octet-stream",
101 }
102 }
103
104 fn generate_uri(file_key: &str, node_id: &str, format: &str, scale: f64) -> String {
105 if scale != 1.0 {
106 format!("figma://file/{}/node/{}@{}x.{}", file_key, node_id, scale as u32, format)
107 } else {
108 format!("figma://file/{}/node/{}.{}", file_key, node_id, format)
109 }
110 }
111}
112
113impl Default for ImageCache {
114 fn default() -> Self {
115 Self::new()
116 }
117}