Skip to main content

client_core/store/
migration.rs

1use super::{Store, StoreError};
2use std::path::{Path, PathBuf};
3
4/// Returns the platform-specific path where the desktop app stored its
5/// SQLite database before consolidation.
6pub fn legacy_desktop_db_path() -> Option<PathBuf> {
7    let home = dirs::home_dir()?;
8    #[cfg(target_os = "macos")]
9    return Some(home.join("Library/Application Support/com.cinchcli.desktop/cinch.db"));
10    #[cfg(target_os = "linux")]
11    return Some(home.join(".local/share/com.cinchcli.desktop/cinch.db"));
12    #[cfg(target_os = "windows")]
13    {
14        if let Some(appdata) = std::env::var_os("APPDATA") {
15            return Some(
16                PathBuf::from(appdata)
17                    .join("com.cinchcli.desktop")
18                    .join("cinch.db"),
19            );
20        }
21        return None;
22    }
23    #[allow(unreachable_code)]
24    None
25}
26
27/// Import the old desktop DB into the new store if present. Idempotent —
28/// safe to call multiple times; it short-circuits once `meta.migrated_from`
29/// is set.
30///
31/// Returns the path that was imported, if any.
32pub fn import_legacy_if_present(
33    store: &Store,
34    new_media_root: &Path,
35    legacy_path: Option<&Path>,
36) -> Result<Option<PathBuf>, StoreError> {
37    let already: Option<String> = store.with_conn(|c| {
38        c.query_row(
39            "SELECT value FROM meta WHERE key='migrated_from'",
40            [],
41            |r| r.get(0),
42        )
43        .map(Some)
44        .or_else(|e| match e {
45            rusqlite::Error::QueryReturnedNoRows => Ok(None),
46            other => Err(other),
47        })
48    })?;
49    if already.is_some() {
50        return Ok(None);
51    }
52
53    let path = match legacy_path {
54        Some(p) => p.to_path_buf(),
55        None => match legacy_desktop_db_path() {
56            Some(p) => p,
57            None => return Ok(None),
58        },
59    };
60    if !path.exists() {
61        return Ok(None);
62    }
63
64    // Run import in one transaction.
65    store.with_conn(|conn| {
66        conn.execute_batch(&format!(
67            "ATTACH DATABASE {p} AS old;
68             BEGIN;
69             INSERT OR REPLACE INTO clips
70               (id, source, source_key, content_type, content, media_path, byte_size, created_at, pinned, pinned_at)
71               SELECT id, source, source_key, content_type, content, media_path, byte_size, created_at,
72                      COALESCE(pinned, 0), pinned_at
73                 FROM old.clips;
74             INSERT OR REPLACE INTO devices
75               SELECT id, hostname, nickname, source_key, machine_id, public_key,
76                      paired_at, last_push_at, online, refreshed_at FROM old.devices;
77             INSERT OR REPLACE INTO retention_prefs SELECT device_id, days FROM old.retention_prefs;
78             INSERT OR REPLACE INTO alert_prefs     SELECT source, enabled  FROM old.alert_prefs;
79             INSERT OR REPLACE INTO meta(key, value)
80               VALUES('migrated_from', {p});
81             COMMIT;
82             DETACH DATABASE old;",
83            p = sql_literal(&path.to_string_lossy())
84        ))?;
85        Ok(())
86    })?;
87
88    // Move media files alongside the new DB.
89    if let Some(parent) = path.parent() {
90        let old_media = parent.join("media");
91        if old_media.exists() {
92            std::fs::create_dir_all(new_media_root)?;
93            for entry in std::fs::read_dir(&old_media)? {
94                let e = entry?;
95                let dest = new_media_root.join(e.file_name());
96                if std::fs::rename(e.path(), &dest).is_err() {
97                    std::fs::copy(e.path(), &dest)?;
98                }
99            }
100        }
101    }
102
103    // Rename old DB to .bak for recovery.
104    // Use explicit string concatenation to get `cinch.db.bak` (not `cinch.bak`).
105    let bak = path.parent().unwrap_or_else(|| Path::new("")).join(format!(
106        "{}.bak",
107        path.file_name().unwrap_or_default().to_string_lossy()
108    ));
109    let _ = std::fs::rename(&path, bak);
110    Ok(Some(path))
111}
112
113fn sql_literal(s: &str) -> String {
114    format!("'{}'", s.replace('\'', "''"))
115}