reactive_graph_sys_binary/
web_resource_provider.rs1use 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 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 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 if !prefix.starts_with("data:") || !prefix.ends_with(";base64") {
210 return None;
211 }
212 }
213 Some(s)
214}