CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 35s
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m29s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m47s
The client half of task 1942's server work. Metadata already rides the delta feed; this fetches the payload so a synced image exists on the device. Blobs are filed under their own sha256, so the same image attached to five notes is stored once and re-downloading it is free — the dedupe the task asks for falls out of content addressing rather than needing bookkeeping. The hash is also the integrity check, applied on the way IN. Bytes that don't hash to what the server advertised are refused rather than filed under a name that lies about them — and because the blob then still counts as missing, the next sync simply tries again. SECURITY: the hash arrives in a server response and becomes a FILENAME, so it is validated as 64 hex characters before touching the filesystem. Without that, a hostile or buggy server could send "../../..." and steer a write outside the blob directory. Tested. A failed attachment never fails the sync. Notes are the primary data and have already landed; aborting here would let one unreachable file block every future sync. Counted, logged, surfaced in the UI as "they'll retry on the next sync", and retried because the blob is still absent. sha2 is pure Rust, so the Windows cross-compile lane pays nothing for it — the constraint recorded in ci-requirements.md. SPLIT, deliberately: this stores the bytes but does NOT yet render them in the webview. That half needs a custom URI scheme or the asset protocol, whose URL form differs by platform (Windows uses http://scheme.localhost/, others scheme://localhost/) — and CI cannot verify webview rendering at all, being headless with no webview. Guessing at it here would ship an unverifiable change on the most fragile lane. Follow-up filed; synced images will show as broken until it lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
184 lines
7.8 KiB
Rust
184 lines
7.8 KiB
Rust
//! ThoughtSync desktop (Tauri v2).
|
|
//!
|
|
//! The window loads the shared Vue 3 frontend (`../../frontend`). The Rust core will
|
|
//! own the on-device SQLite store (M10.4) and the opt-in sync engine (M10.7), which
|
|
//! the frontend reaches through the `frontend/src/adapters/` seam (M10.3) over Tauri
|
|
//! `invoke`. Today it exposes desktop integration (menu-entry install for the Linux
|
|
//! AppImage) and boots the UI.
|
|
|
|
mod integration;
|
|
mod local;
|
|
// `pub` (unlike the modules above) because parts of it have no in-crate caller yet —
|
|
// the engine that will consume them is M10.7b/c, and a private module's unreachable
|
|
// items read as dead code.
|
|
pub mod sync;
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
use tauri_plugin_log::{Target, TargetKind};
|
|
|
|
#[cfg(target_os = "linux")]
|
|
harden_linux_webkit_rendering();
|
|
|
|
tauri::Builder::default()
|
|
// Logging first, so startup diagnostics (and any setup error) are captured to
|
|
// stdout AND a persistent file from the very beginning — the basis for
|
|
// troubleshooting portability across environments.
|
|
.plugin(
|
|
tauri_plugin_log::Builder::new()
|
|
.level(log::LevelFilter::Info)
|
|
.targets([
|
|
Target::new(TargetKind::Stdout),
|
|
Target::new(TargetKind::LogDir { file_name: None }),
|
|
])
|
|
.build(),
|
|
)
|
|
.setup(|app| {
|
|
use tauri::Manager;
|
|
log_environment(app);
|
|
// The on-device store lives in the platform app-data dir (e.g. Linux
|
|
// ~/.local/share/com.fabledsword.thoughtsync/thoughtsync.db), created on
|
|
// first launch. This is what makes the app work with no server or login.
|
|
let dir = app.path().app_data_dir()?;
|
|
std::fs::create_dir_all(&dir)?;
|
|
let db_path = dir.join("thoughtsync.db");
|
|
log::info!("opening local store: {}", db_path.display());
|
|
let db = local::open(&db_path)?;
|
|
log::info!("local store ready — {}", local::summary(&db));
|
|
app.manage(db);
|
|
// Attachment bytes live beside the database, filed by content hash, so a
|
|
// synced image is readable with no network (M10.7d).
|
|
let blobs = sync::blobs::BlobStore::new(dir.join("blobs"))?;
|
|
log::info!("attachment store ready: {}", blobs.root().display());
|
|
app.manage(blobs);
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
log_event,
|
|
integration::integration_status,
|
|
integration::integrate_desktop,
|
|
integration::unintegrate_desktop,
|
|
local::commands::config_get,
|
|
local::commands::auth_me,
|
|
local::commands::notes_list,
|
|
local::commands::notes_get,
|
|
local::commands::notes_create,
|
|
local::commands::notes_create_titled,
|
|
local::commands::notes_update,
|
|
local::commands::notes_complete_reminder,
|
|
local::commands::notes_snooze_reminder,
|
|
local::commands::notes_set_labels,
|
|
local::commands::notes_add_item,
|
|
local::commands::notes_update_item,
|
|
local::commands::notes_delete_item,
|
|
local::commands::notes_delete_attachment,
|
|
local::commands::notes_delete_preview,
|
|
local::commands::notes_reorder,
|
|
local::commands::notes_trash,
|
|
local::commands::notes_restore,
|
|
local::commands::notes_delete_forever,
|
|
local::commands::notes_revisions,
|
|
local::commands::notes_restore_revision,
|
|
local::commands::notes_reminders,
|
|
local::commands::notes_titles,
|
|
local::commands::notes_search,
|
|
local::commands::notes_backlinks,
|
|
local::commands::notes_link_search,
|
|
local::commands::labels_list,
|
|
local::commands::labels_create,
|
|
local::commands::labels_rename,
|
|
local::commands::labels_set_color,
|
|
local::commands::labels_remove,
|
|
local::commands::labels_merge,
|
|
local::commands::saved_filters_list,
|
|
local::commands::saved_filters_create,
|
|
local::commands::saved_filters_remove,
|
|
local::commands::saved_filters_rename,
|
|
sync::commands::sync_probe,
|
|
sync::commands::sync_link,
|
|
sync::commands::sync_unlink,
|
|
sync::commands::sync_status,
|
|
sync::commands::sync_now,
|
|
sync::commands::sync_has_pending,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running the ThoughtSync desktop app");
|
|
}
|
|
|
|
/// Frontend logging bridge: routes boot milestones and errors from the webview into
|
|
/// the same stdout + file log as the Rust side (see frontend/src/desktop/bridge.ts).
|
|
#[tauri::command]
|
|
fn log_event(level: String, message: String) {
|
|
match level.as_str() {
|
|
"error" => log::error!(target: "frontend", "{message}"),
|
|
"warn" => log::warn!(target: "frontend", "{message}"),
|
|
"debug" => log::debug!(target: "frontend", "{message}"),
|
|
_ => log::info!(target: "frontend", "{message}"),
|
|
}
|
|
}
|
|
|
|
/// Log the app version and the environment that determines whether the window
|
|
/// renders — the first thing to check when a build works on one machine but not
|
|
/// another.
|
|
fn log_environment(app: &tauri::App) {
|
|
use tauri::Manager;
|
|
log::info!(
|
|
"ThoughtSync desktop v{} starting ({} {})",
|
|
env!("CARGO_PKG_VERSION"),
|
|
std::env::consts::OS,
|
|
std::env::consts::ARCH,
|
|
);
|
|
match app.path().app_log_dir() {
|
|
Ok(d) => log::info!("log directory: {}", d.display()),
|
|
Err(e) => log::warn!("could not resolve log dir: {e}"),
|
|
}
|
|
#[cfg(target_os = "linux")]
|
|
log_linux_graphics_env();
|
|
}
|
|
|
|
/// The Linux display + graphics stack, and the WebKit render-hardening vars actually
|
|
/// in effect (set by harden_linux_webkit_rendering, which runs before the logger, so
|
|
/// we report the resulting environment rather than logging from inside it).
|
|
#[cfg(target_os = "linux")]
|
|
fn log_linux_graphics_env() {
|
|
let v = |k: &str| std::env::var(k).unwrap_or_else(|_| "(unset)".to_string());
|
|
log::info!(
|
|
"display: session_type={} desktop={} wayland={} x11={} gdk_backend={}",
|
|
v("XDG_SESSION_TYPE"),
|
|
v("XDG_CURRENT_DESKTOP"),
|
|
v("WAYLAND_DISPLAY"),
|
|
v("DISPLAY"),
|
|
v("GDK_BACKEND"),
|
|
);
|
|
log::info!(
|
|
"webkit hardening: dmabuf_disabled={} compositing_disabled={} nv_explicit_sync_disabled={}",
|
|
v("WEBKIT_DISABLE_DMABUF_RENDERER"),
|
|
v("WEBKIT_DISABLE_COMPOSITING_MODE"),
|
|
v("__NV_DISABLE_EXPLICIT_SYNC"),
|
|
);
|
|
}
|
|
|
|
/// WebKitGTK's GPU-accelerated rendering (the DMA-BUF renderer + EGL compositing) fails
|
|
/// to initialize on a wide range of Linux GPU/driver/Wayland setups — "Could not create
|
|
/// default EGL display: EGL_BAD_PARAMETER" → a black/blank window. This is a well-known
|
|
/// WebKitGTK issue that hits Tauri apps broadly, NOT app-specific. Tauri's guidance
|
|
/// (https://v2.tauri.app/develop/debug/linux-graphics/) is to force the software
|
|
/// fallbacks at startup, before the webview is created, so end users don't have to.
|
|
///
|
|
/// Applied on all Linux launches (this UI doesn't need GPU compositing, and the failure
|
|
/// spans AppImage, native, and dev builds), each var left overridable so a user can
|
|
/// re-enable acceleration by exporting it themselves before launch.
|
|
#[cfg(target_os = "linux")]
|
|
fn harden_linux_webkit_rendering() {
|
|
// Ordered per Tauri's escalation ladder; each set only if the user hasn't chosen.
|
|
for (key, value) in [
|
|
("__NV_DISABLE_EXPLICIT_SYNC", "1"),
|
|
("WEBKIT_DISABLE_DMABUF_RENDERER", "1"),
|
|
("WEBKIT_DISABLE_COMPOSITING_MODE", "1"),
|
|
] {
|
|
if std::env::var_os(key).is_none() {
|
|
std::env::set_var(key, value);
|
|
}
|
|
}
|
|
}
|