client_core/sync/
local_pusher.rs1use std::sync::Arc;
19
20use crate::crypto;
21use crate::http::{HttpError, RestClient};
22use crate::rest::{ContentType, PushRequest};
23use crate::store::models::StoredClip;
24use crate::store::{queries, Store, StoreError};
25
26#[derive(Clone)]
32pub struct LocalPusher {
33 store: Arc<Store>,
34 client: Arc<RestClient>,
35 enc_key: Option<[u8; 32]>,
36}
37
38#[derive(Debug, thiserror::Error)]
39pub enum IngestError {
40 #[error("no encryption key available — clip dropped (E2EE required)")]
41 NoEncryptionKey,
42 #[error("encryption failed: {0}")]
43 Crypto(String),
44 #[error("relay push failed: {0}")]
45 Push(#[from] HttpError),
46 #[error("local store write failed: {0}")]
47 Store(#[from] StoreError),
48}
49
50impl LocalPusher {
51 pub fn new(store: Arc<Store>, client: Arc<RestClient>, enc_key: Option<[u8; 32]>) -> Self {
52 Self {
53 store,
54 client,
55 enc_key,
56 }
57 }
58
59 pub async fn push_text(
66 &self,
67 raw: Vec<u8>,
68 source: &str,
69 label: &str,
70 content_type: ContentType,
71 ) -> Result<String, IngestError> {
72 let key = self.enc_key.ok_or(IngestError::NoEncryptionKey)?;
73 let original_size = raw.len() as i64;
74 let ciphertext = crypto::encrypt(&key, &raw).map_err(IngestError::Crypto)?;
75 let wire = content_type.as_wire();
76 let req = PushRequest {
77 content: ciphertext,
78 content_type: wire.to_string(),
79 label: label.to_string(),
80 source: source.to_string(),
81 media_path: None,
82 byte_size: original_size,
83 encrypted: true,
84 target_device_id: None,
85 };
86 let resp = self.client.push_clip_json(&req).await?;
87 self.write_through(&resp.clip_id, source, wire, raw, original_size)?;
88 Ok(resp.clip_id)
89 }
90
91 pub async fn push_image_png(
94 &self,
95 raw_png: Vec<u8>,
96 source: &str,
97 label: &str,
98 ) -> Result<String, IngestError> {
99 let key = self.enc_key.ok_or(IngestError::NoEncryptionKey)?;
100 let original_size = raw_png.len() as i64;
101 let ciphertext = crypto::encrypt(&key, &raw_png).map_err(IngestError::Crypto)?;
102 let req = PushRequest {
103 content: ciphertext,
104 content_type: ContentType::Image.as_wire().into(),
105 label: label.to_string(),
106 source: source.to_string(),
107 media_path: None,
108 byte_size: original_size,
109 encrypted: true,
110 target_device_id: None,
111 };
112 let resp = self.client.push_clip_json(&req).await?;
113 self.write_through(
114 &resp.clip_id,
115 source,
116 ContentType::Image.as_wire(),
117 raw_png,
118 original_size,
119 )?;
120 Ok(resp.clip_id)
121 }
122
123 fn write_through(
124 &self,
125 clip_id: &str,
126 source: &str,
127 content_type: &str,
128 raw: Vec<u8>,
129 byte_size: i64,
130 ) -> Result<(), IngestError> {
131 let stored = StoredClip {
132 id: clip_id.to_string(),
133 source: source.to_string(),
134 source_key: None,
135 content_type: content_type.to_string(),
136 content: Some(raw),
137 media_path: None,
138 byte_size,
139 created_at: chrono::Utc::now().timestamp_millis(),
140 pinned: false,
141 pinned_at: None,
142 };
143 queries::insert_clip(&self.store, &stored)?;
144 let _ = queries::set_watermark(&self.store, clip_id);
146 Ok(())
147 }
148}