Skip to content

Commit af44bc0

Browse files
committed
add cloudsync extension
Signed-off-by: Yujong Lee <yujonglee.dev@gmail.com>
1 parent d9ff2ea commit af44bc0

File tree

18 files changed

+518
-0
lines changed

18 files changed

+518
-0
lines changed

Cargo.lock

Lines changed: 22 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ hypr-cactus-model = { path = "crates/cactus-model", package = "cactus-model" }
6464
hypr-calendar-interface = { path = "crates/calendar-interface", package = "calendar-interface" }
6565
hypr-chatwoot = { path = "crates/chatwoot", package = "chatwoot" }
6666
hypr-cli-tui = { path = "crates/cli-tui", package = "cli-tui" }
67+
hypr-cloudsync = { path = "crates/cloudsync", package = "cloudsync" }
6768
hypr-data = { path = "crates/data", package = "data" }
6869
hypr-db-core = { path = "crates/db-core", package = "db-core" }
6970
hypr-db-parser = { path = "crates/db-parser", package = "db-parser" }
7071
hypr-db-user = { path = "crates/db-user", package = "db-user" }
72+
hypr-db3 = { path = "crates/db3", package = "db3" }
7173
hypr-denoise = { path = "crates/denoise", package = "denoise" }
7274
hypr-detect = { path = "crates/detect", package = "detect" }
7375
hypr-device-monitor = { path = "crates/device-monitor", package = "device-monitor" }

crates/cloudsync/Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[package]
2+
name = "cloudsync"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
dirs = { workspace = true }
8+
sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "sqlite-unbundled"] }
9+
thiserror = { workspace = true }
10+
11+
[dev-dependencies]
12+
tokio = { workspace = true, features = ["macros", "rt"] }

crates/cloudsync/src/bundle.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
4+
use crate::CLOUDSYNC_VERSION;
5+
use crate::error::Error;
6+
7+
macro_rules! configure_cloudsync_target {
8+
($target:literal, $file_name:literal, $path:literal) => {
9+
const CLOUDSYNC_TARGET: &str = $target;
10+
const CLOUDSYNC_FILE_NAME: &str = $file_name;
11+
const BUNDLED_CLOUDSYNC_BYTES: &[u8] = include_bytes!($path);
12+
};
13+
}
14+
15+
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
16+
configure_cloudsync_target!(
17+
"macos/aarch64",
18+
"cloudsync.dylib",
19+
"../vendor/cloudsync/macos/aarch64/cloudsync.dylib"
20+
);
21+
22+
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
23+
configure_cloudsync_target!(
24+
"macos/x86_64",
25+
"cloudsync.dylib",
26+
"../vendor/cloudsync/macos/x86_64/cloudsync.dylib"
27+
);
28+
29+
#[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "aarch64"))]
30+
configure_cloudsync_target!(
31+
"linux/gnu/aarch64",
32+
"cloudsync.so",
33+
"../vendor/cloudsync/linux/gnu/aarch64/cloudsync.so"
34+
);
35+
36+
#[cfg(all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"))]
37+
configure_cloudsync_target!(
38+
"linux/gnu/x86_64",
39+
"cloudsync.so",
40+
"../vendor/cloudsync/linux/gnu/x86_64/cloudsync.so"
41+
);
42+
43+
#[cfg(all(target_os = "linux", target_env = "musl", target_arch = "aarch64"))]
44+
configure_cloudsync_target!(
45+
"linux/musl/aarch64",
46+
"cloudsync.so",
47+
"../vendor/cloudsync/linux/musl/aarch64/cloudsync.so"
48+
);
49+
50+
#[cfg(all(target_os = "linux", target_env = "musl", target_arch = "x86_64"))]
51+
configure_cloudsync_target!(
52+
"linux/musl/x86_64",
53+
"cloudsync.so",
54+
"../vendor/cloudsync/linux/musl/x86_64/cloudsync.so"
55+
);
56+
57+
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
58+
configure_cloudsync_target!(
59+
"windows/x86_64",
60+
"cloudsync.dll",
61+
"../vendor/cloudsync/windows/x86_64/cloudsync.dll"
62+
);
63+
64+
pub fn bundled_extension_path() -> Result<PathBuf, Error> {
65+
#[cfg(not(any(
66+
all(target_os = "macos", target_arch = "aarch64"),
67+
all(target_os = "macos", target_arch = "x86_64"),
68+
all(target_os = "linux", target_env = "gnu", target_arch = "aarch64"),
69+
all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"),
70+
all(target_os = "linux", target_env = "musl", target_arch = "aarch64"),
71+
all(target_os = "linux", target_env = "musl", target_arch = "x86_64"),
72+
all(target_os = "windows", target_arch = "x86_64"),
73+
)))]
74+
{
75+
Err(Error::UnsupportedBundledCloudsync)
76+
}
77+
78+
#[cfg(any(
79+
all(target_os = "macos", target_arch = "aarch64"),
80+
all(target_os = "macos", target_arch = "x86_64"),
81+
all(target_os = "linux", target_env = "gnu", target_arch = "aarch64"),
82+
all(target_os = "linux", target_env = "gnu", target_arch = "x86_64"),
83+
all(target_os = "linux", target_env = "musl", target_arch = "aarch64"),
84+
all(target_os = "linux", target_env = "musl", target_arch = "x86_64"),
85+
all(target_os = "windows", target_arch = "x86_64"),
86+
))]
87+
{
88+
let base_dir = dirs::cache_dir()
89+
.ok_or(Error::MissingCacheDir)?
90+
.join("char")
91+
.join("cloudsync")
92+
.join(CLOUDSYNC_VERSION)
93+
.join(CLOUDSYNC_TARGET);
94+
95+
fs::create_dir_all(&base_dir)?;
96+
97+
let extension_path = base_dir.join(CLOUDSYNC_FILE_NAME);
98+
let needs_write = match fs::metadata(&extension_path) {
99+
Ok(metadata) => metadata.len() != BUNDLED_CLOUDSYNC_BYTES.len() as u64,
100+
Err(_) => true,
101+
};
102+
103+
if needs_write {
104+
let tmp_path =
105+
base_dir.join(format!("{CLOUDSYNC_FILE_NAME}.{}.tmp", std::process::id()));
106+
fs::write(&tmp_path, BUNDLED_CLOUDSYNC_BYTES)?;
107+
108+
#[cfg(unix)]
109+
{
110+
use std::os::unix::fs::PermissionsExt;
111+
112+
fs::set_permissions(&tmp_path, fs::Permissions::from_mode(0o755))?;
113+
}
114+
115+
match fs::rename(&tmp_path, &extension_path) {
116+
Ok(()) => {}
117+
Err(error) if extension_path.exists() => {
118+
let _ = fs::remove_file(&tmp_path);
119+
120+
if fs::metadata(&extension_path)?.len() != BUNDLED_CLOUDSYNC_BYTES.len() as u64
121+
{
122+
return Err(error.into());
123+
}
124+
}
125+
Err(error) => return Err(error.into()),
126+
}
127+
}
128+
129+
Ok(extension_path)
130+
}
131+
}

