mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
B69 — spurious `(sync-conflict)` copies came from `(mtime, size)` being used as a revision identity. The server re-stamps a sealed revision with its own commit time, so the drain diverted a write into a conflict copy of its own base; and `plan_file` classified a no-baseline (local, remote) pair as `Conflict`, forking every pre-existing file on first reconcile. - `Baseline` carries `revision_id` (from the SDK's new `active_revision_id`), captured at write-open and preserved across supersede/rebaseline. `revision_changed` trusts the id and only falls back to `(mtime, size)` for old sidecars. - Planner gains `FilePlan::AdoptBaseline`: equal-size no-baseline pairs are recorded, not conflicted. Divergent sizes stay a real conflict. - New `sweep.rs` (`pdfs-conflict-sweep`, 5-min cadence) removes conflict copies proven identical by size + `content_sha1` and surfaces divergent ones once as an `ActivityKind::Conflict` entry. Never removes what it cannot prove. B70 — browsers downloading into an on-demand mount had their in-flight temps sealed as canonical revisions, exiling the finished file to a conflict copy. `syncignore::is_transient_name` now recognises download/scratch temps, and the on-demand `release`/`rename` paths hold them (`set_create_hold`) instead of sealing until the name is finalized. Workspace deps bumped to proton-sdk/proton-drive-rs 0.2.2 (registry, no patch). Live validation against a real account still pending for both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V8P42asymU158CPnhjRwri
129 lines
4.9 KiB
Rust
129 lines
4.9 KiB
Rust
//! Unified SQLite metadata cache — the single persistence layer behind FUSE
|
|
//! inode bookkeeping, full-text search, content-cache LRU tracking, and pins.
|
|
//!
|
|
//! Only the daemon (`pdfs-fuse`) opens this for writes; the GUI and CLI reach
|
|
//! the same data through the control socket. The connection is wrapped in a
|
|
//! `Mutex` because the FUSE callbacks are synchronous and already serialize
|
|
//! behind the `State` lock, so a connection pool would be overkill.
|
|
//!
|
|
//! This module is the P0 foundation: it opens the database, enables WAL, and
|
|
//! applies the forward-only schema migrations. Write-through of nodes, the
|
|
//! event cursor, FTS, and the cache index land in later phases on this schema.
|
|
//!
|
|
//! The surface is one `Db` type; its methods live in a submodule per table group
|
|
//! (`nodes`, `photos`, `pins`, …) as separate `impl Db` blocks. Everything those
|
|
//! modules define is re-exported here, so callers keep saying `db::StoredPhoto`
|
|
//! and never name a submodule.
|
|
|
|
use parking_lot::Mutex;
|
|
use rusqlite::Connection;
|
|
|
|
use crate::Result;
|
|
use std::path::Path;
|
|
|
|
mod activity;
|
|
mod cache;
|
|
mod devices;
|
|
mod local;
|
|
mod maintenance;
|
|
mod migrations;
|
|
mod nodes;
|
|
mod ops;
|
|
mod photos;
|
|
mod pins;
|
|
mod state;
|
|
mod sync;
|
|
mod trash;
|
|
mod utils;
|
|
|
|
pub use cache::CacheEntryInput;
|
|
pub use devices::StoredDevice;
|
|
pub use local::LocalFileHit;
|
|
pub use maintenance::{DbStats, VacuumOutcome};
|
|
pub use nodes::{SearchHit, StoredNode};
|
|
pub use ops::{
|
|
AttachedBlob, LOCAL_VOLUME, OP_CREATE, OP_MKDIR, OP_RENAME, OP_REVISION, OP_TRASH, PARK_UNTIL,
|
|
PendingCounts, PendingOp, op_supersedes,
|
|
};
|
|
pub use photos::{StoredPhoto, THUMB_HAVE, THUMB_NONE, THUMB_UNKNOWN};
|
|
pub use sync::{StoredSyncEntry, StoredSyncFolder};
|
|
pub use trash::StoredTrash;
|
|
|
|
/// Size the WAL is truncated back to after a checkpoint. Comfortably above the
|
|
/// steady-state working set (a few MB), so the truncation only claws back the
|
|
/// outliers rather than fighting the normal write path for disk.
|
|
const WAL_SIZE_LIMIT: i64 = 64 * 1024 * 1024;
|
|
|
|
/// How long a statement waits for a database lock before giving up.
|
|
///
|
|
/// Long enough to outlast any transaction this daemon writes (the largest, a
|
|
/// full-index FTS rebuild, is well under a second), short enough that a
|
|
/// genuinely stuck holder surfaces as an error rather than an apparent hang.
|
|
const BUSY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
|
|
|
/// Handle to the unified metadata database.
|
|
///
|
|
/// Cheap to wrap in an `Arc`; clone the `Arc`, not this. All access goes through
|
|
/// the inner `Mutex<Connection>`.
|
|
pub struct Db {
|
|
conn: Mutex<Connection>,
|
|
}
|
|
|
|
impl Db {
|
|
/// Open (creating if absent) the database at `path`, enable WAL, and bring
|
|
/// the schema up to [`SCHEMA_VERSION`].
|
|
pub fn open(path: &Path) -> Result<Self> {
|
|
let conn = Connection::open(path)?;
|
|
// WAL: readers never block the single writer. NORMAL sync is the
|
|
// standard durability/throughput tradeoff for WAL.
|
|
conn.pragma_update(None, "journal_mode", "WAL")?;
|
|
conn.pragma_update(None, "synchronous", "NORMAL")?;
|
|
conn.pragma_update(None, "foreign_keys", "ON")?;
|
|
// Hand the WAL's disk back after a checkpoint. Without a limit SQLite
|
|
// reuses the file in place but never shrinks it, so a single large
|
|
// transaction (a full-index FTS rebuild in `local_finish_scan` is the
|
|
// one that reaches this size) leaves the WAL at its high-water mark
|
|
// forever — a multi-GB file next to a database two orders of magnitude
|
|
// smaller. Checkpointing is unaffected; only the file is truncated back.
|
|
conn.pragma_update(None, "journal_size_limit", WAL_SIZE_LIMIT)?;
|
|
// Wait for a lock rather than failing instantly on SQLITE_BUSY.
|
|
//
|
|
// This is deliberately explicit and deliberately redundant: rusqlite
|
|
// already applies a five-second busy timeout of its own, so this changes
|
|
// nothing today. It is here so the value is *ours* — a behaviour the
|
|
// daemon depends on should not be an inherited default that a dependency
|
|
// bump can silently change. `open_bounds_the_wal_size` pins it.
|
|
//
|
|
// (Bare SQLite does default to 0, which is where the belief that this
|
|
// was unset came from. That default is not what we get.)
|
|
conn.busy_timeout(BUSY_TIMEOUT)?;
|
|
|
|
let db = Self {
|
|
conn: Mutex::new(conn),
|
|
};
|
|
db.migrate()?;
|
|
Ok(db)
|
|
}
|
|
|
|
/// Open an in-memory database. For tests.
|
|
#[cfg(test)]
|
|
pub fn open_in_memory() -> Result<Self> {
|
|
let conn = Connection::open_in_memory()?;
|
|
let db = Self {
|
|
conn: Mutex::new(conn),
|
|
};
|
|
db.migrate()?;
|
|
Ok(db)
|
|
}
|
|
|
|
/// Run a closure with the locked connection. Escape hatch for callers that
|
|
/// need a query no typed method covers yet.
|
|
pub fn with_conn<T>(&self, f: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {
|
|
let conn = self.conn.lock();
|
|
f(&conn)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|