Files
inkwell/core/src/sync/sharing.rs
T
bvandeusenandClaude Opus 5.5 3ea7c817bf Core tests open their store through local::memory_conn
Seven test modules each built a migrated in-memory store by hand: open, migrate,
and (in sharing) wrap it in a Db. local::memory_conn() is that, and
open_in_memory uses it too. Each module's db() is now one line, and the schema,
Connection and Mutex imports it needed are gone.

DRY pass #2, batch 2, F9 (#5372).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:24:04 -04:00

135 lines
4.4 KiB
Rust

//! Sharing a note from a linked device (#5175).
//!
//! Shares belong to the server: who is on the instance and who a note is shared
//! with are never kept here, so each call goes straight to the server over the
//! device token. The one thing kept locally is the note's `shared` flag, set from
//! the server's answer so the card's chip changes at once rather than at the next
//! sync (which brings the same value).
//!
//! The server address and token come from the CALLER, read with
//! `state::credentials` and the caller's seal. A stored token may be sealed
//! (Android keeps it under a Keystore key), and reading it raw here sent `sealed:…`
//! as the bearer token, so every share call was refused (#5381).
use rusqlite::params;
use super::client::{self, Directory, NoteShare, ShareTarget};
use super::state;
use crate::local::Db;
/// What an unlinked device says when asked to share. Sharing is between accounts on
/// a server, so there is nothing to do offline and nothing worth queueing.
pub const NEEDS_SERVER: &str =
"Sharing is between people on a server. Link this device to one in Sync to share notes.";
/// The link to share over, or [`NEEDS_SERVER`]. For a client that keeps its token
/// plain, as the desktop does; one with a seal reads `state::credentials` itself.
pub fn stored_link(db: &Db) -> Result<(String, String), String> {
let conn = db.conn()?;
let link = state::credentials(&conn, None).map_err(|e| e.to_string())?;
link.ok_or_else(|| NEEDS_SERVER.to_string())
}
/// Set the local note's `shared` flag from the server's list of its shares. Not a
/// local edit, so it leaves `dirty` and `updated_at` alone.
fn mark_shared(db: &Db, note_id: &str, shares: &[NoteShare]) -> Result<(), String> {
let conn = db.conn()?;
conn.execute(
"UPDATE notes SET shared = ?2 WHERE id = ?1 AND permission = 'owner'",
params![note_id, !shares.is_empty()],
)
.map_err(|e| e.to_string())?;
Ok(())
}
pub async fn directory(url: &str, token: &str) -> Result<Directory, String> {
client::directory(url, token).await
}
pub async fn list(
db: &Db,
url: &str,
token: &str,
note_id: &str,
) -> Result<Vec<NoteShare>, String> {
let shares = client::list_shares(url, token, note_id).await?;
mark_shared(db, note_id, &shares)?;
Ok(shares)
}
pub async fn share(
db: &Db,
url: &str,
token: &str,
note_id: &str,
target: &ShareTarget,
permission: &str,
) -> Result<Vec<NoteShare>, String> {
let shares = client::share_note(url, token, note_id, target, permission).await?;
mark_shared(db, note_id, &shares)?;
Ok(shares)
}
pub async fn unshare(
db: &Db,
url: &str,
token: &str,
note_id: &str,
share_id: &str,
) -> Result<Vec<NoteShare>, String> {
let shares = client::unshare_note(url, token, note_id, share_id).await?;
mark_shared(db, note_id, &shares)?;
Ok(shares)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sync::client::Member;
fn db() -> Db {
crate::local::open_in_memory().expect("in-memory db")
}
#[test]
fn an_unlinked_device_explains_that_sharing_needs_a_server() {
assert_eq!(stored_link(&db()).unwrap_err(), NEEDS_SERVER);
}
#[test]
fn the_shared_flag_follows_the_servers_answer_without_dirtying_the_note() {
let db = db();
{
let conn = db.conn().unwrap();
conn.execute(
"INSERT INTO notes (id, body, created_at, updated_at, dirty)
VALUES ('n1', 'x', '2026-01-01', '2026-01-01', 0)",
[],
)
.unwrap();
}
let one = NoteShare {
id: "s1".into(),
member: Some(Member {
id: "u2".into(),
display_name: "Sam".into(),
email: "sam@example.test".into(),
}),
group: None,
permission: "view".into(),
created_at: None,
};
let read = |db: &Db| -> (bool, i64) {
let conn = db.conn().unwrap();
conn.query_row("SELECT shared, dirty FROM notes WHERE id = 'n1'", [], |r| {
Ok((r.get(0)?, r.get(1)?))
})
.unwrap()
};
mark_shared(&db, "n1", &[one]).unwrap();
assert_eq!(read(&db), (true, 0));
mark_shared(&db, "n1", &[]).unwrap();
assert_eq!(read(&db), (false, 0));
}
}