crates/cloudsync/src/error.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
const SUPPORTED_CLOUDSYNC_TARGETS: &str = concat!(
2+
"macos/{aarch64,x86_64}, ",
3+
"linux/{gnu,musl}/{aarch64,x86_64}, ",
4+
"windows/x86_64"
5+
);
6+
7+
#[derive(Debug, thiserror::Error)]
8+
pub enum Error {
9+
#[error("sqlx error: {0}")]
10+
Sqlx(#[from] sqlx::Error),
11+
#[error("io error: {0}")]
12+
Io(#[from] std::io::Error),
13+
#[error("no cache directory is available for the bundled cloudsync extension")]
14+
MissingCacheDir,
15+
#[error(
16+
"the bundled cloudsync extension is not available for this target; supported targets: {SUPPORTED_CLOUDSYNC_TARGETS}"
17+
)]
18+
UnsupportedBundledCloudsync,
19+
}

crates/cloudsync/src/init.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use sqlx::SqlitePool;
2+
3+
use crate::error::Error;
4+
5+
pub async fn version(pool: &SqlitePool) -> Result<String, Error> {
6+
Ok(sqlx::query_scalar("SELECT cloudsync_version()")
7+
.fetch_one(pool)
8+
.await?)
9+
}
10+
11+
pub async fn init(
12+
pool: &SqlitePool,
13+
table_name: &str,
14+
crdt_algo: Option<&str>,
15+
force: Option<bool>,
16+
) -> Result<(), Error> {
17+
match (crdt_algo, force) {
18+
(None, None) => {
19+
sqlx::query("SELECT cloudsync_init(?)")
20+
.bind(table_name)
21+
.fetch_optional(pool)
22+
.await?;
23+
}
24+
(Some(crdt_algo), None) => {
25+
sqlx::query("SELECT cloudsync_init(?, ?)")
26+
.bind(table_name)
27+
.bind(crdt_algo)
28+
.fetch_optional(pool)
29+
.await?;
30+
}
31+
(None, Some(force)) => {
32+
sqlx::query("SELECT cloudsync_init(?, NULL, ?)")
33+
.bind(table_name)
34+
.bind(force)
35+
.fetch_optional(pool)
36+
.await?;
37+
}
38+
(Some(crdt_algo), Some(force)) => {
39+
sqlx::query("SELECT cloudsync_init(?, ?, ?)")
40+
.bind(table_name)
41+
.bind(crdt_algo)
42+
.bind(force)
43+
.fetch_optional(pool)
44+
.await?;
45+
}
46+
}
47+
48+
Ok(())
49+
}

