Make the sync server client an optional feature (#3216)

* Make the sync server client an optional feature

* fix comment, remove unnecessary allow(dead_code)
This commit is contained in:
Dustin J. Mitchell 2023-12-24 08:57:37 -05:00 committed by GitHub
parent e95f95eb08
commit b52248f146
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 85 additions and 23 deletions

View file

@ -1,5 +1,3 @@
## Run the TaskChampion tests, using both the minimum supported rust version
## and the latest stable Rust.
name: tests - rust
@ -11,6 +9,42 @@ on:
types: [opened, reopened, synchronize]
jobs:
## Run the `taskchampion` crate's tests with various combinations of features.
features:
strategy:
matrix:
features:
- ""
- "server-sync"
name: "taskchampion ${{ matrix.features == '' && 'with no features' || format('with features {0}', matrix.features) }}"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ubuntu-latest-stable-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ubuntu-latest-stable-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: test
run: cargo test -p taskchampion --no-default-features --features "${{ matrix.features }}"
## Run all TaskChampion crate tests, using both the minimum supported rust version
## and the latest stable Rust.
test:
strategy:
matrix:

View file

@ -7,7 +7,7 @@ publish = false
build = "build.rs"
[dependencies]
taskchampion = { path = "../taskchampion" }
taskchampion = { path = "../taskchampion", features = ["server-sync"] }
taskchampion-lib = { path = "../lib" }
taskchampion-sync-server = { path = "../sync-server" }

View file

@ -10,6 +10,14 @@ readme = "../README.md"
license = "MIT"
edition = "2018"
[features]
default = ["server-sync" ]
server-sync = ["crypto", "dep:ureq"]
crypto = ["dep:ring"]
[package.metadata.docs.rs]
all-features = true
[dependencies]
uuid.workspace = true
serde.workspace = true
@ -26,6 +34,9 @@ flate2.workspace = true
byteorder.workspace = true
ring.workspace = true
ureq.optional = true
ring.optional = true
[dev-dependencies]
proptest.workspace = true
tempfile.workspace = true

View file

@ -34,6 +34,7 @@ macro_rules! other_error {
}
};
}
#[cfg(feature = "server-sync")]
other_error!(ureq::Error);
other_error!(io::Error);
other_error!(serde_json::Error);

View file

