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(
67 &self,
68 raw: Vec<u8>,
69 source: &str,
70 label: &str,
71 content_type: ContentType,
72 ) -> Result<String, IngestError> {
73 let key = self.enc_key.ok_or(IngestError::NoEncryptionKey)?;
74 let original_size = raw.len() as i64;
75 let ciphertext = crypto::encrypt(&key, &raw).map_err(IngestError::Crypto)?;
76 let wire = content_type.as_wire();
77 let req = PushRequest {
78 content: ciphertext,
79 content_type: wire.to_string(),
80 label: label.to_string(),
81 source: source.to_string(),
82 media_path: None,
83 byte_size: original_size,
84 encrypted: true,
85 target_device_id: None,
86 };
87 let resp = self.client.push_clip_json(&req).await?;
88 self.write_through(&resp.clip_id, source, wire, raw, original_size)?;
89 Ok(resp.clip_id)
90 }
91
92 pub async fn push_image_png(
95 &self,
96 raw_png: Vec<u8>,
97 source: &str,
98 label: &str,
99 ) -> Result<String, IngestError> {
100 let key = self.enc_key.ok_or(IngestError::NoEncryptionKey)?;
101 let original_size = raw_png.len() as i64;
102 let ciphertext = crypto::encrypt(&key, &raw_png).map_err(IngestError::Crypto)?;
103 let req = PushRequest {
104 content: ciphertext,
105 content_type: ContentType::Image.as_wire().into(),
106 label: label.to_string(),
107 source: source.to_string(),
108 media_path: None,
109 byte_size: original_size,
110 encrypted: true,
111 target_device_id: None,
112 };
113 let resp = self.client.push_clip_json(&req).await?;
114 self.write_through(
115 &resp.clip_id,
116 source,
117 ContentType::Image.as_wire(),
118 raw_png,
119 original_size,
120 )?;
121 Ok(resp.clip_id)
122 }
123
124 fn write_through(
125 &self,
126 clip_id: &str,
127 source: &str,
128 content_type: &str,
129 raw: Vec<u8>,
130 byte_size: i64,
131 ) -> Result<(), IngestError> {
132 let stored = StoredClip {
133 id: clip_id.to_string(),
134 source: source.to_string(),
135 source_key: None,
136 content_type: content_type.to_string(),
137 content: Some(raw),
138 media_path: None,
139 byte_size,
140 created_at: chrono::Utc::now().timestamp_millis(),
141 pinned: false,
142 pinned_at: None,
143 };
144 queries::insert_clip(&self.store, &stored)?;
145 let _ = queries::set_watermark(&self.store, clip_id);
147 Ok(())
148 }
149}