reactive_graph_sys_binary/
web_resource_provider.rs

1use std::str::FromStr;
2use std::sync::LazyLock;
3
4use async_trait::async_trait;
5use base64::Engine;
6use base64::engine::general_purpose::STANDARD;
7use http::Response;
8use http::StatusCode;
9use http::request::Request;
10use log::debug;
11use matchit::Router;
12use reactive_graph_graph::PropertyInstanceSetter;
13use reactive_graph_graph::prelude::*;
14use reactive_graph_plugin_api::EntityInstanceManager;
15use reactive_graph_plugin_api::HttpBody;
16use reactive_graph_plugin_api::WebResourceProvider;
17use reactive_graph_plugin_api::prelude::plugin::*;
18use serde_json::json;
19use strum_macros::AsRefStr;
20use strum_macros::Display;
21use strum_macros::IntoStaticStr;
22use uuid::Uuid;
23
24const CONTEXT_PATH: &str = "binary";
25
26static ID: LazyLock<Uuid> = LazyLock::new(Uuid::new_v4);
27
28#[derive(AsRefStr, IntoStaticStr, Display)]
29enum BinaryRequestType {
30    Uuid,
31    Label,
32}
33
34#[derive(AsRefStr, IntoStaticStr, Display, Clone, Debug)]
35enum EntityInstanceReference {
36    Id(Uuid),
37    Label(String),
38}
39
40#[derive(Clone, Debug)]
41struct PropertyReference {
42    pub entity_instance: EntityInstanceReference,
43    pub property_name: String,
44}
45
46#[derive(Component)]
47pub struct BinaryWebResourceProvider {
48    #[component(default = "crate::plugin::entity_instance_manager")]
49    entity_instance_manager: Arc<dyn EntityInstanceManager + Send + Sync>,
50}
51
52impl BinaryWebResourceProvider {
53    fn get_property_reference(&self, search: String) -> Option<PropertyReference> {
54        let mut matcher = Router::new();
55        let _ = matcher.insert("/entities/:uuid/:property_name", BinaryRequestType::Uuid);
56        let _ = matcher.insert("/entities/label/*label", BinaryRequestType::Label);
57        match matcher.at(search.as_str()) {
58            Ok(matched) => match matched.value {
59                BinaryRequestType::Uuid => match matched.params.get("uuid") {
60                    Some(uuid) => match Uuid::from_str(uuid) {
61                        Ok(uuid) => matched.params.get("property_name").map(|property_name| PropertyReference {
62                            entity_instance: EntityInstanceReference::Id(uuid),
63                            property_name: String::from(property_name),
64                        }),
65                        Err(_) => None,
66                    },
67                    None => None,
68                },
69                BinaryRequestType::Label => matched.params.get("label").map(|label| PropertyReference {
70                    entity_instance: EntityInstanceReference::Label(String::from(label)),
71                    property_name: String::new(),
72                }),
73            },
74            Err(_) => None,
75        }
76    }
77
78    fn get_data_url(&self, property_reference: PropertyReference) -> Option<String> {
79        match property_reference.entity_instance {
80            EntityInstanceReference::Id(id) => self
81                .entity_instance_manager
82                .get(id)
83                .and_then(|entity_instance| entity_instance.as_string(property_reference.property_name))
84                .and_then(filter_by_base64_data_url),
85            EntityInstanceReference::Label(label) => {
86                self.entity_instance_manager
87                    .get_by_label_with_params(label.as_str())
88                    .and_then(|(entity_instance, params)| {
89                        debug!("params {:?}", params);
90                        // Prefer path variable "property"
91                        if let Some(data_url) = params.get("property").and_then(|property_name| entity_instance.as_string(property_name)) {
92                            return Some(data_url);
93                        }
94                        // Try other path variable
95                        params.iter().next().and_then(|(_, property_name)| entity_instance.as_string(property_name))
96                    })
97            }
98        }
99    }
100
101    fn set_data_url_binary(&self, property_reference: PropertyReference, bytes: &Vec<u8>) {
102        if let EntityInstanceReference::Id(id) = property_reference.entity_instance {
103            if let Some(entity_instance) = self.entity_instance_manager.get(id) {
104                if let Some(mime_type) = infer::get(bytes) {
105                    let data_as_base64 = STANDARD.encode(bytes);
106                    let data_url = json!(format!("data:{};base64,{}", mime_type, data_as_base64));
107                    entity_instance.set(property_reference.property_name, data_url);
108                }
109            }
110        }
111    }
112
113    fn set_data_url_base64(&self, property_reference: PropertyReference, data_url_base64: &String) {
114        match property_reference.entity_instance {
115            EntityInstanceReference::Id(id) => {
116                if let Some(entity_instance) = self.entity_instance_manager.get(id) {
117                    debug!("{} {}", id, property_reference.property_name);
118                    entity_instance.set(property_reference.property_name, json!(data_url_base64));
119                }
120            }
121            EntityInstanceReference::Label(_) => {}
122        }
123    }
124
125    fn extract_data_url_payload(&self, data_url: String) -> Option<HttpBody> {
126        let mut parts = data_url.splitn(2, ',');
127        parts.next();
128        match parts.next() {
129            Some(part_base64_encoded_data) => match STANDARD.decode(part_base64_encoded_data) {
130                Ok(bytes) => Some(HttpBody::Binary(bytes)),
131                Err(_) => None,
132            },
133            None => None,
134        }
135    }
136
137    fn decode_data_url(&self, data_url: String) -> http::Result<Response<HttpBody>> {
138        match self.extract_data_url_payload(data_url) {
139            Some(body) => Response::builder().status(StatusCode::OK).body(body),
140            None => not_found(),
141        }
142    }
143
144    fn download(&self, property_reference: PropertyReference) -> http::Result<Response<HttpBody>> {
145        match self.get_data_url(property_reference) {
146            Some(data_url) => self.decode_data_url(data_url),
147            None => not_found(),
148        }
149    }
150
151    fn upload(&self, property_reference: PropertyReference, request: Request<HttpBody>) -> http::Result<Response<HttpBody>> {
152        match request.body() {
153            HttpBody::Binary(bytes) => {
154                debug!("upload binary");
155                self.set_data_url_binary(property_reference.clone(), bytes);
156            }
157            HttpBody::PlainText(data_url_base64) => {
158                debug!("upload data url");
159                self.set_data_url_base64(property_reference.clone(), data_url_base64);
160            }
161            _ => {}
162        }
163        self.download(property_reference)
164    }
165}
166
167#[async_trait]
168#[component_alias]
169impl WebResourceProvider for BinaryWebResourceProvider {
170    fn id(&self) -> Uuid {
171        *ID
172    }
173
174    fn get_context_path(&self) -> String {
175        CONTEXT_PATH.to_string()
176    }
177
178    async fn handle_web_resource(&self, path: String, request: Request<HttpBody>) -> http::Result<Response<HttpBody>> {
179        let uri = request.uri();
180        debug!("uri: {uri}");
181        debug!("path: {path}");
182        let search = format!("/{path}");
183        debug!("search: {search}");
184        let property_reference = self.get_property_reference(search);
185        if property_reference.is_none() {
186            return not_found();
187        }
188        let property_reference = property_reference.unwrap();
189        debug!("property_reference: {} {:?}", property_reference.property_name, property_reference.entity_instance);
190
191        let method = request.method().as_str();
192        debug!("request: {}", request.method());
193
194        match method {
195            "GET" => self.download(property_reference),
196            "POST" => self.upload(property_reference, request),
197            _ => not_found(),
198        }
199    }
200}
201
202fn not_found() -> http::Result<Response<HttpBody>> {
203    Response::builder().status(StatusCode::NOT_FOUND).body(HttpBody::None)
204}
205
206fn filter_by_base64_data_url(s: String) -> Option<String> {
207    if let Some(prefix) = s.split(',').next() {
208        // prefix: data:image/png;base64
209        if !prefix.starts_with("data:") || !prefix.ends_with(";base64") {
210            return None;
211        }
212    }
213    Some(s)
214}