Skip to main content

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}