@ -34,6 +34,14 @@ Create a server with [`ServerConfig`](crate::ServerConfig).
The [`server`](crate::server) module defines the interface a server must meet.
Users can define their own server impelementations.
# Feature Flags
Support for some optional functionality is controlled by feature flags.
Sync server client support:
* `server-sync` - sync to the taskchampion-sync-server
# See Also
See the [TaskChampion Book](http://taskchampion.github.com/taskchampion)

View file

@ -1,7 +1,10 @@
use super::types::Server;
use super::{LocalServer, RemoteServer};
use crate::errors::Result;
use crate::server::local::LocalServer;
#[cfg(feature = "server-sync")]
use crate::server::sync::SyncServer;
use std::path::PathBuf;
#[cfg(feature = "server-sync")]
use uuid::Uuid;
/// The configuration for a replica's access to a sync server.
@ -12,6 +15,7 @@ pub enum ServerConfig {
server_dir: PathBuf,
},
/// A remote taskchampion-sync-server instance
#[cfg(feature = "server-sync")]
Remote {
/// Sync server "origin"; a URL with schema and hostname but no path or trailing `/`
origin: String,
@ -30,11 +34,12 @@ impl ServerConfig {
pub fn into_server(self) -> Result<Box<dyn Server>> {
Ok(match self {
ServerConfig::Local { server_dir } => Box::new(LocalServer::new(server_dir)?),
#[cfg(feature = "server-sync")]
ServerConfig::Remote {
origin,
client_id,
encryption_secret,
} => Box::new(RemoteServer::new(origin, client_id, encryption_secret)?),
} => Box::new(SyncServer::new(origin, client_id, encryption_secret)?),
})
}
}

View file

@ -2,7 +2,6 @@
/// document.
use crate::errors::{Error, Result};
use ring::{aead, digest, pbkdf2, rand, rand::SecureRandom};
use std::io::Read;
use uuid::Uuid;
const PBKDF2_ITERATIONS: u32 = 100000;
@ -177,11 +176,13 @@ pub(super) struct Sealed {
}
impl Sealed {
#[cfg(feature = "server-sync")]
pub(super) fn from_resp(
resp: ureq::Response,
version_id: Uuid,
content_type: &str,
) -> Result<Sealed> {
use std::io::Read;
if resp.header("Content-Type") == Some(content_type) {
let mut reader = resp.into_reader();
let mut payload = vec![];

View file

@ -12,15 +12,17 @@ However, users who wish to implement their own server interfaces can implement t
pub(crate) mod test;
mod config;
mod crypto;
mod local;
mod op;
mod remote;
mod types;
#[cfg(feature = "crypto")]
mod crypto;
#[cfg(feature = "server-sync")]
mod sync;
pub use config::ServerConfig;
pub use local::LocalServer;
pub use remote::RemoteServer;
pub use types::*;
pub(crate) use op::SyncOp;

View file

@ -8,7 +8,7 @@ use uuid::Uuid;
use super::crypto::{Cryptor, Sealed, Secret, Unsealed};
pub struct RemoteServer {
pub struct SyncServer {
origin: String,
client_id: Uuid,
cryptor: Cryptor,
@ -21,19 +21,14 @@ const HISTORY_SEGMENT_CONTENT_TYPE: &str = "application/vnd.taskchampion.history
/// The content-type for snapshots (opaque blobs of bytes)
const SNAPSHOT_CONTENT_TYPE: &str = "application/vnd.taskchampion.snapshot";
/// A RemoeServer communicates with a remote server over HTTP (such as with
/// taskchampion-sync-server).
impl RemoteServer {
/// Construct a new RemoteServer. The `origin` is the sync server's protocol and hostname
/// A SyncServer communicates with a sync server over HTTP.
impl SyncServer {
/// Construct a new SyncServer. The `origin` is the sync server's protocol and hostname
/// without a trailing slash, such as `https://tcsync.example.com`. Pass a client_id to
/// identify this client to the server. Multiple replicas synchronizing the same task history
/// should use the same client_id.
pub fn new(
origin: String,
client_id: Uuid,
encryption_secret: Vec<u8>,
) -> Result<RemoteServer> {
Ok(RemoteServer {
pub fn new(origin: String, client_id: Uuid, encryption_secret: Vec<u8>) -> Result<SyncServer> {
Ok(SyncServer {
origin,
client_id,
cryptor: Cryptor::new(client_id, &Secret(encryption_secret.to_vec()))?,
@ -67,7 +62,7 @@ fn get_snapshot_urgency(resp: &ureq::Response) -> SnapshotUrgency {
}
}
impl Server for RemoteServer {
impl Server for SyncServer {
fn add_version(
&mut self,
parent_version_id: VersionId,

View file

@ -1,7 +1,7 @@
use crate::errors::Result;
use uuid::Uuid;
/// Versions are referred to with sha2 hashes.
/// Versions are referred to with UUIDs.
pub type VersionId = Uuid;
/// The distinguished value for "no version"
@ -52,6 +52,11 @@ pub enum GetVersionResult {
/// A value implementing this trait can act as a server against which a replica can sync.
pub trait Server {
/// Add a new version.
///
/// This must ensure that the new version is the only version with the given
/// `parent_version_id`, and that all versions form a single parent-child chain. Inductively,
/// this means that if there are any versions on the server, then `parent_version_id` must be
/// the only version that does not already have a child.
fn add_version(
&mut self,
parent_version_id: VersionId,