crates/cloudsync/src/lib.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#![forbid(unsafe_code)]
2+
3+
mod bundle;
4+
mod error;
5+
mod init;
6+
mod network;
7+
8+
use std::path::PathBuf;
9+
10+
use sqlx::sqlite::SqliteConnectOptions;
11+
12+
pub use bundle::bundled_extension_path;
13+
pub use error::Error;
14+
pub use init::{init, version};
15+
pub use network::{network_init, network_set_apikey, network_set_token, network_sync};
16+
17+
pub const CLOUDSYNC_VERSION: &str = "0.8.68";
18+
19+
pub fn apply(options: SqliteConnectOptions) -> Result<(SqliteConnectOptions, PathBuf), Error> {
20+
let extension_path = bundled_extension_path()?;
21+
let options = options.extension(extension_path.to_string_lossy().into_owned());
22+
23+
Ok((options, extension_path))
24+
}
25+
26+
#[cfg(any(
27+
all(test, target_os = "macos", target_arch = "aarch64"),
28+
all(test, target_os = "macos", target_arch = "x86_64"),
29+
all(test, target_os = "linux", target_env = "gnu", target_arch = "aarch64"),
30+
all(test, target_os = "linux", target_env = "gnu", target_arch = "x86_64"),
31+
all(
32+
test,
33+
target_os = "linux",
34+
target_env = "musl",
35+
target_arch = "aarch64"
36+
),
37+
all(test, target_os = "linux", target_env = "musl", target_arch = "x86_64"),
38+
all(test, target_os = "windows", target_arch = "x86_64"),
39+
))]
40+
mod tests {
41+
use super::*;
42+
use std::str::FromStr;
43+
44+
use sqlx::sqlite::SqlitePoolOptions;
45+
46+
#[tokio::test]
47+
async fn loads_bundled_cloudsync() {
48+
let options = SqliteConnectOptions::from_str("sqlite::memory:").unwrap();
49+
let (options, _) = apply(options).unwrap();
50+
let pool = SqlitePoolOptions::new()
51+
.max_connections(1)
52+
.connect_with(options)
53+
.await
54+
.unwrap();
55+
56+
let version = version(&pool).await.unwrap();
57+
58+
assert!(!version.is_empty());
59+
}
60+
}

crates/cloudsync/src/network.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use sqlx::SqlitePool;
2+
3+
use crate::error::Error;
4+
5+
pub async fn network_init(pool: &SqlitePool, connection_string: &str) -> Result<(), Error> {
6+
sqlx::query("SELECT cloudsync_network_init(?)")
7+
.bind(connection_string)
8+
.fetch_optional(pool)
9+
.await?;
10+
11+
Ok(())
12+
}
13+
14+
pub async fn network_set_apikey(pool: &SqlitePool, api_key: &str) -> Result<(), Error> {
15+
sqlx::query("SELECT cloudsync_network_set_apikey(?)")
16+
.bind(api_key)
17+
.fetch_optional(pool)
18+
.await?;
19+
20+
Ok(())
21+
}
22+
23+
pub async fn network_set_token(pool: &SqlitePool, token: &str) -> Result<(), Error> {
24+
sqlx::query("SELECT cloudsync_network_set_token(?)")
25+
.bind(token)
26+
.fetch_optional(pool)
27+
.await?;
28+
29+
Ok(())
30+
}
31+
32+
pub async fn network_sync(
33+
pool: &SqlitePool,
34+
wait_ms: Option<i64>,
35+
max_retries: Option<i64>,
36+
) -> Result<(), Error> {
37+
match (wait_ms, max_retries) {
38+
(None, None) => {
39+
sqlx::query("SELECT cloudsync_network_sync()")
40+
.fetch_optional(pool)
41+
.await?;
42+
}
43+
(Some(wait_ms), None) => {
44+
sqlx::query("SELECT cloudsync_network_sync(?)")
45+
.bind(wait_ms)
46+
.fetch_optional(pool)
47+
.await?;
48+
}
49+
(None, Some(max_retries)) => {
50+
sqlx::query("SELECT cloudsync_network_sync(NULL, ?)")
51+
.bind(max_retries)
52+
.fetch_optional(pool)
53+
.await?;
54+
}
55+
(Some(wait_ms), Some(max_retries)) => {
56+
sqlx::query("SELECT cloudsync_network_sync(?, ?)")
57+
.bind(wait_ms)
58+
.bind(max_retries)
59+
.fetch_optional(pool)
60+
.await?;
61+
}
62+
}
63+
64+
Ok(())
65+
}
Binary file not shown.
583 KB
Binary file not shown.

0 commit comments

Comments
 (0)