client_core/sync/map.rs
1use crate::protocol::Clip;
2use crate::store::models::StoredClip;
3
4/// Convert a wire [`Clip`] into a [`StoredClip`].
5///
6/// **The HTTP layer does not decrypt.** Callers must decrypt any
7/// `clip.encrypted == true` clips before calling this function. Passing
8/// ciphertext here will store it verbatim and break FTS5 search and downstream
9/// rendering.
10///
11/// Returns `Ok(None)` for rows that should be skipped without surfacing an
12/// error (e.g. a clip with an empty ID).
13pub fn clip_wire_to_stored(c: &Clip) -> Result<Option<StoredClip>, String> {
14 if c.clip_id.is_empty() {
15 return Ok(None);
16 }
17
18 // Wire `created_at` is RFC 3339; convert to unix milliseconds.
19 let created_at = chrono::DateTime::parse_from_rfc3339(&c.created_at)
20 .map_err(|e| format!("bad created_at {:?}: {e}", c.created_at))?
21 .timestamp_millis();
22
23 // Wire `content` is a plain String (text clips) or base64-encoded bytes
24 // (binary clips). Store it as raw bytes so the local store is type-agnostic.
25 let content: Option<Vec<u8>> = if c.content.is_empty() {
26 None
27 } else {
28 Some(c.content.as_bytes().to_vec())
29 };
30
31 Ok(Some(StoredClip {
32 id: c.clip_id.clone(),
33 source: c.source.clone(),
34 // source_key is not present on the wire Clip; populated later if needed.
35 source_key: None,
36 content_type: c.content_type.clone(),
37 content,
38 media_path: c.media_path.clone(),
39 byte_size: c.byte_size,
40 created_at,
41 pinned: c.is_pinned,
42 // pinned_at is not present on the wire Clip.
43 pinned_at: None,
44 }))
45}