feat(sync): sync_targets CRUD in the storage layer (T9.1)

This commit is contained in:
iamdoubz committed 2026-07-02 20:17:02 -05:00
1 parent 197092220c
commit bb7f704e16
1 file changed
+116
+116
View File
@@ -128,6 +128,30 @@ pub struct Retention {
pub max_size_gb: Option<u32>,
}
/// A configured upload destination as stored in the DB (ADR-0010). Holds only a
/// `credential_ref` into the OS credential store — never the secret itself
/// (FR-SYNC-6). Maps 1:1 to the `sync_targets` table.
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct SyncTargetRow {
pub id: String,
pub name: String,
pub kind: String, // webdav|onedrive|dropbox|box
pub provider_hint: Option<String>,
pub base_url: Option<String>,
pub remote_base_path: String,
pub username: Option<String>,
pub credential_ref: String,
pub enabled: bool,
pub upload_transcript: bool,
pub upload_notes: bool,
pub upload_summary: bool,
pub upload_recording: bool,
pub trigger_on_finalize: bool,
pub allow_plaintext_lan: bool,
pub encrypt_before_upload: bool,
pub created_at: i64,
}
#[async_trait]
pub trait Store: Send + Sync {
async fn create_meeting(&self, m: NewMeeting) -> Result<MeetingId, StoreError>;
@@ -226,6 +250,14 @@ pub trait Store: Send + Sync {
/// Enforce retention; returns count removed. Skips in-progress meetings
/// (anything whose `status` isn't a terminal `ready`/`error`).
async fn enforce_retention(&self, policy: Retention) -> Result<u32, StoreError>;
// ---- Sync targets (Phase 9, ADR-0010) ----
async fn add_sync_target(&self, row: SyncTargetRow) -> Result<(), StoreError>;
/// Full-row replace (caller loads, edits, and preserves `credential_ref`).
async fn update_sync_target(&self, row: SyncTargetRow) -> Result<(), StoreError>;
async fn remove_sync_target(&self, id: &str) -> Result<(), StoreError>;
async fn list_sync_targets(&self) -> Result<Vec<SyncTargetRow>, StoreError>;
async fn get_sync_target(&self, id: &str) -> Result<SyncTargetRow, StoreError>;
}
/// SQLite-backed store. Migrations live in `migrations/` (`sqlx::migrate!`).
@@ -1189,6 +1221,90 @@ impl Store for SqliteStore {
Ok(removed)
}
async fn add_sync_target(&self, row: SyncTargetRow) -> Result<(), StoreError> {
sqlx::query(
"INSERT INTO sync_targets (id, name, kind, provider_hint, base_url, remote_base_path, \
username, credential_ref, enabled, upload_transcript, upload_notes, upload_summary, \
upload_recording, trigger_on_finalize, allow_plaintext_lan, encrypt_before_upload, created_at) \
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
)
.bind(&row.id)
.bind(&row.name)
.bind(&row.kind)
.bind(&row.provider_hint)
.bind(&row.base_url)
.bind(&row.remote_base_path)
.bind(&row.username)
.bind(&row.credential_ref)
.bind(row.enabled)
.bind(row.upload_transcript)
.bind(row.upload_notes)
.bind(row.upload_summary)
.bind(row.upload_recording)
.bind(row.trigger_on_finalize)
.bind(row.allow_plaintext_lan)
.bind(row.encrypt_before_upload)
.bind(row.created_at)
.execute(&self.pool)
.await?;
Ok(())
}
async fn update_sync_target(&self, row: SyncTargetRow) -> Result<(), StoreError> {
sqlx::query(
"UPDATE sync_targets SET name=?, kind=?, provider_hint=?, base_url=?, remote_base_path=?, \
username=?, credential_ref=?, enabled=?, upload_transcript=?, upload_notes=?, \
upload_summary=?, upload_recording=?, trigger_on_finalize=?, allow_plaintext_lan=?, \
encrypt_before_upload=? WHERE id=?",
)
.bind(&row.name)
.bind(&row.kind)
.bind(&row.provider_hint)
.bind(&row.base_url)
.bind(&row.remote_base_path)
.bind(&row.username)
.bind(&row.credential_ref)
.bind(row.enabled)
.bind(row.upload_transcript)
.bind(row.upload_notes)
.bind(row.upload_summary)
.bind(row.upload_recording)
.bind(row.trigger_on_finalize)
.bind(row.allow_plaintext_lan)
.bind(row.encrypt_before_upload)
.bind(&row.id)
.execute(&self.pool)
.await?;
Ok(())
}
async fn remove_sync_target(&self, id: &str) -> Result<(), StoreError> {
// ON DELETE CASCADE clears this target's sync_jobs too.
sqlx::query("DELETE FROM sync_targets WHERE id = ?")
.bind(id)
.execute(&self.pool)
.await?;
Ok(())
}
async fn list_sync_targets(&self) -> Result<Vec<SyncTargetRow>, StoreError> {
let rows = sqlx::query_as::<_, SyncTargetRow>(
"SELECT * FROM sync_targets ORDER BY created_at ASC",
)
.fetch_all(&self.pool)
.await?;
Ok(rows)
}
async fn get_sync_target(&self, id: &str) -> Result<SyncTargetRow, StoreError> {
let row = sqlx::query_as::<_, SyncTargetRow>("SELECT * FROM sync_targets WHERE id = ?")
.bind(id)
.fetch_optional(&self.pool)
.await?
.ok_or_else(|| StoreError::NotFound(format!("sync target {id}")))?;
Ok(row)
}
}
fn dir_size(path: &str) -> u64 {