use std::sync::Arc;
use crate::crypto;
use crate::http::{HttpError, RestClient};
use crate::rest::{ContentType, PushRequest};
use crate::store::models::StoredClip;
use crate::store::{queries, Store, StoreError};
#[derive(Clone)]
pub struct LocalPusher {
store: Arc<Store>,
client: Arc<RestClient>,
enc_key: Option<[u8; 32]>,
}
#[derive(Debug, thiserror::Error)]
pub enum IngestError {
#[error("no encryption key available — clip dropped (E2EE required)")]
NoEncryptionKey,
#[error("encryption failed: {0}")]
Crypto(String),
#[error("relay push failed: {0}")]
Push(#[from] HttpError),
#[error("local store write failed: {0}")]
Store(#[from] StoreError),
}
impl LocalPusher {
pub fn new(store: Arc<Store>, client: Arc<RestClient>, enc_key: Option<[u8; 32]>) -> Self {
Self {
store,
client,
enc_key,
}
}
pub async fn push_text(
&self,
raw: Vec<u8>,
source: &str,
label: &str,
) -> Result<String, IngestError> {
let key = self.enc_key.ok_or(IngestError::NoEncryptionKey)?;
let original_size = raw.len() as i64;
let ciphertext = crypto::encrypt(&key, &raw).map_err(IngestError::Crypto)?;
let req = PushRequest {
content: ciphertext,
content_type: String::new(), label: label.to_string(),
source: source.to_string(),
media_path: None,
byte_size: original_size,
encrypted: true,
target_device_id: None,
};
let resp = self.client.push_clip_json(&req).await?;
self.write_through(&resp.clip_id, source, "text/plain", raw, original_size)?;
Ok(resp.clip_id)
}
pub async fn push_image_png(
&self,
raw_png: Vec<u8>,
source: &str,
label: &str,
) -> Result<String, IngestError> {
let key = self.enc_key.ok_or(IngestError::NoEncryptionKey)?;
let original_size = raw_png.len() as i64;
let ciphertext = crypto::encrypt(&key, &raw_png).map_err(IngestError::Crypto)?;
let req = PushRequest {
content: ciphertext,
content_type: ContentType::Image.as_wire().into(),
label: label.to_string(),
source: source.to_string(),
media_path: None,
byte_size: original_size,
encrypted: true,
target_device_id: None,
};
let resp = self.client.push_clip_json(&req).await?;
self.write_through(&resp.clip_id, source, "image/png", raw_png, original_size)?;
Ok(resp.clip_id)
}
fn write_through(
&self,
clip_id: &str,
source: &str,
content_type: &str,
raw: Vec<u8>,
byte_size: i64,
) -> Result<(), IngestError> {
let stored = StoredClip {
id: clip_id.to_string(),
source: source.to_string(),
source_key: None,
content_type: content_type.to_string(),
content: Some(raw),
media_path: None,
byte_size,
created_at: chrono::Utc::now().timestamp_millis(),
pinned: false,
pinned_at: None,
};
queries::insert_clip(&self.store, &stored)?;
let _ = queries::set_watermark(&self.store, clip_id);
Ok(())
}
}